DEV Community

wallebach
wallebach

Posted on

Ethernaut level #1 - Fallback

Today I decided to go through Ethernaut Challenges. Ethernaut is a good resource to sharpen smart contract security and web3 library skills. There are lot's of solutions to this CTF series and now it's time to add my own. Without further ado - let's crack the Fallback challenge.

Requirements

Here is the smart contract:

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

contract Fallback {

  mapping(address => uint) 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 (uint) {
    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;
  }
}

Enter fullscreen mode Exit fullscreen mode

The goals are:

  • claim ownership of the contract
  • reduce its balance to 0

Solution

There are two functions which set the owner contribute()and receive(). Let's see which one is more suitable for our needs.

contribute() updates the owner in case msg.sender sends more than initial owner. We can notice in constructor() that the contract deployer has generously set their contribution to 1000 ETH. So, utilization of contribute() is a long and expensive way.

Let's take a closer look to receive() function:

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

Seems interesting. In fact, we just have to send any non-zero amount of Ether and become the next owner! But how does receive() function work? Here is an explanation in official Solidity docs:

The receive function is executed on a call to the contract with empty calldata.

Basically, if we send Ether without specifying any method name, receive() should be triggered. Enough talking, let's check it out!

Connect your Metamask using a test network supported by Ethernaut. I use Sepolia. In case you need some testnet ETH go to Sepolia Faucet, e.g. on Alchemy or Infura. Both websites will ask you to create an account.

Open up browser console. We can see that receive() requires us to make some contributions. But it only checks if the contribution is greater than zero. Let's call contribute() method with msg.value equal to 1 wei which is the smallest possible non-zero amount we could send:

1 wei = 0.000000000000000001 ETH

We call contribute() like this:

await contract.contribute.sendTransaction({value: 1})
Enter fullscreen mode Exit fullscreen mode

We are official contributors! But we still didn't reach our goal. Time to conquer this contract. Output current owner address with this command:

await contract.owner()
Enter fullscreen mode Exit fullscreen mode

Looks like it is not someone that we want, right? It's an appropriate time to trigger the receive() function. Again, we can just send 1 wei:

await contract.sendTransaction({value: 1})
Enter fullscreen mode Exit fullscreen mode

Check the owner once again:

await contract.owner()
Enter fullscreen mode Exit fullscreen mode

Yes! We pnwed the contract. The last but not least we need to withdraw() all the contract's balance to our own address. We need to hurry before someone else does the same change of ownership trick as we just did :D. Withdrawal is a trivial task for owners:

await contract.withdraw()
Enter fullscreen mode Exit fullscreen mode

Done. All the balance from the contract has moved to our address. Here is a complete console output:

Fallback solution in browser console

Summary

This exercise demonstrates that incorrect business logic can lead to loss of funds despite of having Ownable functions in place.


If you like this article, feel free to subscribe to my social media accounts:

Top comments (0)