AggregationRouter USDC Drain
Exploit Transactions
0x6150ec6b2b1b46d1bcba0cab9c3a77b5bca218fd1cdaad1ddc7a916e4ce792ecVictim Addresses
0x14bb98581Ac1F1a43fD148db7d7D793308Dc4d80Sei0x9A9F47F38276f7F7618Aa50Ba94B49693293Ab50SeiLoss Breakdown
Similar Incidents
RubicProxy USDC Drain
38%USDC drain via unchecked Uniswap V3-style callback
27%FiberRouter Allowance Reuse Drain
27%V3Utils Arbitrary Call Drain
27%Base USDC drain from malicious transferFrom spender approvals
27%0x7CAE Approved-Spender Drain
27%Root Cause Analysis
AggregationRouter USDC Drain
1. Incident Overview TL;DR
At Sei block 167791783, EOA 0xD43d0660601E613F9097d5C75cd04ee0C19E6f65 called AggregationRouter.swap on 0x14bb98581Ac1F1a43fD148db7d7D793308Dc4d80 and used the router as a privileged spender against a third-party USDC holder. The attacker supplied executor = 0xe15fC38F6D8c56aF07bbCBe3BAf5708A2Bf42392 and raw calldata for transferFrom(0x9A9F47F38276f7F7618Aa50Ba94B49693293Ab50, attacker, 17999880000), which caused 17,999.88 USDC to move from the approved holder to the attacker in one transaction.
The root cause is a code-level arbitrary external call in AggregationRouter.swap. The router lets any caller choose both the target address and calldata, then executes that call as the router itself. Because the victim holder had already approved the router for USDC, the router's spender identity could be repurposed to drain the victim's balance.
2. Key Background
ERC20 transferFrom checks the immediate caller as the spender. If a holder has approved a router contract, that router can move the holder's tokens whenever the router itself executes transferFrom.
Swap routers therefore need a hard boundary around privileged external execution. A safe design constrains the executor set and the calldata semantics so router-held approvals are only used for the current user's swap path. AggregationRouter did not impose that boundary.
The relevant public components were already live before the incident: AggregationRouter at 0x14bb98581Ac1F1a43fD148db7d7D793308Dc4d80, syUSD at 0x059A6b0bA116c63191182a0956cF697d0d2213eC, and USDC at 0xe15fC38F6D8c56aF07bbCBe3BAf5708A2Bf42392. The observed victim holder 0x9A9F47F38276f7F7618Aa50Ba94B49693293Ab50 had 18,167.88 USDC and an effectively unlimited USDC allowance to the router in the public pre-state at block 167791782.
3. Vulnerability Analysis & Root Cause Summary
The vulnerability class is an unrestricted privileged external call. AggregationRouter exposes swap as a permissionless entrypoint and accepts caller-controlled srcToken, dstToken, amount, executor, and executeParams. For ERC20 inputs it transfers amount from msg.sender to params.executor, but when amount == 0 that step adds no safety boundary. The critical operation is the router's direct low-level call to the caller-supplied executor. Because the call is executed by the router contract, any token approvals previously granted to the router become usable in whatever calldata the caller provides. In the incident, the caller pointed executor at the USDC proxy and encoded a direct transferFrom from a third-party approved holder to the attacker. That is the invariant break: router spender privileges were not constrained to assets owned by the swap initiator or to a validated swap path.
4. Detailed Root Cause Analysis
The verified router source shows the defect directly:
function swap(SwapParams calldata params) external payable returns (uint256 returnAmount) {
IERC20 srcToken = params.srcToken;
if (srcToken.isETH() && msg.value != params.amount) revert InvalidMsgValue();
if (!srcToken.isETH()) srcToken.safeTransferFrom(msg.sender, params.executor, params.amount);
(bool success, bytes memory returnData) = params.executor.call{value: msg.value}(params.executeParams);
if (!success) revert ExecuteFailed();
returnAmount = abi.decode(returnData, (uint256));
}
This function validates only the ETH msg.value case. It does not verify that params.executor is trusted, that params.executeParams represent a swap, or that the router's spender privileges are being used only against msg.sender.
The public pre-state captured by the auditor establishes the exploit conditions exactly: the victim holder had USDC balance 18167880000, attacker USDC balance 0, and router allowance 115792089237316195423570985008687907853269984665640564039457584007793045905631. Those conditions are sufficient because USDC enforces spender authorization on the caller, and the router was already the approved spender.
The incident transaction metadata shows the attacker-supplied calldata embedded in the router call:
AggregationRouter.swap(
srcToken = 0x059A6b0bA116c63191182a0956cF697d0d2213eC,
dstToken = 0x059A6b0bA116c63191182a0956cF697d0d2213eC,
amount = 0,
executor = 0xe15fC38F6D8c56aF07bbCBe3BAf5708A2Bf42392,
executeParams = 0x23b872dd...0430e05f40
)
0x23b872dd is the selector for transferFrom(address,address,uint256). Decoding the arguments yields from = 0x9A9F47F38276f7F7618Aa50Ba94B49693293Ab50, to = 0xD43d0660601E613F9097d5C75cd04ee0C19E6f65, and amount = 17999880000.
The trace confirms the exact spender-context transition:
AggregationRouter::swap(... executor: 0xe15fC38F6D8c56aF07bbCBe3BAf5708A2Bf42392, executeParams: 0x23b872dd...)
FiatTokenV2_2::transferFrom(
0x9A9F47F38276f7F7618Aa50Ba94B49693293Ab50,
0xD43d0660601E613F9097d5C75cd04ee0C19E6f65,
17999880000
) [delegatecall]
The post-state is equally deterministic: the victim holder fell from 18167880000 to 168000000 USDC, and the attacker rose from 0 to 17999880000 USDC. The exploit required no privileged role, no private key compromise, and no private-orderflow assumption. Any unprivileged actor observing the same allowance-bearing pre-state could have submitted the same router call.
5. Adversary Flow Analysis
The adversary flow is a single public transaction.
- Before block
167791783, the attacker observes a holder with a live router approval and sufficient USDC balance. - The attacker crafts
swapinput withamount = 0,srcToken = syUSD,dstToken = syUSD,executor = USDC, andexecuteParams = abi.encodeWithSelector(IERC20.transferFrom.selector, victim, attacker, 17999880000). - The attacker sends the transaction directly from EOA
0xD43d0660601E613F9097d5C75cd04ee0C19E6f65. - Inside
swap, the zero-amountsafeTransferFrom(msg.sender, executor, 0)does nothing, after which the router performs the unrestricted external call to USDC. - USDC processes the call with the router as spender, consumes the victim's pre-existing allowance, and transfers
17999880000units to the attacker. - The router decodes the ERC20 boolean return value as
uint256(1)and emits its normalSwappedevent, so the entire exploit path completes without revert.
6. Impact & Losses
The measured loss in the observed incident is one ERC20 asset:
- USDC: raw amount
"17999880000"withdecimal = 6, equivalent to17,999.88USDC.
The affected parties are the approved holder 0x9A9F47F38276f7F7618Aa50Ba94B49693293Ab50, whose USDC balance was drained, and the router users as a broader class because any holder who granted AggregationRouter token allowance was exposed to the same permissionless spend path.
7. References
- Incident transaction:
0x6150ec6b2b1b46d1bcba0cab9c3a77b5bca218fd1cdaad1ddc7a916e4ce792ec - AggregationRouter:
0x14bb98581Ac1F1a43fD148db7d7D793308Dc4d80 - USDC proxy:
0xe15fC38F6D8c56aF07bbCBe3BAf5708A2Bf42392 - syUSD:
0x059A6b0bA116c63191182a0956cF697d0d2213eC - Victim holder:
0x9A9F47F38276f7F7618Aa50Ba94B49693293Ab50 - Verified router source:
https://repo.sourcify.dev/contracts/full_match/1329/0x14bb98581ac1f1a43fd148db7d7d793308dc4d80/sources/src/AggregationRouter.sol - Incident metadata and trace:
/workspace/session/artifacts/collector/seed/1329/0x6150ec6b2b1b46d1bcba0cab9c3a77b5bca218fd1cdaad1ddc7a916e4ce792ec/metadata.jsonand/workspace/session/artifacts/collector/seed/1329/0x6150ec6b2b1b46d1bcba0cab9c3a77b5bca218fd1cdaad1ddc7a916e4ce792ec/trace.cast.log - Auditor pre/post-state summary:
/workspace/session/artifacts/auditor/iter_0/onchain_observations.json