Reproduced Exploit
DODO Cross-Chain DEX — swap output token ≠ withdrawal target drains gateway
1. onCall performs _doMixSwap(..., params.toToken) then withdraws decoded.targetZRC20. 2. Nothing requires toToken == targetZRC20. 3. Attacker crafts a message: swap BTC→USDC, withdraw ETH (accumulated from prior ops). 4. Gateway spends the swap output amount of ETH to the attacker while USDC sits…
Chain
Other
Category
logic
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: 58578-h-1-missing-swap-withdrawal-validation-enables-accumulated-t. Standalone Foundry PoC and full write-up: 58578-h-1-missing-swap-withdrawal-validation-enables-accumulated-t_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic/cross-contract-state-consistency · vuln/loss-of-funds/direct-drain · vuln/bridge/message-parameter-mismatch
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/58578-h-1-missing-swap-withdrawal-validation-enables-accumulated-t_exp.sol.
AuditVault taxonomy: severity/high · sector/bridge · sector/dex · sector/stable · platform/sherlock · cross-contract-state-consistency · variant
Key info#
| Impact | HIGH — attacker swaps cheap input → cheap output, then withdraws a different high-value token accumulated on the gateway |
| Protocol | DODO Cross-Chain DEX — GatewayCrossChain (ZetaChain) |
| Vulnerable code | onCall swaps with params.toToken then withdraws decoded.targetZRC20 with no equality check (GatewayCrossChain.sol#L465-L508, #L371-L376, #L431-L437) |
| Bug class | Missing validation between swap output and withdrawal target |
| Finding | Sherlock 2025-05-dodo-cross-chain-dex · #58578 · reporter patitonar (and many others) |
| Report | sherlock-audit/2025-05-dodo-cross-chain-dex-judging |
| Source | AuditVault |
| Status | Audit finding — fixed in PR 33. Reproduced as a standalone local PoC. |
| Compiler | ^0.8.24 (PoC) |
TL;DR#
onCallperforms_doMixSwap(..., params.toToken)then withdrawsdecoded.targetZRC20.- Nothing requires
toToken == targetZRC20. - Attacker crafts a message: swap BTC→USDC, withdraw ETH (accumulated from prior ops).
- Gateway spends the swap output amount of ETH to the attacker while USDC sits on the contract.
- Fix:
require(toToken == targetZRC20).
The vulnerable code#
uint256 outputAmount = _doMixSwap(fromZRC20, toToken, amount);
// FIX: require(toToken == targetZRC20, "swap/target mismatch");
_withdraw(targetZRC20, receiver, outputAmount - GAS_FEE); // @> VULN
Root cause#
Two independent message fields control the economic flow: the swap's output token and the withdrawal token. The code uses the swap's amount with the withdrawal's token. When they differ, the gateway pays the attacker from whichever balance targetZRC20 has — typically residual high-value tokens from refunds/prior bridges.
Preconditions#
- Gateway holds accumulated ZRC20 balances (common after refunds / partial fills).
- Attacker can initiate a cross-chain
depositAndCall(or the syntheticonCallentry).
Attack walkthrough#
- Gateway holds 1000 ETH.ZRC20 from prior operations.
- Attacker supplies 1 BTC.ZRC20; message sets
toToken = USDC,targetZRC20 = ETH. - Swap converts BTC→USDC on the gateway.
- Withdraw transfers 1 ETH.ZRC20 to the attacker receiver.
- Protocol loses ETH; attacker paid only BTC.
Diagrams#
Impact#
Complete drainage of any accumulated ZRC20 the attacker can name as targetZRC20, sized by how much input they swap (output amount drives the withdraw size). Sherlock PoC showed ~99% drainage of 1000 ETH with a tiny BTC input under realistic pricing.
Sources#
- AuditVault finding #58578
- Sherlock judging issue #158
- Reduced source: sherlock-audit/2025-05-dodo-cross-chain-dex @ d4834a4 —
GatewayCrossChain.onCall/ withdraw helpers
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 58578-h-1-missing-swap-withdrawal-validation-enables-accumulated-t_exp (evm-hack-registry mirror).
- AuditVault finding: 58578-h-1-missing-swap-withdrawal-validation-enables-accumulated-t.
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.