I'm using hardhat to test a UUPS upgradable contract. However since this deploys a "clean" new copy of the contract each time, I wonder if things are different from the production environment where contracts are upgraded.
Specifically question is when and how is the initializer function called?
My contract is based on the wizard UUPS below.
When running with hardhat tests, it seems the ROLES are not initialized, even the DEFAULT_ADMIN_ROLE.
contract RevelToken is
Initializable,
ERC721Upgradeable,
ERC721EnumerableUpgradeable,
UUPSUpgradeable
{
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize() public initializer {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
// etc for other roles
}
In this case how does initialize ever get called?
When a first 'instance' of logic or proxy is deployed, it seems initializers will be disabled before ever being called?
My tests are failing because of needed roles - even for the deployer wallet. So Instead I removed the disableInitializers() and explicitly call initializer() after deploy. Now tests pass but it feels like a hack. I have some feeling the above code is so that on upgrading the impl. contract, initializers are not called again to modify the data in the proxy?
Is there a recommended way to test these type of proxy contracts?
Or a map of lifecycle (constructor, initialize) events for both proxy and upgrades?
When you deploy RevelToken contract, the function constructor() will execute to disable initialize contract later, but when you use it as an implementation for a proxy, you can can initialize in the proxy contract to initialize contract.
reverted with reason string 'Initializable: contract is initializing'
initializer: set a different initializer function to call (see Specifying Fragments), or specify false to disable initialization
I read that constructor funcs are (or should not?) be called at all? But also that the initialize not being called is an attack vector.
To prevent the implementation contract from being used, you should invoke the _disableInitializers function in the constructor to automatically lock it when it is deployed:
is this only on upgrading a contract? ie in the logic/impl. contract?
On calling this on deploy I get all kinds of issues (see above). So this is leading my hunch that deploy and upgrade need different code somehow for the initialize.
When you use this tool to deploy upgradable contracts, all your contracts address are kept in the file name .openzeppelin, you can delete contract address to re-deploy.
Hi @dcsan, there are important differences between constructors and initializers. They are both used for different purposes. See my posts here and here for more details.
The error 'Initializable: contract is initializing' comes from _disableInitializers. This function should only be called from a constructor. Other initialization logic should go in an initializer. This is also explained in the links above.