This is a lower bound: only assets with reliable historical USD prices are counted, so the actual loss may be higher.
0xC36A7887786389405eA8DA0B87602Ae3902B88A1Ethereum0x7164be9FD69F2E1dE9b6B75B17E1B86268F18B45Ethereum0xBD5ca837C759B429398dc55b643f1Dd8d0D72CbDPolygon0x3826367A5563eCE9C164eFf9701146d96cC70AD9PolygonMonoX was exploited on Ethereum and Polygon because the deployed Monoswap implementation allowed an unprivileged caller to redeem other users' MONO LP positions and also allowed tokenIn == tokenOut swaps on the same MONO pool. In Ethereum transaction 0x9f14d093a2349de08f02fc0fb018dadb449351d0cdb7d0738ff69cc6fef5f299 and Polygon transaction 0x5a03b9c03eedcb9ec6e70c6841eaa4976a732d050a6218969e39483bb3004d5d, the adversary first acquired MONO, then forced third-party LP redemptions that emptied the MONO reserve, reseeded the pool with dust liquidity, inflated the quoted MONO price through MONO-to-MONO self-swaps, and finally exchanged small MONO amounts for real assets from unrelated MonoX pools.
The root cause is a compound protocol bug. First, removeLiquidity authenticates timing restrictions against msg.sender but burns LP from the arbitrary to address. Second, the deployed swap path accepts same-token trades, so the attacker can recycle MONO through the sell-side and buy-side accounting path after the reserve has been reduced to dust. Those two defects disconnect price from reserve backing and let the attacker drain real assets from the rest of the protocol.
MonoX used single-token pools. Each pool tracked a token reserve plus accounting fields such as vcashCredit, vcashDebt, lastPoolValue, and price. LP positions were held in the separate MonoXPool ERC1155 contract, keyed by pool id rather than by ERC20 LP shares.
The critical operational split is that Monoswap decides whether a liquidity withdrawal is allowed, but MonoXPool holds the LP balances and the pool reserves. A correct design therefore requires Monoswap to validate and burn the same account's LP balance. If authorization checks are done on one account while the burn and reserve transfer are done on another account, any caller can spend someone else's LP position.
Swap pricing is also pool-local. For distinct assets, the accounting path treats one pool as the seller and a different pool as the buyer. If tokenIn == tokenOut, that assumption collapses: the same pool is updated as both sides of the trade, so a dust reserve can be repriced upward without any real cross-asset market constraint.
The incident is an ATTACK, not pure MEV. MonoX's deployed logic violated two basic invariants. First, only the owner of an LP position should be able to burn that LP position and withdraw reserve assets. Second, a same-token swap should never be allowed to mutate a pool as both the input and output side of the same pricing path. MonoX violated both conditions in production.
The Monoswap source shows the authorization/accounting mismatch directly. In _removeLiquidity, the cooldown and top-holder checks are evaluated against the user argument, which is passed as msg.sender, but the redeemable LP balance is taken from the arbitrary to account:
function _removeLiquidity(address user, address _token, uint256 liquidity, address to)
view public returns (uint256 poolValue, uint256 liquidityIn, uint256 vcashOut, uint256 tokenOut)
{
uint256 lastAdded = monoXPool.liquidityLastAddedOf(pool.pid, user);
...
liquidityIn = monoXPool.balanceOf(to, pool.pid) > liquidity
? liquidity
: monoXPool.balanceOf(to, pool.pid);
}
That means an attacker with no LP balance can call removeLiquidity(MONO, liquidity, holder, 0, 1) and cause the protocol to burn holder's LP position as long as the attacker's own cooldown checks pass.
The second bug is confirmed by the post-incident patch. Commit 51b4ed386bce56a3f93e25037c54e13bdd410518 added the missing same-token guard in both swap directions:
function swapIn(...) public onlyRouter lockToken(tokenIn) lock returns (uint256 amountOut) {
require(tokenIn != tokenOut, "MonoX:SAME_SWAP_TOKEN");
...
}
function swapOut(...) public onlyRouter lockToken(tokenIn) lock returns (uint256 amountIn) {
require(tokenIn != tokenOut, "MonoX:SAME_SWAP_TOKEN");
...
}
Because the deployed exploit transactions predate that patch, the attacker could repeatedly self-swap MONO on the now-empty pool and inflate the quoted MONO price enough to buy out unrelated pools.
The Polygon trace shows the unauthorized LP redemption stage explicitly. The exploit contract 0x119914de3ae03256fd58b66cd6b8c6a12c70cfb2 calls removeLiquidity while MonoXPool checks liquidityLastAddedOf for the attacker address and then reads the victim LP balance for the to address:
Monoswap::removeLiquidity(MonoToken, 231902004155196702, 0x44b8...33d9, 0, 1)
MonoXPool::liquidityLastAddedOf(75, 0x119914De3AE03256fd58B66Cd6b8c6A12c70Cfb2) -> 0
MonoXPool::topLPHolderOf(75) -> 0x846684d5db5A149bAb306FeeE123a268a9E8A7E4
MonoXPool::balanceOf(0x44b8b893A7eB7217195bd925c5a04eC5CbA333d9, 75) -> 231902004155196702
This pattern repeats 70 times in the Polygon exploit transaction. The balance diff confirms the effect on reserve state: the Polygon MonoXPool address 0x3826367A5563eCE9C164eFf9701146d96cC70AD9 loses 41,613,716,810,938,516,746,254 MONO units in that transaction, while the individual third-party LP holders receive MONO balances even though they did not initiate the calls.
After those forced redemptions, the attacker reseeds the MONO pool with only 196875656 wei of MONO and begins the self-swap stage. The same trace then records repeated swapExactTokenForToken(MONO, MONO, ...) calls such as:
Monoswap::swapExactTokenForToken(MonoToken, MonoToken, 196875655, 0, 0x119914..., 1638277812)
Monoswap::swapExactTokenForToken(MonoToken, MonoToken, 314044864, 0, 0x119914..., 1638277812)
Monoswap::swapExactTokenForToken(MonoToken, MonoToken, 500946532, 0, 0x119914..., 1638277812)
Because the reserve has already been reduced to dust, these self-swaps move the quoted MONO price far above any economically backed level. The root cause artifact records the Polygon MONO price rising from 5.252677388528094210e18 to 3.1804433874844068910990439044e28 during this loop.
Once MONO is overpriced, MonoX's cross-pool pricing treats a small MONO input as sufficient to withdraw large balances from unrelated pools. The Polygon trace then shows the protocol sending real assets out of MonoXPool, including a USDC transfer to the exploit contract:
MonoXPool::safeTransferERC20Token(USDC, 0x119914De3AE03256fd58B66Cd6b8c6A12c70Cfb2, 4212223956763)
emit Transfer(from: 0x3826367A5563eCE9C164eFf9701146d96cC70AD9, to: 0x119914De3AE03256fd58B66Cd6b8c6A12c70Cfb2, value: 4212223956763)
The same transaction also transfers USDT, WETH, WBTC, DAI, and GHST, and the Ethereum seed transaction shows the same pattern ending with MIM and IMX at recipient 0x8f6a86f3ab015f4d03ddb13abb02710e6d7ab31b. The exploit is therefore fully explained by the combination of unauthorized LP burning and same-token pool repricing.
The adversary used the same end-to-end playbook on both Ethereum and Polygon.
First, the attacker-funded exploit contract bought an initial MONO position using a real asset already liquid on the chain: WETH on Ethereum and WMATIC on Polygon. This created enough MONO inventory to interact with the protocol's MONO pool.
Second, the exploit contract called removeLiquidity repeatedly with third-party LP holder addresses as the to parameter. Because MonoX validated timing rules against the attacker but burned the LP balances belonging to those third parties, the attacker emptied the MONO reserve without ever holding the LP positions being redeemed.
Third, after the reserve was cleared, the attacker added only dust MONO liquidity back into the MONO pool. That set up the pool for extreme price sensitivity.
Fourth, the exploit contract performed repeated MONO-to-MONO self-swaps. With no same-token guard in the deployed implementation, those calls mutated the same pool as both the input and output side of the pricing path and drove the MONO quote upward by many orders of magnitude.
Finally, the attacker used the manipulated MONO quote to buy real assets out of other MonoX pools. On Polygon, the trace and balance diffs show withdrawals of USDC, USDT, WETH, WBTC, DAI, GHST, and WMATIC-related value. On Ethereum, the balance diff shows MIM and IMX extracted. The flows end at attacker-controlled addresses, while MonoX's pool reserves are depleted.
The exploit broke MonoX's solvency across multiple pools on two chains. The protocol accepted a MONO price derived from unauthorized LP redemption and self-trading as valid collateral for withdrawals from unrelated pools. That caused direct losses of real assets held by MonoXPool rather than merely notional price distortion.
The collected balance diffs support the following losses:
4,125,858,256,772,000,000,000 MIM units and 274,939,044,087,758,284,822 IMX units.4,212,223,956,763 USDC units, 4,568,728,922,634 USDT units, 1,721,831,002,626,163,842,878 WETH units, 1,475,269,784 WBTC units, 1,207,179,654,640,953,488,511 DAI units, and 3,178,613,994,163,467,140,331 GHST units.The practical impact is broader than the listed token totals. The exploit demonstrates that once the MONO pool reserve could be cleared and self-repriced, any MonoX pool still holding value became reachable through the same cross-pool quote distortion.
0x9f14d093a2349de08f02fc0fb018dadb449351d0cdb7d0738ff69cc6fef5f2990x5a03b9c03eedcb9ec6e70c6841eaa4976a732d050a6218969e39483bb3004d5dartifacts/collector/seed/137/0x5a03b9c03eedcb9ec6e70c6841eaa4976a732d050a6218969e39483bb3004d5d/metadata.json and trace.cast.logartifacts/collector/seed/1/0x9f14d093a2349de08f02fc0fb018dadb449351d0cdb7d0738ff69cc6fef5f299/trace.cast.log and balance_diff.jsonhttps://etherscan.io/address/0x66e7d7839333f502df355f5bd87aea24bac2ee63#code and https://polygonscan.com/address/0x70381e29559890a5559defb5a4b4690ce1eacb57#codehttps://github.com/blueocean0724/MonoX-finance-platform/commit/51b4ed386bce56a3f93e25037c54e13bdd410518