LatticeFactory
Inherits: ILatticeFactory
Title: LatticeFactory
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 from cuts
resolved out of the deploy-once ILatticeRegistry (issue #120). Recipe entries become
live-verified Add cuts straight off the registry — no facet re-CREATE, no FFI — classic
custom cuts are appended for facets outside the curated catalog, and the proxy is
CREATE2-deployed and initialized atomically.
The proxy has no constructor args, so its initcode hash (diamondInitCodeHash) is constant and every
diamond address depends only on (factory, keccak256(msg.sender, salt)). Because the initcode commits
to nothing, a raw salt alone would let anyone front-run a counterfactual address with arbitrary cuts;
folding the sender into the salt binds each address to its deployer, which also makes the idempotent
already-deployed return of deploy sound (only the same sender can have occupied it, with cuts of
their own choosing). The flip side: the address never commits to the RECIPE — a repeat deploy for an
occupied (sender, salt) returns the existing diamond unchanged and ignores the call's
entries/cuts/init, while deployStrict reverts. DiamondDeployed carries the applied recipe's hash.
Registry reverts (drift, missing records, unset latest) bubble unchanged — the factory adds no drift
handling of its own. Not a Diamond facet — a standalone, stateless singleton.
Note:
- lattice-version: 0.1.0
Constants
registry
The deploy-once ILatticeRegistry recipe entries are resolved against.
ILatticeRegistry public immutable registry;Returns
| Name | Type | Description |
|---|---|---|
| <none> | address |
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:].
bytes32 public immutable diamondInitCodeHash;Returns
| Name | Type | Description |
|---|---|---|
| <none> | bytes32 |
EXPORT_SELECTOR
The ERC-8153 exportSelectors() self-selector (0x0ef22643). It is never cut into a diamond:
ILatticeRegistry registration rejects it, and both deploy entry points refuse any custom cut
carrying it.
bytes4 private constant EXPORT_SELECTOR = IERC8153.exportSelectors.selector;Functions
constructor
constructor(ILatticeRegistry _registry, address reverseRegistrar, address reverseRecordOwner);Parameters
| Name | Type | Description |
|---|---|---|
| _registry | ILatticeRegistry | The deploy-once ILatticeRegistry recipe entries are resolved against; must hold code. |
| reverseRegistrar | address | The chain's ENS reverse registrar, or zero with reverseRecordOwner to disable ENS. |
| reverseRecordOwner | address | The account that will manage this factory's reverse record, or zero to disable ENS. |
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. |
_deploy
Shared body of deploy (strict == false) and deployStrict (strict == true).
function _deploy(
RecipeEntry[] calldata entries,
FacetCut[] calldata customCuts,
address init,
bytes calldata initCalldata,
bytes32 salt,
bool strict
) private returns (address diamond);Parameters
| Name | Type | Description |
|---|---|---|
| entries | RecipeEntry[] | |
| customCuts | FacetCut[] | |
| init | address | |
| initCalldata | bytes | |
| salt | bytes32 | |
| strict | bool |
Returns
| Name | Type | Description |
|---|---|---|
| diamond | address |
_validate
Argument validation. It runs on EVERY call — including ones that take deploy's idempotent
return — so a recipe that would be refused fresh is never quietly "accepted" against an occupied
address. Refuses — never strips — any custom cut carrying exportSelectors() (registry-resolved
cuts can never contain it: registration rejects it), a zero init with calldata (DiamondLib skips a
zero init without looking at the calldata, which would drop it silently), and in strict mode any
entry that would resolve the movable latest pointer.
function _validate(
RecipeEntry[] calldata entries,
FacetCut[] calldata customCuts,
address init,
bytes calldata initCalldata,
bool strict
) private pure;Parameters
| Name | Type | Description |
|---|---|---|
| entries | RecipeEntry[] | |
| customCuts | FacetCut[] | |
| init | address | |
| initCalldata | bytes | |
| strict | bool |
_materialize
The cuts a fresh deploy applies. Registry-resolved recipe cuts first: getCut live-re-verifies the
code + selector pins and returns an Add cut; any registry revert (drift, missing record, unset
latest) bubbles unchanged. Exclusions are checked against that verified export, so a typo never
silently leaves a selector in. 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 (the
deployer's own diamond, by design).
function _materialize(RecipeEntry[] calldata entries, FacetCut[] calldata customCuts)
private
view
returns (FacetCut[] memory cuts);Parameters
| Name | Type | Description |
|---|---|---|
| entries | RecipeEntry[] | |
| customCuts | FacetCut[] |
Returns
| Name | Type | Description |
|---|---|---|
| cuts | FacetCut[] |
_exclude
Drops every exclude selector from selectors IN PLACE, keeping the export order of the rest, and
shortens the array. Reverts ILatticeFactory.LatticeFactory__ExcludedSelectorNotExported for a
selector the export does not contain, which also catches a selector excluded twice. Excluding every
selector leaves an empty Add cut, which DiamondLib refuses (NoSelectorsGivenToAdd).
function _exclude(bytes4[] memory selectors, bytes4[] calldata exclude, bytes32 nameHash) private pure;Parameters
| Name | Type | Description |
|---|---|---|
| selectors | bytes4[] | |
| exclude | bytes4[] | |
| nameHash | bytes32 |
_checkLoupeCoverage
Two passes over the materialized cuts, making the loupe-routing guarantee SOUND for the assembly transaction:
- PRESENCE — reverts
ILatticeFactory.LatticeFactory__MissingLoupeCoveragewith the FIRST loupe selector noAddcut routes (fixed order:facets(),facetFunctionSelectors(address),facetAddresses(),facetAddress(bytes4)). - NO-UNDO — reverts
ILatticeFactory.LatticeFactory__LoupeSelectorNotReplaceableif anyReplace/Removecut in the SAME deploy touches a loupe selector: without this, a recipe like[Add(loupe), Remove(loupe)]would pass the presence scan yet assemble a loupe-less (or mis-routed) diamond. Fresh-deploy recipes may only ADD loupe routing; re-pointing it is a post-deploy upgrade concern, never an assembly one. What this does NOT guarantee: that the routed code correctly IMPLEMENTS the loupe — custom-cut facets are the recipe author's responsibility (seeIFacet's security note on self-reported selectors); registry cuts at least pin codehash + export blob.
function _checkLoupeCoverage(FacetCut[] memory cuts) private pure;Parameters
| Name | Type | Description |
|---|---|---|
| cuts | FacetCut[] |
_saltFor
Binds the diamond address to its deployer: keccak256(deployer, salt).
function _saltFor(address deployer, bytes32 salt) private pure returns (bytes32);Parameters
| Name | Type | Description |
|---|---|---|
| deployer | address | |
| salt | bytes32 |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | bytes32 |
_predict
Standard CREATE2 address derivation for the Diamond proxy.
function _predict(bytes32 s) private view returns (address);Parameters
| Name | Type | Description |
|---|---|---|
| s | bytes32 |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | address |