Abstract:
The diamond pattern is a pretty recent standard that offers a solution to a lot of solidity issues:
- Single address for multiple contracts
- You can update your functions after contract deployment
- No 24kb size limit
- ... but it also has some downsides or let's say compromises. One of those compromises is that you can't have multiple functions with the same footprint inside one diamond. This can make things complicated when you want to have several tokens under the same diamond (for example an ERC721 NFT contract with an ERC20 token contract). To know more about the Diamonds standard check: https://github.com/mudgen/awesome-diamonds I came across this pattern because I needed some kind of proxy architecture to scale up Parcels contracts (Parcels is a game & company I'm co-founding: https://twitter.com/parcelsgame). ## The issue Let me demonstrate what I mentioned above. This is not possible:
//FacetA.sol
contract FacetA {
function my_function(uint256 number) external {
//... Some logic
}
}
//FacetB.sol
contract FacetB {
function my_function(uint256 number) external {
//... Some other logic, with the same footprint
}
}
Because all facets functions will be exposed under one single contract address (the diamond address), it couldn't determine which one to use on a diamond.my_function(number)
call.
For this example you could simply change footprints, like calling the functions respectively functionA
and functionB
, but you often need a presice footprint in order to implement standards & interfaces. This can for example happend with erc20 and erc721 that both have the same balanceOf(address _owner)
function.
However, this is possible:
//FacetA.sol
contract FacetA {
function function(uint256 number) external {
//...
}
}
//FacetB.sol
contract FacetB {
function function(uint256 number, string str) external {
//...
}
}
Because both functions now have a different footprint, because of the arguments, even under a common name. For those familiar with object-oriented programming, this is pretty similar to how you can declare multiple constructors in most languages.
This means that you cannot have multiple facets implementing the same interface.
But this goes even further as different standards can still clash partly, like ERC20 and ERC721, meaning you cannot implement both at the same time inside a common diamond.
The ERC1155 standard
Fortunately, it happens to exist a standard that is designed to handle multiple token on one contract: ERC1155.
Basically ERC1155 is storing balances behind two parameters: id and address. It is this id that allows us to have multiple tokens at the same time on this contract.
These tokens can be fungible (if you allow minting more than one), or non-fungible (if you only allow one mint per id).
In order to distiguish the different token types you can split the ids in ranges:
Let's say we want an app where you have one tokens: $GLD (that would usually implement an ERC20 fungible token), and 1000 unique miners (that would usually implement an ERC721 non-fungible token). To implement these two tokens under our ERC1155 contract, we need to split the ids: One for the $GLD token and 1000 for the miners NFTs. What we can simply do here is assigning the first id (0) to $GLD, and set ids 1 - 1001 to the miners.
More generally what you would do is define à "base constant" for each token type, so here you would have something like:
uint256 constant GLD_ID = 0;
uint256 constant MINER_BASE_ID = 1;
So that you can access to the miner n°x
with balance(MINER_BASE_ID + x)
.
If you need multiple wide ranges (like two types of nfts) you can set the base id by shifting bits:
uint256 constant GLD_ID = 0;
uint256 constant MINER_BASE_ID = 1;
uint256 constant OTHERNFT_BASE_ID = 1 <<< 128;
For more information on the ERC1155 standard please refer to: https://eips.ethereum.org/EIPS/eip-1155
Architecture
To implement the ERC1155 I advice solidstate-solidity LINK that is compatible with the diamond standard, or adapting OpenZepplin's ones rather than re-writing your own from scratch which could add vulnerabilities.
So, we now have a clear way of creating multiple tokens with our ERC1155 contract, but having all the functions in one single contract for every type of token would lead to messy code, and worse, could even be too much.
To address that we will separate each token logic in it's own Facet contract (see diamond pattern to understand facets).
But we will probably want to call funtions from one contract to another (for example you could want to access the $GLD balance to upgrade a miner). While this is possible by leaving the logic in separated facets, the best option is to move all the logic in Libraries, which simplifies calls. This also allows to only leave external getters / setters functions in the Facets, making the architecture even cleaner, as external exposed logic will be separated form internal logic (respectivly Facets & Libraries).
A simple token ($GLD) + NFT (Miner) architecture would be something like this:
- Facets only expose external functions: ERC1155Facet for standard ERC1155 functions, TokenFacet & NFTFacet for custom funtions (like
getGLDBalance(address of)
). - Libraries LibToken & LibNFT handle all the logic (that will be used as internal and external) so that it can be reused across facets & other libraries.
- LibStorage contains all the data that has to be stored which is not handled by ERC1155, see AppStorage / DiamondStorage pattern.
- LibERC1155Internal is a copy of the functions defined in solidstate's
ERC1155Internal.sol
contract so that we can call internal functions across facets & libraries (like _mint, etc...). You just have to add events from theIERC1155Internal.sol
interface and removevirtual
keywords from functions (as library functions cannot / would not be overwritten). See https://gist.github.com/nohehf/3a1116e47d932bb9477bbc5332e61a9a .
This architecture makes sharing the logic across different parts of the application seamless while keeping concerns in separate codebases.
Snippets examples:
So for our $GLD / Miner example we would have the folowing structure (based on the hardhat-diamond-3 starter):
contracts
├── Diamond.sol
├── facets
│ ├── DiamondCutFacet.sol
│ ├── DiamondLoupeFacet.sol
│ ├── ERC1155Facet.sol 🔺
│ ├── GLDFacet.sol 🔺
│ ├── MinerFacet.sol 🔺
│ └── OwnershipFacet.sol
├── interfaces
│ ├── IDiamondCut.sol
│ ├── IDiamondLoupe.sol
│ ├── IERC165.sol
│ └── IERC173.sol
├── libraries
│ ├── LibDiamond.sol
│ ├── LibERC1155Internal.sol 🔺
│ ├── LibGLD.sol 🔺
│ ├── LibMiner.sol 🔺
│ └── LibStorage.sol 🔺
└── upgradeInitializers
└── DiamondInit.sol
Only files marked with 🔺 are custom, the other ones are provided by the starter repo
ERC1155Facet.sol:
// SPDX-License-Identifier: UNLICENCED
pragma solidity ^0.8.9;
import {ERC1155} from "@solidstate/contracts/token/ERC1155/ERC1155.sol";
contract ERC1155Facet is ERC1155 {}
GLDFacet.sol:
// SPDX-License-Identifier: UNLICENCED
pragma solidity ^0.8.9;
import "../libraries/LibGLD.sol";
import {LibStorage, AppStorage, ArtefactType} from "../libraries/LibStorage.sol";
import {Modifiers} from "../libraries/LibStorage.sol";
contract ArtefactFacet is Modifiers {
// ----- GETTERS -----
function getMyGLDBalance(address addr) external view returns (uint256) {
return LibGLD._getBalance(msg.sender);
}
// ...
// ----- SETTERS -----
// ...
}
LibERC1155Internal.sol (see https://gist.github.com/nohehf/3a1116e47d932bb9477bbc5332e61a9a)
LibGLD.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import {LibStorage, AppStorage, ArtefactType} from "./LibStorage.sol";
import "@solidstate/contracts/token/ERC1155/base/ERC1155BaseStorage.sol";
// import {ERC1155Facet} from "../facets/ERC1155Facet.sol";
import "../facets/ERC1155Facet.sol";
import "./LibERC1155Internal.sol";
// Handles all the $GLD token logic
library LibGLD {
//CONSTANTS
uint256 constant GLD_ID = 0;
//STORAGE GETTERS:
// common storage
function s() internal pure returns (AppStorage storage) {
return LibStorage.diamondStorage();
}
//erc1155 storage (NOTE: you should prefer calling LibERC1155, but it can be usefull)
function s1155() internal pure returns (ERC1155BaseStorage.Layout storage) {
return ERC1155BaseStorage.layout();
}
//GLD LOGIC
function _getBalance(address addr) internal view returns (uint256) {
return LibERC1155Internal._balanceOf(addr, GLD_ID);
}
function _mint(address to, uint256 amount) internal {
LibERC1155Internal._mint(to, GLD_ID, amount, "");
}
// ...
}
Note that I only made examples for The miner would be pretty similar to GLD
Conclusion
Besides fixing the footprint colision problem between multiple tokens facets under the same diamond, this solitions provides a scalable, easy to use and test solution for multi-token Dapps. Calling the internal functions without having to rely on the deployed address of a contract is very handfull (and cheaper). Once established this pattern allowed me to run things smoothly and easly implement new features to each of my tokens, while keeping clean code & directories.
Possible improvements:
-> solidstate-solidity could move all the logic to Libs so we won't have to copy-paste everything.
-> Diamonds standard should remove the ERC165 implementation on the DiamondLoupeFacet
, which we currently have to remove to add an ERC1155 facet (which is IMO a bit problematic).
-> Starter repo for ERC1155 & Diamonds.
I'm also discussing with solidstate & diamonds creators to improve their docs, or even
make a real framework with solidstate-solidity & diamonds along with step-by-step tutorial and docs, supporting natively this kinds of architectures.
Thanks for reading, and please ask me on twitter if you want more details / want to contribute: www.twitter.com/nohehf .
Top comments (0)