合约的可升级性

智能合约在部署后,通常无法像传统软件一样直接进行修改或更新。这是因为区块链上的智能合约一旦被部署,就会被记录在区块链上,并且其代码是不可更改的。这种不可变性是区块链的一个重要特性,但也带来了一些问题,尤其是在智能合约的升级和维护方面。

具备可升级性的智能合约可以帮助开发者在合约的生命周期内进行必要的修改和优化,而不必担心修改后会破坏已有的协议或数据。具体来说,智能合约的可升级性主要有以下几个原因和优势:

  1. 修复漏洞和错误:智能合约在开发和部署后,可能会发现潜在的漏洞或缺陷,无法通过传统方式进行修复。如果合约没有可升级性,一旦发现问题,必须重新部署一个新的合约,用户可能还需要迁移数据和资金。具备可升级性的合约允许开发者修复漏洞,而不需要重新部署整个合约。
  2. 添加新功能或修改逻辑:随着业务需求变化或新技术的出现,智能合约的功能可能需要扩展或修改。如果合约不能升级,添加新功能可能需要完全重写合约,这不仅增加了复杂性,还可能导致新的安全风险。可升级合约支持在保持原有合约逻辑的基础上,灵活地添加或更新功能。
  3. 兼容性和用户体验:如果智能合约需要进行重大升级或修改,并且没有可升级性,用户可能需要手动迁移他们的资产或与新的合约进行交互,这会影响到用户体验和区块链的普及。通过设计可升级性,开发者可以保持合约的兼容性,让用户体验更加平滑,避免频繁的资产迁移。
  4. 符合监管和法律要求:区块链的监管环境可能会发生变化,可能需要合约根据新的法律法规进行调整。没有可升级性的合约无法进行这些调整,导致合约可能违反新的规定。具有可升级性的合约能更好地应对法律环境的变化。
  5. 提高合约的长期稳定性:区块链技术的演进速度很快,智能合约的设计可能随着技术的发展和社区的反馈而需要更新。通过在合约设计中引入可升级性,可以让智能合约在长期运行过程中更加稳定、可靠,避免因为技术迭代导致的过时和不兼容。

可升级合约开发

升级智能合约是一个多步骤且容易出错的过程,因此为了尽量减少人为错误的可能性,使用一个尽可能自动化该过程的工具是理想的。OpenZeppelin提供了一系列的插件,可以帮助开发者在Solidity智能合约中实现可升级性。下面以Foundry为例,介绍如何使用OpenZeppelin插件进行智能合约升级。

创建项目

# 使用vscode作为IDE,所以这里使用了 --vscode 参数
$ forge init foundry-upgrades --vscode && cd foundry-upgrades
# 安装依赖
$ forge install OpenZeppelin/openzeppelin-contracts-upgradeable
$ forge install OpenZeppelin/openzeppelin-foundry-upgrades
# for vscode
$ forge remappings > remappings.txt

在项目的配置文件中增加如下配置:

[profile.default]
src = "src"
out = "out"
libs = ["lib"]
# 新增
# 启用构建信息输出,生成包含详细编译器信息和合约源码的 build-info 文件,便于合约升级工具(例如 OpenZeppelin 的 @openzeppelin/upgrades-core)使用和验证。
build_info = true
# 启用存储布局的输出,帮助开发者分析合约存储变量的布局,特别是在合约升级时确保存储布局一致性,避免数据丢失。
extra_output = ["storageLayout"]
# 启用 Solidity 合约的抽象语法树(AST)输出,提供合约源码的结构化表示,帮助进行静态分析、代码优化和合约安全检查。
ast = true

创建合约

src目录下新增ContractA合约,作为我们的基础合约:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

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

contract ContractA is Initializable{
    uint256 public value;

    function initialize(uint256 _setValue) public initializer {
        value = _setValue;
    }
}

然后新增ContractB合约,用作升级使用:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

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

/// @custom:oz-upgrades-from ContractA
// 或者
/// @custom:oz-upgrades-from ./ContractA.sol:ContractA
contract ContractB is Initializable {
    uint256 public value;

    function initialize(uint256 _setValue) public initializer {
        value = _setValue;
    }

    function increaseValue() public {
        value += 10;
    }
}

/// @custom:oz-upgrades-from标签是OpenZeppelin Upgrades插件中的一个自定义注释,它用于指定合约的来源版本,确保新的逻辑合约与旧版本合约兼容。在实现可升级合约时,这个标签帮助工具识别合约之间的继承关系和存储结构的兼容性,从而支持平滑、安全的合约升级。

编写测试合约

下面需要对我们的合约进行测试,确保它们能正常运行。在test目录下新增Upgrades.t.sol测试合约:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "openzeppelin-foundry-upgrades/Upgrades.sol";
import "../src/ContractA.sol";
import "../src/ContractB.sol";

contract UpgradesTest is Test {
    function testTransparent() public {
        // 部署一个以 ContractA 作为实现的透明代理,并使用 10 作为 ContractA 的初始化参数
        address proxy = Upgrades.deployTransparentProxy(
            "ContractA.sol",
            msg.sender,
            abi.encodeCall(ContractA.initialize, (10))
        );

        // 获取合约实例
        ContractA instance = ContractA(proxy);

        // 获取代理的实现地址
        address implAddrV1 = Upgrades.getImplementationAddress(proxy);

        // 获取代理的 admin 地址
        address adminAddr = Upgrades.getAdminAddress(proxy);

        // 确保 admin 地址有效
        assertFalse(adminAddr == address(0));

        // 记录初始之
        console.log("----------------------------------");
        console.log("Value before upgrade --> ", instance.value());
        console.log("----------------------------------");

        // 验证是否符合预期
        assertEq(instance.value(), 10);

        // 将代理升级到 ContractB
        Upgrades.upgradeProxy(proxy, "ContractB.sol", "", msg.sender);

        // 获取升级后的新实现地址
        address implAddrV2 = Upgrades.getImplementationAddress(proxy);

        // 验证 admin 地址并未改变
        assertEq(Upgrades.getAdminAddress(proxy), adminAddr);

        // 验证实现地址发生了变化
        assertFalse(implAddrV1 == implAddrV2);

        // 调用 increaseValue
        ContractB(address(instance)).increaseValue();

        // 记录并验证升级后的新值
        console.log("----------------------------------");
        console.log("Value after upgrade --> ", instance.value());
        console.log("----------------------------------");
        assertEq(instance.value(), 20);
    }
}

编译测试

使用下面的命令进行验证测试:

$ forge clean && forge test -vvv --ffi --mt testTransparent
[⠊] Compiling...
[⠰] Compiling 63 files with Solc 0.8.28
[⠑] Solc 0.8.28 finished in 2.34s
Compiler run successful!

Ran 1 test for test/Upgrades.t.sol:UpgradesTest
[PASS] testTransparent() (gas: 42618471)
Logs:
  ----------------------------------
  Value before upgrade -->  10
  ----------------------------------
  ----------------------------------
  Value after upgrade -->  20
  ----------------------------------
  ----------------------------------
  Value after upgrade -->  30
  ----------------------------------

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1166.35s (1166.35s CPU time)

Ran 1 test suite in 1166.37s (1166.35s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

孟斯特

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
cnblogs: 恋水无意
腾讯云开发者社区:孟斯特