Reentrancy attack is one of the most common attacks on EVM-based smart contracts. It is an attack with devastating damages, which can be seen in many past incidents such as:
Most reentrancy attacks are done by reentering the same function (Single Function Reentrancy) it is called from; however, there are also other variations that are harder to discover and prevent. In this article, we will show you what Cross-Contract Reentrancy is, how impactful can it be, and how can you prevent it. We also have a hands-on lab that you can follow along to learn about this vulnerability more in detail.
Type of Reentrancy
- Single Function Reentrancy
- Cross-Function Reentrancy
- Cross-Contract Reentrancy
The first two variations are commonly found, the examples can be discovered in Consensys’s Ethereum Smart Contract Best Practices. What we are going to focus on is the third one, Cross-Contract Reentrancy.
Cross-contract reentrancy can happen when a state from one contract is used in another contract, but that state is not fully updated before getting called.
The conditions required for the cross-contract reentrancy to be possible are as follows:
- The execution flow can be controlled by the attacker to manipulate the contract state.
- The value of the state in the contract is shared or used in another contract.
This kind of vulnerability has been used in multiple past attacks, for example:
Example: Simple Vault and an ICO
As an example, take a look at this simple
Vault contract we implemented as a demonstration.
router smart contract is implemented using UniswapV2’s implementation
The users can deposit the base token and get
VaultToken ($VT) that act as the users’ shares of the tokens in the vault.
harvest() functions implemented, the ratio between the number of shares and the amount of base token in the vault will always be 1:1 unless the base token is manually transferred into the vault.
Vault contract, the contract itself is safe from reentrancy attack, as a mutex lock (
nonReentrant modifier) is used, so no attacker can do anything to drain the tokens from this contract.
We have also implemented another simple ICO contract that allows the users to exchange $VT to mint a new token, $GOV. The number of token gained is determined by the value of $VT and the price of token specified in the smart contract.
Can you see the flaw in these smart contracts?
Hint: Is it possible to manipulate the share value of $VT?
You can stop here and read through the smart contracts above carefully, or scroll down to see the explanation of the vulnerability.
Let’s start going deep into the code!
Hijacking the Execution Flow
One of the first things that we should notice in the
Vault contract is that we can control the
_srcToken address and
_destToken address parameters in the
withdrawAndSwap() functions respectively.
The address is then passed through the
path parameter into the
router.swapExactTokensForTokens() function, which is a common token swapping function for a UniswapV2-based router smart contract. The share ($VT) is then minted or burned after the swapping is done.
Moreover, in the
swapAndDeposit() function, the
_srcToken.approve() function is called. As we can control the address of
_srcToken, it is possible to hijack the execution flow before and after the token is swapped!
Using States from Another Contract
ICOGov contract, the
shareToAmount() function from the
Vault contract is called in the
buyToken() function to determine the value of $VT in terms of the base token.
shareToAmount() function calculates the token amount using the balance of the base token in the contract and the total supply.
In this case, if we can inflate the balance of the base token inside the
Vault contract without increasing the total supply, we can inflate the value for each share.
Directly transferring the base token into the contract is one of the options; however, by transferring directly, the token will be shared among the $VT holder, so a part of the funds will be lost.
Combining the Two
Remember what we found out in the previous part? We can hijack the execution flow before and after the swapping of the token!
What if we use the
swapAndDeposit() function and take control of the execution flow after the base token is successfully swapped and sent into the
Vault, but $VT is not yet minted? The value of $VT will be inflated at that moment, and we can call
ICOGov.buyToken() to mint more token using the inflated value!
Let’s take a look at this code again.
At line 48 (12 in the gist), the token is swapped and transferred back to the vault, this means that after this line, the base token balance will increase, while the total supply is not updated, as $VT is not minted yet.
And at line 50 (14 in the gist), the
approve() function of
_srcToken is called. As we can control the address of the token, we can implement a malicious token with a special
approve() function that performs a reentrant calling to
ICOGov.buyToken() when called at this specific point.
Let’s Try Hacking It!
As we want to hijack the execution flow using a malicious token contract, let’s implement it!
We have implemented the smart contracts, you can follow along using this repository: https://github.com/InspexCo/cross-contract-reentrancy
Starting as a standard
ERC20 token, we can create a contract and inherit OpenZeppelin’s
ERC20 contract implementation.
We want to implement a custom
approve() function that triggers at a specific point in the
Vault contract, so we can start with the baseline, the original implementation of the
We want it to trigger here at line 50 (14 in the gist), and we know that the
Vault contract will be the caller, and the approval amount will be zero; therefore, these can be used as the condition to trigger the reentrancy to the
We can implement the conditional trigger by checking that the address of the
msg.sender matches the vault address, and the approval amount is equal to zero.
With the contract above, we can now hijack the execution flow at the location we want to. Next, we need to prepare the $VT needed to buy the token, and call the
ICOGov.buyToken() function. We can do that by allowing the contract to transfer $VT from attacker’s wallet to
EvilERC20, approve the transfer, then buy the token.
Don’t forget to transfer the tokens bought back to the attacker’s wallet! And as we also need to prepare the liquidity pool, let’s mint some token to the attacker’s wallet too!
We are now done with the evil token contract. We can deploy it and perform the attack.
Assuming that the
baseToken is $USDT, the steps to perform are as shown in the following diagram (some minor details are omitted):
- Deploy the
- Prepare $USDT and call
Router.addLiquidity()to create a $EVIL-USDT Pair (Approve the transfer of $EVIL and $USDT for
Vault.deposit()to get $VT for buying $GOV (Approve the transfer of $USDT for
_srcToken(Approve the transfer of $VT for
With these steps, the value of $VT will be inflated depending on the amount deposited in step 4, allowing the attacker to buy $GOV at a lower cost.
Furthermore, using a similar method, a flashloan can also be used to largely increase the impact of this vulnerability, for example:
- Flashloan a large sum of $USDT
- Deposit $USDT
- Inflate the value of $VT and perform a reentrant calling to buy $GOV
- Sell $GOV in the open market
- Withdraw $USDT used in the inflation of $VT value
- Return the flashloaned $USDT and profit from the $GOV sold
Solutions to Prevent Reentrancy
For the first two variations, Single Function Reentrancy and Cross-Function Reentrancy, a mutex lock can be implemented in the contract to prevent the functions in the same contract from being called repeatedly, thus, preventing reentrancy. A widely used method to implement the lock is inheriting OpenZeppelin’s ReentrancyGuard and use the
The better solution is to check and try updating all states before calling for external contracts, or the so-called “Checks-Effects-Interactions” pattern. This way, even when a reentrant calling is initiated, no impact can be made since all states have finished updating.
Another alternative choice is to prevent the attacker from taking over the control flow of the contract. A set of whitelisted addresses can prevent the attacker from injecting unknown malicious contracts into the contract in this lab.
Nevertheless, the contracts that integrate with other contracts, especially when the states are shared, should be checked in detail to make sure that the states used are correct and cannot be manipulated.
Inspex is formed by a team of cybersecurity experts highly experienced in various fields of cybersecurity. We provide blockchain and smart contract professional services at the highest quality to enhance the security of our clients and the overall blockchain ecosystem.
For any business inquiries, please contact us via Twitter, Telegram, firstname.lastname@example.org