ILatticeFactory
Title: ILatticeFactory
Authors: David Dada <daveproxy80@gmail.com> (https://github.com/dadadave80), Modified from ENS ReverseRegistrar (https://github.com/ensdomains/ens-contracts)
Stateless factory that assembles a complete EIP-2535 Diamond in ONE transaction: recipe entries
are resolved into live-verified Add cuts by the deploy-once ILatticeRegistry (no facet
re-CREATE, no FFI), classic custom cuts are appended for facets outside the curated catalog,
and the resulting proxy is CREATE2-deployed and initialized atomically.
The deployer (msg.sender) is folded into the CREATE2 salt, so a given counterfactual address can
only ever be realized by the sender it was derived from. Two entry points share one pipeline:
- deployStrict reverts LatticeFactory__AlreadyDeployed when the address is occupied and refuses
latestentries (version0). It is the entry point for user interfaces and any caller that must know its recipe was applied. - deploy is idempotent: re-running it for an already-deployed
(sender, salt)returns the existing diamond instead of reverting, and ignores the call's entries/cuts/init entirely. The address commits to(sender, salt)ONLY, never to the recipe, so a distinct recipe needs a distinct salt; DiamondDeployed carries a hash of the applied recipe so indexers can tell recipes apart. Because the salt is bound tomsg.sender, every user behind one shared forwarder (Multicall3, a relayer) shares one salt namespace: call the factory directly from the deploying account, or use deployStrict so a second user's call reverts instead of returning the first user's diamond. All registry drift/lookup failures (ILatticeRegistry reverts) bubble unchanged — the factory adds no drift handling of its own.
Functions
deploy
Deploys (or returns the existing) Diamond for (msg.sender, salt) and initializes it with
the registry-resolved recipe cuts, the appended custom cuts, and the recipe's init.
Recipe entries resolve through ILatticeRegistry.getCut, which live-re-verifies the code and
selector pins and returns an Add cut; a version of 0 first resolves the curator's
latest(nameHash) pointer, and each entry's exclude selectors are then dropped from its cut.
Custom cuts are appended AFTER the registry cuts, order preserved — a deliberate custom Replace
may therefore re-point a selector a registry cut just added.
Argument validation (LatticeFactory__EmptyRecipe, LatticeFactory__InitCalldataWithoutInit,
LatticeFactory__ExportSelectorForbidden) runs on every call, including idempotent re-calls;
registry resolution, exclusion, LOUPE-COVERAGE validation (LatticeFactory__MissingLoupeCoverage —
every fresh deploy must route the four EIP-2535 loupe selectors; the registered
lattice.DiamondLoupeFacet entry is the one-line way) and initialization run only when the diamond
is actually deployed.
INIT AUTHORITY: Lattice.initialize is called by THIS FACTORY, so inside init msg.sender is the
factory, not the deployer. An init that grants msg.sender a role or ownership grants it to the
factory, which has no way to use it: pass the admin explicitly in initCalldata (every shipped Init
does).
function deploy(
RecipeEntry[] calldata entries,
FacetCut[] calldata customCuts,
address init,
bytes calldata initCalldata,
bytes32 salt
) external returns (address diamond);Parameters
| Name | Type | Description |
|---|---|---|
| entries | RecipeEntry[] | The registry recipe lines to resolve into cuts (may be empty). |
| customCuts | FacetCut[] | Classic cuts appended verbatim after the registry cuts (may be empty). |
| init | address | Initializer delegatecalled during the diamond's initialization (address(0) to skip, withempty initCalldata). |
| initCalldata | bytes | Calldata for init. |
| salt | bytes32 | Caller-chosen salt; distinct salts yield distinct diamonds for the same sender. |
Returns
| Name | Type | Description |
|---|---|---|
| diamond | address | The deterministic diamond address (equals predict). |
deployStrict
deploy, but it reverts LatticeFactory__AlreadyDeployed instead of returning an existing
diamond, and LatticeFactory__UnpinnedEntry for any entry with version 0.
Every successful call deploys a fresh diamond from exactly this recipe and emits DiamondDeployed, so a double-submitted or replayed transaction fails loudly instead of "succeeding" with another recipe. Same validation, resolution, exclusion and init-authority rules as deploy.
function deployStrict(
RecipeEntry[] calldata entries,
FacetCut[] calldata customCuts,
address init,
bytes calldata initCalldata,
bytes32 salt
) external returns (address diamond);Parameters
| Name | Type | Description |
|---|---|---|
| entries | RecipeEntry[] | The registry recipe lines to resolve into cuts (may be empty; versions must be non-zero). |
| customCuts | FacetCut[] | Classic cuts appended verbatim after the registry cuts (may be empty). |
| init | address | Initializer delegatecalled during the diamond's initialization (address(0) to skip). |
| initCalldata | bytes | Calldata for init. |
| salt | bytes32 | Caller-chosen salt; distinct salts yield distinct diamonds for the same sender. |
Returns
| Name | Type | Description |
|---|---|---|
| diamond | address | The newly deployed diamond (equals predict). |
predict
The counterfactual address deploy(..., salt) will land at when called by deployer.
function predict(address deployer, bytes32 salt) external view returns (address diamond);Parameters
| Name | Type | Description |
|---|---|---|
| deployer | address | The prospective msg.sender. |
| salt | bytes32 | The prospective salt. |
Returns
| Name | Type | Description |
|---|---|---|
| diamond | address | The deterministic CREATE2 address. |
registry
The deploy-once ILatticeRegistry recipe entries are resolved against.
function registry() external view returns (ILatticeRegistry);Returns
| Name | Type | Description |
|---|---|---|
| <none> | ILatticeRegistry |
diamondInitCodeHash
The CREATE2 initcode hash of every diamond this factory deploys:
keccak256(type(Lattice).creationCode) of the Lattice build compiled into the factory.
Lets an off-chain client check which proxy build a factory deploys before it predicts addresses:
address = keccak256(0xff ++ factory ++ keccak256(abi.encode(deployer, salt)) ++ hash)[12:].
function diamondInitCodeHash() external view returns (bytes32);Returns
| Name | Type | Description |
|---|---|---|
| <none> | bytes32 |
Events
DiamondDeployed
Emitted when a diamond is deployed (not re-emitted on idempotent re-calls).
recipeHash is keccak256(abi.encode(cuts, init, initCalldata)) over the MATERIALIZED
FacetCut[] cuts handed to Lattice.initialize: the registry-resolved cuts in entry order (with
each entry's exclude applied), then the custom cuts. It records what was applied, including the
version a latest entry resolved to; it is not part of the address. An init that self-destructs
the diamond leaves the address empty when the transaction ends (EIP-6780), so the same
(sender, salt) can deploy again and emit a second event for the same address: indexers treat the
LATEST event for an address as authoritative.
event DiamondDeployed(
address indexed diamond, address indexed deployer, bytes32 indexed recipeHash, bytes32 salt, address init
);Parameters
| Name | Type | Description |
|---|---|---|
| diamond | address | The deployed Diamond proxy. |
| deployer | address | The msg.sender folded into the CREATE2 salt. |
| recipeHash | bytes32 | The hash of the applied cuts, init and init calldata. |
| salt | bytes32 | The caller-chosen salt distinguishing diamonds for the same deployer. |
| init | address | The initializer delegatecalled during initialization (address(0) if none). |
Errors
LatticeFactory__EmptyRecipe
Thrown when deploy is called with no recipe entries AND no custom cuts (would yield a
diamond with no callable functions).
error LatticeFactory__EmptyRecipe();LatticeFactory__ExportSelectorForbidden
Thrown when a custom cut includes the ERC-8153 exportSelectors() selector (0x0ef22643) —
it must never be cut into a diamond. Refused outright, never silently stripped;
registry-resolved cuts can never contain it because registration rejects it.
error LatticeFactory__ExportSelectorForbidden();LatticeFactory__MissingLoupeCoverage
Thrown when a FRESH deploy's materialized cuts leave an EIP-2535 loupe selector uncovered —
facets() 0x7a0ed627, facetFunctionSelectors(address) 0xadfca15e, facetAddresses()
0x52ef6b2c, facetAddress(bytes4) 0xcdffacc6 must EACH appear in some Add cut.
An un-introspectable diamond is unusable by EIP-2535 tooling (explorers, upgrade dashboards,
the loupe-driven test harnesses), so the factory requires a routing ENTRY for each loupe
selector at assembly time — paired with LatticeFactory__LoupeSelectorNotReplaceable, which
forbids undoing that routing within the same deploy. The check is COVERAGE-based, never
facet-identity-based: any facet may provide the selectors (the stock DiamondLoupeFacet, a
registry-resolved release of it, or a combined facet of the deployer's own). It validates the
selector SURFACE, not the implementation behind it: registry cuts pin codehash + self-reported
export blob, custom cuts are unverified — a facet claiming selectors it does not implement is
the recipe author's responsibility (see IFacet's security note). The CUT facet remains
optional — immutable-by-design diamonds are legal; blind ones are not. Reported selector = the
FIRST missing one in the order listed above. Like registry resolution, this check is skipped
on idempotent re-calls to an occupied (sender, salt) (the address never commits to the
recipe; see deploy).
error LatticeFactory__MissingLoupeCoverage(bytes4 missingSelector);Parameters
| Name | Type | Description |
|---|---|---|
| missingSelector | bytes4 | The first uncovered loupe selector. |
LatticeFactory__LoupeSelectorNotReplaceable
Thrown when a FRESH deploy's cuts contain a Replace or Remove that touches an EIP-2535
loupe selector — e.g. [Add(loupe), Remove(loupe)], which would pass a presence-only scan
yet assemble a loupe-less or mis-routed diamond.
Fresh-deploy recipes may only ADD loupe routing; re-pointing or removing it is a post-deploy upgrade decision made through the diamond's own cut facet, never smuggled into assembly.
error LatticeFactory__LoupeSelectorNotReplaceable(bytes4 loupeSelector);Parameters
| Name | Type | Description |
|---|---|---|
| loupeSelector | bytes4 | The loupe selector the offending non-Add cut touches. |
LatticeFactory__InvalidRegistry
Thrown when constructing a factory with a registry address that holds no code (including zero): a permanent, silent misconfiguration, since the factory is immutable and every recipe entry would revert with empty data.
error LatticeFactory__InvalidRegistry(address registry);Parameters
| Name | Type | Description |
|---|---|---|
| registry | address | The rejected registry address. |
LatticeFactory__AlreadyDeployed
Thrown by deployStrict when the (sender, salt) address already holds a diamond.
error LatticeFactory__AlreadyDeployed(address diamond);Parameters
| Name | Type | Description |
|---|---|---|
| diamond | address | The occupied address. |
LatticeFactory__UnpinnedEntry
Thrown by deployStrict for a recipe entry with version 0, which would resolve the curator's
movable latest pointer at execution time instead of a version the caller signed for.
error LatticeFactory__UnpinnedEntry(bytes32 nameHash);Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 | The entry's curated name key. |
LatticeFactory__ExcludedSelectorNotExported
Thrown when a recipe entry excludes a selector its pinned registry export does not contain (or excludes one selector twice).
error LatticeFactory__ExcludedSelectorNotExported(bytes32 nameHash, bytes4 selector);Parameters
| Name | Type | Description |
|---|---|---|
| nameHash | bytes32 | The entry's curated name key. |
| selector | bytes4 | The excluded selector that was not found. |
LatticeFactory__InitCalldataWithoutInit
Thrown when init is zero but initCalldata is not empty: the diamond would deploy without
running the initialization the caller encoded.
error LatticeFactory__InitCalldataWithoutInit();LatticeFactory__IncompleteENSConfiguration
Thrown when exactly one of the reverse registrar and reverse-record owner is zero.
error LatticeFactory__IncompleteENSConfiguration();LatticeFactory__InvalidReverseRegistrar
Thrown when ENS naming is enabled but the supplied reverse registrar has no bytecode.
error LatticeFactory__InvalidReverseRegistrar(address registrar);Parameters
| Name | Type | Description |
|---|---|---|
| registrar | address | The invalid reverse registrar address. |