CREATE3 多鏈部署合約於相同地址
本文將介紹 CREATE3 的用途、實作以及注意事項。
EVM-compatible chains 百家爭鳴,開發者在部署時會考慮讓多鏈上的 dApps 合約地址相同,在管理上會比較方便,也讓前後端和其他合約用相同地址串接。
目前 EVM 部署合約的 opcode 有 CREATE 和 CREATE2,曾有 EIP-3173 提案新增 CREATE3 opcode,因其想達成的目的可被合約實作,所以直到現在都沒有新增此 opcode 的計畫。
在看 CREATE3 之前,先來看看 CREATE 和 CREATE2 的目的和遇到的問題吧。
CREATE
目的
在合約執行過程中部署新合約。
問題
與使用 EOA 將init_code
送至 address(0)
的規則一樣,新合約地址是根據 sender_address
和 sender_nonce
決定。因為合約 nonce
只在部署合約時會從 1 開始遞增,且此合約的地址也須固定,因此想讓新合約在多鏈上有相同地址是不太容易管理的。
address = keccak256(rlp([sender_address, sender_nonce]))[12:]
CREATE2
目的
- 同 CREATE,差別是新合約地址是根據
init_code
決定而非sender_nonce
,也就是新合約地址在編譯完成後就確定了,而非根據部署時的鏈上sender_nonce
決定。
問題
init_code
包含constructor
的參數,因此當各鏈上需要給定不同constructor
參數時,新合約地址會不同。
address = keccak256(0xff + sender_address + salt + keccak256(init_code))[12:]
CREATE3
目的
- 同 CREATE2,差別是新合約的地址只根據
sender_address
和salt
決定。也就是就算個各鏈上新合約的init_code
不同 (像是constructor
參數),也能部署在相同位置。
步驟
- 假設多鏈上已存在 CREATE3 Factory 且各鏈的地址都相同
- 開發者送部署交易至 CREATE3 Factory,交易內容包含
salt
和新合約的init_code
- CREATE3 Factory 中先用 CREATE2 部署
fixed_init_code
的合約,稱之為 CREATE2 Proxy。因為sender_address
(CREATE3 Factory)、salt
和寫死的init_code
都相同,所以各鏈的 CREATE2 Proxy 地址也是相同的。 - CREATE3 Factory 接著呼叫剛部署好的 CREATE2 Proxy,其
deployed_code
中包含的 CREATE opcode 會部署新合約 。因為sender_address
(CREATE2 Proxy) 和sender_nonce
(從 1 開始) 都相同,所以各鏈新合約的地址也是相同的。需要注意的是,此 CREATE2 Proxy 只會用於此部署交易時,也就是下次要部署其他新合約時會帶不同的salt
並部署另一個 CREATE2 Proxy。
流程圖
上面的步驟如下圖,CREATE 和 CREATE2 拿到的參數都已在送部署交易前就決定好了,所以新合約地址也能事前就確定下來。
由上圖可知新合約地址的計算如下。因此從使用者來看,會影響新地址的因素是自己提供的 salt
和互動的 create3_factory_address
。
new_address = keccak256(rlp([create2_proxy_address, 1]))[12:]
create2_proxy_address = keccak256(0xff + create3_factory_address + salt + keccak256(fixed_init_code))[12:]
fixed_init_code
一個有趣的的地方是步驟 3 寫死的 CREATE2 Proxy fixed_init_code
:
67_36_3d_3d_37_36_3d_34_f0_3d_52_60_08_60_18_f3
上圖中 36~f0 粗體字的這 8 個 opcodes 是最後存放在鏈上的 deployed_code
,也就是 CREATE2 Proxy 的合約內容。可以看到 36~f0 做的事情只是將新合約的init_code
從 calldata
複製到 memory
,並連帶 msg.value
去呼叫 CREATE 部署新合約。
而上圖中 67~f3 是將 deployed_code
放到 return data region 以完成 CREATE2 Proxy 部署。
- 小技巧 1:用
RETURNDATASIZE
(0x3d) 將0
放到stack
,相較於PUSH1 0
省一點 gas。 - 小技巧 2:
CREATE
(0xf0) 會將新合約地址放回stack
,但下一步並沒有將新合約地址回傳,而是在 CREATE3 Factory 計算新地址再檢查code.length > 0
。因為回傳新地址會使deployed_code
從 8 個變成 15 個 opcodes,也就是降低 CREATE2 Proxy 大小而使部署合約的 gas 變低。計算新地址的方式是先算出 CREATE2 Proxy 地址,再和 nonce (1) 做 RLP encoding,如下圖:
CREATE3 Factory
在步驟 1 時有個假設是各鏈已存在相同地址的 CREATE3 Factory。
一個作法是使用新的 EOA,並在各鏈拿到 native token 以支付 gas,接著在各鏈送出第一筆交易 (nonce = 0) 去部署 CREATE3 Factory,達成各鏈的 CREATE3 Fatcory 地址都相同。
另一個作法是用別人已在各鏈部署好的 CREATE3 Factory,像是 https://github.com/ZeframLou/create3-factory。其會用 msg.sender
和 salt
再做一次 keccak256
,因此可以保證不同人使用也不會算出相同新地址。缺點是想部署到沒有 CREATE3 Factory 的新鏈時,只能拜託原本的 deployer 了。
CREATE3 使用須知
D̶e̶v̶e̶l̶o̶p̶e̶r̶s̶ ̶u̶s̶u̶a̶l̶l̶y̶ ̶s̶k̶i̶p̶ ̶t̶h̶e̶ ̶m̶a̶n̶u̶a̶l̶?̶
如果部署的新合約有在 constructor
使用 msg.sender
,msg.sender
會是 CREATE2 Proxy!OpenZeppelin 的 Ownable
是個常見例子。請必須做對應的修改,像是 constructor
最後執行 transferOwnership
,以免憾事發生!
結語
CREATE3 結合 CREATE 和 CREATE2 的特性來減少生成合約地址的變因,從最初的 nonce
到 init_code
進而只剩下 sender_address
和 salt
。相對的是開發者需要更注意其中的細節,像是 constructor
中的邏輯、部署時不要用錯新合約 init_code
否則地址就會被佔用、多餘的部署成本 (CREATE3 Factory & CREATE2 Proxy) 以及較複雜的流程。
感謝 Kevin Mai-Hsuan Chia 的審閱與建議。