CREATE3 多鏈部署合約於相同地址

ZD Hu
Taipei Ethereum Meetup
9 min readOct 18, 2022

本文將介紹 CREATE3 的用途、實作以及注意事項。

EVM-compatible chains 百家爭鳴,開發者在部署時會考慮讓多鏈上的 dApps 合約地址相同,在管理上會比較方便,也讓前後端和其他合約用相同地址串接。

目前 EVM 部署合約的 opcode 有 CREATE 和 CREATE2,曾有 EIP-3173 提案新增 CREATE3 opcode,因其想達成的目的可被合約實作,所以直到現在都沒有新增此 opcode 的計畫。

在看 CREATE3 之前,先來看看 CREATE 和 CREATE2 的目的和遇到的問題吧。

CREATE

目的

在合約執行過程中部署新合約。

問題

與使用 EOA 將init_code 送至 address(0) 的規則一樣,新合約地址是根據 sender_addresssender_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_addresssalt 決定。也就是就算個各鏈上新合約的 init_code 不同 (像是 constructor 參數),也能部署在相同位置。

步驟

  1. 假設多鏈上已存在 CREATE3 Factory 且各鏈的地址都相同
  2. 開發者送部署交易至 CREATE3 Factory,交易內容包含 salt和新合約的 init_code
  3. CREATE3 Factory 中先用 CREATE2 部署 fixed_init_code的合約,稱之為 CREATE2 Proxy。因為 sender_address(CREATE3 Factory)、 salt 和寫死的 init_code都相同,所以各鏈的 CREATE2 Proxy 地址也是相同的。
  4. CREATE3 Factory 接著呼叫剛部署好的 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:]

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

https://github.com/transmissions11/solmate/blob/main/src/utils/CREATE3.sol

上圖中 36~f0 粗體字的這 8 個 opcodes 是最後存放在鏈上的 deployed_code ,也就是 CREATE2 Proxy 的合約內容。可以看到 36~f0 做的事情只是將新合約的init_codecalldata複製到 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,如下圖:
https://github.com/transmissions11/solmate/blob/main/src/utils/CREATE3.sol

CREATE3 Factory

在步驟 1 時有個假設是各鏈已存在相同地址的 CREATE3 Factory。

一個作法是使用新的 EOA,並在各鏈拿到 native token 以支付 gas,接著在各鏈送出第一筆交易 (nonce = 0) 去部署 CREATE3 Factory,達成各鏈的 CREATE3 Fatcory 地址都相同。

另一個作法是用別人已在各鏈部署好的 CREATE3 Factory,像是 https://github.com/ZeframLou/create3-factory。其會用 msg.sendersalt 再做一次 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.sendermsg.sender會是 CREATE2 Proxy!OpenZeppelin 的 Ownable 是個常見例子。請必須做對應的修改,像是 constructor最後執行 transferOwnership,以免憾事發生!

結語

CREATE3 結合 CREATE 和 CREATE2 的特性來減少生成合約地址的變因,從最初的 nonceinit_code 進而只剩下 sender_addresssalt。相對的是開發者需要更注意其中的細節,像是 constructor 中的邏輯、部署時不要用錯新合約 init_code 否則地址就會被佔用、多餘的部署成本 (CREATE3 Factory & CREATE2 Proxy) 以及較複雜的流程。

感謝 Kevin Mai-Hsuan Chia 的審閱與建議。

--

--

No responses yet