README.md
September 2, 2025 · View on GitHub
Foundry Diamond Easy Deploy
A minimal, production-friendly EIP-2535 Diamond implementation using Foundry, with a focus on frictionless facet deployment via selector auto-discovery. It provides:
- A lean Diamond core (
src/Diamond.sol) with proper storage and fallback dispatch - Standard facets: DiamondCut, DiamondLoupe, Ownership
- An example application facet:
AdminFacet - A safe, faithful
LibDiamondcut implementation (add/replace/remove) - A
CutSelectorutility that auto-generates facet cuts by discovering selectors at build time using Foundry FFI - A deployment script that deploys the diamond and batches facet cuts
- An example Foundry test wiring everything together
This repo aims to give you an out-of-the-box diamond you can extend by adding facets and letting the tools compose the correct cut.
Project layout
src/Diamond.sol— Diamond core: sets owner, wires initialdiamondCut, ERC-165 support map, and fallback that delegates to facetssrc/libraries/LibDiamond.sol— Diamond storage and cut logic (selectors, facets, add/replace/remove, init call, owner)src/libraries/LibAppStorage.sol— App-specific storage slot (here: simple authorization mapping)src/facets/DiamondCutFacet.sol— ExposesdiamondCut, owner-gatedDiamondLoupeFacet.sol— Implements loupe queries and ERC-165 via storage mapOwnershipFacet.sol— ERC‑173 ownership interfaceAdminFacet.sol— Example app facet with simple role management usingLibAppStorage
src/interfaces/— Interfaces for DiamondCut, DiamondLoupe, ERC‑165, ERC‑173script/deploy.s.sol— Example deployment script that deploys facets, deploys the diamond, and performs a batched cutscript/utils/SelectorFetcher.sol— Uses Forge FFI to runforge selectors listto collect function selectorsCutSelector.sol— BuildsIDiamondCut.FacetCut[](single or batch) and de‑duplicates selectors across facets
test/Diamond.t.sol— Foundry test showing the end-to-end flow using the same cut utilities
How it works (architecture)
Diamond storage and dispatch
LibDiamond owns the canonical storage layout at DIAMOND_STORAGE_POSITION:
selectorToFacetAndPosition[bytes4] => { facetAddress, functionSelectorPosition }facetFunctionSelectors[facetAddress] => { bytes4[] functionSelectors, facetAddressPosition }facetAddresses[]supportedInterfaces[bytes4] => bool(ERC‑165 map)contractOwner
The diamond contract’s fallback() looks up msg.sig in selectorToFacetAndPosition and delegatecalls into the facet. A zero address reverts with "Function does not exist".
Diamond cut (add/replace/remove)
LibDiamond.diamondCut iterates a batch of FacetCut items and for each selector applies:
- Add: verifies no previous owner of selector, appends to facet’s selector list, updates maps
- Replace: removes old owner’s mapping, appends to new facet; prevents replacing with same facet
- Remove: old facet must exist and not be the diamond itself (immutable), updates arrays/maps (swap-and-pop)
It then optionally calls _init with _calldata (delegatecall if _init != address(this), else call), reverting if the call fails. Only the owner (from LibDiamond.enforceIsContractOwner) can invoke diamondCut via DiamondCutFacet.
ERCs
- ERC‑165:
supportedInterfacesmap in storage, read by loupe facet - ERC‑2535 (Diamond):
DiamondCutFacet,DiamondLoupeFacet - ERC‑173 (Ownership):
OwnershipFacetimplementsowner()andtransferOwnership()
App storage
LibAppStorage provides an app-level storage slot, currently with:
mapping(address => bool) isAuthorizedSetter
AdminFacet exposes grantSetterRole, revokeSetterRole, isSetter and is owner-gated where appropriate.
Deployment and selector auto-discovery
Manually maintaining selectors is error-prone. This repo auto-discovers selectors using Foundry’s selector tooling via FFI.
Utilities
SelectorFetcher- Runs a shell command through
vm.ffi:forge selectors list <ContractName>and parses the resulting function selectors. - Requires running with
--ffi(both in tests and in scripts if you rely on this at runtime).
- Runs a shell command through
CutSelectorgenerateCutData(facetName, facetAddress): single-facet cutgenerateCutDataBatch(string[] facetNames, address[] facetAddresses): multi-facet cut with duplicate selector filtering. EmitsDuplicateSelectorfor collisions.
Deployment script (script/deploy.s.sol)
What it does:
- Deploys standard facets:
DiamondCutFacet,DiamondLoupeFacet,OwnershipFacet - Deploys
Diamond(owner, diamondCutFacetAddress) - Builds a batch cut for the loupe and ownership facets using
CutSelector.generateCutDataBatch - Executes
diamondCutto add those facets - Logs selectors (example)
Note: owner is resolved in this order: explicit CLI arg (run(address)), OWNER env var, then defaults to the broadcaster (msg.sender).
Testing
test/Diamond.t.sol demonstrates the end-to-end flow in a clean environment:
- Deploys the three standard facets and the diamond shell
- Deploys an example app facet (
AdminFacet) - Builds a batch cut over
[DiamondLoupeFacet, OwnershipFacet, AdminFacet] - Calls
diamondCutasowner - Asserts that
IERC173(diamond).owner() == owner
Because the test uses CutSelector/SelectorFetcher, it must run with FFI enabled.
Usage
Prerequisites:
- Foundry toolchain installed (see https://book.getfoundry.sh/)
- For selector auto-discovery: allow FFI when running tests/scripts
Common commands:
- Build:
forge build - Format:
forge fmt - Test (requires FFI):
forge test --ffi - Verbose test:
forge test --ffi -vvvv
Running the deploy script
You can simulate (dry-run) or broadcast. Adjust owner and facets as needed in script/deploy.s.sol.
- Provide owner via env: set
OWNER=0xYourOwnerAddress - Or via CLI function signature:
--sig "run(address)" 0xYourOwnerAddress - If neither provided, it defaults to the broadcaster (msg.sender)
Examples:
- Dry-run (no broadcast), OWNER from env:
OWNER=0xF39fd6e51aad88F6F4ce6aB8827279cffFb92266 forge script script/deploy.s.sol:FetchSelectors --ffi
- Dry-run with explicit CLI arg:
forge script script/deploy.s.sol:FetchSelectors --ffi --sig "run(address)" 0xF39f...2266
- Against Anvil with broadcast:
anvilOWNER=0xF39f...2266 forge script script/deploy.s.sol:FetchSelectors --ffi --rpc-url http://127.0.0.1:8545 --broadcast --private-key <ANVIL_KEY>
- Against a live RPC:
OWNER=0xYourProdOwner forge script script/deploy.s.sol:FetchSelectors --ffi --rpc-url $RPC_URL --broadcast --private-key $DEPLOYER_PK
If you don’t want to allow FFI in production runs, you can precompute selectors offline and hard-code them, or run a preparatory step that writes them to a file consumed by the script.
Extending the diamond (adding a facet)
- Add your new facet contract under
src/facets/ - Deploy that facet in your script/test
- Add its name and address to the arrays passed to
generateCutDataBatch([...], [...]) - Execute
diamondCut
Notes:
- If two facets expose the same selector,
CutSelectorwill filter duplicates and emitDuplicateSelector. You can choose to instead explicitly replace by using a dedicated Replace cut in your script withLibDiamondsemantics. - To remove selectors, build a cut item with
FacetCutAction.Removeand passfacetAddress = address(0)for those selectors. - To run an initializer during cut, set
_initto the initializer facet address and_calldatato the function call data.
Security considerations
- Only the contract owner can call
diamondCut(enforced viaLibDiamond.enforceIsContractOwner) and sensitive admin functions. LibDiamondprevents removing immutable functions (ones defined on the diamond itself).addFacetenforces that the facet address has code.- Be careful with FFI: tests/scripts that use FFI should be run in trusted environments only.
References
- EIP‑2535 Diamond Standard: https://eips.ethereum.org/EIPS/eip-2535
- Foundry Book: https://book.getfoundry.sh/
Troubleshooting
- Missing selectors during cut
- Ensure the facet names you pass to
generateCutData(Batch)match the contract names as recognized byforge selectors list. - Run with
--ffiif using selector auto-discovery.
- Ensure the facet names you pass to
- Reverts on
diamondCut- Check for duplicate selectors (loupe/ownership overlap) and confirm whether you intend Add vs Replace.
- Ensure the caller is the diamond owner.
supportsInterfacereturns false- The diamond constructor marks the standard interfaces. If you add new interfaces, set the flag in storage as part of your initializer.
License
MIT