Building Your First Diamond
Create a working diamond project using Compose then explore every file so you know exactly what you have.
This tutorial uses the Counter example: a minimal diamond with increment and read functions. It's the simplest way to see how diamonds works end-to-end.
Prerequisites
Althought the CLI support both Foundry and Hardhat, this tutorial will focus on a Foundry based project. All learnings from the diamond archtiecture are the same for both framework
If you haven't used Foundry before, follow the official installation guide first. You only need forge and anvil for this tutorial.
Step 1) Install the CLI
Install the Compose CLI so you can run all the CLI commands
Verify it's installed:
You can skip the global package installation. Just use npx @perfect-abstractions/compose-cli wherever you see compose in this tutorial.
Step 2) Scaffold the project
You'll be prompt to choose from a lot of different options from framework, ERC standards templates, and others.
For this tutorial, we will choose the Counter example with the following options:
Take the time to review the full library available to you. We support most major token standards like ERC20, ERC721 with different type of access control.
You can always skip the interactive prompts entirely:
When the CLI finishes, follow the next steps instruction to move into your project and test it:
Step 3) Explore the project structure
The scaffold generates a diamond with two local facets and one from the Compose library.
Each facet owns a slice of the logic. CounterDataFacet exposes the count, CounterIncrementFacet mutates it. The Diamond.sol proxy routes calls to the right facet via function selectors.
Diamond.sol: The proxy
The diamond is the application entrypoint. Users call it, but it holds no logic itself, only the storage state. Every call is delegated to a stateless facet via the fallback function:
When a function is called on the diamond, DiamondMod.diamondFallback() looks up which contract owns that selector and forwards the call.
There nothing more to make a diamond proxy work. Now, let's transition where our application logic lives
CounterDataFacet.sol
Facets need to share state. They do this by pointing to the same storage slot:
Any facet that need Counter data uses the same STORAGE_POSITION [ keccak256("counter") ] and CounterStorage struct. They never import each other. They just agree on the same storage location.
Facets can be scoped based on your project needs. Compose encourages small scoped facets, one facet per responsibility, to allow granular composition. In this example, CounterDataFacet handles reads and CounterIncrementFacet handles mutations. You could combine them into a single facet, but keeping them separate lets you pick exactly the functionality you need at anytime
Step 4) Build and test
Now let's compile and run the tests:
You should see compilation succeed and one passing test in /test/Diamond.t.sol
The test deploys a diamond with all three facets the Counter needs, then calls facetAddresses() on DiamondInspectFacet to verify the right number of facets are registered.
Step 5) Write your own test
Let's interact with the Counter through a test. Open test/Diamond.t.sol and take a look at what's already there:
The test file already has a setUp() function that deploys a fresh Diamond instance before each test. There's also one test that verifies all three facets are properly registered on the proxy.
Take a look at the order here. The facets are deployed first as separate contracts, then the Diamond proxy is created with references to those pre-deployed facet addresses. This is the core of composition over inheritance. Instead of a monolithic contract that is everything through inheritance chains, the Diamond has the facets it needs. They're composed at runtime via the constructor or the upgrade functionality.
Now let's add a test that actually uses our Counter. Add this function inside the DiamondTest contract:
Run the test suite:
All tests pass, demonstrating that data written by one facet is immediately visible to another through the diamond's shared storage.
Step 6) Deploy locally
Start a local Ethereum node in one terminal:
In a second terminal, deploy your Counter application:
The private key above is Anvil's default test key. Never use it on a real network.
You'll see the deployed addresses logged:
Congratulations!
You just deployed your first diamond proxy application. You've scaffolded a Counter project using the CLI, written tests that verify shared storage across facets, and deployed a composed system to a local network.
We recommend you to explore all the template available to scaffold. Use the the catalog command to review what's available to you.
What's next?
Learn Compose foundations
Understand how facets, modules, and storage work together.
Design for composition
Learn patterns for breaking logic into composable facets.
Run npx @perfect-abstractions/compose-cli init again and pick ERC-20 or ERC-721 to see how the same pattern scales to token standards.