paint-brush
How Did Lendf.Me Lose $25 Million to A Reentrancy Attack? [An Analysis]by@Valid.Network
1,263 reads
1,263 reads

How Did Lendf.Me Lose $25 Million to A Reentrancy Attack? [An Analysis]

by Valid Network ResearchApril 26th, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Lendf.me is a DeFi app utilizing smart contracts in order to provide instant, decentralized lending. The platform suffered an attack causing the loss of $25m in cryptocurrency on the day of April 19, 2020. The attack took place on the Ethereum main-net smart contract named MoneyMarket. The main purpose of the MoneyMarket.supply() function is to handle token deposits. After the external call the function is updating the user’s deposited balance (lines 1599–1600)

Coins Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - How Did Lendf.Me Lose $25 Million to A Reentrancy Attack? [An Analysis]
Valid Network Research HackerNoon profile picture

DeFi or decentralized finance is a growing sector in the blockchain and cryptocurrency space that defines an ecosystem of decentralized applications providing financial services with no governing authority.

Lendf.me is a DeFi app utilizing smart contracts in order to provide instant, decentralized lending. The platform suffered an attack causing the loss of $25m in cryptocurrency on the day of April 19, 2020.

Thus, joining the list of other DeFi protocols exploited recently:

Synthetix hack — 37M sETH stolen

bZx hack — $900k stolen

Lendf.me’s current vulnerability is a unique instance of the reentrancy bug. Reentrancy is a well-known issue in the field of computing, referring to the ability of a subroutine to be interrupted in the middle of its execution and (then safely) be called again.

Other reentrancy bugs have been exploited in the past, causing massive damage, including:

The DAO hack — $150m stolen

Spank Chain hack — $30k stolen

The Lendf.me attack took place on the Ethereum main-net smart contract named MoneyMarket, which implements the core logic of the Lendf.me app.

Understanding the Lendf.me vulnerability

In order to best understand the underlying cause of the vulnerability, we should consider the contents of the various functions in the MoneyMarket contract.

First, we will consider the 

MoneyMarket.supply()
 function (line 1508):


    MoneyMarket.supply()
    part 1

    MoneyMarket.supply()
    part 2

    MoneyMarket.doTransferIn()

    The main purpose of the 

    MoneyMarket.supply()
     function is to handle token deposits. The function takes two arguments, the asset (the asset that the user wishes to deposit), and the amount (the number of tokens he wishes to deposit).

The main logic flow of the 

MoneyMarket.supply()
 function is as follows:

First, we read the balance variable that represents the user’s deposited asset balance in

MoneyMarket
storage (line 1514), then, 
MoneyMarket.checkTransferIn()
 function is invoked (line 1526). This function (externally) calls the asset contract in order to figure if the user has the number of tokens he wishes to deposit and that he approved the
MoneyMarket
contract to withdraw this amount on his behalf.

Later 

MoneyMarket.doTransferIn()
 function is invoked (line 1583) which (externally) calls the asset contract’s 
transferFrom()
 function (line 405) that in turn transfers the amount from the user to the
MoneyMarket
contract. After the return from the external call the
MoneyMarket.supply()
 function is updating the user’s deposited balance (lines 1599–1600).

Let’s go over the 

MoneyMarket.withdraw()
 function’s logic briefly. In a simplified manner, this function gets the requested amount of tokens to withdraw, checks that the user holds at least this amount of tokens then transfers these tokens to the user by (externally) calling the token contract 
transfer()
 function.

Can you spot the vulnerability by now?

The issue here is that 

MoneyMarket.supply()
 function is actually updating the user’s asset balance after the external call to 
asset.transferFrom()
 (lines 1599–1600), but based on a value that was read before the external call (line 1514), which means that the update potentially ignores any updates that were made within the external call. In many terms, we can consider this anomaly to be a “Lost Update”.

But why is Lendf.me’s vulnerability exploitable?

In order to understand this, we will have a look at the imBTC contract (or any other ERC-777 compliant contract)

imBTC._transferFrom()

imBTC._callTokensToSend()

The attacker took advantage of the fact that some of the assets implement ERC-777 standard, which means that the 

imBTC._callTokensToSend()
function and thus, 
attackerContract.tokensToSend()
 function are invoked (lines 866, 1056 respectively) before the actual transfer of value between the two parties.

This way, the attacker’s contract gets a chance to call

MoneyMarket.withdraw()
function before the invocation of
MoneyMarket.supply()
is finished!

Attack Strategy

The only prerequisite for attempting the exploit is for an attacker to deploy an attacker contract that holds some amount of any asset that is ERC-777 compliant, let’s assume for example that the attacker holds 10 tokens of imBTC.

Now,

The attacker would place the first transaction that invokes  

MoneyMarket.supply
( asset =
imBTCAddress
, amount = 9). At this point, the attacker holds a supply of 9 imBTC in the
MoneyMarket
contract, and a balance of 1 imBTC in the imBTC token contract.The attacker would place the second transaction that invokes 
MoneyMarket.supply
(asset = imBTCAddress, amount = 1) , but now with an external call to 
MoneyMarket.withdraw
(asset =
imBTCAddress
,
requestedAmount
= 9) inside the 
attackerContract.tokensToSend()
 callback.

By the end of this transaction, the attacker’s imBTC balance in the imBTC token contract is 9, but the imBTC supply in the

MoneyMarket
contract is 10! This unwanted state occurred as the  
MoneyMarket.supply()
 function increases the supply for the attacker (lines 1599–1600) it uses stale data.

Therefore, the function doesn’t “know” at this point, that the attacker has already withdrawn some of his supply.Now, the attacker holds a deposit on the

MoneyMarket
contract, backed by nothing.

The attacker can use this to (falsely) borrow or withdraw assets deposited by other users. Furthermore, these two steps could be potentially performed, again and again, thus draining

MoneyMarket
’s liquidity.

Mitigation

When writing the smart contract code, try not to update any storage variables after an external call.If not possible, deploy some locking mechanism, like the commonly known 

ReentrancyGuard
 instead. Make sure that any pair of code paths that have a possible read/write conflict for a variable will be “reentrancy guarded”. For example, in this case, deploying a reentrancy guard only for the 
MoneyMarket.supply()
 function would not solve the problem, it should be deployed for the
MoneyMarket.withdraw()
 function as well. Valid network’s automated tools can help identify locations where these guards are missing, or incorrectly implemented.

(Written by David Oz Kashi on Behalf of Valid Network)