ParaSwap Augustus V6 uniswapV3SwapCallback() Fake-Pool Approval Drain (2024-03-20)

  • Name: ParaSwap Augustus V6 uniswapV3SwapCallback() fake-pool approval drain
  • Date: 2024-03-20
  • Protocol: ParaSwap / Augustus V6
  • Chain: Ethereum + Arbitrum + Polygon (with broader risk across Uniswap v3-integrated deployments before mitigation)
  • Category: callback-origin authentication failure / fake-pool settlement spoof / token-approval drain
  • Loss: about $24,000 in the first confirmed exploit wave, followed by roughly $1.1M more drained from still-approved wallets before revocations; ParaSwap and white hats later reported about $3.4M rescued
  • Summary: ParaSwap’s newly released Augustus V6 included a gas-optimized direct Uniswap v3 path whose uniswapV3SwapCallback() logic authenticated the callback too weakly. In the vulnerable path, Augustus derived the poolAddress, token ordering, fee tier, and payer from callback calldata instead of from one precommitted in-flight route object. Attackers could therefore create a real Uniswap v3 pool pairing a worthless token against a valuable victim-approved token, point the callback’s payer field at a wallet that had already approved Augustus V6, and make the router settle a fake swap with the victim’s real tokens. The protocol was not drained as a custodian; it was abused as an already-approved spender inside user wallets.
  • Trigger / entry point:
    • ParaSwap released Augustus V6 on 2024-03-18 as a more gas-efficient swap router.
    • The vulnerable surface was the direct Uniswap v3 callback path in Augustus V6, commonly described in public analysis as uniswapV3SwapCallback() inside the UniswapV3Utils helper logic.
    • Public writeups tie the issue to gas-optimized direct-pool functions such as swapExactAmountInOnUniswapV3() / _callUniswapV3PoolsSwapExactAmountOut() rather than the standard Uniswap router.
    • In the affected callback branch, Augustus reconstructed the expected Uniswap v3 pool from token / fee bytes embedded inside callback calldata and separately loaded the payer (fromAddress) from calldata as well. That meant the router was proving caller matches the pool address derivable from these same attacker-shaped bytes, not caller matches the exact pool and payer Augustus itself committed before control left.
  • Root cause:
    • Augustus V6 combined broad spender authority from prior user approvals with a settlement callback that trusted callback context too much.
    • The critical invariant should have been only the real expected Uniswap v3 pool for the in-flight hop may invoke this callback, and only for the exact payer / asset context Augustus initiated.
    • Instead, the callback admitted a circular proof: msg.sender had to match a canonical Uniswap v3 pool address, but that pool address was itself recomputed from callback bytes that also supplied the victim payer and token path. The protocol never pinned one route-local authority object binding canonical pool, canonical token semantics, and authorized payer before callback settlement became reachable.
    • This meant the spender authority users had granted to Augustus V6 became a theft primitive even without any generic arbitrary-call router bug.
  • Exploit path:
    1. The attacker found wallets that had already approved Augustus V6 to spend a real token such as USDC.
    2. They deployed a worthless token and created an attacker-controlled Uniswap v3 pool pairing that fake asset with the victim-approved real token.
    3. They initialized the pool and minted liquidity so the pool could appear to sell the worthless token while demanding real-token payment on callback.
    4. The attacker invoked the Augustus V6 callback path with crafted deltas and callback data, making Augustus believe it was settling a legitimate Uniswap v3 swap for the specified payer.
    5. Augustus used its existing allowance to pull the victim’s real token into the attacker-controlled pool.
    6. Because the attacker controlled the fake asset side and the pool liquidity, they could then burn liquidity or reverse the position to extract the real tokens that Augustus had just paid in.
    7. The attacker repeated the pattern across approved wallets until ParaSwap shut down V6 routing, white hats began rescue sweeps, and users revoked approvals.
  • What the attacker needed to be true:
    • Victim wallets needed live ERC-20 approvals to Augustus V6.
    • The callback logic needed to accept attacker-shaped pool / payer context before conclusively authenticating the caller as the canonical Uniswap v3 pool for that hop.
    • The attacker needed to be able to create or control the pool state so the callback demanded a real token while paying out a worthless one.
    • The callback path needed to remain reachable long enough for MEV bots and follow-on exploiters to enumerate approved wallets.
  • Technical anatomy worth remembering:
    • This was approval abuse through a settlement callback, not through a classic router forwards arbitrary calldata bug.
    • The attacker did not need ParaSwap to call transferFrom(victim, attacker, amount) directly. It was enough that Augustus would honor a fake callback debt and transfer victim-approved tokens into an attacker-controlled pool.
    • In the short-data callback path discussed in public analyses, Augustus’ assembly loaded the pool caller, copied token0 / token1 / fee fields out of callback calldata, recomputed a computedAddress for the supposed canonical Uniswap v3 pool, and then transferred whichever token had a positive delta from fromAddress to poolAddress. The check was therefore not a strong route-authenticity proof; it was a caller check parameterized by attacker-controlled callback bytes.
    • The preserved lesson is authenticate the callback origin and bind the payer context to an actual in-flight swap, not merely be careful with approvals.
    • In approval-funded-route terms, Augustus never kept one route-local authority object that simultaneously fixed the canonical pool, the victim payer, and the settlement meaning of that hop before callback settlement could spend wallet allowance.
    • The fake-pool setup matters because it let the attacker supply the non-valuable side of the trade themselves and cash out the valuable side after Augustus paid.
    • This incident is a close cousin to arbitrary-call approval-drain exploits like Socket and LI.FI, but the dangerous surface here was narrower and easier to miss: the AMM settlement callback itself.
  • Representative transactions and addresses:
    • Augustus V6: 0x00000000FdAC7708D0D360BDDc1bc7d097F47439
    • Uniswap v3 factory: 0x1F98431c8aD98523631AE4a59f267346ea31F984
    • Example exploit transaction highlighted by ParaSwap / public PoCs: 0x35a73969f582872c25c96c48d8bb31c23eab8a49c19282c67509b96186734e60
    • Example single-victim mainnet drain transaction analyzed later: 0x5b1e67ed4c12dcb1b94a32d4e77cd6a6b113d76c1fdb29defd24d0058e4f70de
    • White-hat return / rescue address reported by ParaSwap: 0x66E90d840D7C4F3473E25dD8ca361747058c6Db0
    • White-hat helper contract referenced by public exploit reproductions: 0x6980a47bee930a4584b09ee79ebe46484fbdbdd0
    • Example victim from later public walkthrough: 0x05808Cf9F8aAcFD6a2c2A879326593644F9a339e
    • Example attacker-created fake-token/USDC pool from that walkthrough: 0x18bea6618ce8daf9491f3102d36504aeaab23bf8
  • Response / mitigation:
    • ParaSwap disabled Augustus V6 in the API and reverted the main UI flow back to V5.
    • A cross-team white-hat response involving SEAL 911, Origin Protocol, Conduit, Ironblocks, Tenderly, and others swept exposed wallets where possible, rescuing about $3.4M.
    • ParaSwap called the contract’s self-destruct function on Avalanche, BNB Chain, and Polygon where that mitigation path still worked, but noted that it could not do so on Ethereum Mainnet and Optimism after Dencun-related behavior changes.
    • ParaSwap publicly urged users plus integrators to revoke allowances immediately, published the white-hat Safe address, and later returned rescued funds in batches once approvals were revoked.
    • Follow-on exploiters still drained about $1.1M from wallets that received new funds before revoking, after which ParaSwap negotiated returns and used DAO treasury funds to make affected users whole.

Pattern tags

  • gas-optimized direct-pool path kept live spender authority but skipped one stronger route-authentication layer the canonical router would normally concentrate
  • callback caller check was circular because the same callback bytes supplied canonical-looking pool identity, token ordering, fee tier, and payer context
  • real Uniswap v3 pool authenticity was weaker than proving this exact pool was the only route-local counterparty entitled to charge this exact approved wallet
  • wallet approvals turned a fake AMM settlement debt into an immediate user-balance drain without needing a generic arbitrary-call surface

Why the exploit worked

  • Augustus V6 treated uniswapV3SwapCallback() as if caller is a real pool derivable from callback bytes were close enough to caller is the exact pool Augustus committed for this in-flight hop. It was not. The callback path never kept one route-local truth object binding canonical pool, token semantics, fee tier, and payer before control left.
  • That made the caller check circular. The attacker could create a real Uniswap v3 pool for a worthless token and a valuable approved token, then feed callback data that made Augustus recompute that attacker-chosen pool as canonical while also naming the victim wallet as payer.
  • Because Augustus already held live ERC-20 approvals, the exploit did not need broad arbitrary-call power. Once the fake callback debt was accepted, ordinary allowance-funded settlement logic became the theft rail.
  • The healthy-side failure is therefore sharper than approvals are dangerous: ParaSwap let callback-origin truth and wallet-spend authority drift apart, so a plausible pool identity was enough to spend a real user’s balance on a debt Augustus itself had never authoritatively authored.

Three callback-and-approval promotions this incident collapsed

  1. A real pool-looking counterparty plus callback-shaped debt object could reach Augustus’ settlement path at all.
    • The attacker could create a genuine Uniswap v3 pool pairing a worthless token against a valuable approved token, then call into the vulnerable direct-pool path with callback data that looked like ordinary swap-settlement context.
    • That only established some pool-shaped settlement story exists, not this is the one hop Augustus itself committed before leaving trusted control flow.
  2. That callback-shaped context was over-promoted into the canonical pool-and-payer truth for the hop.
    • Augustus recomputed canonical-looking pool identity from callback bytes that also carried the fee tier, token ordering, and payer context, so the same attacker-shaped data helped both define and authenticate the settlement object.
    • The real break was therefore not merely a fake pool can exist; it was attacker-authored callback context became the authoritative proof of which pool and which wallet this hop was allowed to charge.
  3. That accepted callback story still inherited live wallet-spend authority through standing approvals.
    • Once Augustus accepted the callback as legitimate, its normal allowance-funded settlement logic could pull the victim’s real tokens into the attacker-controlled pool.
    • The final authority jump was from this looks like a settleable Uniswap v3 debt to Augustus may now consume real allowance from this exact wallet for this exact hop, which is why a fake-pool trade became a direct approval drain rather than a harmless bad quote.

Detection and audit pivots

  • Trace every callback path that can reach transferFrom() or other allowance-funded settlement and ask what exact object proves the payer, pool, fee tier, token ordering, and route stage were all committed before the external call.
  • Flag any design where callback bytes both choose the pool and later prove the pool. If canonical-looking address recomputation depends on attacker-shaped callback context, the authenticity story is probably circular.
  • Compare gas-optimized direct integrations against the upstream router or reference integration they are replacing. If the optimization removed an intermediate layer that used to bind route meaning or payer identity, assume the security burden moved somewhere and verify where.
  • Test fake-pool scenarios, not only fake-caller scenarios. A check that rejects arbitrary EOAs may still fail if the attacker can deploy a genuine-looking AMM pool whose economics they control.
  • Treat dormant user approvals as part of the callback threat model. Reviewers should ask not only can this callback move protocol funds? but whose wallet balances become reachable if this callback is admitted once?

Why this incident matters

  • It preserves a distinct wallet-drain branch that is easy to flatten into just another approvals bug even though the real failure lived in callback authentication.
  • It shows how a direct-integration gas optimization can inherit all the blast radius of a router approval surface while bypassing the safer validation layers developers mentally associate with the canonical upstream router.
  • It also broadens the exploit corpus beyond generic arbitrary call approval drains into the more specific fake AMM settlement callback variant.

Failure-prevention checklist

  • Recompute the canonical Uniswap v3 counterparty from trusted route state before honoring callback settlement. The callback should prove the exact factory, token ordering, fee tier, and hop Augustus itself committed, not an attacker-shaped pool reconstructed from callback bytes.
  • Keep one route-local authority object that binds pool identity, payer identity, token semantics, and settlement stage before any callback path can reach allowance-funded transfer logic. The healthy question is not only is this a real pool? but is this the only pool entitled to charge this exact wallet for this exact hop right now?
  • Reject callback formats where the same untrusted bytes both supply the pool ingredients and later help authenticate the pool. Callback data picked it and callback data proved it is a circular trust story.
  • Treat gas-optimized direct integrations as fresh security surfaces, not as inherited safety from the canonical upstream router. If the direct path removes a router layer that used to carry route or payer binding, replace that binding explicitly before launch.
  • Bound spender authority where possible. Permit2, expiring approvals, route-scoped spending, or per-swap approval refreshes reduce the blast radius when a callback bug appears inside an otherwise legitimate router.
  • Fuzz counterfeit-pool scenarios directly: real Uniswap v3 factory deployment, worthless-token / real-token pool creation, payer rebinding, fee-tier substitution, and callback payloads that try to spend approvals from wallets unrelated to the initiating swap.
  • Preserve a concrete kill path for already-approved spender surfaces. If the contract is non-pausable on some chains, the rollout plan should already say whether the API, UI, route family, or settlement helper can be disabled quickly enough to stop new exposure while users revoke.

Audit prompts

  • What exact locally committed fact proves this callback came from the canonical pool for this hop rather than from a real pool whose identity was reconstructed from attacker-shaped callback bytes?
  • Can callback calldata rebind the payer wallet, fee tier, token ordering, or pool address after Augustus has already left its trusted route-construction path?
  • Does the callback authenticate a real Uniswap v3 pool exists for these bytes or the exact pool Augustus previously committed is now entitled to settlement?
  • If transferFrom() or equivalent allowance-funded payment becomes reachable in the callback, which wallets can it touch: only route-local escrow, or any user that ever approved Augustus V6?
  • What would fail closed if the callback came from the right pool with the wrong payer, the right payer with the wrong token semantics, or a real attacker-created pool for a worthless asset against a valuable approved one?
  • Which review step compared the direct Uniswap v3 path against the canonical router path it was replacing, and which route-authentication invariant was supposed to move or stay the same?
  • If the spender contract cannot be paused everywhere, what is the operational containment plan besides tell users to revoke: API rollback, UI downgrade, self-destruct on supported chains, whitehat rescue, or bounded-approval policy?
  • Are dormant approvals being modeled as live custody exposure during callback review, or only as a UX convenience issue after deployment?

Lessons that should already be standard

  • A callback caller is not authenticated just because it is a real canonical-looking pool address. If attacker-shaped bytes help define which pool is canonical, the check is still circular.
  • Approval-backed aggregators are custody surfaces whether or not they custody assets first. Dormant wallet allowances turn callback-authentication mistakes into direct wallet drains.
  • Direct-integration gas savings are not free. When teams bypass a heavier upstream router, they also bypass whatever route-binding and payer-binding discipline that router was implicitly providing.
  • Pool truth, payer truth, and settlement truth have to stay fused. The moment one callback can redefine all three, a plausible AMM debt story becomes enough to spend a real user’s balance.
  • Incident response has to be designed before launch for already-approved spender contracts. If a contract is immutable or only partially destructible after a network upgrade, the fallback may be a revocation campaign plus whitehat race rather than a clean pause.
  • Users can always revoke later is not a safety model. Once a public callback bug exists, any wallet that reloads funds before revoking can still be drained.
  • The clean design question is blunt: which exact pool is entitled to ask this router to spend which exact wallet’s tokens right now, and what trusted state proves that without reusing attacker-authored callback context?

Internal linkages

  • Core recurring pattern: callback-origin-authentication-failure.
  • Positive-design settlement companion: settlement-envelope-truth-security-object — Augustus V6 never kept one authenticated settlement object binding the canonical pool, victim payer, asset semantics, and callback stage before allowance-funded settlement could consume user balances.
  • End-to-end route-design companion: route-commitment-across-callback-frame-and-settlement-envelope — Augustus V6 failed to keep one approved hop bound across the expected Uniswap v3 callback frame, canonical pool identity, and later payer / token settlement object.
  • Approval-funded route companion: approval-funded-route-authority-truth-security-object — the missing middle layer was a route-local authorization object that should have kept approved spender identity, canonical pool counterparty, victim payer, and settlement semantics fused before uniswapV3SwapCallback() could exercise user allowance.
  • Adjacent pattern: arbitrary-call-token-approval-drain — both monetize user approvals, but ParaSwap did it through a spoofed settlement callback rather than a generic arbitrary-call router path.
  • Adjacent pattern: reentrancy — both involve callback edges, but this was not re-entrant state corruption; it was accepting a callback from the wrong place.

Control surface