Started at 12:58:19 AM +UTC, Apr-18–2020, the DeFi platform, Uniswap, was attacked by a hacker using a reentry vulnerability. Around 24 hours later, at Apr-19–2020 12:58:43 AM +UTC, a similar hack occurred on Lendf.Me. Technically, the main logic behind these two incidents is the incompatibility between ERC777 and those DeFi smart contracts, which might be misused by the attacker to utterly hijack a normal transaction and perform additional illicit operations.
Specifically, in the Uniswap hack, the attacker exploits the vulnerability to drain the Uniswap liquidity pool of ETH-imBTC (with about 1,278 ETH）while in the Lendf.Me hack, the attacker makes use of it to (arbitrarily) increase the internal record of the attacker’s imBTC collateral amount so that she can borrow (and indeed borrow) a variety of 10+ assets from all available Lendf.Me liquidity pools (with total asset value of $25,236,849.44).
Meanwhile, it is also important to notice that ERC777 itself is a community-established token standard with its advanced features for various scenarios. However, these advanced features might not be compatible with certain DeFi scenarios. Worse, such incompatibility could further lead to undesirable consequences (e.g., reentrancy). We also notice that other token standards (e.g., ERC1155) have been similarly designed to have a callback function. Having said that, our manual review of the imBTC ERC777 implementation indicates that it indeed follows the ERC777 standard.
Root Cause Analysis
If we delve into the ERC777 source code, the vulnerability lies in the internal logic to call the
tokensToSend() function when the
from address of the
transferFrom() operation has registered itself as the
implementer (through the standard ERC1820 interface). For illustration, we show in the following code snippets the context when the
tokensToSend() function is called. In particular, as shown in line 1054, the
getInterfaceImplementer() of ERC1820 is used to retrieve the registered
implementer, if any. This particular function takes two parameters,
TOKENS_SENDER_INTERFACE_HASH: the first argument is essentially the attacker (e.g., the address supplying imBTC into Lendf.Me) and the second is a constant, i..e,
keccak256("ERC777TokensSender"). Later on, in line 1056, the
tokensToSend() function defined in the
implementer is called, which allows the attacker to hijack the transaction by essentially injecting additional malicious code for execution.
As described in OpenZeppelin’s post in April 2019 and the proof-of-concept exploit published in last July, the attacker can
setInterfaceImplementer() to setup the hook with a smart contract that defines the above-mentioned
tokensToSend() function, the attacker can basically perform additional logic, e.g., selling the same batch of tokens multiple times.
Since the theory behind the Uniswap hack has been described earlier in this post, we’re not going to elaborate further in this blog. Instead, we examine a specific malicious transaction (hash: 0x9cb1d93d6859883361e8c2f9941f13d6156a1e8daa0ebe801b5d0b5a612723c1). Evidently, there is an additional
tokenToEthSwapInput() call embedded inside. It means the attacker can trade another batch of imBTC tokens for ETH when the conversion rate has been manipulated to the attacker’s advantage.
Lendf.Me hack works slightly differently, but still in the same nature. If we examine a particular malicious transaction (hash: 0xae7d664bdfcc54220df4f18d339005c6faf6e62c9ca79c56387bc0389274363b), the deposit function, i.e.,
Lendf.Me is hooked by embedding an additional
withdraw() operation, leading to the effect of increasing the internal record of the attacker’s imBTC collateral amount without actually depositing the amount.
The logic behind is that the attacker did supply certain amount of imBTC into Lendf.Me in the first place (e.g., 289.99999999 imBTC). However, in the second
supply(), the attacker simply supplied 0.00000001 imBTC but additionally
withdraw()'ed 290 imBTC inside the hook (by hijacking the
IMBTC::transferFrom() call inside
doTransferIn() - line 1583). As a result, 290 imBTC was subtracted from the attacker’s balance within the embedded
withdraw(). However, when the execution went back to
supply(), the balance was reset to 290 imBTC (line 1599). That’s how the attacker manipulates the internal record of the attacker’s imBTC collateral amount in
Lendf.Me. With the sufficiently large of collateral amount, the attack can therefore
borrow all available 10+ assets from various liquidity pools (with total asset value of $25,236,849.44).
As a common mitigation mechanism to block such reentrancy attacks, the so-called Checks-Effects-Interactions design pattern always helps. For example, if the Lendf.Me’s
doTransferIn() after saving user updates of token balance, there will be no chance that the attacker could reset the balance updates due to the
On the other hand, the nature of ERC777 inevitably enables the hooking mechanism such that we need to prevent reentrancy in all trading functions. For example, if both
withdraw() try to hold a mutex lock in the beginning of the function, bad actors cannot perform a
withdraw() inside the body of a
supply(). Last but not least, we might need to revisit the choice between ERC20 and ERC777 while noticing the advantage of choosing ERC777 is to facilitate the protection of user assets (as there is no need for users to approve all tokens to the platform for each transaction operation).
The Lendf.Me hack is a huge blow to current DeFi community. In the following, we put together the amount loss of various assets in this incident:
PeckShield Inc. is an industry leading blockchain security company with the goal of elevating the security, privacy, and usability of current blockchain ecosystem. For any business or media inquiries (including the need for smart contract auditing), please contact us at telegram, twitter, or email.