Skip to content
Lattice

LatticeRegistry

Git Source

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

NameTypeDescription
initialOwneraddressThe 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

NameTypeDescription
deployedaddressThe 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

NameTypeDescription
codehashbytes32The runtime codehash to look up.

Returns

NameTypeDescription
deployedaddressThe 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

NameTypeDescription
nameHashbytes32The curated name key.
versionuint64The semver-packed version (must be non-zero).
facetaddressThe 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

NameTypeDescription
nameHashbytes32The curated name key.
versionuint64The 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

NameTypeDescription
nameHashbytes32
versionuint64

Returns

NameTypeDescription
recordRecord

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

NameTypeDescription
nameHashbytes32

Returns

NameTypeDescription
recordRecord

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

NameTypeDescription
keysRecordKey[]The (nameHash, version) lookups.

Returns

NameTypeDescription
recordsRecord[]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

NameTypeDescription
nameHashesbytes32[]The curated name keys.

Returns

NameTypeDescription
recordsRecord[]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

NameTypeDescription
nameHashbytes32
versionuint64

Returns

NameTypeDescription
selectorsbytes4[]

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

NameTypeDescription
nameHashbytes32
versionuint64

Returns

NameTypeDescription
cutFacetCut

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

NameTypeDescription
namestring
versionuint64
facetaddress

setLatest

setLatest by canonical name string (owner-only).

function setLatest(string calldata name, uint64 version) external onlyOwner;

Parameters

NameTypeDescription
namestring
versionuint64

get

get the exact record for a canonical name string.

function get(string calldata name, uint64 version) external view returns (Record memory record);

Parameters

NameTypeDescription
namestring
versionuint64

Returns

NameTypeDescription
recordRecord

latest

latest record for a canonical name string.

function latest(string calldata name) external view returns (Record memory record);

Parameters

NameTypeDescription
namestring

Returns

NameTypeDescription
recordRecord

getSelectors

getSelectors for a canonical name string.

function getSelectors(string calldata name, uint64 version) external view returns (bytes4[] memory selectors);

Parameters

NameTypeDescription
namestring
versionuint64

Returns

NameTypeDescription
selectorsbytes4[]

getCut

getCut for a canonical name string.

function getCut(string calldata name, uint64 version) external view returns (FacetCut memory cut);

Parameters

NameTypeDescription
namestring
versionuint64

Returns

NameTypeDescription
cutFacetCut

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.&lt;FacetName>" (e.g. "lattice.ERC20").

function nameHash(string calldata name) external pure returns (bytes32);

Parameters

NameTypeDescription
namestringThe full canonical facet name.

Returns

NameTypeDescription
<none>bytes32The 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

NameTypeDescription
newOwneraddress

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

NameTypeDescription
nameHashbytes32
versionuint64
facetaddress

_setLatest

Shared body of both setLatest overloads: point latest(nameHash) at an existing version.

function _setLatest(bytes32 nameHash, uint64 version) private;

Parameters

NameTypeDescription
nameHashbytes32
versionuint64

_hashName

The registry key for a canonical facet name — the RAW keccak256(bytes(name)), no prefix. The "lattice.&lt;Name>" convention lives in tooling/docs, not here, so the registry stays name-agnostic.

function _hashName(string calldata name) private pure returns (bytes32);

Parameters

NameTypeDescription
namestring

Returns

NameTypeDescription
<none>bytes32

_get

Shared body of both get overloads.

function _get(bytes32 nameHash, uint64 version) private view returns (Record memory record);

Parameters

NameTypeDescription
nameHashbytes32
versionuint64

Returns

NameTypeDescription
recordRecord

_latest

Shared body of both latest overloads.

function _latest(bytes32 nameHash) private view returns (Record memory record);

Parameters

NameTypeDescription
nameHashbytes32

Returns

NameTypeDescription
recordRecord

_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

NameTypeDescription
nameHashbytes32
versionuint64

Returns

NameTypeDescription
cutFacetCut

_requireCode

Require target to carry real contract code and return its codehash (invariant I4).

function _requireCode(address target) private view returns (bytes32 codehash);

Parameters

NameTypeDescription
targetaddress

Returns

NameTypeDescription
codehashbytes32

_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

NameTypeDescription
codehashbytes32
deployedaddress

_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

NameTypeDescription
facetaddress

Returns

NameTypeDescription
blobbytes

_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

NameTypeDescription
nameHashbytes32
versionuint64

Returns

NameTypeDescription
facetaddress
selectorsbytes4[]

_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

NameTypeDescription
facetaddress

Returns

NameTypeDescription
blobbytes

_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

NameTypeDescription
blobbytes

Returns

NameTypeDescription
selectorsbytes4[]

_key

Deterministic record key for a (nameHash, version) pair.

function _key(bytes32 nameHash, uint64 version) private pure returns (bytes32 recordKey);

Parameters

NameTypeDescription
nameHashbytes32
versionuint64

Returns

NameTypeDescription
recordKeybytes32