This is a mimimal template for Diamonds which allows facet selectors to be generated on the go in solidity tests!
- Clone this repo
- Install dependencies
$ yarn && forge update$ npx hardhat compile$ npx hardhat run scripts/deploy.js$ forge tNote: A lot of improvements are still needed so contributions are welcome!!
Bonus: The DiamondLoupefacet uses an updated LibDiamond which utilises solidity custom errors to make debugging easier especially when upgrading diamonds. Take it for a spin!!
Need some more clarity? message me on twitter, Or join the EIP-2535 Diamonds Discord server
This repository includes a Foundry-based helper and script that make it easy to perform EIP-2535 diamond upgrades in tests and scripts without hand-assembling selector arrays.
test/helpers/DiamondUtils.soldynamically generates function selectors usingforge inspect <Facet> methods --jsonand parses them inside Solidity via FFI.test/helpers/DiamondUpgradeHelper.solbuilds one-shot Add/Replace/Remove cuts and executes diamond upgrades viaIDiamondCut.script/DiamondUpgrade.s.solis a generic Foundry script to upgrade an existing diamond using environment variables.
Note: FFI must be enabled in
foundry.toml(ffi=true) for selector generation.
- Ensure remappings include
forge-stdandsolidity-stringutils(already configured in this repo):- See
remappings.txtandfoundry.toml.
- See
- Ensure
ffi=trueinfoundry.toml.
Located at test/helpers/DiamondUpgradeHelper.sol (import and inherit in your test/script).
buildAddCutByName(address facetAddress, string facetName)- Generates all selectors of
facetNameand returns one Add cut.
- Generates all selectors of
buildAddCutsByNames(address[] facetAddresses, string[] facetNames)- Batch of Add cuts; arrays must match in length and order.
buildReplaceCutByName(IDiamondLoupe loupe, address facetAddress, string facetName)- Computes selectors for
facetNameand returns a Replace cut for selectors that currently exist on the diamond and point to a different facet address.
- Computes selectors for
buildReplaceCutsByNames(IDiamondLoupe loupe, address[] facetAddresses, string[] facetNames)- Batch replacement; see semantics above.
buildAddMissingCutByName(IDiamondLoupe loupe, address facetAddress, string facetName)- Add-only for selectors that do not already exist on the diamond.
buildExtendCutsByName(IDiamondLoupe loupe, address facetAddress, string facetName)- Returns a 1–2 element array combining Replace (existing selectors) and Add (new selectors) to extend a facet implementation with new functions.
buildRemoveCut(bytes4[] selectors)- Returns a Remove cut for the given selectors.
executeDiamondCut(IDiamondCut diamond, IDiamondCut.FacetCut[] cuts, address init, bytes initCalldata)- Executes a diamond cut with optional init call.
- Add new facets to a fresh or partially configured diamond:
address[] memory addAddrs = new address[](2);
addAddrs[0] = address(loupeFacet);
addAddrs[1] = address(ownershipFacet);
string[] memory names = new string[](2);
names[0] = "DiamondLoupeFacet";
names[1] = "OwnershipFacet";
IDiamondCut.FacetCut[] memory cuts = buildAddCutsByNames(addAddrs, names);
executeDiamondCut(IDiamondCut(address(diamond)), cuts, address(0), "");- Extend an existing facet (replace 3 existing selectors and add 1 new selector):
// newFacet implements the same 3 old functions and 1 new
IDiamondCut.FacetCut[] memory cuts = buildExtendCutsByName(
IDiamondLoupe(address(diamond)),
address(newFacet),
"YourFacetName"
);
executeDiamondCut(IDiamondCut(address(diamond)), cuts, address(0), "");- Replace an existing facet implementation (only for selectors that already exist on the diamond):
IDiamondCut.FacetCut[] memory cuts = new IDiamondCut.FacetCut[](1);
cuts[0] = buildReplaceCutByName(IDiamondLoupe(address(diamond)), address(newFacet), "YourFacetName");
executeDiamondCut(IDiamondCut(address(diamond)), cuts, address(0), "");- Remove selectors:
bytes4[] memory toRemove = new bytes4[](2);
toRemove[0] = YourFacet.oldFunction.selector;
toRemove[1] = bytes4(keccak256("someSig(uint256,bool)"));
IDiamondCut.FacetCut[] memory cuts = new IDiamondCut.FacetCut[](1);
cuts[0] = buildRemoveCut(toRemove);
executeDiamondCut(IDiamondCut(address(diamond)), cuts, address(0), "");Use script/DiamondUpgradeExample.s.sol with hardcoded configuration inside run().
- Open
script/DiamondUpgrade.s.soland set:diamondto your target diamond addressaddFacetAddressesandaddFacetNamesfor new facets to addreplaceFacetAddressesandreplaceFacetNamesfor facets whose existing selectors should be migratedremoveSelectorsfor selectors to remove (optional)initandinitCalldataif you need an initialization call (optional)
Example (inside run()):
address diamond = 0x000000000000000000000000000000000000dEaD;
address[] memory addFacetAddresses = new address[](2);
addFacetAddresses[0] = 0x1111111111111111111111111111111111111111;
addFacetAddresses[1] = 0x2222222222222222222222222222222222222222;
string[] memory addFacetNames = new string[](2);
addFacetNames[0] = "DiamondLoupeFacet";
addFacetNames[1] = "OwnershipFacet";
// Optional replace & remove
address[] memory replaceFacetAddresses = new address[](0);
string[] memory replaceFacetNames = new string[](0);
bytes4[] memory removeSelectors = new bytes4[](0);
address init = address(0);
bytes memory initCalldata = hex"";Run the script with broadcast:
forge script script/DiamondUpgradeExample.s.sol:DiamondUpgradeExample \
--rpc-url $RPC_URL \
--private-key $PK \
--broadcastNo environment variables or CLI parameters are required; all configuration is set within the script file.
- The helper relies on the diamond implementing
IDiamondLoupefor replace/add-missing/extend logic to work correctly. buildReplaceCutByNamefilters out selectors that are either missing on the diamond or already mapped to the provided facet address, avoidingSameSelectorReplacementreverts.buildAddMissingCutByNameensures only new selectors (not present on the diamond) are added.buildExtendCutsByNamecombines both behaviors to migrate existing selectors to a new facet and add new selectors in one call.- For fresh deployments, prefer add-only; for migrations, prefer extend or replace.
- If you need per-selector control, you can manually filter the arrays returned by
generateSelectors(facetName)in your own helper or use the signature hash directly.
- Empty selector arrays cause
NoSelectorsInFacet()reverts. Ensure your cuts contain at least one selector. - Ensure facet names match the contract names compiled in your repo.
- Ensure FFI is enabled and
forgeis available on PATH;forge inspectis invoked from Solidity viavm.ffi. - On very large scripts, if you hit "stack too deep", refactor into smaller functions (the provided script already does this).