本文介绍了 CREATE3 的设计目的及实现细节,分析了其与 CREATE 和 CREATE2 的区别,具体阐述了新的合约如何在多链环境下保持相同地址的机制,并提供了相关注意事项与参考资料。
本文將介紹 CREATE3 的用途、實作以及注意事項。
EVM-compatible chains 百家爭鳴,開發者在部署時會考慮讓多鏈上的 dApps 合約地址相同,在管理上會比較方便,也讓前後端和其他合約用相同地址串接。
目前 EVM 部署合約的 opcode 有 CREATE 和 CREATE2,曾有 EIP-3173 提案新增 CREATE3 opcode,因其想達成的目的可被合約實作,所以直到現在都沒有新增此 opcode 的計畫。
在看 CREATE3 之前,先來看看 CREATE 和 CREATE2 的目的和遇到的問題吧。
在合約執行過程中部署新合約。
與使用 EOA 將 init_code
送至 address(0)
的規則一樣,新合約地址是根據 sender_address
和 sender_nonce
決定。因為合約 nonce
只在部署合約時會從 1 開始遞增,且此合約的地址也須固定,因此想讓新合約在多鏈上有相同地址是不太容易管理的。
address = keccak256(rlp([sender_address, sender_nonce]))[12:]
init_code
決定而非 sender_nonce
,也就是新合約地址在編譯完成後就確定了,而非根據部署時的鏈上 sender_nonce
決定。init_code
包含 constructor
的參數,因此當各鏈上需要給定不同 constructor
參數時,新合約地址會不同。address = keccak256(0xff + sender_address + salt + keccak256(init_code))[12:]
sender_address
和 salt
決定。也就是就算個各鏈上新合約的 init_code
不同 (像是 constructor
參數),也能部署在相同位置。salt
和新合約的 init_code
fixed_init_code
的合約,稱之為 CREATE2 Proxy。因為 sender_address
(CREATE3 Factory)、 salt
和寫死的 init_code
都相同,所以各鏈的 CREATE2 Proxy 地址也是相同的。deployed_code
中包含的 CREATE opcode 會部署新合約 。因為 sender_address
(CREATE2 Proxy) 和 sender_nonce
(從 1 開始) 都相同,所以各鏈新合約的地址也是相同的。需要注意的是,此 CREATE2 Proxy 只會用於此部署交易時,也就是下次要部署其他新合約時會帶不同的 salt
並部署另一個 CREATE2 Proxy。上面的步驟如下圖,CREATE 和 CREATE2 拿到的參數都已在送部署交易前就決定好了,所以新合約地址也能事前就確定下來。
CREATE3 flowchart
由上圖可知新合約地址的計算如下。因此從使用者來看,會影響新地址的因素是自己提供的 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:]
一個有趣的的地方是步驟 3 寫死的 CREATE2 Proxy fixed_init_code
:
67_ 36_3d_3d_37_36_3d_34_f0_3d_52_60_08_60_18_f3
https://github.com/transmissions11/solmate/blob/main/src/utils/CREATE3.sol
上圖中 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 部署。
RETURNDATASIZE
(0x3d) 將 0
放到 stack
,相較於 PUSH1 0
省一點 gas。CREATE
(0xf0) 會將新合約地址放回 stack
,但下一步並沒有將新合約地址回傳,而是在 CREATE3 Factory 計算新地址再檢查 code.length > 0
。因為回傳新地址會使 deployed_code
從 8 個變成 15 個 opcodes,也就是降低 CREATE2 Proxy 大小而使部署合約的 gas 變低。計算新地址的方式是先算出 CREATE2 Proxy 地址,再和 nonce (1) 做 RLP encoding,如下圖:https://github.com/transmissions11/solmate/blob/main/src/utils/CREATE3.sol
在步驟 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 了。
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 的審閱與建議。
- 本文转载自: medium.com/taipei-ethere...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!