Skip to content
LogoLogo

Modules

Modules own their storage, errors, events, and functions, and declare the externals they offer. A contract uses modules and exposes exactly what it chooses, by declaring a module's externals or by wrapping its functions. Each module here is a file of its own under lib/, and the contract's file imports them.

lib/ownable.lithe
import "std/evm";
 
module Ownable is {
  storage ownable at "example.ownable" is record {
    owner : address;
  }
 
  error NotOwner;
 
  external view function owner() : address is
    ownable.owner
 
  view function onlyOwner()
    raises NotOwner
  is {
    assert(Evm.sender() = ownable.owner, NotOwner);
  }
 
  external function claim() is {
    ownable.owner := Evm.sender();
  }
}
lib/pausable.lithe
module Pausable is {
  storage pausable at "example.pausable" is record {
    paused : bool;
  }
 
  error Paused;
 
  external view function paused() : bool is
    pausable.paused
 
  view function whenNotPaused()
    raises Paused
  is {
    assert(not pausable.paused, Paused);
  }
 
  function setPaused(const paused : bool) is {
    pausable.paused := paused;
  }
}
lib/admin.lithe
import "./ownable";
import "./pausable";
 
(* A module that builds on another says so with `requires`. *)
module Admin is {
  requires Ownable;
  requires Pausable;
 
  external function pause()
    raises NotOwner
  is {
    Ownable.onlyOwner();
    Pausable.setPaused(true);
  }
}
modules.lithe
(* Modules replace inheritance. A module owns its storage, errors, events, and
   functions, and declares the externals it offers. A contract declares exactly
   which of them it exposes. Nothing becomes public by accident. *)
 
import "./lib/ownable";
import "./lib/pausable";
import "./lib/admin";
 
contract Vault is {
  (* Declare a module's externals to expose them as they are. Each declaration
     restates the external's header, so the contract lists its whole interface. *)
  use Ownable exposing {
    view function owner() : address;
    function claim();
  }
 
  use Pausable exposing {
    view function paused() : bool;
  }
 
  use Admin exposing {
    function pause()
      raises NotOwner;
  }
 
  storage is record {
    total : uint;
  }
 
  (* Or wrap a module's functions. Outside its module, module storage is
     `Module.domain.field`. *)
  external function deposit(const amount : uint)
    raises Paused
  is {
    Pausable.whenNotPaused();
    storage.total := storage.total + amount;
  }
 
  external view function isPaused() : bool is
    Pausable.pausable.paused
}
  • requires declares a module's dependencies. A contract that uses Admin must also use Ownable and Pausable.
  • A declaration in use M exposing must match the module's external exactly: its effect, parameters, result, and errors. It has no body.
  • A module's externals it doesn't declare are not part of the contract.
  • Each module's storage sits at its own ERC-7201 root, so modules never collide.
  • Outside its module, storage is written as Pausable.pausable.paused.