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.
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:
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.
from transferFrom is safe iff 3 conditionsLiFi 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.
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).
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.