Skip to content
Lattice

LatticeFactory

Git Source

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

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

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

NameTypeDescription
_registryILatticeRegistryThe deploy-once ILatticeRegistry recipe entries are resolved against; must hold code.
reverseRegistraraddressThe chain's ENS reverse registrar, or zero with reverseRecordOwner to disable ENS.
reverseRecordOwneraddressThe 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

NameTypeDescription
entriesRecipeEntry[]The registry recipe lines to resolve into cuts (may be empty).
customCutsFacetCut[]Classic cuts appended verbatim after the registry cuts (may be empty).
initaddressInitializer delegatecalled during the diamond's initialization (address(0) to skip, with
empty initCalldata).
initCalldatabytesCalldata for init.
saltbytes32Caller-chosen salt; distinct salts yield distinct diamonds for the same sender.

Returns

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

NameTypeDescription
entriesRecipeEntry[]The registry recipe lines to resolve into cuts (may be empty; versions must be non-zero).
customCutsFacetCut[]Classic cuts appended verbatim after the registry cuts (may be empty).
initaddressInitializer delegatecalled during the diamond's initialization (address(0) to skip).
initCalldatabytesCalldata for init.
saltbytes32Caller-chosen salt; distinct salts yield distinct diamonds for the same sender.

Returns

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

NameTypeDescription
deployeraddressThe prospective msg.sender.
saltbytes32The prospective salt.

Returns

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

NameTypeDescription
entriesRecipeEntry[]
customCutsFacetCut[]
initaddress
initCalldatabytes
saltbytes32
strictbool

Returns

NameTypeDescription
diamondaddress

_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

NameTypeDescription
entriesRecipeEntry[]
customCutsFacetCut[]
initaddress
initCalldatabytes
strictbool

_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

NameTypeDescription
entriesRecipeEntry[]
customCutsFacetCut[]

Returns

NameTypeDescription
cutsFacetCut[]

_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

NameTypeDescription
selectorsbytes4[]
excludebytes4[]
nameHashbytes32

_checkLoupeCoverage

Two passes over the materialized cuts, making the loupe-routing guarantee SOUND for the assembly transaction:

  1. PRESENCE — reverts ILatticeFactory.LatticeFactory__MissingLoupeCoverage with the FIRST loupe selector no Add cut routes (fixed order: facets(), facetFunctionSelectors(address), facetAddresses(), facetAddress(bytes4)).
  2. NO-UNDO — reverts ILatticeFactory.LatticeFactory__LoupeSelectorNotReplaceable if any Replace/Remove cut 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 (see IFacet's security note on self-reported selectors); registry cuts at least pin codehash + export blob.
function _checkLoupeCoverage(FacetCut[] memory cuts) private pure;

Parameters

NameTypeDescription
cutsFacetCut[]

_saltFor

Binds the diamond address to its deployer: keccak256(deployer, salt).

function _saltFor(address deployer, bytes32 salt) private pure returns (bytes32);

Parameters

NameTypeDescription
deployeraddress
saltbytes32

Returns

NameTypeDescription
<none>bytes32

_predict

Standard CREATE2 address derivation for the Diamond proxy.

function _predict(bytes32 s) private view returns (address);

Parameters

NameTypeDescription
sbytes32

Returns

NameTypeDescription
<none>address