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:
- 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.
- 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.
Description
In
Lazyapproval mode,collect_fee()(packages/fee-abstraction/src/storage.rs:188-201) only re-approves when the existing allowance is insufficient:When the existing allowance is already sufficient,
approve()is never called, soexpiration_ledgeris never applied to anything on-chain: the real, operative allowance keeps whatever expiration a previousapprove()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:expiration_ledgerthan 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_amountis 50 (well within the real allowance), and the call still panics with#5006 InvalidExpirationLedgerpurely because the test passesexpiration_ledger: 100 < 101.expiration_ledgerdoesn't shrink anything: the prior, longer expiration silently remains what's actually enforced.The check doesn't add protection the SAC's own
transfer_fromexpiry enforcement wasn't already providing, while adding a real false-rejection surface.Suggested direction
Either drop the check in the
elsebranch (the SAC's own expiry enforcement attransfer_fromalready covers the real allowance), or, if the intent is forLazymode to let a caller tighten an existing approval's expiration without bumping the amount, that needs an actualapprove()call when the expiration should change, not a validation of an unused parameter.Verified against
mainatfbfde38(current HEAD). The existing test suite passes as written and demonstrates the behavior above;collect_fee_with_lazy_approval_expired_ledger_panicsisn't wrong about what the code does, it's confirming the gap.