Reproduced Exploit
NiftyApes — a second Seaport offer item locks 500 SIDE
_validateSaleOrder checks that the first offer item is WETH. It does not require offer.length == 1.
Chain
Other
Category
dos
Date
Jun 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: 60495-locked-funds-when-fulfilling-seaport-orders-quantstamp-nifty. Standalone Foundry PoC and full write-up: 60495-locked-funds-when-fulfilling-seaport-orders-quantstamp-nifty_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/dos/frozen-funds · vuln/input-validation/missing
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/60495-locked-funds-when-fulfilling-seaport-orders-quantstamp-nifty_exp.sol.
Key info#
| Impact | HIGH — fulfilling a two-item offer locks 500 SIDE in seller financing |
| Protocol | NiftyApes SellerFinancing |
| Vulnerable code | fulfillOrder pulls every offer item and forwards only WETH |
| Bug class | Non-WETH offer tokens have no withdraw path |
| Finding | Quantstamp (Michael Boyle) — NiftyApes seller financing · id 60495 |
| Report | Quantstamp NiftyApes |
| Source | AuditVault finding |
| Status | Fixed by requiring a single offer item. Reproduced here as a local synthetic. |
| Compiler | solc 0.8.27, cancun, via IR |
TL;DR#
_validateSaleOrder checks that the first offer item is WETH. It does not require offer.length == 1.
The seller fulfills an order whose offer is 1 WETH plus 500 SIDE. The contract pulls both. It forwards only the 1 WETH to the buyer. 500 SIDE remains in the contract, and there is no withdraw. The seller's SIDE balance is 0.
The control rejects offer.length != 1 before any pull. The seller keeps 500 SIDE.
The vulnerable code#
for (uint256 i; i < offer.length; i++) {
// @> VULN: every offer item is pulled; only WETH is forwarded
Token(offer[i].token).transferFrom(msg.sender, address(this), offer[i].amount);
}
weth.transfer(BUYER, wethOut);
Preconditions#
- The seller holds 1 WETH and 500 SIDE and has approved the financing contract.
- The order's first offer item is the WETH leg. A second item is SIDE.
- The contract forwards only WETH.
Attack walkthrough#
- Build an offer of
[WETH 1, SIDE 500]. fulfillOrdervalidates the WETH leg and does not check the length.- The loop pulls both tokens.
- 1 WETH is sent to the buyer.
- The financing contract still holds 500 SIDE.
Control: offer.length == 1 fails, and the seller still holds 500 SIDE.
Diagrams#
Impact#
The seller loses 500 SIDE. The buyer receives the 1 WETH they were quoted. The SIDE balance stays in seller financing.
Remediation#
In _validateSaleOrder, require order.parameters.offer.length == 1 so the only pulled token is the WETH leg.
Sources#
Taxonomy#
severity/highvuln/dos/frozen-funds·vuln/input-validation/missing
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 60495-locked-funds-when-fulfilling-seaport-orders-quantstamp-nifty_exp (evm-hack-registry mirror).
- AuditVault finding: 60495-locked-funds-when-fulfilling-seaport-orders-quantstamp-nifty.
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.