Reproduced Exploit

XPress: stale trader balance lends the 1000 reserve

withdraw transfers the token before it decreases traderBalance. The callback reads getTraderBalance and still sees 1000. The lender sends that amount from its own reserve.

May 2025•Other•reentrancy•2 min read

Chain

Other

Category

reentrancy

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: 53805-potential-read-only-reentrancy-mixbytes-none-xpress-protocol. Standalone Foundry PoC and full write-up: 53805-potential-read-only-reentrancy-mixbytes-none-xpress-protocol_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/reentrancy/cross-function · vuln/logic/incorrect-order-of-operations

Key info#

ProtocolXPress
FindingMixBytes, AuditVault 53805
Vulnerable codeLOB.withdraw
HarmThe trader withdraws 1000 and, while the stored balance is still 1000, the lender sends its 1000 reserve. The trader holds 2000. The fixed order leaves the lender with 1000.
Chainlocal synthetic

TL;DR#

withdraw transfers the token before it decreases traderBalance. The callback reads getTraderBalance and still sees 1000. The lender sends that amount from its own reserve.

The vulnerable code#

SOLIDITY
        // @> VULN: traderBalance is reduced only after the token transfer returns
        traderBalance[msg.sender] -= amount;

Root cause#

The view used by the lender is updated after the external transfer, so a callback observes the pre-debit balance.

Preconditions#

The trader's stored balance and the LOB's cash are both 1000. The lender holds a separate 1000 reserve and trusts getTraderBalance.

Attack walkthrough#

  1. Credit the trader 1000 and fund the lender with 1000. See output.txt.
  2. withdraw transfers 1000 before the storage write.
  3. The callback reads a balance of 1000.
  4. The lender sends 1000.
  5. traderBalance is then set to 0.
  6. The trader holds 2000 and the lender holds 0. The fixed path updates first, so the lender sends nothing.

Diagrams#

flowchart TD A["Trader balance is 1000"] --> B["Transfer happens first"] B["Transfer happens first"] --> C["Callback still reads 1000"] C["Callback still reads 1000"] --> D["Lender sends its reserve"] D["Lender sends its reserve"] --> E["Balance is reduced late"]

Impact#

The lender loses 1000. The trader receives their own 1000 plus that reserve. Updating the balance before the transfer makes the view return 0.

Remediation#

Decrease traderBalance before the token transfer.

How to reproduce#

BASH
cd 53805-potential-read-only-reentrancy-mixbytes-none-xpress-protocol_exp
forge test -vv

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.