All incidents

AnyswapV4Router WETH9 permit misuse drains WETH to ETH

Share
Jan 19, 2022 16:30 UTCAttackLoss: 312.17 ETH via WETH9Manually checked3 exploit txWindow: 1h 40m
Estimated Impact
312.17 ETH via WETH9
Label
Attack
Exploit Tx
3
Addresses
3
Attack Window
1h 40m
Jan 19, 2022 16:30 UTC → Jan 19, 2022 18:10 UTC

Exploit Transactions

TX 1Ethereum
0xbef94b2a98d0d51ee9c2f65904b45b26f4a7e621a197a325f7fdf06626ed79b4
Jan 19, 2022 16:30 UTCExplorer
TX 2Ethereum
0xe50ed602bd916fc304d53c4fed236698b71691a95774ff0aeeb74b699c6227f7
Jan 19, 2022 17:12 UTCExplorer
TX 3Ethereum
0x35ea606d5097d387b938dac6fd1be173d9b6610290a850f868c38c7dc7419667
Jan 19, 2022 18:10 UTCExplorer

Victim Addresses

0x3e1f13608111de38ec4bd97588d8636718f49516Ethereum
0x3Ee505bA316879d246a8fD2b3d7eE63b51B44FABEthereum
0xd9f3f702db5d4fe3fcdb70e396c1e4f4cde24315Ethereum

Loss Breakdown

312.17ETH via WETH9

Similar Incidents

Root Cause Analysis

AnyswapV4Router WETH9 permit misuse drains WETH to ETH

1. Incident Overview TL;DR

On Ethereum mainnet, an unprivileged adversary abused the anySwapOutUnderlyingWithPermit function of AnyswapV4Router at 0x6b7a87899490ece95443e979ca9485cbe7e71522 together with canonical WETH9 at 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2. By calling a helper contract 0xb5c827fdbbee6f6e9df3a5cb499aedf5927de1b8 entrypoint copyyouattack(address victim, uint256 amount, uint256 bribePercent), the adversary’s EOA 0xfa2731d0bede684993ab1109db7ecf5bf33e8051 triggered router calls that treated a forged permit call on WETH9 as a no-op, then used victims’ pre-existing WETH9 allowances to transfer their balances into the helper and withdraw them to ETH. Across three transactions (txs 0xbef94b2a…, 0xe50ed602…, 0x35ea606d…) in blocks 14037030, 14037237, and 14037497, the attacker drained a total of 312.166644758370382903 WETH9 from three victims and realized approximately 277.787166105615083004 ETH net profit after paying miner bribes and gas.

2. Key Background

Anyswap/Multichain’s V4 router exposes cross-chain bridging functions that accept an Anyswap token and interact with its underlying ERC-20 token. For WETH bridging, the relevant contracts are:

  • AnyswapV4Router at 0x6b7a87899490ece95443e979ca9485cbe7e71522 (verified source available in the artifacts).
  • WETH9 at 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2, the canonical wrapped Ether implementation.
  • A helper Anyswap-style token contract at 0xb5c827fdbbee6f6e9df3a5cb499aedf5927de1b8, which the adversary uses as an AnyswapV1ERC20-compatible token whose underlying() is WETH9.

The router’s anySwapOutUnderlyingWithPermit function is intended to let a user bridge an underlying token using an ERC‑2612-style permit signature so that they do not need to submit a prior approve transaction. Its relevant definition (from the verified router source) 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);
}

This code assumes that _underlying implements permit and that calling it will safely establish an allowance from from to the router. However, WETH9 does not implement permit at all; instead, it only exposes deposit, withdraw, transfer, and transferFrom, and uses a payable fallback that calls deposit() on receipt of ETH:

contract WETH9 {
    mapping (address => uint)                       public  balanceOf;
    mapping (address => mapping (address => uint))  public  allowance;

    function() public payable {
        deposit();
    }
    function deposit() public payable {
        balanceOf[msg.sender] += msg.value;
    }
    function withdraw(uint wad) public {
        require(balanceOf[msg.sender] >= wad);
        balanceOf[msg.sender] -= wad;
        msg.sender.transfer(wad);
    }
    function transferFrom(address src, address dst, uint wad) public returns (bool) {
        require(balanceOf[src] >= wad);
        if (src != msg.sender && allowance[src][msg.sender] != uint(-1)) {
            require(allowance[src][msg.sender] >= wad);
            allowance[src][msg.sender] -= wad;
        }
        balanceOf[src] -= wad;
        balanceOf[dst] += wad;
        return true;
    }
}

A CALL to WETH9 with the router’s permit selector 0xd505accf and zero ETH therefore succeeds via the fallback, does not modify allowances, and does not revert.

The helper contract at 0xb5c8… is decompiled in the artifacts and includes the adversary-facing entrypoint:

/// @custom:selector    0x455a3191
/// @custom:signature   copyyouattack(address victim, uint256 amount, uint256 bribePercent) public
function copyyouattack(address victim, uint256 amount, uint256 bribePercent) public {
    // read victim WETH9 balance
    // ...
    // call router anySwapOutUnderlyingWithPermit
    //   from    = victim
    //   token   = address(this) (0xb5c8...)
    //   to      = address(this)
    //   amount  = amount
    //   deadline, v, r, s = fixed constants
    // router then calls WETH9.permit (no-op) and WETH9.transferFrom(victim, 0xb5c8..., amount)
    // withdraw WETH9 to ETH and pay bribePercent% to block.coinbase
    // send remaining ETH balance to the owner-derived address linked to 0xfa2731...
}

Traces for the three exploit transactions show this exact sequence: 0xfa2731… calls copyyouattack on 0xb5c8…, which calls anySwapOutUnderlyingWithPermit on the router with from set to the victim address and token set to 0xb5c8…, causing WETH9 balances to be pulled from the victim into the helper and immediately withdrawn to ETH.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability arises from AnyswapV4Router’s misuse of anySwapOutUnderlyingWithPermit for tokens that do not implement ERC‑2612 permit. The function accepts an arbitrary from address and unconditionally calls IERC20(_underlying).permit(from, address(this), amount, ...) on the underlying token, then performs safeTransferFrom(_underlying, from, token, amount) without checking that permit succeeded or that msg.sender is equal to from.

For WETH9, which has no permit function and instead routes the call to its payable fallback, the router’s permit call becomes a no-op that always “succeeds”. As long as the victim has previously approved the router as a spender in the standard WETH9 allowance mapping, the subsequent safeTransferFrom will succeed when called by the router even though the transaction was initiated entirely by an unprivileged third-party helper.

The concrete invariant that is broken is:

For any EOA or contract v holding WETH9 on Ethereum, an unprivileged third party that does not control v’s private key or a valid signed permit must not be able to reduce WETH9.balanceOf(v) via AnyswapV4Router by more than v has explicitly authorized through direct transactions or valid permit signatures.

The breakpoint occurs inside anySwapOutUnderlyingWithPermit when the router:

  1. Resolves _underlying = AnyswapV1ERC20(token).underlying() to WETH9 for token 0xb5c8….
  2. Calls IERC20(_underlying).permit(from, address(this), amount, deadline, v, r, s) on WETH9, which silently accepts the call via its fallback and does not change allowances.
  3. Immediately executes TransferHelper.safeTransferFrom(_underlying, from, token, amount) with from set to a victim EOA that has pre-approved the router.

This logic allows any unprivileged actor to drain WETH9 from any address that has granted the router sufficient allowance, simply by setting from = victim in the call and using a helper token whose underlying() is WETH9.

4. Detailed Root Cause Analysis

Pre-state and success conditions

At block 14037030, before tx 0xbef94b2a98d0d51ee9c2f65904b45b26f4a7e621a197a325f7fdf06626ed79b4, the following conditions hold:

  • WETH9 at 0xc02a… holds balances for three victims:
    • 0x3e1f13608111de38ec4bd97588d8636718f49516
    • 0x3Ee505bA316879d246a8fD2b3d7eE63b51B44FAB
    • 0xd9f3f702db5d4fe3fcdb70e396c1e4f4cde24315
  • Each victim has previously executed a standard approve on WETH9 granting AnyswapV4Router (0x6b7a…) a non-zero allowance large enough for the observed transferFrom amounts; this follows from the successful transferFrom calls in the traces.
  • AnyswapV4Router and helper 0xb5c8… are deployed with the code shown above, and helper underlying() resolves to WETH9.

The adversary’s success predicate is purely monetary: the net ETH value of the adversary-related cluster {0xfa2731…, 0xb5c8…} increases after accounting for gas costs and explicit bribes to block.coinbase.

Exploit transaction 1: 0xbef94b2a… (block 14037030)

In the first exploit transaction, 0xfa2731… calls helper 0xb5c8… with selector 0x455a3191 and parameters targeting victim 0x3e1f13608111de38ec4bd97588d8636718f49516 for an amount of exactly 0.08 WETH. The callTracer trace includes the following key calls:

{
  "from": "0x6b7a87899490ece95443e979ca9485cbe7e71522",
  "to": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
  "type": "CALL",
  "input": "0xd505accf...",   // permit-style selector
  "value": "0x0"
},
{
  "from": "0x6b7a87899490ece95443e979ca9485cbe7e71522",
  "to": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
  "type": "CALL",
  "input": "0x23b872dd...3e1f1360... -> 0xb5c8...", // WETH9.transferFrom(victim, helper, 0.08 WETH)
  "value": "0x0"
},
{
  "from": "0xb5c8...",
  "to": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
  "type": "CALL",
  "input": "0x2e1a7d4d...",   // WETH9.withdraw(0.08 WETH)
  "value": "0x0"
}

After the withdraw, helper 0xb5c8… forwards 0.0088 ETH to block.coinbase and 0.0712 ETH to 0xfa2731…, realizing a small profit and confirming that the route from victim WETH9 to adversary ETH operates as described.

Exploit transaction 2 (seed): 0xe50ed602… (block 14037237)

The seed transaction is the main exploit:

  • Sender: 0xfa2731d0bede684993ab1109db7ecf5bf33e8051.
  • To: helper 0xb5c8….
  • Calldata: copyyouattack(victim = 0x3Ee505bA316879d246a8fD2b3d7eE63b51B44FAB, amount = 308636644758370382903, bribePercent = 11).

The decompiled copyyouattack function and the callTracer trace show:

  1. Helper calls WETH9.balanceOf(0x3Ee5…) to read the victim’s WETH balance.
  2. Helper constructs a call to AnyswapV4Router anySwapOutUnderlyingWithPermit with:
    • from = 0x3Ee5… (victim EOA, not equal to msg.sender of the outer transaction).
    • token = 0xb5c8….
    • to = 0xb5c8….
    • amount = 308636644758370382903.
    • deadline, v, r, s as fixed constants (not derived from any victim signature).
  3. Inside the router, the trace records:
    • STATICCALL to 0xb5c8… selector 0x6f307dc3 to fetch underlying(), returning WETH9 at 0xc02a….
    • CALL to WETH9 with selector 0xd505accf and zero value (the supposed permit), which does not revert.
    • CALL to WETH9 with selector 0x23b872dd transferring exactly 308636644758370382903 WETH9 from 0x3Ee5… to 0xb5c8….
  4. Helper then calls WETH9.withdraw(308636644758370382903) and receives the same amount in ETH.
  5. Finally, helper transfers:
    • 33.950030923420742119 ETH to block.coinbase 0xea674fdde714fd979de3edf0f56aa9716b898ec8 as a bribe.
    • 274.686613834949640784 ETH to 0xfa2731….

The prestate tracer diff for this transaction shows WETH9’s storage for 0x3Ee5… decreasing by exactly 308636644758370382903 wei and the WETH9 contract balance decreasing by the same amount when withdraw is called. It also shows the ETH balances:

  • 0xfa2731…: +274.673279555369329864 ETH net (after gas).
  • 0xea674f…: +33.950030923420742119 ETH.
  • WETH9: –308.636644758370382903 ETH (as underlying). No WETH9 allowance mapping entries for 0x3Ee5… are modified in this transaction, confirming that the exploit uses pre-existing approvals and does not rely on a new permit.

Exploit transaction 3: 0x35ea606d… (block 14037497)

The third transaction repeats the same pattern for victim 0xd9f3f702db5d4fe3fcdb70e396c1e4f4cde24315 with an amount of 3.45 WETH9. The trace again shows the router calling anySwapOutUnderlyingWithPermit, making a non-reverting permit-style CALL into WETH9, then calling transferFrom and withdraw, after which helper 0xb5c8… pays a bribe to block.coinbase and forwards the remaining ETH to 0xfa2731….

Victim remediation

After the seed exploit, victim 0x3Ee5… clears its WETH9 allowance to the router. The txlist artifact for this address contains:

{
  "hash": "0x7010eec66a20167403e84b1c8ab50803f92bf4fce450ce2d81248805b11eb025",
  "from": "0x3ee505ba316879d246a8fd2b3d7ee63b51b44fab",
  "to": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
  "input": "0x095ea7b3...6b7a87899490ece95443e979ca9485cbe7e71522...0000000000000000000000000000000000000000000000000000000000000000",
  "functionName": "approve(address _spender, uint256 _value)",
  "blockNumber": "14037295"
}

This is WETH9.approve(0x6b7a8789…, 0), which revokes the router’s allowance and confirms that the victim is an EOA responding to the observed theft.

5. Adversary Flow Analysis

The adversary-related cluster consists minimally of:

  • EOA 0xfa2731d0bede684993ab1109db7ecf5bf33e8051 (transaction sender and ETH profit recipient).
  • Helper contract 0xb5c827fdbbee6f6e9df3a5cb499aedf5927de1b8 (attacker-controlled orchestrator that interacts with the router and WETH9).

The end-to-end flow is:

  1. Preparation (off-chain / prior activity): Victims interact with Anyswap/Multichain, acquire WETH9, and execute standard WETH9.approve(0x6b7a…, allowance) transactions so that the router can move WETH9 on their behalf. These approvals exist before block 14037030 and are not created by the adversary.
  2. Exploit transactions: For each victim address and amount, 0xfa2731… submits a 0 ETH EIP‑1559 transaction calling copyyouattack(victim, amount, bribePercent) on helper 0xb5c8…:
    • The helper reads the victim’s WETH9 balance and constructs a call into AnyswapV4Router anySwapOutUnderlyingWithPermit with from = victim, token = 0xb5c8…, to = 0xb5c8…, amount = victimAmount, and a fixed bogus permit tuple.
    • The router resolves the underlying as WETH9 and calls IERC20(WETH9).permit(...). WETH9 treats this as a fallback deposit with zero value, leaving allowances unchanged and not reverting.
    • The router unconditionally calls WETH9.transferFrom(victim, 0xb5c8…, amount), which succeeds because the victim previously approved the router.
    • The router calls the helper’s stub depositVault, and its internal accounting _anySwapOut burns helper tokens, but this accounting does not affect the theft.
    • Helper 0xb5c8… immediately calls WETH9.withdraw(amount), receiving ETH equal to the drained WETH9.
    • Helper pays bribePercent of the ETH (e.g., 11%) to block.coinbase to incentivize inclusion, and forwards the remainder to 0xfa2731….
  3. Profit realization: For the three observed transactions, the net ETH gains are:
    • Tx 0xbef94b2a…: ~0.0712 ETH after a small bribe.
    • Tx 0xe50ed602…: 274.673279555369329864 ETH net to 0xfa2731… (per prestate state diff and gas usage).
    • Tx 0x35ea606d…: profit consistent with 3.45 WETH9 minus bribe and gas, forwarded to 0xfa2731….

Because the only requirements are (a) existing WETH9 allowances from victims to the router and (b) the ability to deploy or call a helper token whose underlying() is WETH9, the opportunity is an Anyone‑Can‑Take (ACT) strategy. Any unprivileged EOA observing that a given address has granted WETH9 allowance to 0x6b7a… can reproduce this flow with their own helper contract and EOA.

6. Impact & Losses

Across the three exploit transactions, the victims lose a total of 312.166644758370382903 WETH9, withdrawn to ETH via WETH9’s withdraw function. The per-transaction breakdown is:

  • Tx 0xbef94b2a98d0d51ee9c2f65904b45b26f4a7e621a197a325f7fdf06626ed79b4: 0.08 WETH9 drained from 0x3e1f13608111de38ec4bd97588d8636718f49516.
  • Tx 0xe50ed602bd916fc304d53c4fed236698b71691a95774ff0aeeb74b699c6227f7: 308.636644758370382903 WETH9 drained from 0x3Ee505bA316879d246a8fD2b3d7eE63b51B44FAB.
  • Tx 0x35ea606d5097d387b938dac6fd1be173d9b6610290a850f868c38c7dc7419667: 3.45 WETH9 drained from 0xd9f3f702db5d4fe3fcdb70e396c1e4f4cde24315.

The ETH flows derived from traces and balance diffs are:

  • Total WETH9 withdrawn to ETH: 312.166644758370382903 ETH.
  • Miner bribes (to 0xea674fdde714fd979de3edf0f56aa9716b898ec8): 34.338330923420742119 ETH across the three transactions.
  • Gas costs for 0xfa2731…: approximately 0.041147729334558 ETH in total.
  • Net ETH profit to the adversary EOA 0xfa2731…: 277.787166105615083004 ETH.

No WETH9 allowances are modified during the exploit transactions, and AnyswapV4Router’s internal accounting is unaffected; the only persistent impact is the irreversible loss of WETH9/ETH from the three victim addresses whose allowances were abused.

7. References

Key artifacts that substantiate this analysis are:

  • Seed transaction metadata for 0xe50ed602bd916fc304d53c4fed236698b71691a95774ff0aeeb74b699c6227f7 (includes gas usage and sender/recipient addresses).
  • callTracer traces for the three exploit transactions 0xbef94b2a…, 0xe50ed602…, 0x35ea606d…, showing the full call stack through helper 0xb5c8…, AnyswapV4Router anySwapOutUnderlyingWithPermit, WETH9 transferFrom, withdraw, and ETH forwarding.
  • state_diff_prestateTracer for 0xe50ed602…, confirming the exact WETH9 and ETH balance changes for victims, WETH9, and the adversary EOA.
  • Verified AnyswapV4Router source at 0x6b7a87899490ece95443e979ca9485cbe7e71522, especially the implementation of anySwapOutUnderlyingWithPermit.
  • Canonical WETH9 source at 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2, confirming the absence of permit and the behavior of its fallback, deposit, withdraw, and transferFrom.
  • Decompiled helper contract source for 0xb5c827fdbbee6f6e9df3a5cb499aedf5927de1b8, including copyyouattack, withdrawethamount, and ETH forwarding behavior.
  • Txlists for the adversary EOA 0xfa2731d0bede684993ab1109db7ecf5bf33e8051 and victim address 0x3Ee505bA316879d246a8fD2b3d7eE63b51B44FAB, demonstrating the three exploit transactions and the post-incident allowance revocation.