Hi, I'm a bot! This change was automatically merged because: - It only modifies existing Draft or Last Call EIP(s) - The PR was approved or written by at least one author of each modified EIP - The build is passing
28 KiB
eip | title | author | discussions-to | status | type | category | created |
---|---|---|---|---|---|---|---|
2535 | Diamond Standard | Nick Mudge <nick@perfectabstractions.com> | https://github.com/ethereum/EIPs/issues/2535 | Draft | Standards Track | ERC | 2020-02-22 |
Simple Summary
A diamond is a set of contracts that can access the same storage variables and share the same Ethereum address.
A contract architecture that makes upgradeable contracts flexible, unlimited in size, and transparent.
New terminology from the diamond industry.
Abstract
A diamond is a proxy contract that supports using multiple logic/delegate contracts at the same time. In this standard the term for logic/delegate contract is facet. A diamond can have many facets. Which facet is used depends on which function is called. Each facet supplies one or more functions.
A diamond provides:
- A way to add, replace and remove multiple functions atomically (in the same transaction).
- An event that shows what functions are added, replaced and removed.
- A way to look at a diamond to see its functions.
- Solves the 24KB maximum contract size limitation. Diamonds can be any size.
- Enables zero, partial or full diamond immutability as desired, and when desired.
- Designed for tooling and user-interface software.
Motivation
Additional Benefits & Use Cases
- Build trust over time by showing all changes made to a diamond.
- A stable diamond address that provides needed functionality.
- The ability to develop and improve an application over time with an upgradeable diamond and then make it immutable and trustless if desired.
Why Make a Diamond?
The flexibility of this standard makes a lot of designs possible. There are reasons to make diamonds that the author of this standard doesn't know about and reasons to make diamonds that have not been discovered yet. Here are some known reasons:
- You exceed the max size of a contract and you have related functionality that needs to use the same storage variables. Make a diamond.
- Diamonds can be large but still modular because they are compartmented with facets.
- Multiple small contracts calling each other increases complexity. A diamond handling its storage and functionality is simpler.
- You need or want greater control over when and what functions exist.
- Your development is incremental and you want your contracts to grow with your application.
New User-Interface Software & Libraries
User-interface software can be written to show all documentation, functions and source code used by a diamond.
Diamond events can be filtered from the Ethereum blockchain to show all changes to a diamond.
Existing and new programming libraries and software can be used to interact with and use diamonds.
Upgradeable Diamond vs. Centralized Private Database
Why have an upgradeable diamond instead of a centralized, private, mutable database?
- Wide interaction and integration with the Ethereum ecosystem.
- With open storage data and verified source code it is possible to show a provable history of trustworthiness.
- With openness bad behavior can be spotted and reported when it happens.
- Independent security and domain experts can review the change history of contracts and vouch for their history of trustworthiness.
- It is possible for an upgradeable diamond to become immutable and trustless.
Different Kinds of Diamonds
Many designs of diamonds are possible. Here are a few kinds of diamonds and their uses.
Upgradeable Diamond
An upgradeable diamond has the diamondCut
function used to add/replace/remove functions. It is useful for iterative development or improving an application over time.
Finished Diamond
A finished diamond was an upgradeable diamond and had a number of upgrades. Then its diamondCut
function was removed and upgrades are no longer possible. It is no longer possible to add/replace/remove functions. It has become an immutable diamond.
Single Cut Diamond
The specification section of the standard is careful not to say that a diamond has a diamondCut
function. Instead it says a diamond uses a diamondCut
function. This careful wording makes single cut diamonds and other patterns possible. A single cut diamond uses the diamondCut
function in the constructor function of the diamond to add all functions to the diamond, but it does not add the diamondCut
function to the diamond. This means that a single cut diamond is fully created in its constructor and once created can never be upgraded. It has the same immutability and trustless guarantees as a regular vanilla contract. Why would someone do this? There may be a number of reasons. The two use cases below are really good reasons.
The two use cases below assume that you want an immutable contract:
- Your contract hits the max contract size limit and there is too much code or dependency upon storage variables to separate it out into regular contracts. Make it into a single cut diamond. You still break your big contract into smaller facets, modularizing your code to a degree.
- You start with an upgradeable diamond in your development and testing and upgrade it to your heart's delight. Reap the advantages of easy upgrading and a stable address as you work out new features, bugs and kinks. Release the upgradeable diamond on a test network with your application for beta testing and upgrade it when needed. This is iterative development. When it is solid then make a single cut diamond version of it and launch it on the main network.
Specification
Note: The solidity
delegatecall
opcode enables a contract to execute a function from another contract, but it is executed as if the function was from the calling contract. Essentiallydelegatecall
enables a contract to "borrow" another contract's function. Functions executed withdelegatecall
affect the storage variables of the calling contract, not the contract where the functions are defined.
Terms
- A diamond is a contract that uses functions from its facets to execute function calls. A diamond can have one or more facets.
- The word facet comes from the diamond industry. It is a side, or flat surface of a diamond. A diamond can have many facets. In this standard a facet is a contract with one or more functions that executes functionality of a diamond.
- A loupe is a magnifying glass that is used to look at diamonds. In this standard a loupe is a facet that provides functions to look at a diamond and its facets.
- An immutable function is a function that is defined directly in a diamond and so cannot be replaced or removed. Or it is a function that is defined in a facet that cannot be replaced or removed.
General Summary
A diamond calls functions from its facets using delegatecall
.
In the diamond industry diamonds are created and shaped by being cut, creating facets. In this standard diamonds are cut by adding, replacing or removing facets and their functions.
A diamond uses a diamondCut
function to add, replace and remove facets and functions.
The DiamondCut
event is emitted that records all changes to a diamond.
Diamond Interface
The diamondCut
function is marked external
in the interface below but it can be a public, internal or private function when implemented.
Note: Even though the "experimental" pragma directive is used for ABIEncoderV2, it is no longer experimental.
pragma solidity ^0.6.4;
pragma experimental ABIEncoderV2;
interface Diamond {
/// @notice _diamondCut is an array of bytes arrays.
/// This argument is tightly packed for gas efficiency
/// Here is the structure of _diamondCut:
/// _diamondCut = [
/// abi.encodePacked(facet, sel1, sel2, sel3, ...),
/// abi.encodePacked(facet, sel1, sel2, sel4, ...),
/// ...
/// ]
/// facet is the address of a facet
/// sel1, sel2, sel3 etc. are four-byte function selectors
function diamondCut(bytes[] calldata _diamondCut) external;
event DiamondCut(bytes[] _diamondCut);
}
With the diamondCut
function any number of functions can be added/replaced/removed in one transaction. All changes are recorded with the DiamondCut
event.
The _diamondCut
argument is passed directly to the DiamondCut
event in the diamondCut
function.
Diamond Loupe
A loupe is a small magnifying glass used to look at diamonds. These functions look at diamonds.
The function selectors used by a diamond are queried to get what functions the diamond has and what facets are used.
A diamond loupe is a contract that implements this interface:
// A loupe is a small magnifying glass used to look at diamonds.
// These functions look at diamonds.
interface DiamondLoupe {
/// These functions are expected to be called frequently
/// by tools. Therefore the return values are tightly
/// packed for efficiency.
/// @notice Gets all facets and their selectors.
/// @return An array of bytes arrays containing each facet
/// and each facet's selectors.
/// The return value is tightly packed.
/// Here is the structure of the return value:
/// returnValue = [
/// abi.encodePacked(facet, functionSelectors),
/// abi.encodePacked(facet, functionSelectors),
/// ...
/// ]
/// facet is the address of a facet.
/// functionSelectors consists of one or more 4 byte function selectors.
function facets() external view returns(bytes[] memory);
/// @notice Gets all the function selectors supported by a specific facet.
/// @param _facet The facet address.
/// @return A bytes array of function selectors.
/// The return value is tightly packed. Here is an example:
/// return abi.encodePacked(selector1, selector2, selector3, ...)
function facetFunctionSelectors(address _facet)
external
view
returns(bytes memory);
/// @notice Get all the facet addresses used by a diamond.
/// @return A byte array of tightly packed facet addresses.
/// Example return value:
/// return abi.encodePacked(facet1, facet2, facet3, ...)
function facetAddresses() external view returns(bytes memory);
/// @notice Gets the facet that supports the given selector.
/// @dev If facet is not found return address(0).
/// @param _functionSelector The function selector.
/// @return The facet address.
function facetAddress(bytes4 _functionSelector) external view returns(address);
}
See the reference implementation to see an example of how this can be implemented.
These functions are designed for user-interface software. A user interface calls these functions to provide information about and visualize diamonds.
The facetFunctionsSelectors
, facets
, facetAddresses
functions are designed to be called by off-chain software and so are not gas efficient.
Design Points
A diamond implements the following design points:
- A diamond contains a fallback function, a constructor, and zero or more immutable functions that are defined directly within it.
- The
diamondCut
function is used to add/replace/remove functions in the diamond. ThediamondCut
function can be an "immutable function" that is defined directly in the diamond or it can be defined in a facet. - The
diamondCut
function associates function selectors with facets and emits theDiamondCut
event. - When a function is called on a diamond it executes immediately if it is an "immutable function" defined directly in the diamond. Otherwise the diamond's fallback function is executed. The fallback function finds the facet associated with the function and executes the function using
delegatecall
. If there is no facet for the function then execution reverts. - The
bytes[] memory _diamondCut
argument to thediamondCut
function specifies the functions to add, replace and remove. If the facet address to associate function selectors with isaddress(0)
then the provided functions selectors are removed from the diamond, otherwise the function selectors are added or replaced in the diamond. ThediamondCut
function reverts if a supplied function selector is already associated with the supplied facet address. ThediamondCut
function reverts if a supplied function selector can't be removed because it already does not exist. - Each time functions are added, replaced or removed a
DiamondCut
event is emitted to record it. - A diamond implements ERC165. If a diamond has the
diamondCut
function then the interface ID used for it is Diamond.diamondCut.selector. The interface ID used for the DiamondLoupe interface isDiamondLoupe.facets.selector ^ DiamondLoupe.facetFunctionSelectors.selector ^ DiamondLoupe.facetAddresses.selector ^ DiamondLoupe.facetAddress.selector
.
This standard does not prevent other functions from being added that add, replace or remove functions. But in any case the DiamondCut
event must be emitted for any addition, replacement or removal of functions.
The diamond address is the address that users interact with. The diamond address does not change. Only facet addresses can change by using the diamondCut
function.
Implementation
A reference implementation is given in the Diamond repository.
Diamond Example
Note: The following is just an example implementation of the standard. It is not the standard. A diamond is defined by the Specification section.
The diamond example below is taken from the reference implementation of the standard.
The diamond adds the diamondCut
function and diamond loupe functions to itself in the constructor.
pragma solidity ^0.6.12;
pragma experimental ABIEncoderV2;
import "./DiamondStorageContract.sol";
import "./DiamondHeaders.sol";
import "./DiamondFacet.sol";
import "./DiamondLoupeFacet.sol";
interface Diamond {
/// @notice _diamondCut is an array of bytes arrays.
/// This argument is tightly packed for gas efficiency
/// Here is the structure of _diamondCut:
/// _diamondCut = [
/// abi.encodePacked(facet, functionSelectors),
/// abi.encodePacked(facet, functionSelectors),
/// ...
/// ]
/// facet is the address of a facet
/// functionSelectors consists of one or more 4 byte function selectors
function diamondCut(bytes[] calldata _diamondCut) external;
event DiamondCut(bytes[] _diamondCut);
}
contract DiamondExample is Storage {
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
constructor() public {
// Using Diamond Storage. See the storage section of the standard.
// Diamond Storage is implemented in DiamondStorageContract.sol
DiamondStorage storage ds = diamondStorage();
ds.contractOwner = msg.sender;
emit OwnershipTransferred(address(0), msg.sender);
// Create a DiamondFacet contract which implements the Diamond interface
DiamondFacet diamondFacet = new DiamondFacet();
// Create a DiamondLoupeFacet contract which implements the DiamondLoupe interface
DiamondLoupeFacet diamondLoupeFacet = new DiamondLoupeFacet();
// Three facets and their selectors will be stored in this array
bytes[] memory diamondCut = new bytes[](3);
// First facet
// Adding cut function
diamondCut[0] = abi.encodePacked(diamondFacet, Diamond.diamondCut.selector);
// Second facet
// Adding diamond loupe functions
diamondCut[1] = abi.encodePacked(
diamondLoupeFacet,
DiamondLoupe.facetFunctionSelectors.selector,
DiamondLoupe.facets.selector,
DiamondLoupe.facetAddress.selector,
DiamondLoupe.facetAddresses.selector
);
// Third facet
// Adding supportsInterface function
diamondCut[2] = abi.encodePacked(address(this), this.supportsInterface.selector);
// execute cut function
bytes memory cutFunction = abi.encodeWithSelector(Diamond.diamondCut.selector, diamondCut);
(bool success,) = address(diamondFacet).delegatecall(cutFunction);
require(success, "Adding functions failed.");
// adding ERC165 data
ds.supportedInterfaces[ERC165.supportsInterface.selector] = true;
ds.supportedInterfaces[Diamond.diamondCut.selector] = true;
bytes4 interfaceID = DiamondLoupe.facets.selector ^ DiamondLoupe.facetFunctionSelectors.selector ^ DiamondLoupe.facetAddresses.selector ^ DiamondLoupe.facetAddress.selector;
ds.supportedInterfaces[interfaceID] = true;
}
// This is an immutable function because it is defined directly in the diamond
// This implements ERC165
function supportsInterface(bytes4 _interfaceID) external view returns (bool) {
DiamondStorage storage ds = diamondStorage();
return ds.supportedInterfaces[_interfaceID];
}
// Finds facet for function that is called and executes the
// function if it is found and returns any value.
fallback() external payable {
DiamondStorage storage ds;
bytes32 position = DiamondStorageContract.DIAMOND_STORAGE_POSITION;
assembly { ds_slot := position }
address facet = ds.facets[msg.sig];
require(facet != address(0), "Function does not exist.");
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())
let result := delegatecall(gas(), facet, ptr, calldatasize(), 0, 0)
let size := returndatasize()
returndatacopy(ptr, 0, size)
switch result
case 0 {revert(ptr, size)}
default {return (ptr, size)}
}
}
receive() external payable {
}
}
Specifically here is what is happening:
- The constructor creates a new DiamondFacet contract. DiamondFacet is a contract that implements the Diamond interface. The Diamond interface has the
diamondCut
function used to add/replace/remove functions. - The constructor creates a new DiamondLoupeFacet contract. DiamondLoupeFacet is a contract that implements the DiamondLoupe interface. The DiamondLoup interface is a set of functions for inspecting and showing what facets and functions a diamond has.
- The constructor specifies three facets and their selectors to be added to the diamond: DiamondFacet for the
diamondCut
function, DiamondLoupeFacet for the loupe functions and address(this) for thesupportsInterface
function. - The facets and their selectors are stored in the
diamondCuts
array. - The
diamondCut
function, with the_diamondCut
array, is called usingdelegatecall
on diamondFacet to execute the cuts.
The end result is that when a new DiamondExample contract is created it will have the diamondCut
function and the diamond loupe functions and the supportsInterface
function. The diamondCut
function is used to add more functions and the loupe functions show what functions have been added.
Rationale
Using Function Selectors
This standard is designed to make diamonds work well with user-interface software. Function selecters with the ABI of a contract provide enough information about functions to be useful for user-interface software. Instead of function selectors and a contract ABI, function signatures could be used but they do not provide information about their return values or if they are read-only or not. Function selectors are used because they are more gas efficient and contract ABIs need to be used anyway.
Gas Considerations
Delegating function calls does have some gas overhead. This is mitigated in several ways:
- Facets can be small, reducing gas costs. Because it costs more gas to call a function in a contract with many functions than a contract with few functions.
- Because diamonds do not have a max size limitation it is possible to add gas optimizing functions for use cases. For example someone could use a diamond to implement the ERC721 standard and implement batch transfer functions from the ERC1412 standard to reduce gas (and make batch transfers more convenient).
- Because facets can be small the Solidity optimizer can be set to a high setting causing more bytecode to be generated but the facets will use less gas when executed.
Storage
The standard does not specify how data is stored or organized by a diamond. But here are some suggestions:
Diamond Storage Since Solidity 0.6.4 it is possible to create pointers to structs in arbitrary places in contract storage. This enables diamonds and their facets to create their own storage layouts that are separate from each other and do not conflict with each other, but can still be shared between them. See this blog post for more information: New Storage Layout For Proxy Contracts and Diamonds. The reference implementation for the diamond standard uses Diamond Storage.
Inherited Storage A diamond and all of its facets use the same storage layout space. So in the diamond source code and facet source code the same storage variables should be declared in the same order.
Here is a way to implement inherited storage:
- All storage variables should be
internal
. - Create a storage contract that contains the storage variables that your diamond will use.
- Make your diamond inherit the storage contract.
- Make your facets inherit the storage contract.
- If you want to add a new facet that adds new storage variables then create a new storage contract that inherits the old storage contract and adds the new storage variables. Use the new storage contract with the new facet.
- Repeat step 5 for every new facet that adds new storage variables.
Unstructured Storage Assembly is used to store and read data at specific storage locations. An advantage to this approach is that previously used storage locations don't have to be defined or mentioned in a facet if they aren't used by it.
Eternal Storage Data can be stored using a generic API based on the type of data. See ERC930 for more information.
Immutable Diamond
It is possible to make a diamond immutable. This is done by calling the diamondCut
function to remove the diamondCut
function. With this gone it is no longer possible to add, replace or remove functions.
Versions of Functions
Software or a user can verify what version of a function is called by getting the facet address of the function. This can be done by calling the facetAddress
function from the DiamondLoop interface. This function takes a function selector as an argument and returns the facet address where it is implemented.
Sharing Functions Between Facets
In some cases it might be necessary to call a function defined in a different facet. Here are some solutions to this:
- Copy the function code in one facet to the other facet.
- Put common functions in a contract that is inherited by multiple facets.
- Use
delegatecall
to call functions defined in other facets. Here is an example of doing that:
bytes memory myFunction = abi.encodeWithSignature("myFunction(uint256)", 4);
(bool success, uint result) = address(this).delegatecall(myFunction);
require(success, "myFunction failed.");
Security Considerations
Ownership and Authentication
Note: The design and implementation of diamond ownership/authentication is not part of this standard. The examples given in this standard and in the reference implementation are just examples of how it could be done.
It is possible to create many different authentication or ownership schemes with the diamond standard. Authentication schemes can be very simple or complex, fine grained or coarse. The diamond standard does not limit it in any way. For example ownership/authentication could be as simple as a single account address having the authority to add/replace/remove any functions. Or a decentralized autonomous organization could have the authority to only add/replace/remove certain functions.
Consensus functionality could be implemented such as an approval function that multiple different people call to approve changes before they are executed with the diamondCut
function. These are just examples.
The development of standards and implementations of ownership, control and authentication of diamonds is encouraged.
Security of Diamond Storage
If a person can add/replace functions then that person can alter storage willy nilly. This is very powerful and very dangerous. However the capability can be used while eliminating or reducing the danger. The danger is eliminated or reduced by limiting who can add/replace/remove functions, limiting when functions can be added/replaced/removed and by transparency.
Who Here are some ways who can be limited:
- Only allow a trusted individual or organization to make diamond upgrades.
- Only allow a distributed autonomous organization to make diamond upgrades.
- Only allow multi-signature upgrades.
- Only allow the end user who owns his own diamond to make upgrades. This enables users to opt-in to upgrades.
- Don't allow anybody to make upgrades by making a single cut diamond.
When Here are some ways when can be limited:
- Only allow upgrades during development and testing. Make a single cut diamond for main network release.
- Use an upgradeable diamond until it is certain that no new features are needed and then make it a finished diamond by removing the ability to add/replace/remove functions.
- Program into the
diamondCut
function certain periods of time that the diamond can be upgraded. For example thediamondCut
function could be programmed so that a diamond could only be upgraded during a specific five hour period each year. Attention and transparency could be applied to that five hour period to ensure upgrades are done right.
Transparency Transparency provides certainty and proof that upgrades are done correctly and honestly.
- Publish and make available verified source code used by diamonds and facets.
- Provide documentation for diamonds, facets, upgrade plans, and results of upgrades.
- Provide tools that make your diamonds more visible and understandable.
Function Selector Clash
A function selector clash occurs when two different function signatures hash to the same four-byte hash. This has the unintended consequence of replacing an existing function in a diamond when the intention was to add a new function. Function selector clashes are very rare but should be prevented by using tools that check for function selector clashes before calling the diamondCut
function.
Transparency
Diamonds emit an event every time one or more functions are added, replaced or removed. All source code can be verified. This enables people and software to monitor changes to a contract. If any bad acting function is added to a diamond then it can be seen.
Security and domain experts can review the history of change of a diamond to detect any history of foul play.
Backwards Compatibility
This standard makes upgradeable diamonds compatible with future standards and functionality because new functions can be added and existing functions can be replaced or removed.
Inspiration & Development
The Diamond Standard is an improved design over EIP-1538 using ABIEncoderV2 and function selectors.
The Diamond Standard replaces EIP-1538.
This standard was inspired by EIP-1538 and ZeppelinOS's implementation of Upgradeability with vtables.
This standard was also inspired by the design and implementation of the Mokens contract.
Copyright
Copyright and related rights waived via CC0.