Reproduced Exploit
Talentir — canceling order B locks order A's 100 TOKEN
_addOrder pushes new ids to the front of orderList[price]. _removeOrder always pop(false), which removes the tail. The tail is the oldest order.
Chain
Other
Category
logic
Date
Sep 2024
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: 60731-incorrect-accounting-in-removeorder-leading-to-loss-of-orde. Standalone Foundry PoC and full write-up: 60731-incorrect-accounting-in-removeorder-leading-to-loss-of-orde_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic/incorrect-state-transition · vuln/logic/state-update
Reproduction: a self-contained Foundry PoC that compiles and runs offline with only
forge-std— no fork and no RPC. Trace: output.txt. PoC: test/60731-incorrect-accounting-in-removeorder-leading-to-loss-of-orde_exp.sol.
Key info#
| Impact | HIGH — canceling order B unlinks order A and locks its 100 TOKEN escrow |
| Protocol | Talentir TalentirMarketplaceV1 |
| Vulnerable code | _removeOrder calls pop(false) instead of removing the requested id |
| Bug class | The price list drops the oldest order |
| Finding | Quantstamp (Zeeshan Meghji) — Talentir marketplace · id 60731 |
| Report | Quantstamp Talentir |
| Source | AuditVault finding |
| Status | Fixed in commit 29e5bc9a by removing the specific order id. Reproduced here as a local synthetic. |
| Compiler | solc 0.8.27, cancun, via IR |
TL;DR#
_addOrder pushes new ids to the front of orderList[price]. _removeOrder always pop(false), which removes the tail. The tail is the oldest order.
The maker places order A for 100 TOKEN, then order B for 40 TOKEN, at the same price. Canceling B refunds the 40 TOKEN and pops A. A is marked off-book without a refund. cancelOrder(A) then reverts because A is not on the book. The market still holds 100 TOKEN.
getBestOrder returns B, which is already inactive. The book no longer contains the order that still has escrow.
The control removes the requested id. Canceling B leaves A on the book, and a second cancel returns the 100 TOKEN.
The vulnerable code#
token.transfer(msg.sender, o.amount);
// @> VULN: pop(false) drops the tail, not this order id
uint256 removed = _popBack(o.price);
if (removed != orderId) orders[removed].onBook = false;
Preconditions#
- Two orders share a token, a price, and a side.
- A was inserted first, so it is the tail after B is pushed to the front.
- Escrow is returned only for the order id the caller passed. The popped id is only unlinked.
Attack walkthrough#
addOrderescrows 100 TOKEN as order A.addOrderescrows 40 TOKEN as order B. The list is[B, A].cancelOrder(B)refunds 40 TOKEN and pops the tail.- The popped id is A. A is no longer on the book. Its 100 TOKEN stays.
cancelOrder(A)reverts.getBestOrderreturns B.
Control: cancel B removes B only. Cancel A then returns the 100 TOKEN, and the market balance is 0.
Diagrams#
Impact#
The maker who cancels the newer 40 TOKEN order loses access to the older 100 TOKEN escrow. That 100 TOKEN stays in the marketplace. The best order left on the book is the one that was just canceled.
Remediation#
Walk the price list until the id matches the order being removed, and refund that id only.
Sources#
Taxonomy#
severity/highvuln/logic/incorrect-state-transition·vuln/logic/state-update
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 60731-incorrect-accounting-in-removeorder-leading-to-loss-of-orde_exp (evm-hack-registry mirror).
- AuditVault finding: 60731-incorrect-accounting-in-removeorder-leading-to-loss-of-orde.
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.