Upgrade smart contract (Hardhat)

Original link

use OpenZeppelin upgrade plug-in The deployed smart contract can modify the code by upgrading, while retaining the original contract address, status and balance. This helps us add new features to the project or fix any errors that may be found in production.

In this guide, we will learn:

  • Why is upgrading important

  • Use the upgrade plug-in to upgrade our box.

  • Learn how upgrades work under the hood

  • Learn how to write upgradeable contracts

What is a scalable contract

Smart contracts in Ethereum cannot be changed by default. Once created, it cannot be changed, effectively playing the role of tamper proof contract for contract participants.

However, in some scenarios, we want to be able to modify them. Think of a traditional contract: if both parties agree to change it, they can align and change it. Similarly, on Ethereum, we also hope to modify the smart contract to fix the bug s they found (which may even lead hackers to steal their funds!), Add additional functionality, or just change the rules it executes.

Here's what you need to do to fix errors in contracts you can't upgrade.

  1. Deploy a new version of the contract

  2. Manually migrate all States from the old contract to the new contract (this can be a very expensive gas cost!)

  3. Update all contracts interacting with the old contract and use the address of the new contract

  4. Contact all your users and persuade them to start using the new deployment (and deal with the problem of using both contracts at the same time, because the user migration is slow)

To avoid this mess, we built the contract upgrade directly into our plug-in. This allows us to change the contract code while retaining the status, balance and address. Let's see how to implement it.

Use the upgrade plug-in to upgrade the contract

use OpenZeppelin upgrade plug-in When a new contract is deployed by the deployProxy in, the contract instance can realize the upgradeable function. By default, only the address of the initial deployment contract has permission to perform the upgrade operation.

deployProxy will create the following transactions:;

  1. Deployment execution contract (our Box contract)

  2. Deploy ProxyAdmin contract (agent's administrator)

  3. Deploy the agent contract and run the initialization function

Let's see how it works by deploying an upgradeable version of our Box contract using Previous deployment The same settings when:

// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Box {
    uint256 private value;

    // Emitted when the stored value changes
    event ValueChanged(uint256 newValue);

    // Stores a new value in the contract
    function store(uint256 newValue) public {
        value = newValue;
        emit ValueChanged(newValue);
    }

    // Reads the last stored value
    function retrieve() public view returns (uint256) {
        return value;
    }
}

First, you need to install the Upgrades Plugin.

install Hardhat Upgrades plug-in unit.

npm install --save-dev @openzeppelin/hardhat-upgrades

We need to configure Hardhat to use our @ openzeppelin / Hardhat upgrades plug-in. This can be done in Hardhat config. JS file to add the plug-in.

// hardhat.config.js
require('@nomiclabs/hardhat-ethers');
require('@openzeppelin/hardhat-upgrades');

module.exports = {
...
};

In order to upgrade a contract like Box, we need to first deploy it as an upgradeable contract, which is different from the deployment process we saw before. Initialize the Box contract by calling store, with a value of 42.

Hardhat currently does not have a native deployment system, so it needs to be used script To deploy the contract.

Create a script using deployProxy Deploy scalable Box contracts. Save the file as scripts/deploy_upgradeable_box.js.

// scripts/deploy_upgradeable_box.js
const { ethers, upgrades } = require("hardhat");

async function main() {
  const Box = await ethers.getContractFactory("Box");
  console.log("Deploying Box...");
  const box = await upgrades.deployProxy(Box, [42], { initializer: 'store' });
  await box.deployed();
  console.log("Box deployed to:", box.address);
}

main();

Now we can deploy our scalable contract.

Using the run command, you can deploy the Box contract to the development network.

$ npx hardhat run --network localhost scripts/deploy_upgradeable_box.js
All contracts have already been compiled, skipping compilation.
Deploying Box...
Box deployed to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0

We can retrieve the value stored during initialization through the Box contract.

We use Hardhat console To interact with the upgrade contract Box.

We need to specify the address of the proxy contract when deploying the Box contract.

$ npx hardhat console --network localhost
All contracts have already been compiled, skipping compilation.
> const Box = await ethers.getContractFactory("Box")
undefined
> const box = await Box.attach("0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0")
undefined
> (await box.retrieve()).toString()
'42'

For example, suppose we want to add a new function: create a self increasing function in the new Box and add one to the stored value.

// contracts/BoxV2.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract BoxV2 {
    // ... code from Box.sol

    // Increments the stored value by 1
    function increment() public {
        value = value + 1;
        emit ValueChanged(value);
    }
}

After creating the Solidity file, we now use the upgradeProxy function to upgrade the previously deployed instance.

upgradeProxy will create the following transactions:

  1. Deployment execution contract (our BoxV2 contract)

  2. Call ProxyAdmin to update the proxy contract to apply the new implementation

Create a script using upgradeProxy Upgrade the # Box # contract to use # BoxV2. Save this file as scripts/upgrade_box.js. You need to specify the proxy contract address when deploying the Box contract.

// scripts/upgrade_box.js
const { ethers, upgrades } = require("hardhat");

async function main() {
  const BoxV2 = await ethers.getContractFactory("BoxV2");
  console.log("Upgrading Box...");
  const box = await upgrades.upgradeProxy("0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", BoxV2);
  console.log("Box upgraded");
}

main();

Then we can deploy our scalable contract.

Using the run command, you can deploy the upgrade Box contract in the development network.

$ npx hardhat run --network localhost scripts/upgrade_box.js
All contracts have already been compiled, skipping compilation.
Box deployed to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0

Done! Our Box instance has been upgraded to the latest version of code while maintaining its state and previous address. We do not need to deploy a new contract in the new address, nor do we need to manually copy the value of the old Box to the new Box.

Try it by calling the new increment function and checking the value.

You need to specify the proxy contract address when we deploy the Box contract.

$ npx hardhat console --network localhost
All contracts have already been compiled, skipping compilation.
> const BoxV2 = await ethers.getContractFactory("BoxV2")
undefined
> const box = await BoxV2.attach("0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0")
undefined
> await box.increment()
...
> (await box.retrieve()).toString()
'43'

this is it! Please note that during the whole upgrade process, the value of Box and its address are saved. And whether you work on the local blockchain, the test network or the main network, the process is the same.

Let's see OpenZeppelin upgrade plug-in How to achieve it.

How does the upgrade work

This section will be more theoretical than other chapters: you can skip it and come back if you are interested.

When creating a new instance of an upgradeable contract, OpenZeppelin upgrade plug-in Three contracts were actually deployed.

  1. The contract you write is the so-called logical contract implementation.

  2. A ProxyAdmin as the administrator of the agent.

  3. An agent that points to the implementation of the contract, that is, the contract you actually interact with.

Here, the agent is a simple contract, which just delegates all calls to an implementation contract* delegate call * is similar to an ordinary call, except that all code is executed in the context of the caller rather than the context of the callee. Because of this, the transfer in the code executing the contract will actually transfer the balance of the transfer process, and any read or write to the contract storage will be read or written from the agent's own storage.

This allows us to decouple the state of the contract from the code: the agent holds the state and the implementation contract provides the code. And it also allows us to change the code by simply delegating the agent to different implementation contracts.

Upgrading includes the following steps.

  1. Deploy new implementation contracts

  2. Send a transaction to the agent and update its implementation address to the new implementation address.

Note: you can let multiple agents use the same implementation contract, so if you plan to deploy multiple copies of the same contract, you can use this mode to save gas.

The user of the smart contract always interacts with the agent, and the agent will never change its address. This allows you to roll out upgrades or fix errors without requiring users to change anything at their end - they just interact with the same address as usual.

Note: if you want to learn more about how the OpenZeppelin agent works, please check out Proxies.

Limitations of scalable contracts

Although any smart contract can be upgraded, some limitations of the solid language need to be addressed. These problems occur when writing the initial version of the contract and when I upgrade to a new version.

initialization

Upgradeable contracts cannot have constructor s. To help you initialize the code OpenZeppelin Contracts Provided Initializable Base contract, by adding initializer Tag to ensure that it is initialized only once.

For example, we write a new version of the Box contract through the initializer, and set an admin as the only address that can modify the content.

// contracts/AdminBox.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/contracts/proxy/Initializable.sol";

contract AdminBox is Initializable {
    uint256 private value;
    address private admin;

    // Emitted when the stored value changes
    event ValueChanged(uint256 newValue);

    function initialize(address _admin) public initializer {
        admin = _admin;
    }

    // Stores a new value in the contract
    function store(uint256 newValue) public {
        require(msg.sender == admin, "AdminBox: not admin");
        value = newValue;
        emit ValueChanged(newValue);
    }

    // Reads the last stored value
    function retrieve() public view returns (uint256) {
        return value;
    }
}

When deploying the contract, we need to specify the initializer function name (only if the name is not initialize) and provide an administrator address.

// scripts/deploy_upgradeable_adminbox.js
const { ethers, upgrades } = require("hardhat");

async function main() {
  const AdminBox = await ethers.getContractFactory("AdminBox");
  console.log("Deploying AdminBox...");
  const adminBox = await upgrades.deployProxy(AdminBox, ['0xACa94ef8bD5ffEE41947b4585a84BdA5a3d3DA6E'], { initializer: 'initialize' });
  await adminBox.deployed();
  console.log("AdminBox deployed to:", adminBox.address);
}

main();

For practical purposes, initializer acts as a constructor. However, remember that since it is a regular function, you will need to manually call the initial initializer (if any) of all base contract s.

To learn more about this and other considerations when writing scalable contracts, check out our Writing Upgradeable Contracts Guide.

upgrade

Due to technical limitations, when you upgrade a contract to a new version, you cannot change the storage layout of the contract.

This means that if you have declared a state variable in the contract, you cannot delete it, change its type, or declare other variables before it. In our Box example, this means that we can only add new state variables after value.

// contracts/Box.sol
contract Box {
    uint256 private value;

    // We can safely add a new variable after the ones we had declared
    address private owner;

    // ...
}

Fortunately, this restriction only affects state variables. You can change the functions and events of the contract at will.

Note: if you accidentally mess up the storage layout of the contract, the upgrade plug-in will warn you when trying to upgrade.

go to Modifying Your Contracts Guide for more restrictions.

test

In order to test scalable contracts, we should create unit tests for implementing contracts and create higher-level tests to test the interaction with agents. deployProxy can be used in the test, just like when we deploy.

When upgrading, we should create unit tests for the new implementation contract and create higher-level tests at the same time, so that after upgrading, we can use {upgradeProxy} to interact through proxy tests to check whether the status is consistent during the upgrading process.

Next steps

Now you know how to Upgrade smart contract , and you can iteratively develop your project. It's time to bring your project to Test network and Official network In the middle. You can rest assured that if a bug occurs, you have tools to modify your contract and fix it.

Keywords: Blockchain

Added by furtivefelon on Sat, 15 Jan 2022 19:42:03 +0200