Skip to content

fee-abstraction: Lazy-mode expiration check added in #546 validates the wrong value, not the actual allowance #840

Description

@Eras256

Description

In Lazy approval mode, collect_fee() (packages/fee-abstraction/src/storage.rs:188-201) only re-approves when the existing allowance is insufficient:

FeeAbstractionApproval::Lazy => {
    let allowance = token_client.allowance(user, &e.current_contract_address());
    if allowance < max_fee_amount {
        token_client.approve(user, &e.current_contract_address(), &max_fee_amount, &expiration_ledger);
    } else {
        // assuming that in the other cases the expiration ledger is validated in `token.approve()`
        validate_expiration_ledger(e, expiration_ledger);
    }
}

When the existing allowance is already sufficient, approve() is never called, so expiration_ledger is never applied to anything on-chain: the real, operative allowance keeps whatever expiration a previous approve() call set. validate_expiration_ledger(e, expiration_ledger) only checks that the parameter passed to this call is in the future, which has no relationship to the real allowance's actual expiration.

This line was added deliberately in #546, described as "Added explicit expiration ledger validation for the case we don't rely on token.approve()". Checking the wrong variable means it doesn't accomplish that in either direction:

  1. False rejection. A fee collection against a genuinely still-valid allowance can panic if the caller passes a smaller expiration_ledger than the current ledger sequence, even though nothing is actually expired. Demonstrated by the crate's own test, collect_fee_with_lazy_approval_expired_ledger_panics (packages/fee-abstraction/src/test.rs:198-229): the real allowance is approved for 100 units until ledger 200, current ledger is set to 101, max_fee_amount is 50 (well within the real allowance), and the call still panics with #5006 InvalidExpirationLedger purely because the test passes expiration_ledger: 100 < 101.
  2. No real tightening. Conversely, a caller trying to shorten an existing long-lived allowance by passing an earlier expiration_ledger doesn't shrink anything: the prior, longer expiration silently remains what's actually enforced.

The check doesn't add protection the SAC's own transfer_from expiry enforcement wasn't already providing, while adding a real false-rejection surface.

Suggested direction

Either drop the check in the else branch (the SAC's own expiry enforcement at transfer_from already covers the real allowance), or, if the intent is for Lazy mode to let a caller tighten an existing approval's expiration without bumping the amount, that needs an actual approve() call when the expiration should change, not a validation of an unused parameter.

Verified against

main at fbfde38 (current HEAD). The existing test suite passes as written and demonstrates the behavior above; collect_fee_with_lazy_approval_expired_ledger_panics isn't wrong about what the code does, it's confirming the gap.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Review

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions