Skip to main content

Type a word from a question or an article title.

3 · Under the hood

A tour of the Solidity language - types, functions, errors and events by example

A short practical tour of Solidity for those who can read code: contract structure, types, visibility, modifiers, errors and events. Examples from the escrow.

4 min readUpdated:
In this article

Solidity is the main smart contract language for Ethereum and compatible networks. It resembles JavaScript and C++, but with important differences: every operation costs gas, code cannot be fixed after deployment, and a mistake can cost money. Here we will not teach programming from scratch; the goal is to learn to read contracts like ours and understand what happens in them. It is useful to first read what a smart contract is.

Contract structure

solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";

contract Escrow is Ownable2Step {
    uint16 public constant PLATFORM_FEE_BPS = 100;   // a constant
    uint256 public dealCount;                         // a state variable

    event OfferCreated(uint256 indexed dealId);       // an event
    error ZeroAmount();                               // an error

    function createOffer(uint256 amount) external returns (uint256) { ... }
}
  1. The licence and compiler version (pragma) at the start. The escrow is built with version 0.8.24 (how to verify).
  2. Imports from libraries (OpenZeppelin).
  3. The contract - like a class: state + functions.
  4. Inheritance (is): the contract takes over the parents' code.

Data types

TypeDescriptionExample
uint256, uint96, uint16unsigned integers of different sizesamount, rate
int256signed integersrare
booltrue/falsethe funded flag
addressan address (20 bytes)the buyer
address payablean address ETH can be sent toa recipient
bytes3232 arbitrary bytesthe terms hash
string, bytesdynamic datarare in money code
enuma set of named valuesStatus.Active
structa composite typethe Deal
mapping(K => V)a tabledeals[id]

There are no fractional numbers (why). Sizes affect packing in storage.

Enums and structs

solidity
enum Status { None, Offered, Active, Disputed, Settled }

struct Deal {
    address buyer;
    uint96 amount;
    Status status;
    // ...
}

mapping(uint256 => Deal) private _deals;

Statuses form a state machine: a deal passes from one state to another by strict rules (analysis).

Functions and visibility

solidity
function confirmDelivery(uint256 dealId) external nonReentrant { ... }

Visibility:

  • external - callable only from outside;
  • public - from outside and inside;
  • internal - inside the contract and its heirs;
  • private - only inside this contract (though the data is still readable).

Mutability:

  • view - reads state, does not change it;
  • pure - neither reads nor changes;
  • payable - can accept ETH.

Data locations: storage, memory, calldata

For reference types a location is specified:

  • storage - the contract's persistent memory (expensive);
  • memory - temporary within a call;
  • calldata - the transaction's input data, read-only (cheap).
solidity
function createOffer(OfferParams calldata p) external payable { ... }

calldata for function parameters saves gas because the data is not copied.

Modifiers

A modifier is a reusable "wrapper" around a function:

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

_ is where the function body runs. You will meet onlyOwner, whenNotPaused, nonReentrant (access patterns, reentrancy).

Errors

Previously require(cond, "text") was used. The modern way is custom errors:

solidity
error WrongStatus(Status expected, Status actual);

if (d.status != expected) revert WrongStatus(expected, d.status);

They are cheaper in gas and carry parameters meaningful to the interface. A revert cancels all changes of the transaction.

Events

solidity
event DealSettled(uint256 indexed dealId, Outcome outcome, uint256 buyerAmount, ...);
emit DealSettled(dealId, outcome, buyerAmount, sellerAmount, arbiterFee, platformFee);

Events are written to the transaction log and are cheap. Websites and indexers read them to build history (more). indexed fields allow fast filtering.

Global variables

VariableWhat it is
msg.senderwho called
msg.valuehow much ETH was sent
block.timestampthe block's time (seconds)
address(this)the contract's own address
solidity
uint64 deadline = uint64(block.timestamp) + 7 days;

The time units 1 days, 7 days are just numbers of seconds.

Handling ETH and tokens

Sending the main coin:

solidity
(bool ok, ) = payable(to).call{value: amount, gas: 50_000}("");

ERC-20 tokens go through the IERC20 interface and the SafeERC20 wrapper, which allows for non-standard implementations (specifics, implementation). The order "state first, external calls after" protects against attacks.

Reading a contract: a quick algorithm

  1. Find the data structures and statuses - that is the model.
  2. List the external functions - that is the interface.
  3. For each, see who can call and in which state.
  4. Find the places where money goes out (call, transfer) and check the order.
  5. Read the events: they tell what the contract considers important.

If you want to write, not only read: ERC-20 from scratch and the Foundry tutorial. The internals are in the section about the EVM.

Mentioned in

Ready to try it?

Create your first deal in a couple of minutes. The money is locked in a smart contract and paid out by rules you can see in advance.