Solidity Security By Example #05: Cross-Contract Reentrancy
Originally published in Valix Consulting’s Medium.
Smart contract security is one of the biggest impediments toward the mass adoption of the blockchain. For this reason, we are proud to present this series of articles regarding Solidity smart contract security to educate and improve the knowledge in this domain to the public.
Cross-contract reentrancy typically happens when multiple contracts share the same state variable, and some contracts update that variable insecurely. This type of reentrancy might be considered a complicated issue since it is often challenging to discover.
In this article, you will learn how the cross-contract reentrancy attack happens and how to prevent it. Enjoy reading. 😊
You can find all related source code at 👉 https://github.com/serial-coder/solidity-security-by-example/tree/main/05_cross_contract_reentrancy.
Disclaimer:
The smart contracts in this article are used to demonstrate vulnerability issues only. Some contracts are vulnerable, some are simplified for minimal, some contain malicious code. Hence, do not use the source code in this article in your production.
Nonetheless, feel free to contact Valix Consulting for your smart contract consulting and auditing services. 🕵
Table of Contents
The Dependencies
The code below contains dependencies required by the InsecureMoonVault
and the FixedMoonVault
contracts. The dependencies include ReentrancyGuard
abstract contract (lines 3–12), IMoonToken
interface (lines 14–23), and MoonToken
contract (lines 25–121).
|
|
The ReentrancyGuard
contains the noReentrant
modifier that is used to prevent reentrancy attacks. The noReentrant
(lines 6–11) is a simple lock that allows only a single entrance to the function applying it. If there is an attempt to do the reentrant, the transaction would be reverted by the require
statement in line 7.
The IMoonToken
interface defines function prototypes, enabling MoonVault
contract (i.e., InsecureMoonVault
in the code below) to interact with the MoonToken
contract.
Lastly, the MoonToken
contract is a simple ERC-20 token. Users can buy (via the deposit
function in lines 13–16 in the below code) and sell (via the withdrawAll
function in lines 18–27 in the below code) MOON tokens through the MoonVault
contract. The MOON is a stablecoin pegged with Ether one-on-one. Whatever amount of Ether you deposit, what amount in MOON you will get.
Additionally, users can transfer their MOONs (via the transfer
function in lines 50–58 in the above code) or approve the transfer allowance of their MOONs (via the approve
function in lines 85–91 in the above code) to other users or smart contracts.
The Vulnerability
The following code describes the InsecureMoonVault
contract. As mentioned earlier in the previous section, the contract allows users to:
-
Buy MOON tokens (via the
insecureMoonVault.deposit()
function) -
Sell MOON tokens (via the
insecureMoonVault.withdrawAll()
function) -
Transfer MOONs (via the
moonToken.transfer()
function) -
Approve the transfer allowance of their MOONs (via the
moonToken.approve()
function) -
Check the balances of their MOONs (via the
insecureMoonVault.getUserBalance()
ormoonToken.balanceOf()
function)
Undoubtedly, the InsecureMoonVault
contract is vulnerable to a reentrancy attack. But, can you discover the issue? 👀
|
|
Since the InsecureMoonVault
contract applies noReentrant
modifier to the deposit
and withdrawAll
functions, both functions are safe from the reentrancy attack 🕵. Please refer to our previous article on the single-function reentrancy attack to understand how the insecure withdrawAll
function can be exploited.
Unfortunately, the InsecureMoonVault
contract in this article got another level of reentrancy in terms of complexity; we would call it: cross-contract reentrancy. 🤢
The cross-contract reentrancy begins in line 22 in the withdrawAll
function. Figure 1 below illustrates how the cross-contract reentrancy attack occurs.
The root cause of cross-contract reentrancy attack is typically caused by having multiple contracts mutually sharing the same state variable, and some of them update that variable insecurely.
Again, both the deposit
and withdrawAll
functions apply the noReentrant
modifier. Thus, an attacker cannot execute the reentrancy on these functions anymore.
Anyway, the withdrawAll
function does not update the withdrawer’s balance (moonToken.burnAccount(msg.sender)
) before sending Ethers back to the withdrawer (Step 4). Consequently, the attacker can perform the cross-contract reentrancy attack by manipulating the control flow in the Attack #1
contract’s receive
function to transfer its balance (Step 5) to another contract, Attack #2
(i.e., contract instance #2 in Figure 1).
Subsequently, the attacker can trigger another transaction by invoking the attackNext
function of the Attack #2
contract (Step 6) to gradually withdraw Ethers from the InsecureMoonVault
contract and then transfer the Attack #2
contract’s balance to the Attack #1
contract.
To drain all Ethers locked in the InsecureMoonVault
, the attacker executes the attackNext
function of the Attack #1
and Attack #2
contracts alternately. Oh My! 😱
In fact, the attacker can integrate the attack step 6 into a single transaction call to automate the attack. Though, the step 6 was intentionally isolated for the understanding sake.
The Attack
The following code presents the Attack
contract that can exploit the InsecureMoonVault
contract.
|
|
To exploit the InsecureMoonVault
, an attacker has to deploy two Attack
contracts (i.e., attack1
and attack2
) and then perform the following actions:
-
Call:
attack1.attackInit() and supplies 1 Ether
-
Call:
attack2.attackNext()
-
Alternately Call:
attack1.attackNext()
andattack2.attackNext()
to gradually steal all locked Ethers
To understand how the Attack
contract works in more detail, please refer to the attack steps depicted in Figure 1 above.
As mentioned earlier, the actions #2 and #3 can be integrated into the action #1 for an atomic attack transaction call. Nonetheless, we intentionally isolated them for the sake of understanding.
The result of the attack is displayed in Figure 2. As you can see, the attacker stole all locked Ethers by triggering separate transactions to the two Attack
contracts alternately. 🤑
The Solution
The FixedMoonVault
contract below is the remediated version of the InsecureMoonVault
. 👨🔧
|
|
The withdrawAll
function was improved to follow the checks-effects-interactions pattern to resolve the associated issue. In other words, we moved the so-called effect part (moonToken.burnAccount(msg.sender)
) to line 23 to execute it before the interaction part in line 26 (msg.sender.call{value: balance}(“”)
). This coding pattern guarantees that the withdrawer’s balance would be updated before sending Ethers back to the withdrawer, impeding the cross-contract reentrancy attack.
Note, even though the moonToken.burnAccount(msg.sender)
statement in line 23 is interacting with the MoonToken
which is an external contract, we consider that the MoonToken
is trustworthy.
Summary
In this article, you have learned the cross-contract reentrancy vulnerability in the Solidity smart contract, how an attacker exploits the attack, and the preventive solution to resolve the issue. We hope you gain your security knowledge from our article. See you again in our upcoming article.
Again, you can find all related source code at 👉 https://github.com/serial-coder/solidity-security-by-example/tree/main/05_cross_contract_reentrancy.
Author Details
Phuwanai Thummavet (serial-coder), Lead Blockchain Security Auditor and Consultant | Blockchain Architect and Developer.
See the author’s profile.
About Valix Consulting
Valix Consulting is a blockchain and smart contract security firm offering a wide range of cybersecurity consulting services. Our specialists, combined with technical expertise with industry knowledge and support staff, strive to deliver consistently superior quality services.
For any business inquiries, please get in touch with us via Twitter, Facebook, or info@valix.io.
Originally published in Valix Consulting’s Medium.