Solidity Documentation

Solidity is a statically typed, contract-oriented language designed for the Ethereum Virtual Machine. This page is a practical handbook: from syntax and storage to patterns and security.

Target audience: developers who already understand basic Web3 concepts (accounts, transactions, gas) and want to write secure contracts.

What is Solidity

Solidity is the main language used to write smart contracts on Ethereum and EVM-compatible chains. It looks a bit like JavaScript mixed with TypeScript and C++, but it’s compiled to EVM bytecode and runs in a deterministic, gas-metered environment.

  • Statically typed — every variable has a type known at compile time.
  • Contract-oriented — the top-level structure is a contract, not a class.
  • EVM-focused — the language is designed around EVM limitations and gas costs.
Solidity vs JavaScript

Solidity is not dynamic: no eval, no dynamic types, no unbounded recursion, and every operation costs gas. Think more like a small, strict language for financial logic, not a general-purpose scripting language.

Versioning & pragma

Each Solidity file starts with a pragma that defines which compiler versions are allowed:

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

The caret (^) means “compatible with 0.8.20 up to (but not including) 0.9.0”. In production, you usually:

  • Pick a specific minor version, like ^0.8.20.
  • Configure your tooling (Hardhat / Foundry) to use that version.
  • Avoid very old versions (<0.8.0) due to missing built-in overflow checks.

Basic syntax

A minimal contract looks like this:

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

contract Counter {
    uint256 public count;

    function inc() external {
        count += 1;
    }

    function dec() external {
        count -= 1;
    }
}

Key points:

  • contract defines a new contract type.
  • uint256 public count; creates a state variable and an auto-generated getter.
  • external means callable from outside the contract via transactions or calls.

Value & reference types

Solidity has two main categories of types:

  • Value types — copied on assignment:
    • bool, uint256, int256
    • address, bytes32, enum
  • Reference types — reference storage locations:
    • array (fixed / dynamic)
    • struct
    • mapping

Examples:

bool public isActive;
uint256 public totalSupply;
address public owner;
bytes32 public id;

State & local variables

Solidity variables live either in contract storage or in function scope.

  • State variables — declared at contract level, stored permanently on-chain.
  • Local variables — declared inside functions, live only during call execution.
contract Example {
    uint256 public x; // state

    function set(uint256 _x) external {
        uint256 old = x; // local
        x = _x;
        // old disappears after the function returns
    }
}

Storage, memory & calldata

Reference types (arrays, structs, mappings) must specify where data lives:

  • storage — persistent, on-chain, expensive to write.
  • memory — temporary, in-memory during function execution.
  • calldata — read-only view into function arguments (for external functions).

Example

contract Users {
    struct User {
        string name;
        uint256 age;
    }

    mapping(address => User) public users;

    // Reads from storage (no location keyword at call site)
    function getUser(address account) external view returns (User memory) {
        return users[account]; // copied from storage to memory
    }

    // Accepts calldata array
    function sum(uint256[] calldata values) external pure returns (uint256) {
        uint256 s;
        for (uint256 i; i < values.length; i++) {
            s += values[i];
        }
        return s;
    }
}
Use calldata for external params

For external functions receiving arrays or strings, prefer calldata to avoid unnecessary copies and gas costs.

Mappings

Mappings are key-value stores in contract storage, similar to hash maps, but without iteration.

mapping(address => uint256) public balances;

Usage:

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

function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount, "Not enough");
    balances[msg.sender] -= amount;
    payable(msg.sender).transfer(amount);
}
No built-in length or iteration

Mappings do not track keys. If you need to iterate or know how many entries you have, keep a separate array of keys or a counter.

Structs

Structs allow you to group related fields into a custom type.

struct Position {
    uint256 size;
    uint256 collateral;
    bool isLong;
}

mapping(address => Position) public positions;

Usage:

function openLong(uint256 size, uint256 collateral) external {
    positions[msg.sender] = Position({
        size: size,
        collateral: collateral,
        isLong: true
    });
}

Arrays

Solidity supports fixed-size and dynamic arrays.

uint256[] public values;       // dynamic
address[3] public topHolders;  // fixed-size

Working with dynamic arrays

function pushValue(uint256 v) external {
    values.push(v);
}

function getValues() external view returns (uint256[] memory) {
    return values;
}
Be careful with unbounded loops

Iterating over large arrays in a single transaction can run out of gas. Design your storage so that loops over unbounded arrays are avoided or broken into smaller steps.

Functions

Functions define behavior. They can be external, public, internal, or private, and can be view, pure, or state-changing.

function setValue(uint256 _value) external {
    value = _value;
}

function getValue() external view returns (uint256) {
    return value;
}

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

Visibility & state mutability

Visibility keywords:

  • external — callable from outside the contract, not from internal code by default.
  • public — callable from everywhere, auto-generates getter for state variables.
  • internal — only from within the contract or derived contracts.
  • private — only from the current contract.

State mutability:

  • view — reads state, does not modify.
  • pure — does not read or modify state.
  • payable — can receive ETH along with the call.

Modifiers

Modifiers wrap functions with reusable checks, like access control or preconditions.

contract Ownable {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

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

contract Example is Ownable {
    function doAdminThing() external onlyOwner {
        // restricted logic
    }
}
Don’t overuse modifiers

Modifiers are powerful, but too many nested modifiers can make logic harder to read. Use them for common checks, not for complex control flow.

Errors, require & revert

Solidity uses a few mechanisms to signal failure:

  • require(condition, "message") — input validation, access checks.
  • revert("message") — explicit failure.
  • assert(condition) — internal invariants; should never fail in correct code.

Custom errors

Custom errors are cheaper than strings and more structured:

error NotOwner();
error InsufficientBalance(uint256 have, uint256 want);

contract Example {
    address public owner;
    mapping(address => uint256) public balance;

    constructor() {
        owner = msg.sender;
    }

    function withdraw(uint256 amount) external {
        if (msg.sender != owner) revert NotOwner();
        uint256 bal = balance[msg.sender];
        if (bal < amount) revert InsufficientBalance(bal, amount);

        balance[msg.sender] = bal - amount;
    }
}

Events & logging

Events are logs that go into the transaction receipt and can be indexed and read by off-chain systems (indexers, UIs).

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

function transfer(address to, uint256 value) external {
    // update balances...
    emit Transfer(msg.sender, to, value);
}

Use indexed fields for parameters you want to filter on quickly (addresses, ids).

Inheritance

Solidity supports single and multiple inheritance. This is how you compose behavior across contracts.

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

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

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

When multiple parents define the same function, you may need to use the override keyword and specify order carefully.

Interfaces

Interfaces define a contract’s external surface without implementation. They are used for interacting with other contracts.

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address who) external view returns (uint256);
    function transfer(address to, uint256 value) external returns (bool);
}

By using interfaces, your contract can call third-party protocols without needing the full source.

Libraries

Libraries are pieces of reusable code. They can be:

  • Embedded — compiled into the calling contract.
  • Deployed — their code lives at a separate address (less common today).

Using a library

library Math {
    function max(uint256 a, uint256 b) internal pure returns (uint256) {
        return a >= b ? a : b;
    }
}

contract Example {
    using Math for uint256;

    function bigger(uint256 x, uint256 y) external pure returns (uint256) {
        return x.max(y);
    }
}

Fallback & receive

Contracts have two special functions for handling unknown calls and plain ETH transfers:

  • receive() — triggered when contract receives ETH with empty calldata.
  • fallback() — triggered when no other function matches, or when calldata is non-empty but no function exists.
contract FallbackExample {
    event Received(address from, uint256 amount);
    event Fallback(address from, uint256 value, bytes data);

    receive() external payable {
        emit Received(msg.sender, msg.value);
    }

    fallback() external payable {
        emit Fallback(msg.sender, msg.value, msg.data);
    }
}
Keep fallback light

Fallback and receive should be small and cheap. Complex logic in fallback can be dangerous and expensive.

Constructors & immutables

Constructors run once, at deployment, and cannot be called again.

contract Example {
    address public owner;

    constructor(address _owner) {
        owner = _owner;
    }
}

Immutable variables are set in the constructor and then become read-only, but cheaper than regular storage:

contract Example {
    address public immutable owner;

    constructor(address _owner) {
        owner = _owner;
    }
}

Payable & value transfers

Functions and addresses marked as payable can receive ETH.

contract Vault {
    function deposit() external payable {
        // msg.value contains the amount sent
    }

    function withdraw(address payable to, uint256 amount) external {
        to.transfer(amount); // or call/value patterns
    }
}
Prefer call over transfer/send

In newer Solidity versions, .transfer and .send have a fixed gas stipend. Using (bool ok, ) = to.call{value: amount}(""); is more flexible, but must be combined with reentrancy protection.

Gas & optimization basics

Every operation on the EVM costs gas. Gas is paid in ETH and represents computation, storage, and bandwidth.

  • Writing to storage is expensive; reading is cheaper.
  • Loops over large arrays can be very expensive.
  • Emitting events costs gas, but is cheaper than storing the same data on-chain.

Simple optimization tips

  • Prefer uint256 everywhere on EVM chains.
  • Avoid unnecessary storage writes (e.g., don't write if the value doesn’t change).
  • Use calldata for read-only external inputs.
  • Pack tightly used storage variables where appropriate (but don’t sacrifice clarity for tiny gains).

Common patterns

Some classic Solidity patterns you will use all the time:

Checks-Effects-Interactions

function withdraw(uint256 amount) external {
    // Checks
    require(balance[msg.sender] >= amount, "Too much");

    // Effects
    balance[msg.sender] -= amount;

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

Pull over push payments

Instead of sending ETH to many users in a loop (push), store what they are owed and let them withdraw (pull).

Access control via modifiers

Combine modifiers like onlyOwner / onlyRole with OpenZeppelin’s access modules.

Security checklist

Writing Solidity is half about syntax, half about security. A short checklist:

  • Use recent Solidity versions (0.8.x) for built-in overflow checks.
  • Never use tx.origin for authorization.
  • Protect external ETH transfers with reentrancy guards and good patterns.
  • Be careful with delegatecall and raw assembly.
  • Limit who can call sensitive functions (owner/roles/multisig).
  • Pause critical flows when possible (using Pausable from OZ).
  • Write tests for failure cases, not just happy paths.
Assume users are adversarial

Anyone can call your public/external functions with any data. Design every function assuming the caller is trying to break or abuse it.

Testing Solidity

Solidity by itself doesn’t include a testing framework. You usually test contracts with:

  • Hardhat — TypeScript/JavaScript tests with Mocha + Chai + ethers.
  • Foundry — tests written in Solidity with fuzzing, cheatcodes, and invariants.

Example: simple Hardhat test

// test/Counter.ts
import { expect } from "chai";
import { ethers } from "hardhat";

describe("Counter", () => {
  it("increments and decrements", async () => {
    const Counter = await ethers.getContractFactory("Counter");
    const counter = await Counter.deploy();

    await counter.inc();
    expect(await counter.count()).to.equal(1n);

    await counter.dec();
    expect(await counter.count()).to.equal(0n);
  });
});

Example: simple Foundry test

// test/Counter.t.sol
pragma solidity ^0.8.20;

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

contract CounterTest is Test {
    Counter internal counter;

    function setUp() public {
        counter = new Counter();
    }

    function testIncrement() public {
        counter.inc();
        assertEq(counter.count(), 1);
    }
}

Resources

  • Official Solidity documentation and language spec.
  • Solidity by Example — small, focused snippets for most concepts.
  • OpenZeppelin Contracts — practical implementations of common patterns.
  • Hardhat and Foundry docs — for compilers, testing, and tooling integration.