什么是代理合约?
当我们需要同时创建一个合约的很多份实例时,为了避免重复部署同样的合约代码,取而代之的是只部署一次合约代码,我们可以引入一个工厂合约,通过特定的参数来创建对应的合约实例。
例如:
在做 DeFi 借贷应用时,有可能需要对每个借贷单生成一个借贷合约实例。
在做多签钱包时,当用户要创建一个新的多签钱包时,合约层相应的会创建一个新的多签合约实例
代理模式:
Solidity
合约部署在链上之后,代码是不可变的(immutable)。就算合约中存在bug,也不能修改或升级,只能部署新合约。但是新合约的地址与旧的不一样,且合约的数据也需要花费大量gas进行迁移。
代理模式可以让在合约部署后进行修改或升级。
代理模式将合约数据和逻辑分开,数据(状态变量)存储在代理合约中,而逻辑(函数)保存在另一个逻辑合约中。代理合约和逻辑合约的变量存储结构相同
代理合约通过delegatecall将函数调用全权委托给逻辑合约执行,再把最终的结果返回给调用者。
好处:节省gas、可升级
Zeppelin探索了三种代理模式:
- 继承存储
- 永久存储
- 非结构化存储
这三种模式底层都依赖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
,它的selector
为0xd09de08a
。如果直接调用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
类型。
可升级合约
可升级合约就是一个可以更改逻辑合约的代理合约。
使用继承存储实现可升级
继承存储方式需要逻辑合约包含代理合约所需的存储结构。代理和逻辑合约都继承相同的存储结构,以确保两者都存储必要的代理状态变量。
我们使用Registry
合约来跟踪逻辑合同的不同版本。为了升级到新的逻辑合同,开发者需要在注册合约中将新升级的合约进行注册,并要求代理升级到新合约。
如何初始化
- 部署
Registry
合约 - 部署初始版本目标合约(v1)。确保它继承了可升级合约
- 将初始版本的目标合约地址注册到
Registry
合约 - 请求
Registry
合约,创建一个UpgradeabilityProxy
实例 - 请求
UpgradeabilityProxy
,升级到目标合约的初始版本
如何升级
- 部署从初始版本继承的新版本合约(v2),并确保新版本合约保留代理的存储结构和初始版本合约的存储结构。
- 将新版本的合约注册到
Registry
- 请求
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)