All incidents

Anyswap Fake Wrapper Drain

Share
Nov 23, 2022 03:19 UTCAttackLoss: 557,754.45 NUMPending manual check1 exploit txWindow: Atomic
Estimated Impact
557,754.45 NUM
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Nov 23, 2022 03:19 UTC → Nov 23, 2022 03:19 UTC

Exploit Transactions

TX 1Ethereum
0x8a8145ab28b5d2a2e61d74c02c12350731f479b3175893de2014124f998bff32
Nov 23, 2022 03:19 UTCExplorer

Victim Addresses

0x765277eebeca2e31912c9946eae1021199b39c61Ethereum
0x78ac2624a2cd193e8defe9f39a9528e8bd4a368cEthereum

Loss Breakdown

557,754.45NUM

Similar Incidents

Root Cause Analysis

Anyswap Fake Wrapper Drain

1. Incident Overview TL;DR

In Ethereum mainnet transaction 0x8a8145ab28b5d2a2e61d74c02c12350731f479b3175893de2014124f998bff32 at block 16095506, an attacker deployed a fake Anyswap-compatible wrapper, passed that wrapper into AnyswapV4Router.anySwapOutUnderlyingWithPermit, drained 557754450001980916242788 raw NUM from victim 0x78ac2624a2cd193e8defe9f39a9528e8bd4a368c, then liquidated the stolen NUM into USDC, WETH, and ETH. The sender EOA finished with a net gain of 1830232887322425102 wei after gas.

The root cause is that AnyswapV4Router trusted an arbitrary caller-supplied wrapper address as if it were a legitimate bridge wrapper. The router read underlying() from the attacker contract, called permit on NUM even though NUM had no real permit implementation, then used the victim’s pre-existing allowance to execute transferFrom(victim, fakeWrapper, amount). Because the attacker controlled depositVault and burn, the router never enforced real vaulting or burning semantics before the attacker liquidated the stolen NUM.

2. Key Background

AnyswapV4Router exposes anySwapOutUnderlyingWithPermit(address from, address token, address to, uint amount, ...). Its intended flow is to discover the bridge token’s underlying asset, collect fresh authorization via permit, pull the underlying from the user into the wrapper token contract, then call wrapper hooks that represent vaulting and burning before the cross-chain leg is emitted.

The critical code path is:

function anySwapOutUnderlyingWithPermit(
    address from,
    address token,
    address to,
    uint amount,
    uint deadline,
    uint8 v,
    bytes32 r,
    bytes32 s,
    uint toChainID
) external {
    address _underlying = AnyswapV1ERC20(token).underlying();
    IERC20(_underlying).permit(from, address(this), amount, deadline, v, r, s);
    TransferHelper.safeTransferFrom(_underlying, from, token, amount);
    AnyswapV1ERC20(token).depositVault(amount, from);
    _anySwapOut(from, token, to, amount, toChainID);
}

NUM at 0x3496b523e5c00a4b4150d6721320cddb234c3079 is a proxy to implementation 0xd39015041518064743d955cf550c611b0b68888c. The collected implementation source contains a payable fallback and no permit implementation:

function initialize(string _name, string _symbol, uint8 _decimals, address _owner) external initializer {
    ERC20Mintable.initialize(_owner);
    ERC20Detailed.initialize(_name, _symbol, _decimals);
}

function () payable {}

That means a permit call to NUM does not establish fresh authorization. The call succeeds through fallback, and any subsequent transferFrom still depends on already-existing allowance state.

The attacker deployed wrapper 0xa68cce4d90302b728ad60a0fd7bac7737cd3545f, whose recovered behavior shows it was deliberately shaped to satisfy the router interface while doing no real escrow or burn:

function burn(address arg0, uint256 arg1) public pure returns (bool) {
    return 0x01;
}

function depositVault(uint256 arg0, address arg1) public pure returns (uint256) {
    return arg0;
}

3. Vulnerability Analysis & Root Cause Summary

This is an ATTACK-class ACT exploit caused by trust in an attacker-supplied wrapper contract. The router treated token as an authentic bridge wrapper solely because it implemented the expected interface. No allowlist or canonical wrapper registry constrained token, so the attacker could point the router at a contract they fully controlled.

The second failure was treating a raw external permit call as evidence of fresh user consent. NUM did not implement permit semantics, but the router performed no postcondition check on allowance or nonce state before proceeding. As a result, the router advanced into safeTransferFrom using the victim’s standing approval to the router.

The explicit invariant is: the router must only move a user’s underlying token when the supplied wrapper is a legitimate bridge token for that underlying and when the authorization step proves fresh consent for the router to spend that underlying. The code-level breakpoint is anySwapOutUnderlyingWithPermit, where _underlying, permit, safeTransferFrom, depositVault, and _anySwapOut are executed in sequence without authenticating the wrapper or validating the authorization side effect.

4. Detailed Root Cause Analysis

The exploit pre-state was Ethereum mainnet immediately before block 16095506. At that point victim 0x78ac2624a2cd193e8defe9f39a9528e8bd4a368c held 557754450001980916242788 raw NUM and had already approved AnyswapV4Router at 0x765277eebeca2e31912c9946eae1021199b39c61. The reproduced Forge trace and the collector trace both show the allowance was effectively unlimited before the exploit.

The attacker then executed a single adversary-crafted transaction through the public singleton deployer. That transaction created the fake wrapper and routed execution into anySwapOutUnderlyingWithPermit. The collector trace shows the exact permit selector call into NUM and then the victim drain:

CALL router -> NUM input 0xd505accf...   // permit selector
CALL router -> NUM input 0x23b872dd...   // transferFrom(victim, fakeWrapper, drainAmount)

The validator’s independent Forge reproduction confirms the same sequence:

AnyswapV4Router::anySwapOutUnderlyingWithPermit(...)
  FakeWrapper::underlying() -> NUM
  NUM::fallback(... permit args ...) -> [Stop]
  NUM::fallback(victim, FakeWrapper, 557754450001980916242788) -> transferFrom(...)
  FakeWrapper::depositVault(...) -> return amount
  FakeWrapper::burn(...) -> return true

The call to NUM’s permit did not revert, but no permit logic existed. The router therefore proceeded to TransferHelper.safeTransferFrom, which consumed the victim’s existing allowance and transferred the victim’s NUM directly into the fake wrapper. Receipt log 0xddf252ad... records the exact transfer from victim 0x78ac... to fake wrapper 0xa68c... for 0x761bede7ef1343641964, which is 557754450001980916242788 in decimal.

At that point the attacker-controlled depositVault and burn hooks returned success without enforcing bridge custody, escrow, or destruction. The exploit therefore bypassed the normal bridge-token trust assumptions while still satisfying the router’s control flow.

The wrapper then liquidated the stolen NUM. Receipt logs show NUM moving into the NUM/USDC Uniswap V2 pair, 13822280101 raw USDC leaving the pair, USDC entering Uniswap V3 router 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45, and 11925388884037488410 raw WETH leaving the USDC/WETH pool toward the wrapper. The wrapper then withdrew WETH to ETH.

5. Adversary Flow Analysis

The adversary flow had three stages.

First, the attacker EOA 0xb792faf099991f96c5dfef037ae9f248186d9b30 sent public transaction 0x8a8145ab28b5d2a2e61d74c02c12350731f479b3175893de2014124f998bff32. That transaction deployed and executed the fake wrapper 0xa68cce4d90302b728ad60a0fd7bac7737cd3545f.

Second, the fake wrapper invoked the vulnerable router path and drained the victim’s NUM via standing allowance. The receipt proves both the asset pull and router event emission:

NUM Transfer:
from 0x78ac2624a2cd193e8defe9f39a9528e8bd4a368c
to   0xa68cce4d90302b728ad60a0fd7bac7737cd3545f
value 557754450001980916242788

Anyswap router LogAnySwapOut:
token 0xa68cce4d90302b728ad60a0fd7bac7737cd3545f
from  0x78ac2624a2cd193e8defe9f39a9528e8bd4a368c
to    0xa68cce4d90302b728ad60a0fd7bac7737cd3545f
amount 557754450001980916242788
toChainID 12

Third, the wrapper liquidated the stolen NUM across public DEX liquidity and realized ETH-denominated profit. Receipt logs show 13822280101 raw USDC out of the NUM/USDC pair and 11925388884037488410 raw WETH out of the USDC/WETH pool. The collector balance diff then shows:

{
  "address": "0xb792faf099991f96c5dfef037ae9f248186d9b30",
  "before_wei": "1249326286138689763",
  "after_wei": "3079559173461114865",
  "delta_wei": "1830232887322425102"
}

The same balance diff records 10091424440903924264 wei paid to coinbase from the liquidation path. The retained sender-side net gain after gas was still positive, so the ACT success predicate is satisfied.

6. Impact & Losses

The directly observed victim loss was 557754450001980916242788 raw NUM from victim holder 0x78ac2624a2cd193e8defe9f39a9528e8bd4a368c. With NUM using 18 decimals, that corresponds to 557754.450001980916242788 NUM.

The protocol impact is broader than this single holder. Any token holder who had already approved AnyswapV4Router and whose token tolerated the router’s permit call without reverting could be drained if an attacker paired the router with an attacker-controlled wrapper contract exposing the expected interface. The attack required no victim-side transaction in the same block, no privileged access, and no attacker reuse of the incident’s original contracts or keys.

7. References

  • Transaction: 0x8a8145ab28b5d2a2e61d74c02c12350731f479b3175893de2014124f998bff32
  • Vulnerable router: 0x765277eebeca2e31912c9946eae1021199b39c61
  • Victim holder: 0x78ac2624a2cd193e8defe9f39a9528e8bd4a368c
  • NUM proxy: 0x3496b523e5c00a4b4150d6721320cddb234c3079
  • NUM implementation: 0xd39015041518064743d955cf550c611b0b68888c
  • Incident fake wrapper: 0xa68cce4d90302b728ad60a0fd7bac7737cd3545f
  • Evidence used: router function snippet, fake-wrapper decompilation, NUM implementation source, seed receipt logs, seed balance diff, and the validator Forge execution log.