(2 minute read)
As a regular user of the Diamond Standard, one architectural pattern I keep having to impelement is that of multiple tokens backed by an upgradeable Diamond. Specifically, my project may have one or more associated ERC-20 and/or NFTs and I'd like the code for these to be upgradeable.
Why not just implement ERC 1155 as a Diamond? Well, 1155 can handle multiple ERC-20 tokens and a single NFT collection, but not multiple NFT collections (correction: it can if you split the id, thanks Nick Mudge). Besides, most tooling and DeFi protocols that currently exist assume a single token/collection (whether that be fungible or non-fungible) per contract address.
Thus, I would really like each token to be its own smart contract, yet have them all be implemented by a single upgradeable Diamond codebase. How can this be implemented?
Here is the solution:
As shown above, each token is still its own contract. But all the methods internally call through to the Diamond. The Diamond internally routes the calls to the facets which implement them.
For instance, here is how the ERC-20 balanceOf() method would look inside one of the ERC-20 token contracts:
import { IERC20 } from "openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol";
contract MyToken is IERC20 {
...
function balanceOf(address _owner) public view returns (uint256) {
// action this through the Diamond
return diamond.tknBalanceOf(_owner);
}
}
Within the diamond we would have a facet - let's call it ERC20Facet - which implements the tknBalancOf() method as follows:
contract ERC20Facet {
...
function tknBalanceOf(address _owner) public view returns (uint256) {
// return the balance
}
}
Since we want to be able to deploy multiple NFTs and ERC-20s we need to distinguish between them from within the Diamond. One easy way of doing this is to check the msg.sender. Let's take our tknBalanceOf() method above:
function tknBalanceOf(address _owner) public view returns (uint256) {
address token = msg.sender;
// return the balance of "token" for the "_owner" wallet
}
Now that we have a way of identifying which token is making the call through to the diamond we can setup the storage accordingly. Let's use the AppStorage storage option for the Diamond:
/* file: AppStorage.sol */
// app storage structure
struct AppStorage {
// token contract address => wallet owner => wallet balance
mapping(address => mapping(address => uint)) tokenBalance;
}
// app storage slot retriever
library LibAppStorage {
bytes32 internal constant DIAMOND_APP_STORAGE_POSITION = keccak256("diamond.app.storage");
function diamondStorage() internal pure returns (AppStorage storage ds) {
bytes32 position = DIAMOND_APP_STORAGE_POSITION;
assembly {
ds.slot := position
}
}
}
Now we can complete the tknBalanceOf() method as follows:
import { LibAppStorage, AppStorage } from "./AppStorage.sol";
contract ERC20Facet {
...
function tknBalanceOf(address _owner) public view returns (uint256) {
address token = msg.sender;
AppStorage storage s = LibAppStorage.diamondStorage();
return s.tokenBalance[token][_owner];
}
}
The above setup can then be replicated for the remaining ERC-20 methods.
And we can use this system for NFTs and indeed, any ERC/EIP which specifies an outward-facing API. The system can be summarised as:
Facade -> Diamond
The Facade is the contract which adheres to the ERC-20/ERC-721/other standard as expected by clients of said contract. Internally the facade is just a proxy to the Diamond, which internally routes calls to the appropriate facet(s) that implement said calls.
We can now upgrade the code underpinning ALL the facades at once by upgrading the diamond facets. Yay! 🙂
And if we want a particular facade to behave differently to others, we can codify that within the implementation facet since it knows which facade any given call comes from.
Working demos
Demos of the above concept can be found in the following repos: