Solidity Security By Example #09: Denial of Service With Revert
Originally published in Valix Consulting’s Medium.
Smart contract security is one of the biggest impediments to 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.
Denial of service with revert is often caused by a lack of understanding of how the Solidity smart contract works, resulting in the contract being exploited. This article will explain how a vulnerable smart contract can be attacked 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/09_denial_of_service_with_revert.
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
The following is the ReentrancyGuard
abstract contract required by the InsecureWinnerTakesItAll
and the FixedWinnerTakesItAll
contracts.
|
|
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 Vulnerability
The following code presents the InsecureWinnerTakesItAll
contract. This contract runs a challenge that allows anyone to execute the claimLeader
function (lines 26–54) and supply some Ethers to claim the challenge leader.
Initially, the challenge would start with 10 Ethers provided by a contract owner as an initial reward (lines 16, 19, and 21). In addition to claiming the leader, a challenger must supply some Ethers more than the current leader’s deposit (line 29).
Once a new leader gets the throne, a previous leader will be deducted 10% from their deposit (line 41). In other words, a previous leader will receive only 90% of the deposit as a refund (line 51). 👌
The 10% deducted Ether funds will be accrued as the winner’s reward (lines 36–37 and 48–49). In a nutshell, the more challengers (and the more deposited Ether amounts), the more reward for the challenge winner. 💰💰💰
After the challenge period ends (line 58), the last leader will become the winner (line 59). Hence, the winner can claim their principal and challenge reward by invoking the claimPrincipalAndReward
function (lines 57–69). 🗽
Of course, the InsecureWinnerTakesItAll
contract is vulnerable. Can you spot the issue? 👀
|
|
The InsecureWinnerTakesItAll
contract has a design flaw in line 51 in the claimLeader
function.
The (bool success, ) = prevLeader.call{value: refundAmount}(“”);
statement is hired to send an Ether refund back to a previous leader once a new leader claims the throne. We call an action of refunding the previous leader like this as a push model.
Nonetheless, using the push model in the claimLeader
function opens room for an attacker to exploit the challenge with a denial-of-service (DoS) attack and take the reward easily. 🤔
Figure 1 below illustrates how an attacker permanently claims the leader using the DoS attack. 😧
With the push model in the claimLeader
function, an attacker can deploy an Attack
contract implementing a receive
function. The receive
function will revert the transaction upon receiving the Ether refund from the InsecureWinnerTakesItAll
contract, as shown in Figure 1 above.
This way, the attacker can use the deployed Attack
contract to execute the InsecureWinnerTakesItAll.claimLeader()
function to take the lead. If other challengers try to claim the lead in place, the Attack.receive()
function will revert their transactions (i.e., denial-of-service (DoS) attack). 😿
After the challenge period ends, the attacker will become the winner. He can obtain his principle and reward by executing the InsecureWinnerTakesItAll.claimPrincipalAndReward()
function. 🙀
The Attack
The below code shows the Attack
contract that an attacker can use to exploit the InsecureWinnerTakesItAll
contract and seize the challenge reward as a profit.
|
|
To exploit the InsecureWinnerTakesItAll
contract, the attacker performs the following attack steps.
-
Deploy the
Attack
contract and supply theInsecureWinnerTakesItAll
contract address as a target contract (lines 13–16). -
Execute the
Attack.attack()
function and provide enough Ethers for claiming the leader (lines 31–33). -
Wait for a challenge period to end, then invoke the
Attack.claimPrincipalAndReward()
function to obtain the principal and challenge reward (lines 35–40).
Note that, soon after a challenge period ends, the Attack.receive()
function will unblock receiving Ethers from the InsecureWinnerTakesItAll
contract (line 28).
The result of the attack is shown in Figure 2. The challenge period was 24 hours. As you can see, user #1 and user #2 were in the race to take the throne.
However, after the attacker joined the race, he permanently seized the throne. Every attempt to claim the throne back by user #1 and user #2 got reverted. 🤧
Once the challenge period ended, the attacker claimed the principal and challenge reward as a profit. 🤑🤑🤑
The Solution
The FixedWinnerTakesItAll
contract below is the remediated version of the InsecureWinnerTakesItAll
contract. 👨🔧
|
|
To fix the associated issue, we must redesign a new approach to refunding the deposited Ethers from the push to the pull model instead.
Specifically, all previous leaders have to call the FixedWinnerTakesItAll
contract to withdraw their refunds themselves.
To achieve this, we introduce the mapping prevLeaderRefunds
(line 12) for recording available refunds of all previous leaders. When a new leader claims the throne, the available refund amount of the previous leader will be updated by the following computation: prevLeaderRefunds[prevLeader] += refundAmount;
, in line 57 in the claimLeader
function.
All previous leaders can claim their refunds by invoking the claimRefund
function (lines 62–70) anytime.
With the pull model of refunding the deposited Ethers, an attacker can no longer employ the Attack
contract to exploit the challenge. 👍
Summary
In this article, you have learned how the improper push model of sending Ethers can lead to a denial-of-service (DoS) attack in the smart contract.
You have understood how an attacker exploits the vulnerable contract and how to fix the issue. Till we meet again.
Again, you can find all related source code at 👉 https://github.com/serial-coder/solidity-security-by-example/tree/main/09_denial_of_service_with_revert.
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, industry knowledge, and support staff, strive to deliver consistently superior quality services.
For any business inquiries, please contact us via Twitter, Facebook, or info@valix.io.
Originally published in Valix Consulting’s Medium.