Hardhat-deploy: Using Open Zeppelin Upgrade Plugin

Created on 14 Mar 2021  ·  2Comments  ·  Source: wighawag/hardhat-deploy

Hello,

I found that the creator of this package has already looked into integrating hardhat-deploy with the open zeppelin upgrades plugin for hardhat but would also like to echo that this addition would be great.

Currently we are using the oz upgrades plugin and while we'd like to also use the hardhat-deploy plugin, we don't want to sacrifice things that the oz plugin offer us.

enhancement

Most helpful comment

Hello @Remscar, it would definitely be great.
Meanwhile, I developed a "workaround" plugin to do it, I copied some code from @openzeppelin/upgrade-core library.
It's a bit ugly but it can do the work ( use it at your own risk ).

Import this file ( after importing hardhat-deploy ) in the hardhat-config.js :

const { extendEnvironment } = require("hardhat/config")
const AdminUpgradeabilityProxy = require("@openzeppelin/upgrades-core/artifacts/contracts/proxy/AdminUpgradeabilityProxy.sol/AdminUpgradeabilityProxy.json")
const ProxyAdmin = require("@openzeppelin/upgrades-core/artifacts/contracts/proxy/ProxyAdmin.sol/ProxyAdmin.json")
const { assertUpgradeSafe, getVersion, getUnlinkedBytecode } = require("@openzeppelin/upgrades-core")
const { readValidations } = require("@openzeppelin/hardhat-upgrades/dist/validations")

const getSigner = async address => {
  const signers = await hre.ethers.getSigners()
  return signers.find(signer => signer.address === address)
}

const getInitializerData = (ImplFactory, args = [], initializer) => {
  if (initializer === false) {
    return "0x"
  }
  const allowNoInitialization = initializer === undefined && args.length === 0
  initializer = initializer ?? "initialize"
  try {
    const fragment = ImplFactory.interface.getFunction(initializer)
    return ImplFactory.interface.encodeFunctionData(fragment, args)
  } catch (e) {
    if (e instanceof Error) {
      if (allowNoInitialization && e.message.includes("no matching function")) {
        return "0x"
      }
    }
    throw e
  }
}

const deployProxyAdmin = async (owner) => {
  const { deployments } = hre
  const { deploy } = deployments
  return await deploy("ProxyAdmin", {
    contract: ProxyAdmin,
    from: owner,
    log: true,
  })
}

const deployOrUpgrade = async (firstImplName, opts, { initializer, postUpgrade, upgrades }) => {
  const { deployments } = hre
  const { deploy } = deployments
  let proxyAdmin
  try {
    proxyAdmin = await deployments.get("ProxyAdmin")
  } catch (error) {
    proxyAdmin = await deployProxyAdmin(opts.from)
  }
  const proxyName = `${firstImplName}Proxy`
  const firstImpl = await deploy(firstImplName, opts)
  const initData = getInitializerData(
    await ethers.getContractFactory(firstImplName),
    initializer && initializer.args ? initializer.args : [],
    initializer ? initializer.method : false
  )
  const proxy = await deploy(proxyName, {
    contract: AdminUpgradeabilityProxy,
    from: opts.from,
    log: true,
    args: [firstImpl.address, proxyAdmin.address, initData],
  })

  if (upgrades && upgrades.length > 0) {
    let previousImplName, newImplName
    if (upgrades.length === 1) {
      previousImplName = firstImplName
      newImplName = upgrades[0]
    } else {
      newImplName = upgrades.pop()
      previousImplName = upgrades.pop()
      for (oldUpgrade in upgrades) {
        // unsure previous upgrades exists
        await deployments.get(upgrades[oldUpgrade])
      }
    }
    if (previousImplName === newImplName) throw new Error("Same implementation, can't upgrade.")
    const newImplFactory = await ethers.getContractFactory(newImplName)
    const validations = await readValidations(hre)
    const unlinkedBytecode = getUnlinkedBytecode(validations, newImplFactory.bytecode)
    const version = getVersion(unlinkedBytecode, newImplFactory.bytecode)
    assertUpgradeSafe(validations, version, {
      unsafeAllowCustomTypes: false,
      unsafeAllowLinkedLibraries: false,
    })

    const signer = await getSigner(opts.from)
    const proxyAdminContract = await ethers.getContractAt(proxyAdmin.abi, proxyAdmin.address, signer)
    const previousImpl = await deployments.get(previousImplName)
    const actualImpl = await proxyAdminContract.getProxyImplementation(proxy.address)
    const newImpl = await deploy(newImplName, {
      from: opts.from,
      log: true,
    })
    if (newImpl.newlyDeployed)
      if (actualImpl == previousImpl.address) {
        console.log(`Upgrading from ${previousImplName} to ${newImplName}`)
        if (postUpgrade && postUpgrade.method && postUpgrade.args) {
          const upgradeData = getInitializerData(await ethers.getContractFactory(newImplName), postUpgrade.args, postUpgrade.method)
          await proxyAdminContract.upgradeAndCall(proxy.address, newImpl.address, upgradeData)
        } else await proxyAdminContract.upgrade(proxy.address, newImpl.address)
      } else throw new Error(`Proxy is actually pointing on: ${actualImpl}`)
  }
  return proxy
}

extendEnvironment(hre => {
  hre.myPlugin = {
    deployOrUpgrade,
  }
})

Usage :

module.exports = async ({ getNamedAccounts, myPlugin }) => {
  const { deployer } = await getNamedAccounts()
  // This will deploy the "Contract" with a proxy and  a proxyAdmin if it doesn't exist.
  const proxy = await myPlugin.deployOrUpgrade(
    "Contract",
    { from: deployer, log: true },
    {
      initializer: { method: "initialize", args: [] },
      // bellow attributes for upgrading
      postUpgrade: { method: "postUpgrade", args: [] }, // method to  exec after upgrade
      upgrades: ["ContractV2", "ContractV3"], // you should keep the list of upgrades and add the new one at  the last
    }
  )
}

All 2 comments

Hello @Remscar, it would definitely be great.
Meanwhile, I developed a "workaround" plugin to do it, I copied some code from @openzeppelin/upgrade-core library.
It's a bit ugly but it can do the work ( use it at your own risk ).

Import this file ( after importing hardhat-deploy ) in the hardhat-config.js :

const { extendEnvironment } = require("hardhat/config")
const AdminUpgradeabilityProxy = require("@openzeppelin/upgrades-core/artifacts/contracts/proxy/AdminUpgradeabilityProxy.sol/AdminUpgradeabilityProxy.json")
const ProxyAdmin = require("@openzeppelin/upgrades-core/artifacts/contracts/proxy/ProxyAdmin.sol/ProxyAdmin.json")
const { assertUpgradeSafe, getVersion, getUnlinkedBytecode } = require("@openzeppelin/upgrades-core")
const { readValidations } = require("@openzeppelin/hardhat-upgrades/dist/validations")

const getSigner = async address => {
  const signers = await hre.ethers.getSigners()
  return signers.find(signer => signer.address === address)
}

const getInitializerData = (ImplFactory, args = [], initializer) => {
  if (initializer === false) {
    return "0x"
  }
  const allowNoInitialization = initializer === undefined && args.length === 0
  initializer = initializer ?? "initialize"
  try {
    const fragment = ImplFactory.interface.getFunction(initializer)
    return ImplFactory.interface.encodeFunctionData(fragment, args)
  } catch (e) {
    if (e instanceof Error) {
      if (allowNoInitialization && e.message.includes("no matching function")) {
        return "0x"
      }
    }
    throw e
  }
}

const deployProxyAdmin = async (owner) => {
  const { deployments } = hre
  const { deploy } = deployments
  return await deploy("ProxyAdmin", {
    contract: ProxyAdmin,
    from: owner,
    log: true,
  })
}

const deployOrUpgrade = async (firstImplName, opts, { initializer, postUpgrade, upgrades }) => {
  const { deployments } = hre
  const { deploy } = deployments
  let proxyAdmin
  try {
    proxyAdmin = await deployments.get("ProxyAdmin")
  } catch (error) {
    proxyAdmin = await deployProxyAdmin(opts.from)
  }
  const proxyName = `${firstImplName}Proxy`
  const firstImpl = await deploy(firstImplName, opts)
  const initData = getInitializerData(
    await ethers.getContractFactory(firstImplName),
    initializer && initializer.args ? initializer.args : [],
    initializer ? initializer.method : false
  )
  const proxy = await deploy(proxyName, {
    contract: AdminUpgradeabilityProxy,
    from: opts.from,
    log: true,
    args: [firstImpl.address, proxyAdmin.address, initData],
  })

  if (upgrades && upgrades.length > 0) {
    let previousImplName, newImplName
    if (upgrades.length === 1) {
      previousImplName = firstImplName
      newImplName = upgrades[0]
    } else {
      newImplName = upgrades.pop()
      previousImplName = upgrades.pop()
      for (oldUpgrade in upgrades) {
        // unsure previous upgrades exists
        await deployments.get(upgrades[oldUpgrade])
      }
    }
    if (previousImplName === newImplName) throw new Error("Same implementation, can't upgrade.")
    const newImplFactory = await ethers.getContractFactory(newImplName)
    const validations = await readValidations(hre)
    const unlinkedBytecode = getUnlinkedBytecode(validations, newImplFactory.bytecode)
    const version = getVersion(unlinkedBytecode, newImplFactory.bytecode)
    assertUpgradeSafe(validations, version, {
      unsafeAllowCustomTypes: false,
      unsafeAllowLinkedLibraries: false,
    })

    const signer = await getSigner(opts.from)
    const proxyAdminContract = await ethers.getContractAt(proxyAdmin.abi, proxyAdmin.address, signer)
    const previousImpl = await deployments.get(previousImplName)
    const actualImpl = await proxyAdminContract.getProxyImplementation(proxy.address)
    const newImpl = await deploy(newImplName, {
      from: opts.from,
      log: true,
    })
    if (newImpl.newlyDeployed)
      if (actualImpl == previousImpl.address) {
        console.log(`Upgrading from ${previousImplName} to ${newImplName}`)
        if (postUpgrade && postUpgrade.method && postUpgrade.args) {
          const upgradeData = getInitializerData(await ethers.getContractFactory(newImplName), postUpgrade.args, postUpgrade.method)
          await proxyAdminContract.upgradeAndCall(proxy.address, newImpl.address, upgradeData)
        } else await proxyAdminContract.upgrade(proxy.address, newImpl.address)
      } else throw new Error(`Proxy is actually pointing on: ${actualImpl}`)
  }
  return proxy
}

extendEnvironment(hre => {
  hre.myPlugin = {
    deployOrUpgrade,
  }
})

Usage :

module.exports = async ({ getNamedAccounts, myPlugin }) => {
  const { deployer } = await getNamedAccounts()
  // This will deploy the "Contract" with a proxy and  a proxyAdmin if it doesn't exist.
  const proxy = await myPlugin.deployOrUpgrade(
    "Contract",
    { from: deployer, log: true },
    {
      initializer: { method: "initialize", args: [] },
      // bellow attributes for upgrading
      postUpgrade: { method: "postUpgrade", args: [] }, // method to  exec after upgrade
      upgrades: ["ContractV2", "ContractV3"], // you should keep the list of upgrades and add the new one at  the last
    }
  )
}

There is now a builtin way to use Openzeppelin Transparent Proxy. I ll close this issue

Was this page helpful?
0 / 5 - 0 ratings