Lesson 6 / 25

Functions, Modifiers, Events and Errors

Write view, pure and payable functions, modifiers, events and custom errors.

Function building blocks

Functions declare their state mutability: view reads but does not modify state, pure neither reads nor modifies, payable can receive ether, and the default can modify state. Functions can return multiple values, named or unnamed. Modifiers wrap functions with reusable checks, such as onlyOwner, where _; marks where the function body runs. Events (event Transfer(address indexed from, address indexed to, uint256 value)) write logs that off-chain apps index and subscribe to; up to three parameters can be indexed for efficient filtering, and logs are much cheaper than storage but cannot be read by contracts. For errors, require(condition, "message") reverts with a reason string, while custom errors (error InsufficientBalance(uint256 available, uint256 needed); used with revert InsufficientBalance(bal, amount)) are cheaper and carry structured data; recent compilers also accept require(condition, CustomError(...)). assert is for invariants that should never fail. A revert undoes all state changes in the call. Use try/catch to handle failures from external calls.

An owned, pausable piggy bank

Modifiers, payable functions, events and custom errors.

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

contract PiggyBank {
    address public immutable owner;
    bool public paused;
    uint256 public immutable unlockTime;

    event Deposited(address indexed from, uint256 amount);
    event Withdrawn(uint256 amount);

    error NotOwner(address caller);
    error Locked(uint256 unlockTime);
    error Paused();
    error TransferFailed();

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner(msg.sender);
        _;                                   // the function body runs here
    }

    modifier whenNotPaused() {
        if (paused) revert Paused();
        _;
    }

    constructor(uint256 lockSeconds) {
        owner = msg.sender;
        unlockTime = block.timestamp + lockSeconds;
    }

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

    function setPaused(bool value) external onlyOwner {
        paused = value;
    }

    function withdrawAll() external onlyOwner {
        if (block.timestamp < unlockTime) revert Locked(unlockTime);
        uint256 amount = address(this).balance;
        emit Withdrawn(amount);
        (bool ok, ) = owner.call{value: amount}("");
        if (!ok) revert TransferFailed();
    }
}

Prefer custom errors

Custom errors cost less gas than long revert strings, both at deployment and when they trigger, and they give front ends structured data such as the unlock time instead of a vague message.

Quick check: What does the `_;` inside a modifier represent?

  • The place where the modified function's body is executed
  • The end of the contract
  • A comment
  • A revert
Answer

The place where the modified function's body is executed — Code before `_;` runs first, then the function body, then any code after it.