前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Solidity:代理模式升级合约

Solidity:代理模式升级合约

原创
作者头像
孟斯特
发布2024-08-01 09:03:25
1580
发布2024-08-01 09:03:25
举报
文章被收录于专栏:solidity

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

1. 初版

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

代码语言:txt
复制
// 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. 升级版本

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

代码语言:txt
复制
// 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. 代理合约

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

代码语言:txt
复制
// 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. 优势和注意事项

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

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 初版
  • 2. 升级版本
  • 3. 代理合约
  • 4. 升级过程
  • 5. 优势和注意事项
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档