I spent some time trying to understand the constructor code that is added when using the contracts wizard to create an Upgradeable ERC20 contract.
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
Basically, the premise is to not leave a deployed upgradeable contract uninitialized, since it can be exploited by Dark Forest creatures.
So using hardhat node I used the deployProxy provided by Openzeppelin Hardhat Upgrades API, trying to see when the constructor is invoked.
- Default initialize() function is invoked when deployed, constructor not invoked - contract is initialized (Contract safe):
const mytoken = await upgrades.deployProxy(MyToken, { initializer: 'initialize', kind: 'uups' });
or
const mytoken = await upgrades.deployProxy(MyToken, { kind: 'uups' });
- Deployed contract is not initialized, constructor is not invoked, and manual initialization is required (Contract not safe) :
const mytoken = await upgrades.deployProxy(MyToken, { initializer: false, kind: 'uups' });
- The only way I could get the constructor to be executed was with a non-proxy deployment:
const mytoken = await MyToken.deploy();
This set the _initialed variable to 255, so no further initalizations are possible.
So an upgradeable wizard-generated contract can be left uninitialized when deployed, or deployed with a non-default initializer function name, but it has to be done intentionally, and hopefully with a plan or code to prevent exploitation.
Cheers