Reproduced Exploit

DODO Cross-Chain DEX — unauthorized claim of non-EVM chain refunds

1. Refunds store a walletAddress blob (20 bytes for EVM, longer for BTC/etc.). 2. claimRefund only decodes receiver when length is exactly 20; otherwise receiver stays msg.sender. 3. Auth becomes msg.sender == msg.sender → always true for non-bot callers. 4. Attacker frontruns bots and steals the r…

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: 58582-h-5-unauthorized-claim-of-non-evm-chain-refunds-in-claimrefu. Standalone Foundry PoC and full write-up: 58582-h-5-unauthorized-claim-of-non-evm-chain-refunds-in-claimrefu_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/access-control/auth-bypass · vuln/loss-of-funds/direct-drain · vuln/logic/address-encoding

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/58582-h-5-unauthorized-claim-of-non-evm-chain-refunds-in-claimrefu_exp.sol.

AuditVault taxonomy: severity/high · sector/bridge · sector/dex · platform/sherlock · flashloan-callback-auth · direct-drain


Key info#

ImpactHIGH — any caller can claim refunds whose walletAddress is not 20 bytes (Bitcoin, Solana, …)
ProtocolDODO Cross-Chain DEX — GatewayCrossChain / GatewayTransferNative claimRefund
Vulnerable codereceiver = msg.sender when walletAddress.length != 20, then require(bots[msg.sender] || msg.sender == receiver)
Bug classAuth bypass for non-EVM refund receivers
FindingSherlock 2025-05-dodo-cross-chain-dex · #58582 · reporter newspacexyz (and others)
Reportsherlock-audit/2025-05-dodo-cross-chain-dex-judging
SourceAuditVault
StatusAudit finding — fixed in PR 24. Reproduced as a standalone local PoC.
Compiler^0.8.24 (PoC)

TL;DR#

  1. Refunds store a walletAddress blob (20 bytes for EVM, longer for BTC/etc.).
  2. claimRefund only decodes receiver when length is exactly 20; otherwise receiver stays msg.sender.
  3. Auth becomes msg.sender == msg.sender → always true for non-bot callers.
  4. Attacker frontruns bots and steals the refund tokens. Fix: non-EVM path must require bots[msg.sender] only (never equate receiver to caller).

The vulnerable code#

SOLIDITY
address receiver = msg.sender;
if (refundInfo.walletAddress.length == 20) {
    receiver = address(uint160(bytes20(refundInfo.walletAddress)));
}
// FIX: non-EVM → require(bots[msg.sender]); do not set receiver = msg.sender for auth
require(bots[msg.sender] || msg.sender == receiver, "INVALID_CALLER"); // @> VULN

Root cause#

The default receiver = msg.sender was meant as a convenience for the bot path, but it also collapses the authorization predicate whenever the wallet is not a 20-byte EVM address. Non-EVM refunds therefore have no beneficiary check for arbitrary EOAs.

Preconditions#

  • A refund exists with walletAddress.length != 20 (failed Bitcoin/Solana outbound, etc.).
  • Contract holds the refund token balance.

Attack walkthrough#

  1. Seed refund: 10_000 TOKEN for Bitcoin address bc1q… (42 bytes).
  2. Attacker (not a bot) calls claimRefund(externalId).
  3. receiver = msg.sender; require passes; tokens transfer to attacker.
  4. Control: 20-byte EVM refund still reverts INVALID_CALLER for the same attacker.

Diagrams#

flowchart TD A["Refund walletAddress length != 20"] --> B["receiver = msg.sender"] B --> C{"bots or msg.sender == receiver?"} C -->|"always true"| D["Transfer refund to attacker"] E["EVM length == 20"] --> F["receiver = decoded address"] F --> G["Unauthorized caller reverts"]

Impact#

Complete theft of all non-EVM-bound refund inventory at gas cost only. Legitimate users on Bitcoin and similar chains permanently lose those funds.

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.