Q1ngying

今朝梦醒与君别,遥盼春风寄相思

0%

代理合约

代理合约

原文章:https://www.wtf.academy/solidity-application/ProxyContract/

代理模式

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

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

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

代理模式

代理模式将合约数据和逻辑分开,分别保存在不同合约中。

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

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

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

代理合约

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

  • 首先部署逻辑合约Logic
  • 创建代理合约Proxy,状态变量implementation记录Logic合约地址。
  • Proxy合约利用回调函数fallback,将所有调用委托给Logic合约
  • 最后部署调用示例Caller合约,调用Proxy合约

注意:logic合约和Proxy合约的状态变量存储结构相同,不然delegatecall会产生意想不到的行为,有安全隐患。

代理合约Proxy

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

1
2
3
4
5
6
7
8
9
10
11
12
contract Proxy {
address public implementation; // 逻辑合约地址
// implementation 合约同一个位置的状态变量必须和 Proxy 合约的相同,不然会报错(涉及到一个漏洞)

/**
* @dev 初始化逻辑合约地址
*/

constructor(address implementation_) {
implementation = implementatiion_;
}
}

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(p, s):基础版if/else,不同的情况case返回不同值。可以有一个默认的default情况。
  • return(p, s):终止函数执行,返回数据mem[p..p+s))
  • revert(p, s):终止函数执行,回滚状态,返回数据mem[p..(p+s))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* @dev 回调函数,将本合约的调用委托给 `implementation` 合约
* 通过 assembly。让回调函数也能有返回值
*/
fallback() external payable {
address _implementation = implementation
assembly {
// 将 msg.data 拷贝到内存里
// calldatacopy 操作码的参数:内存起始位置,calldata起始位置,calldata 长度
calldatacopy(0, 0, calldatasize())

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

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

switch result
// 如果 delegatecall 失败:revert
case 0 {
revert(0, returndatasize())
}
// 如果delegatecall 成功,返回 men_start 为 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @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类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @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));
}
}