在Solidity中,通过代理模式来升级智能合约是一种常见且有效的做法,它允许在不中断现有合约功能的情况下进行更新。这种模式的基本思路是将合约的状态和主要逻辑分离,使得可以在一个新的合约中部署更新的逻辑,然后通过一个代理合约来调用新的逻辑,从而达到升级的目的。

1. 初版

首先,假设有一个初始版本的智能合约(称为实现合约),包含状态变量和主要的业务逻辑。

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.8.2 <0.9.0;

// 初始版本的合约
contract MyContract {
    uint public data;

    function setData(uint _data) public {
        data = _data;
    }

    function getData() public view returns (uint) {
        return data;
    }
}

2. 升级版本

然后,创建一个新的版本的合约,它包含新的逻辑或修复。

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.8.2 <0.9.0;

// 升级后的合约版本
contract MyContractV2 {
    uint public data;
    mapping(address => bool) public accessAllowed;

    function setData(uint _data) public {
        require(accessAllowed[msg.sender], "Access not allowed");
        data = _data;
    }

    function getData() public view returns (uint) {
        return data;
    }

    function grantAccess(address _addr) public {
        accessAllowed[_addr] = true;
    }

    function revokeAccess(address _addr) public {
        accessAllowed[_addr] = false;
    }
}

3. 代理合约

创建一个代理合约,用于转发调用到实际的合约实现。代理合约通常保持与初始版本相同的接口,并持有一个指向当前实现版本的地址。

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.8.2 <0.9.0;

// 代理合约
contract MyContractProxy {
    address public currentVersion;
    address public owner;

    constructor(address _currentVersion) {
        currentVersion = _currentVersion;
        owner = msg.sender;
    }

    // 转发所有调用到当前版本的合约
    fallback() external payable {
        address implementation = currentVersion;
        require(implementation != address(0), "Contract implementation not set");

        assembly {
            let ptr := mload(0x40)
            calldatacopy(ptr, 0, calldatasize())
            let result := delegatecall(gas(), implementation, ptr, calldatasize(), 0, 0)
            returndatacopy(ptr, 0, returndatasize())

            switch result
            case 0 { revert(ptr, returndatasize()) }
            default { return(ptr, returndatasize()) }
        }
    }

    // 更新合约实现版本
    function upgrade(address newVersion) public {
        require(msg.sender == owner, "Only the owner can upgrade");
        currentVersion = newVersion;
    }
}

在上面的合约中,我们在 fallback 函数中实现了代理合约的核心逻辑。它首先将传入的调用数据复制到内存中,然后使用 delegatecall 将调用转发到逻辑合约,并在当前合约的上下文中执行其代码。最后,根据 delegatecall 的结果,决定是回滚交易并返回错误数据,还是返回成功的数据。

  1. let ptr := mload(0x40)
    • mload(0x40) 读取内存位置 0x40 上的值,该位置通常被称为 “free memory pointer”(空闲内存指针),它指向当前空闲内存的开始位置。
    • let ptr := mload(0x40) 将这个空闲内存地址存储在变量 ptr 中,以供后续使用。
  2. calldatacopy(ptr, 0, calldatasize())
    • calldatacopy 将调用数据(包括函数选择器和参数)从消息的输入数据复制到内存中。
    • ptr 是内存的起始位置。
    • 0 是调用数据的起始位置。
    • calldatasize() 返回调用数据的大小。
    • 这行指令的作用是将所有传入的调用数据复制到内存中,从 ptr 开始存储。
  3. let result := delegatecall(gas(), implementation, ptr, calldatasize(), 0, 0)
    • delegatecall 是一个 EVM 操作码,用于在另一个合约的上下文中执行代码,同时保留当前合约的存储、msg.sender 和 msg.value。
    • gas() 返回当前可用的剩余 gas。
    • implementation 是逻辑合约的地址。
    • ptr 是内存中存储调用数据的起始位置。
    • calldatasize() 是调用数据的大小。
    • 0 是返回数据的存储位置(初始设置为 0)。
    • 0 是返回数据的大小(初始设置为 0)。
    • 这行指令的作用是执行逻辑合约的代码,并将执行结果存储在 result 中。
  4. returndatacopy(ptr, 0, returndatasize())
    • returndatacopy 将返回数据从调用返回位置复制到内存中。
    • ptr 是内存的起始位置。
    • 0 是返回数据的起始位置。
    • returndatasize() 返回上一个调用(即 delegatecall)返回的数据大小。
    • 这行指令的作用是将 delegatecall 的返回数据复制到内存中,从 ptr 开始存储。
  5. switch result
    • switch 语句基于 result 的值进行分支处理。
    • resultdelegatecall 的返回值,如果调用成功则为 1,失败则为 0。
  6. case 0 { revert(ptr, returndatasize()) }
    • 如果 result 为 0,表示 delegatecall 调用失败。
    • revert(ptr, returndatasize()) 会回滚交易,并返回错误数据。
    • ptr 是内存中错误数据的起始位置。
    • returndatasize() 是错误数据的大小。
  7. default { return(ptr, returndatasize()) }
    • 如果 result 为非 0,表示 delegatecall 调用成功。
    • return(ptr, returndatasize()) 会返回成功的数据。
    • ptr 是内存中返回数据的起始位置。
    • returndatasize() 是返回数据的大小。

简单来说,这段汇编代码在代理合约的 fallback 函数中执行以下操作:

  1. 将传入的调用数据复制到内存。
  2. 使用 delegatecall 将调用转发到逻辑合约,并在当前合约的上下文中执行其代码。
  3. 根据 delegatecall 的结果,决定是回滚交易并返回错误数据,还是返回成功的数据。

这种模式确保了代理合约可以灵活地转发调用,并根据逻辑合约的实现来执行具体的业务逻辑。

4. 升级过程

  • 首先部署初始版本的合约(MyContract)和代理合约(MyContractProxy),将代理合约初始化为指向初始版本。
  • 当需要升级时,部署新版本的合约(MyContractV2)。
  • 调用代理合约的 upgrade 函数,将当前版本更新为新版本的合约地址。

5. 优势和注意事项

  • 无中断更新: 使用代理模式,更新过程不会中断已有合约的使用。
  • 灵活性: 可以在需要时随时更新合约逻辑,而无需改变合约地址。
  • 安全性: 更新前的合约状态和余额不会丢失或重置。
  • 成本: 代理模式可以降低升级过程的成本,避免重新部署合约带来的高昂费用。

需要注意的是,代理模式需要谨慎设计,确保新版本的合约与旧版本保持兼容性,以及更新过程的安全性和透明性。


孟斯特

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。

Author: mengbin

blog: mengbin

Github: mengbin92

cnblogs: 恋水无意

腾讯云开发者社区:孟斯特