ILatticeRegistry
Title: ILatticeRegistry
Author: David Dada <daveproxy80@gmail.com> (https://github.com/dadadave80)
Read / admin surface of the LatticeRegistry singleton — the two-tier, immutable, deploy-once
address book that lets every Lattice diamond reuse canonical facet bytecode instead of
re-CREATEing byte-identical facets on every deployment.
TRUST MODEL (issue #118). The registry is deliberately a standalone, NON-upgradeable plain contract (no diamond, no ERC-7201, no proxy) — the smallest possible trust surface for the thing every deployment depends on. Two tiers:
- Tier A (permissionless, trustless): attest / resolve. A content-addressed index from a runtime
codehash to the FIRST address attested with it.
attestis self-verifying (it readsdeployed.codehashon-chain — impossible to spoof) and first-write-wins. No curation, no admin.resolveanswers ONE question: "where is a contract with exactly this code?". It is a code-identity lookup, NOT a selector source and not a statement about behaviour. Two addresses with one codehash behave the same only when the diamonddelegatecalls them AND the facet meets every assumption below; Lattice facets do, arbitrary contracts need not:
- no reads of the facet's OWN storage on any path (a direct call or
staticcallreads the facet's storage, which can differ between two instances of the same code); - a
pureERC-8153exportSelectors()(aviewexporter can answer differently per instance, and astaticcallcannot enforcepure); - no proxy used as a facet (a proxy's codehash says nothing about the code it forwards to);
- selectors come from Tier B (getSelectors / getCut), never from
resolve.registermirrors a curated facet into Tier A only when its codehash is unmapped, so a curated record can name a different address thanresolvereturns for the same codehash.
- Tier B (curated, append-only): register / setLatest + views.
Ownable2Step(→ multisig) governs ONLY this namespace. A(nameHash, version)record is IMMUTABLE once written (invariant I1) — never repointed;latest(nameHash)is a curator-movable convenience pointer that security-critical consumers ignore in favour of pinning an exact version (invariant I3). Curated facets MUST implement ERC-8153exportSelectors(); register pulls and validates the selectors and pinskeccak256(blob), and the live-read views re-verify against that pin so an impure or lyingexportSelectors()can never drift the cut a consumer receives (invariant I2 for selectors).
EXPORTER CALLS ARE BOUNDED. Every exportSelectors() read (at register and on each live read)
forwards at most 100,000 gas and accepts at most 8,192 bytes of return data; a call that runs out of
gas, reverts, or returns more is treated as a non-conformant export. Return data is decoded by hand
with exactly the acceptance rule of abi.decode(ret, (bytes)) (any in-bounds offset, trailing bytes
ignored), so a malformed encoding is reported as LatticeRegistry__NotERC8153 at registration and as
LatticeRegistry__SelectorDrift on a live read, never as an empty revert or a Panic.
version is a semver value packed into a uint64 as major (16 bits) | minor (24) | patch (24)
((major << 48) | (minor << 24) | patch), so lexicographic uint64 ordering equals semver
ordering. The registry treats it as an OPAQUE, ordered key; it never decodes the fields. Packed
version 0 (i.e. 0.0.0) is RESERVED as the "latest unset" sentinel and is not registrable.
Functions
attest
Permissionlessly record that deployed carries a given runtime codehash (first-write-wins).
Self-verifying: reads deployed.codehash on-chain; reverts LatticeRegistry__EmptyCode for an
EOA / empty account. Re-attesting an already-mapped codehash is a documented silent no-op — the
mapping is immutable once set, so the first attester wins and no event re-fires.
function attest(address deployed) external;Parameters
| Name | Type | Description |
|---|---|---|
| deployed | address | The address whose runtime code should be indexed. |
resolve
Resolve a runtime codehash to the first address attested with it (0 if none).
A code-identity lookup only: it never says which selectors an address exports, and two addresses with one codehash are interchangeable only under the Tier A assumptions in the interface NatSpec.
function resolve(bytes32 codehash) external view returns (address deployed);Parameters
| Name | Type | Description |
|---|---|---|
| codehash | bytes32 | The runtime codehash to look up. |
Returns
| Name | Type | Description |
|---|---|---|
| deployed | address | The first attested address, or address(0) if unmapped. |
register
Register a new curated facet under (nameHash, version) (owner-only, append-only).
Reverts LatticeRegistry__RecordExists if the record already exists (I1),
LatticeRegistry__InvalidVersion for version 0, LatticeRegistry__EmptyCode for a codeless
facet, and LatticeRegistry__NotERC8153 if IERC8153(facet).exportSelectors() fails the ERC-8153
contract. Pins keccak256(selectors), mirrors the codehash into Tier A, and emits Registered.
function register(bytes32 nameHash, uint64 version, address facet) external;Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 | The curated name key. |
| version | uint64 | The semver-packed version (must be non-zero). |
| facet | address | The canonical facet address (must have code and implement ERC-8153). |
setLatest
Point latest(nameHash) at an already-registered version (owner-only).
The target record must exist (LatticeRegistry__RecordNotFound otherwise). Moving the pointer to an OLDER version is allowed — it is only a convenience pointer, never the pinned record (I3).
function setLatest(bytes32 nameHash, uint64 version) external;Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 | The curated name key. |
| version | uint64 | The registered version to flag as latest. |
get
Read the exact record for (nameHash, version).
Reverts LatticeRegistry__RecordNotFound if unregistered.
function get(bytes32 nameHash, uint64 version) external view returns (Record memory record);Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 | |
| version | uint64 |
Returns
| Name | Type | Description |
|---|---|---|
| record | Record |
latest
Read the record currently flagged as latest for a name.
Reverts LatticeRegistry__LatestUnset if setLatest was never called for nameHash.
function latest(bytes32 nameHash) external view returns (Record memory record);Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 |
Returns
| Name | Type | Description |
|---|---|---|
| record | Record |
getMany
Batch get: one record per key, in order. A key with version == 0 reads the record
latest(nameHash) points at, the way a factory recipe entry with version 0 resolves.
Reverts on the first key that get (or latest, for version 0) would revert on. Records are
read from storage only; use getSelectors / getCut for live-verified selectors.
function getMany(RecordKey[] calldata keys) external view returns (Record[] memory records);Parameters
| Name | Type | Description |
|---|---|---|
| keys | RecordKey[] | The (nameHash, version) lookups. |
Returns
| Name | Type | Description |
|---|---|---|
| records | Record[] | The records, records[i] for keys[i]. |
latestMany
Batch latest: the record each name's latest pointer flags, in order.
Reverts LatticeRegistry__LatestUnset on the first name whose pointer was never set.
function latestMany(bytes32[] calldata nameHashes) external view returns (Record[] memory records);Parameters
| Name | Type | Description |
|---|---|---|
| nameHashes | bytes32[] | The curated name keys. |
Returns
| Name | Type | Description |
|---|---|---|
| records | Record[] | The records, records[i] for nameHashes[i]. |
getSelectors
Live-read and drift-verify the facet's selectors for (nameHash, version).
Reverts LatticeRegistry__RecordNotFound if unregistered, LatticeRegistry__CodeDrift if
facet.codehash no longer equals the pinned codehash, and LatticeRegistry__SelectorDrift if
keccak256(exportSelectors()) no longer equals the pinned selectorsHash. A facet that reverts,
runs out of its gas budget, returns oversized or malformed data, or lost its code is caught by the
code/selector pins with one of those two errors, so the call never returns a stale cut. Then
unpacks the blob to bytes4[].
function getSelectors(bytes32 nameHash, uint64 version) external view returns (bytes4[] memory selectors);Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 | |
| version | uint64 |
Returns
| Name | Type | Description |
|---|---|---|
| selectors | bytes4[] |
getCut
Live-read, drift-verify, and assemble a ready-to-apply Add FacetCut for (nameHash, version).
Same code + selector drift verification as getSelectors; the returned cut uses FacetCutAction.Add.
function getCut(bytes32 nameHash, uint64 version) external view returns (FacetCut memory cut);Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 | |
| version | uint64 |
Returns
| Name | Type | Description |
|---|---|---|
| cut | FacetCut |
nameHash
Compute the registry key for a canonical facet name: keccak256(bytes(name)).
For callers interacting DIRECTLY with the contract (Etherscan, etc.) so they need not pre-hash a name
off-chain. The registry hashes the RAW string and applies NO prefix, so the string overloads below
and the bytes32 overloads are interchangeable when passed the same full canonical name. By
CONVENTION Lattice facets are registered under "lattice.<FacetName>" (e.g. "lattice.ERC20").
function nameHash(string calldata name) external pure returns (bytes32);Parameters
| Name | Type | Description |
|---|---|---|
| name | string | The full canonical facet name. |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | bytes32 | The bytes32 nameHash used by every Tier-B function. |
register
register by canonical name string instead of its precomputed hash (owner-only).
function register(string calldata name, uint64 version, address facet) external;Parameters
| Name | Type | Description |
|---|---|---|
| name | string | |
| version | uint64 | |
| facet | address |
setLatest
setLatest by canonical name string (owner-only).
function setLatest(string calldata name, uint64 version) external;Parameters
| Name | Type | Description |
|---|---|---|
| name | string | |
| version | uint64 |
get
get the exact record for a canonical name string.
function get(string calldata name, uint64 version) external view returns (Record memory record);Parameters
| Name | Type | Description |
|---|---|---|
| name | string | |
| version | uint64 |
Returns
| Name | Type | Description |
|---|---|---|
| record | Record |
latest
latest record for a canonical name string.
function latest(string calldata name) external view returns (Record memory record);Parameters
| Name | Type | Description |
|---|---|---|
| name | string |
Returns
| Name | Type | Description |
|---|---|---|
| record | Record |
getSelectors
getSelectors for a canonical name string.
function getSelectors(string calldata name, uint64 version) external view returns (bytes4[] memory selectors);Parameters
| Name | Type | Description |
|---|---|---|
| name | string | |
| version | uint64 |
Returns
| Name | Type | Description |
|---|---|---|
| selectors | bytes4[] |
getCut
getCut for a canonical name string.
function getCut(string calldata name, uint64 version) external view returns (FacetCut memory cut);Parameters
| Name | Type | Description |
|---|---|---|
| name | string | |
| version | uint64 |
Returns
| Name | Type | Description |
|---|---|---|
| cut | FacetCut |
owner
The current owner of the curated Tier-B namespace.
function owner() external view returns (address);Returns
| Name | Type | Description |
|---|---|---|
| <none> | address |
pendingOwner
The address that may call acceptOwnership to complete a pending handover (0 if none).
function pendingOwner() external view returns (address);Returns
| Name | Type | Description |
|---|---|---|
| <none> | address |
transferOwnership
Begin a two-step ownership handover to newOwner (owner-only; pass address(0) to cancel).
function transferOwnership(address newOwner) external;Parameters
| Name | Type | Description |
|---|---|---|
| newOwner | address |
acceptOwnership
Complete a pending ownership handover (pending-owner-only).
Reverts LatticeRegistry__NotPendingOwner when no handover is pending (pendingOwner == 0, which
is also the state a cancelled handover leaves), so the owner can never become address(0).
function acceptOwnership() external;Events
Attested
Emitted the first time a runtime codehash is attested into the Tier-A resolver.
event Attested(bytes32 indexed codehash, address indexed deployed);Parameters
| Name | Type | Description |
|---|---|---|
| codehash | bytes32 | The runtime codehash that now resolves to deployed. |
| deployed | address | The first address seen carrying codehash (first-write-wins). |
Registered
Emitted when a new curated (nameHash, version) record is written.
event Registered(
bytes32 indexed nameHash, uint64 indexed version, address indexed facet, bytes32 codehash, bytes32 selectorsHash
);Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 | The curated name key. |
| version | uint64 | The semver-packed version key. |
| facet | address | The canonical facet address. |
| codehash | bytes32 | The facet's runtime codehash (also auto-attested into Tier A). |
| selectorsHash | bytes32 | The pinned keccak256 of the facet's exported selector blob. |
LatestSet
Emitted when the latest convenience pointer for a name is set or moved (rollbacks allowed).
event LatestSet(bytes32 indexed nameHash, uint64 indexed version);Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 | The curated name key. |
| version | uint64 | The version now flagged as latest for nameHash. |
OwnershipTransferStarted
Emitted when a two-step ownership handover is initiated by the current owner.
event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);Parameters
| Name | Type | Description |
|---|---|---|
| previousOwner | address | |
| newOwner | address |
OwnershipTransferred
Emitted when ownership is transferred (construction, and on acceptOwnership).
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);Parameters
| Name | Type | Description |
|---|---|---|
| previousOwner | address | |
| newOwner | address |
Errors
LatticeRegistry__ZeroAddress
Thrown when a constructor / handover would set a zero owner.
error LatticeRegistry__ZeroAddress();LatticeRegistry__Unauthorized
Thrown when a non-owner calls an onlyOwner function.
error LatticeRegistry__Unauthorized(address caller);Parameters
| Name | Type | Description |
|---|---|---|
| caller | address |
LatticeRegistry__NotPendingOwner
Thrown when an account other than the pending owner calls acceptOwnership.
error LatticeRegistry__NotPendingOwner(address caller);Parameters
| Name | Type | Description |
|---|---|---|
| caller | address |
LatticeRegistry__EmptyCode
Thrown when attest / register targets an address with no runtime code (EOA / empty — I4).
error LatticeRegistry__EmptyCode(address target);Parameters
| Name | Type | Description |
|---|---|---|
| target | address |
LatticeRegistry__InvalidVersion
Thrown when register uses the reserved sentinel version 0 (0.0.0).
error LatticeRegistry__InvalidVersion();LatticeRegistry__RecordExists
Thrown when register would overwrite an existing (nameHash, version) record (I1).
error LatticeRegistry__RecordExists(bytes32 nameHash, uint64 version);Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 | |
| version | uint64 |
LatticeRegistry__RecordNotFound
Thrown when a view / setLatest references a (nameHash, version) that was never registered.
error LatticeRegistry__RecordNotFound(bytes32 nameHash, uint64 version);Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 | |
| version | uint64 |
LatticeRegistry__LatestUnset
Thrown when latest is read for a name whose pointer was never set.
error LatticeRegistry__LatestUnset(bytes32 nameHash);Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 |
LatticeRegistry__NotERC8153
Thrown when a curated facet does not satisfy the ERC-8153 selector contract at register
(call reverts or runs out of its 100,000-gas budget / returns fewer than 64 or more than 8,192
bytes / an encoding abi.decode(ret, (bytes)) would reject / empty selectors / length not a
multiple of 4 / duplicates / a blob that self-includes exportSelectors() (0x0ef22643), which
must never be cut).
error LatticeRegistry__NotERC8153(address facet);Parameters
| Name | Type | Description |
|---|---|---|
| facet | address |
LatticeRegistry__SelectorDrift
Thrown by the live-read views when keccak256(exportSelectors()) no longer equals the pinned
selectorsHash — an impure or mutated facet (invariant I2 for selectors). A live read that fails
the bounds LatticeRegistry__NotERC8153 lists (revert, gas, size, malformed encoding) is also
reported as drift.
error LatticeRegistry__SelectorDrift(address facet);Parameters
| Name | Type | Description |
|---|---|---|
| facet | address |
LatticeRegistry__CodeDrift
Thrown by the live-read views when the facet's current codehash no longer equals the codehash
pinned at registration — defends against a metamorphic (CREATE2 + selfdestruct + redeploy)
address swap that keeps exportSelectors() byte-identical (invariant I2 for code).
error LatticeRegistry__CodeDrift(address facet);Parameters
| Name | Type | Description |
|---|---|---|
| facet | address |
Structs
Record
A curated Tier-B facet record, immutable once written (invariant I1).
struct Record {
address facet;
uint64 version;
uint48 registeredAt;
bytes32 codehash;
bytes32 selectorsHash;
}Properties
| Name | Type | Description |
|---|---|---|
| facet | address | The canonical facet address whose selectors are pinned by this record. |
| version | uint64 | Semver packed uint64 (major<<48 | minor<<24 | patch); opaque ordered key. |
| registeredAt | uint48 | Block timestamp of registration (uint48 — good past the year 8·10^6). |
| codehash | bytes32 | facet.codehash captured at registration (also mirrored into Tier A). |
| selectorsHash | bytes32 | keccak256 of the tightly packed selector blob returned byIERC8153(facet).exportSelectors() at registration — the drift pin the live-read views check. |
RecordKey
One lookup for getMany: a curated name and the version to read.
struct RecordKey {
bytes32 nameHash;
uint64 version;
}Properties
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 | The curated name key. |
| version | uint64 | The semver-packed version, or 0 to read the record latest(nameHash) points at. |