Fallout is the shortest level in Ethernaut and the one people stare at longest. The exploit is a single call with no arguments. Finding it means noticing one character. The goal is to claim ownership.
Try it first in the browser: solve Fallout in the SCH lab. The original is Ethernaut level 2.
The contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "openzeppelin-contracts-06/math/SafeMath.sol";
contract Fallout {
using SafeMath for uint256;
mapping(address => uint256) 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 collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}
}
Read the function name again, one character at a time.
The contract is called Fallout. The function is called Fal1out. The second l is the digit 1.
Why one character matters
Before Solidity 0.5.0, a constructor was a function carrying the same name as the contract. Write function Fallout() inside contract Fallout and the compiler treats it as the constructor: it runs once at deployment and is never callable again.
Fal1out is not Fallout. The compiler compares the two strings, finds they differ, and treats it as a normal public function.
Two things follow. It never runs at deployment, so owner stays at the default address 0x0000...0000 and nobody owns the contract when it goes live. And because it is public with no access control, any address can call it and take the owner slot.
The /* constructor */ comment above it is what makes this level worth its slot. The compiler does not read comments. Somebody wrote down what they intended, shipped something different, and left the note behind to make the mistake harder to see.
{
"title": "🎬 What the developer meant versus what shipped",
"stage": { "width": 920, "height": 440 },
"nodes": [
{ "id": "dev", "label": "Developer", "role": "deploys the contract", "emoji": "🧑💻", "x": 60, "y": 55, "color": "purple" },
{ "id": "target", "label": "Fallout", "role": "owner state", "emoji": "📜", "x": 430, "y": 275, "color": "cyan" },
{ "id": "attacker", "label": "Any address", "role": "reads the ABI", "emoji": "🧑💻", "x": 780, "y": 55, "color": "red" }
],
"links": [
{ "from": "dev", "to": "target" },
{ "from": "attacker", "to": "target" },
{ "from": "target", "to": "attacker" }
],
"nets": [
{ "id": "own", "label": "owner" },
{ "id": "funds", "label": "Allocated funds" }
],
"legend": [
{ "cls": "call", "label": "function call" },
{ "cls": "token", "label": "ETH transfer" },
{ "cls": "sig", "label": "state write" },
{ "cls": "fail", "label": "unreachable" }
],
"scenarios": {
"What the developer intended": [
{ "note": "The plan: a constructor named after the contract, the pre-0.5.0 convention.", "hi": ["dev"], "net": { "own": "nobody yet", "funds": "0" } },
{ "note": "Deployment runs the constructor exactly once and sets <code>owner</code> to the deployer.", "tone": "ok", "hi": ["dev","target"], "chip": { "from": "dev", "to": "target", "label": "🚀 deploy → constructor", "cls": "sig" }, "bal": { "target": "owner = developer" }, "net": { "own": "developer" } },
{ "note": "A constructor cannot be called again after deployment. The owner slot is settled forever.", "tone": "ok", "hi": ["attacker","target"], "chip": { "from": "attacker", "to": "target", "label": "⛔ not in the ABI", "cls": "fail" }, "net": { "own": "developer" } },
{ "note": "Users allocate funds. Only the developer can ever collect them.", "tone": "ok", "hi": ["target"], "bal": { "target": "owner = developer" }, "net": { "funds": "safe" } }
],
"What actually shipped": [
{ "note": "The function is spelled <b>Fal1out</b> with a digit. The compiler compares it to <b>Fallout</b> and sees two different names.", "tone": "bad", "hi": ["dev"], "net": { "own": "0x000...000", "funds": "0" } },
{ "note": "Deployment runs no constructor at all. <code>owner</code> keeps its default value, the zero address.", "tone": "bad", "hi": ["dev","target"], "chip": { "from": "dev", "to": "target", "label": "🚀 deploy, no init", "cls": "sig" }, "bal": { "target": "owner = 0x0" } },
{ "note": "<code>Fal1out()</code> compiled into a public function, so it appears in the ABI for anyone to read.", "tone": "bad", "hi": ["attacker","target"] },
{ "note": "The attacker calls it. There is no access control, so the write goes straight through.", "tone": "bad", "hi": ["attacker","target"], "chip": { "from": "attacker", "to": "target", "label": "📞 Fal1out()", "cls": "call" }, "bal": { "target": "owner = attacker" }, "net": { "own": "attacker" } },
{ "note": "<code>collectAllocations()</code> now passes <code>onlyOwner</code>, and every allocated deposit leaves the contract.", "tone": "bad", "hi": ["target","attacker"], "chip": { "from": "target", "to": "attacker", "label": "💸 full balance", "cls": "token" }, "net": { "funds": "drained" } }
]
}
}
The exploit
The lab hands you the deployed contract as falloutInstance. The solution is one line:
function run() external {
vm.startBroadcast(PLAYER_PRIVATE_KEY);
falloutInstance.Fal1out();
vm.stopBroadcast();
}
That is the level. owner becomes your address. You do not need to send value: Fal1out() is payable, but nothing in the win condition looks at msg.value.
Watch the walkthrough
This one really happened
In 2016 a pyramid-scheme contract called Rubixi lost its accumulated fees to exactly this bug. The code was written under the name DynamicPyramid, and when the authors renamed the contract to Rubixi they left the constructor named DynamicPyramid(). It stopped being a constructor and became a public function in the ABI. Anyone who called it became the registered creator and could withdraw the collected fees.
Why you still need this
Solidity closed the specific hole long ago. The constructor keyword arrived in 0.4.22, and from 0.5.0 the old name-based form is a compile error.
It stays relevant because plenty of deployed contracts still run old compilers, and auditing legacy systems and forks is ordinary paid work. The wider point: any time correctness depends on two strings matching, a typo becomes a vulnerability instead of a bug that fails loudly. Function names in low-level calls and role identifiers derived from string hashes have the same shape.
The habit is small. When a comment claims something about the code, check that the code agrees. /* constructor */ sat directly above a function that was not one.
Keep going
Back to Fallback for level 1, or on to Coin Flip. Every level runs in the SCH CTF lab, and the Web3 CTF challenge list has the other wargames. Access control failures are the most common findings in real audit reports, and most look more like Rubixi than like a clever cryptographic break. The Smart Contract Hacking course covers the class on production code.