No token-level loss is recorded for this incident.
No token-level loss detail has been recorded for this incident yet.
0x23b69bef57656f493548a5373300f7557777f352ade8131353ff87a1b27e2bb30x204b937feaec333e9e6d72d35f1d131f187ecea1EthereumOn Ethereum block 23260641, transaction 0x23b69bef57656f493548a5373300f7557777f352ade8131353ff87a1b27e2bb3 exploited two years-old active ETH-escrow HEXOTC orders at 0x204b937feaec333e9e6d72d35f1d131f187ecea1. The adversary sourced HEX from the public Uniswap V3 HEX/WETH pool, filled offers 67 and 43 atomically through take(bytes32), and exited with at least 0.122278953515569393 ETH net of gas plus 445.61624407 residual HEX.
The root cause was not broken accounting inside HEXOTC. The market intentionally leaves ETH-escrow orders active until manual cancellation or fill, and its buyETH settlement path checks only order activity, escrow type, and fixed token amounts. Once market conditions moved far enough, those stale bids became permissionlessly arbitrageable by any account able to acquire the required HEX on-chain.
HEXOTC is an on-chain OTC market for HEX. For escrowType == 1, the maker escrows ETH in the contract and specifies a fixed amount of HEX the taker must deliver in exchange for that ETH. The relevant pre-state at block 23260640 contained two still-active ETH-escrow offers:
67: 0.06942 ETH for 6,942,000,000,000 raw HEX, maker 0x68CBc12a70A14f055110dDBEc73A7F0F5551ffDA43: 0.09 ETH for 2,250,000,000,000 raw HEX, maker 0xFeDc84d0cd5FE6dB2B2f8aC31c7e31e49B665e5cThose offers were created in 2021 and 2020 respectively and remained active because HEXOTC defines activity only by a nonzero stored timestamp. The public Uniswap V3 pool at 0x9e0905249ceefffb9605e034b534544684a58be6 had enough liquidity for a 0.037 WETH exact-input swap to source the needed HEX in the same transaction.
This was an ACT MEV stale-order arbitrage. Verified HEXOTC source shows that an offer is active while offers[id].timestamp > 0, so age alone never invalidates an order. For ETH-escrow orders, buyETH(uint id) requires only that the order is active, escrowType == 1, the caller has enough HEX, and the stored amounts are nonzero. After that, the contract transfers the fixed HEX amount from taker to maker and immediately transfers the fixed escrowed ETH to the taker. take(bytes32) is a public wrapper that routes zero-ETH calls into buyETH, so any unprivileged caller can execute the settlement path. The exploitable predicate is therefore simple: if an old active order pays out more ETH than it costs to buy the required HEX on public markets, any searcher can fill it for profit.
Verified victim code:
function isActive(uint id) public view returns (bool active) {
return offers[id].timestamp > 0;
}
function buyETH(uint id)
public
can_buy(id)
synchronized
returns (bool)
{
OfferInfo memory offer = offers[id];
require(offer.escrowType == 1, "Incorrect escrow type");
require(hexInterface.balanceOf(msg.sender) >= offer.buy_amt, "Balance is less than requested spend amount");
require(offer.buy_amt > 0 && offer.pay_amt > 0, "values are zero");
require(hexInterface.transferFrom(msg.sender, offer.owner, offer.buy_amt), "Transfer failed");
msg.sender.transfer(offer.pay_amt);
delete offers[id];
return true;
}
function take(bytes32 id) public payable {
if (msg.value > 0) {
require(buyHEX(uint256(id)), "Buy HEX failed");
} else {
require(buyETH(uint256(id)), "Sell HEX failed");
}
}
The critical precondition was observable on-chain before the exploit transaction landed. Auditor RPC observations at block 23260640 show offer 67 with pay_amt = 69420000000000000, buy_amt = 6942000000000, owner 0x68CB...ffDA, and escrowType = 1; offer 43 had pay_amt = 90000000000000000, buy_amt = 2250000000000, owner 0xFeDc...5e5c, and escrowType = 1. Both had nonzero timestamps, so isActive returned true for each.
Because HEXOTC never checks order freshness, expiry, or any market reference, the contract would still release escrowed ETH at those original fixed prices years later. That means the full safety condition reduces to an external pricing inequality: the taker's cost to source 9,192,000,000,000 raw HEX must be less than 0.15942 ETH. The public Uniswap V3 pool satisfied that condition at the pre-state block.
The seed transaction proves the opportunity was realizable by an unprivileged actor in one transaction. The trace shows the helper wrapped 0.037 ETH into WETH, swapped it on the HEX/WETH pool, approved HEXOTC, and called take twice:
HEX_WETH_V3_POOL::swap(..., 37000000000000000, ...)
-> HEX::transfer(..., 9236561624407)
HEXOTC::take(0x...0043)
-> HEX::transferFrom(..., 0x68CBc12a70A14f055110dDBEc73A7F0F5551ffDA, 6942000000000)
-> attacker::receive{value: 69420000000000000}()
HEXOTC::take(0x...002b)
-> HEX::transferFrom(..., 0xFeDc84d0cd5FE6dB2B2f8aC31c7e31e49B665e5c, 2250000000000)
-> attacker::receive{value: 90000000000000000}()
Balance deltas close the loop. The sender EOA finished 122278953515569393 wei higher after gas, the makers received exactly the fixed HEX amounts required by the offers, and the sender also retained 44561624407 raw HEX. That matches the strategy-level root cause: standing above-market ETH bids on a permissionless OTC contract remained fillable until someone bought enough HEX and took them.
The adversary transaction was fully end-to-end and self-funded:
0x07185a9e74f8dceb7d6487400e4009ff76d1af46 submitted the transaction and deployed helper contracts during execution.0.037 ETH, wrapped it into WETH, and swapped on the public Uniswap V3 HEX/WETH pool.9,236,561,624,407 raw HEX, enough to cover both offers.take(bytes32) for offer 67, sending 6,942,000,000,000 raw HEX to maker 0x68CB...ffDA and receiving 0.06942 ETH.take(bytes32) for offer 43, sending 2,250,000,000,000 raw HEX to maker 0xFeDc...5e5c and receiving 0.09 ETH.0.15942 ETH proceeds and the remaining 44,561,624,407 raw HEX back to the sender EOA.The transaction did not rely on privileged roles, private state, or attacker-specific protocol access. Its inputs were public order state, public pool liquidity, and ordinary transaction execution.
No internal protocol accounting invariant was violated. The impact was economic: stale makers left ETH-escrow bids standing far above public market price, so a taker could legally extract value from those orders. The observed realization transferred a total of 0.15942 ETH out of HEXOTC escrow in exchange for 9,192,000,000,000 raw HEX and produced at least 0.122278953515569393 ETH net profit after gas to the adversary sender, plus 445.61624407 HEX that was not needed to satisfy the two orders.
Because this is a stale-order MEV event rather than a protocol insolvency event, the measurable protocol-side loss is better described as permissionless extraction from underpriced maker quotes than as broken treasury accounting.
0x23b69bef57656f493548a5373300f7557777f352ade8131353ff87a1b27e2bb30x204b937feaec333e9e6d72d35f1d131f187ecea10x9e0905249ceefffb9605e034b534544684a58be667: 0x68CBc12a70A14f055110dDBEc73A7F0F5551ffDA43: 0xFeDc84d0cd5FE6dB2B2f8aC31c7e31e49B665e5chttps://etherscan.io/address/0x204b937feaec333e9e6d72d35f1d131f187ecea1#code/workspace/session/artifacts