Design for Composition
Here are the guidelines and rules for creating composable facets.
Compose replaces source-code inheritance with on-chain composition. Facets are the building blocks; diamonds wire them together.
We focus on building small, independent, and easy-to-read facets. Each facet is deployed once, then reused and combined with other facets to form complete, modular smart contract systems.
Writing Facets
- A facet is a set of external functions that represent a single, self-contained unit of functionality.
- Each facet is a self-contained, conceptual unit.
- A facet is designed for all of its functions to be added, not just some of them.
- The facet is our smallest building block.
- The source code of a facet should only contain the code (including storage variables, if possible) that it actually uses.
- Facets are fully self-contained. They do not import anything.
Writing Facet Modules
- Facet modules are self-contained code units. They do not import anything.
- Each facet should have one corresponding facet module.
- Facet modules are used to initialize facets on deployment and during upgrades.
- Facet modules are also used to integrate custom facets with Compose facets.
- Facet modules have one or more functions which are used to initialize storage variables during deployment.
Facets & Modules
- Facets and facet modules should not contain owner/admin authorization checks unless absolutely required or fundamental to the functionality being implemented. If permission or authorization checks are required, the facet should integrate with
OwnerFacetorAccessControlFacet.
Extending Facets
- Every extension of a standard or facet should be implemented as a new facet.
- A facet should only be extended with a new facet that composes with it.
- Composition is done by facets sharing the same storage structs and containing complementary external functions.
- Two facets do not compose if they both have one or more of the same external function signatures (function name and parameter types).
- When reusing a struct from an existing facet, store it at the original diamond storage location and remove unused variables from the end of it. Variables must never be removed from the beginning or middle, as this would break storage compatibility.
- Storage structs should be designed so that removable variables (unused by some facets) appear at the end of the struct.
- If an unused variable cannot be removed from the end of a struct, it must remain to preserve compatibility.
- A facet that adds new storage variables must define its own diamond storage struct.
- Never add new variables to an existing struct.
Maintain the same order of variables in structs when reusing them across facets or modules. Unused variables may only be removed from the end of a struct.
Exceptions
There may be reasonable exceptions to these rules. If you believe one applies, please discuss it on
– Discord: https://discord.gg/DCBD2UKbxc
– GitHub Issues: https://github.com/Perfect-Abstractions/Compose/issues
– GitHub Discussions: https://github.com/Perfect-Abstractions/Compose/discussions
For example, ERC721EnumerableFacet does not extend ERC721Facet because enumeration requires re-implementing transfer and mint/burn logic, making it incompatible with ERC721Facet.
Example: Implementing Storage in ERC20PermitFacet
The ERC20PermitFacet extends ERC20Facet by adding permit functionality (gasless approvals).
To do this, it must:
- Access existing ERC20 data.
- Add new storage for permit-specific data.
This requires reusing the ERC20Storage struct from ERC20Facet and defining a separate struct for the nonces variable used by the permit function.
Here is the full ERC20Storage struct from ERC20Facet:
/**
* @notice Storage struct for ERC20.
* @custom:storage-location erc8042:compose.erc20
*/
struct ERC20Storage {
mapping(address owner => uint256 balance) balanceOf;
uint256 totalSupply;
mapping(address owner => mapping(address spender => uint256 allowance)) allowance;
uint8 decimals;
string name;
string symbol;
}
When reusing this struct in ERC20PermitFacet, the Extending Facets rules require removing unused variables at the end of the struct.
ERC20PermitFacet only uses the variables allowance and name from this struct. However, balanceOf, totalSupply, and decimals cannot be removed, even though they are unused, because they appear before the name variable, which is used. Removing them would shift the storage slot used by the name variable which would make it refer to something else.
Only unused variables at the end of a struct may be safely removed. In this case, symbol is the only trailing variable that is unused by ERC20PermitFacet, so it is the only one removed.
Here is the final struct storage code for ERC20PermitFacet:
Summary: How This Example Follows the Guide
-
Reusing storage struct: The
ERC20Storagestruct is copied fromERC20Facetand reused at the same location in storagekeccak256("compose.erc20"), ensuring both facets access the same ERC20 token data. This demonstrates how facets can share storage. -
Maintaining variable order: All variables in the reused
ERC20Storagestruct maintain the same order as the original struct. -
Removing unused variables: The
symbolvariable is removed from the end of the struct since permit functionality doesn't use that variable. This follows the rule that unused storage variables at the end of a struct should be removed. -
Adding custom storage: A new
ERC20PermitStoragestruct is defined with its own storage slotkeccak256("compose.erc20.permit")for thenoncesvariable. This follows the principle that a facet adding new storage variables must define its own diamond storage struct.
Example: Extending ERC20Facet with Staking Functionality
Here's a complete example showing how to correctly extend ERC20Facet by creating a new ERC20StakingFacet that adds staking functionality:
Summary: How This Example Follows the Guide
This example demonstrates proper facet extension by:
-
Extending as a new facet:
ERC20StakingFacetis a separate, self-contained facet that composes withERC20Facet. This follows the principle that every extension should be implemented as a new facet. -
Reusing storage struct: The
ERC20Storagestruct is copied fromERC20Facetand reused at the same storage locationkeccak256("compose.erc20"), ensuring both facets access the same token data. This demonstrates how facets can share storage. -
Maintaining variable order: The
balanceOfvariable remains in the first position of the reusedERC20Storagestruct, exactly as it appears in the original struct, preserving the order of storage variables. -
Removing unused variables: All variables except
balanceOfare removed from the reusedERC20Storagestruct since they are unused byERC20StakingFacet. This follows the rule that storage structs should be designed so removable variables appear at the end, and removal is only done from the end of a struct. -
Adding custom storage: A new
ERC20StakingStoragestruct is defined with its own storage slotkeccak256("compose.erc20.staking")for staking-specific data. This follows the principle that a facet adding new storage variables must define its own diamond storage struct. -
Self-contained design: The facet contains all necessary code (events, errors, storage definitions, and functions) without imports, making it fully self-contained.
-
Composable functionality: This facet can be deployed once and added to any diamond that includes
ERC20Facet, demonstrating true on-chain composition where facets work together without inheritance.
This level of composability strikes the right balance: it enables organized, modular, and understandable on-chain smart contract systems.