All incidents

AggregationRouter USDC Drain

Share
Sep 13, 2025 07:30 UTCAttackLoss: 17,999.88 USDCPending manual check1 exploit txWindow: Atomic
Estimated Impact
17,999.88 USDC
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Sep 13, 2025 07:30 UTC → Sep 13, 2025 07:30 UTC

Exploit Transactions

TX 1Sei
0x6150ec6b2b1b46d1bcba0cab9c3a77b5bca218fd1cdaad1ddc7a916e4ce792ec
Sep 13, 2025 07:30 UTC

Victim Addresses

0x14bb98581Ac1F1a43fD148db7d7D793308Dc4d80Sei
0x9A9F47F38276f7F7618Aa50Ba94B49693293Ab50Sei

Loss Breakdown

17,999.88USDC

Similar Incidents

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.

  1. Before block 167791783, the attacker observes a holder with a live router approval and sufficient USDC balance.
  2. The attacker crafts swap input with amount = 0, srcToken = syUSD, dstToken = syUSD, executor = USDC, and executeParams = abi.encodeWithSelector(IERC20.transferFrom.selector, victim, attacker, 17999880000).
  3. The attacker sends the transaction directly from EOA 0xD43d0660601E613F9097d5C75cd04ee0C19E6f65.
  4. Inside swap, the zero-amount safeTransferFrom(msg.sender, executor, 0) does nothing, after which the router performs the unrestricted external call to USDC.
  5. USDC processes the call with the router as spender, consumes the victim's pre-existing allowance, and transfers 17999880000 units to the attacker.
  6. The router decodes the ERC20 boolean return value as uint256(1) and emits its normal Swapped event, 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" with decimal = 6, equivalent to 17,999.88 USDC.

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.json and /workspace/session/artifacts/collector/seed/1329/0x6150ec6b2b1b46d1bcba0cab9c3a77b5bca218fd1cdaad1ddc7a916e4ce792ec/trace.cast.log
  • Auditor pre/post-state summary: /workspace/session/artifacts/auditor/iter_0/onchain_observations.json