Skip to content

The goal of this project is to highlight the differences between high-level contract development using Solidity and low-level, more gas-efficient development with Huff.

Notifications You must be signed in to change notification settings

Gurkirat155/Horse-store

Repository files navigation

HorseStore Smart Contract

This repository contains two implementations of the HorseStore smart contract: one in Solidity and one in Huff, a low-level EVM assembly language. The contract is responsible for storing and managing the number of horses. The two main functions are updateNumberOfHorses and viewNumberOfHorses.

Table of Contents

Contract Overview

The Solidity version of the contract defines two simple read and write functions:

  • updateNumberOfHorses(uint256): Updates the stored number of horses.
  • viewNumberOfHorses(): Returns the current number of horses.

Compiled Bytecode Breakdown

The compiled bytecode generated from this contract includes:

  • Contract creation code: Responsible for deploying the contract.
  • Runtime code: Executed once the contract is deployed.

During the contract creation phase, the code checks for a non-zero msg.value, ensuring that the constructor does not accept Ether. If any Ether is sent, the deployment is reverted.

The runtime code includes:

  • Function dispatching logic: Based on the function selector (first 4 bytes of the calldata).
  • Storage and memory management: Handles setting and reading the number of horses.

Huff Version

Huff Contract Overview

The Huff version implements the same functionality but at a much lower level, leveraging EVM opcodes for function dispatching and state management.

  • Function Dispatch: The Huff implementation includes a function dispatcher that identifies whether the function selector corresponds to updateNumberOfHorses or viewNumberOfHorses, and jumps to the appropriate handler.
  • Storage and Memory Management:
    • Storage: The number of horses is stored in a dedicated storage slot (STORAGE_SLOT1).
    • Memory: Data is written to memory for return values, with the free memory pointer used to allocate space.

Solidity Compiler Bytecode Overview

The Solidity compiler converts smart contract code into bytecode, which is executed on the Ethereum Virtual Machine (EVM). The bytecode consists of various opcodes that perform different functions like managing memory, interacting with storage, checking conditions, and processing inputs such as msg.value and calldata. This breakdown provides insight into how the compiler optimizes and secures smart contract deployment and execution.

Contract Creation Code

The initial code in the bytecode is responsible for the deployment process, including memory allocation, checking for any Ether (msg.value) sent during deployment, and returning the runtime code to be stored on-chain.

  • Memory Allocation (PUSH1 0x80, PUSH1 0x40, MSTORE): Sets the free memory pointer to 0x80 by storing the location at position 0x40.
  • Deployment Checks (CALLVALUE, ISZERO, REVERT): The CALLVALUE opcode checks if any Ether was sent during deployment. Since the contract constructor does not accept Ether, the deployment reverts if any msg.value is passed. Otherwise, the code proceeds.
  • CodeCopy and Return: The contract's runtime code is copied into memory and returned for deployment.

Runtime Code

The runtime code represents the actual functionality of the contract after deployment. It handles function dispatching, state management, and interaction with external calls.

  • Memory Initialization (PUSH1 0x80, PUSH1 0x40, MSTORE): Similar to the creation code, sets the free memory pointer to 0x80 by storing the location at position 0x40.
  • Checking for msg.value: Before executing any functions, the code checks if any Ether was sent with the transaction (CALLVALUE, ISZERO, REVERT). This ensures that non-payable functions do not accept Ether, enforcing Solidity’s payable function restrictions.
  • Function Selector & Dispatcher: The bytecode uses the first four bytes of the calldata as a function selector to determine which function to call:
    • Function Selector (CALLDATALOAD, SHR): Loads the first 32 bytes of calldata and extracts the function selector by shifting right.
    • Function Matching (EQ, JUMPI): The bytecode compares the extracted function selector with hardcoded values to determine which function to execute. If the selector matches, the code jumps to the relevant function logic.

Example:

  • Function 0x1afef78d: Updates the number of horses in storage.
  • Function 0x67d41eca: Reads the number of horses from storage.

Detailed Function Handling

Update Number of Horses:

  • Store Data (CALLDATALOAD, SSTORE): If the function selector matches the update function, the bytecode reads the data (number of horses) from calldata and stores it in storage at position 0x00.

View Number of Horses:

  • Retrieve Data (SLOAD): If the selector matches the view function, it retrieves the number of horses from storage, prepares the response in memory, and returns it.

Error Handling and Validation

The Solidity compiler introduces multiple checks to ensure robust execution and error handling:

  • Calldata Size Validation (CALLDATASIZE, LT): Ensures that the calldata contains at least 4 bytes for the function selector. If not, the contract reverts with an error.
  • Data Validation for Update Function: For functions like updateNumberOfHorses, the bytecode checks if sufficient calldata (function selector + 32 bytes of data) is present. If not, the transaction is reverted.

Function Flow Summary

  1. Memory Initialization: Memory is set up to track free memory and store runtime data.
  2. Transaction Validation: The compiler checks for valid msg.value and calldata size to ensure that the correct type of transaction (with or without Ether) is processed.
  3. Function Dispatch: The function selector is extracted, and the corresponding function is invoked based on the match. Each function's logic is implemented directly in bytecode, including reading from or writing to storage.
  4. Return or Revert: Based on the function’s outcome, the contract either returns the result (for view functions) or stores data (for state-modifying functions). If an error occurs, the contract reverts.

Automated Solidity Compiler Checks vs Huff Dispatcher

  • Solidity: Automatically checks the function signature, parameter types, and handles reverts if parameters are invalid (e.g., out-of-bounds array access).
  • Solidity: Manages memory allocation and garbage collection. When returning values from a function, Solidity ensures they are properly stored in memory and returned in the correct format.
  • Solidity: Performs checks that could also be written in Huff at a lower level, though Solidity automates these tasks, simplifying the process while optimizing contract execution.
  • NOTE:- In Solidity, the compiler automatically checks for function signatures, parameter types, and handles reverts when errors occur (e.g., invalid parameter types or missing function signatures). These checks are built into the generated bytecode and ensure that a contract operates within the constraints defined in its interface. However, in Huff, you must manually implement similar functionality. For example, you can manually validate the calldata size to ensure it contains at least the function selector (first 4 bytes) and the necessary parameters before processing the function logic. If the validation fails, you would explicitly use opcodes like REVERT to halt execution. Similarly, handling incorrect or missing function signatures requires you to explicitly compare the function selector bytes and jump to a REVERT operation if none of the expected functions match.

Function Dispatching vs Function Selector in Solidity

  • In Solidity, function dispatching is handled automatically by the compiler using the function selector. The first 4 bytes of the calldata are extracted and compared with the expected function selectors. Based on the match, the appropriate function is invoked. This dispatching logic is abstracted away from the developer, making it easier to write and maintain smart contracts without worrying about low-level calldata manipulation.

  • In contrast, Huff requires you to manually extract and compare the function selector. You must implement the dispatching logic by checking the first 4 bytes of calldata, matching them to specific functions, and jumping to the corresponding function’s logic in the bytecode. This provides greater control over the execution flow but requires careful attention to detail.

About

The goal of this project is to highlight the differences between high-level contract development using Solidity and low-level, more gas-efficient development with Huff.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published