Modules and contracts
Lithe has no inheritance. Reusable behavior lives in modules, and a contract composes modules explicitly.
Modules
A module groups storage, types, errors, events, and functions under one name:
module Ownable is {
storage ownable at "example.ownable" is record {
owner : address;
}
error NotOwner;
event OwnershipTransferred of record {
indexed previous : address;
indexed next : address;
}
view function owner() : address is
ownable.owner
view function onlyOwner()
raises NotOwner
is {
assert(Evm.sender() = ownable.owner, NotOwner);
}
function transferOwnership(const next : address)
raises NotOwner
is {
onlyOwner();
emit OwnershipTransferred { previous = ownable.owner; next = next; };
ownable.owner := next;
}
}- A module's storage must be named. Inside the module it is
ownable.owner. Everywhere else it is qualified with the module's name:Ownable.ownable.owner. - A module's functions are internal. A module may also declare
externalfunctions: the interface it offers. A contract exposes them only by declaring them. See Exposing a module's externals. - A module's errors and events keep their names, so contracts that use the module raise and log them under the standard signatures.
Contracts
A contract uses modules, and exposes what a module offers in one of two ways: by declaring the module's externals, or by wrapping its functions in externals of its own.
module Counter is {
storage counter at "example.counter" is record {
count : uint;
}
function bump(const by : uint) is {
counter.count := counter.count + by;
}
(* The interface the module offers. *)
external view function count() : uint is
counter.count
external function reset() is {
counter.count := 0;
}
}
contract App is {
(* Declaring exposes the module's externals as they are. *)
use Counter exposing {
view function count() : uint;
}
storage is record {
calls : uint;
}
(* Wrapping adds behavior around a module's functions. *)
external function bump(const by : uint) is {
storage.calls := storage.calls + 1;
Counter.bump(by);
}
external view function current() : uint is
Counter.counter.count
}App exposes count, bump, and current. It doesn't declare reset, so reset is not part of its interface. Nothing is exposed implicitly: every entrypoint a contract has is written in the contract, and if a module adds an external, no contract gains an entrypoint until it declares it.
Exposing a module's externals
use M exposing ... uses M, as use M does, and names the interfaces whose methods the contract exposes from M: named ones, an inline one in braces, or both. Each method must be one of M's externals:
use ERC20 exposing IERC20, IERC20Metadata;
use ERC20Permit exposing IERC20Permit, {
view function nonces(owner : address) : uint;
}A method is an external's header as an interface writes it: no external, name : type parameters, and ; in place of the body. It must match the module's external:
- the name and parameter types pick one external. An overloaded external is exposed once for each overload the interfaces list
- the parameter names and result must match
- the effect must match:
view,pure, or stateful. So mustpayable: a module'sexternal payable functionis exposed aspayable function raisesmust list the same errors. A named error set counts as its members
nonreentrant, and whether a parameter is const or var, are how the module defines the external, so an interface doesn't repeat them. A module appears in one use M exposing, which lists all its interfaces. For part of an interface, declare the part inline, or as an interface of its own.
A declaration that differs is an error that shows both headers:
module Ledger is {
storage ledger at "example.ledger" is record {
n : uint;
}
external view function balance() : uint is
ledger.n
}
contract Token is {
use Ledger exposing {
function balance() : uint;
}
}module `Ledger` does not have the contract's inline interface: `balance` is stateful in the inline interface and `view` in `Ledger`
interface: function balance() : uint
module: view function balance() : uintAn interface never adds behavior, so its methods have no body. To change what an external does, leave it out of the contract's interfaces, and write the contract's own external instead. A module's externals are not functions: nothing inside the contract calls them. Put shared logic in an internal function that the external and the contract both call.
lithe stubs file.lithe M prints a use M exposing block with every external M has.
Delegated modules
delegated after the module's name in a use runs the module from its facet: a contract of its own, compiled from the module alone and shared by every contract that marks it. None of the module's code is in the contract, which is how a contract stays under the size limit. The contract forwards the externals it exposes to the facet, and calls the module's functions across into it:
module Counter is {
storage counter at "example.facet.counter" is record {
count : uint;
}
function bump(const by : uint) : uint is {
counter.count := counter.count + by;
} with counter.count
external view function count() : uint is
counter.count
}import "std/evm";
module Clock is {
view function now() : timestamp is
Evm.blockTimestamp()
}import "./lib/counter";
import "./lib/clock";
contract App is {
use Counter delegated exposing {
view function count() : uint;
}
use Clock delegated;
storage is record {
last : timestamp;
}
external function tick() : uint is {
storage.last := Clock.now();
} with Counter.bump(1)
}count runs in Counter's facet. tick runs in App, and calls Clock.now and Counter.bump across, on App's storage, with the results, errors, and events they would have inline. use Clock delegated; marks a module the contract uses without exposing anything, and use Counter delegated exposing { ... } one it exposes externals of.
A function crosses when its parameters and result are ABI values, as an external's are. One that takes a storage reference, a map, or an option, returns one, or is generic can't, and calling it from the contract is an error. A function that defers crosses too: its calls join the queue of the contract's transition, and run in order with its own after its body commits, as they would inline:
module Tally is {
storage tally at "example.facet.tally" is record {
n : uint;
}
pure function first(const xs : list(uint)) : option(uint) is
if xs.length = 0 then None else Some(xs[0])
}
contract App is {
use Tally delegated;
storage is record {
n : uint;
}
external function keep(const xs : list(uint)) is {
storage.n := case Tally.first(xs) of {
| Some(x) -> x
| None -> 0
};
}
}`Tally.first` runs in `Tally`'s facet, and contract code can't call it across: its result is an option; move the call into `Tally`, or use `Tally` without `delegated`A module keeps a function to itself with private: only the module's own functions call it, so its facet has no entrypoint for it, and a contract, or another module, that calls it is an error:
module Tally is {
storage tally at "example.private.tally" is record {
n : uint;
}
private function step(const by : uint) is {
tally.n := tally.n + by;
}
function bump() is {
step(1);
}
}
contract App is {
use Tally;
storage is record {
n : uint;
}
external function go() is {
Tally.step(2);
}
}`Tally.step` is private to `Tally`; only `Tally`'s own functions call itSee Contract size limit for what runs where, what a call across costs, and how facets are deployed.
Guards
Some of a module's externals need a check that only the contract can write: who may price a token, pause trading, or mint depends on the contract's own roles and state. Solidity leaves an abstract _canSetX() for the contract to override. In Lithe, the module declares a guard, its externals name it with guarded, and every contract that exposes them writes the check:
import "std/evm";
module Prices is {
storage prices at "example.prices" is record {
ofToken : map(uint, uint);
}
error ZeroPrice;
(* Who may price a token is the contract's to say. *)
guard pricer(const tokenId : uint);
external function setPrice(const tokenId : uint; const price : uint)
guarded pricer(tokenId)
raises ZeroPrice
is {
assert(price > 0, ZeroPrice);
prices.ofToken[tokenId] := price;
}
external view function price(const tokenId : uint) : uint is
prices.ofToken[tokenId]
}
contract Shop is {
storage is record {
makers : map(uint, address);
}
error NotMaker of uint;
use Prices exposing {
function setPrice(tokenId : uint; price : uint)
raises ZeroPrice | NotMaker;
view function price(tokenId : uint) : uint;
}
(* A token's maker prices it. *)
guard Prices.pricer(const tokenId : uint)
raises NotMaker
is {
assert(Evm.sender() = storage.makers[tokenId], NotMaker(tokenId));
}
external function make(const tokenId : uint) is {
storage.makers[tokenId] := Evm.sender();
}
}A call to setPrice runs Shop's check first, with the call's tokenId, then the module's body. The module never sees the check, so its code is the same in every contract, and two contracts that expose Prices can guard it differently.
- A module declares
guard name(params);: a name and typed parameters, with no body. A guard with no parameters isguard name;. - An external names one guard with
guarded name(args), after its result and beforeraises. The arguments are the external's own parameters, by name, one for each of the guard's, of the same types: the check sees exactly what the caller sent. An external that dispatches on its argument into several entrypoints can't pass that argument, since each case is an entrypoint of its own. - A contract that exposes an external guarded by
M.namewrites exactly oneguard M.name(params) raises E is { ... }, whose parameters have the typesMdeclares; the names are the contract's. Leaving it out, writing it twice, writing one with other parameters, or writing one that no exposed external needs, is an error. guard M.name is f;checks by callingf, a function of the contract's or of a module it uses, that returns nothing. It passesfnone of the guard's parameters, or all of them. It raises whatfdoes:
use Ownable;
guard Prices.pricer is Ownable.checkOwner;A guard only fails. Its body is checked as a view: it can read storage, the call's context, such as Evm.sender(), and other contracts with query, but it can't write, emit, or defer. It runs once, before the external's body, and can't see or change what the body does. There are no default guards and no overrides: a check that lets everyone through is written as one, with a body of skip.
Guards cover entrypoints. When the contract's own code calls a module's internal functions, no guard runs: that is the contract trusting itself.
Errors
A guarded external raises its module's errors and its guard's. The interface that exposes it lists both, as setPrice lists ZeroPrice | NotMaker above; the ABI and lithe errors show the same. lithe stubs can't know the contract's guard, so it prints a guarded external with the module's errors and a reminder to write the check and add its errors.
lithe errors lists each guard with the errors it raises, lithe footprint shows the guard of each entrypoint that has one, and lithe abi-tree lists it under the entrypoint's guards.
Where a guard runs
A guard is contract code, so it runs where the contract's code runs. In a contract that runs the module itself, it is the first thing the entrypoint does. When the module is a facet, the router decodes the call's arguments, runs the guard, and only then forwards the call to the module's facet. The facet is built from the module alone and runs its externals unguarded, so every contract that exposes the module shares one facet at one address, whatever its guards. Calling a facet directly skips every guard, but a facet called directly runs against its own storage, not the storage of any contract that uses it, so there is nothing there to protect.
The upgrade guard is the same idea for the upgrade machinery: the check upgradeToAndCall runs before it changes anything, with the same rules, upgrade guard is f; included.
Mistakes
A contract that exposes a guarded external must write its guard:
module Notes is {
storage notes at "example.notes" is record {
ofId : map(uint, uint);
}
guard writer(const id : uint);
external function write(const id : uint; const note : uint) guarded writer(id) is {
notes.ofId[id] := note;
}
}
contract Board is {
use Notes exposing {
function write(id : uint; note : uint);
}
}the contract exposes `write`, which `Notes` guards with `writer`, so it must write the check:
guard Notes.writer(const id : uint) raises ... is { ... }Once, with the module's parameter types:
module Notes is {
storage notes at "example.notes" is record {
ofId : map(uint, uint);
}
guard writer(const id : uint);
external function write(const id : uint; const note : uint) guarded writer(id) is {
notes.ofId[id] := note;
}
}
contract Board is {
use Notes exposing {
function write(id : uint; note : uint);
}
guard Notes.writer(const id : uint) is { skip }
guard Notes.writer(const id : uint) is { skip }
}`guard Notes.writer` is written twicemodule Notes is {
storage notes at "example.notes" is record {
ofId : map(uint, uint);
}
guard writer(const id : uint);
external function write(const id : uint; const note : uint) guarded writer(id) is {
notes.ofId[id] := note;
}
}
contract Board is {
use Notes exposing {
function write(id : uint; note : uint);
}
guard Notes.writer(const id : address) is { skip }
}`guard Notes.writer` takes different parameters than `Notes` declares
module: guard Notes.writer(const id : uint)
contract: guard Notes.writer(const id : address)And only for a guard that an external it exposes names:
module Notes is {
storage notes at "example.notes" is record {
ofId : map(uint, uint);
}
guard writer(const id : uint);
external function write(const id : uint; const note : uint) guarded writer(id) is {
notes.ofId[id] := note;
}
external view function note(const id : uint) : uint is
notes.ofId[id]
}
contract Board is {
use Notes exposing {
view function note(id : uint) : uint;
}
guard Notes.writer(const id : uint) is { skip }
}no external the contract exposes is guarded by `Notes.writer`; remove this guardA guard reads, and nothing else:
import "std/evm";
module Notes is {
storage notes at "example.notes" is record {
ofId : map(uint, uint);
}
guard writer(const id : uint);
external function write(const id : uint; const note : uint) guarded writer(id) is {
notes.ofId[id] := note;
}
}
contract Board is {
storage is record {
checks : uint;
}
use Notes exposing {
function write(id : uint; note : uint);
}
guard Notes.writer(const id : uint) is {
storage.checks := storage.checks + 1;
}
}guard `Notes.writer` writes storage; a guard only reads, and reverts or lets the call throughmodule Notes is {
storage notes at "example.notes" is record {
ofId : map(uint, uint);
}
guard writer(const id : uint);
external function write(const id : uint; const note : uint) guarded writer(id) is {
notes.ofId[id] := note;
}
}
contract Board is {
event Checked of record {
id : uint;
}
use Notes exposing {
function write(id : uint; note : uint);
}
guard Notes.writer(const id : uint) is {
emit Checked { id = id; };
}
}guard `Notes.writer` cannot emit; a guard only readsimport "std/evm";
module Notes is {
storage notes at "example.notes" is record {
ofId : map(uint, uint);
}
guard writer(const id : uint);
external function write(const id : uint; const note : uint) guarded writer(id) is {
notes.ofId[id] := note;
}
}
contract Board is {
use Notes exposing {
function write(id : uint; note : uint);
}
guard Notes.writer(const id : uint) is {
defer Evm.transfer(Evm.sender(), 1wei);
}
}guard `Notes.writer` cannot defer an operation; a guard only readsA module passes a guard its external's parameters, and nothing else:
import "std/evm";
module Notes is {
storage notes at "example.notes" is record {
ofId : map(uint, uint);
owner : address;
}
guard writer(const id : uint);
external function write(const id : uint; const note : uint) guarded writer(owner) is {
notes.ofId[id] := note;
}
}
contract Board is {
use Notes exposing {
function write(id : uint; note : uint);
}
guard Notes.writer(const id : uint) is { skip }
}`owner` is not a parameter of `write`; a guard takes the external's parameters, by nameHooks
A module sometimes needs the contract's code in the middle of its own: to react to a transfer, or to supply a value only the contract knows, such as a token's name. Solidity leaves a virtual function, _beforeTokenTransfer or tokenURI, for the contract to override. In Lithe, the module declares a hook, its code calls the hook by name, and the contract that uses the module implements it:
import "std/evm";
module Tokens is {
storage tokens at "example.tokens" is record {
ownerOf : map(uint, address);
}
error NotOwner of uint;
(* What the contract does around every transfer. *)
hook beforeTransfer(const from : address; const to : address; const tokenId : uint);
hook afterTransfer(const from : address; const to : address; const tokenId : uint);
(* A token's name, which only the contract knows. *)
view hook nameOf(const tokenId : uint) : string;
function transfer(const to : address; const tokenId : uint) raises NotOwner is {
const from : address := tokens.ownerOf[tokenId];
assert(from = Evm.sender(), NotOwner(tokenId));
beforeTransfer(from, to, tokenId);
tokens.ownerOf[tokenId] := to;
afterTransfer(from, to, tokenId);
}
external function mint(const tokenId : uint) is {
tokens.ownerOf[tokenId] := Evm.sender();
}
external function send(const to : address; const tokenId : uint) raises NotOwner is
transfer(to, tokenId)
external view function tokenName(const tokenId : uint) : string is
nameOf(tokenId)
}
contract Gallery is {
storage is record {
frozen : map(uint, bool);
moves : uint;
}
error Frozen of uint;
use Tokens exposing {
function mint(tokenId : uint);
function send(to : address; tokenId : uint) raises NotOwner | Frozen;
view function tokenName(tokenId : uint) : string;
}
(* A frozen token doesn't move, and the gallery counts the moves of the others. *)
hook Tokens.beforeTransfer(const from : address; const to : address; const id : uint)
raises Frozen
is {
assert(not storage.frozen[id], Frozen(id));
storage.moves := storage.moves + 1;
}
hook Tokens.afterTransfer is skip;
hook Tokens.nameOf is label;
view function label(const tokenId : uint) : string is
if storage.frozen[tokenId] then "a frozen piece" else "a gallery piece"
external function freeze(const tokenId : uint) is {
storage.frozen[tokenId] := true;
}
}A call to send runs the module's transfer, which calls Gallery's beforeTransfer before it moves the token. The module never sees the contract, so its code is the same in every contract, and two contracts that use Tokens can implement its hooks differently. The standard library's tokens work this way: ERC20, ERC721, ERC1155, and ERC6909 call beforeTransfer and afterTransfer around every balance change, and their extensions are functions a contract calls from those hooks. See Transfer hooks.
- A module declares
[view | pure] hook name(params) [: T];: a function's header, with no body. The effect is the most the contract's implementation may do. Aview hookmay read andquery, apure hookonly computes from its parameters, and a plainhookmay also write,emit, anddefer. - The module's own code calls a hook by name, as it calls its own functions. Nothing else calls it: not the contract, and not another module. A function that calls a stateful hook is stateful, so a
viewfunction can call onlyviewandpurehooks, and the module checks on its own. - A hook's parameters and result are ABI values, whatever the module does with them, so any module with hooks can be delegated. A generic module can't declare hooks.
- Only the contract implements hooks. There are no default implementations, no overrides, and no
super, and an extension module can't attach to another module's hook: a contract that wants two things to happen inbeforeTransfercalls both in its body, in the order it writes them.
The contract implements a hook in one of three ways:
| Form | Meaning |
|---|---|
hook M.h(params) [: T] [raises E] is { ... } [with e] | a body, like a function's. The parameter names are the contract's; their types, and the result, are the module's |
hook M.h is f; | calls f, a function of the contract or of a module it uses, with none of the hook's parameters or all of them, and returns what f returns. It raises what f does |
hook M.h is skip; | explicitly unimplemented: calling it does nothing. Only for a hook without a result |
Which hooks a contract implements
A contract implements a hook exactly when code it runs can reach it, and then exactly once. That code starts at the contract's entrypoints, the module externals it exposes, deploy, migrations, guards, and hook bodies, and runs through the functions they call, in any module; an external that dispatches on its argument reaches what each of its cases does. A hook that only an external the contract doesn't expose reaches isn't required, and implementing it anyway is an error, as for a guard no exposed external names. Exposing that external makes the hook required.
lithe stubs file.lithe M prints the hooks M's externals reach after its use M exposing block, as is skip; for a hook without a result and as a stub for one with a result. Given interfaces, lithe stubs file.lithe M I J prints the hooks the externals of I and J reach.
What a hook may do
Anything an internal function of its declared effect may do. A stateful hook may defer a call. The call joins the queue of the transition that reached the hook, in the order the transition's code reached it, and runs after that transition's body commits, not at the end of the hook. Its continuation, the then block, runs then too, and sees the hook's bindings:
import "std/abi";
import "std/evm";
interface Registry is {
function moved(tokenId : uint) : uint;
}
module Tokens is {
storage tokens at "example.tokens" is record {
ownerOf : map(uint, address);
}
hook afterTransfer(const to : address; const tokenId : uint);
external function send(const to : address; const tokenId : uint) is {
tokens.ownerOf[tokenId] := to;
afterTransfer(to, tokenId);
}
}
contract Tracked is {
storage is record {
registry : Registry;
receipts : map(uint, uint);
}
use Tokens exposing {
function send(to : address; tokenId : uint);
}
hook Tokens.afterTransfer(const to : address; const id : uint) is {
defer storage.registry.moved(id) then (receipt : uint) {
storage.receipts[id] := receipt;
}
}
}send moves the token, and only after its body ends does it call the registry. A hook reached twice in one transition queues its calls twice, and a transition's own deferred calls and those of the hooks it reaches run in the order its code reached them. The reentrancy warning of an external counts what its hooks defer.
A hook that reaches itself again, through the module's code, is recursion, which Lithe doesn't have: the error shows the loop.
Errors
A module declares no errors for its hooks. What the contract's implementation raises joins what every external that reaches the hook raises, whatever the module's functions on the way declare. The interface that exposes those externals lists them, as send lists NotOwner | Frozen above, and the ABI and lithe errors show the same.
lithe errors lists each hook the contract implements with the errors it raises, and each external's hooks. lithe footprint and lithe abi-tree show the hooks each entrypoint reaches and where each runs: in the contract, called back from a facet, or skipped.
Where a hook runs
A hook is contract code. When the contract runs the module's code itself, a hook is a plain internal call, and is skip compiles to nothing. When the module is delegated, its facet is built from the module alone, so it calls each hook back into the contract, which costs a round trip; a hook the contract skips, or that its code doesn't reach, costs the facet one bit test.
Mistakes
A contract implements each hook its code reaches. The error names the way there and prints what to write:
module Tally is {
storage tally at "example.tally" is record {
count : uint;
}
hook counted(const count : uint);
function bump() is {
tally.count := tally.count + 1;
counted(tally.count);
}
external function add() is bump()
}
contract Counter is {
use Tally exposing {
function add();
}
}Tally.counted is reached through `add` (exposed) → Tally.bump; implement it:
hook Tally.counted(const count : uint) is { ... }
or declare it unimplemented: hook Tally.counted is skip;And only those:
module Tally is {
storage tally at "example.tally" is record {
count : uint;
}
hook counted(const count : uint);
external function add() is {
tally.count := tally.count + 1;
counted(tally.count);
}
external view function count() : uint is tally.count
}
contract Counter is {
use Tally exposing {
view function count() : uint;
}
hook Tally.counted is skip;
}no code the contract runs calls `Tally.counted`; remove this hookOnce, with the module's parameter types:
module Tally is {
storage tally at "example.tally" is record {
count : uint;
}
hook counted(const count : uint);
external function add() is {
tally.count := tally.count + 1;
counted(tally.count);
}
}
contract Counter is {
use Tally exposing {
function add();
}
hook Tally.counted is skip;
hook Tally.counted is skip;
}`hook Tally.counted` is written twicemodule Tally is {
storage tally at "example.tally" is record {
count : uint;
}
hook counted(const count : uint);
external function add() is {
tally.count := tally.count + 1;
counted(tally.count);
}
}
contract Counter is {
use Tally exposing {
function add();
}
hook Tally.counted(const count : address) is { skip }
}`hook Tally.counted` doesn't match `Tally`'s declaration: its `count` is `address`, and `Tally`'s `count` is `uint`
module: hook Tally.counted(const count : uint)
contract: hook Tally.counted(const count : address)A hook with a result can't be skipped:
module Tally is {
storage tally at "example.tally" is record {
count : uint;
}
pure hook step() : uint;
external function add() is {
tally.count := tally.count + step();
}
}
contract Counter is {
use Tally exposing {
function add();
}
hook Tally.step is skip;
}`Tally.step` returns a value, so it can't be skipped; implement it:
hook Tally.step : uint is { ... } with ...An implementation does no more than the module's declaration allows:
module Tally is {
storage tally at "example.tally" is record {
count : uint;
}
view hook limit() : uint;
external function add() is {
if tally.count < limit() then {
tally.count := tally.count + 1;
}
}
}
contract Counter is {
storage is record {
asked : uint;
}
use Tally exposing {
function add();
}
hook Tally.limit() : uint is {
storage.asked := storage.asked + 1;
} with 10
}declared effect is weaker than the body's: view hook `Tally.limit` writes storageimport "std/evm";
module Tally is {
storage tally at "example.tally" is record {
count : uint;
}
view hook limit() : uint;
external function add() is {
if tally.count < limit() then {
tally.count := tally.count + 1;
}
}
}
contract Counter is {
use Tally exposing {
function add();
}
hook Tally.limit() : uint is {
defer Evm.transfer(Evm.sender(), 1wei);
} with 10
}declared effect is weaker than the body's: view hook `Tally.limit` cannot deferis f passes the function none of the hook's parameters, or all of them:
module Tally is {
storage tally at "example.tally" is record {
count : uint;
}
hook counted(const count : uint);
external function add() is {
tally.count := tally.count + 1;
counted(tally.count);
}
}
contract Counter is {
storage is record {
last : uint;
}
use Tally exposing {
function add();
}
function note(const count : uint; const by : uint) is {
storage.last := count + by;
}
hook Tally.counted is note;
}`note` takes 2 parameters; a hook that names a function passes it none of its parameters, or all 1A hook takes and returns ABI values:
module Tally is {
storage tally at "example.tally" is record {
count : uint;
}
hook counted(const counts : map(uint, uint));
}
contract Counter is {
use Tally;
}hook `counted` takes `counts` as `map(uint, uint)`; a hook's parameters and result are ABI values, so any module with hooks can be `delegated`A hook that reaches itself is rejected with the loop:
module Tally is {
storage tally at "example.tally" is record {
count : uint;
}
hook counted(const count : uint);
function bump() is {
tally.count := tally.count + 1;
counted(tally.count);
}
external function add() is bump()
}
contract Counter is {
use Tally exposing {
function add();
}
hook Tally.counted(const count : uint) is {
if count < 10 then {
Tally.bump();
}
}
}hook `Tally.counted` reaches itself: Tally.counted → Tally.bump → Tally.countedThe interface that exposes an external lists what its hooks raise:
module Tally is {
storage tally at "example.tally" is record {
count : uint;
}
hook counted(const count : uint);
external function add() is {
tally.count := tally.count + 1;
counted(tally.count);
}
}
contract Counter is {
error TooMany;
use Tally exposing {
function add();
}
hook Tally.counted(const count : uint) raises TooMany is {
assert(count <= 10, TooMany);
}
}`add` raises `TooMany` in `Tally` with hook `Tally.counted`, and the inline interface lists no errors
interface: function add()
module: function add()
hook: Tally.counted raises TooManyOnly the module's own code calls a hook:
module Tally is {
storage tally at "example.tally" is record {
count : uint;
}
view hook limit() : uint;
external function add() is {
if tally.count < limit() then {
tally.count := tally.count + 1;
}
}
}
contract Counter is {
use Tally exposing {
function add();
}
hook Tally.limit() : uint is { skip } with 10
external view function limit() : uint is Tally.limit()
}`Tally.limit` is a hook of `Tally`: only `Tally`'s own code calls it, and the contract implements it with `hook Tally.limit`A delegated module's facet calls a hook back in a frame of its own. A hook that defers works there as it does inline: its calls join the queue of the transition that reached it, and its continuations run then, with the hook's bindings. But the frame's calldata is the facet's call to the hook, not the transaction's, so a hook the facet reaches can't read it; see hooks in delegated modules:
import "std/evm";
module Tally is {
storage tally at "example.tally" is record {
count : uint;
}
hook counted(const count : uint);
external function add() is {
tally.count := tally.count + 1;
counted(tally.count);
}
}
contract Counter is {
use Tally delegated exposing {
function add();
}
storage is record {
size : uint;
}
hook Tally.counted(const count : uint) is {
storage.size := Evm.calldata().length;
}
}hook `Tally.counted` is called back from `Tally`'s facet, and it can't be: it reads the call's calldata, and a hook called back sees the facet's call to it, or use `Tally` without `delegated`ERC-165
A contract announces interfaces through ERC-165 with supports, which lists named interfaces:
use ERC721 exposing IERC721, IERC721Metadata;
supports IERC721, IERC721Metadata;The compiler then generates supportsInterface(bytes4), true for ERC-165 itself, 0x01ffc9a7, and for each interface listed. An interface's id is the XOR of its methods' selectors, so the standard library's IERC721, for example, answers for 0x80ac58cd, which is Abi.interfaceId(IERC721). The contract must have each interface it lists: dispatch every one of its methods, with the same result, whether they come from use ... exposing or from its own externals. A method it lacks is an error that names it:
import "std/interfaces/erc721";
import "std/token/erc721";
contract Collection is {
use ERC721 exposing IERC721, IERC721Name;
supports IERC721, IERC721Metadata;
hook ERC721.beforeTransfer is skip;
hook ERC721.afterTransfer is skip;
}the contract supports `IERC721Metadata` but does not dispatch:
tokenURI(uint256)
expose them from a module with `use M exposing`, or write them as externalsOnly the contract says what it announces. A contract without supports has no supportsInterface, so interfaces that only group externals cost nothing and announce no ids. Inline interfaces have no name, and no id, so supports can't list them, and it lists each interface once. supports goes in the contract, or at the top level of a file with one contract; an imported file can't declare it.
A contract can write its own supportsInterface, to add an id no methods derive, such as ERC-4906's 0x49064906. Abi.supportsInterface(id) answers for ERC-165 and what supports lists, as the generated function would:
external pure function supportsInterface(const interfaceId : bytes4) : bool is
interfaceId = 0x49064906 or Abi.supportsInterface(interfaceId)A contract with its own supportsInterface and supports must call Abi.supportsInterface, or the list would announce nothing. Only the contract's own functions call it: a module's code is the same in every contract.
Dependencies between modules
A module that calls another module, or reads its storage, declares it with requires. A contract that uses the dependent module must also use every module it requires. A module can call only its own functions and those of the modules it requires, never a function of the contract that uses it, so its code is the same in every contract:
import "std/evm";
module Pausable is {
storage pausable at "example.pausable" is record {
paused : bool;
}
error Paused;
view function whenNotPaused()
raises Paused
is {
assert(not pausable.paused, Paused);
}
external function pause() is {
pausable.paused := true;
}
}
module Ledger is {
requires Pausable;
storage ledger at "example.ledger" is record {
balances : map(address, uint);
}
error Insufficient of uint;
external function transfer(const to : address; const amount : uint)
raises Paused | Insufficient
is {
Pausable.whenNotPaused();
const have : uint := ledger.balances[Evm.sender()];
assert(have >= amount, Insufficient(have));
ledger.balances[Evm.sender()] := have - amount;
ledger.balances[to] := ledger.balances[to] + amount;
}
(* Reading a required module's storage is allowed. *)
external view function frozen() : bool is
Pausable.pausable.paused
}
contract Token is {
use Pausable exposing {
function pause();
}
use Ledger exposing {
function transfer(to : address; amount : uint)
raises Paused | Insufficient;
view function frozen() : bool;
}
storage is record {
admin : address;
}
}module Ledger is {
storage ledger at "lithe.ledger" is record {
n : uint;
}
function set(const n : uint) is {
ledger.n := n;
}
}
module Guarded is {
requires Ledger;
external function set(const n : uint) is {
Ledger.set(n);
}
}
contract Token is {
use Guarded exposing {
function set(n : uint);
}
}module `Guarded` requires `Ledger`Qualified storage
Outside its module, a module's storage is always written with the module's name, so a reviewer can see whose state is being touched:
module Ledger is {
storage ledger at "lithe.ledger" is record {
n : uint;
}
function set(const n : uint) is {
ledger.n := n;
}
}
contract Token is {
use Ledger;
external function go(const n : uint) is {
ledger.n := n;
}
}storage `ledger` belongs to `Ledger`; write `Ledger.ledger`Reaching into a module's storage like this is allowed, but calling the module's functions is usually clearer, because they maintain its invariants. Because every module's storage sits at its own ERC-7201 root, modules compose without slot collisions. Two modules that claim the same storage id are rejected.
Extension modules
An extension adds behavior and its own storage on top of a base module, without changing the base:
module ERC20Votes is {
requires ERC20;
storage votes at "lithe.std.erc20.votes" is record {
delegates : map(address, address);
}
function delegate(const delegatee : address) is {
votes.delegates[Evm.sender()] := delegatee;
}
}The contract that uses ERC20 and ERC20Votes decides whether a transfer should also move votes, and says so in its ERC20.afterTransfer hook: hook ERC20.afterTransfer is ERC20Votes.transferVotingUnits;. An extension cannot silently change the base's behavior.
Modules in other files
Modules are usually written in their own files and brought in with import. See Imports. Modules can also take type parameters. See Generics.

