Reproduced Exploit

Hanji OnchainLOB — a transfer hook still sees the old trader balance

withdraw transfers the tokens and only then subtracts balances[trader]. A token hook runs in between. getTraderBalance still returns 1000, and a lender that borrows against that view pays 1000. The trader already has the withdrawn tokens. The same withdrawal on a book that subtracts first shows a b…

Apr 2025•Other•reentrancy•3 min read

Chain

Other

Category

reentrancy

Date

Apr 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: 55173-potential-read-only-reentrancy-mixbytes-none-hanji-markdown. Standalone Foundry PoC and full write-up: 55173-potential-read-only-reentrancy-mixbytes-none-hanji-markdown_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/reentrancy/read-only

Key info#

ProtocolHanji OnchainLOB
FindingMixBytes, AuditVault 55173
Vulnerable codewithdraw writes the trader balance after the token transfer
HarmA desk that trusts getTraderBalance during the hook lends 1000. The book that updates first lends 0.
Chainlocal synthetic

TL;DR#

withdraw transfers the tokens and only then subtracts balances[trader]. A token hook runs in between. getTraderBalance still returns 1000, and a lender that borrows against that view pays 1000. The trader already has the withdrawn tokens. The same withdrawal on a book that subtracts first shows a balance of 0, and the lender pays 0.

The vulnerable code#

SOLIDITY
require(token.transfer(msg.sender, amount), "transfer");
if (!cei) {
    // @> VULN: trader balance is written after the external token transfer
    balances[msg.sender] -= amount;
}

getTraderBalance returns balances[trader]. The report says that view is stale for any integration that reads it from a transfer hook, and that the balance must be updated before the transfer.

Root cause#

The storage write is on the other side of an external call. The view is correct only after withdraw returns. Callers inside the token hook observe the pre-transfer balance.

Preconditions#

The trader has deposited 1000. A desk holds 1000 and lends the amount getTraderBalance reports. The token notifies the recipient when the book is the sender. No admin action is required.

Attack walkthrough#

  1. Deposit 1000. The stored balance is 1000.
  2. Withdraw 1000. The token moves first.
  3. In the hook, getTraderBalance returns 1000.
  4. The desk transfers 1000 to the trader.
  5. The book then writes the balance down to 0.
  6. On the checks-effects book the hook reads 0 and the desk transfers 0.

Diagrams#

flowchart TD A["Withdraw transfers 1000"] --> B["Hook reads getTraderBalance"] B --> C["View still returns 1000"] C --> D["Desk lends 1000"] D --> E["Balance is written down after the hook"] E --> F["Fixed book shows 0 and the desk pays 0"]

Impact#

The trader finishes with the withdrawn 1000 plus 1000 taken from the desk. Depositors of the desk lose that 1000. The book that writes the balance first does not create a balance the desk will lend against.

Remediation#

Subtract balances[msg.sender] before the token transfer.

How to reproduce#

BASH
cd 55173-potential-read-only-reentrancy-mixbytes-none-hanji-markdown_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.