上次的文章《零时科技 || Fount-running, MEV和Hacker》介绍了Hopelend的成功攻击是由抢跑者发起的。本文,我们将详细介绍抢跑者是如何进行攻击的。
HopeLend简介
HopeLend是一个去中心化的虚拟货币借贷平台,能够在无需双方单独撮合的情况下,根据资金池状态实现即时贷款。类似于Aave等DeFi项目,HopeLend的运行模式如下图:
HopeLend攻击分析
简单来讲,黑客攻击流程主要分为两部分:
- 利用Hopelend的hEthWBTC交易池流动性不足(流动性为0)抬高hEthWBTC的价值,然后通过borrow掏空所有代币(HOPE,stHOPE,wstETH,WETH,USDT,USDC);
- 利用rayDiv精度丢失问题,掏空从Aave中flashloan的其中2000 WBTC给Hopelend中存入的2000 WBTC。(掏空黑客前期攻击投入的所有WBTC)。
在具体分析攻击事件之前,我们需要简单了解一下HopeLend的相关内容。
Hopelend是去中心化借贷平台,用户通过deposti存入underlyingAssets(标的资产),获得对应的hToken;反之,通过withdraw取出underlyingAssets时,销毁对应的hToken。
其中,underlyingAssets标的资产和hToken的兑换比例是通过liquidityIndex(流动性指数)来控制的,简单来讲liquidityIndex(流动性指数)就是hToken的价值。例如,当liquidityIndex为2时,1个对应的hToken可以兑换2个对应的underlyingAssets(标的资产)。其中,liquidityIndex是通过收益进行计算的。计算方式如下:
对应的代码如下:
🔹在此次攻击中,简单来说,黑客通过操纵liquidityIndex(流动性指数)来抬高hToken的价值,使得hToken的价值失真。最终使用很小单位的hToken来借贷大额的其他标的资产。从而掏空Hopelend其他池子的underlyingAssets(标的资产)。随后,黑客通过利用rayDiv精度丢失问题,反复deposit和withdraw,最终掏空转给池子的所有WBTC完成攻击。
步骤1详细分析
黑客通过deposit存入标的资产WBTC,获得mint的hEthWBTC,然后通过重复的flashloan来操纵hEthWBTC的流动性指数liquidityIndex,使hEthWBTC的价格虚高,最后利用极小的hEthWBTC作为抵押物,通过borrow掏空除了WBTC之外的所有标的资产。
首先,黑客从Aave中利用flashloan贷了2300 WBTC,向Hopelend中deposit了2000 WBTC。同时,WTBC对应的池子向黑客mint了2000 hEthWBTC作为存款凭证。随后,黑客通过Hopelend的flashloan贷了2000 WBTC,然后向Hopelend的池子中transfer转入2000 WBTC,接着从Hopelend中withdraw了1999.99999999 WBTC,剩下0.00000001 WBTC(最小单位)。向Hopelend的池子中transfer和withdraw的操作只有在第一次flashloan中进行。
🔹这是因为,黑客向Hopelend中deposit了2000 WBTC,Hopelend随后mint了2000对应的hToken(hEthWBTC)作为存款凭证。随后,黑客通过Hopelend的flashloan贷了2000 WBTC,随后向标的资产对应的池子内transfer了2000 WBTC后withdraw了1999.99999999 WBTC。因此,该池子未初始化,所以对应的liquidityIndex为1,因此,池子销毁了1999.99999999 hEthWBTC,剩余0.00000001 hEthWBTC。
我们看一下,Hopelend在flashloan中是如何更新liquidityIndex的
首先,IERC20(reserveCache.hTokenAddress).totalSupply() + reserve.accruedToTreasury 是hToken对应的总价值,premiumToLP为本次flashloan的收益,也就是新增价值。因为flashloan的贷款利率是0.09%,且池子获得利润的30%归项目方,70%归流动性提供方,所以每次黑·客通过flashloan借贷2000 WBTC后,给池子产生的利润为2000 * 0.09% * 70% = 1.26 WBTC。所以在上面的公式中,premiumToLP 为 1.26 WBTC。
因为reserve.accruedToTreasury 为 0,所以简单来讲,flashloan后hToken的价值(liquidityIndex)=(池子新增的价值 / 池子hToken的总价值 + 1) * 池子当前hToken的价值。所以,黑客要抬高hToken的价值,就是让池子hToken的总价值尽可能小,这就解释了为什么黑客在flashloan借贷了2000 WBTC后要把2000 WBTC transfer转给池子。
因为 withdraw 操作会销毁(burn)hToken,但是当前如果不给池子转账,那么池子没有资产(最开始存的2000WBTC目前由于进行闪电贷中所以已经锁定),黑客就无法通过withdraw来销毁对应的hToken。
黑客为了让池子的hToken的总价值尽可能小,所以黑客withdraw了1999.99999999 WBTC,剩下0.00000001 WBTC(最小单位)。所以,池子也销毁了1999.99999999 hEthWTBC而剩下了0.00000001 hEthWTBC(最小单位)。由于WBTC对应了hEthWTBC的池子未初始化,所以此时的liquidityIndex为1,当前池子的总价值就是0.00000001 hEthWTBC(最小单位)。
经过第一次flashloan后,hToken的价值liquidityIndex被更新为,(126000000 * 10^27 / 1 + 1)* 1*= 126000001000000000000000000000000000。此时,0.00000001 hEthWTBC价值为1.26000001 WBTC。
第二次,黑客通过flashloan借贷2000 WBTC,因为第一次借贷后,liquidityIndex为126000001000000000000000000000000000,所以通过算法得到此时liquidityIndex为252000000999999999999999999948221218。
🔹黑客通过重复执行flashloan,最终将hEthWTBC的liquidityIndex提升到7560000001000000000000000009655610336,也就0.00000001 hEthWTBC可以兑换75.60000001 WBTC。
下图是每次flashloan后hEthWTBC的liquidityIndex的值
因为,池子中仍有黑客的0.00000001 hEthWBTC(价值75.60000001 WBTC)且价值巨大。所以,黑客利池子中的抵押物(0.00000001 hEthWBTC),通过borrow借空了所有代币(HOPE,stHOPE,WETH,USDT,USDC)。
步骤2详细分析
黑客利用rayDiv精度丢失问题,重复deposit和withdraw操作,掏空前期攻击投入的所有WBTC。
黑客首先存入了151.20000002 WBTC,随后取出了113.40000000 WBTC。因为,此时的liquidityIndex已经提升到7560000001000000000000000009655610336,所以黑客通过deposit存入151.20000002 WBTC后,池子同样mint了0.00000002 hEthWBTC。黑客通过withdraw取出了113.40000000 WBTC,应该销毁的hEthWBTC为0.000000019999999998015872个。但是,由于solidity截断导致只销毁了0.00000001 hEthWBTC。
具体看一下销毁(Burn)的代码
其中,_burnScaled为缩放需要销毁的hToken的函数。
我们可以看到,需要销毁的hToken数量 =(取出标的资产的数量 / hToken的流动性指数)。但是这里为了减少数学运算的gas消耗,此处除法使用的是rayDiv,代码如下:
因为,需要销毁的hToken的数量的计算公式为销毁的hToken数量 = (取出资产的数量 * 10^27 + (hToken的流动性指数 / 2) ) / hToken的流动性指数。此时,需要取出的资产数量为113.40000000 WBTC, 此时hEthWBTC的流动性指数为7560000001000000000000000009655610336。通过python计算我们可以得到
需要销毁的数量为(1.9999999998015872 * 10^-8)个hEthWBTC。但是因为此处为evm opcode div进行的整数除法运算,所以,此处相当于做了一个地板除(floor division)舍去了小数位。因此,最终得到需要销毁的数量为(1 * 10^-8)个hEthWBTC也就是0.00000001 hEthWBTC,相当于只销毁了75.6 WBTC价值的hEthWBTC。所以,至此黑客通过精度丢失漏洞获利(113.4 - 75.6 = 37.8 WBTC)。
随后,黑客通过继续deposit存入75.60000001 WBTC获得池子mint的0.00000001 hEthWBTC(因为此时hEthWBTC的流动性指数为7560000001000000000000000009655610336,相当于1个最小单位的hEthWBTC,也就是0.00000001 hEthWBTC价值是75.60000001 WBTC)。因此,池子又剩下0.00000002 hToken。接着,黑客通过withdraw取出了113.40000000 WBTC,由于solidity的截断,导致只销毁了0.00000001 hToken。
🔹也就是说,黑客通过deposit存入75.60000001 WBTC然后通过withdraw取出了113.40000000 WBTC,可以凭空得到37.8000000 WBTC,黑客通过持续重复此操作,最终掏空了之前投入的WBTC,从而归还了Aave的贷款。至此,所有攻击流程均已完成。
总结
黑客首先利用标的资产对应的池子流动差的原因,反复操作标的资产对应的hToken的流动性指数,使其价值失真。随后,通过极小的hToken做抵押借出其他所有的标的资产。接着,利用合约存在的除法精度丢失的漏洞,反复存取掏空黑客攻击中投入的标的资产。至此,黑客完成了一次针对DeFi项目Hopelend的一次复杂的攻击,掏空了HopeLend的所有资产。
参考
https://github.com/Dapp-Learning-DAO/Dapp-Learning/blob/main/defi/Aave/contract/9-LendingPool.md
https://docs.hope.money/lend/for-developers/contracts
https://docs.hope.money/lend/getting-started/understanding-hopelend
https://www.metaverse456.com/453414.html