LatticeRegistry
Inherits: ILatticeRegistry
Title: LatticeRegistry
Author: David Dada <daveproxy80@gmail.com> (https://github.com/dadadave80)
Two-tier, immutable, deploy-once on-chain registry of canonical Lattice facets (issue #118). Tier A
is a permissionless, self-verifying codehash → first-attested-address index (code identity only,
never a selector source; see ILatticeRegistry); Tier B is a curated, append-only
(name, version) catalog governed by two-step ownership. Because Lattice facets are stateless (all
state lives in the caller diamond's ERC-7201 slots), one deployed facet safely serves unlimited
diamonds via delegatecall, so recording addresses here replaces re-CREATEing byte-identical
bytecode on every deployment.
DELIBERATELY NOT A DIAMOND. This is a minimal, standalone, NON-upgradeable plain contract: plain storage (no ERC-7201 — there is no upgrade path by design), no facets, no proxy. It is the single thing every deployment depends on, so it carries the smallest possible trust surface and is meant to be deployed once per chain (via CreateX at a fixed salt → same address everywhere). Only the curated namespace is admin-governed; Tier A and all reads are trustless. Original Lattice work.
Note:
- lattice-version: 0.1.0
Constants
EMPTY_CODE_HASH
keccak256("") — the runtime codehash of an existing-but-codeless account (e.g. a funded EOA).
Together with 0 (a non-existent account) these are the two "no code" states _requireCode
rejects, so only real contract bytecode is ever attested / registered (invariant I4).
bytes32 private constant EMPTY_CODE_HASH = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;EXPORT_GAS
Gas forwarded to every exportSelectors() call (finding R-2). A compiled pure exporter returns a
constant blob: every release facet's export runs in at most 2,233 gas (AccessManager; measured over
the whole release inventory), so this leaves over 40x headroom while bounding what a looping or
gas-burning exporter can take from the caller (a register transaction, a getCut read, or every
factory deploy resolving it).
uint256 private constant EXPORT_GAS = 100_000;MAX_EXPORT_SIZE
Largest exportSelectors() return the registry copies (finding R-2): 8,192 bytes, room for 2,032
selectors in the canonical encoding (64-byte head + data). The largest release export is 224 bytes
(Governor, 36 selectors), and EIP-170 caps a facet at 24,576 bytes of code with every routed
selector costing a dispatcher several bytes, so no real facet comes close. The cap stops a
return-data bomb from taxing every live read: a larger return is never copied at all.
uint256 private constant MAX_EXPORT_SIZE = 8192;EXPORT_SELECTOR
The ERC-8153 exportSelectors() self-selector (0x0ef22643). A conformant facet MUST exclude it
from its own export (it is never cut into a diamond); register rejects any blob that self-includes
it, matching BaseDeploy's cut-path strip and the parity gate.
bytes4 private constant EXPORT_SELECTOR = IERC8153.exportSelectors.selector;State Variables
owner
Owner of the curated Tier-B namespace (two-step; see transferOwnership / acceptOwnership).
address public owner;pendingOwner
Address permitted to complete a pending ownership handover (address(0) when none pending).
address public pendingOwner;_resolver
Tier A: runtime codehash → first attested address (first-write-wins, immutable once set).
mapping(bytes32 codehash => address deployed) private _resolver;_records
Tier B: keccak256(abi.encode(nameHash, version)) → immutable curated record.
mapping(bytes32 recordKey => Record record) private _records;_latestVersion
Tier B: name → the version flagged as latest. 0 (0.0.0) is the reserved "unset" sentinel.
mapping(bytes32 nameHash => uint64 version) private _latestVersion;Functions
onlyOwner
Restricts a call to the current curated-namespace owner.
modifier onlyOwner();constructor
constructor(address initialOwner);Parameters
| Name | Type | Description |
|---|---|---|
| initialOwner | address | The first curated-namespace owner (typically a multisig); must be non-zero. |
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 onlyOwner;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 onlyOwner;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 |
register
register by canonical name string instead of its precomputed hash (owner-only).
function register(string calldata name, uint64 version, address facet) external onlyOwner;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 onlyOwner;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 |
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. |
transferOwnership
Begin a two-step ownership handover to newOwner (owner-only; pass address(0) to cancel).
function transferOwnership(address newOwner) external onlyOwner;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;_register
Shared body of both register overloads: append-only write of a curated (nameHash, version)
record after pulling + validating + pinning the facet's ERC-8153 selectors and codehash.
function _register(bytes32 nameHash, uint64 version, address facet) private;Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 | |
| version | uint64 | |
| facet | address |
_setLatest
Shared body of both setLatest overloads: point latest(nameHash) at an existing version.
function _setLatest(bytes32 nameHash, uint64 version) private;Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 | |
| version | uint64 |
_hashName
The registry key for a canonical facet name — the RAW keccak256(bytes(name)), no prefix. The
"lattice.<Name>" convention lives in tooling/docs, not here, so the registry stays name-agnostic.
function _hashName(string calldata name) private pure returns (bytes32);Parameters
| Name | Type | Description |
|---|---|---|
| name | string |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | bytes32 |
_get
Shared body of both get overloads.
function _get(bytes32 nameHash, uint64 version) private view returns (Record memory record);Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 | |
| version | uint64 |
Returns
| Name | Type | Description |
|---|---|---|
| record | Record |
_latest
Shared body of both latest overloads.
function _latest(bytes32 nameHash) private view returns (Record memory record);Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 |
Returns
| Name | Type | Description |
|---|---|---|
| record | Record |
_buildCut
Shared body of both getCut overloads: live-verify then assemble an Add cut.
function _buildCut(bytes32 nameHash, uint64 version) private view returns (FacetCut memory cut);Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 | |
| version | uint64 |
Returns
| Name | Type | Description |
|---|---|---|
| cut | FacetCut |
_requireCode
Require target to carry real contract code and return its codehash (invariant I4).
function _requireCode(address target) private view returns (bytes32 codehash);Parameters
| Name | Type | Description |
|---|---|---|
| target | address |
Returns
| Name | Type | Description |
|---|---|---|
| codehash | bytes32 |
_attest
First-write-wins insert into the Tier-A resolver; a duplicate codehash is a silent no-op so that register auto-attest and any prior permissionless attest compose without reverting.
function _attest(bytes32 codehash, address deployed) private;Parameters
| Name | Type | Description |
|---|---|---|
| codehash | bytes32 | |
| deployed | address |
_fetchSelectors
Read IERC8153(facet).exportSelectors() through _exportedBlob and enforce the ERC-8153 contract
at registration: the blob must be non-empty, 4-aligned, duplicate-free, and must not self-include
exportSelectors(). Reverts LatticeRegistry__NotERC8153 on any violation, including every call or
decoding failure _exportedBlob reports as an empty blob. Returns the packed blob (to be pinned).
function _fetchSelectors(address facet) private view returns (bytes memory blob);Parameters
| Name | Type | Description |
|---|---|---|
| facet | address |
Returns
| Name | Type | Description |
|---|---|---|
| blob | bytes |
_verifiedSelectors
Load a record and re-verify BOTH pins against the facet's current on-chain state: its codehash
(defends against a metamorphic address swap) and the live keccak256(exportSelectors()). Reverts
LatticeRegistry__RecordNotFound if unregistered, LatticeRegistry__CodeDrift if the code changed,
and LatticeRegistry__SelectorDrift if the live selector blob no longer matches the pin.
function _verifiedSelectors(bytes32 nameHash, uint64 version)
private
view
returns (address facet, bytes4[] memory selectors);Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 | |
| version | uint64 |
Returns
| Name | Type | Description |
|---|---|---|
| facet | address | |
| selectors | bytes4[] |
_exportedBlob
Bounded read of exportSelectors() (findings R-1 and R-2). Staticcalls facet with at most
EXPORT_GAS gas, copies nothing until the return size is known, and refuses a return shorter than an
ABI bytes encoding (64 bytes) or longer than MAX_EXPORT_SIZE. The encoding is then decoded by hand
with exactly the acceptance rule of abi.decode(ret, (bytes)): the offset word must leave room for the
length word, and the declared length must fit in the bytes that follow it (no padding is required,
trailing bytes are ignored). Returns the decoded blob, or EMPTY bytes on any failure, so no hostile
return can make the caller revert with empty data or a Panic.
function _exportedBlob(address facet) private view returns (bytes memory blob);Parameters
| Name | Type | Description |
|---|---|---|
| facet | address |
Returns
| Name | Type | Description |
|---|---|---|
| blob | bytes |
_unpack
Unpack a tightly packed selector blob (length % 4 == 0, validated by the caller) to bytes4[].
function _unpack(bytes memory blob) private pure returns (bytes4[] memory selectors);Parameters
| Name | Type | Description |
|---|---|---|
| blob | bytes |
Returns
| Name | Type | Description |
|---|---|---|
| selectors | bytes4[] |
_key
Deterministic record key for a (nameHash, version) pair.
function _key(bytes32 nameHash, uint64 version) private pure returns (bytes32 recordKey);Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 | |
| version | uint64 |
Returns
| Name | Type | Description |
|---|---|---|
| recordKey | bytes32 |