Banked Cycle 209 from OKX DEX-Router-EVM-V1 (Cantina $1M, saturated LEARN lane).
Use as a PRE-READ checklist on any swap router / aggregator / intent-settlement contract.
Each primitive: MECHANISM -> what GOOD looks like -> CANARY grep/check.
"Can the funds-pull from/payer argument ever be set to anyone other than msg.sender (or the order signer)?"
Trace EVERY transferFrom / claimTokens / permitTransferFrom / asm-claim sink back to an entrypoint. If from is user-controllable independent of caller identity -> drain. Everything else is secondary.
.call user-supplied adapter+pool addresses with user-supplied calldata. Looks like a drain; usually is not.from is hard-bound to msg.sender; a malicious adapter/pool can only move the CALLER's own in-flight funds; end-to-end measured min-return caps loss to the caller's own slippage.grep -n "claimTokens(\|safeTransferFrom(\|permitTransferFrom(" -> for each, trace arg from/who to msg.sender. Also: is payer ever set to anything but msg.sender (e.g. arbitrary calldata, an order field)? That inversion is the bug..call ITSELF as the vuln. It is intended; the defense lives at the funds-pull, not the call.TokenApprove <- TokenApproveProxy.allowedApprove whitelist <- router(s). Adapters STATELESS, balance-scoped, exact-amount approve(pool, sellAmount), no standing balance, not in whitelist.from. (b) grep -rn "approve(.*max\|approve(.*type(uint256)" adapter/ for leftover infinite approvals to a user-supplied pool on an adapter that ever holds a standing balance.returnAmount = balanceOf(toToken, receiver)_after - _before; require(returnAmount >= minReturn), snapshot brackets fee/commission logic; toToken/receiver are fixed and re-required to match the pool route.require(... >= minReturn). Verify it reads balanceOf (not a returned uint), brackets the fee deduction, and the destination token cannot be swapped to a worthless token by the same calldata.uniswapV3SwapCallback / flash / hook callbacks are externally callable; if they pull payer's funds while trusting msg.sender to be a real pool, any contract calls with payer=victim -> drain.msg.sender == transientlyStoredPool, OR CREATE2 re-derivation keccak(ff ++ factory ++ keccak(token0,token1,fee) ++ initCodeHash) == caller() then revert BadPool. payer read from router-appended calldata, not from the external caller.*Callback / *Call / hook / onFlashLoan that moves funds, check the FIRST lines authenticate msg.sender. SEAM: if it derives pool from ONE hardcoded factory+initCodeHash, multi-fork V3 support is a gap - a fork pool with a different init-code-hash either reverts (DoS) or, if a separate unguarded variant exists, drains. Hunt routers with several *V3Callback variants vs one set of CREATE2 params.msg.value == amount enforced on wrap; safety is MEASURED delta so FoT auto-handled (user gets post-fee delta vs minReturn); adapters compute on balanceOf(this) not passed-in amount.grep adapters/router for arithmetic using the passed-in amount/sellAmount as the RECEIVED quantity instead of balanceOf(this) - FoT/rebasing breaks those. Check native sweep forwards full balance and no ETH is stranded.from to the signer.from is msg.sender/signer, not arbitrary calldata. Reserved Permit2 modes are no-ops until wired.grep "permit(\|_MODE_PERMIT2\|permitTransferFrom"; verify a reverting permit does not revert the whole swap, and the transferFrom from is the signer. NOTE: if Permit2 gets wired (its from+recipient are signature-bound, a DIFFERENT trust model), re-audit P1.from is the ORDER SIGNER not msg.sender (1inch AggregationRouterV6, 0x Settler, CoW/UniswapX-class) -> signature-replay + recipient-substitution become the seam.// fix DRW-02) tell you which classes prior auditors swept; diff those lines vs HEAD on the next fresh fork.