Skip to the content.

Solidity Master Cheatsheet

Welcome to the Solidity Master Cheatsheet—created especially for new Solidity developers! Whether you’re just starting to explore the fundamentals of smart contract programming or need a convenient reference while building your DApps, this guide has you covered.

This cheatsheet is based on version 0.8.29

Table of Contents

Getting Started

// SPDX-License-Identifier: MIT
// Filename: HelloWorld.sol
// The function `greet()` will return the message "Hello, Solidity!"
pragma solidity ^0.8.29;

contract HelloWorld {
    string public message = "Hello, Solidity!";

    function greet() external view returns (string memory) {
        return message;
    }
}

Specifying compiler version

pragma solidity 0.8.29; // The contract must be compiled with exactly version 0.8.29

pragma solidity ^0.8.29; // Any version greater than or equal to 0.8.29, but strictly less than 0.9.0

pragma solidity >=0.8.0 <0.9.0; // Any version greater than or equal to 0.8.0, but strictly less than 0.9.0

Best Practice

Basic Data Types

Summary

Type Description Example
bool Boolean type, true or false bool isReady = true;
uint Unsigned integer (by default 256 bits, i.e. uint256) uint256 count = 10;
int Signed integer (by default 256 bits, i.e. int256) int256 temperature = -5;
address 20-byte Ethereum address (e.g. 0xABC123...), non-payable address owner = msg.sender;
address payable Same as address, but can receive Ether address payable wallet = payable(msg.sender);
bytes Dynamically sized byte array bytes data = hex"001122";
bytesN Fixed-size byte array of length N (1 ≤ N ≤ 32) bytes32 hash = keccak256(...);
string Dynamically sized UTF-8 data string name = "Alice";

bool

Integers: uint and int

// Unsigned integer
uint256 public totalSupply = 10000;
// Signed integer
int256 public temperature = -25;

Best Practice

address and address payable

Important

In Solidity ^0.8.0, you must explicitly convert an address to address payable if you want to send Ether:

address payable receiver = payable(someAddress);

bytes and bytesN

bytes: dynamically sized array of bytes.

bytesN: fixed-size array of length N (where 1 <= N <= 32).

// Dynamically sized
bytes public data = hex"DEADBEEF";

// Fixed-size, exactly 32 bytes
bytes32 public myHash = keccak256(abi.encodePacked("Solidity"));

string

string public greeting = "Hello, World!";

Variables & Visibility

In Solidity, variables are categorized based on where they are declared and how they can be accessed:

  1. State Variables
  2. Local Variables
  3. Global (Built-in) Variables
  4. Visibility Keywords

State Variables

pragma solidity ^0.8.29;

contract MyContract {
    // State variables
    uint256 public count;         // defaults to 0
    bool public isActive = true;

    // ...
}

Constants

uint256 public constant constantVariable = 10;

Immutable Variables

uint256 public immutable immutableVariable = 10;

Best Practice

Local Variables

function multiplyByTwo(uint256 _x) public pure returns (uint256) {
    // Local variable
    uint256 result = _x * 2;
    return result; // This value is not saved on-chain
}

Note

Global (Built-in) Variables

These are pre-defined variables and functions that give information about the blockchain, transaction, or message context. Examples include:

These variables are read from the environment and cannot be directly overwritten. They do not require a declaration like normal variables.

For more global variables, see here.

Example:

function whoCalledMe() public view returns (address) {
    // msg.sender is a global variable
    return msg.sender;
}

Visibility Keywords

In Solidity, visibility determines which parts of the contract or external entities can access a function or state variable.

Visibility Accessible By Common Use Cases

Visibility Accessible By Common Use Cases
public - Externally (via transactions or other contracts)
- Internally within the contract itself
Functions/variables that need to be read or called externally
external - Externally only (cannot be called internally without this.) Functions intended solely for external interaction (e.g., an API for Dapp users)
internal - Only within this contract or inheriting contracts Helper functions and state variables used by derived contracts
private - Only within this specific contract Sensitive logic or state variables that shouldn’t be accessed even by child contracts

Note

public

uint256 public count;

This allows reading count externally. The contract ABI will have a function count() that returns the variable’s value.

function getCount() public view returns (uint256) {
    return count;
}

external

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

// Inside another function in the same contract, you'd have to call: (but not recommended)
this.externalFunction();

internal

function internalHelper() internal pure returns (uint256) {
    return 42;
}

private

bool private privateVariable = true;

function privateHelper() private pure returns (uint256) {
    return 123;
}

Best Practices for Visibility

  1. Explicitly Specify Visibility

    • In Solidity, the default function visibility is internal if not specified.
    • Always define visibility (public, external, internal, private) for every function and state variable to avoid confusion and ensure clarity in your code.
  2. Use external for Functions Called Externally Only

    • If a function is never intended to be called internally, mark it external.
    • external functions can be slightly more gas-efficient than public because Solidity handles arguments differently for external calls.
  3. Restrict Access Whenever Possible

    • Follow the principle of least privilege.
    • Use private or internal whenever you don’t need external or inherited access.
    • This minimizes the contract’s attack surface and reduces the likelihood of unintended behavior.

Functions

Solidity functions define the behavior of your smart contract. They can be used to read or modify the contract’s state, interact with other contracts, or perform computations.

Basic Syntax

function functionName(Type param1, Type param2) [visibility] [stateMutability] returns (ReturnType) {
    // function body
}

Where:

Visibility

As covered in Visibility Keywords, a function’s visibility determines who can call it. The most common visibilities for functions are:

State Mutability: view, pure, and payable

  1. view
function getCount() public view returns (uint256) {
    return count;  // reading a state variable
}
  1. pure
function addNumbers(uint256 a, uint256 b) public pure returns (uint256) {
    return a + b;
}
  1. payable
function deposit() public payable {}

Return Values

You can return one or more values from a function. There are multiple ways to do so:

  1. Return Single Value
function getNumber() public pure returns (uint256) {
    return 42;
}
  1. Return Multiple Values
function getValues() public pure returns (uint256, bool) {
    return (100, true);
}
  1. Named Returns
function namedReturn() public pure returns (uint256 count, bool status) {
    count = 10;
    status = true;
}

Function Parameters and Data Location

For parameters of reference types (e.g., string, bytes, arrays, structs), you must specify the data location (memory, storage, or calldata):

Example using calldata:

function concatStrings(
    string calldata str1,
    string calldata str2
) external pure returns (string memory) {
    return string(abi.encodePacked(str1, str2));
}

Overloading and Overriding

function setValue(uint256 _value) public {
    // ...
}

function setValue(uint256 _value, bool _flag) public {
    // ...
}
contract Parent {
    function greet() public virtual pure returns (string memory) {
        return "Hello from Parent";
    }
}

contract Child is Parent {
    function greet() public pure override returns (string memory) {
        return "Hello from Child";
    }
}

Internal vs External Calls

Gas Considerations

  1. Function Complexity:

    • Avoid excessive loops or large data copy operations within a single function.
    • If possible, break down large operations into smaller functions or use off-chain solutions for heavy computations.
  2. Function Parameters:

    • For external functions, using calldata for parameters instead of memory is cheaper in many cases.
    • Passing large arrays around increases gas due to data copy overhead.

Best Practices for Functions

Control Flow

Control flow in Solidity is largely similar to other languages like JavaScript, C, or Python. You can use if/else, for, while, and do-while loops to direct program execution.

If / Else Statements

Syntax:

function checkValue(uint256 _x) public pure returns (string memory) {
    if (_x > 100) {
        return "Greater than 100";
    } else if (_x == 100) {
        return "Exactly 100";
    } else {
        return "Less than 100";
    }
}

require

require statements revert the transaction immediately if a condition is not met:

require(condition, "Error message");

This is often used for input validation or access control checks.

For Loops

function sumArray(uint256[] memory _arr) public pure returns (uint256) {
    uint256 total = 0;
    for (uint256 i = 0; i < _arr.length; i++) {
        total += _arr[i];
    }
    return total;
}

While Loops

function decrement(uint256 _x) public pure returns (uint256) {
    while (_x > 0) {
        _x--;
    }
    return _x; // returns 0
}

Do-While Loops

Solidity also supports do-while loops, which execute the loop body at least once before checking the condition:

function doWhileExample(uint256 _x) public pure returns (uint256) {
    uint256 counter = _x;

    do {
        counter--;
    } while (counter > 0);

    return counter;
}

Break and Continue

Like many languages, Solidity provides break and continue statements for early termination or skipping an iteration in loops.

function loopWithBreak(uint256 _x) public pure returns (uint256) {
    for (uint256 i = 0; i < _x; i++) {
        if (i == 5) break; // exits loop when i equals 5
        if (i == 3) continue; // skips remaining statements when i equals 3
        // ...
    }
}

Best Practices for Loops

Vanila Loop

Don’t use >= and <= in the condition

// 37975 gas (+347)
function loop_lte() public returns (uint256 sum) {
    for(uint256 n = 0; n <= 99; n++) {
        sum += n;
    }
}

Increment the variable in an unchecked block

// 32343 gas (-5285)
function loop_unchecked_plusplus() public returns (uint256 sum) {
    for(uint256 n = 0; n < 100;) {
        sum += n;
        unchecked {
            n++;
        }
    }
}

Just write it in assembly

// 26450 gas (-11178)
function loop_assembly() public returns (uint256) {
    assembly {
        let sum := 0
        for {let n := 0} lt(n, 100) {n := add(n, 1)} {
            sum := add(sum, n)
        }
        mstore(0, sum)
        return(0, 32)
    }
}

Array Loop

“Cache” the array’s length for the loop condition

// 25182 gas (-230)
function loopArray_cached(uint256[] calldata ns) public returns (uint256 sum) {
    uint256 length = ns.length;
    for(uint256 i = 0; i < length;) {
        sum += ns[i];
        unchecked {
            i++;
        }
    }
}

Error Handling

require vs revert vs assert

require(condition, "Error Message")

require(msg.sender == owner, "Not the owner");
require(amount > 0, "Amount must be greater than 0");

revert("Error Message")

if (balance < amount) {
    revert("Insufficient balance to withdraw");
}

assert(condition)

assert(totalSupply == sumOfAllBalances);

Key Points:

Custom Errors (>=0.8.4)

error Unauthorized(address caller);
error InvalidAmount(uint256 requested, uint256 available);

function restrictedAction() public view {
    if (msg.sender != owner) {
        revert Unauthorized(msg.sender);
    }
    // ...
}

function withdraw(uint256 amount) public {
    if (amount > balances[msg.sender]) {
        revert InvalidAmount(amount, balances[msg.sender]);
    }
    // ...
}

Benefits:

try/catch for External Calls

interface DataFeed { function getData(address token) external returns (uint value); }

contract FeedConsumer {
    DataFeed feed;
    uint errorCount;
    function rate(address token) public returns (uint value, bool success) {
        // Permanently disable the mechanism if there are
        // more than 10 errors.
        require(errorCount < 10);
        try feed.getData(token) returns (uint v) {
            return (v, true);
        } catch Error(string memory /*reason*/) {
            // This is executed in case
            // revert was called inside getData
            // and a reason string was provided.
            errorCount++;
            return (0, false);
        } catch Panic(uint /*errorCode*/) {
            // This is executed in case of a panic,
            // i.e. a serious error like division by zero
            // or overflow. The error code can be used
            // to determine the kind of error.
            errorCount++;
            return (0, false);
        } catch (bytes memory /*lowLevelData*/) {
            // This is executed in case revert() was used.
            errorCount++;
            return (0, false);
        }
    }
}

Best Practices for Error Handling

Arrays, Mappings, Structs, Enums

Arrays

Fixed-size Arrays

uint256[3] public fixedArray = [1, 2, 3];

Dynamic Arrays

uint256[] public dynamicArray;

Declaring and Using Arrays

Storage vs. Memory

Accessing Elements

uint256[] public numbers;

function addNumber(uint256 num) external {
    numbers.push(num); // dynamic array
}

function getNumber(uint256 index) external view returns (uint256) {
    return numbers[index];
}

Length

Push and Pop (Dynamic Arrays in Storage)

function removeLast() external {
    numbers.pop();
}

Gas Considerations:

Mappings

mapping(address => uint256) public balances;

function deposit() external payable {
    balances[msg.sender] += msg.value;
}
// Nested mapping
mapping(address => mapping(address => uint256)) public allowance;

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

Structs

struct User {
    string name;
    uint256 age;
    address wallet;
}

User[] public users; // dynamic array of User structs

function createUser(string memory _name, uint256 _age) external {
    // Option 1: Direct struct creation
    User memory newUser = User(_name, _age, msg.sender);
    users.push(newUser);

    // Option 2: Struct with named fields
    users.push(User({name: _name, age: _age, wallet: msg.sender}));
}

Enums

enum Status {
    Pending,    // 0
    Shipped,    // 1
    Delivered,  // 2
    Canceled    // 3
}

Status public currentStatus;

function setStatusShipped() external {
    currentStatus = Status.Shipped;
}

function cancelOrder() external {
    currentStatus = Status.Canceled;
}

function getStatus() external view returns (Status) {
    return currentStatus;
}

Enum Advantages:

Best Practices and Tips

  1. Use Arrays for Ordered Collections
  1. Use Mappings for Fast Lookups
  1. Structs for Grouped Data
  1. Guard Against Invalid Enum Values
  1. Avoid Large Arrays in Storage
  1. Be Aware of Storage Collisions

Modifiers

Syntax

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

function sensitiveAction() public onlyOwner {
    // This code runs after the `onlyOwner` checks
    // ...
}
  1. When sensitiveAction() is called, Solidity first executes the code in the onlyOwner modifier.
  2. If all checks (e.g., require) pass, it proceeds to execute the body of sensitiveAction().
  3. If any check fails, it reverts and never calls the function body.

Anatomy of a Modifier

modifier checkValue(uint256 _value) {
    // Code executed before the function body
    require(_value > 0, "Value must be greater than zero");

    _; // The function body is inserted here

    // Code executed after the function body
    emit ValueChecked(_value);
}

function doSomething(uint256 amount) public checkValue(amount) {
    // function body
}

the compiled code effectively looks like:

function doSomething(uint256 amount) public {
    require(amount > 0, "Value must be greater than zero");

    // function body (original code of doSomething)

    emit ValueChecked(amount);
}

Multiple Modifiers on One Function

modifier onlyOwner() { /* ... */ _; }
modifier whenNotPaused() { /* ... */ _; }

function specialAction() public onlyOwner whenNotPaused {
    // function body
}

Events

Key Characteristics

Declaring Events

event Transfer(address indexed from, address indexed to, uint256 value);

Emitting Events

function transfer(address _to, uint256 _amount) external {
    // ... perform transfer logic ...
    emit Transfer(msg.sender, _to, _amount);
}

Viewing Events Off-Chain

Example(in Viem):

import { parseAbiItem } from "viem"
import { publicClient } from "./client"

publicClient.watchEvent({
    address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
    event: parseAbiItem(
        "event Transfer(address indexed from, address indexed to, uint256 value)"
    ),
    args: {
        from: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
        to: "0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac"
    },
    onLogs: (logs) => console.log(logs)
})

Indexed vs. Non-Indexed Parameters

For example, in event Transfer(address indexed from, address indexed to, uint256 value);:

const filter = await publicClient.createContractEventFilter({
    abi: wagmiAbi,
    address: "0xfba3912ca04dd458c843e2ee08967fc04f3579c2",
    eventName: "Transfer",
    args: {
        from: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
        to: "0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac"
    }
})

Common Use Cases

  1. Token Transfers
    • ERC20 tokens emit a Transfer event whenever tokens move from one address to another.
  2. State Changes
    • Logging changes of ownership, updates to config variables, or changes in contract status.
  3. Debugging
    • Emitting certain events during testing can help you trace contract execution.
  4. Off-Chain Processing
    • Subgraphs (e.g., The Graph) or other indexing services use events to build databases of contract activity, enabling complex queries and analytics.

Simple Example

pragma solidity ^0.8.21;

contract Payment {
    event Deposit(address indexed sender, uint256 amount);
    event Withdraw(address indexed recipient, uint256 amount);

    function deposit() external payable {
        require(msg.value > 0, "No ether sent");
        emit Deposit(msg.sender, msg.value);
    }

    function withdraw(uint256 amount) external {
        require(amount > 0, "Zero withdrawal");
        require(address(this).balance >= amount, "Insufficient contract balance");

        (bool success, ) = payable(msg.sender).call{value: amount}("");
        require(success, "Withdraw failed");

        emit Withdraw(msg.sender, amount);
    }
}

Best Practices

Contract Inheritance & Interfaces

Contract Inheritance

Inheritance is achieved using the is keyword. A derived (child) contract can access and override the functions and state variables of its parent (base) contract(s).

Single Inheritance

contract Parent {
    string public parentName = "Parent";

    function greet() public pure returns (string memory) {
        return "Hello from Parent";
    }
}

contract Child is Parent {
    function sayHello() public view returns (string memory) {
        // Access parent's state variable
        return parentName;
    }
}

Multiple Inheritance

A contract can inherit from multiple base contracts using comma separation:

contract A {
    function foo() public pure returns (string memory) {
        return "A";
    }
}

contract B {
    function bar() public pure returns (string memory) {
        return "B";
    }
}

// Child inherits both A and B
contract C is A, B {
    // C now has both foo() from A and bar() from B
}

Overriding Functions

contract Parent {
    function greet() public pure virtual returns (string memory) {
        return "Hello from Parent";
    }
}

contract Child is Parent {
    // Overriding greet()
    function greet() public pure override returns (string memory) {
        return "Hello from Child";
    }
}
contract Child is Parent, Grandparent {
    function greet() public pure override(Parent, Grandparent) returns (string memory) {
        return "Hello from Child";
    }
}

Constructors in Inheritance

contract Parent1 {
    uint256 public x;

    constructor(uint256 _x) {
        x = _x;
    }
}

contract Parent2 {
    uint256 public y;

    constructor(uint256 _y) {
        y = _y;
    }
}

contract Child is Parent1, Parent2 {
    // Must call Parent1,2 constructor
    constructor(uint256 _childValue) Parent1(_childValue) Parent2(_childValue) {
        // additional child init
    }
}

Interfaces

An interface in Solidity is like a contract but with these restrictions:

  1. No state variables or constructor definitions.
  2. All functions must be external (or public in older versions of Solidity, but typically we use external).
  3. No function implementations—only signatures.
  4. Usually includes events that implementing contracts should emit.

Purpose: They define a standard for other contracts to implement, ensuring compatibility without forcing an implementation approach.

interface ICounter {
    function increment() external;
    function getCount() external view returns (uint256);
    event Counted(address indexed caller, uint256 newCount);
}

Implementing an Interface

contract Counter is ICounter {
    uint256 private count;

    function increment() external override {
        count++;
        emit Counted(msg.sender, count);
    }

    function getCount() external view override returns (uint256) {
        return count;
    }
}

Using Interfaces

Contracts can call interface functions to interact with external contracts that implement them:

contract Caller {
    function doIncrement(ICounter _counter) external {
        _counter.increment();
    }

    function readCount(ICounter _counter) external view returns (uint256) {
        return _counter.getCount();
    }
}

Combining Inheritance and Interfaces

You can mix inheritance and interfaces. For example, you could have a base abstract contract that implements some parts of an interface and leaves others for child contracts.

interface IVault {
    function deposit(uint256 amount) external;
    function withdraw(uint256 amount) external;
}

abstract contract VaultBase is IVault {
    // partially implement deposit logic
    // but keep withdraw abstract or add some logic

    function deposit(uint256 amount) external virtual override {
        // partial logic
    }
}

contract MyVault is VaultBase {
    // Must override deposit if not fully implemented
    // Must implement withdraw
    function deposit(uint256 amount) external override {
        // full deposit logic
    }

    function withdraw(uint256 amount) external override {
        // implement withdraw logic
    }
}

Abstract Contracts

Abstract contracts are those that cannot be deployed directly because they have at least one function without an implementation (virtual function). They serve as base contracts.

abstract contract Animal {
    function speak() public virtual returns (string memory);
}

contract Dog is Animal {
    function speak() public pure override returns (string memory) {
        return "Woof!";
    }
}

Diamond Inheritance and the “Linearization of Base Contracts”

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

contract B is A {
    function foo() public pure virtual override returns (string memory) {
        return "B";
    }
}

contract C is A {
    function foo() public pure virtual override returns (string memory) {
        return "C";
    }
}

// D inherits from both B and C
contract D is B, C {
    // Must override foo() again
    function foo() public pure override(B, C) returns (string memory) {
        // decide which parent's implementation to call, or write new logic
        // it will return "C"
        return super.foo(); // picks the rightmost parent's override by default (C) in linearization
    }
}

Best Practices

Libraries

Key Characteristics of Libraries

Library Function Types

Solidity libraries support two ways of using their functions:

Example: Internal Library

Most libraries are used as internal because it’s more gas-efficient (no external call) and simpler to manage.

// MathLib.sol
pragma solidity ^0.8.29;

library MathLib {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        return a + b;
    }

    function multiply(uint256 a, uint256 b) internal pure returns (uint256) {
        return a * b;
    }
}

// TestMath.sol
pragma solidity ^0.8.21;

import "./MathLib.sol";

contract TestMath {
    function testAdd(uint256 x, uint256 y) public pure returns (uint256) {
        return MathLib.add(x, y);
    }

    function testMultiply(uint256 x, uint256 y) public pure returns (uint256) {
        return MathLib.multiply(x, y);
    }
}

Example: External Library

You can define a library with public or external functions. In that case, your contract calls the library via delegatecall at runtime.

// ExternalLib.sol
pragma solidity ^0.8.29;

library ExternalLib {
    function externalAdd(uint256 a, uint256 b) external pure returns (uint256) {
        return a + b;
    }
}

To use this library in another contract:

pragma solidity ^0.8.29;

import "./ExternalLib.sol";

contract UseExternalLib {
    // The compiler inserts a reference that must be linked to the ExternalLib deployed address
    function compute(uint256 x, uint256 y) public pure returns (uint256) {
        return ExternalLib.externalAdd(x, y);
    }
}

Library for Struct Extensions

pragma solidity ^0.8.29;

library ArrayUtils {
    function findIndex(uint256[] storage arr, uint256 value) internal view returns (int256) {
        for (uint256 i = 0; i < arr.length; i++) {
            if (arr[i] == value) {
                return int256(i);
            }
        }
        return -1; // Not found
    }
}

contract MyArray {
    using ArrayUtils for uint256[];  // "using for" directive

    uint256[] private data;

    function addValue(uint256 value) external {
        data.push(value);
    }

    function findValue(uint256 value) external view returns (int256) {
        // We can now call findIndex() as if it's a member of data
        return data.findIndex(value);
    }
}

Best Practices

References