TSURUWrapper onERC1155Received bug mints unbacked tokens and drains WETH
Exploit Transactions
Victim Addresses
0x75ac62ea5d058a7f88f0c3a5f8f73195277c93daBaseLoss Breakdown
Similar Incidents
Odos router signature-validation bug enables arbitrary token drains
34%SynapLogicErc20 Router Flash-Loan Over-Mint Exploit
32%USDC drain via unchecked Uniswap V3-style callback
31%RewardsHypervisor reentrant deposit mints unbacked vVISR and drains VISR
31%MPRO Staking Proxy unwrapWETH Flash-Loan Exploit (Base)
31%Treasury allowance-router abuse drains USDC/USDT/WBTC cross-chain
30%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 bytx.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 callsTSURUWrapper.onERC1155Receivedfrom its own address, passing the helper asfromand a chosenamount.
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)andR = 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 == tokenIDcheck is enforced; anyamountis 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.safeTransferFromwas called, no verification that TSURUWrapper’s ERC1155 balance increased, and no require tyingmsg.sendertoerc1155Contract.
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+167200000000000000000000000TSURUWrapper tokens (holder delta from 0 to that value). - No ERC1155 balance change occurs for TSURUWrapper:
erc1155Contract.balanceOf(address(TSURUWrapper), tokenID)remains999across 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_RATIOis violated sinceΔH = 0andΔ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 pool0x913b…. - The pool’s WETH9 reserve decreases by
137783057102494581685units, and WETH9’s own ETH balance falls by the same amount. - EOA
0x7a5e…ends the tx with+137782560470775496246wei, 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…, EOA0x7a5e…hasdelta_wei = -370183100363958(gas/L1 fees only). - In
0xfe09…, the same EOA hasdelta_wei = +137782560470775496246. - Combined, the portfolio change attributable to
bis:
Δ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:
- Deploy helper and orchestrator contracts tied to EOA
0x7a5e…viatx.originchecks. - Use the orchestrator to invoke TSURUWrapper’s flawed
onERC1155Receivedand mint a large unbacked TSURUWrapper position into the helper. - 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 to0x7a5e…). - Orchestrator contract:
0xa2209b48506c4e7f3a879ec1c1c2c4ee16c2c017(invokesonERC1155Receivedon TSURUWrapper with adversary-chosen parameters; also tx.origin-locked). - Victim contract: TSURUWrapper
0x75ac62ea5d058a7f88f0c3a5f8f73195277c93daon Base (verified source). - Liquidity venue: TSURUWrapper/WETH pool
0x913b1658dd001dff93d3af2a657523f1eed53917on 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
137783057102494581685wei of ETH to0x7a5e….
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…loses137783057102494581685WETH 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):
TSURUWrapperat0x75ac62ea5d058a7f88f0c3a5f8f73195277c93da(collected Foundry project withsrc/Contract.sol). - Exploit mint tx trace: Base tx
0xe63a8df8759f41937432cd34c590d85af61b3343cf438796c6ed2c8f5b906f62(cast run -vvvvvstyle trace and balance diffs). - Profit-taking swap tx trace and balances: Base tx
0xfe091a2d175f488bd366d3a84e9c37a622d789bf4539defacfc5f2a08169e2ca(execution trace, receipt, andbalance_diff.jsonshowing TSURUWrapper, WETH, and ETH movements). - Helper and orchestrator contracts: Decompiled helper
0x66e915f192ef3d121ad518f0fb37a74fed1c090aand orchestrator0xa2209b48506c4e7f3a879ec1c1c2c4ee16c2c017(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.