How to call initialize on implementation when upgrading with OZ Upgrades?

I'm currently using a Transparent Proxy and writing some tests to make sure that the implementation contract was initialized.

The basic idea is the following:

      //coin is our proxy
      const CoinV2Factory = await ethers.getContractFactory("CoinV2");
      await upgrades.upgradeProxy(
        coin.address, 
        CoinV2Factory,
        {
          call: {fn: "initialize", args: ["Coin Name", "COIN"]} 
        }
      );
      const impl2Address = await getImplementationAddress(ethers.provider, coin.address);
      const implementation = await ethers.getContractAt("CoinV2", impl2Address);
      await expect(implementation.initialize("Coin Name", "COIN")).to.be.reverted;

However, I am getting the error "'Initializable: contract is already initialized'".

Disclaimer: I know initializing the Name/Symbol of the Token does not matter from a security perspective, but just giving a simple example.

There are 3 different concepts here:

  1. Initializing a proxy
  2. Re-initializing a proxy during an upgrade
  3. Initializing the implementation itself to prevent someone else from initializing it

1 and 2 are done by initializers or reinitializers (see https://docs.openzeppelin.com/contracts/4.x/api/proxy#Initializable).

Initializers or reinitializers can be called from deployProxy or upgradeProxy as in your example above -- these happen in the context of the proxy's address.

3 is done by having a constructor like the below, which happens in the context of the implementation. See https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#initializing_the_implementation_contract

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
    _disableInitializers();
}

This sets the initialized version to the maximum value for the implementation itself, which prevents someone from calling an initializer or reinitializer in the context of the implementation address.

1 Like