Solidity Security By Example #03: Reentrancy via Modifier
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.
Reentrancy via modifier might be another level of reentrancy in terms of complexity. Sometimes, it might be challenging to catch up on this vulnerability in a smart contract.
In this article, you will learn how the reentrancy via modifier 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/03_reentrancy_via_modifier.
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 Dependency
Below is just the IAirdropReceiver interface mutually required by the InsecureAirdrop, the Attack, and the FixedAirdrop contracts, which we will discuss later on.
|
|
The Vulnerability
The following shows the InsecureAirdrop contract, providing an airdrop to anyone calling its receiveAirdrop function (lines 15–19). The receiveAirdrop function applies two modifiers neverReceiveAirdrop (lines 21–24) and canReceiveAirdrop (lines 40–51) to validate that the claimer is eligible and has never received the airdrop before.
Of course, the InsecureAirdrop contract is vulnerable to a reentrancy attack. But, can you find the issue? 👀
|
|
To receive the airdrop, a claimer has to call the receiveAirdrop function (lines 15–19) of the InsecureAirdrop contract. The receiveAirdrop function would validate that the claimer must never receive the airdrop through the neverReceiveAirdrop modifier (lines 21–24).
Then, the receiveAirdrop function would validate that the claimer is eligible to receive the airdrop via the canReceiveAirdrop modifier (lines 40–51). The canReceiveAirdrop modifier will first check if the claimer is a regular user or a smart contract (line 42). The claimer will pass the eligibility check right away without further validation if they are a user.
Nonetheless, if the claimer is a smart contract, the canReceiveAirdrop modifier would do further check by calling the canReceiveAirdrop function of the claiming contract to demonstrate that the contract supports receiving the airdrop (line 46). In other words, the claiming contract must implement the canReceiveAirdrop function and return true when the InsecureAirdrop contract invokes it.
A reentrancy is a programmatic approach in which an attacker performs recursive requests to gain airdrops over the expectation.
In the case of InsecureAirdrop contract, the reentrancy begins in line 46 in the canReceiveAirdrop modifier. Figure 1 below portrays how the reentrancy via modifier attack occurs.
Figure 1. How the reentrancy via modifier happens
To gain the airdrops, an attacker uses the Attack contract implementing the canReceiveAirdrop function as shown in Figure 1. This function would be executed by the InsecureAirdrop contract to validate that the Attack contract supports receiving the airdrop (Step 5).
To perform reentrants, the canReceiveAirdrop function would interrupt the control flow in the middle and execute the loop calls to the receiveAirdrop function of the InsecureAirdrop contract (Step 6).
On the reentrancy calls, the neverReceiveAirdrop modifier (Step 3) would think that the Attack contract has never received the airdrop before 🤔 because the receiveAirdrop function’s body statement for minting the airdrop (i.e., userBalances[msg.sender] += airdropAmount; in line 17) and the statement for marking the Attack contract as the airdrop receiver (i.e., receivedAirdrops[msg.sender] = true; in line 18) would never be executed yet.
Note that the receiveAirdrop function’s body statements in question would be triggered after the exit of each reentrancy call. For this reason, the validation check in Step 3 would always be bypassed on every reentrancy call. 🤧
After that, the canReceiveAirdrop modifier would be invoked in Step 4. Subsequently, another loop call begins again (Steps 5 and 6). This process would be recursively performed again and again to gain more airdrop tokens. Oh, No! 🙀
The Attack
The Attack contract below can be used to exploit the InsecureAirdrop contract.
|
|
To exploit the InsecureAirdrop, an attacker invokes Attack.attack(10) function (the passing argument: 10, in this case, represents the number of amplification times the attacker would like to gain the airdrops).
To understand how the Attack contract works in more detail, please refer to the attack steps described in Figure 1 above.
Figure 2. The attack result
The result of the attack is shown in Figure 2. As you can see, a regular user could receive only 999 tokens, whereas the attacker could gain the number of tokens as many times as they wanted. Specifically, the attacker effortlessly gained the airdrop tokens 10 times beyond the minting permission. 🤑
The Solutions
There are three preventive solutions to address the associated reentrancy attack. 👨🔧
-
Calling the
canReceiveAirdropmodifier before theneverReceiveAirdrop -
Applying the mutex lock (
noReentrantmodifier) as the first modifier -
Using both solutions #1 and #2
|
|
The FixedAirdrop contract above is the resolved version of the InsecureAirdrop. We apply both solutions #1 (calling the canReceiveAirdrop modifier before the neverReceiveAirdrop) and #2 (applying the mutex lock as the first modifier) here.
For solution #1 (calling the canReceiveAirdrop modifier before the neverReceiveAirdrop), the receiveAirdrop function was improved by substituting the execution order of the modifiers (line 28) as follows: function receiveAirdrop() external canReceiveAirdrop neverReceiveAirdrop. The improved execution order results in validating that the claimer has never received the airdrop after verifying the claimer’s eligibility. This improvement guarantees that the reentrancy via modifier will never happen.
For solution #2 (applying the mutex lock as the first modifier), we attached the noReentrant as the first modifier to the receiveAirdrop function (line 28) as follows: function receiveAirdrop() external noReentrant canReceiveAirdrop neverReceiveAirdrop. The noReentrant (lines 8–13) 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 9.
Summary
In this article, you have learned the reentrancy via modifier vulnerability in a smart contract, how an attacker exploits the attack, and the preventive solutions to fix the issue. We hope you enjoy reading our article. See you in the forthcoming article.
Again, you can find all related source code at 👉 https://github.com/serial-coder/solidity-security-by-example/tree/main/03_reentrancy_via_modifier.
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.