We do not have a reliable USD price for the recorded assets yet.
0xb13b2ab202cb902b8986cbd430d7227bf3ddca831b79786af145ccb5f00fcf3f0x03339ecae41bc162dacae5c2a275c8f64d6c80a0Arbitrum0x61E66F0D08F64681d891D2B7E03fa3304d7b68d2ArbitrumAt Arbitrum block 373990723, EOA 0xAA06FDE501a82cE1C0365273684247a736885dAf used helper contract 0x2faD746CfaAF68AA098f704fB6537b0a05786Df8 to borrow 884760000 WBTC units, buy 60000 EVA from a live resting sell order on OrderBookFactory 0x03339ecae41bc162dacae5c2a275c8f64d6c80a0, dump the EVA into public Uniswap V3 and Pancake V3 pools, repay the flash loan, and keep 119331045 WBTC units of profit. The root cause was not broken protocol accounting or missing access control. The opportunity existed because OrderBookFactory intentionally allows permissionless matching of resting orders at the maker's posted price, and maker 0x61E66F0D08F64681d891D2B7E03fa3304d7b68d2 left a stale EVA sell order priced below contemporaneous public pool bids.
OrderBookFactory exposes addNewOrder(bytes32,uint256,uint256,bool,uint256) as a public entrypoint for any taker or maker on an enabled pair. For the EVA/WBTC market, the pair id is 0x3e0eda1b16003a6bbf05702d0b0474c698229478dc3cf66aa0f56dcb3d4df98f, the pair was enabled, and the trading fee was zero in the pre-state.
The matching logic lives in PairLib.createOrder. A new buy order starts from the current lowest sell price and matches as long as the taker's limit price is at least that price. Settlement uses the resting maker price rather than an oracle or external market snapshot:
bool shouldMatch = isBuy ? newOrder.price >= currentPricePoint : newOrder.price <= currentPricePoint;
The relevant pre-state contained maker order 0x5f10c77ae865c7d18861f527c1cc9ccb6f526303eef674cf7871a83ee3aff3ce: a live sell order for 100000 EVA at price 14746, fully funded inside the order book contract.
The adversary used helper contract 0x2faD746CfaAF68AA098f704fB6537b0a05786Df8 only as an orchestrator. Bytecode-level analysis shows that its dispatcher includes exploit entrypoint selector 0xe3f2be84, stores the pair id, price, and quantity from calldata, invokes OrderBookFactory.addNewOrder inside onMorphoFlashLoan(uint256,bytes), and routes the acquired EVA through Uniswap V3 and Pancake V3 before forwarding profit to EOA 0xAA06FDE501a82cE1C0365273684247a736885dAf.
This incident is a pure MEV stale-order capture. OrderBookFactory behaved as designed: it exposed a permissionless order book, accepted a taker buy order on an enabled pair, and settled against the live resting maker quote at the maker's stored price. PairLib's matching rule is the decisive code-level breakpoint because any taker can consume the lowest sell order whenever newOrder.price >= currentPricePoint. No privileged access, compromised keys, or accounting bug was required. The exploitable condition was purely economic: the maker's resting EVA ask at price 14746 had drifted below bids available in public WBTC liquidity pools. Once that condition existed on public state, any unprivileged actor with enough temporary WBTC liquidity could buy EVA from the order book, sell it into the pools, and keep the spread. The helper contract automated this public path, but it did not create the opportunity.
The verified victim code makes the public taker path explicit. OrderBookFactory.addNewOrder is external and only checks that the pair is enabled before forwarding into PairLib.addBuyOrder or addSellOrder:
function addNewOrder(bytes32 _pairId, uint256 _quantity, uint256 _price, bool _isBuy, uint256 _timestamp)
external
onlyEnabledPair(_pairId)
nonReentrant
whenNotPaused
{
if (_isBuy) {
pairs[_pairId].addBuyOrder(_price, _quantity, _timestamp);
} else {
pairs[_pairId].addSellOrder(_price, _quantity, _timestamp);
}
}
Inside PairLib.createOrder, a buy order begins from pair.sellOrders.getLowestPrice() and continues matching while the taker price is at least the resting sell price. fillOrder and partiallyFillOrder transfer inventory at matchedOrder.price, which means the order book honors the stale maker quote even if public pool pricing has moved away from it.
The seed trace for transaction 0xb13b2ab202cb902b8986cbd430d7227bf3ddca831b79786af145ccb5f00fcf3f shows the exact state transition:
0x03339ECAE41bc162DAcAe5c2A275C8f64D6c80A0::addNewOrder(
0x3e0eda1b16003a6bbf05702d0b0474c698229478dc3cf66aa0f56dcb3d4df98f,
60000000000000000000000,
15000,
true,
3244234234234234
)
...
StandardArbERC20::transferFrom(
0x2faD746CfaAF68AA098f704fB6537b0a05786Df8,
0x03339ECAE41bc162DAcAe5c2A275C8f64D6c80A0,
884760000
)
...
0x45D9831d8751B2325f3DBf48db748723726e1C8c::transfer(
0x2faD746CfaAF68AA098f704fB6537b0a05786Df8,
60000000000000000000000
)
The same trace then shows public pool exits through Uniswap V3 and Pancake V3, each for 30000 EVA, followed by profit realization and transfer:
0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45::exactInputSingle(... amountIn = 30000000000000000000000 ...)
0x1b81D678ffb9C0263b24A97847620C99d213eB14::exactInputSingle(... amountIn = 30000000000000000000000 ...)
...
← [Return] 119331045
StandardArbERC20::transfer(0xAA06FDE501a82cE1C0365273684247a736885dAf, 119331045)
The balance-diff artifact corroborates the economic result: the order book lost 60000000000000000000000 EVA and gained 884760000 WBTC units, while adversary EOA 0xAA06... gained 119331045 WBTC units after the transaction. This is the deterministic ACT predicate.
The adversary flow had three on-chain stages. First, helper contract 0x2faD... borrowed 884760000 WBTC units from flash lender 0x6c247b1f6182318877311737bac0844baa518f5e. Second, inside the flash-loan callback, the helper approved the order book and submitted a buy order for 60000 EVA with max price 15000, which matched the maker's stale sell order priced at 14746. Third, the helper sold 30000 EVA through Uniswap V3 pool 0x42a475... and 30000 EVA through Pancake V3 pool 0x57df94..., repaid the lender principal, and transferred the residual 119331045 WBTC units to the controlling EOA.
The helper bytecode analysis is consistent with the trace. It shows selector 0xe3f2be84 storing the token, order book, pair id, price, and quantity into storage, and later onMorphoFlashLoan(uint256,bytes) loading those values to call addNewOrder(bytes32,uint256,uint256,bool,uint256), then invoking the two V3 routers. The helper therefore did not rely on private attacker-side artifacts beyond ordinary orchestration logic.
The direct loser was maker 0x61E66F0D08F64681d891D2B7E03fa3304d7b68d2, whose order sold 60000 EVA at the stale resting price. The measurable asset loss was:
{"token_symbol":"EVA","amount":"60000000000000000000000","decimal":18}
In exchange, the maker received only 884760000 WBTC units through the order book, while the same EVA was immediately sold into public pools for 1004091045 WBTC units. The adversary captured the difference, 119331045 WBTC units, as deterministic stale-order arbitrage profit.
0xb13b2ab202cb902b8986cbd430d7227bf3ddca831b79786af145ccb5f00fcf3f0xb13b2ab202cb902b8986cbd430d7227bf3ddca831b79786af145ccb5f00fcf3f0xb13b2ab202cb902b8986cbd430d7227bf3ddca831b79786af145ccb5f00fcf3fOrderBookFactory.sol for 0x03339ecae41bc162dacae5c2a275c8f64d6c80a0PairLib.sol for 0x03339ecae41bc162dacae5c2a275c8f64d6c80a00x2faD746CfaAF68AA098f704fB6537b0a05786Df8