Solidity
Zach Lafeer
Smart Contract Development
Overview
● Ethereum Virtual Machine
● Account Types
● Solidity Contracts
● Memory Management and Data Types
● Functions and Modifiers
● Error Handling
About Myself
● Undergraduate student at Purdue University
○ Majoring in Computer Science and Data Science (BS 2024)
● Background in Machine Learning
● Inspired by coursework in Computer Architecture and
Algorithm Optimization
Ethereum Virtual Machine (EVM)
● Ethereum is a distributed state machine
○ Each node stores and agrees on the current machine state
● Current state stores all account information
● EVM defines how a new block can alter the state
○ State changes are invoked via transactions
● Executes as a stack machine with unique opcodes
○ Every opcode costs gas to execute, which is paid with ETH
● Turing-Complete
Accounts
● Each account contains four fields:
○ Nonce (no. of transactions sent)
○ Balance
○ Code
○ Storage
● Externally owned accounts are controlled by private keys
○ Do not contain code
● Contract accounts are controlled by contract code
○ Do not have private keys
Smart Contracts
● Accounts that execute arbitrary code on the blockchain
○ Code execution begins when called by an external account
○ Contracts do not necessarily perform the same functions as a legal contract
● All code is publicly visible
● Cannot be modified after deployment
● Compiled bytecode is limited in size
● Serve as the core for Decentralized Applications
Solidity
● A high-level language for programming in the EVM
● Influenced by C++, Python, JavaScript
○ Statically typed
○ Object-oriented
○ Inheritance
○ Libraries
● Rapidly developing with frequent breaking changes
● Covering version 0.8.x
Solidity Contract Structure
● State Variables
● Constructor
● Functions
● Events
● Errors
● Structs and Enums
Data Sections
● Storage
○ Persistent part of EVM state and stored on the blockchain
○ Requires a transaction to modify
○ High gas costs for read/write
● Memory
○ Temporary section provided to a function call
○ Only accessible within functions
○ Lower gas consumption
● Calldata
○ Temporary and immutable section for function arguments
Data Types
● Integer sizes are supported by multiples of 8 bits up to 256 bits
○ All reads are 256 bits wide
○ Default size is 256 bits
● State variables can be declared as constant or immutable
○ Cannot be modified after instantiation
○ Constants must be known at compile time
○ Immutables can be assigned in the constructor
○ Consume less gas
● Public state variables have compiler generated getter functions
Functions
● Usually defined within contract
○ Free functions are still executed in the context of the calling contract
● Multiple return values are supported
○ Helpful for limiting external function calls when retrieving many variables
● View functions cannot write to storage
○ Cost zero gas when called externally
● Pure functions cannot read from or write to storage
○ Cost zero gas when called externally
Function Modifiers
● Reusable code to modify the behavior of a function
● Typically used to perform common checks
○ i.e. permissions, data validation, etc.
● onlyOwner modifier restricts function access to contract deployer
○ Implemented via inheritance from OpenZeppelin’s Ownable contract
Function Visibility
● External
○ Can only be called from outside the contract
● Public
○ Can be called from anywhere
● Internal
○ Can only be called from within the contract or any inheriting contracts
● Private
○ Can only be called from within the contract
Error Handling
● Require checks a condition and refunds gas upon failure
○ Used for verifying valid inputs
● Assert checks a condition and consumes all gas upon failure
○ Used sparingly to catch catastrophic/unlikely errors
● Exceptions thrown by these commands cause EVM state to revert
● Care is required when checking for errors from calls to other contracts
○ Updating state variables after interaction call can lead to reentrancy problem
○ Assume success and update state before making calls to external contracts
○ Called the Checks-Effects-Interactions pattern
Gas Optimization
Overview
● Gas Costs
● External View Functions
● Storage Packing
● Unchecked Arithmetic
● Inline Assembly (Yul)
● External Functions
● Compiler Optimization
Computational Expense
● Traditional optimization focuses on time/space efficiency
○ Run time is less important due to slow mining speed
○ Space efficiency is still important, particularly in storage
● Gas is the meaningful expense on Ethereum
○ Every operation performed on the EVM costs ether
○ Users may have to pay to interact with a contract
● Different rules for gas optimization compared to Von Neumann architecture
○ Gas costs do not always correspond to traditional notions of expense
Operation Gas Costs
Arithmetic Storage Access
Memory Access External Function Calls
Account Creation
Cheap Expensive
External View Functions
● When called by externally owned accounts, view functions are free
○ These functions can be either external or public
○ Still cost gas when called internally or by another contract
● Often favorable to repeatedly compute information within a view function
○ Minimizes state variables on the blockchain
○ Example: Compute mean of list on request, rather than storing mean
● Pure functions follow the same rules for gas prices
○ However offer less utility as they cannot read from storage
Storage Packing
● Storage is divided into 32 byte slots
● Smaller variables can be packed into a single slot
○ Packing is done in order of declaration
○ Variables will never be split across several slots
● Packing can reduce storage usage
● Gas prices for read can increase as a consequence
○ Gas is spent cleaning extraneous bits when accessing just one variable
○ If variables are accessed concurrently, gas may be saved
Unpackable Packed
uint128 a;
uint256 b;
uint128 c;
uint128 a;
uint128 c;
uint256 b;
a b c
Slot 0 Slot 1 Slot 2
a b [empty]
Slot 0 Slot 1 Slot 2
c
Unchecked Arithmetic
● Beginning in v0.8.0, all arithmetic is checked for overflow and underflow
○ Either condition will throw an exception and refund gas
○ Checked arithmetic calls additional instructions that consume gas
● Unchecked blocks bypass these checks to save gas
○ Used if over/underflow is desired or not a risk
○ Example: Incrementing an index for a small fixed-size array
Inline Assembly
● Yul is a low-level intermediate language between Solidity and EVM
○ Offers high-level control flow and readable syntax
● Assembly blocks can be used to bypass additional checks
○ Used with caution as security and memory checks are skipped as well
○ Fine-grained control can produce additional gas savings
○ Example: Bypass array bounds check during iteration
● Arithmetic is unchecked by default in assembly
External Function Calls
● Calls to other contracts are expensive
○ View functions are not free when called by another contract
● Multiple contracts are sometimes required
○ A single contract can have a maximum of 24.5 KB of bytecode
● EIP-2535: Diamonds
○ Lowers gas costs from inter-contract communication
○ Provides framework for upgradability
○ Higher amounts of bytecode can be reachable from one address
Compiler Optimization
● Trade-off between deployment cost and execution cost
○ Gas optimizations can produce longer contract bytecode
○ Execution cost from calling the contract is reduced by such optimizations
○ One-time deployment cost is higher for larger amounts of bytecode
● Runs parameter indicates how many times code is expected to be called
○ Lower values will produce code that favors low deployment cost
○ Higher values will favor low execution cost
Code Example
● Sample contract with 5 levels of
optimization
● Each implementation computes
sum of a dynamic array
● Gas costs applicable when called
by another contract
● Detailed comments at:
github.com/zlafeer/solidity-optimization
// Constructs dynamic array
// of n unsigned integers
uint[] private storageArray;
constructor(uint n) {
for (uint i = 0; i < n; i++) {
storageArray .push(i%10);
}
}
// Naive
// 100% Gas Cost
function sumA() public view returns (uint) {
uint sum = 0;
for (uint i = 0; i < storageArray.length; i++) {
sum += storageArray[i];
}
return sum;
}
// Memory Caching
// ~93% Gas Cost
function sumB() public view returns (uint) {
uint[] memory memoryArray = storageArray;
uint sum = 0;
for (uint i = 0; i < memoryArray.length; i++) {
sum += memoryArray[i];
}
return sum;
}
// Unchecked Arithmetic
// ~91% Gas Cost
function sumC() public view returns (uint) {
uint[] memory memoryArray =
storageArray;
uint sum = 0;
uint i = 0;
while (i < memoryArray.length) {
sum += memoryArray[i];
unchecked {
i++;
}
}
return sum;
}
// Inline Assembly
// ~87% Gas Cost
function sumD() public view returns (uint) {
uint[] memory memoryArray = storageArray;
uint sum = 0;
uint i = 0;
while (i < memoryArray.length) {
assembly {
sum := add (sum, mload(add(add(memoryArray, 0x20), mul(i, 0x20))))
i := add (i, 0x01)
}
}
return sum;
}
// Full Assembly
// ~85% Gas Cost
function sumE() public view returns (uint) {
assembly {
let pointer := keccak256(storageArray.slot, 0x20)
let length := sload (storageArray.slot)
let sum := 0
for { let i := 0 } lt(i, length) { i := add(i, 0x01) }
{
sum := add (sum, sload(add(pointer, i)))
}
mstore (0x00, sum)
return(0x00, 0x20)
}
}
Summary
● High gas costs for persistent storage
○ Sometimes advantageous to recompute derived data
● Large tradeoff between optimization and readability
○ Inline assembly offers large gas savings
○ Compromises explainability and maintenance
● Decision to optimize for deployment or execution cost
○ Larger bytecode footprint increases deployment costs
● Room for improvement in Solidity compiler optimization
○ Gas savings from inline assembly may decrease as the optimizer improves

Solidity and Ethereum Smart Contract Gas Optimization

  • 1.
  • 2.
    Overview ● Ethereum VirtualMachine ● Account Types ● Solidity Contracts ● Memory Management and Data Types ● Functions and Modifiers ● Error Handling
  • 3.
    About Myself ● Undergraduatestudent at Purdue University ○ Majoring in Computer Science and Data Science (BS 2024) ● Background in Machine Learning ● Inspired by coursework in Computer Architecture and Algorithm Optimization
  • 4.
    Ethereum Virtual Machine(EVM) ● Ethereum is a distributed state machine ○ Each node stores and agrees on the current machine state ● Current state stores all account information ● EVM defines how a new block can alter the state ○ State changes are invoked via transactions ● Executes as a stack machine with unique opcodes ○ Every opcode costs gas to execute, which is paid with ETH ● Turing-Complete
  • 5.
    Accounts ● Each accountcontains four fields: ○ Nonce (no. of transactions sent) ○ Balance ○ Code ○ Storage ● Externally owned accounts are controlled by private keys ○ Do not contain code ● Contract accounts are controlled by contract code ○ Do not have private keys
  • 6.
    Smart Contracts ● Accountsthat execute arbitrary code on the blockchain ○ Code execution begins when called by an external account ○ Contracts do not necessarily perform the same functions as a legal contract ● All code is publicly visible ● Cannot be modified after deployment ● Compiled bytecode is limited in size ● Serve as the core for Decentralized Applications
  • 7.
    Solidity ● A high-levellanguage for programming in the EVM ● Influenced by C++, Python, JavaScript ○ Statically typed ○ Object-oriented ○ Inheritance ○ Libraries ● Rapidly developing with frequent breaking changes ● Covering version 0.8.x
  • 8.
    Solidity Contract Structure ●State Variables ● Constructor ● Functions ● Events ● Errors ● Structs and Enums
  • 9.
    Data Sections ● Storage ○Persistent part of EVM state and stored on the blockchain ○ Requires a transaction to modify ○ High gas costs for read/write ● Memory ○ Temporary section provided to a function call ○ Only accessible within functions ○ Lower gas consumption ● Calldata ○ Temporary and immutable section for function arguments
  • 10.
    Data Types ● Integersizes are supported by multiples of 8 bits up to 256 bits ○ All reads are 256 bits wide ○ Default size is 256 bits ● State variables can be declared as constant or immutable ○ Cannot be modified after instantiation ○ Constants must be known at compile time ○ Immutables can be assigned in the constructor ○ Consume less gas ● Public state variables have compiler generated getter functions
  • 11.
    Functions ● Usually definedwithin contract ○ Free functions are still executed in the context of the calling contract ● Multiple return values are supported ○ Helpful for limiting external function calls when retrieving many variables ● View functions cannot write to storage ○ Cost zero gas when called externally ● Pure functions cannot read from or write to storage ○ Cost zero gas when called externally
  • 12.
    Function Modifiers ● Reusablecode to modify the behavior of a function ● Typically used to perform common checks ○ i.e. permissions, data validation, etc. ● onlyOwner modifier restricts function access to contract deployer ○ Implemented via inheritance from OpenZeppelin’s Ownable contract
  • 13.
    Function Visibility ● External ○Can only be called from outside the contract ● Public ○ Can be called from anywhere ● Internal ○ Can only be called from within the contract or any inheriting contracts ● Private ○ Can only be called from within the contract
  • 14.
    Error Handling ● Requirechecks a condition and refunds gas upon failure ○ Used for verifying valid inputs ● Assert checks a condition and consumes all gas upon failure ○ Used sparingly to catch catastrophic/unlikely errors ● Exceptions thrown by these commands cause EVM state to revert ● Care is required when checking for errors from calls to other contracts ○ Updating state variables after interaction call can lead to reentrancy problem ○ Assume success and update state before making calls to external contracts ○ Called the Checks-Effects-Interactions pattern
  • 15.
  • 16.
    Overview ● Gas Costs ●External View Functions ● Storage Packing ● Unchecked Arithmetic ● Inline Assembly (Yul) ● External Functions ● Compiler Optimization
  • 17.
    Computational Expense ● Traditionaloptimization focuses on time/space efficiency ○ Run time is less important due to slow mining speed ○ Space efficiency is still important, particularly in storage ● Gas is the meaningful expense on Ethereum ○ Every operation performed on the EVM costs ether ○ Users may have to pay to interact with a contract ● Different rules for gas optimization compared to Von Neumann architecture ○ Gas costs do not always correspond to traditional notions of expense
  • 18.
    Operation Gas Costs ArithmeticStorage Access Memory Access External Function Calls Account Creation Cheap Expensive
  • 19.
    External View Functions ●When called by externally owned accounts, view functions are free ○ These functions can be either external or public ○ Still cost gas when called internally or by another contract ● Often favorable to repeatedly compute information within a view function ○ Minimizes state variables on the blockchain ○ Example: Compute mean of list on request, rather than storing mean ● Pure functions follow the same rules for gas prices ○ However offer less utility as they cannot read from storage
  • 20.
    Storage Packing ● Storageis divided into 32 byte slots ● Smaller variables can be packed into a single slot ○ Packing is done in order of declaration ○ Variables will never be split across several slots ● Packing can reduce storage usage ● Gas prices for read can increase as a consequence ○ Gas is spent cleaning extraneous bits when accessing just one variable ○ If variables are accessed concurrently, gas may be saved
  • 21.
    Unpackable Packed uint128 a; uint256b; uint128 c; uint128 a; uint128 c; uint256 b; a b c Slot 0 Slot 1 Slot 2 a b [empty] Slot 0 Slot 1 Slot 2 c
  • 22.
    Unchecked Arithmetic ● Beginningin v0.8.0, all arithmetic is checked for overflow and underflow ○ Either condition will throw an exception and refund gas ○ Checked arithmetic calls additional instructions that consume gas ● Unchecked blocks bypass these checks to save gas ○ Used if over/underflow is desired or not a risk ○ Example: Incrementing an index for a small fixed-size array
  • 23.
    Inline Assembly ● Yulis a low-level intermediate language between Solidity and EVM ○ Offers high-level control flow and readable syntax ● Assembly blocks can be used to bypass additional checks ○ Used with caution as security and memory checks are skipped as well ○ Fine-grained control can produce additional gas savings ○ Example: Bypass array bounds check during iteration ● Arithmetic is unchecked by default in assembly
  • 24.
    External Function Calls ●Calls to other contracts are expensive ○ View functions are not free when called by another contract ● Multiple contracts are sometimes required ○ A single contract can have a maximum of 24.5 KB of bytecode ● EIP-2535: Diamonds ○ Lowers gas costs from inter-contract communication ○ Provides framework for upgradability ○ Higher amounts of bytecode can be reachable from one address
  • 25.
    Compiler Optimization ● Trade-offbetween deployment cost and execution cost ○ Gas optimizations can produce longer contract bytecode ○ Execution cost from calling the contract is reduced by such optimizations ○ One-time deployment cost is higher for larger amounts of bytecode ● Runs parameter indicates how many times code is expected to be called ○ Lower values will produce code that favors low deployment cost ○ Higher values will favor low execution cost
  • 26.
    Code Example ● Samplecontract with 5 levels of optimization ● Each implementation computes sum of a dynamic array ● Gas costs applicable when called by another contract ● Detailed comments at: github.com/zlafeer/solidity-optimization // Constructs dynamic array // of n unsigned integers uint[] private storageArray; constructor(uint n) { for (uint i = 0; i < n; i++) { storageArray .push(i%10); } }
  • 27.
    // Naive // 100%Gas Cost function sumA() public view returns (uint) { uint sum = 0; for (uint i = 0; i < storageArray.length; i++) { sum += storageArray[i]; } return sum; }
  • 28.
    // Memory Caching //~93% Gas Cost function sumB() public view returns (uint) { uint[] memory memoryArray = storageArray; uint sum = 0; for (uint i = 0; i < memoryArray.length; i++) { sum += memoryArray[i]; } return sum; }
  • 29.
    // Unchecked Arithmetic //~91% Gas Cost function sumC() public view returns (uint) { uint[] memory memoryArray = storageArray; uint sum = 0; uint i = 0; while (i < memoryArray.length) { sum += memoryArray[i]; unchecked { i++; } } return sum; }
  • 30.
    // Inline Assembly //~87% Gas Cost function sumD() public view returns (uint) { uint[] memory memoryArray = storageArray; uint sum = 0; uint i = 0; while (i < memoryArray.length) { assembly { sum := add (sum, mload(add(add(memoryArray, 0x20), mul(i, 0x20)))) i := add (i, 0x01) } } return sum; }
  • 31.
    // Full Assembly //~85% Gas Cost function sumE() public view returns (uint) { assembly { let pointer := keccak256(storageArray.slot, 0x20) let length := sload (storageArray.slot) let sum := 0 for { let i := 0 } lt(i, length) { i := add(i, 0x01) } { sum := add (sum, sload(add(pointer, i))) } mstore (0x00, sum) return(0x00, 0x20) } }
  • 32.
    Summary ● High gascosts for persistent storage ○ Sometimes advantageous to recompute derived data ● Large tradeoff between optimization and readability ○ Inline assembly offers large gas savings ○ Compromises explainability and maintenance ● Decision to optimize for deployment or execution cost ○ Larger bytecode footprint increases deployment costs ● Room for improvement in Solidity compiler optimization ○ Gas savings from inline assembly may decrease as the optimizer improves