代理合约

1,257 阅读6分钟

什么是代理合约?

当我们需要同时创建一个合约的很多份实例时,为了避免重复部署同样的合约代码,取而代之的是只部署一次合约代码,我们可以引入一个工厂合约,通过特定的参数来创建对应的合约实例。

例如:
在做 DeFi 借贷应用时,有可能需要对每个借贷单生成一个借贷合约实例。
在做多签钱包时,当用户要创建一个新的多签钱包时,合约层相应的会创建一个新的多签合约实例

代理模式:

Solidity合约部署在链上之后,代码是不可变的(immutable)。就算合约中存在bug,也不能修改或升级,只能部署新合约。但是新合约的地址与旧的不一样,且合约的数据也需要花费大量gas进行迁移。

代理模式可以让在合约部署后进行修改或升级。

image.png

代理模式将合约数据和逻辑分开,数据(状态变量)存储在代理合约中,而逻辑(函数)保存在另一个逻辑合约中。代理合约和逻辑合约的变量存储结构相同

代理合约通过delegatecall将函数调用全权委托给逻辑合约执行,再把最终的结果返回给调用者。

好处:节省gas、可升级

Zeppelin探索了三种代理模式:

  1. 继承存储
  2. 永久存储
  3. 非结构化存储

这三种模式底层都依赖delegatecalls来实现。
注意点:

  • 当调用的方法在合约中不存在时,合约会调用fallback函数。可以在fallback里面添加逻辑。
  • 每当合约A将调用代理到另一个合同B时,它都会在合约A的上下文中执行合约B的代码。

proxy

它有三个部分:代理合约Proxy,逻辑合约Logic,和一个调用示例Caller
源码:openzeppelin-contracts/Proxy.sol at master · OpenZeppelin/openzeppelin-contracts (github.com)

逻辑:
1.部署逻辑合约logic
2.创建代理合约,状态变量implementation记录Logic合约地址。
3.合约利用回调函数fallback,(fallback里面有delegate操作)将所有调用委托给Logic合约
4.部署调用示例Caller合约,调用Proxy合约。

Proxy的主要逻辑
implementation变量存逻辑合约

function _delegate(address implementation) internal virtual {
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) 
     
     returndatacopy(0, 0, returndatasize())  
     switch result    
     case 0 {       
       revert(0, returndatasize())    
  }            
     default {   
       return(0, returndatasize())   
       }     
    }      
}
  • calldatacopy(t, f, s):将calldata(输入数据)从位置f开始复制s字节到mem(内存)的位置t
  • returndatacopy(t, f, s):将returndata(输出数据)从位置f开始复制s字节到mem(内存)的位置t
  • switch:基础版if/else,不同的情况case返回不同值。可以有一个默认的default情况

Logic主要逻辑

根据项目的逻辑不同,合约不同(一个简单的示例):

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;  
}  
}
  • increment()函数:会被Proxy合约调用,释放CallSuccess事件,并返回一个uint,它的selector0xd09de08a。如果直接调用increment()返回100,但是通过Proxy调用它会返回1

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));  
}  
}
  • proxy:状态变量,记录代理合约地址。
  • increment():利用call来调用代理合约的increment()函数,并返回一个uint。在调用时,我们利用abi.encodeWithSignature()获取了increment()函数的selector。在返回时,利用abi.decode()将返回值解码为uint类型。

可升级合约

可升级合约就是一个可以更改逻辑合约的代理合约。

image.png

使用继承存储实现可升级

继承存储方式需要逻辑合约包含代理合约所需的存储结构。代理和逻辑合约都继承相同的存储结构,以确保两者都存储必要的代理状态变量。

我们使用Registry合约来跟踪逻辑合同的不同版本。为了升级到新的逻辑合同,开发者需要在注册合约中将新升级的合约进行注册,并要求代理升级到新合约。

如何初始化

  1. 部署Registry合约
  2. 部署初始版本目标合约(v1)。确保它继承了可升级合约
  3. 将初始版本的目标合约地址注册到 Registry合约
  4. 请求Registry合约,创建一个UpgradeabilityProxy实例
  5. 请求UpgradeabilityProxy,升级到目标合约的初始版本

如何升级

  1. 部署从初始版本继承的新版本合约(v2),并确保新版本合约保留代理的存储结构和初始版本合约的存储结构。
  2. 将新版本的合约注册到 Registry
  3. 请求UpgradeabilityProxy,将目标合约升级为新版本。

简单实现

代理合约

pragma solidity ^0.8.4;  
  
// 简单的可升级合约,管理员可以通过升级函数更改逻辑合约地址,从而改变合约的逻辑。  
// 教学演示用,不要用在生产环境  
contract SimpleUpgrade {  
address public implementation; // 逻辑合约地址  
address public admin; // admin地址  
string public words; // 字符串,可以通过逻辑合约的函数改变  
  
// 构造函数,初始化admin和逻辑合约地址  
constructor(address _implementation){  
admin = msg.sender;  
implementation = _implementation;  
}  
  
// fallback函数,将调用委托给逻辑合约  
fallback() external payable {  
(bool success, bytes memory data) = implementation.delegatecall(msg.data);  
}  
  
// 升级函数,改变逻辑合约地址,只能由admin调用  
function upgrade(address newImplementation) external {  
require(msg.sender == admin);  
implementation = newImplementation;  
}  
}
  • 构造函数:初始化admin和逻辑合约地址。
  • fallback():回调函数,将调用委托给逻辑合约。
  • upgrade():升级函数,改变逻辑合约地址,只能由admin调用

旧逻辑合约

// 逻辑合约1  
contract Logic1 {  
// 状态变量和proxy合约一致,防止插槽冲突  
address public implementation;  
address public admin;  
string public words; // 字符串,可以通过逻辑合约的函数改变  
  
// 改变proxy中状态变量,选择器: 0xc2985578  
function foo() public{  
words = "old";  
}  
}

新逻辑合约

// 逻辑合约2  
contract Logic2 {  
// 状态变量和proxy合约一致,防止插槽冲突  
address public implementation;  
address public admin;  
string public words; // 字符串,可以通过逻辑合约的函数改变  
  
// 改变proxy中状态变量,选择器:0xc2985578  
function foo() public{  
words = "new";  
}  
}

参考:46. 代理合约 | WTF Academy 以太坊实现智能合约升级的三种代理模式 | 登链社区 | 区块链技术社区 (learnblockchain.cn)