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 LibDiamond cut implementation (add/replace/remove)
  • A CutSelector utility 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 initial diamondCut, ERC-165 support map, and fallback that delegates to facets
  • src/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 — Exposes diamondCut, owner-gated
    • DiamondLoupeFacet.sol — Implements loupe queries and ERC-165 via storage map
    • OwnershipFacet.sol — ERC‑173 ownership interface
    • AdminFacet.sol — Example app facet with simple role management using LibAppStorage
  • src/interfaces/ — Interfaces for DiamondCut, DiamondLoupe, ERC‑165, ERC‑173
  • script/deploy.s.sol — Example deployment script that deploys facets, deploys the diamond, and performs a batched cut
  • script/utils/
    • SelectorFetcher.sol — Uses Forge FFI to run forge selectors list to collect function selectors
    • CutSelector.sol — Builds IDiamondCut.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: supportedInterfaces map in storage, read by loupe facet
  • ERC‑2535 (Diamond): DiamondCutFacet, DiamondLoupeFacet
  • ERC‑173 (Ownership): OwnershipFacet implements owner() and transferOwnership()

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).
  • CutSelector
    • generateCutData(facetName, facetAddress): single-facet cut
    • generateCutDataBatch(string[] facetNames, address[] facetAddresses): multi-facet cut with duplicate selector filtering. Emits DuplicateSelector for collisions.

Deployment script (script/deploy.s.sol)

What it does:

  1. Deploys standard facets: DiamondCutFacet, DiamondLoupeFacet, OwnershipFacet
  2. Deploys Diamond(owner, diamondCutFacetAddress)
  3. Builds a batch cut for the loupe and ownership facets using CutSelector.generateCutDataBatch
  4. Executes diamondCut to add those facets
  5. 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 diamondCut as owner
  • Asserts that IERC173(diamond).owner() == owner

Because the test uses CutSelector/SelectorFetcher, it must run with FFI enabled.


Usage

Prerequisites:

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:
    • anvil
    • OWNER=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)

  1. Add your new facet contract under src/facets/
  2. Deploy that facet in your script/test
  3. Add its name and address to the arrays passed to generateCutDataBatch([...], [...])
  4. Execute diamondCut

Notes:

  • If two facets expose the same selector, CutSelector will filter duplicates and emit DuplicateSelector. You can choose to instead explicitly replace by using a dedicated Replace cut in your script with LibDiamond semantics.
  • To remove selectors, build a cut item with FacetCutAction.Remove and pass facetAddress = address(0) for those selectors.
  • To run an initializer during cut, set _init to the initializer facet address and _calldata to the function call data.

Security considerations

  • Only the contract owner can call diamondCut (enforced via LibDiamond.enforceIsContractOwner) and sensitive admin functions.
  • LibDiamond prevents removing immutable functions (ones defined on the diamond itself).
  • addFacet enforces that the facet address has code.
  • Be careful with FFI: tests/scripts that use FFI should be run in trusted environments only.

References


Troubleshooting

  • Missing selectors during cut
    • Ensure the facet names you pass to generateCutData(Batch) match the contract names as recognized by forge selectors list.
    • Run with --ffi if using selector auto-discovery.
  • 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.
  • supportsInterface returns 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