0001. Modules are an interface, a library and a stateless facet
- Status: Accepted
- Date: in force since the first module, AccessControl (#1, 2026-04-25); recorded 2026-10-09
Context
An EIP-2535 diamond delegatecalls its facets, so a facet runs against the diamond's storage, never its
own. Lattice ships modules that integrators cut together in one diamond, and several pieces of code need
the same module logic: the module's own facet, combined facets that do two things on one path (for
example GovernedVault), and the init contracts that seed state
during initialize.
Options considered
- Interface, library, facet. The ABI lives in the interface, all logic and storage access in an internal library, and the facet only forwards.
- Logic in the facet. Another facet or an init could reach the logic only through an external call or by inheriting the whole facet.
- One shared AppStorage struct. Every module would add fields to one struct, so independently written modules would share and reorder one layout.
Decision
Every module with state or an ABI has three files:
src/interfaces/<area>/I<Module>.sol: functions, custom errors and events.src/<area>/libraries/<Module>Lib.sol: all logic asinternalfunctions, the module's ERC-7201 storage (0002),registerInterface()(0003) and__<Module>_init.src/<area>/<Module>.sol: a stateless facet ofvirtualone-line forwards to the library, plus the ERC-8153exportSelectors()list of its cuttable selectors.
Pure helpers with no storage, facet or interface live in src/utils/libraries/ and skip the split. A few
contracts are standalone where the standard or deployment model needs it (LatticeRegistry,
LatticeFactory, AccountFactory, the *Standalone variants).
Consequences
- Internal library calls compile into the calling facet, so a combined facet or an init reuses a module's logic with no external call and no shared deployment.
- Facets hold no state, so one deployed facet serves every diamond, which is what the release (0004) and the registry (0007) rely on.
- A module is three files plus its registration in the release inventory, the slot test, the storage probe and the module catalog, which the add-a-module checklist lists.
- Libraries are not
virtual, so behaviour cannot be changed by overriding a hook. Extensions that must see every token movement replace selectors instead (0009).
Confirmation
ExportSelectorsParityTestchecks that every facet inFacetInventoryexports exactly itsforge inspectmethod identifiers, withoutexportSelectors()itself.make storage-checkguards every namespaced storage struct. No check proves a facet stateless; review does.make readme-checkrequires everysrc/contract in the README module catalog.
References
- README: three-layer facet pattern
- AGENTS.md: Solidity architecture and storage
- #85 (domain-mirrored interface folders)