Solidity Cheatsheet

Gas and Optimization

Use this Solidity reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Gas Basics

  • Gas is the unit of computation on the EVM. Every opcode costs a defined amount.
  • Gas price (in gwei) × gas used = ETH paid to validator.
  • gasleft() returns remaining gas in the current call.
  • Transactions have a gas limit (set by caller) and a block gas limit (set by network ~30M on mainnet).

Opcode Cost Quick Reference

OperationGas (approximate)
Zero → non-zero SSTORE (cold)22 100
Non-zero → non-zero SSTORE (cold)5 000
Non-zero → zero SSTORE5 000 (+ 4 800 refund)
SLOAD (cold)2 100
SLOAD (warm)100
External call (CALL)2 600 base
Memory expansion (per 32 bytes)3 × n + n²/512
LOG (event, 3 topics)~1 875 base + 8/byte
KECCAK25630 + 6/word
ADD / MUL3 / 5
EXP10 + 50/byte of exponent

Storage Optimization

Pack Variables

// BAD — 3 storage slots
struct Wasteful {
    uint256 a;   // slot 0
    uint8   b;   // slot 1 (wastes 31 bytes)
    uint256 c;   // slot 2
}

// GOOD — 2 storage slots
struct Packed {
    uint256 a;   // slot 0
    uint8   b;   // slot 1 (byte 0)
    uint8   c;   // slot 1 (byte 1)
    uint240 d;   // slot 1 (bytes 2–31)
}

Cache Storage in Memory

// BAD — SLOAD on every iteration
for (uint i; i < items.length; ++i) { // items.length = SLOAD each time
    total += items[i]; // SLOAD each time
}

// GOOD — 1 SLOAD for length, fewer for items
uint256 len = items.length;
for (uint i; i < len; ++i) {
    total += items[i];
}

constant and immutable

// constant — compiler-inlined, zero SLOAD
uint256 constant MAX = 10_000;

// immutable — stored in bytecode, CODECOPY instead of SLOAD
address immutable public factory;

// Regular state variable — SLOAD (2100 cold, 100 warm)
address public factorySlow;

Zero vs Non-Zero Storage

// Setting to non-zero costs 22 100 gas (cold slot)
// Setting non-zero → non-zero costs 2 900 gas (warm)
// Setting to zero costs 5 000 gas but gives 4 800 refund
// Use 1/2 instead of false/true for reentrancy guards:

uint256 private _locked = 1; // non-zero init
modifier nonReentrant() {
    require(_locked == 1);
    _locked = 2; // non-zero → non-zero: cheap
    _;
    _locked = 1; // non-zero → non-zero: cheap
}

unchecked Arithmetic

// 0.8+ checks overflow by default — adds ~80-200 gas per op
// Use unchecked when you can PROVE no overflow:

function sum(uint256[] calldata arr) external pure returns (uint256 total) {
    uint256 len = arr.length;
    for (uint256 i; i < len;) {
        unchecked {
            total += arr[i]; // no overflow check
            ++i;             // no overflow check on loop counter
        }
    }
}

Calldata vs Memory vs Storage

// cheapest for external array inputs: calldata (no copy)
function processCalldata(uint256[] calldata data) external pure { }

// memory: copied from calldata, mutable but costs more
function processMemory(uint256[] memory data) public pure { }

// storage pointer: references state — no copy, but SLOAD/SSTORE
function processStorage() external {
    uint256[] storage ref = myArray; // pointer, not a copy
}

Short-Circuiting

// Put cheapest / most likely to fail check FIRST
function check(address token, address user, uint256 amount) external view returns (bool) {
    return amount > 0                     // cheap: no SLOAD
        && amount <= MAX_AMOUNT           // cheap: constant
        && IERC20(token).balanceOf(user) >= amount; // expensive: external call
}

Events vs Storage

// SSTORE (non-zero): 22 100 gas
// LOG3 (3 topics + 32 bytes data): ~2 000 gas

// Use events for historical/audit data you don't need on-chain:
event Transfer(address indexed from, address indexed to, uint256 value);

// Only store what you actually need to read on-chain
mapping(address => uint256) public balances; // needed for on-chain logic
// transaction history → use events, NOT storage

Loop Gas Patterns

// Prefer ++i over i++ (saves 1 gas — no temp copy)
for (uint i; i < len; ++i) { }

// Cache array length
uint256 n = arr.length;
for (uint i; i < n; ++i) { }

// Reverse loop (avoids comparison with non-zero for uint)
for (uint i = arr.length; i != 0;) {
    unchecked { --i; }
    process(arr[i]);
}

calldata Encoding Efficiency

// Each zero byte in calldata = 4 gas; non-zero = 16 gas (EIP-2028)
// Prefer function args that produce many zero bytes where possible
// This is why many protocols left-shift addresses and amounts

bytes32 vs string

// bytes32: fixed size, 1 storage slot, no dynamic allocation
bytes32 public constant NAME = "MyToken"; // cheap

// string: dynamic, pointer + length, 2+ SLOADs for short strings
string public name = "MyToken"; // expensive to read/compare

Bit Packing in a Single uint256

// Pack multiple values into one uint256 to save slots
// e.g., uint128 price + uint128 tokenId in one 256-bit word

uint256 packed;

function setPricedToken(uint128 price, uint128 tokenId) external {
    packed = (uint256(price) << 128) | uint256(tokenId);
}

function getPrice() external view returns (uint128) {
    return uint128(packed >> 128);
}

function getTokenId() external view returns (uint128) {
    return uint128(packed);
}

Function Selector Gas

// Functions with selectors starting with 0x00 are ~22 gas cheaper per call
// (4 zero bytes in calldata vs 4 non-zero bytes)
// Solidity/Yul miners sometimes reorder selectors for savings (niche)

view and pure (Off-Chain Free)

// Called off-chain (eth_call) = ZERO gas
function getBalance(address user) external view returns (uint256) {
    return balances[user];
}

// But called internally from a state-changing tx: costs normal gas

Gas Estimation and Limits

// Leave buffer in gas limit:
// actual_gas_used < gas_limit or tx fails (OOG)

// Gas reporting in Foundry:
// forge test --gas-report
// forge snapshot --check

// Foundry invariant test:
// assertLt(gasleft() after op, EXPECTED_LIMIT);

Minimal Proxy (EIP-1167)

// Deploy cheap clones of a contract — 45-byte bytecode proxy
// Each clone delegates all calls to the implementation

// OpenZeppelin Clones library:
address clone = Clones.clone(implementation);
// Or with salt for deterministic address:
address clone2 = Clones.cloneDeterministic(implementation, salt);

Key Rules Summary

TipSaving
Pack storage structs1 SSTORE/SLOAD per eliminated slot
Cache storage reads in memorySLOAD: 2100 → 100 (warm)
Use calldata not memory for external arraysavoids copy (~3 gas/byte)
Use constant / immutableeliminates SLOAD entirely
Use unchecked for safe arithmetic~80-200 gas per op
Use custom errors over strings~250 gas per revert + deployment savings
Avoid zero→non-zero SSTORE22 100 gas; batch into one write
Use events for logs, not storage~10× cheaper than SSTORE
++i not i++3-5 gas per iteration
uint256 for arithmetic, smaller for packingsmaller types have masking overhead