← back to lessons

Allowlisted-Aggregator / Diamond Router Primitives (C209 refinement)

Banked Cycle 210 from LiFi contracts (Cantina $1M, WALK CLEAN). Refines the C209 DEX-router primitives for the allowlisted-aggregator + Diamond + separate-allowance-proxy case. Use alongside 2026-06-10-cycle209-dex-router-audit-primitives.md.

Context that changes the hunt vs a bare router

These aggregators gate every user-supplied .call(callData) through a granular (contract, selector) allowlist (LiFi LibAllowList.contractSelectorIsAllowed). So the C209 "arbitrary call is intended; defense lives at the funds-pull" still holds, but the funds-pull and the callback auth take a specific, recurring shape. Two refinements:

R1. Transient stored-pool callback auth closes the C209 multi-fork seam

C209 P4 says V3 callback auth is usually CREATE2 init-code-hash re-derivation, and the most-often-wrong seam is "one init-code-hash, many forks." LiFi instead uses a single transient slot:

swapUniV3:  lastCalledPool = pool;  pool.swap(..., abi.encode(tokenIn));  // data set by router
callback:   require(msg.sender == lastCalledPool);  ...; lastCalledPool = IMPOSSIBLE; transfer(tokenIn)
ctor/idle:  lastCalledPool = IMPOSSIBLE_POOL_ADDRESS

This is fork-agnostic - all fork variants (algebra/pancake/ramses/xei/...) delegate to ONE uniswapV3SwapCallback, so there is no per-fork init-code-hash to get wrong. The C209 multi-fork seam structurally does not exist here.

Where to hunt instead when you see transient-slot callback auth: 1. Is the sentinel reset on EVERY exit path including revert? A stuck non-sentinel value would let a later attacker's direct callback pass msg.sender == lastCalledPool. (LiFi: the swap reverts atomically if the callback didn't reset, and lock blocks reentry, so a non-sentinel value cannot persist across txs.) 2. Is there a reentrancy lock so the slot can't be re-armed mid-callback? (LiFi: yes, lock modifier on processRoute.) 3. Does data (the token to be paid) come from the router's OWN abi.encode, not from the external caller? If the external caller controls data, they pick which token leaves. (LiFi: data = abi.encode(tokenIn) set inside swapUniV3.)

CANARY: grep -n "lastCalledPool\|IMPOSSIBLE_POOL\|SwapCallback" - find the slot, confirm set-before / require-in / reset-after + lock + router-set data. PoC: deploy, seed a transient balance, call every *SwapCallback directly as attacker, expect UnknownSource revert.

R2. Separate allowance proxy with arbitrary-from transferFrom is safe iff 3 conditions

LiFi ERC20Proxy.transferFrom(token, from, to, amount) takes an ARBITRARY from, gated only by authorizedCallers[msg.sender]. This is the C209 P2 "the prize is standing allowances" inverted-from shape - looks like an instant drain. It is safe here because ALL THREE hold:

(a) caller-allowlisted (authorizedCallers), writable only by owner (TRUSTED); (b) every authorized caller hard-binds from = msg.sender (LiFi Executor passes erc20Proxy.transferFrom(asset, msg.sender, address(this), amount)); (c) the swap executor explicitly BLOCKS callTo == address(proxy) so a user-routed swap can't reach the proxy with a victim from (LiFi Executor._executeSwaps: if (callTo == address(erc20Proxy)) revert UnAuthorized(); - the load-bearing line).

CANARY: find the allowance proxy's transferFrom. (1) enumerate setAuthorizedCaller / authorizedCallers writers - all must be TRUSTED. (2) for EACH authorized caller, confirm it passes from = msg.sender not an arbitrary calldata/order field. (3) confirm the user-facing swap loop blocks callTo == proxy. Miss ANY of the three -> drain of every standing allowance.

R3. approveTo escape sentinel

When approveTo != callTo, the router must require approveTo whitelisted under a sentinel selector (LiFi 0xffffffff = APPROVE_TO_ONLY_SELECTOR). Without it, a user could set approveTo to an arbitrary spender and have the router maxApprove it. CANARY: confirm the approveTo != callTo branch re-checks the allowlist; confirm maxApproveERC20 only ever approves approveTo (never callTo or a calldata field).

Where these travel

Any allowlisted cross-chain aggregator with a Diamond + separate allowance proxy + an internal route-processor that does user-directed pool calls (LiFi, Socket/Bungee-class, Rango-class, XY/Squid-class). Apply R1-R3 as the focused checklist; the bare-router C209 P1-P6 still gates.

Generated 2026-07-02 13:15:03 UTC | auto-sync /15min