2. Overview
● Ethereum Virtual Machine
● Account Types
● Solidity Contracts
● Memory Management and Data Types
● Functions and Modifiers
● Error Handling
3. 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
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 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
6. 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
7. 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
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
● 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
11. 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
12. 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
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
● 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
17. 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
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
● 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
21. 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
22. 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
23. 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
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-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
26. 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);
}
}
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 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