Vulnerable Swap Router – Payer Parameter Abuse
A custom, unverified swap router contract at 0xc87c815c03b6cd45880cbd51a90d0a56ecfba9da on Ethereum mainnet contains a critical access control flaw that allows any caller to execute token swaps using another user’s token approvals. On February 13, 2026 at 17:06:47 UTC (block 24,449,245), an attacker exploited this vulnerability to drain 13,906.72 USDT from a victim who had previously approved the router for token spending.
Financial Impact
The attacker stole approximately 13,906.72 USDT from victim address 0x222e674fb1a7910ccf228f8aecf760508426b482. After repaying flash loans and covering gas costs, the net profit was:
- 13,801.64 USDT transferred to attacker EOA
0x4fd9669fb676ea2ace620afb6178ae300ecfd8a9 - 0.0305 ETH (~$85 at current prices) also transferred to the attacker EOA
The victim’s entire USDT balance of 13,906.72 USDT was drained in a single transaction.
Attack Vector
Access Control Bypass via Payer Parameter Injection: The vulnerable router exposes a function (selector 0x8943ec02) that accepts an arbitrary payer address as its first parameter. This address is stored in contract storage slot 6 and subsequently used by the uniswapV3SwapCallback to execute transferFrom calls, pulling tokens from the payer – not from msg.sender. There is no validation that msg.sender is authorized to spend on behalf of the specified payer.
Vulnerable Contract
- Address:
0xc87c815c03b6cd45880cbd51a90d0a56ecfba9da - Name: Unverified custom swap router (not a standard Uniswap deployment)
- Source: Not verified on Etherscan; analysis based on TAC decompilation and call trace
The contract is not a proxy. It exposes functions resembling the Uniswap V3 SwapRouter interface (exactInputSingle at 0x1a9d82d5, uniswapV3SwapCallback at 0xfa461e33) but with non-standard signatures and a critical additional function at selector 0x8943ec02.
Vulnerable Function
Selector: 0x8943ec02
Signature: tokenReceived(address,uint256,bytes) (per 4byte directory; functionally acts as execute(address payer, uint256 nonce, bytes commands))
This function accepts three parameters:
address payer– the address whose tokens will be spentuint256 nonce– a nonce value (used for internal accounting)bytes commands– encoded sub-calls to execute via DELEGATECALL to self
Vulnerable Code Snippet
The following is reconstructed from the TAC decompilation at 0xc87c815c03b6cd45880cbd51a90d0a56ecfba9da/contract.tac. The vulnerable function (entry at block 0x2b6, core logic at block 0xc16) proceeds as follows:
// Block 0xc16: Check persistent enabled-flag (storage slot 7, byte at offset 0xa0)
// Note: This flag is set at the end of each successful call and persists in storage between
// transactions. It functions as a "contract enabled" gate, not a per-invocation reentrancy guard.
slot7 = SLOAD(7)
locked = (slot7 >> 160) & 0xff
require(locked != 0, "LOK") // revert with "LOK" if flag not set
// Block 0xc53: Clear lock, record payer, set up DELEGATECALL
SSTORE(7, slot7 & 0xffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff) // clear lock
caller = CALLER
// Block 0x1b90: Update internal nonce mapping[payer][caller] += nonce
// Then emit event fd531bfb... (log4 with caller, payer, nonce)
// Block 0xc86: Store payer address in slot 6
slot6_value = (SLOAD(6) & 0xffffffffffffffffffffffff0000000000000000000000000000000000000000) | payer
SSTORE(6, slot6_value)
// Store msg.sender in slot 7 (low 160 bits)
slot7_value = CALLER | (SLOAD(7) & 0xffffffffffffffffffffffff0000000000000000000000000000000000000000)
SSTORE(7, slot7_value)
// Block 0xcde: DELEGATECALL to self with the `commands` bytes
// This invokes exactInputSingle(0x1a9d82d5) within the same storage context
success = DELEGATECALL(gas, ADDRESS, commands_data, commands_len, 0, 0)
The uniswapV3SwapCallback (selector 0xfa461e33) then reads the payer from storage slot 6 and issues transferFrom:
// In the swap callback path (function 0x1a47, block around 0x1add):
payer = SLOAD(6) & 0xffffffffffffffffffffffffffffffffffffffff // reads victim address
caller_stored = SLOAD(7) & 0xffffffffffffffffffffffffffffffffffffffff
// Later executes: USDT.transferFrom(payer, pool, amountOwed)
// using selector 0x23b872dd
Flaw Description
The root cause is a missing authorization check in the function at selector 0x8943ec02. The function stores an arbitrary caller-supplied payer address into storage slot 6 without verifying that msg.sender has any right to spend tokens on behalf of that payer. When the subsequent DELEGATECALL invokes exactInputSingle, which triggers a swap on the Uniswap V3 pool, the pool calls back into uniswapV3SwapCallback. The callback reads the payer address from storage slot 6 and calls transferFrom(payer, pool, amount) on the input token contract, effectively pulling the victim’s tokens.
In a correctly designed router (such as the canonical Uniswap V3 SwapRouter), the payer is always msg.sender – hardcoded, not parameterized. The standard Uniswap callback uses a payer field that is either msg.sender or address(this), ensuring only the transaction initiator’s tokens can be spent. This custom router breaks that invariant by accepting the payer as an untrusted input parameter.
Any address that has ever approved this router for token spending is vulnerable. The victim (0x222e674fb1a7910ccf228f8aecf760508426b482) had an active USDT allowance of approximately 9,007,199,234.74 USDT to the router – effectively unlimited – which enabled the full balance drain.
Call Flow
The attacker EOA 0x4fd9669fb676ea2ace620afb6178ae300ecfd8a9 calls the attacker contract 0xc8540a70aa191651d7cf8ed854ea3d346c897b2a with function 0x657fcedd (three address parameters: router, pool, victim).
Step 1 – Setup approvals: The attacker contract approves Morpho Blue (0xbbbbbbbbbb9cc5e90e3b3af64bdaf62c37eeffcb) for unlimited USDT and WETH spending.
Step 2 – Flash loan USDT: Calls MorphoBlue.flashLoan (0xe0232b42) to borrow 200 USDT. Morpho transfers 200 USDT to the attacker contract and calls onMorphoFlashLoan (0x31f57072).
Step 3 – Flash loan WETH (nested): Inside the first callback, calls MorphoBlue.flashLoan again to borrow 1 WETH. Morpho transfers 1 WETH and calls onMorphoFlashLoan again.
Step 4 – Reconnaissance: The attacker contract calls USDT.balanceOf(victim) returning 13,906.72 USDT, and USDT.allowance(victim, router) returning ~9B USDT. This confirms the victim is exploitable.
Step 5 – Mint liquidity: Calls UniswapV3Pool.mint (0x3c8a7d8d) on the USDT/WETH pool (0xddbb864c2541e27152dbb87037ece852afb1faf5) with full-range tick positions (-887,220 to 887,220) and liquidity amount 437,896,813,350. The attacker provides 0.01123 WETH and 17.09 USDT as liquidity. This positions the attacker to capture value from the forced swap.
Step 6 – Exploit the router: Calls VulnerableRouter.0x8943ec02(victim, 0, exactInputSingleCalldata) where the embedded calldata encodes: exactInputSingle(USDT, WETH, fee=3000, recipient=attackerContract, deadline, amountIn=13906.718432 USDT, amountOutMinimum=0, sqrtPriceLimitX96=0). The router stores the victim address in slot 6, then DELEGATECALLs to itself to execute exactInputSingle (0x1a9d82d5).
Step 7 – Swap execution: The exactInputSingle function calls UniswapV3Pool.swap (0x21a76118). The pool executes the swap and calls uniswapV3SwapCallback (0xfa461e33) on the router. The callback reads the payer (victim) from storage slot 6 and calls USDT.transferFrom(victim, pool, 13906.718432 USDT). The pool sends 0.04169 WETH to the attacker contract as swap output.
Step 8 – Burn and collect: The attacker calls UniswapV3Pool.burn (0xa34123a7) and UniswapV3Pool.collect (0x2f7f176c) to remove liquidity and collect the USDT that flowed into the pool from the victim’s forced swap. Collects 13,818.73 USDT and 0.0000139 WETH.
Step 9 – Repay flash loans: Returns 1 WETH and 200 USDT to Morpho Blue via transferFrom.
Step 10 – Extract profits: Transfers 13,801.64 USDT to the attacker EOA. Unwraps remaining WETH (0.0305 ETH) via WETH.withdraw (0x2e1a7d4d) and sends native ETH to the attacker EOA.
Impact Assessment
Who lost funds: The victim at 0x222e674fb1a7910ccf228f8aecf760508426b482 lost their entire USDT balance of 13,906.72 USDT.
Scope of vulnerability: Any address that has approved the router contract 0xc87c815c03b6cd45880cbd51a90d0a56ecfba9da for any ERC-20 token is at risk. The attacker can drain the full approved amount (up to the victim’s balance) for any token, not just USDT.
Protocol impact: The router contract is unverified and not a known Uniswap deployment. It appears to be a third-party or custom router that some users interacted with and granted token approvals to. The Uniswap V3 pool itself is not vulnerable – it functions correctly as designed.
Note: The pool contract at
0xddbb864c2541e27152dbb87037ece852afb1faf5uses DELEGATECALL to an implementation contract (0x55d36836209e91952e33a058c7eaf1ce3ece0ee0), which is consistent with the UniswapV3 pool clone pattern. The EIP-1967 implementation slot is empty; this is not a standard transparent proxy.
Attacker sophistication: The attack uses a purpose-built contract (with function literally named exploit at selector 0x657fcedd) that orchestrates flash loans, liquidity provision, forced swaps, and profit extraction in a single atomic transaction. The flash loans ensure the attacker needs no upfront capital.
Evidence
- Storage slot 6 abuse: TAC decompilation at block 0xc86 (line 9578-9583) shows the first parameter of
0x8943ec02is stored directly into slot 6 without msg.sender validation. - Callback reads slot 6: TAC at block 0x1add (line 873) shows
SLOAD(6)retrieves the payer for use intransferFrom. - transferFrom from victim: Call trace confirms
USDT.transferFrom(0x222e674f..., 0xddbb864c..., 13906718432)executed by the router at depth 8, pulling the victim’s tokens directly to the Uniswap pool. - Event log at index 0xba: The router emits event
0xfd531bfb...with topics including the attacker contract (0xc8540a...) and the victim (0x222e674f...), recording the payer parameter abuse. - Full balance drain:
USDT.balanceOf(victim)returned 13,906.72 USDT before the attack, and exactly that amount was transferred via the forced swap.
Related URLs: