Reproduced Exploit
DODO Cross-Chain DEX — missing `msg.value` check on ETH placeholder drains ZRC20
1. For non-ETH inputs, the gateway pulls tokens via transferFrom. 2. For zrc20 == _ETH_ADDRESS_, that pull is skipped — but msg.value is never checked. 3. Attacker claims amount = 100e18 with value: 0, sets targetZRC20 to real USDC.ZRC20 in the message. 4. Gateway withdraws 100 USDC.ZRC20 from its…
Chain
Other
Category
access-control
Date
May 2025
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 58579-h-2-any-attacker-will-steal-accumulated-zrc20-tokens-from-ga. Standalone Foundry PoC and full write-up: 58579-h-2-any-attacker-will-steal-accumulated-zrc20-tokens-from-ga_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/access-control/missing-auth · vuln/bridge/message-spoofing · vuln/loss-of-funds/direct-drain
Reproduction: a self-contained Foundry PoC that compiles & runs in an isolated project with only
forge-std— no fork, no RPC, noanvil_state. Full trace: output.txt. PoC: test/58579-h-2-any-attacker-will-steal-accumulated-zrc20-tokens-from-ga_exp.sol.
AuditVault taxonomy: severity/high · sector/bridge · sector/dex · sector/stable · sector/token · platform/sherlock · bridge-sender-auth · message-spoofing · direct-drain · variant · bridge-is-secure · add-check · cross-chain-message
Key info#
| Impact | HIGH — attacker calls withdrawToNativeChain with zrc20 = ETH_ADDRESS, value: 0, and a message targeting real accumulated ZRC20; drains gateway with only gas cost |
| Protocol | DODO Cross-Chain DEX — GatewayTransferNative |
| Vulnerable code | withdrawToNativeChain skips transferFrom for _ETH_ADDRESS_ without requiring msg.value >= amount (GatewayTransferNative.sol#L534-L537) |
| Bug class | Missing native-value validation on placeholder token path |
| Finding | Sherlock 2025-05-dodo-cross-chain-dex · #58579 · reporter X0sauce (and others) |
| Report | sherlock-audit/2025-05-dodo-cross-chain-dex-judging |
| Source | AuditVault |
| Status | Audit finding — fixed in PR 32. Reproduced as a standalone local PoC. |
| Compiler | ^0.8.24 (PoC) |
TL;DR#
- For non-ETH inputs, the gateway pulls tokens via
transferFrom. - For
zrc20 == _ETH_ADDRESS_, that pull is skipped — butmsg.valueis never checked. - Attacker claims
amount = 100e18withvalue: 0, setstargetZRC20to real USDC.ZRC20 in the message. - Gateway withdraws 100 USDC.ZRC20 from its own balance to the attacker. Cost: gas only.
- Fix:
require(msg.value >= amount)whenzrc20 == _ETH_ADDRESS_.
The vulnerable code#
if (zrc20 != ETH_ADDRESS) {
require(IZRC20(zrc20).transferFrom(msg.sender, address(this), amount), "...");
}
// FIX: if (zrc20 == ETH_ADDRESS) require(msg.value >= amount, "INSUFFICIENT NATIVE TOKEN");
uint256 outputAmount = amount; // @> VULN: amount accepted with no msg.value check
_withdraw(targetZRC20, receiver, outputAmount - gasFee);
Root cause#
The ETH placeholder path assumes the caller paid native value equal to amount, but never enforces it. Combined with a message that names a different real ZRC20 as the withdrawal target, the claimed amount is satisfied from gateway inventory the attacker never deposited.
Preconditions#
- Gateway holds accumulated ZRC20 (refunds / prior ops).
- Anyone can call
withdrawToNativeChain.
Attack walkthrough#
- Gateway holds 100 USDC.ZRC20.
- Attacker calls
withdrawToNativeChain{value:0}(ETH_ADDRESS, 100e18, message)withtargetZRC20 = USDC. transferFromskipped; no native paid.- Withdraw burns/transfers 100 USDC to attacker receiver.
Diagrams#
Impact#
Near-complete drainage of any accumulated ZRC20 the attacker can name as targetZRC20, for gas fees only. Sherlock logs showed 99/100 tokens stolen in one call.
Sources#
- AuditVault finding #58579
- Sherlock judging issue #219
- Reduced source: sherlock-audit/2025-05-dodo-cross-chain-dex @ d4834a4 —
GatewayTransferNative.withdrawToNativeChain
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 58579-h-2-any-attacker-will-steal-accumulated-zrc20-tokens-from-ga_exp (evm-hack-registry mirror).
- AuditVault finding: 58579-h-2-any-attacker-will-steal-accumulated-zrc20-tokens-from-ga.
Alerts & third-party analyses
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.