Idiots Guide to Using an EIP-2535 Diamond Proxy
First, I want to say I am not a programmer by trade... but 2.5 years in lockdown provides some unique opportunities to learn for those so inclined.
So why not start with destroying your personal finances while dabbling in blockchain cryptocurrencies and learn a little Solidity and front end JS, right?
Exactly. I did not do that… until I finally caught COVID and quarantined for 10 days and cracked open my IDE and started to write some solidity code. Guess what? It was fun. So I built a rudimentary back-end ERC-1155 contract but I needed a front end. Learned a lot about Node, NextJS, and some legacy React. So now I am a very dangerous full stack hack.
The problem was that I kept running into the 24K limit of contracts and tweaking my contract until I just got tired to tweaking it and decided to figure out what is the deal with Diamonds.
I read about it re-read about it and frankly it just sounded hard and I didn’t understand it and I just didn’t want to take the time to understand the programming mumbo jumbo. I just wanted to be spoon fed.
Turns out, I am not alone in that desire because I cant seem to find any basic tutorials for people to implement a Diamond proxy with EIP-2535. There are lots of tutorials that tell you “what” a Diamond proxy “IS”, how it “should work”, and even some code snippets that explain cool things like shared state variables in libraries, but as a non-programmer/hack I can only say I understand the value of all that, but I cant figure out how to actually use it.
So, after pouring over the EIP-2535 for quite some time I decided to try and figure out what Nick M and the discord community has been trying to tell me in their post by writing this medium you are reading. In short, this is my attempt to break it down for those of us who don’t program all day and are barely keeping up with their day jobs, let alone a programming blockchain or full stack side hustle - hence my “Idiots Guide To Using an EIP-2535 Diamond Proxy”.
What you are about to read is by no means 100% correct.
I am sure I will say things that are dumb and laughable and illustrate my lack of knowledge… but I am sure the Internet will correct me and I will learn. On the learning note, huge shout out to the Ethereum community! And a big thank you go Nick Mudge on his EIP-2535 work, ANY Ethereum EIP collaborator, the team at Openzeppelin, the SolidState Network, and especially Mr. Patrick Collins (Chainlink). I have been quietly stalking his YouTube videos and Chainlink blogs and his explanations have helped immensely (especially for someone who started programming 2 months ago)…. so here we go.
Open this webpage: https://github.com/mudgen/diamond-1-hardhat
RTF REAME.MD and do what it says
Assuming you got this far without any errors - congratulations! Excellent start!
From a setup perspective, the only delta I made from the original clone of Nicks work was that I added hardhat-gas-reporter. My experience is that this is an invaluable tool that will visually tell you the gas units to deploy or transact a function min/max/avg. From there you can figure out if your project costs too much ETH to actually use and FIX IT. So, very important visual tool. Again, for the folks like me here is how you install hardhat-gas-reporter…
<project root> npm install hardhat-gas-reporter
Edit the hardhat-config.js file in the root of the project folder and add:
require(“hardhat-gas-reporter”);
By default it will work when you run hardhat tests. If your got a .env then you are more advanced than my target audience and I presume you can fix it yourself if you do/don’t want it to run on each test.
First, let us just see what happens when we try and test Nick’s github repo we cloned earlier. The reason I want to start with the test and not the deploy script is because the test script actually calls the deploy script first and then runs all the test cases in memory. For the curious, the deploy is line 25 in the test script diamondTest.js
To do this, simply open a terminal and type:
user@hostname:~/<project root directory>$ npx hardhat test test/diamondTest.js
You should see the diamond deploy and a bunch of tests run and then the pretty hardhat-gas-reporter output like this:
Another gold star if you have made it this far!
Now that we ran the test in memory we are going to start up a hardhat local node and run the deploy script. To do that, simply open two terminals. In both terminals, go to the projects root directory.
Terminal 1:
user@hostname:~/<project root directory>$ npx hardhat node
This starts the local hardhat blockchain node where we are going to deploy the Diamond Proxy.
Terminal 2:
user@hostname:~/<project root directory>$ npx hardhat run scripts/deploy.js
This will execute the deploy.js script that was in the repo you cloned.
What you will see is the Diamond be deployed and some facets being added (DiamondLoupeFacet and OwnershipFacet)
What you will see are the function selectors for functions in each of these facets as they are being added to the Diamond via the DiamondCutFacet (which seems to be the first thing deployed).
In short, for the “idiots guide”, you should see something like this terminal:
Translation - Unlike a test which just runs the deploy and tests in memory, you have now started a local hardhat blockchain node and you have successfully deployed a Diamond Proxy that is ready to do stuff.
But what the heck does that mean? Well, I would like to say I could walk you through all the lines of code on the setup deploy script, but I cant…. at least not for now because that is not what I wanted to do with this tutorial. All us novices out there just need to know that you have deployed a Diamond Proxy and performed two DiamondCuts to add two facets (DiamondLoupFacet and OwnershipFacets).
Now for the fun part. Notice that my tutorial isn’t about deploying a Diamond Proxy. It’s about using a diamond proxy standard. What I am really after is how to use shared storage and functions in libraries. In fact, what I really want to do is get some basic understanding of how to work with the EIP-2535 shared storage variables and functions in libraries so I can transfer that knowledge to a ERC-1155 DAPP and make it more modular and reusable.
HOW DO WE USE THIS BEAUTIFUL DIAMOND?
We are going to take Nicks Diamond Proxy GitHub repo and start re-tooling that with a basic contract we already know and love… the Greeter.sol contract.
As you are probably familiar, Greeter.sol is the “Hello World” of Solidity in Hardhat. It has a basic construct with a get and set function and a single string variable.
But why stop at one facet? Let’s make it more interesting and split the Greeter.sol contract into two facets. One facet that Sets the shared state variable and one the Gets the shared state variable. And, we are going to do this by utilizing the LibDiamond library which already has the constructs for Diamond Storage, shared storage variables, and source repository of facet functions.
Quick recap form what is cool about EIP-2535:
What’s cool here is that the Library holds all the common source code for my diamond storage AND the functions that my facets will utilize. I said it this way on purpose because it wasn’t immediately obvious to me as a non-programmer until I tried to use the diamond.
The Diamond Storage is where I will create the shared state variables. So, any variables you want to share state between facets need to be added to this DiamondStorage structure.
To use those shared state variables, we create functions that call on those shared state variables inside the library. The functions just simply reference the state variables through the diamondStorage function. The key is that it’s all done inside the library.
The reason this is cool is I don’t have to rewrite ALL the functions multiple times in the facets. I just simply import the library and call the library function from INSIDE the facet.
This provides modularity and reusability between the code and facets and a common shared storage for all the facets inside the Diamond.
So, let’s get down to business and modify the library. You will find the library in <project home directory>/libraries/LibDiamond.sol
Open the library, find the DiamondStorage structure and add a new string variable as the shared state storage variable for our new facets. You can see in the code below that I used the “string greeterMessage” as my shared state variable inside the DiamondStorage structure. This is what I am pointing too with the top green arrow.
Before we leave the library, lets also create two functions that will USE the shared state variable. One function will set the variable. One function will get the variable.
These functions are in the following image, toward the bottom with the two green arrows.
Lets start with the get function. We will call it “libraryGetGreeting”. And, since the libraryGetGreeting function is in a library, it will be marked as ‘internal’. Also, since the function is just reading state, it can be marked as ‘view’. And lastly, it will return a state variable which is a string that will be passed from memory.
But, how do I address the shared state variable in the library?
Basically we just want the function to return the value of the string “greeterMessage” that is inside the DiamondStorage structure in the LibDiamond library.
So how do we do that…?
Well, look at the above code snip. See the diamondStorage function? Look at it and see what you think it does?
Well, there is a bunch of code mumbo-jumbo that basically has the diamondStorage function access the DiamondStorage structure at a specific point called ds.
There is probably a more technical description, but this is good enough for me if it works. So, given that the diamondStorage function will work as described, we just need to tell the diamondStorage function to go get the shared state variable greeterMessage as follows:
Now, let’s define the second function that will set the shared state variable. We will call that the librarySetGreeting function. This function will take one string argument from memory. Since the librarySetGreeting function is going inside the library it can be marked ‘internal’. For our purposes, we don’t need to return anything.
What does it need to do?
This will seem out of order, but to explain what we are doing I will explain it this way:
Action 1: It needs pass a storage variable _greeting into the DiamondStorage structure through the diamondStorage function.
But how? This is where the reverse order comes in..
Action 2: In order to accomplish Action 1, our function needs to have a way to reference the diamondStorage function. We accomplish this by defining a storage variable called ds and setting it equal to the diamondStorage function:
DiamondStorage storage ds = diamondStorage();
Then you smash these two actions into the function in reverse order as follows:
Seems simple, but it’s the reverse ordering that confuses me. I presume people that program would understand this pretty quick as it seems to happen a lot when I try to read/understand code.
And there you have it. The library is complete for our purposes. We now have a shared state variable called greeterMessage and two functions that set and get that state variable.
Before we leave the library, I want to recap.
The diamondStorage function is the gateway to the DiamondStorage structure. As built, the diamondStorage function gives us access to the shared state variables in a very unique storage slot and does so in such a way as to ensure it won’t clash with other state variables in the Diamond. The diamondStorage function allows us to easily call shared state variables defined in the library. The library contains internal functions that reference the DiamondStorage structure. The facets just need to access the library functions and we will have shared storage between the facets, which, after all, was the point of using the diamond proxy with DiamondStorage - right?
Now that the library is ready with shared state variables and functions, we can move on and create the two set/get facets: getGreeter.sol and setGreeter.sol
setGreeter.sol:
You can see from the above code snip that getGreeter.sol is a VERY simple contract.
It imports the library and defines one function called facetGetGreeting.
But how do we reference the library functions from inside the getGreeter.sol contract?
Generally speaking, once the contract imports the library, we can reference elements in the library like this:
<library_name>.<library_function_name>
So we will write the getGreeting.sol function called facetGetGreeting such that is uses the library function libraryGetGreeting.
This will enable our new facet to access the shared state variable by using the libraryGetGreeting function in the library.
As you can see, the facet facetGetGreeting function is ‘external’ (because another contract, like your diamond proxy, or another 3rd party contract, will be calling it), it is returning state only, so it can be ‘view’, and it returns a string from memory
But what about the “do stuff” part of the function?
Well, you can see the function simply returns what is in the LibDiamond library using the libraryGetGreeting function. Notice that the facet references the library and the libraries functions with the generalized format library name and library function name.
So, a quick recap to check understanding:
If we called the getGreeting.sol contract from ANYWHERE (not just through the diamond proxy), it is externally accessible and will return something from the blockchain. The thing it will return is actually referenced in the LibDiamond Library and the function it is returning is whatever the libraryGetGreeting function does.
Lets go the extra mile. Working it backwards from here and looking at the LibDiamond library, you should see that the libraryGetGreeting function (below) will return whatever the diamondStorage function finds in the greeterMessage structure position.
Whew!
Let’s do the same thing for the setGreeter.sol contract then….
setGreeter.sol:
You can see that setGreeter.sol is a VERY simple contract.
It too imports the library and defines one function called facetSetGreeting.
The function is ‘external’ (because the diamond proxy contract will be calling it). However, this time, we are going to accept a string memory argument called _greeting.
When it comes to the ‘do something’ part, we are calling the LibDiamond library and the librarySetGreeting function in the library.
One more time, how do we call the shared state variable and the function that references it from the library inside the new setGreeter.sol contract?
Simple.
You import the library and call the <library name>.<library_function_name>
So our facetSetGreeting function will look like this:
Now let’s go all the way by looking at what happens when we call the LibDiamond library librarySetGreeting function.
If we look at the LibDiamond library, the librarySetGreeting function creates a storage object called ‘ds’ that is equal to the diamondStorage function. If you recall that the diamondStorage function is the gateway to the DiamondStorage structure, you will then see a call to the ds.greeterMessage = _greeter
. IOW, use the diamondStorage function (aka ds) to store _greeter in the greeterMessage slot.
Yeah… that’s why Diamond Storage and Diamond proxies are complex for us non-programmers. But that’s it.
Hopefully that explains how to USE Diamond Storage and Diamond Proxy.
Now, let’s move on to add some tests and make sure what we thought was gonna happen actually happens.
TESTING:
Lets start by modifying the testDiamond.js script to perform the following:
Add a deploy test for the two facets we just built.
Add a test to check the initial state of the shared state variable
Add a test to check the set/get efficacy of the shared state variable.
Luckily for us, Nick’s GitHub repo test script, testDiamond.js, already provides an example for adding two facets called Test1Facet.sol and Test2Facet.sol. Since the original/vanilla test ran without any errors, we just need to find those specific test deploy scripts (aka DiamondCutFacet) that add a facet in the testDiamond.js script and then do a little RAD (rip off and duplicate) work.
And away we go….
First, open the testDimaond.js script, pulled out the old CTRL-F and searched for Test1Facet. The first hit will be a deploy JavaScript construct that should look familiar, however if you read the test case it has a bunch of functions it’s adding and interfaces it’s add/removing. That seems like more than we need. Let’s see if Test2Facet is a cleaner/simpler deploy a facet and DiamondCutFacet. CTRL-F and search for Test2Facet. This brings us to line 115. The test looks about the same as Test1Facet but without all the extra stuff… after a quick review, it looks like we can copy/paste this and use it as our boilerplate for adding our new facets in two new tests.
Here is the code snip for the test that deploys the getGreeter.sol facet:
Again, this was just a copy/paste and then find and replace the variable names. The first constant variable just gets the getGreeter.sol contract from the contract factory (minus the *.sol part). The second gives the deploy function a name ‘getgreeter’. The rest just uses ‘getgreeter’ on a find/replace.
Here is a code snip for the test that deploys the setGreeter.sol facet:
Big surprise, we basically copy/paste the Test2Facet deploy test case again. Then did a find/replace to update the names of the variables as before.
Last I built two test cases. The first test case checks that the initial value of the shared state variable was empty. It’s probably not necessary but I like the idiot checks to make sure it’s doing what I think it should.
The second test case is the real test! Here we use the setGreeter facet to set the value of the shared state variable. Then we use the getGreeter facet to get the shared state variable. Then we check they are the same.
Here is that code snip test case:
You will notice that I manually made a new variable called input that was the string “Hello World!” That’s because there is no return from setGreeter to set a variable too and I didn’t want to rewrite this whole thing with updated code just to fix this anomaly.
Maybe someone will tell me how to do it…
And to close this out, here is the final test run that shows the 15 tests pass. If you scroll to the top, you will see the original clone of the repo has 11 tests, and we added four: two facet cuts (e.g. deploy), one idiot check, and one set/get test of the shared state variable.
Appreciate any feedback (andy@advuni.com).
Hope you enjoyed this write up.
Andy Edwards
8-26-2022