Ethernaut#1 - Fallback

What is Ethernaut?

Ethernaut is a Web3/Solidity based wargame meant to be played in the EVM. Each level is a smart contract that the user must hack, and the game allows users to both learn about ethereum and see their skills compare to historical hacks. The game may have an infinite number of levels.

Solving Ethernaut helps solidify your knowledge of Solidity, Vulnerability and Security in smart contracts and i will try to explain how to pass each challenge.

I will be interacting with the contracts mostly with Remix.

Ethernaut #1 - Fallback

#1

Look carefully at the contract’s code below.
You will beat this level if

  • you claim ownership of the contract
  • you reduce its balance to 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 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;
}
}

Contract Breakdown

Let us breakdown the contracts to understand what each function and piece of code does. Mere looking at it we can see the code consists of a single contract Fallback that is compiled with solidity version ^0.8.0.

Variables
1
2
mapping(address => uint) public contributions;
address public owner;

A global mapping of address to uint256 is declared called contributions.
A global variable of type address called owner

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

In the constructor, the owner is set to msg.sender,the deployer of the contract and the owner’s contribution is set to 1000 eth.

Functions
1
2
3
4
5
6
7
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}

This function is used to contribute ether to the contract. It checks that the ether being sent is less than 0.001 and sets msg.value to msg.sender with the contributor mapping.

If the contribution made by that msg.sender address is greater than that of the deployer of the contract (1000eth), the msg.sender address will become the new owner.

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

This withdraw function allows the withdrawal of eth from the contract. It can only be called by the owner because of the onlyOwner modifier and once called the total balance of the contract will be sent to the caller of the withdraw function.

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

In the receive function the msg.sender is set to the owner if the amount sent is greater than 0 and the contributions of msg.sender is greater than 0.

Solution

Steps to pass the challenge:

  • Find a way to make ourselves the owner.
  • Call the withdraw function to steal all of the eth.

Setting owner to our address

Inspecting the contract, we can see there are only 2 places where the owner variable is updated. It is updated in both the contribute function and in the receive function but contributing to the contract isn’t enough to set owner to our address because it requires that the amount contributed is greater than that of the present owner (which is 1000eth) yet each contribution must be less than 0.001 eth.

Looking at the receive function, one of the requirements is that the contributions of msg.sender > 0 so we call the contribute function with wei less than 0.001 eth to set that.

Now that the contributions[msg.sender] > 0, we can now proceed to call the receive function with some amount of wei to satisfy the first condition and set owner to msg.sender.

Ps: To interact with the receive function on Remix, we have to call the low level interaction Transact with empty call data.

Stealing the eth balance.

After owner has been set to our address, we can then call the withdraw function and the eth gets sent to our address (msg.sender).