EGD攻击事件相关信息
发生在BSC上
EGD-Finance代码分析及攻击流程讲解
EGD_Finance合约的调用都是通过代理合约进行调用
在区块链浏览器上点击“Read as Proxy”可以查看到最终的合约。
EGD Finance
合约中实现的主要功能就是质押USDT一段时候,可提取奖励EGD token,相当于银行存款,存一段时间之后可以提取利息。
下面质押步骤和兑换奖励步骤都是攻击者真实发起的交易步骤
质押步骤
Address 0xbc5e8602c4fba28d0efdbf3c6a52be455d9558f5 | BscScan 调用攻击合约的stake()
函数进行相应的抵押操作,该地址应该也是攻击者地址,其创建了攻击合约
具体交易如下:BNB Smart Chain Transaction Hash (Txhash) Details | BscScan
我们可以在Phalcon上看到该交易的具体调用信息:
这里进一步进行分析:
EGD_Finance | Address 0x93c175439726797dcee24d08e4ac9164e88e7aee | BscScan中bond()
函数应该只是填写以下邀请人,应该跟web2一样,每个地址的邀请人和质押收益相关
1 2 3 4 5 6 7
| function bond(address invitor) external { require(userInfo[msg.sender].invitor == address(0), 'have invitor'); require(userInfo[invitor].invitor != address(0) || invitor == fund, 'wrong invitor'); userInfo[msg.sender].invitor = invitor; userInfo[invitor].refer ++;
}
|
接下来的swapETHForExactTokens()
调用是Defi中很常见的代币交换操纵,通过地址看源码,其与uniswap_v2的对应函数一致
从名称中可以看出是向通过不确定数量的ETH来换取一定数量的代币,可以确定交换的是USDT
uniswap的参数列表
1 2 3 4 5 6 7 8 9 10
| function swapETHForExactTokens( uint amountOut, // 交易获得的代币数量 address[] calldata path, // 交易路径列表 address to, // 交易获得的 token 发送到的地址 uint deadline // 过期时间 ) external virtual override payable ensure(deadline) returns ( uint[] memory amounts // 交易期望数量列表 ){ ... }
|
PancakeSwap: Router v2 | Address 0x10ed43c718714eb63d5aa57b78b54704e256024e | BscScan
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) external virtual override payable ensure(deadline) returns (uint[] memory amounts) { //检查是否为WETH进行交换 require(path[0] == WETH, 'PancakeRouter: INVALID_PATH'); // 从library中获知得到amountOut数量的USDT,需要多少ETH amounts = PancakeLibrary.getAmountsIn(factory, amountOut, path); //发给pancake的ETH必须大于所需数量 require(amounts[0] <= msg.value, 'PancakeRouter: EXCESSIVE_INPUT_AMOUNT'); // 将 WETH 换成 ETH(对应phalcon的操作) IWETH(WETH).deposit{value: amounts[0]}(); // 将 amounts[0] 数量的 path[0] 代币从用户账户中转移到 path[0], path[1] 的流动池 assert(IWETH(WETH).transfer(PancakeLibrary.pairFor(factory, path[0], path[1]), amounts[0])); // 按 path 列表执行交易集合,不细究了,之后再详细看uniswap-qwq _swap(amounts, path, to); // 返回多余的ETH if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]); }
|
最后调用代理合约的stake()
函数,质押100个USDT,从Phalcon中的调用记录可看出,具体函数代码不进行分析了,主要是记录了很多相关信息,用于后续的奖励计算,可见EGD_Finance | Address 0x93c175439726797dcee24d08e4ac9164e88e7aee | BscScan
兑换奖励步骤
Address 0xee0221d76504aec40f63ad7e36855eebf5ea5edd | BscScan攻击者调用攻击合约的harvest()
函数进行相应的兑换奖励,phalcon的交易分析如下图所示:
首先调用合约的calculateAll()函数
对用户的质押奖励进行计算,计算用户总共能得到多少收益
1 2 3 4 5 6 7 8 9 10 11
| function calculateReward(address addr, uint slot) public view returns (uint){ UserSlot memory info = userSlot[addr][slot]; if (info.leftQuota == 0) { return 0; } uint totalRew = (block.timestamp - info.claimTime) * info.rates; if (totalRew >= info.leftQuota) { totalRew = info.leftQuota; } return totalRew; }
|
随后用户就展开了闪电贷操作,
先是通过PancakeSwap
(0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae)调用swap()函数
借到200个USDT,随后乐观转账,回调攻击合约的pancakeCall()函数
可见之前的闪电贷分析文章。
在pancakecall()函数中
又发起闪电贷,从Pancake LPs
(0xa361433e409adac1f87cdf133127585f8a93c67d)中调用swap()函数
借到424456个USDT,再次回调到pancakeCall()函数
中,这是用户调用claimAllReward()函数
兑换自己之前质押的奖励。
为什么在这里兑换奖励,应该很明显能够猜到是因为,闪电贷的大笔金额影响了奖励的计算方法,这里应该是计算质押奖励的函数出现了漏洞问题。
下面去PancakeSwap: Router v2 | Address 0x10ed43c718714eb63d5aa57b78b54704e256024e | BscScan项目的claimAllReward()
中看一下具体源码,进行了详细的注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| 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; //计算对应的奖励 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; } //关键步骤,计算对应的奖励,仔细看一下getEGDPrice()函数 //根据EGD的价格,来确定奖励多少EGD 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); } function getEGDPrice() public view returns (uint){ //可在phalcon上看到行营的记录 uint balance1 = EGD.balanceOf(pair); uint balance2 = U.balanceOf(pair); //EGD的价格仅仅是根据两种代币的实时数量(流动性)来进行计算,可以被攻击者操纵 return (balance2 * 1e18 / balance1); } function initialize() public initializer { __Context_init_unchained(); __Ownable_init_unchained(); rate = [200, 180, 160, 140]; startTime = block.timestamp; referRate = [6, 3, 1, 1, 1, 1, 1, 1, 2, 3]; rateList = [547,493,438,383]; dailyStakeLimit = 1000000 ether; wallet = 0xC8D45fF624F698FA4E745F02518f451ec4549AE8; fund = 0x9Ce3Aded1422A8c507DC64Ce1a0C759cf7A4289F; EGD = IERC20(0x202b233735bF743FA31abb8f71e641970161bF98); U = IERC20(0x55d398326f99059fF775485246999027B3197955); router = IPancakeRouter02(0x10ED43C718714eb63d5aA57B78B54704E256024E); pair = IPancakeFactory(router.factory()).getPair(address(EGD),address(U)); }
|
EGD的价格是根据两种代币在一个地址上的数量进行计算的,我们在initialize()函数
中得到pair地址,pair地址是根据router地址进行计算,router为一个代理合约,在区块链浏览器上我们可以看到pair的地址为0xa361433e409adac1f87cdf133127585f8a93c67d,为pancake的一个提供流动性的合约,是不是有点眼熟。
到这里我们也肯定发现了攻击为什么能成功?
- 用户先通过闪电贷在
Pancake LPs
0xa361…中借走了大量的USDT,导致Pancake LPs
中USDT与EGDpair对中,EGD的价格变得十分便宜。
- 这时用户在
pancakeCall()回调函数
中,兑换奖励,奖励的计算是根据Pancake LPs
中两种代币的数量进行计算EDG价格,导致EDG的价格很便宜,这是看到rew
的计算公式,用户获得超额奖励。
下面简单介绍一下,phalcon上后续的调用:
先将Pancake LPs
上借用的闪电贷归还;随进行了相应的k值验证(确保还款=原款+手续费)
随后在PancakeSwap: WBNB-BSC-USD 2
借用的闪电贷,进行相应的approve授权
调用swapExactTokensForTokensSupportingFeeOnTransferTokens函数将得到的EGD全部兑换成USDT
随后PancakeSwap: WBNB-BSC-USD 2
借用的闪电贷归还,并进行相应的k值验证
最后攻击者获利36044 USDT
污点分析的方式为什么能够进行检测?
针对脆弱的询价机制,该种机制中某种代币的计算是根据其它代币的余额
- taint source:代表代币的余额,能够被攻击者之间或间接的进行操纵,这时攻击者就能够操作代币的余额
- taint sink:转账的操作,Defi交易中,最终将具体的收益或金额转给用户,判断这里的转账地址能够被攻击者操纵
- source与sink之间存在路径:说明某种代币的余额能够被攻击者所操纵(能够被闪电贷操纵),并且收益或转账金额是于该代币余额有关(说明金融模型询价机制的脆弱),并且最终的收款地址能被攻击者操纵。