AMM Callback Counterparty Recomputation and Payer Pinning Discipline
- Name: AMM callback counterparty recomputation and payer pinning discipline
- Category: AMM-integration reference / callback-settlement design note / direct-pool routing checklist / approval-funded settlement hardening
- Summary: AMM callback integrations stay safe only when they answer two questions from trusted state before control returns:
which exact pool / pair / market is entitled to callback for this stageandwhich exact payer bucket is allowed to satisfy that debt. Protocols keep failing when those answers are reconstructed from the same untrusted route bytes, callback payload, or remembered address that first selected the counterparty. The narrow design target for direct-pool integrations is therefore stronger thanmsg.sender is a real pooland stronger thanthe router already has allowance. A healthy design recomputes the canonical callback counterparty from trusted route state, pins the payer bucket and token semantics before the external call, and lets the callback consume only that exact stage-local object once. This note exists to preserve that narrower AMM-integration checklist instead of leaving readers to reconstruct it from broader notes like settlement-envelope-truth-security-object or approval-funded-route-authority-truth-security-object. - Why this note exists:
- The corpus already has the broader callback-side and settlement-side design notes in callback-frame-truth-security-object, settlement-envelope-truth-security-object, route-commitment-across-callback-frame-and-settlement-envelope, and approval-funded-route-authority-truth-security-object.
- AMM callback incidents keep preserving one narrower implementation seam inside those broader categories: the integration must recompute the exact pool or pair and preserve the exact payer bucket at callback time.
- Primitive Finance, Sushi RouteProcessor2, and ParaSwap Augustus V6 all lost that seam in slightly different ways. Each bug class becomes easier to prevent when reviewers ask the direct question:
what exact locally committed fact proves this pool may callback, and what exact locally committed fact proves this wallet / inventory bucket may pay it?
The two claims a safe AMM callback path must prove separately
1. Exact callback counterparty truth
- The integration must prove one exact counterparty for one exact stage: factory, token ordering, fee tier, pool type, market object, and route stage should all identify one callback-capable AMM object.
Real poolis weaker thanthe canonical pool for this exact stage.Stored last poolis weaker thanpool recomputed from trusted stage state.Factory-valid pairis weaker thanthe pair this route and token semantics actually required.
2. Exact payer-bucket truth
- The integration must also prove who is allowed to fund the callback debt: one wallet approval, one route-local escrow bucket, one router inventory bucket, one flash-settlement bucket, or one other explicitly named source.
Router has allowanceis weaker thanthis route stage may charge this exact wallet now.Router still has tokensis weaker thanthis stage is allowed to spend this exact inventory bucket now.fromAddress was decodedis weaker thanpayer identity was committed before control left.
3. Why these claims must stay separate but fused
- A design can authenticate the counterparty weakly and still know the payer correctly; that still loses money.
- A design can pin the payer weakly and still know the pool is real; that still loses money.
- Safe AMM callback settlement therefore keeps both answers fused into one stage-local object: exact counterparty, exact payer bucket, exact asset semantics, exact stage meaning, exact amount bounds.
The six fields a healthy AMM callback slot should keep fused
-
Canonical factory or market family
- Which factory, router family, hook registry, or market family defines the legitimate callback-capable counterparties for this path?
- A pool address without a trusted family anchor is not much of a proof.
-
Exact pool / pair / market identity
- Which exact pair or pool is allowed to callback?
- For Uniswap-style systems this usually means token ordering plus fee tier plus factory; for custom markets it may mean a market object or series id too.
-
Stage meaning
- Which route hop or callback stage is currently live?
- The same pool can be safe to trust in one stage and unsafe in another if the debt semantics differ.
-
Payer bucket identity
- Which wallet, escrow bucket, inventory sleeve, or approval-funded source may satisfy the callback debt?
- This should be written before the external call, not discovered from callback bytes afterward.
-
Asset and direction semantics
- Which token is owed, in which direction, under which delta or amount sign convention?
Positive deltais not enough if token ordering or market semantics can still drift.
-
Amount bound and consumption rule
- What maximum debt may this callback collect, and how many times may it collect it?
- A callback slot should behave like a single-use capability, not like an ambient permission to keep charging until the route ends.
What sound design looks like
-
Pool identity is recomputed, not replayed
- The callback handler re-derives the expected pool, pair, or market from trusted stage state written before the external call.
- It does not trust the same attacker-shaped bytes that selected the pool in the first place.
-
Payer identity is pinned before control leaves
- Wallet payer, router inventory bucket, or route-local escrow is committed in local state before the swap or flash step hands control away.
- The callback only consumes that committed payer object.
-
Callback origin and settlement meaning stay fused
- The admitted callback source must still correspond to one exact debt meaning: token ordering, owed asset, route stage, and payer bucket all remain bound.
- A real pool must not be able to redefine who pays or what is owed.
-
Direct-pool paths are treated as fresh custody boundaries
- Gas-optimized direct integrations do not inherit safety from the canonical upstream router by default.
- If the direct path bypasses one router or helper layer, it must explicitly recreate the route-binding and payer-binding guarantees that layer used to provide.
-
Wallet approvals and router inventory stay stage-local
- A callback admitted for one hop should not automatically gain authority over every wallet approval or every token balance the router can touch.
- The callback should consume one smaller payer bucket than the raw reachable spend surface.
-
Fallback callback paths become smaller paths
- If only one emergency direct-pool integration or settlement helper remains live during degraded operation, that path should lose ordinary healthy-mode wallet scope, counterparty freedom, and amount budget.
Only path leftshould imply a smaller callback slot, not continuity at the old spend budget.
Useful positive design shapes
1. Stage-local callback slot with recomputed pool identity
- Before calling the pool, write one slot containing route id, stage id, canonical factory, ordered tokens, fee tier, payer bucket, owed token semantics, and max amount.
- At callback time, recompute the expected pool from that slot and compare
msg.senderagainst the recomputed address. - Clear the slot on success and failure.
2. Distinct authorization for wallet pull versus callback settlement
- Pulling the initial input from a wallet and paying callback debt are different stages.
- Healthy designs treat them as separate permissions with separate counterparties and separate amount ceilings.
- This prevents
router had allowancefrom silently becomingcallback may charge any approved wallet.
3. Route-local payer sleeves
- Keep approval-funded wallet spends, router inventory, flash-liquidity debt, and final proceeds in separate route-local sleeves.
- A callback should only reach the sleeve explicitly authorized for that stage.
- This is the clean answer to
which exact bucket was supposed to pay right now?
4. Typed callback context instead of ad hoc suffix folklore
- If callback data must carry metadata, keep the parser version, field order, and stage meaning explicit.
- Typed context is not a substitute for recomputation, but it reduces the chances that one compatibility path lets callback bytes redefine pool or payer identity.
5. Degraded-mode callback slots with narrower authority
- If incident response leaves one direct-pool or operator-assisted callback path alive, emit a separate degraded-mode slot with smaller counterparty set, smaller wallet scope, and lower per-window amount bounds.
- Do not preserve ordinary route authority just because one path remains operational.
Design questions worth asking first
- What exact fact proves this callback came from the canonical pool for this route stage rather than from a merely real-looking or attacker-selected pool?
- Where was the payer bucket written before control left?
- Can callback bytes or route suffixes change token ordering, fee tier, market meaning, or payer identity after the route is already live?
- If the direct integration bypassed a heavier router path, which route-binding guarantee moved into this code and where is it now enforced?
- When the callback settles, does it spend one route-local sleeve or any balance / approval the router can still reach?
Red flags
msg.sender == lastCalledPoolfactory.getPair(...)or pool recomputation that still depends entirely on attacker-shaped callback bytes- callback data that contains
fromAddress, payer, or token ordering with no prior trusted-state commitment - direct-pool fast paths that bypass common target / selector / route-binding middleware
- settlement that pays from ambient router balances or any wallet with a standing approval
- one callback slot that remains live across multiple route stages or multiple payer buckets
Failure-prevention checklist
- Recompute the canonical AMM counterparty at callback time from trusted route state written before the external call: factory, token ordering, fee tier, pool family, and stage id should already pin one exact counterparty.
- Commit the payer bucket before control leaves: wallet, router inventory sleeve, flash-debt sleeve, or route-local escrow should already be named in trusted local state before the callback can ask to be paid.
- Separate wallet-pull authority from callback-settlement authority. A router that can pull approved input should not automatically let any admitted callback spend against that same approval surface.
- Bind token semantics and direction explicitly. Positive delta, token ordering, and route stage should identify one owed asset meaning rather than leaving the callback to reinterpret signs or ordering.
- Keep callback slots single-use, stage-indexed, and amount-bounded. Clear them on revert, retry, or success so stale callback state cannot be replayed against a later hop.
- Route settlement through route-local payer sleeves rather than ambient balances or broad approvals.
- Treat gas-optimized direct integrations as fresh custody surfaces, and compare them against the safer upstream path they replaced to identify which route-binding or payer-binding guarantees need to be recreated.
- Fail closed on legacy callback formats, compatibility shims, or mixed parser paths that let the same bytes both choose and later authenticate the counterparty.
- Pre-commit degraded-mode callback policy too: if only one surviving callback path remains live, narrow counterparty set, wallet scope, and amount bounds automatically.
- Fuzz fake-pool, wrong-fee-tier, reversed-token-order, payer-rebinding, wrong-stage, and stale-slot replay cases as first-class wallet-drain or inventory-drain scenarios.
Audit prompts
- What exact trusted-state fields are sufficient to recompute the canonical callback counterparty for this stage?
- Which fields are re-derived from trusted state, and which are still merely decoded from callback bytes or route calldata?
- Where is the payer bucket committed before the external call, and can callback data later substitute a different wallet or inventory source?
- If the callback source is genuine, what still proves the owed token, amount direction, and stage meaning belong to the same route slot?
- Can one direct-pool fast path bypass the target / selector / route-binding checks that other routed paths rely on?
- Does the callback settle from a route-local sleeve, or from any wallet approval or router balance still reachable at runtime?
- What clears callback-slot state on revert, retry, partial fill, or stage abandonment?
- If only one emergency or degraded callback path remained live today, what wallet scope, counterparty scope, and amount budget would disappear automatically?
- Which telemetry would reveal drift early: wrong-pool callback attempts, payer substitutions, unexpected fee tiers, settlement from ambient inventory, or repeated slot-consumption failures?
Lessons that should already be standard
Real poolis not the security property.Exact route-stage pool recomputed from trusted stateis.Router has allowanceis not the security property.This exact callback stage may charge this exact payer bucketis.- The same bytes should not both choose and later authenticate the callback counterparty.
- Gas savings from direct integration do not preserve the route-binding work that an upstream router used to do for free.
- Callback admission and payer authority have to stay fused. A pool that is genuine but allowed to choose who pays is still an attacker-controlled spend path.
- Ambient router balances and dormant wallet approvals are not safe fallback funding sources for callback settlement.
- If only one callback path survives in degraded mode, it should become a smaller path with a smaller spend budget.
- The clean reviewer question is blunt:
which exact pool is allowed to callback, which exact bucket is allowed to pay it, and where were both facts committed before control left?
Negative exemplar cluster
- Primitive Finance preserves the mixed counterfeit-market plus callback-counterparty version where the paying wallet approval surface and the market object both drifted.
- Sushi RouteProcessor2 preserves the circular
lastCalledPoolvariant where route bytes chose the pool and later the same stored choice became the callback proof. - ParaSwap Augustus V6 preserves the direct-pool fast-path variant where pool identity and payer identity were both reconstructed from callback context.
How to use this note with the corpus
- Start here when the sharper design question is
how should an AMM callback integration recompute the exact counterparty and pin the exact payer bucket before settlement can move value? - Use callback-frame-truth-security-object when the live problem is broader stale in-flight authority across actor, target, or provider.
- Use settlement-envelope-truth-security-object when the live problem is broader downstream settlement-context authenticity beyond AMM callbacks.
- Use approval-funded-route-authority-truth-security-object when the sharper question is the whole allowance-backed routing object above raw approvals.
- Use route-commitment-across-callback-frame-and-settlement-envelope when the design task is end-to-end route lifecycle binding rather than the narrower AMM callback seam.
- Pair this note with callback-origin authentication failure when the negative-side classification matters alongside the positive design answer.
Internal linkages
- Narrow exploit-side companion: callback-origin-authentication-failure.
- Broader callback-side companion: callback-frame-truth-security-object.
- Broader settlement-side companion: settlement-envelope-truth-security-object.
- Broader approval-side companion: approval-funded-route-authority-truth-security-object.
- End-to-end route companion: route-commitment-across-callback-frame-and-settlement-envelope.
- Best exemplar cluster: Primitive Finance, Sushi RouteProcessor2, and ParaSwap Augustus V6.
Control surface
-
The decisive control surface is the callback-settlement seam in direct-pool or callback-capable AMM integrations.
-
The recurring mistake is not merely
callback exists; it is that the integration authenticates a plausible pool and a plausible payer story instead of one exact route-stage counterparty and one exact route-stage payer bucket. -
AMM callback paths stay healthiest when they recompute the counterparty, pre-commit the payer, keep settlement stage-local, and consume that slot once.
-
Sources:
- callback-origin-authentication-failure
- callback-frame-truth-security-object
- settlement-envelope-truth-security-object
- approval-funded-route-authority-truth-security-object
- route-commitment-across-callback-frame-and-settlement-envelope
- primitive-finance-connectors-fake-option-flash-swap-callback-approval-drain-whitehack-2021-02-22
- sushiswap-routeprocessor2-fake-pool-callback-approval-drain-2023-04-09
- paraswap-augustus-v6-uniswapv3swapcallback-fake-pool-approval-drain-2024-03-20
-
Last reviewed: 2026-07-29 UTC