Author: Temirzhan Yussupov****
Source code: https://github.com/scaffold-eth/scaffold-eth-examples/tree/denial-of-service-example
Intended audience: Intermediate
Topics: Scaffold-eth basics, Smart Contracts
Make contract unusable by exploiting push external calls 😈
Table of Contents
- About The Project
- Getting Started
- Exploring smart contracts
- Attack vector
- Practice
- Prevention
- Additional resources
- Contact
This little side quest will allow you to explore the concept of "Denial of Service".
One exploit we introduce here is denial of service by making the function to send Ether fail.
You should be familiar with calls and reverts.
Let's start our environment for tinkering and exploring how "DoS attack" works.
- Clone the repo first
git clone -b https://github.com/scaffold-eth/scaffold-eth-examples.git denial-of-service-example
cd denial-of-service-example
- Install dependencies
yarn install
- Start your React frontend
yarn start
- Spin up your local blockchain using Hardhat
yarn chain
- Deploy your smart contracts to a local blockchain
yarn deploy
Pro Tip: Use tmux to easily start all commands in a single terminal window!
This is how it looks like in my terminal:
If everything worked fine, you have to have something like this opened in your browser:
Let's navigate to packages/hardhat/contracts
folder and check out what contracts we have there.
This smart contract will become unusable once we exploit it.
The logic is pretty straightforward:
function bid() payable external {
require(msg.value >= highestBid);
if (highestBidder != address(0)) {
(bool success, ) = highestBidder.call.value(highestBid)("");
require(success);
}
highestBidder = msg.sender;
highestBid = msg.value;
}
Smart contract immitates auction by keeping track of the highest bid made. If you want to become a highestBidder
you have to send ETH greater than the previous highestBid
.
Try to find a way to exploit this contract (make it unusable) before reading further.
Our contract for exploitation.
Note that this block of code is commented in the contract.
function () external payable {
assert(false);
}
Try to guess why :)
The attack we are going to do is called DoS with (Unexpected) revert
. So how does it work?
Basically, If attacker bids using a smart contract which has a fallback function that reverts any payment, the attacker can win any auction.
When it tries to refund the old leader, it reverts if the refund fails. This means that a malicious bidder can become the leader while making sure that any refunds to their address will always fail. In this way, they can prevent anyone else from calling the bid()
function, and stay the leader forever.
This is why part with fallback()
was commented in our Attack.sol
.
Let's use our awesome frontend provided by scaffold-eth
to make sure our assumption works fine.
Run two different sessions. One will be for a simple user and one will be for an evil hacker.
This is how it looks like for me:
My first tab is for a simple user and second tab is for a hacker.
Let's make an initial bid and become a highestBidder as a simple user.
Now let's run our attack
method as an attacker and disable our VulnerableAuction
forever!
Seems we became a new highestBidder
.
Now simple user can not become a new highestBidder
even though he puts more ETH that we did.
In order to mitigate this attack, we have to favor pull over push for external calls
.
This is demonstrated nicely in GoodAuction.sol
. Note how we added a new method withdrawRefund
. Now we do not depend on any push external calls like sending money back to someone.
function withdrawRefund() external {
uint refund = refunds[msg.sender];
refunds[msg.sender] = 0;
(bool success, ) = msg.sender.call.value(refund)("");
require(success);
}
Using this we are no longer vulnerable!
Join the telegram support chat 💬 to ask questions and find others building with 🏗 scaffold-eth!