Reproduced Exploit

Enjoyoors — claimWithdrawal pays before the request is marked claimed

_claimWithdrawal sends the request amount and only then sets request.claimed = true. A token that callbacks on transfer, the ERC-777 case in the report, reenters claimWithdrawal while claimed is still false.

Dec 2024•Ethereum•reentrancy•3 min read

Chain

Ethereum

Category

reentrancy

Date

Dec 2024

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: 49875-reentrancy-vulnerability-in-claimwithdrawal-function-mixbyt. Standalone Foundry PoC and full write-up: 49875-reentrancy-vulnerability-in-claimwithdrawal-function-mixbyt_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/reentrancy/single-function · vuln/dependency/unsafe-external-call

Key info#

ProtocolEnjoyoors
FindingMixBytes, December 2024, AuditVault 49875
Vulnerable code_claimWithdrawal transfers, then sets claimed
HarmA 100 claim is paid 10 times. The caller receives 1000. The guarded order pays 100 and leaves 900.
Chainlocal synthetic (audited Ethereum contracts)

TL;DR#

_claimWithdrawal sends the request amount and only then sets request.claimed = true. A token that callbacks on transfer, the ERC-777 case in the report, reenters claimWithdrawal while claimed is still false.

The vault holds 1000: this request is 100, and 900 is still other depositors' cash. Each reentry pays another 100 until the cash is below 100. The caller receives 1000. Setting claimed before the transfer pays 100 once and leaves 900.

The vulnerable code#

The audited line is IERC20(token).safeTransfer(request.user, amount) followed by request.claimed = true.

SOLIDITY
require(request.user == msg.sender, "user");
// @> VULN: the token is sent before claimed is set, so the callback reenters.
token.transfer(request.user, amount);
request.claimed = true;

The control flips the flag first:

SOLIDITY
request.claimed = true;
token.transfer(request.user, amount);

Root cause#

The external token call sits in the middle of the claim. claimed is the only lock, and it is written after the call. The callback is the claimant, so request.user == msg.sender still holds.

Preconditions#

The claimed token callbacks the recipient. The vault's token balance is larger than this one request because other users have deposited. Here the request is 100 and the cash is 1000. The approver delay is not what the finding breaks; once the claim is callable, one transaction repeats it. No warp.

Attack walkthrough#

  1. Vault cash is 1000. The honest payment is 100.
  2. claimWithdrawal transfers 100 before setting claimed.
  3. The token calls tokensReceived. Cash is still at least 100, so the callback claims again.
  4. Ten payments later the caller holds 1000 and the vault holds 0.
  5. 1000 - 100 = 900 was other depositors' cash.

Control pays 100 and leaves 900.

Diagrams#

flowchart TD A["Vault holds 1000 and the request is 100"] --> B["claimWithdrawal transfers before claimed"] B --> C["VULN: token callback reenters"] C --> D["Ten payments empty the vault"] D --> E["Caller holds 1000"] E --> F["Guarded claim pays 100 and leaves 900"]

Impact#

The claimant receives 1000 tokens on a 100 request. 900 of that was cash still owed to other depositors. The guarded claim pays 100.

Remediation#

Set claimed before the token transfer, and add nonReentrant on the claim.

How to reproduce#

BASH
cd 49875-reentrancy-vulnerability-in-claimwithdrawal-function-mixbyt_exp
forge test -vv

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.