簡介
工廠模式 (Factory Pattern) 是一種程式設計的設計模式 (Design Pattern),用來生成物件的一種方式。在智能合約開發中,有些情況會使用到。例如:Uniswap 的交易對或是生成 Token。只需要輸入一些不同的參數,像是名稱、總發行量之類的設定,就可以就快速部署功能相同的合約。我們可以透過智能合約中撰寫一些程式碼來產生新合約,本篇文章會介紹一些方法。
在智能合約中用來建立合約實例的指令有三種寫法:
new
create
create2
而根據實作的方法主要分為兩種:
- 部署整份合約
- 使用 Proxy
部署整份合約
先假設我們有一個這樣的合約
1 2 3 4 5
| import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract NormalToken is ERC20 { constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} }
|
我們希望在戀上可以快速的生成不同 Token。
new
如何一般物件導向語言的寫法,智能合約也可以用相同的方式直接 new
:
1 2 3 4 5 6 7 8
| contract NormalNew { event Create(address token);
function create(string memory name_, string memory symbol_) external { NormalToken token = new NormalToken(name_, symbol_); emit Create(address(token)); } }
|
消耗 Gas 約 529657。
create
另一種方式可以使用 create
,上面功能大約和接近下面寫法:
1 2 3 4 5 6 7 8 9 10 11
| function create(string memory name_, string memory symbol_) external { bytes memory bytecode = abi.encodePacked(type(NormalToken).creationCode, abi.encode(name_, symbol_)); address instance; assembly { instance := create(0, add(bytecode, 0x20), mload(bytecode)) if iszero(extcodesize(instance)) { revert(0, 0) } } emit Create(instance); }
|
消耗 Gas 約 537179,這寫法比較複雜,消耗的 Gas 也比較多,所以一般情況不會這樣使用。
create2
最後一種指令多了參數 salt
要帶入。
1 2 3 4 5 6 7 8 9 10 11
| function create(string memory name_, string memory symbol_, bytes32 salt) external { bytes memory bytecode = abi.encodePacked(type(NormalToken).creationCode, abi.encode(name_, symbol_)); address instance; assembly { instance := create2(0, add(bytecode, 0x20), mload(bytecode), salt) if iszero(extcodesize(instance)) { revert(0, 0) } } emit Create(instance); }
|
消耗 Gas 約 538382。同樣消耗的 Gas 比較多,不過和 create
不同的是,產生新合約的地址機制不同,可以用 salt
來看控制。create2
是在 EIP-1014 提出。
使用 Proxy
由於上面的方法每次部署完整的合約消耗比較多的 Gas,所以部署一次實作 (implementation) 合約,後面產生的實例都使用 Proxy 的方式,指向同一個實作。這樣就不需要每次部署完整的合約,只需要部署 Proxy 合約,以減少 Gas 消耗。未來有機會再說明 Proxy,基本上是使用 delegatecall
這個指令。
由於使用 Proxy,不能使用 constructor
,所以要先把 Token 的部分修改成以下寫法
1 2 3 4 5 6 7
| import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
contract ProxyToken is ERC20Upgradeable { function initialize(string memory name_, string memory symbol_) external initializer { __ERC20_init(name_, symbol_); } }
|
接著我們寫一個簡單的 Proxy 合約
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 35 36 37 38
| library StorageSlot { struct AddressSlot { address value; }
function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { assembly { r.slot := slot } } }
contract Proxy { bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
constructor(address _implementation) { StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = _implementation; }
function _delegate(address _implementation) internal virtual { assembly { calldatacopy(0, 0, calldatasize()) let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) switch result case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } }
fallback() external virtual payable { _delegate(StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value); } }
|
new
我們先部署一次 Token 合約,而之後每次要產生 Token 的時候,實際上只部署 Proxy 合約指向 Token 的實作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import "./ProxyToken.sol"; import "./Proxy.sol";
contract ProxyNew { ProxyToken token;
event Create(address token);
constructor() { token = new ProxyToken(); }
function create(string memory name_, string memory symbol_) external { Proxy instance = new Proxy(address(token)); ProxyToken(address(instance)).initialize(name_, symbol_); emit Create(address(instance)); } }
|
消耗 Gas 約 209725,比上面的方法少了很多。
create
把上面改寫成 create
的寫法
1 2 3 4 5 6 7 8 9 10 11 12
| function create(string memory name_, string memory symbol_) external { bytes memory bytecode = abi.encodePacked(type(Proxy).creationCode, abi.encode(address(token))); address instance; assembly { instance := create(0, add(bytecode, 0x20), mload(bytecode)) if iszero(extcodesize(instance)) { revert(0, 0) } } ProxyToken(instance).initialize(name_, symbol_); emit Create(instance); }
|
消耗 Gas 約 211212。
create2
最後是 create2
的寫法
1 2 3 4 5 6 7 8 9 10 11 12
| function create(string memory name_, string memory symbol_, bytes32 salt) external { bytes memory bytecode = abi.encodePacked(type(Proxy).creationCode, abi.encode(address(token))); address instance; assembly { instance := create2(0, add(bytecode, 0x20), mload(bytecode), salt) if iszero(extcodesize(instance)) { revert(0, 0) } } ProxyToken(instance).initialize(name_, symbol_); emit Create(instance); }
|
消耗 Gas 約 211904。
EIP-1167
上面可以看到 Proxy 的機制可以省下不少 Gas,不過我們還可以進一步優化。上面 Proxy 的合約是使用了 storage 的變數去儲存 implementation 的地址,但如果地址是一個固定值的話,就可以移除 storage 和相關存取的程式。假設我們的 implementation 地址已知是固定的 0xbebebebebebebebebebebebebebebebebebebebe
,那上面的 Proxy 可以修改成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| contract Proxy { fallback() external virtual payable { assembly { calldatacopy(0, 0, calldatasize()) let result := delegatecall(gas(), 0xbebebebebebebebebebebebebebebebebebebebe, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) switch result case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } } }
|
智能合約中不能動態編譯智能合約,但我們可以從 bytecode 下手,假想上面的 bytecode 是一段如下面的資料
1
| .....bebebebebebebebebebebebebebebebebebebebe....
|
我們只要替換掉地址的部分即可,前面和後面的程式為固定內容。基於這個想法,有人提出了 EIP-1167,他寫出了最優化的 bytecode 如下:
1
| 363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3
|
不過實際上要下面的 bytecode 才能運作
1
| 3d602b80600a3d3981f3363d3d373d3d3d363d71bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602957fd5bf3
|
由於要操作 bytecode,所以只能用 create
和 create2
的方式來改寫我們的程式。
create
簡單使用 abi.encodePacked
來組出 bytecode
1 2 3 4 5 6 7 8 9 10 11 12
| function create(string memory name_, string memory symbol_) external { bytes memory bytecode = abi.encodePacked(0x3D602d80600A3D3981F3363d3d373d3D3D363d73, address(token), bytes15(0x5af43d82803e903d91602b57fd5bf3)); address instance; assembly { instance := create(0, add(bytecode, 0x20), mload(bytecode)) if iszero(extcodesize(instance)) { revert(0, 0) } } ProxyToken(instance).initialize(name_, symbol_); emit Create(instance); }
|
消耗 Gas 約 162463,又比一般的 Proxy 少了一些。
create2
改成 create2
寫法
1 2 3 4 5 6 7 8 9 10 11 12
| function create(string memory name_, string memory symbol_, bytes32 salt) external { bytes memory bytecode = abi.encodePacked(0x3D602d80600A3D3981F3363d3d373d3D3D363d73, address(token), bytes15(0x5af43d82803e903d91602b57fd5bf3)); address instance; assembly { instance := create2(0, add(bytecode, 0x20), mload(bytecode), salt) if iszero(extcodesize(instance)) { revert(0, 0) } } ProxyToken(instance).initialize(name_, symbol_); emit Create(instance); }
|
消耗 Gas 約 163093。
使用套件
我們也可以使用 OpenZeppelin Clones 套件 來建立合約。
create
引用套件之後使用 clone
對應的是使用 create
指令,有兩個用法:
1 2
| function clone(address implementation) internal returns (address instance); function clone(address implementation, uint256 value) internal returns (address instance);
|
如果建立合約的同事要帶入 ether 的話可以用第二個函式填入 value。下面為使用套件後改寫:
1 2 3 4 5 6 7
| import "@openzeppelin/contracts/proxy/Clones.sol";
function create(string memory name_, string memory symbol_) external { address instance = Clones.clone(address(token)); ProxyToken(address(instance)).initialize(name_, symbol_); emit Create(instance); }
|
消耗 Gas 約 162352,比上面的寫法還省一點,套件又針對組合 bytecode 部分做了進一步優化。
create2
引用套件之後使用 cloneDeterministic
對應的是使用 create2
指令,一樣有兩個用法:
1 2
| function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance); function cloneDeterministic(address implementation, bytes32 salt, uint256 value) internal returns (address instance);
|
下面為使用套件後改寫:
1 2 3 4 5 6 7
| import "@openzeppelin/contracts/proxy/Clones.sol";
function create(string memory name_, string memory symbol_, bytes32 salt) external { address instance = Clones.cloneDeterministic(address(token), salt); ProxyToken(address(instance)).initialize(name_, symbol_); emit Create(instance); }
|
消耗 Gas 約 163037。
new 使用 salt
使用 new 也可以帶入 salt,功能對應於 create2,寫法如下
1
| NormalToken token = new NormalToken{salt: salt}(name_, symbol_);
|
延伸閱讀
Solidity 智能合約 - 地址生成規則
Solidity 智能合約 - 工廠模式的重組問題 (Reorg)
Solidity 智能合約 - 工廠模式的搶先交易問題 (Front Running)