EIP-2535: Diamond Standard¶
The Diamond Standard is an improvement over EIP-1538. It has the same idea: To map single functions for a delegatecall to addresses, instead of proxying a whole contract through.
The important part of the Diamond Standard is the way storage works. Unlike the unstructured storage pattern that OpenZeppelin uses, the Diamond Storage is putting a single struct
to a specific storage slot.
Function wise it looks like this, given from the EIP Page:
// A contract that implements diamond storage.
library LibA {
// This struct contains state variables we care about.
struct DiamondStorage {
address owner;
bytes32 dataA;
}
// Returns the struct from a specified position in contract storage
// ds is short for DiamondStorage
function diamondStorage() internal pure returns(DiamondStorage storage ds) {
// Specifies a random position from a hash of a string
bytes32 storagePosition = keccak256("diamond.storage.LibA")
// Set the position of our struct in contract storage
assembly {ds.slot := storagePosition}
}
}
// Our facet uses the diamond storage defined above.
contract FacetA {
function setDataA(bytes32 _dataA) external {
LibA.DiamondStorage storage ds = LibA.diamondStorage();
require(ds.owner == msg.sender, "Must be owner.");
ds.dataA = _dataA
}
function getDataA() external view returns (bytes32) {
return LibDiamond.diamondStorage().dataA
}
}
Having this, you can have as many LibXYZ and FacetXYZ as you want, they are always in a separate storage slot as a whole, because of the whole struct
. To completely understand it, this is stored in the Proxy contract that does the delegatecall, not in the Faucet itself.
That's why you can share storage across other faucets. Every storage slot is defined manually (keccak256("diamond.storage.LibXYZ")
).
The Proxy Contract¶
In the "Diamond Standard" everything revolves around the Diamond terms. The idea is quite visually cutting a Diamond to add functions (or mapping of addresses to functions and vice versa).
The function to add Facets and functions is called "diamondCut".
The functionality to view what functions a Facet has is called "Loupe": It returns the function signatures and addresses and everything else you might want to know about a Facet.
There is not one way to implement this functionality. Nick went ahead and created three different ways to do a reference implementation, which can be seen on his repository.
First, checkout how the Smart Contracts are deployed in the migration file. This reveals that deploying the Diamond
contract already gives the addresses and function selectors of the DiamondCutFacet and the DiamondLoupeFacet. Essentially making them part of the Diamond Proxy.
If you checkout the test-case, then you see exactly that the first test cases are getting back address<->signature mapping and checking that these were really set in the Diamond proxy. Line 121 is where the Test1Facet and then later Test2Facet functions are added.
Giving It A Try¶
First, we need to clone the repository:
git clone https://github.com/mudgen/diamond-1.git
then we start ganache-cli (download it with npm install -g ganache-cli
if you don't have it), in a second terminal window:
ganache-cli
then we simply run the tests and have a look what happens
truffle test
What you can observe is that the diamondCut interface is only available through the library and called in the Diamond contract in the constructor. If you were to remove the complete update functionality, you can simply remove the diamondCut function.
Let's add a new file "FacetA.sol" in the contracts/facets folder with a bugfixed version of the content given above to write a simple variable and add it to the Diamond in the test case!
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
library LibA {
// This struct contains state variables we care about.
struct DiamondStorage {
address owner;
bytes32 dataA;
}
// Returns the struct from a specified position in contract storage
// ds is short for DiamondStorage
function diamondStorage() internal pure returns(DiamondStorage storage ds) {
// Specifies a random position from a hash of a string
bytes32 storagePosition = keccak256("diamond.storage.LibA");
// Set the position of our struct in contract storage
assembly {
ds.slot := storagePosition
}
}
}
// Our facet uses the diamond storage defined above.
contract FacetA {
function setDataA(bytes32 _dataA) external {
LibA.DiamondStorage storage ds = LibA.diamondStorage();
ds.dataA = _dataA;
}
function getDataA() external view returns (bytes32) {
return LibA.diamondStorage().dataA;
}
}
Let's also adapt our migrations file:
const FacetA = artifacts.require('Test2Facet')
module.exports = function (deployer, network, accounts) {
deployer.deploy(FacetA)
}
If you paid attention so far, then you'll see the code, as it is right now, isn't very secure because anyone in any facet can retrieve keccak256("diamond.storage.LibA"); and overwrite the storage slot.
Add the following unit-test:
/* eslint-disable prefer-const */
/* global contract artifacts web3 before it assert */
const Diamond = artifacts.require('Diamond')
const DiamondCutFacet = artifacts.require('DiamondCutFacet')
const DiamondLoupeFacet = artifacts.require('DiamondLoupeFacet')
const OwnershipFacet = artifacts.require('OwnershipFacet')
const FacetA = artifacts.require('FacetA')
const FacetCutAction = {
Add: 0,
Replace: 1,
Remove: 2
}
const zeroAddress = '0x0000000000000000000000000000000000000000';
function getSelectors (contract) {
const selectors = contract.abi.reduce((acc, val) => {
if (val.type === 'function') {
acc.push(val.signature)
return acc
} else {
return acc
}
}, [])
return selectors
}
contract('FacetA Test', async (accounts) => {
it('should add FacetA functions', async () => {
let facetA = await FacetA.deployed();
let selectors = getSelectors(facetA);
let addresses = [];
addresses.push(facetA.address);
let diamond = await Diamond.deployed();
let diamondCutFacet = await DiamondCutFacet.at(diamond.address);
await diamondCutFacet.diamondCut([[facetA.address, FacetCutAction.Add, selectors]], zeroAddress, '0x');
let diamondLoupeFacet = await DiamondLoupeFacet.at(diamond.address);
result = await diamondLoupeFacet.facetFunctionSelectors(addresses[0]);
assert.sameMembers(result, selectors)
})
it('should test function call', async () => {
let diamond = await Diamond.deployed();
let facetAViaDiamond = await FacetA.at(diamond.address);
const dataToStore = '0xabcdef';
await facetAViaDiamond.setDataA(dataToStore);
let dataA = await facetAViaDiamond.getDataA();
assert.equal(dataA,web3.eth.abi.encodeParameter('bytes32', dataToStore));
})
})
If you run the test with truffle test test/facetA.test.js
then you'll see that it adds the functions from FacetA.sol to the Diamond. In the second test case it stores a value and retrieves it again.
Pros and Cons¶
On the plus side, this is an interesting concept for circumventing very large Smart Contracts limits and gradually updating your Contracts. It definitely is in its infancy and should be investigated further.
I was hoping you could get a framework that let's you break up your Smart Contracts into smaller parts and deploy and update each one of them separately. It does that, somehow, but it also doesn't, since Facets still need a complete picture of internally used functions and signatures.
All in all, I believe Nick is on a good way to get there. There are, however, a few major drawbacks which need makes it un-usable for us:
-
The proxy could be a central point of entry to a larger ecosystem of Smart Contracts. Unfortunately, larger systems often make use of inheritance quite heavily and therefore you have to be extremely careful with adding functions to the Diamond proxy. Also function signatures could easily collide for two different parts of the system with the same name.
-
Every Smart Contract in the System needs adoption for the Diamond Storage, unless you use only one single facet that uses unstructured storage. Simply adding the OpenZeppelin ERC20 or ERC777 tokens wouldn't be advised, as they would start writing to the Diamond Contract storage slot 0.
-
Sharing storage between facets is dangerous. It puts a lot of liability on the admin.
-
Adding functions to the Diamond via diamondCut is quite cumbersome. I do understand that there are other techniques where the facets bring their own configuration - which is much better, like in this blog post.
-
Adding functions to the Diamond via DiamondCut could become quite gas heavy. Adding the two functions for our FacetA Contract costs 109316. That's $20. Extra.
Alright, now we come to the last part of this article. Wild Magic with CREATE2...