(舊文) 用 CREATE2 原地換 Code

Luoh Ren-Shan (LCamel)
Taipei Ethereum Meetup
6 min readAug 18, 2022

注意: CREATE2 在 EIP-6780 後已不適用了.

Ethereum 自從引入 opcode CREATE2 後, 同一個 address 可能會換上不一樣的 code. 我們來實驗看看, 如何用 CREATE2 生成 / 自爆(SELFDESTRUCT) / 再原地換上新的 code.

New Contract Address

EIP-1014 這麼說:

Specification

Adds a new opcode (CREATE2) at 0xf5, which takes 4 stack arguments: endowment, memory_start, memory_length, salt. Behaves identically to CREATE (0xf0), except using keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:] instead of the usual sender-and-nonce-hash as the address where the contract is initialized at.

CREATE2 多了第四個參數 salt. 而 address 也多了對應的新規則 (重要):

傳統 CREATE / contract creation transaction 的 new contract address:
function addr(sender_address, nonce)
CREATE2 的 new contract address:
function addr(sender_address, salt, init_code)

以往 CREATE / contract creation transaction 因為 nonce 一直遞增, 所以無法重複生在同一個 address.
而 CREATE2 沒有用到 nonce! 所以只要我們從同一個 sender 送出 CREATE2 並帶著相同的 salt 和 init_code, 就可以把新的 contract 重複生到同一個 address 上.

SELFDESTRUCT 自爆

在 CREATE2 重複生到同一個 address 前, contract 要先自爆來空出位子. 這需要該 contract 直接呼叫 opcode SELFDESTRUCT, 或者間接 CALLCODE / DELEGATECALL 到其他含有 SELFDESTRUCT 的 contract.

相同的 init, 不同的 contract body

雖然我們想換掉 contract body (code), 但是因為 init code 變了 address 就會跟著變, 所以我們必須重用同一段 init code, 並在前後兩次產生不同的 contract body.

平常 Solidity compile 出來的 init code 是將夾帶的唯一一份 contract body 用 CODECOPY 的方式複製出來, 所以同一份 init code 會生出同一份 body.

然而 Ethereum 本身並沒有這樣的限制. 生出怎樣的 body 不光取決於 init code 的 binary 而已, 也取決於執行 init 時的 environment 和 state:

錯誤的想像:
function body(init_code)
正確的想像:
function body(init_code, environment, state)

所以如果 init 參考了 environment (如 value / time / block number), 或者參考了 state (如呼叫別的 contract 的 function 來下載 body, 或動態 EXTCODECOPY 不同的 contract 來用), 即使 init 沒變, 仍然可以生出不同的 body !

實作

我們需要準備兩段 code: 會生出不同 body 的 init, 和發送這段 init 的 CREATE2 factory contract.

實作細節建議先參考前一篇文章: Init.

Init

CREATE2 有個不會影響 address 又容易操控的參數: value (endowment). 為了 demo, 這裡選擇不參考 state, 而是在 init 中參考 value 來動態決定 body:

如果 value 是 0, 就生出 bomb contract body “0x3DFF”. 這個 contract 收到任何 message 都會 0xFF SELFDESTRUCT. (並把餘額送到指定的 address 0)如果 value 不是 0, 就生出另一個 contract body “0xCafeBabe”:

用嵌在 JavaScript 中的這段 assembler 編一下, 會得到 bytecode 34600F57613DFF6000526002601EF35B63CAFEBABE6000526004601CF3 .

如果有裝 geth, 可以用 geth 的 command line evm 餵不同的 value 先測看看 (搭配這份 state):

Factory

有了 init code, 再來需要寫個 factory contract, 用 CREATE2 把上面這段 init 發送到 new contract address 去執行.

回頭想想, 平常 address 0 雖然沒有實際的 code, 但是概念上就像個通用的 factory contract: 只要我們用 sendTransaction 把 init 當 data 送給它, 它就會把 init 送到目的地去執行(並傳送 value).

我們也來刻意模仿這個行為, 寫個半通用的 factory contract: 只要我們用 sendTransaction 把 init 當 data 送給它, 它就會把 init 透過 CREATE2 送到目的地去執行(並傳送 value).

RUN !

Init 和 factory 準備好, 就可以來 生成 / 自爆 / 再生成新的 code 了:

執行結果:

可以看到在生成/自爆之後, 因為用了 CREATE2, 所以第二次還是會 deploy 到同一個 address, 而此 address 的 code 也從 0x3dff 換成了 0xcafebabe !

Etherscan 也會在 contract tab 加上紅字 “ReInit” 和文字說明來提醒使用者這個 contract 被換了 code :

最後驗證一下我們的理解:

尾聲

從上面的例子可以看到, 如果:

  1. contract 有能力 SELFDESTRUT
  2. 可以重複 deploy 到同一個 address (因為引入了 CREATE2)

則這個 contract 就可以原地換 code.

完整的程式碼在這裡.

下一篇, 我們將繼續討論換 code 造成的影響, 以及如何用 CREATE 原地換 code.

(感謝 NIC LinPing Chen 的審閱與建議)

--

--