SlowMist: Details of Lendf.Me Reentrancy Attack
According to the information provided by SlowMist Zone, Lendf.Me, an Ethereum DeFi platform, suffered from Reentrancy Attack. The SlowMist Security Team quickly analysed the emergency and figured out the cause of the problem.
Based on the primary statistics analysed by the SlowMist AML System, the total amount of loss caused by the attack for Lendf.Me was about $24,696,616, which includes the following cryptocurrencies:
After the attack, the stolen cryptocurrencies were converted to ETH and other tokens by the attacker on some DEX platforms such as 1inch.exchange, ParaSwap, and Tokenlon.
Here we present the detailed analysis of the whole attack.
The address that performed this attack aimed at Lendf.Me is 0xa9bf70a420d364e923c74448d9d817d3f2a77822, and the attacker hacked the Lendf.Me by deploying the contract 0x538359785a8d5ab1a741a0ba94f26a800759d91d.
By looking into one of the attack transactions at Etherscan:
We found that the attacker first deposited 0.00021593 imBTC, but later successfully withdrew 0.00043188 imBTC, which is almost twice the deposits, from Lendf.Me. So how did the attacker double the balance in just one single transaction? We are going deep into every action of the whole transaction process to figure out what actually happened.
By checking this transaction at bloxy.info, we can clearly see the whole transaction process.
By analysing the details, it is easy to find the function supply() was called twice for Lendf.Me, while each call was independent of the other, instead of one call made within another one.
Next, after the second function call of supply(), the attacker called the function withdraw() for Lendf.Me in their own contract and finally withdrew from Lendf.Me.
By now, clearly, the function call of withdraw() was made within the function transferFrom(), in another word, it was called when Lendf.Me called transferFrom function which finally called the tokensTosend() function in user contract. It’s obvious that the attacker made a reentrancy attack by calling the supply() function. To take a closer look at the attack , let us follow into Lendf.Me contract codes.
For the purpose of depositing the coins provided by the users into the contract, a function named doTransferln was called by Lendf.Me funciton supply() after a series of handling, and after these steps, some of the keys of the variable Market were assigned with values. To look back on the attacking process we have informed, it was at the second function call of supply() that the attacker called the withdraw() function by using reentrant to withdraw the coins, which means, the codes that has a row index larger than 1590 were never executed until withdraw() function call finished during the second function call of supply(). This operation was responsible for the incorrect extra balance that the attacker withdrew.
Let us take a closer look at the function supply():
According to this picture, the balance of both the market and the user would be updated at the end of the function execution of supply(). Before that, the balance of the user would have been obtained and stored in localResults.userSupplyCurrent when the function supply() began to execute, as follows:
By assigning value to variable localResults, user’s transaction input data would be temporarily stored in this variable. Then, withdraw() function was executed by attacker and here is the code:
We can find two key points here:
- At the beginning of the function, the contract first gets two storage variables, market and supplyBalance.
- At the end of the withdraw() function, there is the same logic that updates the balance information (supplyBalance) of the market user, and the updated value is the balance after deducting the user’s withdrawal amount.
For normal and correct withdrawal logics, when the function withdraw() was executed alone, the user balance should have been deducted and been updated to the correct value. But due to the attacker embedding the function withdraw() within the function supply(), user balance was updated once more when the remaining part of the supply() codes was executed, which had row index larger than 1590, even after the function withdraw() had updated the user balance earlier. While for the second update, the updating value would be the sum of two parts: one from the original user balance value stored in variable localResults, located at the beginning of the function supply(), and another from user balance value in the first function call of supply().
After all these operations, though the user’s withdrawal had been deducted from user balance, the following programming logic of the function supply() would once again cover user balance with the user balance value before withdrawal, which resulted in the increment of user balance value instead of the normal deduction in spite of the attacker having executed withdrawal. Through these methods, the attacker can withdraw by exponential growth until they empty the Lendf.Me.
Advice on Defense
Advice from the SlowMist Security Team applying to this attack:
- Import lock mechanism in the key part of business, such as: OpenZeppelin’s ReentrancyGuard
- While in the development of a contract, such developing style should be adopted: changing local variables of the contract before calling the outside contract.
- Ask for thorough secure audits from excellent third-party security teams to find out the potential security risks as many as possible.
- When multiple contracts are connected, it is also necessary to check the code security and business security of multiple contracts, and comprehensively consider the security issues under the combination of various business scenarios.
- Try setting the switch for the pause of the contract, so as to get informed and stop loss when the black swan occurred.
- Realize that security is dynamic so every part of the project should capture the threat intelligence possibly involved with the project and check out the potential security risks.