Repository navigation
[simplex]: approve the gateway for exactly what a fill pays, and clear the allowance after it - #1423
Open
seunlanlege wants to merge 3 commits into
Open
seunlanlege wants to merge 3 commits into
seunlanlege wants to merge 3 commits into
Conversation
Wizdave97
reviewed
Oct 19, 2026
Wizdave97
left a comment
Member
There was a problem hiding this comment.
Verdict: approve with minor comments.
I traced every pull from the solver through IntentsBase._payLeg, ExtrinsicIntents._post, HyperApp.dispatchWithFeeToken and EvmHost.dispatch. The exact approval can't come in short:
- Fee pull. On a cross-chain fill the gateway pulls
request.fee, which isfillOptions.relayerFeein the destination fee token's decimals. There's no per-byte fee, and the host pulls from the gateway, not from the solver. - Per-leg pull.
paid = max(credited, ceil(offered·released/quoted)), which is never more thanoutputs[i].amount. That holds by hand and in a 230k-case randomised check. debitOrder. Itsusedcalculation is unchanged, and it ends withforceApprove(_intentGateway, 0), so skipping the budget token's reset is correct.- USDT. The existing
[0n, required]loop resets the allowance before setting it, so USDT-style tokens are fine. - Gas. 45k per token covers a cold
approve(0), the 0→x set and the warm reset. - Tests. They drive the real
prepareBidUserOpbatch and fail on the old code.
-
Allowances left by fills before this PR aren't cleared (ContractInteractionService.ts:1219).
- A solver that ran the old code may still have about 1.09e16 units of destination USDC approved to the gateway proxy. Only its next fill in that token on that chain clears it, through the leading
approve(0). - If the wallet stops filling that token or destination, the allowance stays indefinitely. That is the exposure this PR sets out to remove.
- Suggestion: at startup, read
allowance(solver, gateway)for the configured fee token, USDC and USDT on each chain, and zero any that aren't zero. Failing that, tell operators in the release notes to revoke these allowances.
- A solver that ran the old code may still have about 1.09e16 units of destination USDC approved to the gateway proxy. Only its next fill in that token on that chain clears it, through the leading
-
A comment and the changelog note misstate what the bundler estimate covers.
- ContractInteractionService.ts:59 and line 24 of the changelog note say it covers "the prepends and a lone
fillOrder". - But
estimateGasFillPostpassesprependCalls: undefined(:369). So the estimate covers a lonefillOrder, andFUNDING_GAS_PER_CALLcovers the prepends.
- ContractInteractionService.ts:59 and line 24 of the changelog note say it covers "the prepends and a lone
Notes:
- #1418's
assertUnsponsoredBidFundedreads the same localcallGasLimit, so it picks up the added per-token gas. The two PRs are compatible. - The only conflicts with #1411 and #1418 are mechanical, in
bid-budget.test.ts.
CI: the failing live job is unrelated. It runs SDK tests only. getRequestBscAmoy.test.ts timed out after self-delivery failed on eth_getProof with "missing trie node": the Chapel RPC had already discarded the state for that block.
seunlanlege
added a commit
that referenced
this pull request
Oct 19, 2026
- Revoke gateway allowances that fills before this change left standing. At boot, after delegation setup, with a signer, and on chains that are not watch-only, read allowance(solver, gateway) for the gateway's fee token plus the configured USDC and USDT, and send approve(gateway, 0) through TokenSender.submitCalls, one call per token. Best-effort and idempotent: each revoke is logged, a failure is a warning that does not block boot, and a revoked allowance reads zero on the next boot. Costs one small transaction per affected token and chain, once. - Correct the comment and the note: the bundler estimate covers a lone fillOrder (estimateGasFillPost passes no prepends); FUNDING_GAS_PER_CALL covers the prepends. - Revise the changelog note to match.
seunlanlege
added a commit
that referenced
this pull request
Oct 19, 2026
Send the boot-time gateway allowance revoke on its own EntryPoint nonce key so it cannot collide with the key-0 senders that run at boot, via an optional nonceKey on TokenSender.submitCalls. Correct the note's description of the sponsored/native path and its scope.
…r the allowance after it
- Revoke gateway allowances that fills before this change left standing. At boot, after delegation setup, with a signer, and on chains that are not watch-only, read allowance(solver, gateway) for the gateway's fee token plus the configured USDC and USDT, and send approve(gateway, 0) through TokenSender.submitCalls, one call per token. Best-effort and idempotent: each revoke is logged, a failure is a warning that does not block boot, and a revoked allowance reads zero on the next boot. Costs one small transaction per affected token and chain, once. - Correct the comment and the note: the bundler estimate covers a lone fillOrder (estimateGasFillPost passes no prepends); FUNDING_GAS_PER_CALL covers the prepends. - Revise the changelog note to match.
Send the boot-time gateway allowance revoke on its own EntryPoint nonce key so it cannot collide with the key-0 senders that run at boot, via an optional nonceKey on TokenSender.submitCalls. Correct the note's description of the sponsored/native path and its scope.
seunlanlege
force-pushed
the
fix/simplex-fee-token-approval
branch
from
October 19, 2026 15:08
aa22560 to
e3935d6
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A cross-chain fill's batch approved the IntentGateway for far more than it pulls, and left the allowance standing afterwards. On mainnet, one fill from BSC into a 6-decimal USDC destination approved 10,901,000,000,036,480 USDC units (about 10.9 billion USDC); the gateway pulled 36,480 units, the dispatch fee. No funds moved, but every cross-chain fill left a huge standing allowance to an upgradeable proxy.
Why:
prepareBidUserOppassedtotalCostInSourceFeeToken + relayerFeeas the fee-token approval.GasEstimator.ts:434-436). BSC's USDC has 18 decimals, so 0.010901 USDC of gas became 10,901,000,000,000,000 and was added to a 6-decimal destination amount.Fix (
src/services/ContractInteractionService.ts):fillOptions.relayerFeein the fee token on a cross-chain fill.fillOptionsthe fill encodes, so the approval, the fill anddebitOrder'sfeecan't disagree.fillOrderanddebitOrder, every approved token getsapprove(gateway, 0), so nothing stands once the fill completes. The budget token is skipped, becausedebitOrderalready clears it.callGasLimitFornow addsAPPROVAL_GAS_PER_TOKEN(45k) per approved token. It is counted by the same helper that builds the approvals.I traced this through
IntentsBase._payLeg,ExtrinsicIntents,HyperAppandEvmHost: the gateway never pulls more than outputs plusrelayerFee, so an exact approval can't make a fill revert.debitOrder'sused = approved − allowance − feeis unchanged, because the gas term cancelled out in it anyway.Validation
bid-approvals,bid-budgetandbid-call-gas: 35 tests. They include:fillOrder/debitOrder, and the budget token's is skipped;testcommands, locally: simplexlintexit 0;test:data146,test:unit261 andtest:filler59 passed.tscshows only the 14 errors already on main.Not covered: no mainnet run. The audit had this as c30, refuted as "no money at stake". That holds only while the gateway proxy is fully trusted.