Reproduced Exploit
IOTA Heroes — cancelTradeOer pays the same order until the post is empty
cancelTradeOer sends o.price to the seller and only afterwards sets orders[id].open = false. There is no reentrancy guard. The seller re-enters on the refund, the order is still open, and the same price is paid again until the contract's ETH is gone.
Chain
Other
Category
reentrancy
Date
Apr 2023
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: 21014-contract-funds-can-be-drained-out-auditone-none-lotaheros-ma. Standalone Foundry PoC and full write-up: 21014-contract-funds-can-be-drained-out-auditone-none-lotaheros-ma_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/reentrancy/single-function · vuln/logic/incorrect-order-of-operations
Key info#
| Protocol | IOTA Heroes TradingPost |
| Finding | AuditOne, AuditVault 21014 |
| Vulnerable code | TradingPost.cancelTradeOer |
| Harm | A 1 ETH order is refunded six times. The seller receives 6 ETH. The post, which also held 5 ETH for other sellers, ends at 0. Closing first refunds 1 ETH and leaves 5 ETH. |
| Chain | local synthetic |
TL;DR#
cancelTradeOer sends o.price to the seller and only afterwards sets orders[id].open = false. There is no reentrancy guard. The seller re-enters on the refund, the order is still open, and the same price is paid again until the contract's ETH is gone.
The cited TradingPost.sol lines at commit 3e345747 are unpublished. The reduction keeps the function name and the order the report states: the external call, then the state update.
The vulnerable code#
function cancelTradeOer(uint256 id) external {
Order memory o = orders[id];
require(o.open && o.seller == msg.sender, "order");
// @> VULN: refund call happens before orders[id] is closed
(bool ok,) = msg.sender.call{value: o.price}("");
require(ok, "refund");
orders[id].open = false;
}
The control clears open before the call.
Root cause#
o is a memory copy. Storage still says the order is open while the ETH is in flight. The recipient is msg.sender, who is allowed to cancel, so the re-entered call passes the same checks and pays o.price from whatever ETH the post holds.
Preconditions#
The caller is the seller of an open order. The post's ETH balance is greater than that one order. Here the price is 1 ETH and the post holds 6 ETH.
Attack walkthrough#
- Seed order 7 for the seller at 1 ETH. Fund the post with 6 ETH.
- The seller calls
cancelTradeOer(7). - The refund of 1 ETH hits the seller. The order is still open, so
receivecallscancelTradeOeragain. - That repeats while the post holds at least 1 ETH. Six payments of 1 ETH land on the seller.
- The post balance is 0.
The control marks the order closed before the call. The re-entered cancel reverts and is caught. The seller receives 1 ETH. 5 ETH stays in the post.
Diagrams#
Impact#
Every other seller's ETH in the post can be pulled by cancelling one order. Here 6 ETH leaves and 5 ETH of it was not this order. The fix pays exactly the 1 ETH price.
Remediation#
Set orders[id].open = false before the refund, and add nonReentrant on cancelTradeOer.
How to reproduce#
cd 21014-contract-funds-can-be-drained-out-auditone-none-lotaheros-ma_exp
forge test -vv
Sources#
- AuditVault finding: https://github.com/Auditware/AuditVault/blob/main/findings/21014-contract-funds-can-be-drained-out-auditone-none-lotaheros-ma.md
- AuditOne report: https://github.com/solodit/solodit_content/blob/main/reports/AuditOne/2023-04-13-Lotaheros.md
- Cited source, now unpublished: https://github.com/Crelde/IOTA-Heroes-Contracts/blob/3e345747f723637c0a1ce884d1ae0e1584015e98/contracts/TradingPost.sol#L154
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 21014-contract-funds-can-be-drained-out-auditone-none-lotaheros-ma_exp (evm-hack-registry mirror).
- AuditVault finding: 21014-contract-funds-can-be-drained-out-auditone-none-lotaheros-ma.
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.