On November 30, BEOSIN Eagle-Eye detected that MonoX, an automatic market maker protocol, suffered a flash loan attack with a loss about USD 31 million. Regarding this attack, the BEOSIN technical team immediately conducted an incident analysis.
After the attack, MonoX confirmed on its official Twitter that its contract was attacked, and the team is investigating and will do its best to recover the stolen funds.
MonoX uses a unilateral token pool model, which uses vCASH stablecoins and tokens provided by AMM to create virtual trading pairs. Simply put, MonoX creates a token-vCASH transaction pair. When adding liquidity, only tokens need to be added. When performing any token exchange, the exchange path is: token A -> vCASH -> token B, instead of going through multiple pairs like Uniswap: Token A -> Pair1-> Pair2-> Pair3-> Token B.
The attackers used the same address 0xEcbE385F78041895c311070F344b55BfAa953258 to launch an attack on MonoX on Ethereum as well as MATIC, with the same contracts deployed to conduct the attack on both platforms. The attack transactions are：
Since the codes of the two platforms are exactly the same, the following analysis will be based on the attack transaction on Ethereum.
Exchange 0.1 WETH to 79.98609431154262101 MONO through Monoswap;
Remove all liquidity from Monoswap. An arbitrary address liquidity removal vulnerability in the Monoswap contract is exploited here.
The Monoswap contract does not check whether the owner of the liquidity to is msg.sender. In the _removeLiquidity function, as shown in Figure 2, line 443, obtains the timestamp of the last time the caller (attack contract) added liquidity. The return result is 0, so the detection in line 445 passes. In line 446, if topLPHolderOf is not the address of the caller (attack contract), the detection in line 447 passes. Thereafter there are no more msg.sender related operations in the remove liquidity related code.
Add a very small amount of MONO to Monoswap. This step is to prepare for the rapid increase in the price of MONO later.
The attacker exploited the token exchange price override vulnerability in the Monoswap contract, repeatedly conducted exchanging of the same tokens to increase the price of MONO. In step 3, the attacker controlled the reserves of MONO in the Monoswap contract to a very small value. The purpose is to increase the price of MONO faster with a very low amount of MONO.
The token exchange process of the Monoswap contract is: check whether the exchange parameters are normal, then calculate the number of input and output tokens and the price after the token exchange, and finally perform the exchange operation and write the new token price into the ledger. The above logic will work normally when different kinds of tokens are exchanged. However, when the same tokens are exchanged, there will be two problems:
(1) When the _getNewPrice function calculates the number of tokens that should be input and output, the change in the transaction pool token reserves during the exchange process is not considered. The same token is calculated based on the same initial price after the exchange.
(2) In the last step of the token update process, it is not taken into account that when the same token is exchanged, the price update operation of the output token (line 841 in Figure 6) will overwrite the update operation of the input token (Figure 6) Line 830). The vulnerability caused the price of MONO to increase abnormally when MONO tokens were exchanged for MONO tokens. In addition, the problem exists not only in the swapExactTokenForToken function used by the attacker, but also in the swapTokenForExactToken function.
Now let’s see how the attacker exploited vulnerability 2 to carry out an attack:
(1) As shown in Figure 10, the initial price of MONO is 5.218 vCASH/MONO.
Then the attacker repeated the exchange of MONO->MONO, and a total of 55 exchanges were made, as shown in the following figure:
Analyzing one of the exchange transactions, we can find the amount of each exchange is the total amount of MONO in the transaction pool minus 1, which is the exchange amount that can maximize the MONO price (Figure 8 _getNewPrice line 527, the denominator is 1). In addition, since the total amount of MONO in the transaction pool is low (step 3), the attacker has ensured that there is sufficient balance for exchange operations through step 1.
As of the end of the exchange, the price of MONO has been pulled up to 843,741,636,512.366 vCASH/MONO. The attack contract remained 51.92049285389317 MONO.
(2) 847.2066974335073 WETH was borrowed through the USDC/WETH pool of Uniswap V2. The attacker then converted 0.0709532091008681 MONO to 4,029,106.880396 USDC via Monoswap, and then returned the USDC to the USDC/WETH pool. Note that here the attacker is actually converting the USDC exchanged from Monoswap to Uniswap V2 for WETH, which is not a flash loan attack.
(3) All assets transferred out by the attacker are as follows:
All stolen assets are sent to 0x8f6a86f3ab015f4d03ddb13abb02710e6d7ab31b address.
The project has already communicated with the attacker, and BEOSIN will continue to monitor the incident.
In this attack, the attackers exploited two vulnerabilities in the contract: (1) Any address can arbitrarily remove the liquidity of the specified address; (2) The override problem of price write operations in special cases.
It is recommended that the project owner do a good job of checking permissions during the contract development process; take special cases into account during development and testing, such as the transfer of the same token.