Introduce of HopeLend
HopeLend is a decentralized non-custodial lending protocol with multiple liquidity pools. It enables instant loans based on the state of the pool, and loans do not need to be individually matched on both sides; instead, they rely on pooled funds, the amounts borrowed, and their collateral. Like Aave, the diagram below describes the business flow of HopeLend.
In simple terms, users can deposit underlying assets to provide liquidity. When this liquidity generates income through lending, a portion of the earnings is distributed to the liquidity providers.
Analysis Of HopeLend Attack
In simple terms, the hacker attack process can be divided into two main parts:
- Exploiting the insufficient liquidity in Hopelend’s hEthWBTC trading pool (with zero liquidity) to artificially inflate the value of hEthWBTC. Subsequently, the hacker uses the borrow function to drain all tokens (HOPE, stHOPE, wstETH, WETH, USDT, USDC) from the pool.
- Exploiting the precision loss vulnerability in function rayDiv to drain the 2000 WBTC borrowed from Aave through a flashloan and deposited into Hopelend, thereby emptying all the WBTC initially invested by the hacker in the early stages of the attack.
The exchange ratio between underlying assets and hTokens is controlled by the liquidityIndex, a metric representing the value of hTokens. In simple terms, when the liquidityIndex is, for instance, 2, one hToken can be exchanged for 2 units of the corresponding underlying assets. The liquidityIndex is determined based on the earnings generated, and its calculation involves the following method:
Here is the corresponding code:
In this attack, the hacker manipulated the liquidityIndex to artificially inflate the value of hTokens, causing a distortion in their value. Subsequently, the hacker used small units of hTokens to borrow substantial amounts of other underlying assets. This ultimately led to draining the underlying assets from other pools within Hopelend.
Following this, the hacker exploited the precision loss issue in function rayDiv by repeatedly executing deposit and withdraw operations. Eventually, the hacker successfully emptied all the initially deposited WBTC into the pool, completing the attack.
Stage 1
The hacker initiated the attack by using the deposit function to deposit the underlying asset WBTC and obtaining hEthWBTC tokens from mint function. Subsequently, through repeated flashloan transactions, the hacker manipulated the liquidityIndex of hEthWBTC, artificially inflating its price. Finally, leveraging minimal amounts of hEthWBTC as collateral, the hacker executed borrow operations to deplete all underlying assets except for WBTC.
Firstly, the hacker utilized a flashloan from Aave to borrow 2300 WBTC. Subsequently, the hacker executed a deposit of 2000 WBTC into Hopelend. Simultaneously, the corresponding trading pool minted 2000 hEthWBTC as a deposit certificate for the hacker. Following this, the hacker employed Hopelend’s flashloan to borrow an additional 2000 WBTC. The hacker then transfer 2000 WBTC into Hopelend’s trading pool. Subsequently, the hacker executed a withdraw operation, retrieving 1999.99999999 WBTC and leaving a minimal amount of 0.00000001 WBTC in the pool. The transfer and withdraw operations into Hopelend’s pool were conducted only during the initial flashloan.
This is because the hacker initiated a deposit of 2000 WBTC into Hopelend, prompting Hopelend to mint 2000 corresponding hTokens (hEthWBTC) as deposit certificates. Subsequently, the hacker used Hopelend’s flashloan to borrow an additional 2000 WBTC. Following this, the hacker executed a transfer of 2000 WBTC into the pool corresponding to the underlying asset and then conducted a withdraw operation, retrieving 1999.99999999 WBTC. Since this pool was uninitialized, its liquidityIndex was set to 1. Consequently, the pool destroyed 1999.99999999 hEthWBTC, leaving behind 0.00000001 hEthWBTC.
Let’s examine how Hopelend updates the liquidityIndex during a flashloan.
Firstly, IERC20(reserveCache.hTokenAddress).totalSupply() + reserve.accruedToTreasury represents the total value of the corresponding hToken, and premiumToLP is the profit generated in this flashloan, i.e., the additional value created. Because the flashloan’s borrowing interest rate is 0.09%, and 30% of the profits go to the protocol, while 70% goes to the liquidity providers, each time the hacker borrows 2000 WBTC through a flashloan, the profit generated for the pool is 2000 * 0.09% * 70% = 1.26 WBTC. Therefore, in the formula above, premiumToLP is equal to 1.26 WBTC.
Because reserve.accruedToTreasury is 0, in simple terms, the value of hToken after the flashloan (liquidityIndex) is calculated as follows:
Flashloaned hToken Value = ((Total Value of hToken in the Pool / Premiums to the Pool )+ 1) × Current Value of hToken in the Pool
Therefore, for the hacker to increase the value of hToken, it involves minimizing the total value of hToken in the pool. This explains why the hacker transfers the borrowed 2000 WBTC to the pool after the flashloan. Since only withdraw operations lead to the destruction (burning) of hToken, if the hacker does not transfer assets to the pool, there are no assets for the pool to use in withdraw operations, preventing the destruction of the corresponding hToken.
To minimize the total value of hToken in the pool, the hacker executes a withdraw of 1999.99999999 WBTC, leaving only 0.00000001 WBTC (the minimum unit). Consequently, the pool bruns 1999.99999999 hEthWTBC, leaving behind 0.00000001 hEthWTBC (the minimum unit). As the pool corresponding to WBTC and hEthWTBC is uninitialized, the liquidityIndex at this point is 1, and the current total value of the pool is 0.00000001 hEthWTBC (the minimum unit).
After the first flashloan, the value of liquidityIndex for hToken is updated to (126000000 * 10²⁷ / 1 + 1) * 1 = 126000001000000000000000000000000000. At this point, the value of 0.00000001 hEthWTBC is equivalent to 1.26000001 WBTC.
For the second time, the hacker borrows 2000 WBTC through a flashloan. Since the liquidityIndex was 126000001000000000000000000000000000126000001000000000000000000000000000 after the first borrowing, the algorithm calculates the updated liquidityIndex at this point as 252000000999999999999999999948221218252000000999999999999999999948221218.
By repeatedly executing flashloans, the hacker eventually raises the liquidityIndex for hEthWTBC to 75600000010000000000000000096556103367560000001000000000000000009655610336, meaning 0.00000001 hEthWTBC can be exchanged for 75.60000001 WBTC.
These are the values of liquidityIndex for hEthWTBC after each flashloan.
Because there is still the hacker’s 0.00000001 hEthWBTC (valued at 75.60000001 WBTC) in the trading pool and its value is substantial, the hacker utilized the collateral in the pool (0.00000001 hEthWBTC) to borrow all tokens (HOPE, stHOPE, WETH, USDT, USDC) through the borrow operation.
Stage 2
The hacker exploited the precision loss issue in function rayDiv by repeatedly performing deposit and withdraw operations, depleting all the initially invested WBTC from the early stages of the attack.
The hacker initially deposited 151.20000002 WBTC and subsequently withdrew 113.40000000 WBTC. At this point, the liquidityIndex had already increased to 7560000001000000000000000009655610336. When the hacker deposited 151.20000002 WBTC, the pool also minted 0.00000002 hEthWBTC. Upon the withdrawal of 113.40000000 WBTC, the corresponding hEthWBTC to be destroyed should be 0.000000019999999998015872. However, due to truncation, only 0.00000001 hEthWBTC was burned.
This is Solidity code snippet of burn function.
In the provided context, _burnScaled appears to be a function responsible for scaling down the amount of hEthWBTC to be burned.
We can observe that the quantity of hToken to be burned is determined by the formula
Amount of burn = (Amount of withdrawn underlying asset / liquidity index of hToken).
However, to minimize the gas consumption associated with mathematical operations, rayDiv function is used for division. The code snippet is as follows:
Because the calculation formula for the quantity of hToken to be burned is given by:
Amount of hToken to be burned = (Amount of withdrawn asset * 10²⁷ +(liquidity index of hToken / 2)) / liquidity index of hToken
At this point, the amount of asset to be withdrawn is 113.40000000 WBTC, and the liquidity index of hEthWBTC is 7560000001000000000000000009655610336. Through calculation using python, we can obtain:
The quantity to be burned is calculated as (1.9999999998015872 * 10^-8) hEthWBTC. However, since this involves integer division using evm opcode div, it effectively performs floor division, discarding the decimal places. As a result, the final amount to be burned is (1 * 10^-8) hEthWBTC, equivalent to 0.00000001 hEthWBTC. This translates to only burning hEthWBTC worth 75.6 WBTC. Therefore, at this point, the hacker has profited by exploiting the precision loss vulnerability, gaining 37.8 WBTC (113.4 WBTC- 75.6 WBTC).
Subsequently, the hacker proceeded to continue deposit, adding 75.60000001 WBTC, and obtaining the pool’s mint 0.00000001 hEthWBTC. (Given that the liquidity index of hEthWBTC is 7560000001000000000000000009655610336, equivalent to 1 unit of the smallest hEthWBTC, i.e., 0.00000001 hEthWBTC, valued at 75.60000001 WBTC.) Consequently, the pool had 0.00000002 hTokens remaining.
Following this, the hacker executed a withdraw, retrieving 113.40000000 WBTC. Due to truncation, only 0.00000001 hToken was burned.
In other words, the hacker deposited 75.60000001 WBTC through deposit and subsequently withdrew 113.40000000 WBTC through withdraw, thereby generating a profit of 37.8000000 WBTC seemingly out of thin air. By continuously repeating this operation, the hacker ultimately emptied the previously invested WBTC, enabling the repayment of the loan from Aave. At this point, the entire attack process has been completed.
Conclusion
The hacker initially exploit of the liquidity imbalance in the pool corresponding to the target asset, manipulating the liquidity index of the hToken associated with the target asset to distort its value. Subsequently, leveraging the collateral of a minimal amount of hToken, the hacker borrowed all other underlying assets. Following this, by exploiting a precision loss vulnerability in the contract’s division operation, the hacker repeatedly deposited and withdrew, depleting the underlying assets invested in the Hopelend attack. At this point, the hacker completed a complex attack against the DeFi project Hopelend, successfully draining all assets from HopeLend.