Ethernaut Lvl 11 Elevator Walkthrough: How to abuse Solidity interfaces and function state modifiers
This is a in-depth series around Zeppelin team’s smart contract security puzzles. We learn key Solidity concepts to solve the puzzles 100% on your own.
This levels requires you to take the elevator to the top floor.
What are Pure & View functions
Solidity functions have function modifiers which execute at the start of each function call. You are already familiar with visibility modifiers such as public
and private
, which determine who gets to invoke these functions.
Similarly, pure
and view
are built-in state modifiers. They “promise” how functions will interact with data on Ethereum blockchain, i.e. the state.
In order of increasing state permissions:
pure
: promises functions that will neither read from nor modify the state. Note: Pure replacesconstant
in more recent compilers.view
: promises functions that will only read, but not modify the statedefault
: [no modifier] promises functions that will read and modify the the state
In theory, you should follow modifier best practices (see below):
Security warning: in earlier compiler versions, it allows and doesn’t give warnings when the function betrays their modifier promise. So a pure
function can break its promise and modify function state, without any warning.
It’s important to treat these data modifiers as promises on data mutability, rather than guarantees.
What are Interfaces
Interfaces allow different contract classes to talk to each other.
Think of interfaces as an ABI (or API) declaration that forces contracts to all communicate in the same language/data structure. But interfaces do not prescribe the logic inside the functions, leaving the developer to implement his own business layer.
Contract Interfaces specifies the WHAT but not the HOW
Developers typically use interfaces:
- To design contracts: by generating a working ABI first, before implementing the actual contract.
- For token contracts: by declaring a shared language, so different contracts can use these tokens to handle their business logic.
- Not used: some developer want to scrap interfaces altogether, in favor of abstract classes*.
*Note: Abstract classes share similar security vulnerabilities with interfaces. In abstract contracts some functions are already programmed, but can be easily overridden.
Detailed Walkthrough
To pass this level, we have to make this check first return true and then return false within a single goTo()
function call:
// 1st check has to return false
if (! building.isLastFloor(_floor)) {
floor = _floor;// 2nd check has to return true
top = building.isLastFloor(floor); //winning statement
}
Notice Elevator.sol never implemented the isLastFloor()
function from the Building interface.
This means we can create a malicious contract called Building that implements this function. Then, if we invoke goto() from the malicious contract, our version of the isLastFloor
function will be used in the context of our level’s Elevator.sol instance!
- Below
Elevator.sol
, create aBuilding.sol
contract that eventually invokesElevator.goto()
on some arbitrary floor:
contract Building {
Elevator public el = Elevator(YOUR_LVL_INSTANCE); function hack() public {
el.goTo(1);
}
}
2. Implement isLastFloor
with a switch that guarantees a true, then false response. Make sure your function declaration and modifiers are consistent with the interface:
3. Using Remix, invoke hack() to topple all the dominos. Notice that although isLastFloor
promised to be a pure
function, it did change blockchain state.
A quick await contract.top()
now reveals we are at the top floor.
Key Security Takeaways
- Interfaces do not guarantee contract security. Remember that just because another contract uses the same interface, doesn’t mean it will behave as intended!
- Be careful when inheriting contracts that extend from interfaces. Each layer of abstraction introduces security issues through information obscurity. This makes each generation of the contract less and less secure than the previous.
- What out for the compiler version you are using or inheriting from.
View
andpure
promises might be violated without you knowing!
“Solidity compiler does nothing to enforce that a
view
orconstant
function is not modifying state. The same applies topure
functions, which should not read state but they can. Make sure you read Solidity's documentation and learn its caveats.” — Ethernaut