The EIP-2535 Diamonds is a way to organize your Solidity code and contracts to give them the right amount of modularity and cohesion for your system. In addition, flexible upgrade capability is a key part of it.
Let's get defined what a diamond actually is. A diamond is a contract that uses the external functions of other contracts as its own.
The contracts that a diamond uses for its external functions are called facets.
A diamond has a mapping inside of it that associates functions to contracts (facets) that have those functions. When an external function is called on a diamond the diamond looks in the mapping to find the facet to retrieve the function from and execute.
Of course, a diamond is implemented according to EIP-2535 Diamonds. That way tools can be used with them and it is easier to understand and work with people's contracts because they follow the standard.
Let's look at a diagram that shows a simple diamond. For simplicity generic names of things are used in this diagram. You should use names that make sense for your system.
In the diagram above you can see that functions func1 and func2 are associated with FacetA. Functions func3, func4, func5 are associated with FacetB. Functions func6 and func7 are associated with FacetC.
Also in this diagram you see that different structs within the diamond are used by different facets. FacetA uses DiamondStorage3. FacetB uses DiamondStorage3 and DiamondStorage2. FacetC uses DiamondStorage2 and DiamondStorage1.
The diagram above shows the DiamondStorage structs in the diamond. It is true that all contract storage data is stored in the diamond, not in its facets. But the struct definitions exist in the facet source code. Or the struct definitions can exist in separate solidity files or Solidity libraries that are imported by facets, or the struct definitions can be in a contract that is inherited by multiple facets.
A key part of the Diamond Standard is how it helps you with contract storage. It introduces a new storage technique called Diamond Storage. It is a new technique because until recently it wasn't possible to do.
By default, when you create new state variables like unsigned integers, structs, mappings etc. Solidity automatically takes care of where exactly these things are stored within contract storage. But this default automatic functionality becomes a problem when upgrading diamonds with new facets. New facets declaring new state variables clobber existing state variables -- data for new state variables gets written to where existing state variables exist.
Diamond Storage solves this problem by bypassing Solidity's automatic storage location mechanism by enabling you to specify where your data gets stored within contract storage.
This might sound risky but it is not if you use a hash of a string that applies to your application or is specific to your application. Use the hash as the starting location of where to store your data in contract storage.
Doing that might seem risky to you too. But it is not. Realize that this is how Solidity's storage location mechanism works for maps and arrays. Solidity uses hashes of data for starting locations of data stored in contract storage. You can do it too.
Modularity by Decoupling Facets from Each Other
In the past it was common for a facet to contain within its source code every single state variable that was ever used by the diamond, in the order they were first declared. Or at least these facets contained state variables in their source code that they didn't use. This was done to avoid the problem of a new facet clobbering existing state variables.
This problem is now solved with Diamond Storage. With Diamond Storage the source code of a facet can just contain the state variables that it actually needs and there is no concern about overwriting existing state variables.
This means that facets that use Diamond Storage are independent from each other and it means that these facets can be reused by different diamonds. In this way facets become reusable libraries for diamonds.
diamondCut, the Swiss-Army Knife for Contract Upgrades
The diamondCut
function enables you to add, replace and remove any number of functions from a diamond in a single transaction. For example in one function call to diamondCut
you can add 3 new functions, replace 6 functions and remove 4 functions. Or you could call diamondCut
one time to add one function. This is really flexible.
In addition the diamondCut
function emits an event that shows all changes made to a diamond. It records all additions, replacements and removals of functions.
The Loupe
A loupe is a magnifying glass that is used to look at diamonds.
The Diamond Standard provides 4 functions that are used to provide information about what functions and facets are currently stored or used in a diamond. Together these functions are called 'the loupe'. Any diamond implements these functions. See the Diamond Standard for more information about them.
How to Get Started Making Your Diamond
The most important thing is reading and understanding the Diamond Standard. If something is unclear let me know!
The second important thing is using a Diamond Standard reference implementation.
The reference implementation is more than a reference implementation. It is the boilerplate code you need for a diamond. Also, using the reference implementation makes your diamond compliant with the standard.
Calling Diamond Functions
In order to call a function that exists in a diamond you need to use the ABI information of the facet that has the function.
Here is an example that uses web3.js:
let myUsefulFacet = new web3.eth.Contract(
MyUsefulFacet.abi,
diamondAddress
)
In the code above we create a contract variable so we can call contract functions with it.
In this example we know we will use a diamond because we pass a diamond's address as the second argument. But we are using an ABI from the MyUsefulFacet facet so we can call functions that are defined in that facet. MyUsefulFacet's functions must have been added to the diamond (using diamondCut) in order for the diamond to use the function information provided by the ABI of course.
Similarly you need to use the ABI of a facet in Solidity code in order to call functions from a diamond. Here's an example of Solidity code that calls a function from a diamond:
string result = MyUsefulFacet(diamondAddress).getResult()
Get Help and Join the Community
If you need help or would like to discuss diamonds then send me a message on twitter, or email me. Or join the Diamond Standard Discord server.
Top comments (3)
Great article !!! I am working on developing NFT artifacts by using Diamond standard. I still am unclear about several points in Diamond standard:
Thanks in advance
Great questions Gavin.
The
diamondCut
function which adds/replaces/removes functions prevents the same function selectors from being added.Contract storage can be upgraded by adding state variables to the ends of structs. Also more areas of storage can be accessed using diamond storage. Info on that here: dev.to/mudgen/how-diamond-storage-...
I use git to manage contract source code. I use git tags to label deployments and upgrades to help keep track of things. It would be great if more tools and/or practices were published about this.
I do like the modularity aspect. I would love to see an implementation of this integrated with IPFS around modular NFTs. Have you seen the work from Async.art . I think this flows well with their vision