使用Hardhat和OpenZeppelin创建可升级的智能合约


在开始的开始,首先要问一个问题: 为什么会出现可升级的智能合约

为什么会出现可升级的智能合约,在之前最初接触这个概念时,这是我脑海中第一个闪过的念头。区块链的精神不是去中心化,不可篡改吗。确实, 区块链的核心精神确实是不可篡改性,但是,设想如果有人在区块链上发布了一个智能合约,里面可能又数额巨大的金额记账,智能合约一旦部署在区块链上,其代码就会永久存在。但这也意味着如果代码中存在漏洞,攻击者可能会利用这些漏洞进行攻击。通过允许智能合约升级,可以及时修复这些漏洞,提升系统的安全性。随着时间的推移,应用需求和环境可能会发生变化。可升级的智能合约允许开发者根据新的需求和技术进步,添加新功能或改进现有功能,从而确保合约能够持续满足用户需求。

什么是可升级的智能合约

可升级的智能合约是一种设计模式,使得部署在区块链上的智能合约可以在不改变其地址的情况下进行修改和升级。简单讲,通过代理模式(Proxy Pattern),通过将逻辑拆分为代理合约(Proxy Contract),逻辑合约(Logic Contract),代理合约通过委托调用(delegatecall)将调用转发给逻辑合约,并使用自身的存储。这意味着即使逻辑合约更改了,存储仍然保持不变。
目前市场上运行的绝大多数项目都是使用的这种模式。是的,不得不遗憾的说,这确实表明,现在市面上发布的绝大多数合约依然是中心化的,但区别就是,在区块链逐渐发展的阶段,这是目前运行在分布式网络的记账体系中是目前最好的妥协产物,毕竟,记账是真实且可信的,变化的只是业务而已。

接下来就结合个人在开发中使用的技术栈(使用Hardhat和OpenZeppelin)来写一下创建代理合约的步骤

1. 首先安装环境。使用Hardhat和OpenZeppelin是运行于js环境的,先要Node.js和npm。接着,我们需要安装Hardhat,它是一个Ethereum开发环境,可以帮助开发者管理和自动化智能合约的开发工作。
mkdir my-upgradeable-contract
cd my-upgradeable-contract
npm init -y
npm install --save-dev hardhat

接下来,运行下面的命令来创建一个Hardhat项目:

npx hardhat

安装OpenZeppelin Contracts和OpenZeppelin Upgrades插件:

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

在Hardhat配置文件(hardhat.config.js)中引入Upgrades插件:

```cpp
require('@openzeppelin/hardhat-upgrades');

module.exports = {
  solidity: "0.8.9",
};
```
2. 编写可升级的合约

创建一个新的Solidity文件,命名为MyContract.sol,在contracts目录下,该合约从 OpenZeppelin 的 Initializable 类继承,这个基类为合约提供了必要的工具来安全地初始化状态变量。Initializable 提供了 initializer 修饰符,它确保初始化函数在合约的生命周期中只能被调用一次,防止初始化逻辑被重复执行。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

contract MyContract is Initializable {
uint256 public value;
// 初始化函数
function initialize(uint256 _value) public initializer {
value = _value;
}

function increment() public {
value += 1;
}
}
3. 部署脚本

在scripts目录下创建一个名为deploy.js的文件,用于部署代理合约:

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

async function main() {
  const MyContract = await ethers.getContractFactory("MyContract");
  const myContract = await upgrades.deployProxy(MyContract, [42], {
    initializer: 'initialize'
  });
  await myContract.deployed();

  console.log("MyContract deployed to:", myContract.address);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

开始部署:

npx hardhat run scripts/deploy.js --network localhost

注意,这个命令时将合约发布到本地链,如果没有跑链还得先跑一下本地测试链

npx hardhat node
4. 合约升级

若要升级合约,首先需修改MyContract.sol,添加新的功能或变更,然后编写一个新的部署脚本使用upgradeProxy来应用更新。抛开原理不谈,可以认为合约已经升级到最新代码

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

async function main() {
  const MyContractV2 = await ethers.getContractFactory("MyContract");
  const myContract = await upgrades.upgradeProxy(PROXY_ADDRESS, MyContractV2);
  console.log("MyContract upgraded");
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
5. 至此,一次完整的升级合约流程就走了一遍。
这种开发方式看起来很方便,理论上来说已经非常接近于传统服务器开发了,有bug,有功能更新可以很方便的通过合约升级来完成,但是有一些特备需要注意的点
  1. 初始化安全性不可忽略 也就是需要正确使用initializer修饰符:由于构造函数在代理模式下 不会被执行,所以初始化必须通过initialize函数来完成。确保使用Initializable基类中的initializer修饰符,防止初始化函数被多次调用。 多变量初始化:如果合约随着版本升级而引入新的状态变量,确保在新的初始化函数中合理设置它们的初始
  2. 数据处理记录仍然是永久存在的 在区块链上,所有的交易记录都是不可篡改和永久存储的。这意味着,即使智能合约的数据被更新或部分数据被删除,之前的所有交易记录(包括创建、修改和可能的删除操作)都仍然可以通过区块链浏览器查询到。虽然升级合约可以导致合约逻辑的改变,甚至可能涉及到数据的重新组织或删除,但这些操作本身也是一种交易,同样会被记录在区块链上。