使用OpenZeppelin的Upgrades插件开发可升级的智能合约

一、原理

https://docs.openzeppelin.com/learn/upgrading-smart-contracts#how-upgrades-work

当创建一个可升级合约的时候,OpenZeppelin Upgrades Plugins实际部署了3个合约:

  1. 原始业务逻辑合约,也叫实现合约
  2. ProxyAdmin合约
  3. 提供给用户进行交互的Proxy合约,它是原始业务逻辑合约的代理,外部交互的地址和方法都由它提供。

代理合约负责委托调用实现合约。委托调用(delegate call)类似于常规调用,不同之处在于所有代码都在调用者的上下文中执行,而不是被调用者的上下文中。因此,实现合约代码中的转账实际上会转移代理合约的余额,任何对合约存储的读写操作都会从代理合约自身的存储中读取或写入。

这使我们能够将合约的状态和代码解耦:代理合约持有状态,而实现合约提供代码。这也使我们可以通过让代理合约委托给不同的实现合约来更改代码。

一次升级涉及以下步骤:

  1. 部署新的实现合约。

  2. 向代理合约发送一笔交易,将其实现地址更新为新的地址。

智能合约的任何用户始终与代理合约交互,代理合约的地址永远不会改变。这使你能够推出升级或修复漏洞,而无需请求用户在他们的端更改任何内容——他们只需继续与一如既往的地址进行交互。

你可以有多个代理合约来代理同一个实现合约,因此如果你计划部署多个相同合约的副本,使用这种模式可以节省Gas费用。

注:应该是一般实现合约的代码多,而代理合约结构更简单而代码少,所以可以节省部署时候的Gas费。

二、开发实践

cd upgrades-contract
npx hardhat init
npm install --save-dev @openzeppelin/hardhat-upgrades  //安装可升级插件
//hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
//require('@nomiclabs/hardhat-ethers');
require('@openzeppelin/hardhat-upgrades');
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.24",
};
(1)编写合约Box
// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Box {
    uint256 private _value;

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

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

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

测试一下

const main = async () => {
    const [owner, randomPerson] = await hre.ethers.getSigners();

    const contract = await hre.ethers.deployContract("Box");
    //const contract = await contractFactory.deploy();
    //await contract.deployed();
    console.log("合约部署到如下地址:", await contract.getAddress());
    console.log("合约部署人:", owner.address);

    let txn;
    let value = 42;

    txn = await contract.store(value);
    await txn.wait();
  
    value = await contract.retrieve();
    console.log("合约内存储的值是:", value);

};

const runMain = async () => {
    try{
        await main();
        process.exit(0);
    }catch(error){
        console.log(error);
        process.exit(1);
    }
};

runMain();

npx hardhat compile编译后,执行npx hardhat run run.js输出:

npx hardhat run .\ignition\modules\run.js
合约部署到如下地址: 0x5FbDB2315678afecb367f032d93F642f64180aa3
合约部署人: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
合约内存储的值是: 42n
(2)把Box合约变成可升级合约

只需要在部署的时候用upgrades插件进行deployProxy部署,并在后续升级的时候用这个插件将新版本合约代码upgradeProxy升级上去即可。

使用upgrades.deployProxy部署Box合约:

const { ethers, upgrades } = require('hardhat');

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

main();

npx hardhat run .\ignition\modules\deploy.js --network localhost
Deploying Box...
Box deployed to: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512

在这之前要先把本地网络启动起来, npx hardhat node

编写v2版本合约:

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

contract BoxV2 {
    uint256 private _value;

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

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

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

    function increment() public {
        _value = _value + 1;
        emit ValueChanged(_value);
    }
}

把本地网络里的合约用v2版本进行升级:

const { ethers, upgrades } = require('hardhat');

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

main();

npx hardhat run .\ignition\modules\upgradeV2.js --network localhost
Compiled 1 Solidity file successfully (evm target: paris).
Upgrading Box...
Box upgraded

然后再次进行测试:这里直接使用npx hardhat console进入命令行交互,直接写脚本。

npx hardhat console --network localhost
Welcome to Node.js v20.9.0.
Type ".help" for more information.
> const BoxV2 = await ethers.getContractFactory('BoxV2');
undefined

> const box = await BoxV2.attach('0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512');
undefined

> await box.increment();
ContractTransactionResponse {
  provider: HardhatEthersProvider {
    _hardhatProvider: LazyInitializationProviderAdapter {
      _providerFactory: [AsyncFunction (anonymous)],
      _emitter: [EventEmitter],
      _initializingPromise: [Promise],
      provider: [BackwardsCompatibilityProviderAdapter]
    },
    _networkName: 'localhost',
    _blockListeners: [],
    _transactionHashListeners: Map(0) {},
    _eventListeners: []
  },
  blockNumber: 5,
  blockHash: '0x53cde3207dbcc1762000a0d2e7457b7d6e21439dbfb57274473d4c644afede67',
  index: undefined,
  hash: '0xe4f86449d85b0314025e96ae6565c1c0080c33422808cae9a0014689656720ba',
  type: 2,
  to: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',
  from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
  nonce: 4,
  gasLimit: 30000000n,
  gasPrice: 655623046n,
  maxPriorityFeePerGas: 157200340n,
  maxFeePerGas: 655623046n,
  maxFeePerBlobGas: null,
  data: '0xd09de08a',
  value: 0n,
  chainId: 31337n,
  signature: Signature { r: "0x57c07d987a9df1231142836b7e9d2eccd01f30e1561a4ee61d39b1f4e4fc4965", s: "0x447d9f942add1c47bf97f67ca8e129e2aba5f2f026c0d64f617f40ce963918af", yParity: 0, networkV: null },
  accessList: [],
  blobVersionedHashes: null
}


> (await box.retrieve()).toString();
'43'

可以看到,0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512这个合约保留了合约地址和状态变量的值,事实上合约的balance也会保留。

三、智能合约升级的限制

1、Initializable

可升级合约不能有constructor,通过让业务合约继承Initializable ,然后在里边用initializer 标记一个方法,使之成为初始化方法。

另外,会留一个空constructor,如下, 为了安全目的:

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

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract AdminBox is Initializable {
    uint256 private _value;
    address private _admin;

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

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

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

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

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

详见 https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable

2、升级无法改变合约存储布局

由于技术限制,把合约升级到新版本后,无法删除已有的状态变量、或修改其类型、或在这个环境变量之前插入新的环境变量。

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

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

    // ...
}

we can only add new state variables owner after value

这种存储布局的升级限制只影响状态变量,不影响function和event

详见 https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#modifying-your-contracts

@问题:

好像还是对hardhat里边的一些内置对象使用不是很清楚,比如:

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();

ethers, upgrades这俩对象怎么require出来的,然后await box.deployed()为啥报错提示deployed()方法不存在。

是不是跟依赖安装冲突有关, hardhat-ethers 这个插件我没装进去,npm进行安装的时候以及hardhat.config.js里边没有import

参考

https://docs.openzeppelin.com/learn/upgrading-smart-contracts

https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable

https://hardhat.org/tutorial/testing-contracts

关于nodejs的require

https://stackoverflow.com/questions/9901082/what-is-require-in-javascript-and-nodejs
https://medium.com/@mtorre4580/understanding-require-function-node-js-bbda09952ded

posted on 2024-06-20 17:53  肥兔子爱豆畜子  阅读(33)  评论(0编辑  收藏  举报

导航