Upgrade contract without changes to storage layout

I'm trying to upgrade a smart contract using the UUPSProxy. The smart contract was previously deployed and upgraded successfully already. Now I made another change to the contract that just 2 more methods but didn't change anything of the storage layout.

Using upgrades.prepareUpgrade() or defender.proposeUpgrade() now always skips deployment because it will find and use my previously deployed implementation contract and seems to think it's the same (even though it's missing the 2 new methods).

Using export DEBUG=@openzeppelin:upgrades:* gives me the following output:

 @openzeppelin:upgrades:core fetching deployment of implementation 3ea64c77d47fa926979be270f802d38301853572d3d949a0f6cc8a3963452d27 +0ms
 @openzeppelin:upgrades:core polling timeout 60000 polling interval 5000 +238ms
 @openzeppelin:upgrades:core verifying code in target address 0x67cF62Da24c93142ebd8A43581c7fA28e677072E +0ms
 @openzeppelin:upgrades:core code in target address found 0x67cF62Da24c93142ebd8A43581c7fA28e677072E +224ms

Can I upgrade a contract without any changes to the storage layout and if yes, how?

:1234: Code to reproduce

This is the method I'm using to upgrade:

export const prepareUpgrade = async (
  hre: HardhatRuntimeEnvironment,
  proxyAddress: string,
  contractName: string,
  _args: string
) => {
  const { deployments, upgrades, ethers, getChainId } = hre
  const { save } = deployments

  console.log('Attempting upgrade on chainId:', await getChainId())
  const newImplFactory = await ethers.getContractFactory(contractName)

  // prepare upgrade
  const result = await upgrades.prepareUpgrade(proxyAddress, newImplFactory, {
    unsafeAllowRenames: true,
  })

  console.log(`Upgrade considered compatible with existing storage layout.`)

  const address = await upgrades.erc1967.getImplementationAddress(proxyAddress)
  console.log(`Using implementation at ${address}.`)

  const artifact = await deployments.getExtendedArtifact(contractName)
  let proxyDeployments = {
    address: proxyAddress,
    ...artifact,
  }

  await save(contractName, proxyDeployments)
  console.log(`Stored deployment information in deployments folder.`)
}

:computer: Environment

hardhat-defender: 1.6.0 (not used in the above example but also tried with it)
hardhat-upgrades: 1.15.0

Hi @cmdzro,

The implementation version string that you see in the debug logs (3ea64c77d47fa926979be270f802d38301853572d3d949a0f6cc8a3963452d27) is a hash of the contract's creation bytecode and is used to determine the contract's version.

Since a previous deployment was found with the same hash, this could mean that either the old contract is still being provided in your script, or the new contract implementation was already deployed and it is simply reusing it (perhaps it was deployed for a different proxy before -- deploying multiple proxies with the same implementation contract code would just use the same implementation contract address for all of those proxies).

So I would suggest to check the following:

  • whether const newImplFactory = await ethers.getContractFactory(contractName) is indeed the latest version of your contract implementation's code
  • whether 0x67cF62Da24c93142ebd8A43581c7fA28e677072E contains the bytecode of the latest version of your contract implementation. For example, if you deploy a new proxy using that implementation address, are you able to call the new methods?