Fallback - The Ethernaut - writeup
The goal of this challenge is to:
- Claim ownership of the contract
- 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:
-
After connecting our Metamask wallet, click on “Get New Instance”:

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

-
Now, we can interact with the contract using the javascript console via the
contractobject.
For example, if we want to calll the getContribution function, we can execute this line:
await contract.getContribution()

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
receivefunction is a special function in solidity smart contracts. It is anexternal payablefunction 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, thereceivefunction 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()

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:

