抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

1. ERC1167 简介

EIP-1167,又称Minimal Proxy Contract,提供了一种低成本复制合约的方法,也可以叫作是克隆合约的方法。如何理解克隆呢?克隆就是类似复制的意思,这里的合约克隆是指:克隆合约和原合约具有相同的逻辑功能。而且创建克隆合约的成本比直接部署原合约低,部署克隆合约的前提是得有一个原件。

2. 原理复现

2.1 工作原理

一说到代理,首先就会想到代理合约,合约升级。但是ERC1167不是合约升级,它只是负责合约的调用转发。

可升级合约的代理合约架构:

image-20240502181034807

整个架构中存在一个代理合约和多个逻辑合约,只有一套数据(即代理合约的数据),需要升级时则替换掉代理合约中的逻辑合约,而且同一时间只能存在一个逻辑合约。

Minimal Proxy Contract合约架构:

image-20240502181259046

整个架构中存在多个代理合约和一个逻辑合约,有多套数据分别存储在不同的代理合约中,所有代理合约共享逻辑合约的执行逻辑,同一时间存在多个代理合约。Minimal Proxy Contract的原理就是将代理合约作为逻辑合约的复制品,各个代理合约存储各自的数据,需要多少份复制品就创建多少个代理合约。而代理合约本身只负责请求转发,因此其内容很少,从而耗费的gas就更少。

2.2 解析字节码

从官方文档上可以看到这串字节码: 363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3,经过反编译之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 0x0: CALLDATASIZE
0x1: RETURNDATASIZE
0x2: RETURNDATASIZE
0x3: CALLDATACOPY
0x4: RETURNDATASIZE
0x5: RETURNDATASIZE
0x6: RETURNDATASIZE
0x7: CALLDATASIZE
0x8: RETURNDATASIZE
0x9: PUSH20 0xbebebebebebebebebebebebebebebebebebebebe
0x1e: GAS
0x1f: DELEGATECALL
0x20: RETURNDATASIZE
0x21: DUP3
0x22: DUP1
0x23: RETURNDATACOPY
0x24: SWAP1
0x25: RETURNDATASIZE
0x26: SWAP2
0x27: PUSH1 0x2b
0x29: JUMPI
0x2a: REVERT
0x2b: JUMPDEST
0x2c: RETURN

这串字节码的执行的逻辑就是对 0xbebebebebebebebebebebebebebebebebebebebe地址执行delegatecall,如果调用失败则revert,如果调用成功则返回代理调用返回的结果。

可以自己用汇编语言写出来,returndata的部分可能不太对,但是大体逻辑是这样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
// delegatecall returns 0 on error.
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}

2.3 实现复制功能

要如何实现克隆功能,可以参考openzeppelin官方的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function clone(address implementation, uint256 value) internal returns (address instance) {
if (address(this).balance < value) {
revert Errors.InsufficientBalance(address(this).balance, value);
}
/// @solidity memory-safe-assembly
assembly {
// Stores the bytecode after address
mstore(0x20, 0x5af43d82803e903d91602b57fd5bf3)
// implementation address
mstore(0x11, implementation)
// Packs the first 3 bytes of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0x88, implementation), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
instance := create(value, 0x09, 0x37)
}
if (instance == address(0)) {
revert Errors.FailedDeployment();
}
}

直接看到汇编部分,三个mstore操作码的作用是拼接克隆合约的createionCode,拼接的结果:

1
0x3d602d80600a3d3981f3363d3d373d3d3d363d73 + implementation + 5af43d82803e903d91602b57fd5bf3

然后通过create操作码部署克隆合约。

演示:

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
33
34
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.22;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Clones.sol";

contract Demo {

uint256 public num;

fallback() external payable {
++num;
}
}

contract Test {

uint256 public num;
event result(uint256);

function call(address cloner) public {
cloner.call("aaa"); // call fallback()
(, bytes memory _result) = cloner.call(abi.encodeWithSignature("num()"));
emit result(abi.decode(_result, (uint256)));
}
}

contract CloneLib {

using Clones for address;

function clone(address implementation) public returns (address cloner){
cloner = implementation.clone();
}
}
  1. 先部署 Demo合约
  2. 部署CloneLib合约,并调用clone函数,并传入Demo合约地址
  3. 最后部署Test合约,并调用call函数,传入克隆地址

结果:

image-20240503134611595

可以看到结果返回1,说明克隆成功。

同理,知道了克隆的逻辑,可以用另一种方式复现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
contract Clone {

uint256 public num;

event a(bytes);

constructor(address implementation) {
bytes memory head = hex"363d3d373d3d3d363d73";
bytes memory tail = hex"5af43d82803e903d91602b57fd5bf3";
bytes memory runtimeCode = abi.encodePacked(head, implementation, tail);
emit a(runtimeCode);

assembly {
return(add(runtimeCode, 0x20), mload(runtimeCode))
}
}
}

solidity的智能合约执行的逻辑都是通过runtimeCode,而只要将合约runtimeCode部分的内容按克隆合约的逻辑编写,即照样也可以完成相同的要求。

执行操作相同,执行结果为:

image-20240503135117581

3. 节省gas费用

通过部署原合约和部署克隆合约所需的gas费的多少来判断

部署Demo所需的gas费用为:"122325"

image-20240503135408840

部署克隆合约所需的gas费用为:"63334"

image-20240503135510002

可以看到几乎是节省了一倍的花销。

评论



政策 · 统计 | 本站使用 Volantis 主题设计