Ethernaut Elevator Challenge (11)

Enebeli david
5 min readJun 12, 2022

--

If you have attempted Challenge 11 of the Ethernaut game and didn’t have much success or perhaps you were successful but you care about another perspective for learning purposes, then this article is for you. 😀

If this is the first time you are hearing of Ethernaut games, it’s a great game to help you test your knowledge of solidity 💪. Click here to learn more.

The Challenge:

The value of top seems destined to always equal false. That means even if the floor is the topmost floor, the Elevator just won’t declare top to be true

Elevator’s goTo function needs a false returned from isLastFloor to access the critical section for setting the value of top. After accessing the critical section, the return of another call to isLastFloor is used to set the value of top. Since the value of floor (the param of isLastFloor) has not changed between gaining access to the critical section and setting the value of top, the return of the second call to isLastFloor is expected to remain the same, false, which thus destines the value of top to false.

The Vulnerability to exploit

Absence of state mutability modifiers

Apparently, the isLastFloor function is expected to do a check. Such a function is not supposed to change the state. However, the Building interface does not specify any state mutability modifier to enforce this. So since it’s up to the player to create the Building contract wherein the isLastFloor function is domiciled, the player can write it to change the state. By changing the state, the isLastFloor function can track whether Elevator has made the first call to it or not.

What to do

Write the isLastFloor function such that the first call to it returns a false but the second call returns a true whether or not the value of floor remains same.

The Hack

The Fix

One solution is to update the Building interface used by Elevator contract so that isLastFloor becomes a pure function.

interface Building {   
function isLastFloor(uint) external pure returns (bool);
}

Notice we added the pure state mutability modifier, that way, we expect a run time exception if Elevator uses aBuilding contract containing a non-pure isLastFloor function.

First Lesson

Be sure to use state mutability modifiers to avoid unexpected state changes.

I’m sure you are wondering what the second lesson is 🤔

The Caveat (EVM Vulnerability)

Even with the pure modifier, the isLastFloor function can still be implemented in a malicious way. Why?

“Sometimes solidity is not good at keeping promises”

Ethernaut

What does that mean? Well, the EVM at this time, treats pure and view modifiers the same way under the hood. In other words, even though you use the pure modifier, EVM will still let the function read from state as though it were simply a view function.

How can this EVM-based vulnerability help us write an alternative hack to the Elevator Contract?

What to do

“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()"

Ethernaut

gasleft() is a global function that returns the amount of gas remaining. This value changes from line to line of code. By checking the gasleft() at the beginning and end of a block of code, we can know how much gas was used to execute the block of code. If we can know ahead of time how much gas will be used up at the point of first call to isLastFloor function, we can use that as a check to know whether a call to isLastFloor is the first call or not. We can know the gas used by sending the transaction and using a debugger to monitor how gas is used up during the execution.

Sadly 🙁,

After knowing the amount of gas used up at the first call to isLastFloor, we need to modify the code by adding an if statement in our isLastFloor function that compares actual gas used with our predetermined value. However, Adding this extra piece of code has an impact on the contract size. Funny enough, Contract size affects gas usage even though the particular execution path for a given transaction is not affected by the change made to the contract. This means our predetermined gas usage might now be different from the actual gas usage.

Changing the predetermined value used in the if statement to somehow exactly match the actual gas usage will be like a man chasing his own shadow. This is because even changes to a single value in a contract might impact the contract size, which in turn likely affects the actual gas used.

Happily, 😊

We must not use an exact match in our if statement, a relative match is fine. This means we consider our predetermined gas usage to simply be an upper bound. So keep varying the predetermined gas usage in response to actual gas usage until you have an estimate that works.

What worked for me?

With the aid of remix debugger, given my specific contract, I found that when I assume an upper bound of gas usage at the first call to isLastFloor to be 28000, the actual gas usage at the first call to isLastFloor never hits the upper bound whereas the actual usage at the second call to isLastFloor exceeds that upper bound.

The Hack

The Fix

The fix is the second lesson below 😀

Second Lesson

Don’t trust a contract just because the contract inherits the desired interface. A contract may inherit the interface but still have a malicious implementation. Use only contracts whose implementation you trust.

--

--