Home » Blog » Solidity Vulnerability: Reentrance Attack

Solidity Vulnerability: Reentrance Attack

This is the first in a series of blogs I will be making to help you protect your contracts from security vulnerabilities.

Code – https://github.com/derekarends/solidity-vulnerabilities/tree/main/reentrance

Demo – https://youtu.be/lH9mHiArjO8

The first vulnerability I would like to help with is the reentrance attack. Described simply as, allowing a caller to reenter the contract before the state has been updated to exploit the contract. In most cases it would be used to drain the contract of ETH but there may be other items in storage impacted by this as well.

Let’s get some code on the screen and show you what a vulnerable contract might look like, why it is vulnerable and then we will go through some scenarios of how to protect against it.

Vulnerable Contract

/// This contract is vulnerable to a reentrance attack
contract VulnerableContract {
    mapping(address => uint256) public balances;
    /// Allow senders to deposit ETH
    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }
    /// Allow senders to withdraw their ETH
    function withdraw() public {
        uint256 bal = balances[msg.sender];
        require(bal > 0);
        // Vulnerability: Calling out to the sender to send them their ETH
        (bool sent, ) = msg.sender.call{value: bal}("");
        require(sent, "Failed to send Ether");
        // Clear out the senders balance
        balances[msg.sender] = 0;
    }
    /// Check the balance of this contract
    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

You can see in the contract where we are calling out “Vulnerability” the contract makes a call to an external source before it updates the state of the msg.sender.

This allows an attacker to reenter the contract because their ETH balance is still greater than 0. This means they will bypass the required check at the beginning of the function allowing them to receive more ETH.

Let’s take a look at what an attacker contract might look like to cause this behavior.

Attacker Contract

/// This contract will attempt to steal all the ETH from the vulnerable contract
contract AttackerContract {
    VulnerableContract public vulnerableContract;
    
    /// Initialize the Attacker contract with the address of
    /// the vulnerable contract.
    constructor(address _vulnerableContract) {
        vulnerableContract = VulnerableContract(_vulnerableContract);
    }
    // Fallback will be called when the VulnerableContract sends ETH to this contract.
    fallback() external payable {
        /// This will check if the vulnerable contract still has ETH
        /// and if it does, continue to try and call withdraw() 
        if (address(vulnerableContract).balance >= 1 ether) {
            vulnerableContract.withdraw();
        }
    }
    
    /// This function will start the attack on the vulnerable contract
    function attack() external payable {
        require(msg.value >= 1 ether);
        vulnerableContract.deposit{value: 1 ether}();
        vulnerableContract.withdraw();
    }
    // Check the balance of this contract
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

We can see in the attacker contract, it will start the attack by sending over some ETH to get put in the balance allowing it to bypass the required check in the vulnerable contract’s withdraw function.

Next it will call the withdraw function of the vulnerable contract and once the vulnerable contract sends the ETH to this contract the fallback function is called.

Finally the fallback function will check to see if the vulnerable contract still has ETH and if it does, it will continue to call the withdraw function until the vulnerable contract no longer has any ETH.

Ways to Prevent Reentry

There are a few ways we can prevent the reentrance attack and be more mindful of which functions may be vulnerable to it.

I like examples first and then we can talk about each option.

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
// 1: Use Openzeppelin's ReentrancyGuard
/// This contract is can no longer be exploited by the reentrance attack
contract SecureContract is ReentrancyGuard {
    mapping(address => uint256) public balances;
    /// Allow senders to deposit ETH
    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }
    // 1: Add the nonReentrant modifier from Openzeppelin
    // 3: Name functions that are making external calls as untrusted
    /// Allow senders to withdraw their ETH
    function withdrawUntrusted() external nonReentrant {
        uint256 bal = balances[msg.sender];
        require(bal > 0);
        // 2: Update the state of the msg.send before the external call
        // Clear out the senders balance
        balances[msg.sender] = 0;
        // Vulnerability: Calling out to the sender to send them their ETH
        (bool sent, ) = msg.sender.call{value: bal}("");
        require(sent, "Failed to send Ether");
    }
    /// Check the balance of this contract
    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

1: Openzeppelin’s ReentrancyGuard

One way to protect against the reentrance attack is to use Openzeppelin’s ReentrancyGuard. By using their contract instead of writing your own code you know it has been reviewed by many people and is being used in production environments already.

This builds confidence that the code is secure and with it being open sourced you are able to view the code and understand what it is doing. It also allows you to have confidence it isn’t doing anything malicious.

On thing to note with the nonReentrant modifier is the function should be external. If another function inside the contract calls the nonReentrant function it is no longer protected

2: Re-order State Modification

Another option and one that is preferred over having to use modifiers is to just re-order when the state is modified. By setting the balance to zero of the msg.sender’s balance before transferring it to the msg.sender it will prevent them from reentering this function.

A pattern that should be used is one called checks-effects-interaction.

The idea with this pattern is to first do all your function checks, followed by updating any and all state, lastly do any contract interactions.

3: Naming Functions as Untrusted

This one doesn’t protect your contract, however, it does remind you and any others using your contract that this function is calling out to an external source.

One thing to remember is if another function in your contract calls this function it should be labeled as untrusted as well… Untrusted all the way down.

Wrapping Up

The reentrance attack can be prevented by taking a few simple steps. There are a few other ways to protect against it, not touched on here, but these few tips should help prevent your contract from being exploited.

Leave a Reply

Your email address will not be published.