MonoX Monoswap MonoToken Self-Price-Pump VCASH Exploit
Exploit Transactions
0x9f14d093a2349de08f02fc0fb018dadb449351d0cdb7d0738ff69cc6fef5f299Victim Addresses
0x59653e37f8c491c3be36e5dd4d503ca32b5ab2f4Ethereum0xc36a7887786389405ea8da0b87602ae3902b88a1EthereumLoss Breakdown
Similar Incidents
WETH Drain via Unprotected 0xfa461e33 Callback on 0x03f9-62c0
29%CauldronV4 solvency-check bypass enables uncollateralized MIM borrowing
29%GPv2Settlement allowance leak lets router drain WETH and USDC
28%PumpToken removeLiquidityWhenKIncreases Uniswap LP Drain
28%WBTC Drain via Insecure Router transferFrom Path
28%SBR reserve desynchronization exploit drains WETH from UniswapV2 pair
28%Root Cause Analysis
MonoX Monoswap MonoToken Self-Price-Pump VCASH Exploit
1. Incident Overview TL;DR
On Ethereum mainnet block 13,715,026 (2021‑11‑30), an attacker exploited the MonoX Monoswap controller to artificially inflate the internal price and VCASH-based pool value of MonoToken in a single-sided pool, then used the overvalued MonoToken to drain Magic Internet Money (MIM), IMX, and other assets from the MonoXPool proxy. The exploit was executed in a single attacker-crafted transaction, 0x9f14d093a2349de08f02fc0fb018dadb449351d0cdb7d0738ff69cc6fef5f299, sent from unprivileged EOA 0xecbe385f78041895c311070f344b55bfaa953258 to attacker-deployed orchestrator contract 0xf079d7911c13369e7fd85607970036d2883afcfd. Internally, Monoswap allowed self-swaps where tokenIn == tokenOut == MonoToken, and its VCASH accounting invariant only enforced non-decreasing pool value, not conservation of value when the same pool appears on both sides of a trade. By chaining a series of MonoToken self-swaps to ratchet up the stored price, then swapping MonoToken into other tokens, the attacker converted 0.1 WETH plus gas into a portfolio including at least 4,125.858256772 MIM and 274.939044087758284822 IMX, with losses borne by the MonoXPool proxy and its LPs.
2. Key Background
MonoX / Monoswap is an AMM-style protocol that supports single-sided pools. For each listed token T, the Monoswap controller maintains a Pool entry in a mapping pools[T] that tracks tokenBalance, price, vcashCredit, vcashDebt, status, and lastTradedBlock. Trades are modeled as token <-> VCASH <-> token flows: a tokenIn -> tokenOut swap first values tokenIn against VCASH, then uses that VCASH value to determine how much tokenOut to give, updating pools[tokenIn] and pools[tokenOut] accordingly. The core liquidity is held by a separate MonoXPool contract, deployed at implementation address 0x7164be9fd69f2e1de9b6b75b17e1b86268f18b45 behind proxy 0x59653e37f8c491c3be36e5dd4d503ca32b5ab2f4; the Monoswap controller 0xc36a7887786389405ea8da0b87602ae3902b88a1 acts as owner of MonoXPool and can instruct it to move ERC20 tokens.
MonoXPool is a simple ERC1155-based LP token contract with an admin/owner model. Relevant to this exploit, it exposes a generic ERC20 transfer helper restricted to its owner:
// MonoXPool.sol (implementation for 0x7164be9f..., owner is Monoswap controller)
function safeTransferERC20Token(address token, address to, uint256 amount) external onlyOwner{
IERC20(token).safeTransfer(to, amount);
}
Because the Monoswap controller is the owner, any successful Monoswap swap that decides some amount of tokenOut should be delivered to a user ultimately results in MonoXPool::safeTransferERC20Token moving ERC20 balances from the pool proxy 0x59653e37... to arbitrary user addresses.
In the exploited configuration, MonoToken (0x2920f7d6134f4669343e70122ca9b8f19ef8fa5d) was a listed pool token with a substantial tokenBalance and an existing price in pools[MonoToken]. The MonoXPool proxy also held significant balances of MIM (0x99d8a9c45b2eca8864373a26d1459e3dff1e17f3), IMX (0xf57e7e7c23978c3caec3c3548e3d615c346e79ff), USDC, WBTC, WETH, and other tokens that could be purchased with MonoToken at the stored internal price.
3. Vulnerability Analysis & Root Cause Summary
The vulnerability is an AMM accounting bug in Monoswap’s internal VCASH-based pricing logic that fails to enforce that tokenIn and tokenOut refer to distinct pools. The controller intends to maintain a value-based invariant for each Pool entry T:
- For each listed token T, define poolValue(T) = tokenBalance(T) * price(T) / 1e18 + vcashCredit(T) – vcashDebt(T).
- For normal trades and liquidity operations, poolValue(T) should not increase unless new net assets or VCASH are added.
However, Monoswap’s swapExactTokenForToken and swapTokenForExactToken functions accept arbitrary _tokenIn and _tokenOut and forward them to getAmountOut/getAmountIn without enforcing _tokenIn != _tokenOut. When tokenIn == tokenOut == MonoToken, the controller:
- Computes a tradeVcashValue by “selling” MonoToken into VCASH, then immediately “buys” MonoToken back using the same tradeVcashValue.
- Calls _updateTokenInfo twice on pools[MonoToken]—once treating MonoToken as the selling side and once as the buying side—while synchronizing tokenBalance to the actual MonoToken.balanceOf(monoXPool) on each call.
- Only checks that poolValue_new >= poolValue_old (or a minimum) for the LISTED MonoToken pool, not that the combined effect of the two updates preserves overall value when they apply to the same pool.
This logic allows carefully chosen self-swaps to monotonically increase the stored price and poolValue for MonoToken without the attacker injecting any external collateral beyond transient MonoToken movements. Once pools[MonoToken].price has been inflated enough, ordinary swaps with tokenIn = MonoToken and tokenOut in {MIM, IMX, WBTC, WETH, USDC, etc.} can be executed at the manipulated price, draining those assets from the MonoXPool proxy while the controller and pool accounting still appear consistent with their flawed invariant.
4. Detailed Root Cause Analysis
Monoswap’s internal state for each token is held in a Pool struct, conceptually:
- tokenBalance: the token’s on-chain balance held by MonoXPool.
- price: the internal price used to translate between token units and VCASH.
- vcashCredit / vcashDebt: net VCASH position of the pool.
- status / lastTradedBlock: listing and activity metadata.
The helper function _updateTokenInfo(token, price, _vcashIn, _vcashOut, _ETHDebt) enforces a per-token monotonicity condition:
- It computes an initial pool value poolValue_old = tokenBalance_old * price_old / 1e18 + vcashCredit_old – vcashDebt_old.
- It synchronizes tokenBalance to IERC20(token).balanceOf(monoXPool), optionally adjusting for ETH debt.
- It sets pools[token].price = price and updates vcashCredit/vcashDebt via _updateVcashBalance(_vcashIn, _vcashOut).
- It recomputes poolValue_new with the updated tokenBalance, price, and VCASH fields.
- For LISTED pools it enforces poolValue_new >= max(poolValue_old, poolSizeMinLimit).
For a normal tokenIn -> tokenOut swap where tokenIn != tokenOut, Monoswap’s swap functions do the following:
- Compute tradeVcashValue representing the VCASH value of the trade based on tokenIn-side pool state.
- Derive amountOut or amountIn for tokenOut such that the value evolution of pools[tokenIn] and pools[tokenOut] across the trade respects the per-pool monotonicity constraint up to fees.
- Call _updateTokenInfo on pools[tokenIn] and pools[tokenOut] with appropriately signed VCASH flows to reflect the trade.
The protocol implicitly assumes that tokenIn and tokenOut refer to different Pool entries. The exploit arises because this assumption is not enforced.
In the seed transaction trace, after an initial swap of 0.1 WETH into MonoToken, the attacker performs a series of Monoswap self-swaps where tokenIn == tokenOut == MonoToken. A representative snippet from the cast trace (artifacts/root_cause/data_collector/iter_1/tx/1/0x9f14d0...f299/trace.cast.log) shows this pattern:
Monoswap::swapExactTokenForToken(
MonoToken: [0x2920f7d6134f4669343e70122cA9b8f19Ef8fa5D],
MonoToken: [0x2920f7d6134f4669343e70122cA9b8f19Ef8fa5D],
969575022640567 [9.695e14],
0,
0xf079d7911c13369E7fd85607970036D2883aFcfD,
1638278872
) [delegatecall]
...
MonoXPool::safeTransferERC20Token(MonoToken, 0xf079d7..., 392539033887820 [3.925e14]) [delegatecall]
Across the full sequence, the attacker repeatedly:
- Transfers MonoToken from 0xf079d7... to the MonoXPool proxy via transferFrom.
- Immediately receives a slightly different amount of MonoToken back via MonoXPool::safeTransferERC20Token.
- Leaves MonoXPool’s MonoToken balance nearly unchanged, so tokenBalance synchronized inside _updateTokenInfo remains close to its prior value.
Meanwhile, for each self-swap:
- getAmountOut/getAmountIn treat pools[MonoToken] as both the “sell” and “buy” side, computing new tokenInPrice and tokenOutPrice and a tradeVcashValue based on MonoToken’s own pool state.
- The controller calls _updateTokenInfo twice on MonoToken:
- Once with parameters that effectively reduce VCASH on the “selling” side (applying _vcashOut = tradeVcashValue + oneSideFeesInVcash, _vcashIn = 0).
- Once with parameters that increase VCASH on the “buying” side (applying _vcashIn = tradeVcashValue + oneSideFeesInVcash, _vcashOut = 0).
- Because tokenBalance is re-synchronized from MonoXPool’s actual MonoToken holdings on each call, and because the invariant only checks that poolValue_new >= poolValue_old (or >= poolSizeMinLimit) per call, this pair of updates is free to raise pools[MonoToken].price and poolValue across the sequence even though no new external collateral is added.
The root cause is therefore:
- Monoswap’s value-based invariant is defined per pool entry and enforces monotonicity but not conservation of value when the same pool appears on both sides of a trade.
- swapExactTokenForToken and swapTokenForExactToken do not enforce tokenIn != tokenOut, and getAmountOut/getAmountIn similarly accept tokenIn == tokenOut.
- Two successive calls to _updateTokenInfo on the same Pool entry with inconsistent VCASH flows allow an attacker to “write up” poolValue and price using self-trades that leave their net MonoToken position essentially unchanged.
After sufficiently inflating the stored MonoToken price via this self-swap loop, the attacker uses standard swaps with tokenIn = MonoToken and tokenOut in {MIM, IMX, WETH, WBTC, USDC, USDT, other tokens} to buy these assets from the MonoXPool proxy at the manipulated price. Each such swap results in a call to MonoXPool::safeTransferERC20Token(tokenOut, 0x8f6a86..., amountOut), moving real ERC20 balances (e.g., MIM and IMX) from the pool proxy to the profit EOA while Monoswap’s internal accounting still passes its flawed invariant check.
5. Adversary Flow Analysis
The adversary-related cluster accounts and their roles are:
- 0xecbe385f78041895c311070f344b55bfaa953258 (EOA): Attacker origin and funder.
- Deploys orchestrator contract 0xf079d7... in tx 0x3c25b170a1690602d994cdafad49805fa07e6309ab3a80ceccef0aed9f5271a7 (block 13,714,930).
- Sends the exploit transaction with 0.1 ETH to 0xf079d7... (tx 0x9f14d0...f299).
- 0xf079d7911c13369e7fd85607970036d2883afcfd (contract): Attacker-controlled orchestrator.
- Receives the 0.1 ETH call from 0xecbe38....
- Forwards calls into Monoswap controller 0xc36a78... and the MonoXPool proxy 0x59653e37..., executes MonoToken self-swaps, and routes proceeds to the profit EOA.
- 0x8f6a86f3ab015f4d03ddb13abb02710e6d7ab31b (EOA): Profit-recipient.
- Receives drained MIM and IMX directly from MonoXPool via Monoswap swaps during the exploit tx.
- Later receives ETH from 0xecbe38... and interacts with WETH9 to unwrap WETH.
The end-to-end execution flow for the exploit transaction 0x9f14d0...f299 is:
-
Funding and setup:
- Prior to block 13,715,026, 0xecbe38... holds ETH and deploys 0xf079d7... in block 13,714,930.
- The Monoswap/MonoX system is live with MonoToken, MIM, IMX, USDC, WBTC, WETH, and other tokens listed; MonoXPool proxy 0x59653e37... holds substantial balances of these tokens.
-
Entry:
- At block 13,715,026, 0xecbe38... sends a type-2 EIP‑1559 transaction with 0.1 ETH to 0xf079d7..., invoking methodId 0xa70cfccf with parameters that encode MonoToken, a set of target tokens, and a sequence of liquidity and swap operations (see artifacts/root_cause/seed/1/0x9f14d0...f299/metadata.json).
-
Initial WETH -> MonoToken swap:
- 0xf079d7... calls into controller 0xc36a78..., which in turn uses the MonoXPool proxy 0x59653e37... to swap 0.1 WETH (wrapped from the incoming ETH) into a large amount of MonoToken for the attacker path, via a standard Monoswap::swapExactTokenForToken(WETH9 -> MonoToken) call.
- MonoXPool::safeTransferERC20Token transfers MonoToken from the pool proxy to 0xf079d7..., and the pools[WETH] and pools[MonoToken] entries are updated accordingly.
-
MonoToken self-swap price pump:
- With a substantial MonoToken balance in 0xf079d7..., the orchestrator executes a series of self-swaps:
- Monoswap::swapExactTokenForToken(MonoToken, MonoToken, amountIn_i, 0, 0xf079d7..., deadline), where amountIn_i increases across the sequence.
- For each self-swap:
- MonoToken::transferFrom moves amountIn_i from 0xf079d7... to the MonoXPool proxy.
- MonoXPool::safeTransferERC20Token sends a slightly different amount of MonoToken back to 0xf079d7....
- The trace shows MonoToken.balanceOf(0x59653e37...) before and after each self-swap remaining close, so the net pool balance of MonoToken is almost unchanged, while the controller’s price and VCASH updates raise pools[MonoToken].price and poolValue.
- After many such iterations, MonoToken’s internal price is significantly higher than its economic value in external markets.
- With a substantial MonoToken balance in 0xf079d7..., the orchestrator executes a series of self-swaps:
-
Liquidity removal and VCASH minting:
- The transaction includes MonoXPool::burn and related calls that remove liquidity for the MonoToken pool on behalf of LP addresses 0x7b9aa6ed8b514c86ba819b99897b69b608293ffc and 0x81d98c8fda0410ee3e9d7586cb949cd19fa4cf38.
- VCASH (0x532d7ebe4556216490c9d03460214b58e4933454) is minted to these LPs and to admin address 0xab5167e8cc36a3a91fd2d75c6147140cd1837355, consistent with the pool’s accounting of increased value due to the manipulated price.
-
Draining other tokens using overvalued MonoToken:
- With the inflated MonoToken price in pools[MonoToken], the orchestrator swaps MonoToken into other tokens via calls like:
- Monoswap::swapTokenForExactToken(MonoToken -> USDC, ...)
- Monoswap::swapTokenForExactToken(MonoToken -> MIM, ...)
- Monoswap::swapTokenForExactToken(MonoToken -> IMX, ...)
- Similar swaps into WETH, WBTC, and other listed assets.
- Each swap results in MonoXPool::safeTransferERC20Token(tokenOut, 0x8f6a86..., amountOut), sending the purchased tokens directly from the pool proxy 0x59653e37... to the profit EOA.
- ERC20 balance diffs for tx 0x9f14d0...f299 (artifacts/root_cause/seed/1/0x9f14d0...f299/balance_diff.json) confirm:
- 0x59653e37... loses large amounts of MonoToken, USDC, MIM, IMX, and other tokens.
- 0x8f6a86... ends the transaction with +4,125.858256772 MIM and +274.939044087758284822 IMX, starting from zero balances for these tokens and sending none out.
- 0xecbe38...’s only net outflows are 0.1 WETH plus gas.
- With the inflated MonoToken price in pools[MonoToken], the orchestrator swaps MonoToken into other tokens via calls like:
-
Post-exploit consolidation:
- Subsequent transactions (e.g., from 0xecbe38... to 0x8f6a86... and from 0x8f6a86... to WETH9) consolidate ETH and potentially unwrap WETH, but they do not affect the root cause or ACT characterization of the single exploit transaction.
This flow is fully consistent with an anyone-can-take opportunity: all interactions with Monoswap and MonoXPool use public functions; 0xecbe38... and 0x8f6a86... are ordinary EOAs; and 0xf079d7... is a non-privileged orchestrator contract whose behavior any other adversary could reproduce by deploying its own variant and sending similar calldata from the same pre-state.
6. Impact & Losses
The primary victim is the MonoXPool proxy 0x59653e37f8c491c3be36e5dd4d503ca32b5ab2f4 on Ethereum mainnet, which holds pooled liquidity for the Monoswap protocol. From ERC20 balance diffs for tx 0x9f14d0...f299:
- 0x59653e37... loses:
- 101,816,394.026231575402022 MonoToken (approximate, from 101,844,459.211294874991464 down to 28,065,185.063299589442).
- 4,029,106.880396 USDC (FiatTokenV2_2).
- 4,125.858256772 MIM (MagicInternetMoneyV1).
- 274.939044087758284822 IMX (IMXToken).
- Additional losses in other tokens (WETH, WBTC, etc.) consistent with the draining swaps seen in the trace.
The main beneficiary is profit EOA 0x8f6a86f3ab015f4d03ddb13abb02710e6d7ab31b, which:
- Starts the transaction with zero MIM and IMX balances.
- Ends with:
- +4,125.858256772 MIM.
- +274.939044087758284822 IMX.
- Sends no MIM or IMX out during the transaction.
The only capital supplied by the adversary cluster is:
- 0.1 WETH (funded via the 0.1 ETH value from 0xecbe38... to 0xf079d7... and a WETH9 deposit).
- Gas fees for the exploit and follow-up consolidation transactions.
On-chain DEX pricing artifacts at block 13,715,026 (artifacts/root_cause/data_collector/iter_2/pricing/1/dex_price_snapshots_block_13715026.json and artifacts/root_cause/data_collector/iter_3/pricing/1/dex_price_snapshots_block_13715026_mim_imx.json) confirm that WBTC and WETH had nonzero market value and that MIM and IMX were actively traded, but MIM/IMX-WETH pools returned empty or unusable data, preventing a precise ETH or USD valuation purely from on-chain state at that block. The attacker_profit_estimate artifact (artifacts/root_cause/data_collector/iter_3/pricing/1/attacker_profit_estimate_block_13715026.json) therefore records a failure to compute a numeric profit in ETH or USD.
Even without an exact number, the sign of profit is unambiguous: the adversary cluster converts 0.1 WETH plus gas into strictly positive quantities of multiple fungible ERC20 assets with nonzero market value, funded by a net loss of those assets from the MonoXPool proxy. The economic loss is borne by MonoX LPs and users whose pooled assets were drained, and the exploit undermined confidence in MonoX’s single-sided pool design and internal VCASH-based pricing model.
7. References
-
Seed index and seed transaction artifacts:
- artifacts/root_cause/seed/index.json (target transaction listing).
- artifacts/root_cause/seed/1/0x9f14d093a2349de08f02fc0fb018dadb449351d0cdb7d0738ff69cc6fef5f299/metadata.json (Etherscan-style metadata for tx 0x9f14d0...f299).
- artifacts/root_cause/seed/1/0x9f14d0...f299/balance_diff.json (native and ERC20 balance diffs for the exploit tx).
- artifacts/root_cause/seed/1/0x9f14d0...f299/trace.cast.log (cast -vvvvv execution trace for the exploit tx).
-
Data collector artifacts:
- artifacts/root_cause/data_collector/iter_1/tx/1/0x9f14d0...f299/trace.cast.log (full call stack and decoded Monoswap/MonoXPool calls).
- artifacts/root_cause/data_collector/iter_1/address/1/0xecbe385f78041895c311070f344b55bfaa953258/normal_txs.json (origin EOA history and orchestrator deployment tx 0x3c25b170...).
- artifacts/root_cause/data_collector/iter_1/address/1/0xf079d7911c13369e7fd85607970036d2883afcfd/normal_txs.json (orchestrator contract activity).
- artifacts/root_cause/data_collector/iter_1/address/1/0x8f6a86f3ab015f4d03ddb13abb02710e6d7ab31b/normal_txs.json (profit EOA incoming transfers and subsequent actions).
-
Contract code:
- artifacts/root_cause/data_collector/iter_2/contract/1/0x7164be9fd69f2e1de9b6b75b17e1b86268f18b45/source/src/MonoXPool.sol (MonoXPool implementation for LP tokens and ERC20 transfer helpers).
- artifacts/root_cause/data_collector/iter_3/contract/1/0xc36a7887786389405ea8da0b87602ae3902b88a1/source (Monoswap/MonoX controller proxy and admin code; used with the trace to reconstruct pools[token] and _updateTokenInfo behavior).
- artifacts/root_cause/seed/1/0x99d8a9c45b2eca8864373a26d1459e3dff1e17f3/src/Contract.sol (MIM token contract, confirming 18 decimals).
- artifacts/root_cause/seed/1/0xf57e7e7c23978c3caec3c3548e3d615c346e79ff/src/Contract.sol (IMX token contract, confirming 18 decimals).
-
Pricing and profit estimation:
- artifacts/root_cause/data_collector/iter_2/pricing/1/dex_price_snapshots_block_13715026.json (on-chain WBTC-WETH and WETH-USDC pool state at block 13,715,026).
- artifacts/root_cause/data_collector/iter_3/pricing/1/dex_price_snapshots_block_13715026_mim_imx.json (attempted MIM/IMX pricing pools at block 13,715,026).
- artifacts/root_cause/data_collector/iter_3/pricing/1/attacker_profit_estimate_block_13715026.json (failed numeric profit estimation artifact).