簡介

工廠模式 (Factory Pattern) 是一種程式設計的設計模式 (Design Pattern),用來生成物件的一種方式。在智能合約開發中,有些情況會使用到。例如:Uniswap 的交易對或是生成 Token。只需要輸入一些不同的參數,像是名稱、總發行量之類的設定,就可以就快速部署功能相同的合約。我們可以透過智能合約中撰寫一些程式碼來產生新合約,本篇文章會介紹一些方法。

在智能合約中用來建立合約實例的指令有三種寫法:

  1. new
  2. create
  3. create2

而根據實作的方法主要分為兩種:

  1. 部署整份合約
  2. 使用 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,所以只能用 createcreate2 的方式來改寫我們的程式。

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)