Design for Composition
Here are guidelines and rules to create composable facets.
Compose replaces source-code inheritance with onchain 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 set of external functions that represent a single unit of self-contained 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 Libraries
- Facet libraries are self-contained code units. They do not import anything.
- Each facet should have one corresponding facet library.
- Facet libraries are used to initialize facets on deployment and during upgrades.
- Facet libraries are also used to integrate custom facets with Compose facets.
- Facet libraries have an initialize function which is used to initialize storage variables during deployment.
Facets & Libraries
- Facets and facet libraries should not contain owner/admin authorization checks unless absolutely required or fundamental to the functionality being implemented.
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.
- When reusing structs or storage layouts from existing facets or libraries, reuse the original diamond-storage locations and structs, but only include the variables that the new facet actually needs if possible. Of course a reused struct must maintain the same storage layout as an originally defined struct.
- Reusing a struct is done by copying it and removing unused variables.
- Storage structs should be designed so that removable variables (unused by some facets) appear at the end of the struct.
- Storage structs should also be designed for packed storage (smaller sized variables using the same storage slot).
- Removing storage variables is done only from the end of a struct. If a variable cannot be removed without breaking layout, it must remain to preserve compatibility. 8.A facet that adds new storage variables must define its own diamond-storage 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.
This level of composability strikes the right balance: it enables highly organized, modular, and understandable onchain smart contract systems.