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…

May 2025Otheraccess-control3 min read

Chain

Other

Category

access-control

Date

May 2025

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

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.

Loading fork state…

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-registry mirror.


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, no anvil_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#

ImpactHIGH — attacker calls withdrawToNativeChain with zrc20 = ETH_ADDRESS, value: 0, and a message targeting real accumulated ZRC20; drains gateway with only gas cost
ProtocolDODO Cross-Chain DEX — GatewayTransferNative
Vulnerable codewithdrawToNativeChain skips transferFrom for _ETH_ADDRESS_ without requiring msg.value >= amount (GatewayTransferNative.sol#L534-L537)
Bug classMissing native-value validation on placeholder token path
FindingSherlock 2025-05-dodo-cross-chain-dex · #58579 · reporter X0sauce (and others)
Reportsherlock-audit/2025-05-dodo-cross-chain-dex-judging
SourceAuditVault
StatusAudit finding — fixed in PR 32. Reproduced as a standalone local PoC.
Compiler^0.8.24 (PoC)

TL;DR#

  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 own balance to the attacker. Cost: gas only.
  5. Fix: require(msg.value >= amount) when zrc20 == _ETH_ADDRESS_.

The vulnerable code#

SOLIDITY
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#

  1. Gateway holds 100 USDC.ZRC20.
  2. Attacker calls withdrawToNativeChain{value:0}(ETH_ADDRESS, 100e18, message) with targetZRC20 = USDC.
  3. transferFrom skipped; no native paid.
  4. Withdraw burns/transfers 100 USDC to attacker receiver.

Diagrams#

flowchart TD A["withdrawToNativeChain value:0 zrc20=ETH_ADDRESS amount=100"] --> B{"zrc20 == ETH_ADDRESS?"} B -->|yes| C["Skip transferFrom"] C --> D["outputAmount = amount"] D --> E["_withdraw real USDC.ZRC20"] E --> F["Attacker receives 100 USDC from gateway balance"]

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#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

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.