0x01 如何理解 EIP1167
在我们基于智能合约做应用的时候,很多时候需要创建同一个合约的很多份实例。比如在做 DeFi 借贷应用时,有可能需要对每个借贷单生成一个借贷合约实例,在做多签钱包时,当用户要创建一个新的多签钱包时,合约层相应的会创建一个新的多签合约实例。
这种情况下,我们往往会引入一个工厂合约,通过特定的参数来创建对应的合约实例。就像下面的多签合约工厂一样。
pragma solidity ^0.4.15;
import "./Factory.sol";
import "./MultiSigWallet.sol";
contract MultiSigWalletFactory is Factory {
/*
* Public functions
*/
/// @dev Allows verified creation of multisignature wallet.
/// @param _owners List of initial owners.
/// @param _required Number of required confirmations.
/// @return Returns wallet address.
function create(address[] _owners, uint _required)
public
returns (address wallet)
{
wallet = new MultiSigWallet(_owners, _required);
register(wallet);
}
}
这有个什么毛病呢?就是我们每创建一个新的合约实例,会把几乎一模一样的合约字节码也完全拷贝一份,放到新生成的合约地址所对应的代码存储空间里面。这样就造成了大量的合约代码在存储上不必要的冗余。
在这种情况下,有没有可能在保留必要状态的同时,重用已有代码的逻辑呢?
像下面这个图所展示的这样,我们通过代理合约去访问真正的业务逻辑实现合约,需要不同合约实例的时候我们创建代理合约实例就可以了,代码量比较大的合约实现保持一份不动。这样其实就可以节省不少空间了,节省空间就是节省 gas 费啊。
image.png
EIP1167 其实就是这种代理合约的更底层支持,让通过代理来创建新合约实例所花费的 gas 更少。
0x02 EIP1167 原理
EIP1167 所做的事情可以概括如下,它只是把这个步骤翻译成了字节码形式
- 接收请求数据
- 将请求数据通过 DELEGATECALL 指令传递给目标实现合约。
- 得到合约调用的返回数据
- 将结果返回给调用者或者将交易回滚
0x03 如何使用 EIP1167
我们可以 clone 工厂去动态创建代理合约并返回新合约的地址
contract CloneFactory {
function createClone(address target) internal returns (address result) {
bytes20 targetBytes = bytes20(target);
assembly {
let clone := mload(0x40)
mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
mstore(add(clone, 0x14), targetBytes)
mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
result := create(0, clone, 0x37)
}
}
}
可以进一步封装一下,比如:
contract WalletFactory is CloneFactory {
address Template = 0x692a70D2e424a56D2C6C27aA97D1a86395877b3A;
function createWallet() external returns (address newWallet) {
newWallet = createClone(Template);
}
}
0x04 注意事项
- 所要 clone 的合约构造函数应该是无参的
对一个合约来说,它编译之后会生成两种字节码,一种是用来创建合约的字节码,一种是用来运行合约的运行时字节码。构造函数会被编译为合约创建字节码,而通过 EIP1167 clone 的合约只是和运行时字节码有关。这就意味着构造函数是会被忽略的,如果是带参数的构造函数,需要替换为 setter 方法,然后在调用 clone 方法后马上调用这个 setter 方法进行初始化。
// constructor
constructor(address _owner) external {
owner = _owner;
}
// initializer
function setOwner(address _owner) external {
require(owner == address(0));
owner = _owner;
}
contract WalletFactory is CloneFactory {
address Template = 0x692a70D2e424a56D2C6C27aA97D1a86395877b3A;
function createWallet(address _owner) external returns (address newWallet) {
newWallet = createClone(Template);
newWallet.setOwner(_owner);
}
}
- 和构造函数类似,在函数外对状态变量的赋值也是合约实例初始化过程的一部分。这部分代码也不会被 “clone” 合约执行的,当我们使用 EIP1167,应当避免在模版合约中进行状态变量的函数外赋值。有两个替代性做法,一个做法是把所有在初始化过程中的赋值操作都放到用来做初始化的 setter 方法中。另一个做法是使用常量,因为常量是在编译期就确定了,所以不受影响。
// 使用 setter 方法
mapping(address => bool) public isOwner;
uint public dailyWithdrawLimit;
uint public signaturesRequired;
function set(address[] _owner, uint limit, uint required) external {
require(dailyWithdrawLimit == 0 && signaturesRequired == 0);
dailyWithdrawLimit = limit;
signaturesRequired = required;
//DO SOMETHING ELSE
}
// 使用常量
string public constant name = "DemoToken";
string public constant symbol = "DET";
- 尽管通过 EIP1167 创建代理合约可以节省大量创建合约的 gas 费,因为在方法调用时会用到 delegatecall,也会多少增加一点儿合约方法调用的费用。如果合约不是特别大,并且合约创建出来之后会被大量调用,EIP1167 并没什么优势。