代理合约

本章讲解代理合约 proxy 的作用、原理和实现代码。

视频Bilibili  |  Youtube
官网binschool.app
推特@BinSchool    DiscordBinDAO   微信:bkra50 

代理合约(Proxy Contract)是一种特殊的智能合约,调用者可以通过其提供的接口与目标智能合约进行交互。目标合约通常称为逻辑合约或者实现合约。

代理合约位于调用者和目标合约之间,起到类似中介的作用。

调用者、代理合约和逻辑合约之间的关系如下图所示:

代理合约的工作原理主要依赖于区块链的消息调用机制。当一个代理合约收到消息调用时,它可以将该消息调用转发给另一个合约,即逻辑合约。

1. 代理合约的使用场景

1.1 实现可升级性

可以实现用户无感的情况下平滑升级逻辑合约,来增加逻辑合约的新功能或者修复漏洞。

1.2 实现权限控制

可以在代理合约中为用户设定操作权限,确保用户按照授权执行允许的操作。

1.3 实现审计功能

可以在代理合约中记录关键操作,便于日后进行安全审计和监控,确保合约使用的安全性和可追溯性。

代理合约最常使用的场景还是实现可升级合约,我们将会在下一章节详解讲解。

2.  代理合约的实现

在代理合约中,需要保存一个指向逻辑合约的地址。当调用者向代理合约提交请求时,代理合约将请求转发给逻辑合约,并将执行结果返回给调用者。

那么,代理合约如何将请求转发给逻辑合约呢?

我们在基础教程中介绍过回退函数 fallback 的工作机制:如果有人调用了一个合约中不存在的函数,那么将自动执行合约中的 fallback 函数。

因此,只要在代理合约中不定义处理业务逻辑的函数,那么外部的调用请求就会转给 fallback 函数,我们在 fallback 函数中再把请求转给逻辑合约。

2.1 代理合约 Proxy 的实现代码

// 代理合约
contract Proxy {
    // 逻辑合约地址
    address public logicContractAddress;

    // 构造函数
    constructor(address _logicContractAddress) {
        // 设置逻辑合约地址
        logicContractAddress = _logicContractAddress;
    }
    // 调用逻辑合约的 fallback 函数
    fallback() external {
        // 转发请求给逻辑合约
        (bool success, bytes memory result) = logicContractAddress.call(msg.data);
        require(success, "Forwarding request to logic contract failed");
        // 返回执行结果给调用者
        assembly {
            return(add(result, 0x20), mload(result))
        }
    }
}

说明: 代理合约 Proxy 中的 fallback 函数将接收到的请求转发给逻辑合约,并将执行结果返回给调用者。

2.2 逻辑合约 Logic 的实现代码

// 逻辑合约
contract Logic {
    // 业务逻辑实现
    function foo() external pure returns (uint) {
        // 实现具体的业务逻辑
        return 1024;
    }
}

说明: 逻辑合约中定义了一个函数 foo,它返回整型值 1024

2.3 调用合约 Caller 的实现代码

// 调用合约
contract Caller {
    // 代理合约地址
    address public proxy; 

    // 设置代理合约地址
    constructor(address _proxy){
        proxy = _proxy;
    }
    // 通过代理合约调用 foo 函数
    function foo() external returns(bool, uint) {
        // 使用 call 方法,传入 foo 函数签名
        (bool success, bytes memory data) = proxy.call(abi.encodeWithSignature("foo()"));
        // 将代理返回的结果进行解码
        return (success, abi.decode(data,(uint)));
    }
}

说明: 调用合约 Caller 中的 foo 函数将通过代理合约 Proxy,调用逻辑合约 Logicfoo 函数。

3.  测试和验证

我们要按照逻辑合约、代理合约和调用者合约的顺序依次部署。

3.1  部署逻辑合约 Logic,获得逻辑合约的地址:

3.2  部署代理合约 Proxy,需要填入上一步中逻辑合约的地址:

3.3  部署调用者合约 Caller, 需要填入上一步中代理合约的地址:

3.4  点击调用者合约 Caller 的地址, 将会列出所有可以调用的函数。

点击调用 foo 函数后,右边的控制台会输出函数的执行情况:

 

返回的结果有两项:

第一项:执行逻辑合约的函数是否成功,结果为 true

第二项:执行逻辑合约的函数的返回值,结果为 1024

4.  使用 delegatecall 改进代理合约

我们在代理合约 Proxy 中,使用 call 函数来调用逻辑合约的函数,但这并不是最佳选择,应该使用 delegatecall 函数来代替 call

使用 call 调用时,如果执行的函数改变了状态,其结果将存储在逻辑合约的状态变量和存储中。因此,当我们将原来的逻辑合约更换为新逻辑合约时,保存在旧逻辑合约中的数据将全部丢失。

相较之下,delegatecall 调用则不同。如果执行的函数改变了状态,其结果将保存在代理合约的状态变量和存储上。因此,即便我们对逻辑合约进行了升级,代理合约上保存的数据不会丢失。

总体而言,delegatecall 调用方式实现了业务逻辑和数据存储的分离,更为适用于代理合约的使用场景,能够确保数据的持久性。

调用者、代理合约和逻辑合约之间的关系如下图所示:

依照上面的思路,我们更改一下代理合约的实现,将 call 改为 delegatecall

// 调用逻辑合约的 fallback 函数
fallback() external {
    // 转发请求给逻辑合约
    (bool success, bytes memory result) = logicContractAddress.delegatecall(msg.data);
    require(success, "Forwarding request to logic contract failed");
    // 返回执行结果给调用者
    assembly {
        return(add(result, 0x20), mload(result))
    }
}

5.  使用 openzepplin 编写代理合约

openzepplin 合约库中,有完整的 Proxy 合约的实现代码。它使用了内联汇编编写了函数 fallback,所以运行效率比较高,也更节省 Gas,但是对于初学者来说,理解起来有一定的难度。

我们在编写自己的代理合约时,可以直接继承 openzepplinProxy 合约。

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

import "@openzeppelin/contracts/proxy/Proxy.sol";

// 代理合约
contract ProxyOpenZepplin is Proxy {
    // 逻辑合约地址
    address public logicContractAddress;

    // 构造函数
    constructor(address _logicContractAddress) {
        // 设置逻辑合约地址
        logicContractAddress = _logicContractAddress;
    }
    // 覆盖 openzepplin proxy 合约函数
    function _implementation() internal view override returns (address) {
        return logicContractAddress;
    }
}