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
| Operation | Gas (approximate) |
|---|---|
| Zero → non-zero SSTORE (cold) | 22 100 |
| Non-zero → non-zero SSTORE (cold) | 5 000 |
| Non-zero → zero SSTORE | 5 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 |
| KECCAK256 | 30 + 6/word |
| ADD / MUL | 3 / 5 |
| EXP | 10 + 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
| Tip | Saving |
|---|---|
| Pack storage structs | 1 SSTORE/SLOAD per eliminated slot |
| Cache storage reads in memory | SLOAD: 2100 → 100 (warm) |
Use calldata not memory for external arrays | avoids copy (~3 gas/byte) |
Use constant / immutable | eliminates 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 SSTORE | 22 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 packing | smaller types have masking overhead |