1. ERC1167 简介
EIP-1167,又称Minimal Proxy Contract
,提供了一种低成本复制合约的方法,也可以叫作是克隆合约的方法。如何理解克隆呢?克隆就是类似复制的意思,这里的合约克隆是指:克隆合约和原合约具有相同的逻辑功能。而且创建克隆合约的成本比直接部署原合约低,部署克隆合约的前提是得有一个原件。
2. 原理复现 2.1 工作原理 一说到代理,首先就会想到代理合约,合约升级。但是ERC1167
不是合约升级,它只是负责合约的调用转发。
可升级合约的代理合约架构:
整个架构中存在一个代理合约和多个逻辑合约,只有一套数据(即代理合约的数据),需要升级时则替换掉代理合约中的逻辑合约,而且同一时间只能存在一个逻辑合约。
Minimal Proxy Contract
合约架构:
整个架构中存在多个代理合约和一个逻辑合约,有多套数据分别存储在不同的代理合约中,所有代理合约共享逻辑合约的执行逻辑,同一时间存在多个代理合约。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(); } }
先部署 Demo
合约
部署CloneLib
合约,并调用clone
函数,并传入Demo
合约地址
最后部署Test
合约,并调用call
函数,传入克隆地址
结果:
可以看到结果返回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
部分的内容按克隆合约的逻辑编写,即照样也可以完成相同的要求。
执行操作相同,执行结果为:
3. 节省gas费用 通过部署原合约和部署克隆合约所需的gas费的多少来判断
部署Demo
所需的gas费用为:"122325"
部署克隆合约所需的gas费用为:"63334"
可以看到几乎是节省了一倍的花销。