It is common in smart contract systems to setup and initialize new contracts using constructor functions. But constructor functions in an implementation contract or facet cannot set state variables in a proxy contract or diamond. So another way is needed to initialize state in a proxy contract or diamond.
Implementation contracts, like from OpenZeppelin, simulate constructor functions with a series of special initialization functions that can only be called once within the context of a proxy contract. These special initialization functions are chained together through inheritance - a contractâs special initializer function can be called which can then call its parent contract initializer function, which can then call that contractâs parent contract etc., simulating the builtin functionality of constructor functions which do this automatically.
Code organization and extension and customization of proxy contracts that use a single impelemnation is done by contract inheritance. So people get used to the idea that each contract, inherited or not, has its own initialization function.
But things get different with an EIP-2535 Diamond. A diamond can use inheritance to organize code, extend it and customize it, just as a single implementation proxy can, but it has an additional major, major tool it can take advantage of. That tool is composition.
With inheritance you end up with one implemenation contract made up of multiple contracts. Inheritance is an âis aâ relationship.
With composition you end up with multiple separate contracts. You can add some or all of these to any number of diamonds. Composition is a âhas a relationshipâ.
Back to what I was talking about before. People have been told and educated and are in the habit and practice of having an initialization function for every contract that sets state and every parent contract that sets state throughout the inheritance chain. Some people think this is how it works, and thatâs it. But guess what, it doesnât have to work that way, and in the case of diamonds, there is a better way.
A Better Way To Initialize Diamonds
Here are things I got rid of with a better way to initialize diamonds:
Got rid of special initializer logic to enforce that initialization functions are only called once.
Got rid of chains of initialization functions through inheritance.
Got rid of initialization functions from facets.
People new to EIP-2535 Diamonds and coming from other upgradeable contract systems automatically think that each facet of a diamond should have its own initialization function. It could work that way but I donât recommend it for many applications. It is possible that in some applications that initialization model fits best. The range of possible applications that can be built with EIP-2535 is too large to say what is best in all instances.
The following is the solution I recommend and EIP-2535 Diamonds was designed to initialize diamonds this way:
Instead of writing a separate initialization function for each facet, just write one initialization function for all facets, or for whatever changes in an upgrade. It doesnât matter if there are 3 facets or 100 facets, a small upgrade or a large upgrade.
I will say it again: write a single initialization function that initializes state for the whole diamond and for all of its facets. Put this initialization function in its own separate contract, not in a facet or anywhere else. Deploy the contract with this initialization function. An example of such a contract with a diamond initialization function is here: DiamondInit.sol.
Execute the initialization function during diamond deployment or an upgrade by using the last two parameters of the diamondCut function. Here is the diamondCut function:
function diamondCut(
FacetCut[] calldata _diamondCut,
address _init,
bytes memory _calldata
)
The _init
parameter is the address to the contract that has the initialization function. _calldata
is the function call to the initialization function.
According to the EIP-2535 Diamonds standard the initialization function call is executed with DELEGATECALL. This ensures that setting state variables occurs within the context of the diamond proxy contract. If you have any doubts or uncertainty about how DELEGATECALL works I recommend reading this article: Understanding delegatecall And How to Use It Safely and experimenting with it.
An example of a diamond deployment script that deploys a diamond, including executing an initialization function is here: https://github.com/mudgen/diamond-1-hardhat/blob/main/scripts/deploy.js
Initialization functions can also have parameters to make them more flexible and resuable on-chain.
This method of initialization is again a form of composition instead of inheritance. It is cleaner and works great!