Skip to content

Solidity チートシート

Object-oriented language for writing smart contracts on Ethereum.

01

Getting Started

Contract Basics

Solidity is the primary language for Ethereum smart contracts. pragma sets the compiler version. A contract is like a class. Functions can be public, private, view (read-only), or payable.

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleStorage {
    uint256 storedData;

    function set(uint256 x) public {
        storedData = x;
    }

    function get() public view returns (uint256) {
        return storedData;
    }
}

Pragma & Compiler Version

pragma specifies the compiler version. ^0.8.0 allows any 0.8.x patch. For production, lock the version exactly (0.8.19) to avoid surprises. Always include the pragma at the top of every .sol file.

solidity
// Lock to a single version
pragma solidity 0.8.19;

// Allow patches (^0.8.0 means >=0.8.0 <0.9.0)
pragma solidity ^0.8.0;

// Range of versions
pragma solidity >=0.8.0 <0.9.0;

// Experimental features (ABIEncoderV2 in older versions)
pragma experimental ABIEncoderV2;

Comments & NatSpec

NatSpec comments (/// or /** */) generate documentation. @notice is for end users, @dev for developers, @param describes parameters, @return describes return values. They are picked up by tools like Etherscan and Remix.

solidity
// Single-line comment

/* Multi-line
   comment */

/// @title A Simple Contract
/// @author Alice
/// @notice This is visible to end users
/// @dev This is for developers
contract Commented {
    /// @notice Sets the value
    /// @param _x The new value
    /// @dev Internal details here
    function set(uint256 _x) public {}
}

SPDX License

Since Solidity 0.6.8, the SPDX license identifier is mandatory at the top of the file. It helps tools and users understand how the code can be reused. Use MIT for permissive open-source, GPL-3.0 for copyleft, or UNLICENSED for private code.

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Common licenses:
// MIT - permissive
// GPL-3.0 - copyleft
// Apache-2.0 - permissive with patent grant
// UNLICENSED - private/no license
// BSD-2-Clause, BSD-3-Clause

contract Licensed {}

File Structure & Imports

Imports bring in contracts, libraries, or interfaces. Use {Name} to import only specific items. Relative paths work as expected. The 'is' keyword establishes inheritance. Files typically have one main contract but may contain several.

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./OtherContract.sol";
import {SpecificContract, Helper} from "./Library.sol";
import * as Lib from "./Library.sol";  // namespace
import "openzeppelin/contracts/token/ERC20.sol";

contract MyContract is OtherContract {
    // ...
}
02

Value Types

Integers (uint / int)

uint = unsigned int (no negatives), int = signed. Sizes range from 8 to 256 in steps of 8. uint defaults to uint256. Since 0.8.0, arithmetic auto-checks for overflow/underflow and reverts. Use type(T).max / type(T).min to get bounds.

solidity
uint256 public bigNumber = 1 ether;       // 10^18
uint8 public small = 255;                  // max 255
int256 public signed = -100;
uint public defaultSize = 42;              // uint256 by default

// Arithmetic
uint256 a = 10;
uint256 b = 3;
uint256 sum = a + b;        // 13
uint256 diff = a - b;       // 7  (0.8+ reverts on underflow)
uint256 product = a * b;    // 30
uint256 quotient = a / b;   // 3
uint256 remainder = a % b;  // 1
uint256 power = a ** 2;     // 100

// Type min/max (0.8+)
uint256 max = type(uint256).max;
int256 min = type(int256).min;

Boolean

bool can only be true or false. && and || short-circuit, meaning the right side is only evaluated if needed. Default value is false. Booleans cost more gas than uint256 in some contexts (storage packing).

solidity
bool public flag = true;
bool public negated = !flag;       // false
bool public and = flag && false;   // false
bool public or = flag || false;    // true

// Short-circuit evaluation
bool result = (flag && expensiveCall());  // skips call if flag false
bool result2 = (!flag || expensiveCall());

// Equality
bool equal = (1 == 1);   // true
bool notEq = (1 != 2);   // true

// Boolean defaults to false
bool public defaultValue; // false

Address

address holds a 20-byte Ethereum address. address payable can receive Ether (has transfer/send methods). Use payable(x) to convert. Addresses have .balance property (in wei). Always verify address literals against EIP-55 checksum to catch typos.

solidity
address public owner = msg.sender;
address payable public treasury = payable(owner);

// Balance (read-only)
uint256 bal = owner.balance;        // in wei

// Send Ether (payable address only)
treasury.transfer(1 ether);
bool sent = treasury.send(1 ether);

// Convert from address to payable
address addr = 0x123...;
address payable payableAddr = payable(addr);

// Address literals (checksummed)
address public constant BURN = 0x000000000000000000000000000000000000dEaD;

// Compare addresses
bool same = (addr1 == addr2);

Bytes & Byte Arrays

Fixed bytes (bytes1..bytes32) are cheap and used for hashes/selectors. Dynamic bytes is for raw binary data; string is for UTF-8 text. Use bytes32 for hashes (keccak256 returns bytes32). bytes.concat() (0.8.4+) is cheaper than abi.encodePacked for byte arrays.

solidity
// Fixed-size byte arrays (bytes1 to bytes32)
bytes32 public hash = keccak256(abi.encodePacked("data"));
bytes1 public b1 = 0x41;       // single byte
bytes4 public selector = bytes4(keccak256("transfer(address,uint256)"));

// Dynamic byte arrays
bytes public dynamic = "hello";
bytes public empty;

// String vs bytes
string public text = "Hello";  // UTF-8 string
bytes public raw = bytes(text);

// Operations on fixed bytes
bytes32 data = keccak256(abi.encode("x"));
bytes1 first = data[0];        // access by index
uint256 length = data.length;  // 32

// Concatenation
bytes memory combined = bytes.concat(bytes("a"), bytes("b"));

Enums

Enums define a finite set of named values. They are stored as uint8 (max 256 members). Default value is the first member (index 0). Convert between enum and uint using casts. type(EnumName).max gives the highest member.

solidity
contract EnumExample {
    enum Status { Pending, Active, Paused, Closed }

    Status public status = Status.Pending;

    function activate() public {
        require(status == Status.Pending, "Not pending");
        status = Status.Active;
    }

    function next() public {
        // Cast to uint to do math
        uint current = uint(status);
        if (current < uint(type(Status).max)) {
            status = Status(current + 1);
        }
    }

    // Default value is the first member (Pending)
    Status public defaultValue; // Pending
}

Type Conversion & Casting

Implicit conversions only happen when no data loss is possible (uint8 -> uint256). For narrowing conversions (uint256 -> uint8) or signed/unsigned, use explicit casts. Converting a negative int to uint produces a huge number due to two's complement.

solidity
// Implicit conversion (safe, widening)
uint8 small = 10;
uint256 big = small;        // OK: uint8 fits in uint256

// Explicit conversion (narrowing, may lose data)
uint256 large = 300;
uint8 narrow = uint8(large);  // truncates to 44 (300 % 256)

// Address conversion
address addr = 0xAb8483F64d9C6d1Ec37F8D2C4fA6bA5d5C5e5C5e;
address payable payableAddr = payable(addr);

// Bytes conversion
bytes32 b32 = bytes32(uint256(42));
uint256 num = uint256(b32);
bytes4 selector = bytes4(keccak256("foo()"));

// String <-> bytes
string memory s = "hi";
bytes memory bs = bytes(s);
string memory s2 = string(bs);

// int <-> uint (explicit, watch for negatives)
int256 signed = -5;
uint256 unsigned = uint256(signed); // huge number!
03

Reference Types

String

Strings in Solidity are UTF-8 byte arrays — length() returns byte count, not character count (emoji/multibyte chars differ). Equality must be checked via keccak256 hash. string.concat (0.8.12+) is cheaper than abi.encodePacked. Strings are expensive in gas.

solidity
contract Strings {
    string public greeting = "Hello, World!";

    function concat(string memory a, string memory b)
        public pure returns (string memory)
    {
        return string.concat(a, " ", b);  // 0.8.12+
    }

    function length(string memory s) public pure returns (uint256) {
        return bytes(s).length;  // byte length, not char count
    }

    function equals(string memory a, string memory b)
        public pure returns (bool)
    {
        return keccak256(bytes(a)) == keccak256(bytes(b));
    }

    // Storage string (state variable)
    string public stored;
    function set(string memory s) public { stored = s; }
}

Arrays (Fixed & Dynamic)

Fixed arrays [N] have a compile-time size; dynamic arrays [] grow with push/pop. Memory arrays must be created with 'new Type[](size)'. Reading/writing storage arrays costs gas; prefer memory for temporary data. push() returns a reference in 0.6+.

solidity
contract Arrays {
    // Fixed-size array
    uint256[5] public fixedArr = [1, 2, 3, 4, 5];

    // Dynamic array
    uint256[] public dynamicArr;

    function pushPop() public {
        dynamicArr.push(10);       // append: [10]
        dynamicArr.push(20);       // [10, 20]
        dynamicArr.pop();          // remove last: [10]
    }

    function getLength() public view returns (uint256) {
        return dynamicArr.length;
    }

    function iterate() public view returns (uint256 sum) {
        for (uint256 i = 0; i < dynamicArr.length; i++) {
            sum += dynamicArr[i];
        }
    }

    // Array of arrays (2D)
    uint256[][] public matrix;

    // Memory array (must have fixed length when created)
    function memArray() public pure returns (uint256[] memory) {
        uint256[] memory arr = new uint256[](3);
        arr[0] = 1; arr[1] = 2; arr[2] = 3;
        return arr;
    }
}

Structs

Structs group related fields. They can be stored in storage, memory, or passed as function arguments. Define them at file or contract level. Initializing with named fields {field: value} is clearer; positional is shorter. Mapping values cannot be struct directly accessed as a whole in older versions.

solidity
contract Structs {
    struct User {
        address wallet;
        uint256 balance;
        bool active;
        string name;
    }

    // Single struct (storage)
    User public owner;
    User[] public users;            // array of structs
    mapping(address => User) public userByAddr;

    function createUser(string memory name) public {
        User memory u = User({
            wallet: msg.sender,
            balance: 100,
            active: true,
            name: name
        });
        users.push(u);
    }

    // Shorter syntax
    function createShort() public {
        users.push(User(msg.sender, 50, true, "anon"));
    }

    // Update a field
    function deactivate(uint256 index) public {
        users[index].active = false;  // direct storage write
    }
}

Mappings

Mappings are hash tables: keccak256(key) => value. They have no length and cannot be iterated. Keys can be value types (uint, address, bytes, enum); values can be anything. Nested mappings allow multi-key lookups. To iterate, maintain a separate array of keys. delete resets a key to its zero value.

solidity
contract Mappings {
    // keyType => valueType
    mapping(address => uint256) public balances;
    mapping(address => mapping(address => uint256)) public allowance;
    mapping(address => bool) public isRegistered;

    function setBalance(uint256 amount) public {
        balances[msg.sender] = amount;
    }

    function approve(address spender, uint256 amount) public {
        allowance[msg.sender][spender] = amount;
    }

    function register() public {
        isRegistered[msg.sender] = true;
    }

    // Iterable mapping pattern
    address[] public userList;
    mapping(address => bool) public exists;
    function addUser(address u) public {
        if (!exists[u]) {
            userList.push(u);
            exists[u] = true;
        }
    }

    // CANNOT iterate or get length of a mapping directly
    // CANNOT delete a mapping, only individual keys
    function remove() public {
        delete balances[msg.sender];  // sets to 0
    }
}

Data Locations (storage / memory / calldata)

storage = persistent on-chain state (expensive). memory = temporary, lives during function call (cheap). calldata = read-only, only for external function inputs (cheapest). Always use calldata for external function args you only read. Default location: state vars are storage; function local complex types default to memory.

solidity
contract DataLocations {
    string public stateVar;          // storage (state)

    function f(string calldata input) external {
        // calldata: read-only, only for external function args

        string memory local = input; // memory: temporary copy
        stateVar = local;            // writes to storage

        // storage reference (only inside a function)
        string storage ref = stateVar;
        ref = "new value";           // modifies stateVar
    }

    // External params can be calldata (cheapest)
    function g(string calldata s) external pure returns (bytes32) {
        return keccak256(bytes(s));
    }

    // Public/internal params default to memory
    function h(string memory s) public pure returns (uint256) {
        return bytes(s).length;
    }

    // Assignment rules:
    // storage -> storage: reference (both point to same slot)
    // memory -> memory: reference (both point to same memory)
    // storage <-> memory: copy
}

Nested Structures

Structs can contain arrays, mappings (only in storage), and other structs. Accessing nested struct fields via a storage reference is gas-efficient. Be careful with deep nesting — it increases storage slot usage and deployment cost. Mapping-of-struct is the most common on-chain database pattern.

solidity
contract Nested {
    struct Order {
        uint256 id;
        uint256 amount;
        Item[] items;
    }

    struct Item {
        string sku;
        uint256 qty;
    }

    mapping(uint256 => Order) public orders;
    Order[] public allOrders;

    function addOrder(uint256 id, string memory sku, uint256 qty) public {
        Order storage o = orders[id];
        o.id = id;
        o.amount += qty * 1 ether;
        o.items.push(Item(sku, qty));
    }

    // Mapping of struct arrays
    mapping(address => Item[]) public userItems;

    // Struct in a struct
    struct Profile {
        User user;
        uint256 score;
    }
    struct User {
        address addr;
        string name;
    }
    Profile public profile;
}
04

State Variables

Public & Private State Variables

public state variables auto-generate a getter function. private restricts access to within the contract; internal allows subclasses too. Default is internal. CRITICAL: 'private' does NOT mean secret — all blockchain data is publicly readable. Use encryption/hashing if you need confidentiality.

solidity
contract Visibility {
    uint256 public publicVar = 1;     // auto getter created
    uint256 private privateVar = 2;   // only this contract
    uint256 internal internalVar = 3; // this + subclasses
    // No 'external' for state variables

    // internal is the default
    uint256 defaultVar = 4;           // internal

    function readPrivate() public view returns (uint256) {
        return privateVar;  // readable from inside
    }

    // 'private' only means 'not via other contracts'.
    // The data is still visible on-chain!
}

// public creates an automatic getter:
// function publicVar() external view returns (uint256) { return publicVar; }

Constant Variables

constant variables are evaluated at compile time and inlined into bytecode — they cost zero gas at runtime (no SLOAD). Only value types (uint, address, bytes, etc.) can be constant. The value must be a compile-time expression. Use constant for fixed values like MAX_SUPPLY, DECIMALS, addresses.

solidity
contract Constants {
    // constant: evaluated at compile time, inlined into code
    uint256 public constant MAX_SUPPLY = 1_000_000 * 10**18;
    address public constant DEAD = 0x000000000000000000000000000000000000dEaD;
    uint8 public constant DECIMALS = 18;

    // Must be value types, assigned a literal/expression
    // No assignment in constructor
    // Numeric separators (1_000_000) work since 0.8.0

    function compute() public pure returns (uint256) {
        return MAX_SUPPLY / 2;   // inlined, no SLOAD
    }
}

Immutable Variables

immutable variables are set once in the constructor and then read-only. They are stored in bytecode (not storage), so reads cost less than regular state vars. Unlike constant, immutable can use constructor arguments and runtime values. Use immutable for things set at deployment like owner, token name, or chain-specific config.

solidity
contract Immutable {
    // immutable: set once in constructor, then read-only
    address public immutable owner;
    uint256 public immutable creationBlock;

    constructor(address _owner) {
        owner = _owner;
        creationBlock = block.number;
    }

    // Can be any value type
    // Cheaper than regular state vars (no SLOAD slot, uses code)
    // More flexible than constant (can use constructor args)

    function check() public view returns (bool) {
        return msg.sender == owner;  // cheap read
    }
}

// Use case: factory-deployed contracts with constructor args
contract Factory {
    Child[] public children;
    function create(address owner) public {
        children.push(new Child(owner));
    }
}
contract Child {
    address public immutable owner;
    constructor(address _owner) { owner = _owner; }
}

Global Variables (msg, tx, block)

msg provides call context: msg.sender (immediate caller), msg.value (wei sent), msg.data (raw calldata), msg.sig (function selector). tx gives transaction info: tx.origin (the EOA, NOT recommended for auth). block gives current block: block.timestamp, block.number, block.chainid, block.coinbase. Never use tx.origin for authorization — use msg.sender.

solidity
contract Globals {
    function msgVars() public view returns (
        address sender,
        bytes4 selector,
        uint256 value,
        bytes memory data
    ) {
        sender = msg.sender;          // caller address
        selector = msg.sig;           // function selector
        value = msg.value;            // wei sent with tx
        data = msg.data;              // full calldata
    }

    function txVars() public view returns (address origin, uint256 gasprice) {
        origin = tx.origin;           // EOA that started the tx
        gasprice = tx.gasprice;       // gas price in wei
    }

    function blockVars() public view returns (
        uint256 number,
        uint256 timestamp,
        address miner,
        uint256 chainId
    ) {
        number = block.number;        // current block height
        timestamp = block.timestamp;  // current block time (uint256 in 0.8+)
        miner = block.coinbase;       // block miner/validator
        chainId = block.chainid;      // chain ID
    }
}

Block & Tx Properties

blockhash() only returns hashes for the last 256 blocks; older returns 0. block.timestamp is set by the miner and can be off by ~15 seconds — never use it for precise timing or randomness. block.basefee is from EIP-1559. gasleft() replaces the deprecated msg.gas. chainid is critical for replay protection.

solidity
contract BlockProps {
    function getBlockInfo() public view returns (
        uint256 number,
        uint256 timestamp,
        uint256 gasLimit,
        uint256 gasPrice,
        uint256 baseFee
    ) {
        number = block.number;
        timestamp = block.timestamp;
        gasLimit = block.gaslimit;
        gasPrice = tx.gasprice;
        baseFee = block.basefee;  // EIP-1559 (London)
    }

    function getChainId() public view returns (uint256) {
        return block.chainid;
    }

    function getPrevHash(uint256 n) public view returns (bytes32) {
        require(n < 256, "out of range");
        return blockhash(block.number - n);  // last 256 blocks
    }

    function gasLeft() public view returns (uint256) {
        return gasleft();  // remaining gas
    }
}
05

Functions

Function Syntax

Function syntax: function name(params) visibility mutability returns(...). Visibility (public/external/internal/private) is required. Mutability (pure/view/payable) is optional but recommended. Multiple return values are supported. Named returns let you assign to them directly and skip the return keyword.

solidity
contract Func {
    // function name(params) <visibility> <state-mutability> returns(...)
    function add(uint256 a, uint256 b)
        public
        pure
        returns (uint256)
    {
        return a + b;
    }

    // Multiple returns
    function split(uint256 x)
        public pure returns (uint256, uint256)
    {
        return (x / 2, x % 2);
    }

    // Named returns (no explicit return needed)
    function named(uint256 x)
        public pure returns (uint256 half, uint256 rest)
    {
        half = x / 2;
        rest = x % 2;
        // return statement optional
    }

    // Destructure returns
    function caller() public pure returns (uint256) {
        (uint256 h, uint256 r) = named(10);
        return h + r;
    }
}

View Functions

view functions can read state but not modify it. When called externally via eth_call, they are free (no gas fee). When called internally from a modifying function, they consume gas. Use view for any function that only reads state — this signals intent and allows off-chain free reads.

solidity
contract ViewFn {
    uint256 public counter = 0;

    // view: can READ state but not modify
    function getCount() public view returns (uint256) {
        return counter;          // reading state
    }

    function doubleCount() public view returns (uint256) {
        return counter * 2;      // reading state
    }

    // Can call other view/pure functions
    function combined() public view returns (uint256) {
        return getCount() + 1;
    }

    // CANNOT:
    // - write to state variables
    // - emit events
    // - call non-view functions
    // - use selfdestruct
    // - send Ether

    // view functions can be called off-chain for free (eth_call)
}

Pure Functions

pure functions cannot read or write state — they only compute on their inputs. They are the most restricted and cheapest to call off-chain. Use pure for utility functions (math, hashing of inputs). If you find yourself needing msg.sender or block data, use view instead. pure can call other pure functions only.

solidity
contract PureFn {
    // pure: cannot read OR write state
    function add(uint256 a, uint256 b) public pure returns (uint256) {
        return a + b;
    }

    function square(uint256 x) public pure returns (uint256) {
        return x * x;
    }

    // pure can only use:
    // - function parameters
    // - local memory variables
    // - other pure functions

    function combined(uint256 x) public pure returns (uint256) {
        uint256 y = square(x);   // calling another pure fn
        return add(y, 1);
    }

    // CANNOT read: state variables, msg.sender, block.timestamp, etc.
    // CANNOT call view functions
}

Payable Functions

payable functions can receive Ether via msg.value. The Ether is automatically added to the contract's balance. Only payable functions and the receive/fallback can accept Ether. Always validate msg.value if you expect a specific amount. Non-payable functions revert if Ether is sent.

solidity
contract Payable {
    address payable public owner;

    constructor() {
        owner = payable(msg.sender);
    }

    // payable: function can receive Ether
    function deposit() public payable {
        // msg.value is the amount of wei sent
        // Ether is automatically added to contract balance
    }

    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }

    // Withdraw to owner
    function withdraw() public {
        require(msg.sender == owner, "not owner");
        owner.transfer(address(this).balance);
    }

    // payable address (the contract itself can be paid)
    receive() external payable {
        // called when calldata is empty + value sent
    }
}

Return Values & Named Returns

Functions can return multiple values via tuples. Named returns improve readability and let you assign directly without a return statement. Explicit return statements override named returns. Use destructuring (a, b) = f() to capture multiple returns; use (, b) or (a,) to skip values you don't need.

solidity
contract Returns {
    // Single return
    function one() public pure returns (uint256) {
        return 42;
    }

    // Multiple returns
    function two() public pure returns (uint256, bool) {
        return (1, true);
    }

    // Named returns — assign directly
    function named() public pure returns (uint256 a, uint256 b) {
        a = 1;
        b = 2;
        // no return keyword needed
    }

    // Mixed: explicit return overrides
    function mixed(bool x) public pure returns (uint256 a, uint256 b) {
        a = 1;
        if (x) return (10, 20);  // explicit return
        b = 2;
    }

    // Destructuring assignment
    function caller() public pure returns (uint256) {
        (uint256 x, uint256 y) = two();
        return x + y;
    }

    // Skip values with commas
    function skip() public pure returns (bool) {
        (, bool y) = two();
        return y;
    }
}

Function Overloading

Solidity supports function overloading: multiple functions with the same name but different parameter types or count. The compiler resolves which to call based on argument types. Return type alone cannot distinguish overloads. Overloads are useful for accepting both address and address payable, or different number ranges.

solidity
contract Overload {
    // Same name, different parameter types/counts
    function add(uint256 a, uint256 b) public pure returns (uint256) {
        return a + b;
    }

    function add(uint256 a, uint256 b, uint256 c) public pure returns (uint256) {
        return a + b + c;
    }

    function add(string memory a, string memory b)
        public pure returns (string memory)
    {
        return string.concat(a, b);
    }

    // Caller selects which via argument types
    function test() public pure returns (uint256, uint256, string memory) {
        return (add(1, 2), add(1, 2, 3), add("a", "b"));
    }

    // NOTE: return type alone does NOT distinguish overloads
    // function f() returns (uint256) and f() returns (bool) — INVALID
}
06

Constructor & Special Functions

Constructor

constructor runs once when the contract is deployed, used to initialize state. It cannot be called again. Constructor arguments are appended to deployment bytecode or passed via the ABI depending on the framework. For inheritance, pass parent constructor args in the inheritance list (Owned(_owner)) or modifier-style (A() B()).

solidity
contract Owned {
    address public owner;

    // constructor runs once at deployment
    constructor(address _owner) {
        owner = _owner;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    function transferOwnership(address newOwner) public onlyOwner {
        owner = newOwner;
    }
}

// Inheritance: constructor args
contract Child is Owned {
    constructor(address _owner) Owned(_owner) {}
}

// No constructor = default constructor (no args)
contract NoCtor {}

// Multiple inheritance: order matters
contract A { constructor() {} }
contract B { constructor() {} }
contract C is A, B {
    constructor() A() B() {}
}

Receive Function

receive() is invoked when a call with no calldata sends Ether (e.g., send/transfer to the contract). It must be 'external payable' with no arguments or returns. Only one receive function is allowed. If absent and no fallback, plain Ether transfers revert (except via selfdestruct which can force Ether).

solidity
contract Receiver {
    event Received(address from, uint256 amount);

    // receive: called on plain Ether transfers (no calldata)
    // Must be 'external payable', no args, no returns
    // Only ONE receive per contract
    receive() external payable {
        emit Received(msg.sender, msg.value);
    }

    // Triggered by: transfer(), send(), or empty calldata tx
    // Use this to react to incoming Ether

    function balance() public view returns (uint256) {
        return address(this).balance;
    }
}

// If no receive/fallback exists, the contract rejects plain Ether.
// Exception: contracts can be forced via selfdestruct (always).

Fallback Function

fallback() is called when no function matches the selector, or when receive is absent and a plain Ether transfer is attempted. It can be payable. Keep fallback logic minimal — when invoked via send()/transfer() it only has 2300 gas (the stipend), which won't cover storage writes or emit events.

solidity
contract Fallback {
    event Log(string message, address from, uint256 value, bytes data);

    // fallback: called when no function matches the calldata
    // OR when receive is absent and calldata is empty
    fallback() external payable {
        emit Log("fallback", msg.sender, msg.value, msg.data);
    }

    // With calldata (function not found)
    // Without calldata + no receive => fallback runs

    // Modern: separate receive and fallback
    // receive() external payable { ... }  // empty calldata + value
    // fallback() external payable { ... } // unknown function

    // Make fallback cheap — it can run out of gas on send()/transfer() (2300 stipend)
    function safeFallback() external {
        // logic here must be < 2300 gas if called via transfer/send
    }
}

Selfdestruct

selfdestruct sends all remaining Ether to a specified address and (pre-Dencun) removes the contract code. Post-EIP-6780 (March 2024), it only removes code if called within the same deployment transaction — otherwise it just transfers Ether. Never rely on selfdestruct for security: forced Ether can still arrive, and code may persist.

solidity
contract SelfDestruct {
    address payable public owner;

    constructor() { owner = payable(msg.sender); }

    function destroy() public {
        require(msg.sender == owner, "not owner");
        // Sends all Ether to recipient, then deletes the contract
        selfdestruct(payable(msg.sender));
    }
}

// IMPORTANT WARNINGS:
// - After EIP-6780 (Dencun), selfdestruct no longer deletes code
//   unless called in the same transaction that deployed the contract
// - It still sends all Ether to the recipient
// - The recipient could be a contract with no receive/fallback and
//   it still gets the Ether (forced transfer)
// - Do NOT rely on selfdestruct for security; prefer a 'paused' flag

Function Selector

Every function has a 4-byte selector = first 4 bytes of keccak256(signature). The signature is the function name and parameter types without spaces, e.g., 'transfer(address,uint256)'. The EVM uses the selector to route calls. msg.sig gives the current function's selector. abi.encodeWithSelector/Signature build calldata for low-level calls.

solidity
contract Selector {
    // First 4 bytes of keccak256 of the function signature
    // signature = "functionName(paramType1,paramType2,...)"

    bytes4 public constant SELECTOR =
        bytes4(keccak256("transfer(address,uint256)"));

    function getSelector() public pure returns (bytes4) {
        return bytes4(keccak256("transfer(address,uint256)")));
        // = 0xa9059cbb
    }

    function thisSelector() public pure returns (bytes4) {
        return msg.sig;  // selector of the currently running function
    }

    // Used for low-level calls
    function callTransfer(address token, address to, uint256 amount)
        public returns (bool, bytes memory)
    {
        (bool ok, bytes memory data) = token.call(
            abi.encodeWithSelector(0xa9059cbb, to, amount)
        );
        return (ok, data);
    }

    // abi.encodeWithSignature uses the string form
    function altCall(address token, address to, uint256 amount)
        public returns (bool, bytes memory)
    {
        return token.call(
            abi.encodeWithSignature("transfer(address,uint256)", to, amount)
        );
    }
}
07

Modifiers

Modifier Basics

Modifiers wrap functions with reusable pre/post conditions. The _; placeholder is where the function body executes. Modifiers are great for access control (onlyOwner), validation (validInput), and reentrancy guards. They are applied before the function body by default — use _; to control where the body runs.

solidity
contract ModBasics {
    address public owner;
    uint256 public count;

    constructor() { owner = msg.sender; }

    // Define a modifier
    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;  // placeholder for the function body
    }

    // Apply it
    function increment() public onlyOwner {
        count += 1;
    }

    function reset() public onlyOwner {
        count = 0;
    }

    // Anyone can read
    function getCount() public view returns (uint256) {
        return count;
    }
}

Modifier with Arguments

Modifiers can take arguments, making them flexible. The arguments are evaluated at the call site and bound when the modifier runs. Common patterns: costs(price) for pay-per-call, onlyRole(role) for RBAC, rateLimit(window) for throttling. Logic before _; is pre-condition, after _; is post-condition.

solidity
contract ModArgs {
    modifier costs(uint256 price) {
        require(msg.value >= price, "insufficient payment");
        _;
        // optional: refund excess after function runs
        if (msg.value > price) {
            payable(msg.sender).transfer(msg.value - price);
        }
    }

    modifier onlyAddress(address allowed) {
        require(msg.sender == allowed, "not allowed");
        _;
    }

    function buyItem() public payable costs(1 ether) {
        // msg.value must be >= 1 ether to enter
        // excess refunded after body runs
    }

    function adminOnly() public onlyAddress(0xAbC...) {
        // restricted to a hardcoded address
    }
}

Multiple Modifiers

A function can have multiple modifiers. They execute in order, wrapping the function body like a stack: the first modifier's pre-condition runs first, then the second, ..., then the body, then they unwind in reverse. Order matters — put the cheapest/most-likely-to-fail checks first to save gas.

solidity
contract MultiMod {
    address public owner;
    bool public paused;
    uint256 public value;

    constructor() { owner = msg.sender; }

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    modifier whenNotPaused() {
        require(!paused, "paused");
        _;
    }

    modifier validValue(uint256 v) {
        require(v > 0, "invalid value");
        _;
    }

    // Multiple modifiers — executed in order
    // Stack-like: A B body B' A'
    function setValue(uint256 v)
        public
        onlyOwner        // 1. check owner
        whenNotPaused    // 2. check not paused
        validValue(v)    // 3. validate input
    {
        value = v;
    }
}

Modifier Order & Gas

Modifier order affects gas and clarity. Put cheap checks first (booleans, address comparisons) and expensive checks last (storage writes, external calls). Also put checks most likely to fail first — this short-circuits and refunds unused gas. Document the intended order with comments.

solidity
contract ModOrder {
    // Order modifiers from cheapest to most expensive
    // OR most likely to fail first (to short-circuit)

    modifier onlyOwner() { require(msg.sender == owner, "1"); _; }
    modifier nonZero(uint256 x) { require(x != 0, "2"); _; }
    modifier withinRange(uint256 x) {
        require(x >= 1 && x <= 100, "3");
        _;
    }
    modifier notPaused() { require(!paused, "4"); _; }

    // Good order: cheap + likely-to-fail first
    function good(uint256 x)
        public
        notPaused           // 1. cheap bool check
        onlyOwner           // 2. address compare
        nonZero(x)          // 3. cheap input check
        withinRange(x)      // 4. more complex range check
    {}

    address public owner;
    bool public paused;
    constructor() { owner = msg.sender; }
}

Common Modifier Patterns

Common modifier patterns: nonReentrant (set a lock before body, unset after), whenNotPaused/whenPaused (circuit breaker), onlyRole (RBAC with bytes32 roles), rateLimit (per-user throttle using timestamps). Reentrancy guard is the most important — apply it to any function that makes an external call after a state change.

solidity
contract Patterns {
    // 1. Reentrancy guard
    uint256 private _status = 1;
    modifier nonReentrant() {
        require(_status == 1, "reentrant");
        _status = 2;
        _;
        _status = 1;
    }

    // 2. Pausable
    bool public paused;
    modifier whenNotPaused() {
        require(!paused, "paused");
        _;
    }
    modifier whenPaused() {
        require(paused, "not paused");
        _;
    }

    // 3. Role-based
    mapping(bytes32 => bool) public hasRole;
    modifier onlyRole(bytes32 role) {
        require(hasRole[role], "missing role");
        _;
    }

    // 4. Rate limit (basic)
    mapping(address => uint256) public lastCall;
    modifier rateLimit() {
        require(block.timestamp >= lastCall[msg.sender] + 1 minutes, "rate");
        lastCall[msg.sender] = block.timestamp;
        _;
    }
}
08

Events

Event Declaration & Emit

Events are logged to the blockchain and readable by off-chain clients (via eth_getLogs). They are NOT readable by other contracts. Use indexed on up to 3 parameters to make them filterable. Events cost less gas than storage writes. Always emit events for important state changes so dApps and indexers can react.

solidity
contract Events {
    // Declare event (outside functions, can be at file level)
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) public returns (bool) {
        require(balances[msg.sender] >= amount, "insufficient");

        balances[msg.sender] -= amount;
        balances[to] += amount;

        // Emit the event
        emit Transfer(msg.sender, to, amount);
        return true;
    }

    function approve(address spender, uint256 amount) public returns (bool) {
        emit Approval(msg.sender, spender, amount);
        return true;
    }
}

Indexed Parameters

indexed parameters become topics that off-chain clients (dApps, The Graph, indexers) can filter on. Maximum 3 indexed params per event (4th topic is reserved for the event signature hash). Indexed value types (uint, address) are stored directly; indexed reference types (string, bytes, arrays) are keccak256-hashed and unrecoverable.

solidity
contract Indexed {
    // Up to 3 indexed params (in non-anonymous events)
    // Indexed params become 'topics' that clients can filter on
    event OrderPlaced(
        address indexed buyer,        // topic 1
        address indexed token,        // topic 2
        uint256 indexed orderId,      // topic 3
        uint256 amount,               // data (not filterable)
        uint256 price                 // data
    );

    function placeOrder(address token, uint256 amount, uint256 price)
        public returns (uint256 id)
    {
        id = uint256(keccak256(abi.encodePacked(msg.sender, block.number)));
        emit OrderPlaced(msg.sender, token, id, amount, price);
    }

    // Clients can filter:
    //   eth_getLogs({topics: [sig, buyerAddr, tokenAddr, orderId]})
    //
    // Indexed arrays/strings/bytes are hashed (keccak256) —
    // you can't recover the original value, only compare hashes.
}

Anonymous Events

anonymous events skip the event signature hash in topic[0], freeing a slot — so you get 4 indexed params instead of 3. The trade-off: clients cannot filter by event name (signature), so it's harder to distinguish multiple anonymous events in a contract. Rarely useful; the gas savings are tiny.

solidity
contract Anonymous {
    // anonymous: skips storing the event signature as topic[0]
    // So you can have up to 4 indexed params instead of 3
    event Log(
        address indexed from,
        address indexed to,
        uint256 indexed amount,
        uint256 indexed timestamp,  // 4th indexed (only with anonymous)
        bytes data
    ) anonymous;

    function log() public {
        emit Log(msg.sender, address(0), 100, block.timestamp, "");
    }

    // Trade-offs:
    // + 1 more indexed param allowed
    // - Clients cannot filter by event signature
    // - Harder to distinguish multiple anonymous events
    // - Slightly cheaper gas
    // Rarely used; prefer named events for clarity.
}

Event Best Practices

Best practices: emit after state changes (so the event reflects final state), include fields that help indexers avoid extra RPC calls (like newBalance), avoid emitting in tight loops (batch instead), and index the fields most often filtered on (addresses, IDs, status codes). Events are the primary contract-to-off-chain communication channel.

solidity
contract EventBest {
    // 1. Name events with past tense or noun: Transfer, Deposit, OrderPlaced
    event Transfer(address indexed from, address indexed to, uint256 value);

    // 2. Emit AFTER state changes succeed
    mapping(address => uint256) public balances;
    function transfer(address to, uint256 amt) public {
        balances[msg.sender] -= amt;  // state first
        balances[to] += amt;
        emit Transfer(msg.sender, to, amt);  // event after
    }

    // 3. Include enough data to reconstruct state
    event Deposit(
        address indexed account,
        uint256 amount,
        uint256 newBalance,    // helps indexers avoid extra reads
        uint256 timestamp
    );

    // 4. Don't emit in a loop — batch instead
    event BatchTransfer(address[] to, uint256[] amounts);
    function batch(address[] memory to, uint256[] memory amounts) public {
        emit BatchTransfer(to, amounts);  // one event, not N
    }

    // 5. Index the fields users filter on (addresses, IDs)
}

Events vs Logs vs Storage

Storage is in the state trie — readable by other contracts and view functions, but expensive (~20k gas per SSTORE). Events/logs are in the receipt trie — NOT readable by contracts, but cheap (~1-2k gas) and accessible off-chain via eth_getLogs. Use storage for on-chain decisions, events for communicating state changes to dApps and indexers.

solidity
contract Compare {
    // Storage: persistent, readable by contracts, expensive
    uint256 public storedValue;  // SSTORE costs ~20k gas

    // Events (logs): persistent-ish, NOT readable by contracts, cheap
    event ValueChanged(uint256 oldValue, uint256 newValue);

    function set(uint256 newValue) public {
        uint256 old = storedValue;
        storedValue = newValue;                 // SSTORE (~20k gas)
        emit ValueChanged(old, newValue);       // LOG (~1-2k gas)
    }

    // Logs live in the block's receipt trie, NOT the state trie.
    // They are NOT accessible from other contracts.
    // function readLog() public view returns (...) {
    //     CANNOT access emitted events
    // }

    // Off-chain tools (dApps, The Graph) read logs via eth_getLogs.
    // Use storage for on-chain logic; events for off-chain observers.
}
09

Error Handling

Require

require(cond, msg) reverts the transaction and refunds unused gas if cond is false. It's the most common validation primitive — use it for input checks, access control, and preconditions. The message string costs gas (storage in deployed bytecode). For gas savings, use custom errors (revert ErrorName()) instead of string messages.

solidity
contract Require {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        require(msg.value > 0, "must send ether");
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) public {
        // require(condition, message)
        // Reverts with the message if condition is false
        // Refunds all unused gas to the caller (post-0.8.0 with custom errors, also refunds)
        require(balances[msg.sender] >= amount, "insufficient balance");

        balances[msg.sender] -= amount;
        payable(msg.sender).transfer(amount);
    }

    // Pre-0.8.0: require with no message was cheaper
    // 0.8.4+: prefer custom errors (cheaper, see other item)
    function cheap(uint256 x) public pure {
        if (x == 0) revert ZeroValue();  // custom error
    }

    error ZeroValue();
}

Revert

revert(msg) explicitly reverts the transaction with a reason string. Use revert() inside if-statements when you need branching logic (require is just syntactic sugar). revert() with no message is cheaper than revert("msg"). Custom errors (revert ErrorName(args)) are cheapest and most descriptive — prefer them in new code.

solidity
contract Revert {
    function explicit(uint256 x) public pure {
        if (x == 0) {
            revert("zero not allowed");   // explicit revert with message
        }
        if (x > 100) {
            revert("too big");            // can place anywhere
        }
    }

    // revert() with no message (cheapest string revert)
    function silent(uint256 x) public pure {
        if (x == 0) revert();             // no reason
    }

    // Custom error (0.8.4+) — recommended
    error TooBig(uint256 given, uint256 max);
    function custom(uint256 x) public pure {
        if (x > 100) revert TooBig(x, 100);
    }

    // Inside an internal function, revert propagates up
    function caller(uint256 x) public pure {
        inner(x);  // reverts here if x is bad
    }
    function inner(uint256 x) internal pure {
        if (x == 0) revert("zero");
    }
}

Assert

assert(cond) is for invariants that should never be false if the code is correct — a failed assert indicates a bug. Pre-0.8.0, assert consumed all gas; since 0.8.0 it behaves like revert. Use require for input validation and expected error conditions; use assert only for impossible states that signal a bug. Modern auto-overflow checks use revert, not assert.

solidity
contract Assert {
    function check(uint256 x) public pure returns (uint256) {
        uint256 result = x + 1;
        // assert: for invariants that should NEVER be false
        // If it fails, something is critically broken.
        assert(result > x);  // overflow check (auto in 0.8+)

        return result;
    }

    // Pre-0.8.0: assert was used for overflow checks (used all gas)
    // Post-0.8.0: arithmetic auto-checks use revert, not assert
    //
    // Modern guidance:
    // - Use require/revert for input validation & expected failures
    // - Use assert ONLY for invariants that indicate a code bug
    //   (something that should be impossible if the code is correct)
    //
    // Example: a state machine that should never reach an invalid state

    enum State { Created, Paid, Shipped }
    State public state;
    function ship() public {
        require(state == State.Paid, "not paid");
        state = State.Shipped;
        assert(state == State.Shipped);  // invariant
    }
}

Custom Errors

Custom errors (0.8.4+) are the recommended way to revert. They encode as a 4-byte selector plus ABI-encoded arguments — much cheaper than string messages (which are stored as full UTF-8 in bytecode). Clients decode the selector to know which error occurred and read the structured arguments. Define errors at file or contract level, then 'revert ErrorName(args);'.

solidity
contract CustomError {
    // Define errors (outside functions, can be at file level)
    error InsufficientBalance(uint256 available, uint256 required);
    error Unauthorized(address caller);
    error TransferFailed(address from, address to);

    mapping(address => uint256) public balances;

    function withdraw(uint256 amount) public {
        if (balances[msg.sender] < amount) {
            // Cheaper than require(..., "insufficient balance")
            revert InsufficientBalance(balances[msg.sender], amount);
        }

        balances[msg.sender] -= amount;
        (bool ok, ) = msg.sender.call{value: amount}("");
        if (!ok) revert TransferFailed(address(this), msg.sender);
    }

    function adminOnly() public {
        if (msg.sender != address(0x123)) {
            revert Unauthorized(msg.sender);
        }
    }

    // Why cheaper? The error selector (4 bytes) replaces a long string,
    // saving bytecode size and gas per call.
    // ABI decodes arguments so clients get structured data.
}

Try / Catch

try/catch handles errors from external calls only — it cannot catch reverts from internal function calls. The 'returns' clause captures the success value. catch Error() catches revert/require with string messages; catch Panic() catches assert/overflow panics; catch (bytes) is a fallback for custom errors and unknown reverts. Use it to gracefully handle external contract failures.

solidity
contract TryCatch {
    interface IToken {
        function transfer(address to, uint256 amount) external returns (bool);
    }

    IToken public token;

    constructor(address _token) { token = IToken(_token); }

    function safeTransfer(address to, uint256 amount)
        public returns (bool success, bool tokenOk)
    {
        try token.transfer(to, amount) returns (bool ok) {
            // Called if no revert; 'ok' captures the return value
            return (ok, true);
        } catch Error(string memory reason) {
            // revert("reason") or require(_, "reason")
            // log reason
            return (false, false);
        } catch Panic(uint256 code) {
            // assert failures (code 0x01) or overflow (0x11)
            return (false, false);
        } catch (bytes memory lowLevelData) {
            // Any other revert (including custom errors)
            return (false, false);
        }
    }

    // try/catch ONLY works with EXTERNAL calls (or this.f()).
    // It does NOT catch errors in internal calls.
    // The called function MUST return something (not void).
}

Error Messages & Patterns

Modern error handling uses custom errors for structured, cheap reverts. Older string messages are still common in pre-0.8.4 code. Always include actionable context: which value was wrong, what was expected. Avoid putting dynamic addresses/numbers in string messages — they bloat bytecode and aren't parseable. Use prefixed messages ('INVALID:') if you must use strings, to help log indexing.

solidity
contract Errors {
    // 1. Custom errors (best for new code)
    error InvalidAmount(uint256 given);
    error NotOwner(address caller, address owner);

    // 2. String errors (older style)
    function old(uint256 x) public pure {
        require(x > 0, "INVALID: must be > 0");   // prefix helps indexing
        require(x < 100, "INVALID: must be < 100");
    }

    // 3. Custom error with structured data
    function newStyle(uint256 x) public pure {
        if (x == 0) revert InvalidAmount(x);
    }

    // 4. Error with multiple fields
    address public owner;
    constructor() { owner = msg.sender; }
    function guarded() public view {
        if (msg.sender != owner) {
            revert NotOwner(msg.sender, owner);
        }
    }

    // 5. Don't put dynamic data in string errors (expensive & unparseable)
    // BAD:  require(msg.sender == owner, "not owner: caller 0x123...");
    // GOOD: revert NotOwner(msg.sender, owner);
}
10

Inheritance

Inheritance with 'is'

Inheritance uses the 'is' keyword. A child contract inherits all state variables, functions, and modifiers from its parent. Parent constructor arguments are passed in the inheritance list (Animal("dog")) or via the constructor modifier style. A child can override functions marked virtual in the parent. Solidity supports multiple inheritance (linearization via C3).

solidity
contract Animal {
    string public species;

    constructor(string memory _species) {
        species = _species;
    }

    function speak() public virtual returns (string memory) {
        return "...";
    }
}

contract Dog is Animal {
    constructor() Animal("dog") {}

    // Override parent function
    function speak() public override returns (string memory) {
        return "Woof";
    }
}

// Multiple inheritance
contract Puppy is Dog {
    function speak() public override returns (string memory) {
        return "Yip";
    }
}

Virtual & Override

virtual marks a function as overridable; override marks a function as overriding its parent. Functions are non-virtual by default (safer). When overriding from multiple parents that share a base, list them: override(A, B). To allow further overrides, use virtual override. State variables cannot be overridden (only shadowed in memory).

solidity
contract Base {
    function f() public pure virtual returns (string memory) {
        return "base";
    }

    function g() public pure virtual returns (string memory) {
        return "base-g";
    }
}

contract Middle is Base {
    // Override a single parent
    function f() public pure override returns (string memory) {
        return "middle";
    }
}

contract Multi is Base {
    // Override (also virtual so children can override again)
    function g() public pure virtual override returns (string memory) {
        return "multi";
    }
}

contract Combined is Middle, Multi {
    // Override multiple parents (explicit list)
    function f() public pure override(Middle, Base) returns (string memory) {
        return "combined";
    }
    function g() public pure override(Multi, Base) returns (string memory) {
        return "combined-g";
    }
}

Super Keyword

super.method() calls the parent's version of the method, allowing you to extend rather than replace behavior. With single inheritance it's intuitive. With multiple inheritance, super follows the C3 linearization — it may call a sibling contract, not the lexical parent. Always check the linearization order (most-derived last) when using super in diamond inheritance.

solidity
contract A {
    function label() public pure virtual returns (string memory) {
        return "A";
    }
}

contract B is A {
    function label() public pure virtual override returns (string memory) {
        return string.concat("B>", super.label());  // calls A.label()
    }
}

contract C is B {
    function label() public pure override returns (string memory) {
        return string.concat("C>", super.label());  // calls B.label()
    }
}

// super refers to the NEXT contract in the linearization,
// not necessarily the lexical parent. With multiple inheritance,
// super.label() may call a sibling.

contract P1 { function f() public pure virtual returns (uint) { return 1; } }
contract P2 { function f() public pure virtual returns (uint) { return 2; } }
contract Child is P1, P2 {
    function f() public pure override(P1, P2) returns (uint) {
        return super.f();  // calls P2.f() (last in linearization)
    }
}

Multiple Inheritance

Solidity supports multiple inheritance via the 'is' keyword. List parents from most-base to most-derived: 'is Ownable, Pausable'. The C3 linearization algorithm produces a deterministic order, resolving the diamond problem. Use 'override(A, B)' when overriding a function inherited from multiple parents. Constructors run in linearization order (parents first).

solidity
contract Ownable {
    address public owner;
    constructor() { owner = msg.sender; }
    modifier onlyOwner() { require(msg.sender == owner); _; }
}

contract Pausable {
    bool public paused;
    modifier whenNotPaused() { require(!paused); _; }
}

// Multiple inheritance: list parents from most base to most derived
contract Token is Ownable, Pausable {
    mapping(address => uint256) public balances;

    function mint(address to, uint256 amount)
        public
        onlyOwner        // from Ownable
        whenNotPaused    // from Pausable
    {
        balances[to] += amount;
    }
}

// C3 Linearization order (for 'Token'):
// Token -> Pausable -> Ownable -> (object)
// Determined by the order in 'is' list (left to right, base first)
//
// Diamond problem is resolved by C3 — there is a single
// deterministic order, no ambiguity. But you must understand it
// when using super.

Constructor Inheritance

Parent constructor arguments can be passed in the inheritance list (A(42)) for fixed values, or in the child constructor via modifier syntax (A(_a)) for runtime values. With multiple parents, pass them in linearization order (most-base first). If a parent has a no-arg constructor, you can omit it. Constructors run once at deployment, in linearization order.

solidity
contract A {
    uint256 public a;
    constructor(uint256 _a) { a = _a; }
}

// Style 1: pass args in inheritance list
contract B1 is A(42) {
    // 'a' is set to 42 at deployment
}

// Style 2: pass via modifier-style in child constructor
contract B2 is A {
    constructor(uint256 _b) A(_b * 2) {
        // 'a' is set to _b * 2
    }
}

// Style 3: child has its own constructor + parent args
contract B3 is A {
    uint256 public b;
    constructor(uint256 _a, uint256 _b) A(_a) {
        b = _b;
    }
}

// With multiple parents, order = linearization order
contract C1 { constructor(uint) {} }
contract C2 { constructor(uint) {} }
contract C3 is C1, C2 {
    constructor(uint x) C1(x) C2(x+1) {}
}
11

Abstract Contracts & Interfaces

Abstract Contracts

An abstract contract has at least one function without a body (declared with virtual, no implementation). It cannot be deployed directly — a child must implement all abstract functions. Use abstract contracts when you want shared state + modifiers + partial implementation. The 'abstract' keyword is mandatory before 0.6.0 if any function lacks a body.

solidity
abstract contract Ownable {
    address public owner;

    constructor() { owner = msg.sender; }

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    // abstract function — no body, must be implemented by children
    function sensitiveAction() public virtual;

    // Can have implemented functions too
    function transferOwnership(address newOwner) public onlyOwner {
        owner = newOwner;
    }
}

contract Vault is Ownable {
    function sensitiveAction() public override onlyOwner {
        // implementation here
    }
}

Interfaces

Interfaces are like pure abstract contracts: no state, no constructors, no implemented functions, all functions external. They define a contract's ABI for interacting with unknown implementations. Use interfaces to call other contracts without inheriting logic. Name them with 'I' prefix (IERC20, IUniswapV2Pair). Functions in interfaces cannot have modifiers other than external view/payable.

solidity
interface IERC20 {
    // Functions only — no implementation, no state, no constructor
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function approve(address spender, uint256 amount) external returns (bool);
    function allowance(address owner, address spender)
        external view returns (uint256);
    function transferFrom(address from, address to, uint256 amount)
        external returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

contract MyToken is IERC20 {
    // Must implement ALL functions declared in the interface
    mapping(address => uint256) private _balances;
    uint256 private _totalSupply;

    function totalSupply() external view returns (uint256) {
        return _totalSupply;
    }
    // ... implement the rest
}

Virtual vs Abstract

abstract = at least one function has no body; the contract cannot be deployed. virtual = a function with a body that children CAN override. A function can be both (virtual + no body). Interfaces are implicitly fully abstract — every function is virtual, has no body, and is external. Use abstract for shared logic, interfaces for pure ABI definitions.

solidity
// ABSTRACT contract: has at least one unimplemented function
// Must declare 'abstract'; cannot be deployed directly
abstract contract Counter {
    uint256 public count;
    function increment() public virtual;  // no body -> abstract
    function reset() public {
        count = 0;
    }
}

// VIRTUAL: function HAS a body, but CAN be overridden
contract Base {
    function f() public pure virtual returns (uint) {
        return 1;
    }
}
contract Child is Base {
    function f() public pure override returns (uint) {
        return 2;
    }
}

// A function can be both virtual and abstract (no body)
abstract contract A {
    function f() public pure virtual;  // virtual + no body
}

// Interfaces are implicitly abstract — all functions virtual+abstract
interface I {
    function f() external;  // implicit virtual, no body
}

Interface Best Practices

Interface best practices: prefix with 'I', keep them minimal (only what callers need), all functions external, no state/constructors/implementation, events allowed. Use interfaces to: (1) call other contracts whose source you don't have, (2) define standards (IERC20, IERC721), (3) reduce bytecode by not inheriting implementation. Implementing an interface forces your contract to satisfy the standard.

solidity
// 1. Name with 'I' prefix
interface IERC721 {
    function ownerOf(uint256 tokenId) external view returns (address);
    function transferFrom(address from, address to, uint256 tokenId) external;
}

// 2. Keep minimal — only what callers need
interface IPriceFeed {
    function latestAnswer() external view returns (int256);
    function decimals() external view returns (uint8);
}

// 3. All functions external (required)
// 4. No state variables, no constructors, no implemented functions
// 5. Events ARE allowed in interfaces
interface IEvents {
    event SomethingHappened(uint256 indexed id);
}

// 6. Use to call contracts you don't have source for
contract Consumer {
    IPriceFeed public feed;
    constructor(address _feed) { feed = IPriceFeed(_feed); }
    function price() public view returns (int256) {
        return feed.latestAnswer();
    }
}

// 7. Implement to satisfy a standard (ERC20, ERC721)
contract MyToken is IERC20 { /* ... */ }

IERC20 Example (Interface)

IERC20 defines the ERC-20 fungible token standard. The interface declares the 6 required functions (totalSupply, balanceOf, transfer, approve, allowance, transferFrom) plus optional metadata (name, symbol, decimals) and 2 events. Any contract implementing this interface can be treated as an ERC-20 token. Use the interface to interact with any ERC-20 without knowing its internal implementation.

solidity
// Canonical ERC-20 interface
interface IERC20 {
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function decimals() external view returns (uint8);
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function allowance(address owner, address spender)
        external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount)
        external returns (bool);
}

// Minimal interaction using the interface
contract Wallet {
    IERC20 public token;

    constructor(address _token) { token = IERC20(_token); }

    function balance() public view returns (uint256) {
        return token.balanceOf(address(this));
    }

    function send(address to, uint256 amount) public returns (bool) {
        return token.transfer(to, amount);
    }
}
12

Libraries

Library Basics

Libraries are like contracts but cannot have state, inherit, or be inherited. They are deployed once and reused via DELEGATECALL (external) or inlined (internal). internal library functions are inlined into the calling contract — no separate deployment. Use libraries to group reusable functions (math, validation, formatting). The 'using SafeMath for uint256' directive lets you call methods as a.b().

solidity
library SafeMath {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "overflow");
        return c;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b <= a, "underflow");
        return a - b;
    }

    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) return 0;
        uint256 c = a * b;
        require(c / a == b, "overflow");
        return c;
    }

    // Libraries CANNOT have state variables
    // Libraries CANNOT inherit or be inherited
    // All functions must be internal or external
    // No constructor, no fallback, no receive
}

contract UsingSafeMath {
    using SafeMath for uint256;

    function calc(uint256 a, uint256 b) public pure returns (uint256) {
        return a.add(b).mul(2);   // a.add(b) calls SafeMath.add(a, b)
    }
}

Using For

'using Library for Type' attaches the library's functions to that type, so you can call value.method() instead of Library.method(value). The first parameter of the library function must match the type. You can also use the directive at file level (0.8.13+) with 'using {fn1, fn2} for Type;'. This is purely syntactic sugar — the compiler rewrites it to direct calls.

solidity
library Strings {
    function toString(uint256 value) internal pure returns (string memory) {
        // convert uint to decimal string
        if (value == 0) return "0";
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) { digits++; temp /= 10; }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits -= 1;
            buffer[digits] = bytes1(uint8(48 + value % 10));
            value /= 10;
        }
        return string(buffer);
    }
}

contract UsingExample {
    // Attach library functions to a type
    using Strings for uint256;

    function describe(uint256 x) public pure returns (string memory) {
        // x.toString() calls Strings.toString(x)
        return string.concat("Value: ", x.toString());
    }

    // Attach to a specific type globally (Solidity 0.8.13+)
    // using {toString} for uint256;  // file-level
    // using SafeMath for uint256;
    // using SafeMath for *;          // not allowed
}

Internal Libraries

Internal library functions are inlined into the calling contract's bytecode at compile time — there is no runtime DELEGATECALL, no library deployment, no call overhead. This makes them essentially free to call (just gas for the operations themselves). Use internal libraries for pure/view helpers (math, string conversion). The bytecode size of the caller grows, but call gas is lower than external libraries.

solidity
library Math {
    // internal functions are INLINED into the caller's bytecode
    // No separate deployment needed — saves gas on each call
    function max(uint256 a, uint256 b) internal pure returns (uint256) {
        return a > b ? a : b;
    }

    function min(uint256 a, uint256 b) internal pure returns (uint256) {
        return a < b ? a : b;
    }

    function average(uint256 a, uint256 b)
        internal pure returns (uint256)
    {
        // (a + b) / 2 can overflow; this avoids it
        return (a & b) + ((a ^ b) / 2);
    }
}

contract Calc {
    using Math for uint256;

    function doMath(uint256 x, uint256 y)
        public pure returns (uint256, uint256, uint256)
    {
        return (x.max(y), x.min(y), x.average(y));
    }
}

// Internal libraries are like helper functions that don't add
// runtime call overhead. Good for: math, validation, conversions.

External (Deployed) Libraries

External library functions are deployed as a separate contract and called via DELEGATECALL — they run in the caller's context (storage, msg.sender). This is useful for shared utility code that many contracts use (saves total deployment gas). Trade-offs: per-call DELEGATECALL overhead (~1400 gas) and the library must be deployed and its address linked at compile time. Most libraries use internal functions; external is rare.

solidity
library BigMath {
    // external functions => library is DEPLOYED as a separate contract
    // Calls use DELEGATECALL — runs in caller's context (storage/msg)
    function complexOperation(uint256[] storage arr)
        external view returns (uint256 sum)
    {
        for (uint256 i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
    }
}

contract UsesExternal {
    uint256[] public data;
    using BigMath for uint256[];

    function total() public view returns (uint256) {
        return data.complexOperation();  // DELEGATECALL to BigMath
    }
}

// External library trade-offs:
// + Shared bytecode: many contracts use one deployed library (saves deployment gas)
// + Caller bytecode stays small
// - DELEGATECALL overhead per call (~1400 gas)
// - Library must be deployed first and its address linked at compile time
//
// Most libraries are internal; external is rare (e.g., Create2BeaconProxy).

SafeMath Example (Pre-0.8.0)

SafeMath was the canonical overflow-checking library pre-0.8.0. Since 0.8.0, arithmetic auto-reverts on overflow, making SafeMath unnecessary for basic math. It's still useful for: (1) reading legacy code, (2) wrapping unchecked operations explicitly, (3) the pattern of 'require post-condition' as a teaching example. The OpenZeppelin version remains widely referenced.

solidity
// Before 0.8.0, arithmetic did NOT auto-check overflow.
// SafeMath was essential. Now it's mostly historical,
// but the pattern is instructive and still useful for
// explicit intent.

library SafeMath {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");
        return c;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        return sub(a, b, "SafeMath: subtraction overflow");
    }
    function sub(uint256 a, uint256 b, string memory errorMessage)
        internal pure returns (uint256)
    {
        require(b <= a, errorMessage);
        return a - b;
    }

    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) return 0;
        uint256 c = a * b;
        require(c / a == b, "SafeMath: multiplication overflow");
        return c;
    }

    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        return div(a, b, "SafeMath: division by zero");
    }
    function div(uint256 a, uint256 b, string memory errorMessage)
        internal pure returns (uint256)
    {
        require(b > 0, errorMessage);
        return a / b;
    }
}
13

Ether Transfers

Payable & msg.value

payable functions accept Ether via msg.value (in wei). The Ether is automatically added to the contract's balance — you don't need to manually track it unless you want per-user accounting. Validate msg.value for exact or minimum payments. Always refund excess Ether to avoid locking it in the contract. 1 ether = 10^18 wei.

solidity
contract PayableContract {
    address payable public owner;

    constructor() {
        owner = payable(msg.sender);
    }

    // payable = function accepts Ether
    function deposit() public payable {
        // msg.value = wei sent with this call
        // Ether auto-added to address(this).balance
        // No need to update a balance variable here
    }

    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }

    // Specify required payment
    function buyItem() public payable {
        require(msg.value == 1 ether, "must pay 1 ether");
        // process purchase
    }

    // Send excess back
    function refundExcess(uint256 price) public payable {
        uint256 excess = msg.value - price;
        if (excess > 0) {
            (bool ok, ) = msg.sender.call{value: excess}("");
            require(ok, "refund failed");
        }
    }
}

Transfer

address.transfer(amount) sends Ether and reverts on failure. It forwards a fixed 2300 gas stipend, which historically prevented reentrancy but is now considered fragile (gas costs changed in Istanbul 2019). Modern guidance: use transfer only when sending to a trusted address (like owner). For arbitrary recipients, use .call{value:} with explicit reentrancy guards, since the 2300 stipend may be insufficient for legitimate receive/fallback logic.

solidity
contract TransferExample {
    address payable public recipient = payable(0xAbC...);

    function sendViaTransfer() public payable {
        // transfer: throws on failure, forwards 2300 gas stipend
        // Simple and safe against reentrancy (limited gas)
        recipient.transfer(1 ether);
    }

    function sendFromContract() public {
        // address(this).balance = current contract balance
        recipient.transfer(address(this).balance);
    }

    // Characteristics:
    // - Reverts if recipient's receive/fallback uses > 2300 gas
    // - 2300 gas stipend: enough for an event log, not a storage write
    // - Safer than send (which returns false on failure)
    // - But BEWARE: post-Istanbul (2019), some operations cost more,
    //   breaking contracts that relied on 2300 gas. Prefer .call{}.
}

// transfer is DEPRECATED for pushing to arbitrary addresses.
// Use .call{value:} and handle the return value.

Send

address.send(amount) returns a boolean (true on success, false on failure) — it does NOT revert automatically. You MUST check the return value and revert on failure. Like transfer, it forwards only 2300 gas. send is error-prone (developers forget to check the return value) and is rarely used in modern code. Prefer .call{value:} for new contracts.

solidity
contract SendExample {
    address payable public recipient = payable(0xAbC...);

    function sendViaSend() public payable returns (bool) {
        // send: returns false on failure (does NOT revert)
        // Also forwards 2300 gas stipend like transfer
        bool success = recipient.send(1 ether);
        require(success, "send failed");  // you MUST check the return value
        return success;
    }

    // Common bug: forgetting to check the return value
    function buggy() public payable {
        recipient.send(1 ether);  // BUG: silently fails
        // state already updated => funds lost
    }

    // Correct: check + revert
    function correct() public payable {
        bool ok = recipient.send(1 ether);
        require(ok, "send failed");
    }
}

// send is rarely used in modern code.
// Prefer .call{value:} which gives more control + return data.

Call (Recommended)

.call{value: x}(...) is the recommended way to send Ether since 0.8.0. It forwards all available gas (subject to the 63/64 rule), so recipients can do complex logic. It returns (bool, bytes) — you MUST check the boolean and revert on failure. Because it forwards all gas, it's vulnerable to reentrancy — always pair with a nonReentrant modifier or follow checks-effects-interactions. Use call for arbitrary recipients; transfer only for trusted addresses.

solidity
contract CallExample {
    address payable public recipient = payable(0xAbC...);

    function sendViaCall() public payable returns (bool) {
        // .call returns (bool success, bytes memory data)
        // Forwards ALL gas (63/64 rule), so recipient can do more
        (bool success, ) = recipient.call{value: 1 ether}("");
        require(success, "call failed");
        return success;
    }

    // With calldata (call a function while sending Ether)
    function callWithFunction(address token, address to, uint256 amount)
        public returns (bool, bytes memory)
    {
        (bool ok, bytes memory data) = token.call{value: 0}(
            abi.encodeWithSignature("transfer(address,uint256)", to, amount)
        );
        return (ok, data);
    }

    // Forward gas explicitly
    function withGas() public returns (bool) {
        (bool ok, ) = recipient.call{gas: 100000, value: 0}("");
        return ok;
    }

    // IMPORTANT: call forwards all gas, so a malicious recipient
    // can re-enter your contract. Always use a reentrancy guard.
}

Balance Checking

address.balance returns the wei balance of any address (including address(this) for the contract itself). It's a view, free to call off-chain. WARNING: balance can be inflated by anyone via selfdestruct (forced Ether), so never rely on exact balance equality for logic — use >= or maintain your own accounting variables. Always cross-check that deposits sum equals contract balance as an invariant.

solidity
contract BalanceCheck {
    mapping(address => uint256) public deposits;

    function deposit() public payable {
        deposits[msg.sender] += msg.value;
    }

    function contractBalance() public view returns (uint256) {
        // Balance of THIS contract
        return address(this).balance;
    }

    function userBalance(address user) public view returns (uint256) {
        // Balance of any address (in wei)
        return user.balance;
    }

    function myBalance() public view returns (uint256) {
        return msg.sender.balance;
    }

    // Verify accounting invariant
    function verify() public view returns (bool) {
        // Sum of deposits should equal contract balance
        // (in a real contract, this would be expensive to compute)
        return address(this).balance >= deposits[msg.sender];
    }

    // CAUTION: address.balance can be manipulated by an attacker
    // who forces Ether via selfdestruct. Never use balance == X
    // as a strict equality check for logic — use >= or tracking vars.
    function dangerous() public view returns (bool) {
        // BAD: an attacker can selfdestruct-ETH into the contract
        // and break this exact check
        return address(this).balance == 1 ether;
    }
}
14

Gas Optimization

Storage vs Memory

Storage reads (SLOAD) cost ~2100 gas cold / 100 warm per access. Memory reads (MLOAD) cost only 3 gas. When a function reads a state variable multiple times (especially in a loop), cache it in a memory variable first. The first SLOAD is expensive; subsequent memory reads are nearly free. This is one of the easiest and most impactful optimizations.

solidity
contract StorageVsMemory {
    uint256 public stateVar;  // storage slot 0

    function expensive(uint256 x) public view returns (uint256) {
        // Each access to stateVar = SLOAD (~2100 gas cold, 100 warm)
        uint256 sum = 0;
        for (uint256 i = 0; i < x; i++) {
            sum += stateVar;  // SLOAD every iteration
        }
        return sum;
    }

    function cheap(uint256 x) public view returns (uint256) {
        // Cache to memory once: MLOAD costs 3 gas
        uint256 cached = stateVar;  // 1 SLOAD
        uint256 sum = 0;
        for (uint256 i = 0; i < x; i++) {
            sum += cached;  // MLOAD (cheap)
        }
        return sum;
    }

    // Rules of thumb:
    // - storage: persistent, ~2100 gas per cold SLOAD, ~5000-20000 per SSTORE
    // - memory: temporary, ~3 gas per access, free to create
    // - calldata: read-only, free (passed in)
    // Always cache state variables used multiple times in a function.
}

Variable Packing

Solidity packs consecutive state variables into 32-byte storage slots. To save gas, group small types (uint8, uint64, address) together so they share a slot. Reorder struct fields so small ones pack tightly. Each saved slot = ~20k gas on first access. Note: mapping and dynamic array entries always start a new slot and can't be packed.

solidity
contract Packing {
    // BAD: each takes a full 32-byte slot
    // 3 slots = 60k gas for cold SLOADs
    struct Bad {
        uint64 time;
        address from;   // 20 bytes
        uint64 amount;
    }

    // GOOD: pack small types together into 32 bytes
    // 1 slot = 20k gas (one SLOAD)
    struct Good {
        uint64 time;
        uint64 amount;
        address from;   // 20 bytes => total 8+8+20 = 36... still 2 slots
    }

    // Pack carefully: declare small types together
    struct Packed {
        uint32 a;       // 4
        uint32 b;       // 4
        uint32 c;       // 4
        uint32 d;       // 4 => 16 bytes => can fit address (20)? No, 16+20=36 > 32
        address e;      // => 2 slots
    }

    struct Tight {
        uint128 a;      // 16
        uint128 b;      // 16 => 1 slot (32 bytes)
    }

    // Order matters: variables are packed in declaration order.
    // A variable that doesn't fit starts a new slot.
    // Mapping/dynamic-array always start a new slot.

Unchecked Blocks

unchecked { ... } disables overflow/underflow checks inside the block, saving gas (~50-80 per op). Safe to use when you've already validated the bounds (e.g., after require(a >= b), a - b can't underflow). The classic safe use case is loop counters (i++ in a for loop will never overflow 2^256). Don't use unchecked blindly — silent overflow is a security risk.

solidity
contract UncheckedBlock {
    function loopWith(uint256 n) public pure returns (uint256 sum) {
        // 0.8+ checks overflow on every operation (extra gas)
        for (uint256 i = 0; i < n; i++) {
            sum += i;
        }
    }

    function loopUnchecked(uint256 n) public pure returns (uint256 sum) {
        unchecked {
            for (uint256 i = 0; i < n; i++) {
                sum += i;   // skips overflow check (cheaper)
            }
        }
    }

    // Safe to use when overflow is impossible:
    // - Loop counters that won't reach 2^256
    // - Differences of values you've already bounds-checked
    function safeSub(uint256 a, uint256 b) public pure returns (uint256) {
        require(a >= b, "underflow");  // checked here
        unchecked { return a - b; }    // safe: can't underflow now
    }

    // NEVER use unchecked when overflow/underflow is possible
    // unless you intentionally want wraparound (rare).
}

Custom Errors vs Require

Custom errors (0.8.4+) are cheaper than require(_, "msg") because the message string is stored in bytecode (costing gas per byte) and re-encoded on every revert. A custom error is just a 4-byte selector plus ABI-encoded args. For non-trivial error messages, custom errors save thousands of gas per call AND reduce deployment bytecode size. Always prefer custom errors in new code.

solidity
contract Errors {
    // BAD: string messages bloat bytecode + cost gas per call
    function oldStyle(uint256 x) public pure {
        require(x > 0, "value must be greater than zero");
        require(x < 100, "value must be less than one hundred");
    }

    // GOOD: custom errors are cheap and structured
    error InvalidValue(uint256 given, uint256 min, uint256 max);
    function newStyle(uint256 x) public pure {
        if (x == 0 || x >= 100) {
            revert InvalidValue(x, 1, 99);
        }
    }

    // Gas comparison (approximate):
    // require(_, "msg")     ~ 400 + 50 per byte of message
    // revert CustomError() ~ 400 + small selector cost
    //
    // For long messages, custom errors save thousands of gas
    // and reduce deployed bytecode size.

    // Define errors at file level to reuse across contracts
    error Unauthorized(address caller);
    function guarded() public {
        if (msg.sender != address(0)) revert Unauthorized(msg.sender);
    }
}

Cache State Variables in Loops

Cache array length in a local variable before the loop (saves an SLOAD per iteration). If the array is small, copy it to memory once (one SLOAD per element, then cheap MLOADs in the loop). Accumulators should always be local (memory) variables. For storage struct fields, cache the whole struct to memory if used multiple times.

solidity
contract Cache {
    uint256[] public data;
    mapping(address => uint256) public balances;
    address[] public users;

    // BAD: reads mapping in every iteration (SLOAD each time)
    function sumBad() public view returns (uint256) {
        uint256 total = 0;
        for (uint256 i = 0; i < users.length; i++) {
            total += balances[users[i]];  // SLOAD each iteration
        }
        return total;
    }

    // GOOD: cache length, use local var for accumulator
    function sumBetter() public view returns (uint256) {
        uint256 total = 0;
        uint256 len = users.length;  // cache length
        for (uint256 i = 0; i < len; i++) {
            address u = users[i];     // SLOAD once
            total += balances[u];     // still SLOAD, but unavoidable
        }
        return total;
    }

    // BEST: cache array to memory (if small enough)
    function sumMemory() public view returns (uint256) {
        address[] memory memUsers = users;  // copy to memory once
        uint256 total = 0;
        uint256 len = memUsers.length;
        for (uint256 i = 0; i < len; i++) {
            total += balances[memUsers[i]];  // MLOAD each iteration
        }
        return total;
    }
}

Short-circuit & Loop Optimization

Short-circuit evaluation: put cheap checks first in && and ||. Use ++i (prefix) instead of i++ (slightly cheaper — no temporary). Wrap loop counters in unchecked (i can't realistically overflow 2^256). Early-exit loops when you find what you need. Avoid expensive operations (external calls, storage writes) inside loops — batch them outside.

solidity
contract ShortCircuit {
    // && and || short-circuit: right side only evaluated if needed
    function cheap(address a, address b) public view returns (bool) {
        // Put the cheap check first
        return (a == b || expensiveCheck(a));
    }

    function expensiveCheck(address) internal view returns (bool) {
        // imagine a heavy computation
        return true;
    }

    // Loop optimization: prefer prefix increment
    function good() public pure returns (uint256) {
        uint256 sum = 0;
        for (uint256 i = 0; i < 10; ++i) {  // ++i slightly cheaper than i++
            sum += i;
        }
        return sum;
    }

    // Use unchecked for loop counters (always safe)
    function best() public pure returns (uint256) {
        uint256 sum = 0;
        uint256 i = 0;
        unchecked {
            while (i < 10) {
                sum += i;
                ++i;
            }
        }
        return sum;
    }

    // Avoid unnecessary work: precompute, batch, early-exit
    function find(uint256[] memory arr, uint256 target)
        public pure returns (int256)
    {
        for (uint256 i = 0; i < arr.length; ++i) {
            if (arr[i] == target) return int256(i);  // early exit
        }
        return -1;
    }
}
15

Time Units & Block Properties

block.timestamp

block.timestamp (alias 'now' pre-0.7.0) is the current block's Unix timestamp in seconds. It's set by the miner/validator and can be manipulated by ~15 seconds — NEVER use it for randomness or precise timing. It's fine for coarse durations (>=1 minute) and ordering. Common uses: time-locks, vesting schedules, deadlines. Use block.number for stricter guarantees.

solidity
contract Time {
    uint256 public startTime;
    uint256 public constant DURATION = 1 days;

    constructor() {
        startTime = block.timestamp;  // uint256 in 0.8+
    }

    function isExpired() public view returns (bool) {
        return block.timestamp >= startTime + DURATION;
    }

    function elapsed() public view returns (uint256) {
        return block.timestamp - startTime;
    }

    // WARNING: block.timestamp is set by the miner/validator.
    // They can manipulate it by ~15 seconds. NEVER use it for:
    //   - Randomness (keccak256(block.timestamp) is exploitable)
    //   - Precise timing (use block.number as a fallback)
    // It's fine for:
    //   - Coarse durations (>= 1 minute)
    //   - Deadlines that tolerate 15s drift
    //   - Order of events (timestamp comparison)

    // Time locks: require(block.timestamp > unlockTime)
    function lock() public payable returns (uint256) {
        return block.timestamp + 1 weeks;
    }
}

block.number & blockhash

block.number is the current block height. blockhash(n) returns the hash of block n, but ONLY for the last 256 blocks (older returns 0) and not the current block (returns 0). Both block.number and blockhash can be influenced by miners, so don't use them for high-stakes randomness. For real randomness, use Chainlink VRF or a commit-reveal scheme with user-provided entropy.

solidity
contract BlockInfo {
    function currentBlock() public view returns (uint256) {
        return block.number;
    }

    function prevHash(uint256 n) public view returns (bytes32) {
        // Only the last 256 blocks' hashes are available
        require(n > 0 && n <= 256, "out of range");
        return blockhash(block.number - n);
    }

    // blockhash returns 0 for current block, future blocks, or >256 old
    function currentHash() public view returns (bytes32) {
        return blockhash(block.number);  // returns 0!
    }

    // Use case: weak randomness (still exploitable by miners)
    function pseudoRandom() public view returns (uint256) {
        // A miner CAN see this and choose to withhold blocks
        // to manipulate the result. Don't use for high value.
        return uint256(keccak256(abi.encodePacked(
            blockhash(block.number - 1),
            block.timestamp,
            msg.sender
        )));
    }

    // For real randomness, use Chainlink VRF or a commit-reveal scheme.
}

Time Units (seconds/minutes/hours/days/weeks)

Time unit suffixes (seconds, minutes, hours, days, weeks) convert to seconds as uint256. 'years' was removed in 0.5.0 due to leap-year ambiguity. Ether units (wei, gwei, ether) are separate: 1 ether = 10^18 wei, 1 gwei = 10^9 wei. Always store amounts in wei and use the suffixes for readability in calculations.

solidity
contract TimeUnits {
    // Suffixes convert to seconds (uint256):
    uint256 public oneSecond = 1 seconds;     // 1
    uint256 public oneMinute = 1 minutes;     // 60
    uint256 public oneHour = 1 hours;         // 3600
    uint256 public oneDay = 1 days;           // 86400
    uint256 public oneWeek = 1 weeks;         // 604800

    // 'years' was removed in 0.5.0 (leap years made it ambiguous)

    function deadline() public view returns (uint256) {
        return block.timestamp + 7 days;
    }

    function hourly() public pure returns (uint256) {
        return 24 hours;  // 86400
    }

    // Ether units (different from time units!):
    uint256 public oneWei = 1 wei;            // 1
    uint256 public oneGwei = 1 gwei;          // 10^9
    uint256 public oneEther = 1 ether;         // 10^18

    function fromEther(uint256 e) public pure returns (uint256) {
        return e * 1 ether;  // convert to wei
    }

    function toEther(uint256 weiAmount) public pure returns (uint256) {
        return weiAmount / 1 ether;
    }
}

Gas & gasleft()

gasleft() returns the remaining gas at the point of the call (replaces deprecated msg.gas). It's useful for measuring how much gas a piece of code consumes. The 63/64th rule: external calls forward 63/64 of remaining gas, keeping 1/64 reserved so the caller can handle the return. Don't rely on exact gas values across hard forks — opcodes costs change.

solidity
contract GasTrack {
    function checkGas() public returns (uint256, uint256) {
        uint256 start = gasleft();   // gas remaining at this point
        // ...do some work...
        uint256 sum = 0;
        for (uint256 i = 0; i < 100; i++) {
            sum += i;
        }
        uint256 end = gasleft();
        return (start, start - end);  // (initial, used)
    }

    // 63/64th rule: when an external call is made, only 63/64 of
    // remaining gas is forwarded; 1/64 is reserved so the caller
    // can finish processing (e.g., revert handling).

    function forwardGas(address target) public returns (bool, uint256) {
        uint256 before = gasleft();
        (bool ok, ) = target.call{gas: 100000}("");
        uint256 after = gasleft();
        return (ok, before - after);
    }

    // gasleft() replaces the deprecated msg.gas.
    // It's a view, so it can be used in modifiers to enforce budgets.

    modifier usesLessThan(uint256 maxGas) {
        uint256 start = gasleft();
        _;
        require(start - gasleft() <= maxGas, "too much gas");
    }
}

Chain ID & Network Detection

block.chainid returns the current chain ID. It's essential for replay protection (EIP-155) and EIP-712 typed-data signatures (domain separator includes chainId). Cache chainId as immutable in the constructor for gas efficiency, but for EIP-712 you may need to recompute if the contract is forked to a new chain. Always include chainId in any hash that signs authorization.

solidity
contract Chain {
    function getChainId() public view returns (uint256) {
        return block.chainid;   // current chain ID
    }

    // Common chain IDs:
    // 1 = Ethereum mainnet
    // 5 = Goerli testnet (deprecated)
    // 11155111 = Sepolia testnet
    // 137 = Polygon
    // 42161 = Arbitrum One
    // 10 = Optimism
    // 56 = BNB Smart Chain

    modifier onlyMainnet() {
        require(block.chainid == 1, "only mainnet");
        _;
    }

    // Prevent cross-chain replay attacks by including chainId in hashes
    function domainSeparator() public view returns (bytes32) {
        return keccak256(abi.encode(
            keccak256("EIP712Domain(...)"),
            block.chainid,
            address(this)
        ));
    }

    // For multi-chain deployments, store chainId at construction
    uint256 public immutable chainId;
    constructor() {
        chainId = block.chainid;
    }

    // NOTE: chainId can change during a fork (rare). For EIP-712
    // signatures, cache it but allow re-initialization on mismatch.
}
16

Visibility Modifiers

Public

public functions can be called from anywhere: externally (by users or other contracts), internally (from within the contract), and from derived contracts. public state variables automatically get a getter function. Public is the most permissive but adds bytecode (the function must support both internal and external call conventions). When a public function is called internally via 'this.f()', it becomes an expensive external call.

solidity
contract Public {
    uint256 public counter;   // public state var => auto getter

    // public function: callable from anywhere
    // - externally (via transaction or call)
    // - internally (from this contract)
    // - from derived contracts
    function increment() public {
        counter += 1;
    }

    function caller() public {
        increment();         // internal call (cheap)
        this.increment();    // external call (expensive, creates a msg)
    }

    // Public is the most permissive. Auto-getters for state vars
    // are 'external' view functions generated by the compiler.
    //
    // Cost: public functions are part of the ABI and add bytecode.
    // Internal calls to public functions skip the external wrapper.
}

External

external functions can ONLY be called from outside the contract — not internally. They are slightly cheaper than public because they read directly from calldata (no memory copy). Use external for functions that should be entry points only. If you need to call a function both internally and externally, make it public OR split it into an external wrapper + internal impl.

solidity
contract External {
    // external: ONLY callable from outside the contract
    // CANNOT be called internally (this.f() works but is expensive)
    function doWork(uint256 x) external pure returns (uint256) {
        return x * 2;
    }

    function caller() public pure returns (uint256) {
        // doWork(5);  // COMPILER ERROR: external not callable internally
        return this.doWork(5);  // works but creates an external call (expensive)
    }

    // external with calldata params is cheapest for inputs
    function process(string calldata input)
        external
        pure
        returns (bytes32)
    {
        return keccak256(bytes(input));
    }

    // Use external when:
    // - The function is only meant to be called by users/other contracts
    // - You want to read calldata directly (cheaper than memory)
    //
    // State variables CANNOT be external.
}

Internal

internal functions can be called from within the contract and from any derived contract (subclasses). It is the default for state variables. internal is cheaper than public because it doesn't need the external-call wrapper. Use internal for helper functions and core logic that subclasses should be able to call or override. The convention is to prefix internal functions with underscore (_helper).

solidity
contract Internal {
    uint256 internal secret;   // internal state var (default)

    // internal: callable from this contract AND derived contracts
    function _helper(uint256 x) internal pure returns (uint256) {
        return x + 1;
    }

    function caller() public pure returns (uint256) {
        return _helper(5);   // internal call works
    }

    // Cannot be called externally:
    // contract Other { function f() public { Internal._helper(5); } }  // ERROR
}

contract Child is Internal {
    function useParent() public pure returns (uint256) {
        return _helper(10);   // OK: child can call internal
    }

    function readSecret() public view returns (uint256) {
        return secret;        // OK: child can read internal
    }
}

// internal is the DEFAULT for state variables.
// Use internal when:
//   - You want subclasses to use/override it
//   - You don't need external access
// It's cheaper than public (no external wrapper).

Private

private functions and state variables are accessible only within the defining contract — not even subclasses. It is the most restrictive visibility. CRITICAL: 'private' does NOT mean secret — all blockchain data is publicly readable via eth_getStorageAt and bytecode analysis. Use private only to prevent other contracts from calling/inheriting; use encryption/hashing for actual confidentiality.

solidity
contract Private {
    uint256 private counter;   // private state var

    // private: ONLY callable from THIS contract (not even subclasses)
    function _secret(uint256 x) private pure returns (uint256) {
        return x * 42;
    }

    function caller() public pure returns (uint256) {
        return _secret(5);   // OK: same contract
    }
}

contract Sub is Private {
    // function useIt() public pure returns (uint256) {
    //     return _secret(5);  // COMPILER ERROR: not accessible
    // }

    // function readCounter() public view returns (uint256) {
    //     return counter;     // COMPILER ERROR: not accessible
    // }
}

// IMPORTANT: 'private' does NOT mean hidden on-chain.
// All storage and bytecode are publicly readable via RPC.
// 'private' only means 'not part of the contract's ABI'
// and 'not callable/inheritable by other contracts'.
// For real confidentiality, use encryption or zero-knowledge proofs.

Visibility Best Practices

Best practices: use the most restrictive visibility that works (private > internal > external > public). Avoid public state variables unless you want the auto-getter — explicit getters let you add validation later. Use external+calldata for entry points. Prefix internal/private helpers with underscore. Each public/external function adds bytecode; minimizing the ABI reduces deployment and call gas.

solidity
contract Best {
    // 1. Default to the MOST restrictive visibility that works.
    //    Order: private < internal < external < public

    // 2. State variables: NEVER public unless you want a free getter.
    //    Prefer private/internal + explicit getter functions,
    //    so you can add validation/logic later.
    uint256 private _balance;
    function balance() public view returns (uint256) {
        return _balance;
    }

    // 3. External functions: use 'external' + 'calldata' for entry points
    function buy(uint256[] calldata ids) external payable {
        for (uint256 i = 0; i < ids.length; i++) {
            _buyOne(ids[i]);
        }
    }

    // 4. Internal helper: prefix with underscore convention
    function _buyOne(uint256 id) internal {
        // ...
    }

    // 5. Private for truly internal-only logic
    function _computeFee(uint256 amount) private pure returns (uint256) {
        return amount / 100;
    }

    // 6. Avoid public functions that are never called externally
    //    (wastes bytecode). Make them internal.
}

// Summary: visibility is part of the contract's API design.
// Smaller ABI = less bytecode = lower deployment + call gas.
17

Contract Interaction

Calling Other Contracts

To call another contract, cast its address to an interface type: IToken(addr).method(). View calls are free off-chain but cost gas when called from a transaction. Always check return values of state-changing calls (token.transfer returns bool). Casting an address to an interface does NOT verify the contract actually implements it — if the function doesn't exist, the call reverts.

solidity
interface IToken {
    function balanceOf(address) external view returns (uint256);
    function transfer(address, uint256) external returns (bool);
}

contract Caller {
    IToken public token;
    address public owner;

    constructor(address _token) {
        token = IToken(_token);
        owner = msg.sender;
    }

    // Call a view function on another contract
    function getBalance(address who) public view returns (uint256) {
        return token.balanceOf(who);  // external call
    }

    // Call a state-changing function
    function sendTokens(address to, uint256 amount)
        public returns (bool)
    {
        require(msg.sender == owner, "not owner");
        bool ok = token.transfer(to, amount);
        require(ok, "transfer failed");
        return true;
    }

    // Cast any address to an interface (no validation!)
    function checkAny(address maybeToken)
        public view returns (uint256)
    {
        return IToken(maybeToken).balanceOf(address(this));
    }
}

Interface-based Calls

Use interfaces to interact with any contract whose ABI you know, without needing its source. This decouples callers from implementations and works with any token/protocol following a standard (ERC20, ERC721, Chainlink feeds). Interfaces keep your bytecode small (no inherited logic) and let you write generic code (a wallet that works with any ERC20). Always prefer interfaces over concrete contracts for external calls.

solidity
interface IERC20 {
    function name() external view returns (string memory);
    function balanceOf(address) external view returns (uint256);
    function transfer(address, uint256) external returns (bool);
}

contract Wallet {
    IERC20 public immutable token;
    address public owner;

    constructor(address _token) {
        token = IERC20(_token);
        owner = msg.sender;
    }

    function tokenBalance() public view returns (uint256) {
        return token.balanceOf(address(this));
    }

    function withdraw(uint256 amount) public {
        require(msg.sender == owner, "not owner");
        token.transfer(owner, amount);
    }

    function tokenName() public view returns (string memory) {
        return token.name();
    }
}

// Why use interfaces?
// - Decouple: Wallet doesn't need to know token implementation
// - Reusability: works with ANY ERC20 (USDC, DAI, WETH, ...)
// - Smaller bytecode: don't inherit the token's code
// - Standardization: IERC20 is a documented ABI

Low-level call

address.call(payload) is the lowest-level call, returning (bool, bytes). Use it when you don't have an interface, want to forward arbitrary calldata, or need fine control (gas, value). Build calldata with abi.encodeWithSignature/Selector/Call. CRITICAL: 'ok' is false only on EVM failures (revert, out-of-gas), not on logical failures — decode 'data' to inspect revert reasons or custom errors.

solidity
contract LowLevel {
    // call: lowest-level call, returns (bool, bytes)
    function callFn(address target, bytes memory payload)
        public returns (bool, bytes memory)
    {
        (bool ok, bytes memory data) = target.call(payload);
        require(ok, "call failed");
        return (ok, data);
    }

    // With Ether
    function callWithEth(address target, bytes memory payload)
        public payable returns (bool, bytes memory)
    {
        (bool ok, bytes memory data) = target.call{value: msg.value}(payload);
        return (ok, data);
    }

    // With gas limit
    function callWithGas(address target, bytes memory payload, uint256 gas)
        public returns (bool, bytes memory)
    {
        return target.call{gas: gas}(payload);
    }

    // Helpers to build calldata:
    // abi.encodeWithSignature("transfer(address,uint256)", to, amount)
    // abi.encodeWithSelector(bytes4(selector), to, amount)
    // abi.encodeWithCall(IERC20.transfer.selector, to, amount)

    // WARNING: low-level call returns true even if the called function
    // reverts internally — you only get false on EVM-level failure.
    // Decode 'data' to inspect custom errors or return values.
    function safeCall(address target) public returns (uint256) {
        (bool ok, bytes memory data) = target.call(
            abi.encodeWithSignature("getValue()")
        );
        require(ok, "call reverted");
        return abi.decode(data, (uint256));
    }
}

Delegatecall

delegatecall runs the target's code in the CALLER's storage context — so state changes affect the caller, not the target. msg.sender and msg.value are preserved. This is the foundation of upgradeable proxies (EIP-1967, UUPS) and the Diamond pattern (EIP-2535). CRITICAL: storage layout must match between caller and target, or state gets corrupted. Always use a shared storage layout or the AppStorage pattern.

solidity
contract Delegate {
    // delegatecall: executes target's code in CALLER's context
    // - msg.sender is the ORIGINAL caller (preserved)
    // - msg.value is preserved
    // - Storage reads/writes affect the CALLER, not the target
    // - address(this) is the CALLER

    uint256 public num;
    address public sender;

    function delegateSet(address target, uint256 value) public {
        // target's setNum() runs in THIS contract's storage
        (bool ok, ) = target.delegatecall(
            abi.encodeWithSignature("setNum(uint256)", value)
        );
        require(ok, "delegatecall failed");
    }
}

contract Target {
    // MUST have the same storage layout as Delegate for sane behavior!
    uint256 public num;
    address public sender;

    function setNum(uint256 value) public {
        num = value;
        sender = msg.sender;  // this is the ORIGINAL caller, not Delegate
    }
}

// Use cases:
// - Proxy patterns (EIP-1967, UUPS, Transparent): upgradeable logic
// - Diamond pattern (EIP-2535): modular facets
//
// DANGER: storage layout mismatch => corrupted state.
// Both contracts MUST declare variables in the same order/types.
// Use a shared storage contract or app storage pattern.

Staticcall

staticcall executes the target's code but reverts if any state modification occurs — it's the safe way to read from untrusted contracts. The compiler automatically uses staticcall when you call a view/pure function via an interface. Use staticcall directly when you want to ensure a read-only call to a contract that might lie about being a view. It returns (bool, bytes) like call.

solidity
contract Static {
    // staticcall: like call, but REVERTS if the target modifies state.
    // Used to safely READ from untrusted contracts.

    function safeRead(address target, bytes memory payload)
        public view returns (bytes memory)
    {
        (bool ok, bytes memory data) = target.staticcall(payload);
        require(ok, "staticcall reverted");
        return data;
    }

    function getBalance(address token, address who)
        public view returns (uint256)
    {
        (bool ok, bytes memory data) = token.staticcall(
            abi.encodeWithSignature("balanceOf(address)", who)
        );
        require(ok, "balanceOf failed");
        return abi.decode(data, (uint256));
    }

    // Use cases:
    // - Read from untrusted contracts without risk of state changes
    // - Verify a contract behaves as a view function
    // - Compose view-only queries across multiple contracts

    // Distinguish from call():
    // - call: allows state changes (target can write)
    // - staticcall: forbids state changes (reverts if target writes)

    // view/pure functions are called via staticcall automatically
    // by the compiler when you call them normally.
    function normalCall(IToken t, address who) public view returns (uint256) {
        return t.balanceOf(who);  // compiler emits staticcall
    }
}

interface IToken { function balanceOf(address) external view returns (uint256); }
18

Security Patterns

Reentrancy Guard

The reentrancy guard is a mutex that prevents a function from being re-entered while it's still executing. Set a flag before the body, unset after. This prevents the classic attack where a malicious contract's receive/fallback re-calls withdraw() before the balance is zeroed (the DAO hack pattern). Apply nonReentrant to any function that makes an external call after a state change — even if you also follow checks-effects-interactions (defense in depth).

solidity
contract ReentrancyGuard {
    // Classic mutex pattern using a storage flag
    uint256 private _status = 1;  // 1 = NOT_ENTERED, 2 = ENTERED

    modifier nonReentrant() {
        require(_status == 1, "REENTRANT");
        _status = 2;
        _;
        _status = 1;
    }

    mapping(address => uint256) public balances;

    function withdraw() public nonReentrant {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "no balance");

        // CRITICAL: update state BEFORE the external call
        balances[msg.sender] = 0;

        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "send failed");
    }

    // Why needed: if we sent Ether FIRST, the attacker's receive()
    // could call withdraw() again before balances was zeroed,
    // draining the contract. This is the DAO hack pattern.
    //
    // Even with checks-effects-interactions, the guard is defense-in-depth.
    // Apply nonReentrant to ANY function that:
    //   - Sends Ether to msg.sender
    //   - Calls an external contract
    //   - Then continues executing
}

Checks-Effects-Interactions

Checks-Effects-Interactions (CEI) is the single most important security pattern: (1) run all require/validate checks, (2) update all state variables, (3) THEN make external calls. If an external call re-enters your contract, the state has already been updated, so the attacker can't double-spend. CEI alone prevents most reentrancy bugs; pair it with a nonReentrant modifier for defense-in-depth on critical functions.

solidity
contract CEI {
    mapping(address => uint256) public balances;

    // BAD: interaction BEFORE effect => reentrancy vulnerability
    function vulnerable() public {
        uint256 amount = balances[msg.sender];
        (bool ok, ) = msg.sender.call{value: amount}("");  // INTERACTION
        require(ok);
        balances[msg.sender] = 0;   // EFFECT (too late!)
        // Attacker's receive() re-called this; balances still has funds.
    }

    // GOOD: checks, then effects, then interactions
    function safe() public {
        // 1. CHECKS: validate conditions
        uint256 amount = balances[msg.sender];
        require(amount > 0, "no balance");

        // 2. EFFECTS: update state BEFORE external calls
        balances[msg.sender] = 0;

        // 3. INTERACTIONS: external calls LAST
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "send failed");
        // If attacker re-enters, balances[msg.sender] is already 0.
    }

    // Pattern: order your code as
    //   require() checks first,
    //   state updates second,
    //   external calls last.
    // This single rule prevents most reentrancy bugs.
    // Pair with nonReentrant for defense-in-depth.
}

Pull over Push

Pull over Push: instead of pushing Ether to many recipients in one transaction (where one bad recipient blocks everyone), credit each user's balance and let them pull their funds via withdraw(). This isolates failures (one user's bad receive/fallback doesn't block others) and reduces gas per transaction. Use for airdrops, exchanges, auction payouts, refunds. Combined with nonReentrant on withdraw().

solidity
contract PullOverPush {
    // BAD (push): send Ether to many recipients in one tx
    // If any recipient reverts (or is a contract that blocks), the WHOLE tx fails.
    mapping(address => uint256) public pendingWithdrawals;

    function pushPay(address[] memory recipients) public payable {
        uint256 share = msg.value / recipients.length;
        for (uint256 i = 0; i < recipients.length; i++) {
            // If recipients[i].call fails, the whole loop reverts.
            (bool ok, ) = recipients[i].call{value: share}("");
            require(ok, "push failed");
        }
    }

    // GOOD (pull): credit each user, let them withdraw individually
    function credit(address[] memory recipients) public payable {
        uint256 share = msg.value / recipients.length;
        for (uint256 i = 0; i < recipients.length; i++) {
            pendingWithdrawals[recipients[i]] += share;  // just update state
        }
    }

    function withdraw() public {
        uint256 amount = pendingWithdrawals[msg.sender];
        require(amount > 0, "nothing to withdraw");
        pendingWithdrawals[msg.sender] = 0;  // effect
        (bool ok, ) = msg.sender.call{value: amount}("");  // interaction
        require(ok, "withdraw failed");
    }

    // Pull pattern isolates failures: one user's failed withdrawal
    // doesn't block others. Common in: airdrops, exchanges, auctions.
}

Pausable Pattern

The Pausable pattern adds an emergency stop: a boolean 'paused' that gates critical functions via whenNotPaused modifier. The owner can pause/unpause. Use it as a circuit breaker when a bug or attack is detected — pause to stop further damage while preparing a fix. Often combined with upgradeable proxies (pause, then deploy patched logic). OpenZeppelin provides a battle-tested Pausable contract you should use instead of rolling your own.

solidity
contract Pausable {
    bool public paused;
    address public owner;

    constructor() { owner = msg.sender; }

    modifier whenNotPaused() {
        require(!paused, "contract is paused");
        _;
    }
    modifier whenPaused() {
        require(paused, "contract is not paused");
        _;
    }
    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    function pause() public onlyOwner whenNotPaused {
        paused = true;
    }
    function unpause() public onlyOwner whenPaused {
        paused = false;
    }

    // Critical functions check whenNotPaused
    function deposit() public payable whenNotPaused {
        // ...
    }

    function withdraw(uint256 amount) public whenNotPaused {
        // ...
    }

    // Use case: emergency stop. If a bug is discovered, pause the
    // contract to prevent further damage while a fix is prepared.
    // Often combined with upgradeable proxies to deploy a patched version.
}

// OpenZeppelin provides Pausable as a reusable contract.

Access Control

Access control patterns: (1) Ownable — single owner with onlyOwner modifier (simple, centralized). (2) RBAC — role-based with bytes32 roles and onlyRole modifier (flexible, multi-admin). (3) Multi-sig or Timelock — for high-value contracts, defer to Gnosis Safe or OpenZeppelin Timelock. CRITICAL: always use msg.sender for auth, NEVER tx.origin (which can be spoofed by malicious intermediate contracts). OpenZeppelin provides Ownable and AccessControl.

solidity
contract AccessControl {
    // 1. Simple owner (Ownable pattern)
    address public owner;
    constructor() { owner = msg.sender; }
    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }
    function setOwner(address newOwner) public onlyOwner {
        require(newOwner != address(0), "zero address");
        owner = newOwner;
    }

    // 2. Role-based (RBAC)
    mapping(bytes32 => mapping(address => bool)) public roles;
    bytes32 public constant ADMIN = keccak256("ADMIN");
    bytes32 public constant MINTER = keccak256("MINTER");

    modifier onlyRole(bytes32 role) {
        require(roles[role][msg.sender], "missing role");
        _;
    }

    function grantRole(bytes32 role, address account)
        public onlyRole(ADMIN)
    {
        roles[role][account] = true;
    }
    function revokeRole(bytes32 role, address account)
        public onlyRole(ADMIN)
    {
        roles[role][account] = false;
    }

    function mint(address to, uint256 amount) public onlyRole(MINTER) {
        // ...
    }

    // 3. Multi-sig / time-locked (defer to Gnosis Safe or OpenZeppelin Timelock)

    // Always use msg.sender (NOT tx.origin) for authorization.
    // tx.origin can be spoofed if a user interacts with a malicious
    // intermediate contract.
}
19

ERC20 Token Standard

ERC20 Interface

ERC-20 is the standard for fungible tokens (USDC, DAI, UNI). The interface defines 6 required functions (totalSupply, balanceOf, transfer, allowance, approve, transferFrom) and 2 events (Transfer, Approval). Optional metadata (name, symbol, decimals) is universally implemented. Always use OpenZeppelin's ERC20 base contract for new tokens — it's audited, handles edge cases, and is recognized by wallets/DEXs.

solidity
interface IERC20 {
    // Events
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    // Metadata (optional but standard)
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function decimals() external view returns (uint8);

    // Required functions
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function allowance(address owner, address spender)
        external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount)
        external returns (bool);
}

// ERC-20 is THE standard for fungible tokens (USDC, DAI, UNI, ...).
// Implemented by thousands of tokens; supported by every wallet/Dex.
// Implementing the interface correctly ensures interoperability.
// Use OpenZeppelin's ERC20 base contract — battle-tested and audited.

Transfer Function

transfer(to, amount) moves tokens from the caller to 'to'. Standard checks: nonzero recipient address, sufficient balance. Update balances BEFORE emitting the Transfer event (CEI pattern). Minting is modeled as Transfer from address(0); burning is Transfer to address(0). The decimals field (usually 18) means amounts are in atomic units (wei-equivalent); UIs divide by 10^decimals for display.

solidity
contract SimpleToken is IERC20 {
    string public name = "Simple Token";
    string public symbol = "SIM";
    uint8 public constant decimals = 18;
    uint256 public totalSupply;

    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    constructor(uint256 _initialSupply) {
        totalSupply = _initialSupply;
        balanceOf[msg.sender] = _initialSupply;
        emit Transfer(address(0), msg.sender, _initialSupply);
    }

    function transfer(address to, uint256 amount) external returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[msg.sender] >= amount, "insufficient");

        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }

    // Note: 0.8+ auto-checks underflow, so we can simplify:
    //   balanceOf[msg.sender] -= amount;
    // If insufficient, it reverts automatically.
    // But explicit require gives clearer error messages.
}

Approve & Allowance

approve(spender, amount) sets how much 'spender' can transfer on your behalf (via transferFrom). Common for DEXs and staking. WARNING: the approve race condition — changing allowance from A to B lets an attacker spend both A and B if they front-run. Mitigate by setting to 0 first, then to the new value, OR use increaseAllowance/decreaseAllowance (OpenZeppelin). The Approval event must be emitted on every change.

solidity
contract ApprovalExample is IERC20 {
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    // approve: allow 'spender' to spend up to 'amount' on your behalf
    function approve(address spender, uint256 amount) external returns (bool) {
        require(spender != address(0), "zero address");
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    // Common use cases:
    //   - DEX trading: approve the DEX router to spend your tokens
    //   - Staking: approve the staking contract
    //   - Subscriptions: approve a recurring spender

    // WARNING: the approve race condition.
    // If you change allowance from 100 to 50, an attacker who saw 100
    // could spend 100 BEFORE your tx, then spend 50 AFTER = 150 total.
    // Mitigation (EIP-20): set to 0 first, then to new value.
    //   approve(spender, 0); approve(spender, 50);
    // Or use increaseAllowance / decreaseAllowance (OpenZeppelin).

    function increaseAllowance(address spender, uint256 added)
        public returns (bool)
    {
        uint256 newAllowance = allowance[msg.sender][spender] + added;
        allowance[msg.sender][spender] = newAllowance;
        emit Approval(msg.sender, spender, newAllowance);
        return true;
    }
}

TransferFrom

transferFrom(from, to, amount) lets an approved spender move tokens from 'from' to 'to'. The spender's allowance must be >= amount; decrement the allowance as part of the effect (CEI). Used by DEX routers, staking contracts, and any pull-based workflow. Token quirks: USDT doesn't decrement allowance; some tokens revert on zero-address transfers. Always test with the specific token before integrating.

solidity
contract TransferFromExample is IERC20 {
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    // transferFrom: spender moves tokens from 'from' to 'to'
    // Requires allowance[from][spender] >= amount
    function transferFrom(address from, address to, uint256 amount)
        external returns (bool)
    {
        require(to != address(0), "zero address");
        require(balanceOf[from] >= amount, "insufficient balance");
        require(allowance[from][msg.sender] >= amount, "insufficient allowance");

        // Update allowance FIRST (CEI)
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }

    // Used by:
    //   - DEX routers (user approves router, router calls transferFrom)
    //   - Staking contracts
    //   - Any 'pull' workflow where a contract takes tokens from a user

    // Note on infinite allowance:
    // Some tokens (USDT) do NOT decrement allowance on transferFrom,
    // and others set allowance to max uint256 by default. Read the
    // specific token's docs before integrating.
}

ERC20 Events

ERC-20 defines two events: Transfer (emitted on every token move, including mint from address(0) and burn to address(0)) and Approval (emitted on every allowance change). These events are how dApps and indexers (The Graph, Dune, Etherscan) track token activity without querying state directly. Always emit them — omitting events breaks interoperability with wallets and exchanges that rely on logs.

solidity
contract ERC20Events {
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    uint256 public totalSupply;

    function _mint(address to, uint256 amount) internal {
        require(to != address(0), "zero address");
        totalSupply += amount;
        balanceOf[to] += amount;
        // Minting = transfer from address(0)
        emit Transfer(address(0), to, amount);
    }

    function _burn(address from, uint256 amount) internal {
        require(balanceOf[from] >= amount, "insufficient");
        balanceOf[from] -= amount;
        totalSupply -= amount;
        // Burning = transfer to address(0)
        emit Transfer(from, address(0), amount);
    }

    function _approve(address owner, address spender, uint256 amount) internal {
        emit Approval(owner, spender, amount);
    }

    // Events let dApps and indexers (The Graph, Dune) track:
    //   - Every transfer (incl. mint/burn)
    //   - Every approval change
    //   - Token holdings over time
    // Without events, integrating with wallets/DEXs is much harder.
}
20

Deployment & Testing

Remix IDE

Remix is the easiest way to start with Solidity — a browser IDE with no installation. It includes a compiler, JavaScript VM for local testing, deployment to testnets, a transaction debugger, and static analysis. Great for learning and prototyping. For production, move to Hardhat or Foundry for scripting, automated tests, and CI/CD integration.

solidity
// Remix is a browser-based IDE for Solidity.
// URL: https://remix.ethereum.org

// Features:
//   - File explorer, editor, compiler, deployer in one tab
//   - Built-in Solidity compiler (no install needed)
//   - Deploy to JavaScript VM, local node, or testnet
//   - Interact with deployed contracts via GUI
//   - Debug transactions step-by-step
//   - Static analysis tools (Remix Analyzer)

// Workflow:
// 1. Create a .sol file in the File Explorer
// 2. Compile in the "Solidity Compiler" tab (pick version)
// 3. Deploy in the "Deploy & Run Transactions" tab
// 4. Interact via the deployed contract panel

// Good for: learning, prototyping, quick tests, debugging.
// Not ideal for: production workflows, automated testing, scripting.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Hello {
    string public msg = "Hello from Remix";
}

Hardhat Setup

Hardhat is a JavaScript/TypeScript-based dev environment. Install via npm, configure networks and compiler in hardhat.config.ts. It provides compilation, testing (Mocha/Chai), a local node (hardhat node), deployment scripts, and console. The Hardhat Toolbox plugin bundle includes ethers.js, chai, typechain, and more. Use .env for secrets (private keys, RPC URLs).

solidity
// Hardhat: JavaScript/TypeScript development environment
// Install: npm install --save-dev hardhat
// Init:    npx hardhat init

// hardhat.config.ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";

const config: HardhatUserConfig = {
  solidity: {
    version: "0.8.19",
    settings: { optimizer: { enabled: true, runs: 200 } },
  },
  networks: {
    hardhat: {},                    // built-in local network
    sepolia: {
      url: `${process.env.ALCHEMY_URL}`,
      accounts: [process.env.PRIVATE_KEY ?? ""],
    },
  },
};
export default config;

// Project layout:
//   contracts/    Solidity source files
//   scripts/      deployment scripts (JS/TS)
//   test/         test files (Chai/Mocha)
//   ignition/     Hardhat Ignition modules (declarative deploy)

// Common commands:
//   npx hardhat compile
//   npx hardhat test
//   npx hardhat run scripts/deploy.ts --network sepolia
//   npx hardhat node                // start local node
//   npx hardhat console             // interactive REPL

Hardhat Tests

Hardhat tests use Mocha + Chai + ethers.js. getSigners() returns test accounts with ETH. connect(addr) sends a tx from a specific account. Assertions: equality (equal), reverts (revertedWith), events (emit + withArgs). TypeChain auto-generates TypeScript types from your ABIs, giving you autocomplete and compile-time checks. Run tests with 'npx hardhat test'.

solidity
// test/Token.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { Token } from "../typechain-types";

describe("Token", () => {
  let token: Token;
  let owner: any, addr1: any;

  beforeEach(async () => {
    [owner, addr1] = await ethers.getSigners();
    const Factory = await ethers.getContractFactory("Token");
    token = await Factory.deploy(1000);
    await token.waitForDeployment();
  });

  it("assigns initial supply to owner", async () => {
    expect(await token.balanceOf(owner.address)).to.equal(1000);
  });

  it("transfers tokens between accounts", async () => {
    await token.transfer(addr1.address, 100);
    expect(await token.balanceOf(addr1.address)).to.equal(100);
  });

  it("reverts on insufficient balance", async () => {
    await expect(
      token.connect(addr1).transfer(owner.address, 1)
    ).to.be.revertedWith("insufficient");
  });

  it("emits Transfer event", async () => {
    await expect(token.transfer(addr1.address, 100))
      .to.emit(token, "Transfer")
      .withArgs(owner.address, addr1.address, 100);
  });
});

// Run: npx hardhat test
// TypeChain generates typed contract bindings in typechain-types/.

Foundry Setup

Foundry (forge) is a fast Rust-based toolkit where tests are written in Solidity itself — no JavaScript/TypeScript needed. Install via foundryup. Key tools: forge (build, test, script), cast (CLI for RPC calls), anvil (local node), chisel (Solidity REPL). It's faster than Hardhat and includes built-in fuzz testing. Dependencies are git submodules under lib/. Config lives in foundry.toml.

solidity
// Foundry: Rust-based, fast Solidity dev toolkit
// Install:  curl -L https://foundry.paradigm.xyz | bash
//           foundryup
// Init:     forge init my-project

// Project layout:
//   src/        Solidity sources (Default = src/Contract.sol)
//   test/       Solidity tests (*.t.sol)
//   script/     Deployment scripts (*.s.sol)
//   lib/        Dependencies (git submodules, e.g. forge-std, openzeppelin)

// Commands:
//   forge build                 // compile
//   forge test                  // run tests
//   forge test -vvv             // verbose (shows console logs)
//   forge coverage              // code coverage
//   forge script script/Deploy.s.sol --rpc-url $RPC --broadcast

// Foundry config (foundry.toml):
// [profile.default]
// src = "src"
// out = "out"
// libs = ["lib"]
// solc = "0.8.19"
// optimizer = true
// optimizer_runs = 200

// Why Foundry?
// - Tests are written IN Solidity (no JS/TS context switch)
// - Extremely fast (parallelized, cached)
// - Built-in fuzzing and invariant testing
// - Cast/anvil/chisel CLI tools included

Foundry Tests (Solidity)

Foundry tests are Solidity contracts extending forge-std/Test. Use assertEq, assertTrue, etc. for assertions. Cheatcodes (vm.*) manipulate the EVM: vm.prank to spoof msg.sender, vm.warp to set time, vm.deal to mint ETH, vm.expectRevert to assert failures. Fuzz tests (testFuzz_*) auto-generate random inputs within constraints (vm.assume). Invariant tests (testInvariant_*) run random sequences to find broken invariants.

solidity
// test/Token.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "forge-std/Test.sol";
import "../src/Token.sol";

contract TokenTest is Test {
    Token public token;
    address owner = address(this);
    address user = address(0x1);

    function setUp() public {
        token = new Token(1000);
    }

    function testInitialSupply() public view {
        assertEq(token.balanceOf(owner), 1000);
    }

    function testTransfer() public {
        token.transfer(user, 100);
        assertEq(token.balanceOf(user), 100);
    }

    function testRevertOnInsufficient() public {
        vm.expectRevert("insufficient");
        token.transfer(user, 99999);
    }

    // Fuzz test: 'x' is randomized by the framework
    function testFuzzTransfer(uint256 x) public {
        vm.assume(x <= 1000);
        token.transfer(user, x);
        assertEq(token.balanceOf(user), x);
    }

    // Cheatcodes (vm.*):
    //   vm.prank(addr)         // next call from addr
    //   vm.startPrank(addr)    // all subsequent calls from addr
    //   vm.warp(timestamp)     // set block.timestamp
    //   vm.roll(blockNum)      // set block.number
    //   vm.deal(addr, 1 ether) // give ETH to addr
    //   vm.expectRevert(...)   // assert next call reverts
    //   vm.expectEmit(...)     // assert next call emits event
}

// Run: forge test -vvv

Was this helpful?