Hardhat-deploy: Question on deploying Upgradable contracts

Created on 8 Jun 2021  ·  9Comments  ·  Source: wighawag/hardhat-deploy

What is the correct way to deploy OpenZeppelin upgradable contracts?
OpenZeppelin upgrdable have default initialize() function which is called by the hardhat upgrades plugin.
There is no automatic call from hardhat-deploy, only post-upgrade method which may be called.

Should I deploy the contracts first through the scripts in the deploy folder and then call initialize functions from some script outside deploy folder? Or is there a way to incorporate calling initialize() function from the deploy folder itself?

Most helpful comment

Re initialise function for proxies

I added a new options (instead of methodName that is still available) in hardhat-deploy 0.8.0

see https://github.com/wighawag/template-ethereum-contracts/blob/595c1b5ec9cdf1276f4d3a43b4825bcef78bd2cd/deploy/004_deploy_erc20_always_proxied_via_openzeppelin_proxy.ts#L12-L26

Let me know if you hit any issues

All 9 comments

The way hardhat-deploy consider deploy script by default is that they are idempotent. The same applies to proxy deployment. This make it nice for development/deployment as you only need to think about what state you want to be and hardhat-deploy will make everything to reach that state,

so to answer your question, what I usually do is to make my initialise function idempotent too so instead of throw it simply skip if it is already initialised. I can thus add that function as the postUpgrade function and all work fine. In development I can even change the initialise function to do things when I upgrade if needed.

For more complex scenario you can organise each upgrade in separate deploy script.

There is an undocumented feature that is subject to changes: the upgradeIndex parameter that let you specify the deploy script that need to be executed in order.

This is great for test as you can test yoru proxy at any point between the upgrade history and ensure all works.

Thanks for your quick reply.
It seems I managed to make all working smoothly. I use deploy() and then execute with initialize function of OpenZeppelin Initializable base contract. The execute() returns true and this makes it not to run when I upgrade the contracts:

// deploy/01_deploy_skin_rewards.js

module.exports = async ({
  getNamedAccounts,
  deployments,
}) => {
  const {deploy} = deployments;
  const {deployer} = await getNamedAccounts();
  await deploy('SkinRewards', {
    from: deployer,
    proxy: {
      owner: deployer,
      proxyContract: 'OpenZeppelinTransparentProxy',
    },
    args: [],
    log: true,
  });
};
module.exports.tags = ['SkinRewards'];

and then

// deploy/02_init_skin_rewards.js

require('dotenv').config();
const RewardPeriods = ethers.BigNumber.from(process.env.REWARD_PERIODS);
module.exports = async ({getNamedAccounts, deployments}) => {
  const { deployer } = await getNamedAccounts();
  const SnookGame = await deployments.get('SnookGame');
  const SnookState = await deployments.get('SnookState');
  const SnookToken = await deployments.get('SnookToken');
  const SkillToken = await deployments.get('SkillToken');
  const Afterdeath = await deployments.get('Afterdeath');
  const Treasury = await deployments.get('Treasury');

  await deployments.execute(
    'SkinRewards',
    {from:deployer},
    'initialize',
    RewardPeriods,
    Treasury.address,
    SnookGame.address,
    SnookState.address,
    SnookToken.address,
    SkillToken.address,
    Afterdeath.address
  );
  deployments.log('Initialized SkinRewards');
  return true;
};
module.exports.tags = ['initSkinRewards'];
module.exports.id = 'initSkinRewards';

Can this method be considered _good practice_ ? Is it how you sought it?

As I mentioned above, in SkinRewards I inherit from OpenZeppelin's Initializable contract which defines initialize() function to be called first. So I don't see how I can run it postupgrade. Can you explain? Thanks

That sounds good, the disadvantage compared to using methodName ("postUpgrade") is that you are executing 2 tx instead of 1

If you make your initialise function idempotent, that is it can be called multiple time without throwing by making sure further call will not modify state (by simply skipping if already initialised), then you can use it via the methodName options. the first deployment will execute it too.

Note, because you use arguments and the deploy function is setup so that you can simply switch off the proxy option and have a working immutable contract, the methodName option use the args options for argument.
This means the constructor of the proxy implementation also need to have these arguments. If you never intend to use the contract without proxy, you can just do nothing with the arguments.

Actually for convenience if the implementation contract have a zero arg construtcor it ignore the constructor entirely but I am not sure it was a good idea.

Here would be the script with the methodName option:

// deploy/01_deploy_skin_rewards.js

module.exports = async ({
  getNamedAccounts,
  deployments,
}) => {
  const {deploy} = deployments;
  const {deployer} = await getNamedAccounts();
  await deploy('SkinRewards', {
    from: deployer,
    proxy: {
      methodName: 'initialize`,
      owner: deployer,
      proxyContract: 'OpenZeppelinTransparentProxy',
    },
    args: [
      RewardPeriods,
      Treasury.address,
      SnookGame.address,
      SnookState.address,
      SnookToken.address,
      SkillToken.address,
      Afterdeath.address
    ],
    log: true,
  });
};
module.exports.tags = ['SkinRewards'];

_If you make your initialise function idempotent, that is it can be called multiple time without throwing by making sure further call will not modify state (by simply skipping if already initialised), then you can use it via the methodName options. the first deployment will execute it too._

This means that we cannot use initialize() function AS-IS from https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/release-v4.0/contracts/proxy/utils/Initializable.sol .

@freebyte indeed

I ll consider adding an option to get this kind of behavior

Great, thank you!

Another question is about using deployed contracts. Particularly, I'm asking regarging Uniswap.

From this part of the plugin docs I don't see how I supply the address of the deployed Uniswap contract. Uniswap NPM comes with build folder with the artifacts but how do I make the plugin to know about the contract at specific address? We don't want to use ethers in the deployment script, right?

The following code is NOT what we want in the deploy script:

const { ethers } = require("hardhat");

const UniswapV2FactoryArtifact = require('@uniswap/v2-core/build/UniswapV2Factory.json');
const UniswapV2FactoryAddress = '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f';

module.exports = async ({getNamedAccounts, deployments}) => {
  const {deployer} = await getNamedAccounts();
  const UniswapFactory = await ethers.getContractAt(UniswapV2FactoryArtifact.abi, UniswapV2FactoryAddress);
// continue...

How do I get Uniswap deployment into deployments var?

for already deployed contracts the best is to create files for them in the corresponding deployments folder
so for mainnet uniswap and assuming you have a mainnet network confirgured in hardhat.config.js, you can create the file "deployments/mainnet/UniswapFactory.json". At minimum it needs an address and abi field

then you can access it via deployments when on that network

The doc link you point to allow you to access artifacts (so the uniswap build folder) or deploy script, but the latter works only if the project in question exposed these deploy script somewhere

I do that in my own project like https://github.com/wighawag/universal-forwarder which allow user to simply install the npm package and they get the whole project deployed in their test or on their own network. But uniswap does not provide that

Great. Thanks alot.

Re initialise function for proxies

I added a new options (instead of methodName that is still available) in hardhat-deploy 0.8.0

see https://github.com/wighawag/template-ethereum-contracts/blob/595c1b5ec9cdf1276f4d3a43b4825bcef78bd2cd/deploy/004_deploy_erc20_always_proxied_via_openzeppelin_proxy.ts#L12-L26

Let me know if you hit any issues

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tennox picture tennox  ·  4Comments

smartcontracts picture smartcontracts  ·  20Comments

jsidorenko picture jsidorenko  ·  3Comments

gitpusha picture gitpusha  ·  6Comments

lepidotteri picture lepidotteri  ·  5Comments