Diamond (aka Multi-Facet Proxy)
Disclaimer: This is a summary of patterns we have observed during our research and should not be considered any form of technical or investment advice. Also, the given “known examples” do not imply they are the best implementations of the said pattern or any superior to any other implementation of the pattern not listed.
Summary
A smart contract is modularised into multiple logic contracts called facets. A proxy contract then contains a mapping that maps function selectors to facet addresses. Users then interact with the proxy contract that relays all requests to the latest version of the respective facet contract.
Context
Like any software application, a smart contract may need to be eventually upgraded to fix bugs, overcome security weaknesses, or add new functionality. In general, business logic and data changes are required at different times and frequencies.
Problem
A smart contract deployed on a blockchain cannot be upgraded because the smart contract (binary) code is stored as immutable ledger data. Therefore, when an updated smart contract is deployed, it will be assigned a different address. Not all transaction issuers know the existence of the updated smart contract version or its address. How to reach the latest smart contracts version and preserve the data/state of the previous one?
Forces
- Immutability – Every bit of data, including deployed smart contracts, stored on the blockchain is immutable.
- Upgradability – There is a fundamental need to upgrade all but short-lived applications and their smart contracts over time.
- Coupling – Data are embedded in a smart contract. A smart contract can live forever on the blockchain if not explicitly terminated. If a smart contract is deactivated in this way, the data stored in the smart contract cannot be accessed through the smart contract functions anymore – although it can still be accessed with some effort for provenance or audit purposes.
- Cost – If a public blockchain is used, storing data costs money. Thus, copying data from an old smart contract version to a new version should be avoided or minimised.
- Inconsistencies: Even if data are migrated there could be other inconsistencies related to data or functions.
Solution
Like the proxy pattern, the idea of separating an interface from its implementation can solve this problem. However, the implementation contract is split into a set of contracts called facets. Facets are separate, independent contracts that can share internal functions, libraries, and state variables. A stable proxy known as the diamond contract presents the smart contract interface and its address is well known. All external transactions call the diamond contract, which in turn calls the respective facet contracts. Whenever a facet contract is upgraded, the diamond only needs to be informed of the new facet address. In the diamond industry diamonds are created and shaped by being cut, creating facets. Likewise, a diamond is cut by adding, replacing, or removing functions from facets.
As seen in the above figure, first, modularise the implementation contract into a set of contract facets. Second, deploy each facet contract. Third, deploy the diamond contract while specifying the function definitions of each facet and its contract addresses. Whenever an updated facet is deployed, the diamond must be updated with the new contract’s address.
Diamonds must maintain function signatures to facet mappings. For this, the diamond should provide a function (called the diamond cut function) to add and remove function signatures and facet addresses. Diamond contracts, however, should avoid exposing all the implementation facets’ interfaces as functions. This is because any changes to a facet’s function signatures require redeploying the diamond to update the interface definition. This breaks its well-known address. Instead, one should rely on the fallback function supported by languages such as Solidity. The fallback function gets executed when a contract does not have a matching function that the transaction is trying to invoke. Therefore, the diamond’s fallback function can be implemented to redirect calls to the implementation contract. The fallback function should determine which facet to call based on the function’s signature specified in the transaction.
Even though facets can store data, it is recommended to store data on the diamond using the data segregation pattern. However, as facets are related functions of the same implementation contract, they may need to access the same data. Hence, it is desirable to store all data in the same place. For example, depending on the smart contract language, data on the diamond can be manipulated via delegate calls without explicit authorisation management like in the data segregation pattern. A delegate call is like a regular call, except that all code is executed in the context of the caller, not of the callee. Thus, the diamond is stateful while the facets are stateless. This also allows multiple diamonds to use the same facet, but manage their state independently. However, it is important to carefully manage the data schema across facets such that an update to the data schema in one face does not break the existing data stored on the diamond.
Benefits
- Fixed contract address – The facets can be updated without breaking the diamond’s address.
- Upgradability – By separating the interface from the implementation, the application logic can be upgraded without affecting the diamond contract. Compared to the proxy pattern, diamond allows fine-grained upgrades as the update is focused on a single facet’s functionality. If delegate calls are used, some data schema updates can be performed without breaking the existing data stored on the diamond.
- Cost – Because the data are separated from the rest of the code, there is no cost to migrate data when the application is upgraded. Also, as a facet is smaller than the entire implementation contract cost of an upgrade on a public blockchain is lower.
- Transparency – A facet could be replaced by an updated version while ensuring it is visible to all stakeholders. Also, all the previous addresses of the facet are still stored on the blockchain.
- Contract size – As the diamond can split the logic into multiple smaller smart contracts (i.e., facets), this pattern can be used to overcome any size limitations on a single smart contract, e.g., it can overcome 24KB maximum contract size in Ethereum.
- Flexibility – A deployed facet can be used by any number of diamonds. Therefore, multiple diamonds can expose different combinations or versions of facets.
Drawbacks
- Cost – If a public blockchain is used, it costs more to deploy multiple facet contracts and call one smart contract from another.
- Upgradability – Data schema changes that are inconsistent with how data are already stored on the diamond cannot be supported. The only option is to update the diamond and migrate data to the updated diamond contract instance.
- Complexity – It is non-trivial to coordinate the data schema updates across facets without breaking the existing data stored on the diamond. Also, if data are stored on the diamond, developers need to be careful not to implement a constructor in the facets as it will initialise the state in the facets, not on the diamond. Also, the facets should not allow self-destructing of the contract either directly or via a delegate call.
Related Patterns
- The data segregation pattern can be used to separate business logic from data.
- Contract registry and this pattern can work together to improve smart contracts’ upgradability.
- The proxy pattern can be used to keep track of the latest instance of the implementation contract.
Known uses
- ERC-2535 standard proposed the diamond pattern.
- Aavegotchi – a decentralized application (DApp) that combines DeFi, NFTs, and gaming – uses the AavegotchiDiamond contract. It is a diamond that implements EIP-2535 diamond standard.