A better way to initialize upgradeable contracts
EIP-2535 Diamonds
Overview
Smart contract code is immutable, which means that the code cannot be changed after deployment. This presents challenges when developing on the blockchain because it prevents the ability to perform maintenance or updates. Smart contracts also face a 24KB contract size limit, introducing scalability dilemmas. The EIP-2535 standard offers a sophisticated proxy pattern that allows the addition, replacement, and removal of functions while virtually removing any constraint on code size.
While smart contracts are immutable, they can add and remove references to external functions on other smart contracts and still preserve storage context by using delegate calls. This is the standard architecture for proxy systems.
EIP-2535 Diamond standard introduces a generic proxy contract, the Diamond, which includes an internal diamondCut() function and exposes a fallback function, which dynamically dispatches function calls to the facets called from the Diamond.
The fallback function allows the facet functions to be executed as if the Diamond had implemented them. The msg.sender
and msg.value
values remain the same, and the Diamond's storage is read and written to.
A diamond proxy contract is a single source for all state variable data. Facets provide the code and state variable definitions used to read and write to a diamond proxy's contract storage.
A diamond contains within it a mapping of function selectors to facet addresses. Functions are added/replaced/removed by modifying this mapping.
The diamondCut
function is used to add/replace/remove any number of facets and functions to a diamond in a single transaction, preventing an inconsistent state on the Diamond.
The diamondCut()
function can execute an external function with delegatecall
during an upgrade. This is used to initialize state variables and make any changes needed for an upgrade.
Initializing State
The second and third arguments of the diamondCut()
function are used for initializing the state after an upgrade. The _init
argument holds the contract address of a function to call to initialize the state of the Diamond. The _calldata
argument has a function call to send to the contract at _init
.
Example - if an ERC20 Facet is added to a Diamond using diamondCut(), the_calldata
argument provides the function call to set token name and symbol values and the _init
argument provides the address of the contract with the init function. The init function is executed in the same transaction as the diamondCut().
The _calldata execution is skipped if the _init
value is address(0), therefore, _calldata
can contain 0 bytes or custom information.
After adding/replacing/removing functions, the _calldata
argument is executed with delegatecall
on _init
. This execution is done to initialize data, set up, or remove anything no longer needed. DiamondInit functions can have parameters and can be reused as needed once deployed.
The EIP-2535 also defines a DiamondMultiInit function that executes multiple initializer functions for a single upgrade. This can be useful when users want to support a unique initialization function per cut. DiamondMultiInit exists as a library to prevent it from being deleted as a result of a delegatecall
to selfdestruct
.
During the execution of an upgrade, using the _init
and _calldata
arguments allow state initialization to occur in the same transaction. This is important because it prevents the Diamond from falling into an inconsistent state, which could arise if state variables are not initialized in sync with the functions being added.
Conclusion
EIP-2535 Diamonds introduce a new and better approach for initializing upgradeable contracts. The standard provides a function for single and multiple function initializations after the diamondCut()
function. The approach synchronizes upgradeability and state initialization into the same transaction, helping ensure the Diamond always maintains a consistent state.