Ethernaut is a series of fun challenges that teach you how to hack smart contracts on the Ethereum platform. One of the challenges is called "Fallback." In this challenge, you will need to use your coding skills to exploit a vulnerability in a contract to take control of it and change its behavior.
To beat this challenge, you will need to:
Claim ownership of the contract
Reduce its balance to 0
If you're a beginner, don't worry! You'll learn a lot as you go. Just make sure to try the challenge and then come back to the walkthrough for guidance.
Now, let's analyze the code to see how we can achieve these goals:
Code Analysis
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Fallback {
// ...
}
These are basic things for all the smart contracts we look into.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// 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);
}
// ...
}
The contract has a few state variables, including a mapping
called contributions
and an address
called owner
The constructor
function sets the owner
to the deployer's address and adds the deployer's address to the contributions
mapping
with a value of 1000 ETH. It sets the owner's address ๐ค but but but... we can't call a constructor as
constructor
is a special function that is invoked only once at the time of deployment
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
There is also a modifier
called onlyOwner
which restricts access to certain functions to the owner
only.
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
The contribute
function is payable
(meaning it can receive ETH) and publicly accessible. It updates the amount that a contributor has contributed and if the contributor's contribution is greater than the owner
's a contribution, sets the owner
to the contributor.
However, there is a restriction on the amount that can be contributed (it must be less than 0.001 ETH).
So there might be fewer chances of exploiting it as numerous transactions are needed to be performed to achieve our goal.
function getContribution() public view returns (uint) {
return contributions[msg.sender];
}
The getContribution
function is a view
(meaning it does not change the blockchain state) and publicly accessible. It simply returns the contribution amount for the msg.sender
. This acts like a utility function to check the contribution amount.
function withdraw() public onlyOwner {
payable(owner).transfer(address(this).balance);
}
The withdraw
function is publicly accessible, but can only be called by the owner
. It allows the owner
to transfer the entire contract balance to their own address.
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
Finally, there is a receive
function (also known as a fallback function) which can be called by external contracts and is payable
. It requires the msg.value
to be greater than 0 and the contributions
of the msg.sender
to be greater than 0. If these conditions are met, it sets the owner
to the msg.sender
.
Fallback functions are called when a contract receives ETH without any other function being specified.
There are a few things to note about this code. ๐
First, the logic in the receive
function is questionable โ it only requires the msg.value
to be greater than 0 and the contributions
of the msg.sender
to be greater than 0, which may make it easier to exploit.
Additionally, the contribute
function has a low limit on the amount that can be contributed, which may make it difficult to achieve our goal of claiming ownership of the contract.
With these points in mind, let's think about how we can exploit this contract to achieve our goals. One possibility is to try and call the receive
function with a msg.value
that is greater than 0 and a contributions
value that is also greater than 0. This may allow us to claim ownership of the contract.
Once we have ownership, we can call the withdraw
function to transfer the contract balance to our address, reducing the balance to 0.
Remember, if you get stuck or need help, don't hesitate to come back to this walkthrough for guidance. In the next blog post, we go through the actual challenge hands-on. Good luck! โจ