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.
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.
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
canReceiveAirdrop
modifier before theneverReceiveAirdrop
-
Applying the mutex lock (
noReentrant
modifier) 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.