All incidents

TSURUWrapper onERC1155Received bug mints unbacked tokens and drains WETH

Share
May 10, 2024 13:48 UTCAttackLoss: 137.78 ETH, 167,200,000 TSURUWrapperManually checked2 exploit txWindow: 22s
Estimated Impact
137.78 ETH, 167,200,000 TSURUWrapper
Label
Attack
Exploit Tx
2
Addresses
1
Attack Window
22s
May 10, 2024 13:48 UTC → May 10, 2024 13:49 UTC

Exploit Transactions

TX 1Base
0xe63a8df8759f41937432cd34c590d85af61b3343cf438796c6ed2c8f5b906f62
May 10, 2024 13:48 UTCExplorer
TX 2Base
0xfe091a2d175f488bd366d3a84e9c37a622d789bf4539defacfc5f2a08169e2ca
May 10, 2024 13:49 UTCExplorer

Victim Addresses

0x75ac62ea5d058a7f88f0c3a5f8f73195277c93daBase

Loss Breakdown

137.78ETH
167,200,000TSURUWrapper

Similar Incidents

Root Cause Analysis

TSURUWrapper onERC1155Received bug mints unbacked tokens and drains WETH

1. Incident Overview TL;DR

On Base (chainid 8453), an adversary-controlled EOA 0x7a5e… deployed a helper contract 0x66e9… and an orchestrator contract 0xa2209…. The orchestrator invoked a flawed TSURUWrapper.onERC1155Received hook in tx 0xe63a8d…, causing TSURUWrapper to mint a large amount of unbacked ERC20 tokens to the helper. In a follow-up tx 0xfe09…, the helper swapped those unbacked TSURUWrapper tokens into WETH/ETH via a TSURUWrapper/WETH pool at 0x913b… and forwarded the ETH back to the EOA.

The root cause is an inverted msg.sender guard in TSURUWrapper.onERC1155Received: when msg.sender is not the underlying ERC1155 contract, the function mints amount * ERC1155_RATIO TSURUWrapper tokens to the from address without verifying any ERC1155 transfer. Any unprivileged contract can therefore mint unbacked TSURUWrapper and dump it into the TSURUWrapper/WETH pool, creating an ACT-style profit opportunity that yielded a net gain of 137.782190287675132288 ETH to the adversary after gas and L1 fees.

2. Key Background

TSURUWrapper (0x75ac62ea5d058a7f88f0c3a5f8f73195277c93da) is an ERC20 wrapper intended to represent claims on an underlying ERC1155/ERC721 position. Its total supply is bounded by _maxTotalSupply = 431_386_000 * 1e18, and ERC1155-based minting is parameterized by ERC1155_RATIO = 400_000 * 1e18, so that depositing one ERC1155 unit is supposed to mint 400_000 * 1e18 TSURUWrapper tokens. The verified source is available in the collected Foundry project for the victim contract.

The contract implements IERC1155Receiver and IERC721Receiver, with onERC1155Received intended to be called by the underlying ERC1155 contract when TSURUWrapper receives ERC1155 tokens. Users can later call unwrap to burn TSURUWrapper and receive ERC1155 tokens back in proportion to ERC1155_RATIO.

On Base there exists a TSURUWrapper/WETH pool at 0x913b1658dd001dff93d3af2a657523f1eed53917 that trades TSURUWrapper against WETH9 (0x4200…0006) via a router at 0x2626664c2603336e57b271c5c0b26f421741e481. Traces and balance diffs for tx 0xfe09… show TSURUWrapper flowing from the helper into this pool and WETH/ETH flowing out toward the adversary.

The adversary deployed two helper contracts:

  • Helper 0x66e9… implements functions that, when called by tx.origin == 0x7a5e…, approve TSURUWrapper to the router and execute a "sell all TSURUWrapper for WETH/ETH" path, then forward ETH to the EOA.
  • Orchestrator 0xa2209… implements a function that takes TSURUWrapper and helper addresses, then calls TSURUWrapper.onERC1155Received from its own address, passing the helper as from and a chosen amount.

These contracts are unprivileged; any adversary cluster can deploy analogous contracts to reproduce the same exploit so long as TSURUWrapper is deployed with the same vulnerable onERC1155Received logic and sufficient liquidity exists in the TSURUWrapper/WETH pool.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability is an unbacked mint via inverted receiver guard in an ERC1155-based wrapper. TSURUWrapper intends to mint ERC20 supply only when it receives ERC1155 tokens from the designated underlying ERC1155 contract. Instead, its onERC1155Received implementation mints when msg.sender is not that contract.

The intended backing invariant, specialized to the ERC1155 path, is:

  • Let H = erc1155Contract.balanceOf(address(TSURUWrapper), tokenID) and R = ERC1155_RATIO.
  • Excluding any explicitly configured pre-mint, incremental TSURUWrapper supply created via ERC1155 deposits should satisfy incrementalSupply <= H * R.

In the verified victim code, onERC1155Received is implemented as:

function onERC1155Received(
    address,
    address from,
    uint256 id,
    uint256 amount,
    bytes calldata
) external override nonReentrant returns (bytes4) {
    require(id == tokenID, "Token ID does not match");

    if (msg.sender == address(erc1155Contract)) {
        return this.onERC1155Received.selector;
    }

    _safeMint(from, amount * ERC1155_RATIO);
    return this.onERC1155Received.selector;
}

This logic inverts the usual authorization: if msg.sender is the legitimate ERC1155 contract, TSURUWrapper skips minting and simply returns. For any other msg.sender, TSURUWrapper calls _safeMint(from, amount * ERC1155_RATIO) without verifying that ERC1155 tokens were transferred or that msg.sender is authorized. As a result, any contract can call onERC1155Received directly and mint TSURUWrapper tokens to an arbitrary from address, constrained only by _maxTotalSupply.

The invariant breakpoint is therefore the if (msg.sender == address(erc1155Contract)) branch combined with the unconditional _safeMint in the else path. In tx 0xe63a8d…, orchestrator 0xa2209… calls TSURUWrapper.onERC1155Received from its own address with from = 0x66e9… and amount = 418. Because msg.sender is the orchestrator, not the ERC1155 contract, TSURUWrapper mints 418 * 400_000 * 1e18 = 167200000000000000000000000 TSURUWrapper tokens to the helper without any additional ERC1155 deposit, breaking the backing invariant.

4. Detailed Root Cause Analysis

4.1 Pre-state at block 14279785

Data collected before block 14279786 (state immediately prior to tx 0xe63a8d…) shows:

TSURUWrapper.totalSupply() = 264079600000000000000000000
erc1155Contract.balanceOf(address(TSURUWrapper), tokenID) = 999

(from pre-state snapshots totalSupply_block_14279785.txt and erc1155_balance_block_14279785.txt). At this point TSURUWrapper is already live with a significant pre-mint, but the backing ERC1155 position is fixed at 999 units.

4.2 Vulnerable onERC1155Received hook

The verified source for TSURUWrapper (collected from the explorer into a Foundry project) shows the onERC1155Received implementation quoted above. The key properties are:

  • Only the id == tokenID check is enforced; any amount is allowed.
  • If msg.sender == address(erc1155Contract), the function returns without minting.
  • For all other callers, _safeMint(from, amount * ERC1155_RATIO) is executed.
  • There is no check that erc1155Contract.safeTransferFrom was called, no verification that TSURUWrapper’s ERC1155 balance increased, and no require tying msg.sender to erc1155Contract.

Thus the ERC1155-backed-mint path is effectively disabled for the legitimate ERC1155 contract and enabled for arbitrary external contracts.

4.3 Exploit mint tx 0xe63a8d… (unbacked TSURUWrapper mint)

In tx 0xe63a8df8759f41937432cd34c590d85af61b3343cf438796c6ed2c8f5b906f62 on Base, the adversary executes the following sequence (as captured in the seed trace.cast.log):

[94407] 0xa2209b48…::bbdb9f61(… 75ac62ea5d05… 66e915f1… )
  ├─ [22266] 0x66e915f1…::218684fc()           // record tx.origin
  ├─ [2544] TSURUWrapper::totalSupply()        // returns 264079600000000000000000000
  ├─ [62796] TSURUWrapper::onERC1155Received(
  │       0x0,
  │       0x66e915f1…,   // from
  │       0,
  │       418,
  │       0x
  │   )
  │   ├─ emit Transfer(0x0 → 0x66e9…, value: 167200000000000000000000000)
  │   └─ storage slot 2 (totalSupply) increases accordingly
  └─ ← [Stop]

The trace and the associated balance_diff.json for this tx show that:

  • Helper 0x66e9… receives +167200000000000000000000000 TSURUWrapper tokens (holder delta from 0 to that value).
  • No ERC1155 balance change occurs for TSURUWrapper: erc1155Contract.balanceOf(address(TSURUWrapper), tokenID) remains 999 across blocks 14279785 and 14279786.

Combining these facts:

  • Incremental ERC1155 backing ΔH = 0.
  • Incremental TSURUWrapper minted to the helper ΔS = 167200000000000000000000000.
  • The invariant ΔS <= ΔH * ERC1155_RATIO is violated since ΔH = 0 and ΔS > 0.

This is a pure contract-level bug: the adversary does not need any special privileges or off-chain coordination, only the ability to deploy a contract that calls onERC1155Received with chosen parameters.

4.4 Profit-taking swap tx 0xfe09… (TSURUWrapper → WETH/ETH)

In tx 0xfe091a2d175f488bd366d3a84e9c37a622d789bf4539defacfc5f2a08169e2ca on Base, the adversary realizes profit by dumping the unbacked TSURUWrapper into the TSURUWrapper/WETH pool via the helper contract. The collected trace and balance_diff.json show:

{
  "native_balance_deltas": [
    { "address": "0x7a5e…", "delta_wei": "137782560470775496246" },
    { "address": "0x4200…0006", "delta_wei": "-137783057102494581685" }
  ],
  "erc20_balance_deltas": [
    {
      "token": "0x4200000000000000000000000000000000000006",
      "holder": "0x913b1658dd001dff93d3af2a657523f1eed53917",
      "delta": "-137783057102494581685"
    },
    {
      "token": "0x75ac62ea5d058a7f88f0c3a5f8f73195277c93da",
      "holder": "0x66e9…",
      "delta": "-167200000000000000000000000"
    },
    {
      "token": "0x75ac62ea5d058a7f88f0c3a5f8f73195277c93da",
      "holder": "0x913b1658dd001dff93d3af2a657523f1eed53917",
      "delta": "167200000000000000000000000"
    }
  ]
}

From this we obtain the concrete on-chain effects of tx 0xfe09…:

  • Helper 0x66e9… transfers its entire TSURUWrapper balance (167200000000000000000000000) into pool 0x913b….
  • The pool’s WETH9 reserve decreases by 137783057102494581685 units, and WETH9’s own ETH balance falls by the same amount.
  • EOA 0x7a5e… ends the tx with +137782560470775496246 wei, which is slightly less than the pool’s WETH outflow due to gas/L1 fees.

The decompiled helper contract confirms that the function with selector 0xc80fb189 (called in 0xfe09…) is only callable when tx.origin == 0x7a5e… and performs an approve-and-swap sequence via router 0x2626…, then forwards ETH proceeds to the origin EOA.

4.5 End-to-end profit computation

Across the minimal ACT sequence b = (0xe63a8d…, 0xfe09…), the adversary’s net ETH-based profit is computed purely from native-balance diffs:

  • In 0xe63a8d…, EOA 0x7a5e… has delta_wei = -370183100363958 (gas/L1 fees only).
  • In 0xfe09…, the same EOA has delta_wei = +137782560470775496246.
  • Combined, the portfolio change attributable to b is:
ΔETH = 137782560470775496246 - 370183100363958
     = 137782190287675132288 wei
     = 137.782190287675132288 ETH

This matches the profit fields in root_cause.json and satisfies the ACT success predicate: the adversary’s net portfolio value in ETH strictly increases after accounting for all fees, using only public on-chain data and standard inclusion rules.

5. Adversary Flow Analysis

The adversary strategy is a two-tx, single-chain ACT exploit on Base:

  1. Deploy helper and orchestrator contracts tied to EOA 0x7a5e… via tx.origin checks.
  2. Use the orchestrator to invoke TSURUWrapper’s flawed onERC1155Received and mint a large unbacked TSURUWrapper position into the helper.
  3. Immediately swap the unbacked TSURUWrapper into WETH/ETH via the TSURUWrapper/WETH pool and forward the ETH to the EOA.

5.1 Adversary-related cluster and victim

  • Adversary EOA: 0x7a5eb99c993f4c075c222f9327abc7426cfae386 (controller of both contracts, pays gas, receives profit).
  • Helper contract: 0x66e915f192ef3d121ad518f0fb37a74fed1c090a (holds TSURUWrapper and executes the swap; tx.origin-locked to 0x7a5e…).
  • Orchestrator contract: 0xa2209b48506c4e7f3a879ec1c1c2c4ee16c2c017 (invokes onERC1155Received on TSURUWrapper with adversary-chosen parameters; also tx.origin-locked).
  • Victim contract: TSURUWrapper 0x75ac62ea5d058a7f88f0c3a5f8f73195277c93da on Base (verified source).
  • Liquidity venue: TSURUWrapper/WETH pool 0x913b1658dd001dff93d3af2a657523f1eed53917 on Base.

5.2 Lifecycle stages

Stage 1 – Adversary contract deployment
Txs 0x368c98… and 0xb3790c… (Base blocks 14279750 and 14279764) deploy helper 0x66e9… and orchestrator 0xa2209… from EOA 0x7a5e…. Etherscan txlists and the decompiled code show both contracts enforcing tx.origin == 0x7a5e…, confirming control by this EOA.

Stage 2 – Unbacked TSURUWrapper mint (tx 0xe63a8d…)
In block 14279786, EOA 0x7a5e… calls orchestrator 0xa2209…. The orchestrator’s bbdb9f61-labelled function stores tx.origin in helper state (via selector 0x218684fc), queries TSURUWrapper.totalSupply(), and then calls:

TSURUWrapper::onERC1155Received(
    operator = 0x0000000000000000000000000000000000000000,
    from     = 0x66e915f1… ,
    id       = 0,
    amount   = 418,
    data     = 0x
)

Because msg.sender for this call is the orchestrator, not the ERC1155 contract, the vulnerable onERC1155Received executes _safeMint(from, 418 * ERC1155_RATIO) and mints 167200000000000000000000000 TSURUWrapper tokens to the helper. Pre/post state queries around this block confirm that TSURUWrapper’s ERC1155 holdings remain at 999 units, so these new tokens are entirely unbacked by additional ERC1155 deposits.

Stage 3 – Profit-taking swap (tx 0xfe09…)
In block 14279797, EOA 0x7a5e… calls helper 0x66e9… with selector 0xc80fb189. As decompiled, this function:

  • Requires tx.origin == 0x7a5e….
  • Approves router 0x2626… to spend TSURUWrapper from the helper.
  • Reads the helper’s current TSURUWrapper balance.
  • Calls the router’s swap function, sending all TSURUWrapper into pool 0x913b… and receiving WETH.
  • Unwraps WETH into ETH and forwards 137783057102494581685 wei of ETH to 0x7a5e….

The balance_diff.json for this tx confirms the asset flows and the net ETH gain of 137782560470775496246 wei for the EOA after gas/L1 fees.

6. Impact & Losses

Over the ACT sequence b = (0xe63a8d…, 0xfe09…), the adversary’s EOA 0x7a5e… realizes a net profit of 137.782190287675132288 ETH as measured by native-balance diffs. This profit is financed directly by the TSURUWrapper/WETH pool and the WETH9 contract:

  • Pool 0x913b… loses 137783057102494581685 WETH from its reserves.
  • WETH9 (0x4200…0006) unwraps the same amount into ETH and transfers it out.
  • After subtracting gas and L1 fees paid across both txs, the EOA’s net gain remains strictly positive.

On the TSURUWrapper side, the exploit mints 167200000000000000000000000 new TSURUWrapper tokens to helper 0x66e9… without any increase in the wrapper’s ERC1155 holdings. These unbacked tokens are then permanently parked in the TSURUWrapper/WETH pool after the swap, leaving the pool holding TSURUWrapper that is no longer fully backed by the intended ERC1155 collateral while having paid out ETH to the adversary.

7. References

  • TSURUWrapper source (verified victim contract): TSURUWrapper at 0x75ac62ea5d058a7f88f0c3a5f8f73195277c93da (collected Foundry project with src/Contract.sol).
  • Exploit mint tx trace: Base tx 0xe63a8df8759f41937432cd34c590d85af61b3343cf438796c6ed2c8f5b906f62 (cast run -vvvvv style trace and balance diffs).
  • Profit-taking swap tx trace and balances: Base tx 0xfe091a2d175f488bd366d3a84e9c37a622d789bf4539defacfc5f2a08169e2ca (execution trace, receipt, and balance_diff.json showing TSURUWrapper, WETH, and ETH movements).
  • Helper and orchestrator contracts: Decompiled helper 0x66e915f192ef3d121ad518f0fb37a74fed1c090a and orchestrator 0xa2209b48506c4e7f3a879ec1c1c2c4ee16c2c017 (heimdall decompilation and inferred ABI used to justify control and behavior).
  • State snapshots: Pre/post-state queries for TSURUWrapper.totalSupply and erc1155Contract.balanceOf(address(TSURUWrapper), tokenID) around blocks 14279785–14279786 on Base.