【从0学习Solidity】46. 代理合约

news/2024/7/21 10:51:34 标签: 区块链, web3, solidity

【从0学习Solidity】46. 代理合约

在这里插入图片描述

  • 博主简介:不写代码没饭吃,一名全栈领域的创作者,专注于研究互联网产品的解决方案和技术。熟悉云原生、微服务架构,分享一些项目实战经验以及前沿技术的见解。
  • 关注我们的主页,探索全栈开发,期待与您一起在移动开发的世界中,不断进步和创造!
  • 本文收录于 不写代码没饭吃 的学习汇报系列,大家有兴趣的可以看一看。
  • 欢迎访问我们的微信公众号:不写代码没饭吃,获取更多精彩内容、实用技巧、行业资讯等。您关注的是我们前进的动力!

这一讲,我们介绍代理合约(Proxy Contract)。教学代码由OpenZeppelin的Proxy合约简化而来。

代理模式

Solidity合约部署在链上之后,代码是不可变的(immutable)。这样既有优点,也有缺点:

  • 优点:安全,用户知道会发生什么(大部分时候)。
  • 坏处:就算合约中存在bug,也不能修改或升级,只能部署新合约。但是新合约的地址与旧的不一样,且合约的数据也需要花费大量gas进行迁移。

有没有办法在合约部署后进行修改或升级呢?答案是有的,那就是代理模式

46-1.png

代理模式将合约数据和逻辑分开,分别保存在不同合约中。我们拿上图中简单的代理合约为例,数据(状态变量)存储在代理合约中,而逻辑(函数)保存在另一个逻辑合约中。代理合约(Proxy)通过delegatecall,将函数调用全权委托给逻辑合约(Implementation)执行,再把最终的结果返回给调用者(Caller)。

代理模式主要有两个好处:

  1. 可升级:当我们需要升级合约的逻辑时,只需要将代理合约指向新的逻辑合约。
  2. 省gas:如果多个合约复用一套逻辑,我们只需部署一个逻辑合约,然后再部署多个只保存数据的代理合约,指向逻辑合约。

提示:对delegatecall不熟悉的朋友可以看下本教程第23讲Delegatecall。

代理合约

下面我们介绍一个简单的代理合约,它由OpenZeppelin的Proxy合约简化而来。它有三个部分:代理合约Proxy,逻辑合约Logic,和一个调用示例Caller。它的逻辑并不复杂:

  • 首先部署逻辑合约Logic
  • 创建代理合约Proxy,状态变量implementation记录Logic合约地址。
  • Proxy合约利用回调函数fallback,将所有调用委托给Logic合约
  • 最后部署调用示例Caller合约,调用Proxy合约。
  • 注意Logic合约和Proxy合约的状态变量存储结构相同,不然delegatecall会产生意想不到的行为,有安全隐患。

代理合约Proxy

Proxy合约不长,但是用到了内联汇编,因此比较难理解。它只有一个状态变量,一个构造函数,和一个回调函数。状态变量implementation,在构造函数中初始化,用于保存Logic合约地址。

solidity">contract Proxy {
    address public implementation; // 逻辑合约地址。implementation合约同一个位置的状态变量类型必须和Proxy合约的相同,不然会报错。

    /**
     * @dev 初始化逻辑合约地址
     */
    constructor(address implementation_){
        implementation = implementation_;
    }

Proxy的回调函数将外部对本合约的调用委托给 Logic 合约。这个回调函数很别致,它利用内联汇编(inline assembly),让本来不能有返回值的回调函数有了返回值。其中用到的内联汇编操作码:

  • calldatacopy(t, f, s):将calldata(输入数据)从位置f开始复制s字节到mem(内存)的位置t
  • delegatecall(g, a, in, insize, out, outsize):调用地址a的合约,输入为mem[in..(in+insize)) ,输出为mem[out..(out+outsize)), 提供gwei的以太坊gas。这个操作码在错误时返回0,在成功时返回1
  • returndatacopy(t, f, s):将returndata(输出数据)从位置f开始复制s字节到mem(内存)的位置t
  • switch:基础版if/else,不同的情况case返回不同值。可以有一个默认的default情况。
  • return(p, s):终止函数执行, 返回数据mem[p..(p+s))
  • revert(p, s):终止函数执行, 回滚状态,返回数据mem[p..(p+s))
solidity">/**
* @dev 回调函数,将本合约的调用委托给 `implementation` 合约
* 通过assembly,让回调函数也能有返回值
*/
fallback() external payable {
    address _implementation = implementation;
    assembly {
        // 将msg.data拷贝到内存里
        // calldatacopy操作码的参数: 内存起始位置,calldata起始位置,calldata长度
        calldatacopy(0, 0, calldatasize())

        // 利用delegatecall调用implementation合约
        // delegatecall操作码的参数:gas, 目标合约地址,input mem起始位置,input mem长度,output area mem起始位置,output area mem长度
        // output area起始位置和长度位置,所以设为0
        // delegatecall成功返回1,失败返回0
        let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)

        // 将return data拷贝到内存
        // returndata操作码的参数:内存起始位置,returndata起始位置,returndata长度
        returndatacopy(0, 0, returndatasize())

        switch result
        // 如果delegate call失败,revert
        case 0 {
            revert(0, returndatasize())
        }
        // 如果delegate call成功,返回mem起始位置为0,长度为returndatasize()的数据(格式为bytes)
        default {
            return(0, returndatasize())
        }
    }
}

逻辑合约Logic

这是一个非常简单的逻辑合约,只是为了演示代理合约。它包含2个变量,1个事件,1个函数:

  • implementation:占位变量,与Proxy合约保持一致,防止插槽冲突。
  • xuint变量,被设置为99
  • CallSuccess事件:在调用成功时释放。
  • increment()函数:会被Proxy合约调用,释放CallSuccess事件,并返回一个uint,它的selector0xd09de08a。如果直接调用increment()回返回100,但是通过Proxy调用它会返回1,大家可以想想为什么?
solidity">/**
 * @dev 逻辑合约,执行被委托的调用
 */
contract Logic {
    address public implementation; // 与Proxy保持一致,防止插槽冲突
    uint public x = 99; 
    event CallSuccess(); // 调用成功事件

    // 这个函数会释放CallSuccess事件并返回一个uint。
    // 函数selector: 0xd09de08a
    function increment() external returns(uint) {
        emit CallSuccess();
        return x + 1;
    }
}

调用者合约Caller

Caller合约会演示如何调用一个代理合约,它也非常简单。但是要理解它,你需要先学习本教程的第22讲 Call和第27讲 ABI编码。

它有1个变量,2个函数:

  • proxy:状态变量,记录代理合约地址。
  • 构造函数:在部署合约时初始化proxy变量。
  • increase():利用call来调用代理合约的increment()函数,并返回一个uint。在调用时,我们利用abi.encodeWithSignature()获取了increment()函数的selector。在返回时,利用abi.decode()将返回值解码为uint类型。
solidity">/**
 * @dev Caller合约,调用代理合约,并获取执行结果
 */
contract Caller{
    address public proxy; // 代理合约地址

    constructor(address proxy_){
        proxy = proxy_;
    }

    // 通过代理合约调用increment()函数
    function increment() external returns(uint) {
        ( , bytes memory data) = proxy.call(abi.encodeWithSignature("increment()"));
        return abi.decode(data,(uint));
    }
}

Remix演示

  1. 部署Logic合约。

46-2.jpg

  1. 调用Logic合约的increment()函数,返回100

46-3.jpg

  1. 部署Proxy合约,初始化时填入Logic合约地址。

46-4.jpg

  1. 调用Proxy合约increment()函数,无返回值。

    调用方法:在Remix部署面板中点Proxy合约,在最下面的Low level interaction中填入increment()函数的选择器0xd09de08a,并点击Transact

46-5.jpg

  1. 部署Caller合约,初始化时填入Proxy合约地址。

46-6.jpg

  1. 调用Caller合约increment()函数,返回1

46-7.jpg

总结

这一讲,我们介绍了代理模式和简单的代理合约。代理合约利用delegatecall将函数调用委托给了另一个逻辑合约,使得数据和逻辑分别由不同合约负责。并且,它利用内联汇编黑魔法,让没有返回值的回调函数也可以返回数据。前面留给大家的问题是:为什么通过Proxy调用increment()会返回1呢?按照我们在[第23讲Delegatecall]中所说的,当Caller合约通过Proxy合约来delegatecall Logic合约的时候,如果Logic合约函数改变或读取一些状态变量的时候都会在Proxy的对应变量上操作,而这里Proxy合约的x变量的值是0(因为从来没有设置过x这个变量,即Proxy合约的storage区域所对应位置值为0),所以通过Proxy调用increment()会返回1。

下一讲,我们会介绍可升级代理合约。

代理合约虽然很强大,但是它非常容易出bug,用的时候最好直接复制OpenZeppelin的模版合约。

在这里插入图片描述

如果这份博客对大家有帮助,希望各位给作者一个免费的点赞👍作为鼓励,并评论收藏一下⭐,谢谢大家!!!
制作不易,如果大家有什么疑问或给作者的意见,欢迎评论区留言。


http://www.niftyadmin.cn/n/5047925.html

相关文章

基于PyTorch3D的GeoAI实现【ESRI】

Esri 的 AI 原型团队正在以 PyTorch3D API 的一系列 PR 的形式分享一些功能增强功能。 这些功能支持 obj 格式的网格的输入/输出 (I/O),该网格具有多个纹理和代表真实世界几何形状的顶点坐标。 对于 GeoAI 任务,这些功能支持跨网格分割管道的任务&#x…

flink集群与资源@k8s源码分析-运行时

1 运行时 运行时提供了Flink作业运行过程依赖的基础执行环境,包含Dispatcher、ResourceManager、JobManager和TaskManager等核心组件,本节分析资源相关运行时组件构建和启动。 flink没有使用spring,缺少ioc的构建过程相当复杂,所有依赖手动关联和置入,为了共享组件,fli…

c==ubuntu debug c

在 Visual Studio Code (VSCode) 中调试 C 代码,你需要使用适当的扩展,并配置调试环境。以下是一个详细的示例,包括完整的代码、配置步骤和演示。 1. 安装 C/C 扩展: 首先,确保你已安装 Visual Studio Code。然后&am…

【Linux】系统编程线程池单例模式(C++)

目录 【1】什么是单例模式 【2】什么是设计模式 【3】单例模式的特点 【4】饿汉实现方式和懒汉实现方式 【5】饿汉方式实现单例模式 【6】懒汉方式实现单例模式 【7】将线程池改为单例模式 【1】什么是单例模式 单例模式是一种 "经典的, 常用的, 常考的" 设计…

实用!Python大型Excel文件处理:快速导入、导出与批量处理

Python 是一种功能强大的编程语言,它提供了丰富的库和工具,使得处理大型 Excel 文件变得容易和高效。下面将介绍如何使用 Python 快速导入、导出和批量处理大型 Excel 文件。下面是一些建议和实践经验,希望能对你有所帮助。 一、Excel 文件处…

mojo 笔记 (Python/Cython)

目录 1. mojo 笔记 (Python/Cython)1.1. Alternatives to Mojo 1. mojo 笔记 (Python/Cython) Mojo may be the biggest programming language advance in decades 1.1. Alternatives to Mojo Mojo is not the only attempt at solving the Python performance and deployme…

MySQL高级语句(第一部分)

MySQL高级语句(第一部分)一、MySQL进阶查询1、select ----显示表格中一个或数个字段的所有数据记录2、distinct ----不显示重复的数据记录3、where ----有条件查询4、and or ----且 或5、in ----显示已知的值的数据记录6、between ----显示两个值范围内的数据记录7、通配符8、l…

SPA项目的登录注册实现以及数据交互问题

目录 前言 一. 登录,注册静态页面实现 1.1 ElementUI简介 1.2 基于SPA项目完成登录注册 1.2.1 在SPA项目中添加elementui依赖 1.2.2 在main.js中添加elementui模块 1.2.3 在src目录下创建views目录,用于存放vue组件 1.2.4 配置路由 1.2.5 修改项目…