Analysis of the UniLend Hack

SlowMist
5 min readJan 16, 2025

--

Author | Sissice
Editor | Liz

Background

On January 13, 2025, according to the SlowMist MistEye security monitoring system, UniLend on the EVM chain was attacked, resulting in a loss of approximately $197K. The SlowMist security team analyzed the incident and shared the findings as follows:

https://x.com/SlowMist_Team/status/1878651772375572573

Related Information

Attacker’s address: 0x55f5f8058816d5376df310770ca3a2e294089c33
Vulnerable contract address: 0xc86d2555f8c360d3c5e8e4364f42c1f2d169330e
Attack transaction: 0x44037ffc0993327176975e08789b71c1058318f48ddeff25890a577d6555b6ba

Core of the Attack

The core of the attack lies in the contract’s use of the old USDC balance in the pool when calculating the health factor during asset redemption. This caused the health factor to be higher than the actual situation, leading the system to wrongly believe that the user’s lending status was safe. The attacker exploited this vulnerability by using flash loans to borrow large amounts of assets. By depositing and redeeming assets, they bypassed the correct health factor check, thus obtaining the target assets without bearing the actual risk.

Attack Flow

  1. Pledge Assets in Advance: The attacker made a pre-transaction (0xdaf42127499f878b62fc5ba2103135de1c36e1646487cee309c077296814f5ff) to pledge 200 USDC to the UnilendV2Pool, obtaining 150,237,398 USDC lendShares. The attacker then transferred the LP tokens in preparation for future asset redemption.

2. Use Flash Loan to Borrow Assets: The attacker used a flash loan to borrow 60M USDC and 5 wstETH, converting the wstETH to 6 stETH.

3. Deposit Assets to Obtain Lending Shares: The attacker called the lend function twice, depositing USDC and stETH into the previously prepared LP, thus obtaining the corresponding shares. At this point, the attacker held the following shares:

USDC lendShare: 150237398 + 45070847435535 = 45070997672933
stETH lendShare: 6663517741687683225

4. Borrow Target Assets: Since the attacker had previously deposited a large amount of USDC, they were able to borrow 60 stETH by calling the borrow function. At this point, the stETH borrowShare increased to 60239272000126842038.

5. Redeem Pledged stETH: The attacker called the redeemUnderlying function to redeem all the pledged stETH. Since the attacker had never borrowed USDC, the USDC borrowShare was 0, allowing them to redeem the full amount of stETH, with the stETH lendShare going to zero.

6. Redeem Pledged USDC: The attacker then called the redeemUnderlying function again to redeem all the pledged USDC. In the redeemUnderlying function, the _burnLPposition function was first called to burn the corresponding USDC lendShare, leaving 150,237,398 USDC lendShare. The contract then checked the health factor in the checkHealthFactorLtv1 function, and finally transferred the redeemed USDC to the user.

Theoretically, since the attacker had already borrowed part of the stETH using the pledged USDC, the health factor check should have failed when they tried to redeem the full amount of USDC. However, this was not the case, so let’s look deeper into the checkHealthFactorLtv1 function:

It’s clear from the image above that the function calculates the current USDC lendBalance and stETH borrowBalance using the userBalanceOftoken0 and userBalanceOftoken1 functions, then compares the USDC health factor with the safety threshold to determine whether the redemption should be allowed.

Let’s continue by examining the userBalanceOftoken0 function:

Clearly, this is the key issue. The contract directly used the current USDC balance in the pool and the USDC lendShare to calculate the lendBalance. However, this balance includes the amount of USDC that the user is preparing to redeem, while the USDC lendShare had already been deducted by the _burnLPposition function for the portion to be redeemed. As a result, the USDC lendBalance shifted from the expected 150237398*(728895404+4829907565)/4175666009 = 200001650 to 150237398*(60000728895404+4829907565)/4175666009 = 2158955960717, which was significantly larger, causing the health factor to return an unexpectedly high value, thus passing the check.

7. Complete Attack and Profit: Finally, the attacker returned the flash loaned USDC and wstETH, and exited the attack with profits. Due to the vulnerability, the attacker was able to obtain 60 stETH by only pledging 200 USDC.

Conclusion

The core of the attack lies in the attacker’s use of the redeemUnderlying function, which relied on the old token balance in the pool to calculate the health factor. As the user’s tokens had not yet been withdrawn from the pool, the health factor was calculated as higher than the actual situation, causing the system to mistakenly believe the user’s lending status was safe. This allowed the attacker to bypass the correct health factor check and illegally acquire the target assets. The SlowMist security team recommends that project teams ensure real-time updates of asset status during health factor calculations to prevent similar attacks from occurring.

--

--

SlowMist
SlowMist

Written by SlowMist

SlowMist is a Blockchain security firm established in 2018, providing services such as security audits, security consultants, red teaming, and more.

No responses yet