Reproduced Exploit
OriginToken migration leaves V00 Marketplace escrow on the paused token
Loss
A 10 OGN listing deposit and a 20 OGN offer (30 OGN total) remain escrowed in the Marketplace after migration…
Chain
Ethereum
Category
dependency
Date
Jan 2022
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: 17100-origintoken-contract-migration-breaks-marketplace-ofer-refer. Standalone Foundry PoC and full write-up: 17100-origin-token-migration-marketplace-reference_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/dependency/upgradeable-contract · vuln/logic/incorrect-state-transition · vuln/dos/lockup
Reproduction: the test deploys the audited
OriginToken,TokenMigration, andV00_Marketplacesources unmodified. It escrows a listing deposit and an accepted offer, runs the real token migration (mint replacement OGN + pause the old token), and then proves that bothfinalize()andwithdrawListing()revert, leaving all escrow permanently locked. The passing trace is in output.txt.
Key info#
| Field | Value |
|---|---|
| Loss | A 10 OGN listing deposit and a 20 OGN offer (30 OGN total) remain escrowed in the Marketplace after migration, but neither can be settled nor withdrawn. |
| Vulnerable contract | V00_Marketplace — src/contracts/marketplace/v00/Marketplace.sol, specifically tokenAddr, Offer.currency, and finalize/paySeller/withdrawListing. |
| Migration contracts | Exact OriginToken and TokenMigration sources from Origin's repository. |
| Attack transaction | PoC_17100.test_migrationLeavesMarketplacePointingAtPausedToken() |
| Chain / block / date | Ethereum-compatible execution · local Foundry (empty genesis, no fork) · 2022-01 report |
| Compiler | Solidity ^0.4.24 (compiled with solc 0.4.26; the audited source pragma) |
| Bug class | Migration pauses the old token, but the Marketplace has no migration hook for its token reference (tokenAddr) or existing offer currencies (Offer.currency). |
Real source and audited commit#
The vendored contracts are byte-for-byte the audited Origin source (only the
relative import path depths were adjusted, and the local marketplace ERC20
opaque-token interface was renamed to IERC20 in the Playground synthetic
only, to deconflict with OpenZeppelin's ERC20 in a single-file build):
- Repo: https://github.com/OriginProtocol/origin
- Commit:
981e580fa3ba9325e10eb0608fe6aeb4605e7a23(pathorigin-contracts/contracts/) — the pre-#1422version reviewed by Trail of Bits, before therequire(fromToken.paused())migration guard was added. - V00 Marketplace — identical to the commit.
- OriginToken — identical (comment header aside).
- TokenMigration — identical to the commit.
- WhitelistedPausableToken — identical.
The OpenZeppelin-solidity 1.10.0 dependency required by those files is in
node_modules/openzeppelin-solidity.
Vulnerable code path#
The Marketplace records the OGN token address once in its constructor and never lets it change except through an owner-only setter:
IERC20 public tokenAddr; // Origin Token address
constructor(address _tokenAddr) public {
owner = msg.sender;
setTokenAddr(_tokenAddr); // Origin Token contract
}
Every ERC20 offer independently caches the currency address the buyer supplied:
offers[listingID].push(Offer({ ..., currency: _currency, value: _value, ... }));
finalize -> paySeller then settles through that stored currency:
function paySeller(uint listingID, uint offerID) private {
...
require(offer.currency.transfer(offer.buyer, offer.refund), "Refund failed");
require(offer.currency.transfer(listing.seller, value), "Transfer failed");
}
After the migration offer.currency / tokenAddr are still the paused old
OriginToken, whose transfer reverts (whenNotPaused -> require(!paused),
no reason string in OZ 1.10.0). withdrawListing reverts for the same reason,
so the deposit cannot be recovered either.
Reproduction walkthrough (with numbers)#
- Deploy the real
OriginTokenwith a 30 OGN supply and the realV00_Marketplacepointing at it. - Create a listing with a 10 OGN deposit and an accepted 20 OGN offer denominated in the old token. The Marketplace now escrows 30 OGN.
- Deploy the real
TokenMigration, hand it the new token's minting ownership,pause()the old token, andmigrateAccount(marketplace). The new token mints 30 OGN to the Marketplace, whilemarketplace.tokenAddr()still returns the old token address. finalize(0, 0, ...)reverts —paySellercallsoldToken.transfer, which is paused.withdrawListing(0, ...)reverts identically. The test asserts the old-token escrow (30 OGN), the un-spendable replacement balance (30 OGN), and the listing deposit (10 OGN) all remain in the Marketplace.
Impact and remediation#
Existing listings and offers can neither be finalized nor withdrawn after migration. Un-pausing the old token would settle against a token that is no longer the active OGN contract, while the newly minted balance is ignored. Migration must update the Marketplace token reference and every outstanding offer atomically, or provide a validated escrow conversion/refund path before pausing the old token.
How to reproduce#
_shared/run-poc/run_poc.sh 17100-origin-token-migration-marketplace-reference_exp -vvvvv
# or, from the folder:
cd evm-hack-registry/17100-origin-token-migration-marketplace-reference_exp
forge test -vvvvv
Sources#
- AuditVault finding #17100
- Trail of Bits Origin review
- Origin source repository @
981e580fa3ba9325e10eb0608fe6aeb4605e7a23 - Real-source Forge test
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 17100-origin-token-migration-marketplace-reference_exp (evm-hack-registry mirror).
- AuditVault finding: 17100-origintoken-contract-migration-breaks-marketplace-ofer-refer.
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.