😎

create2 で予測可能なスマートコントラクトアドレスを実現する

2023/05/19に公開

最近 AA 周りを調査していますが、その中で1つ面白い知識がありました。表題どおり、create2 という opcode を使って予測可能なスマートコントラクトアドレスを実現する ということです。そもそもスマートコントラクトをデプロイする際にアドレスがどう決められるかも知らなかかったので、合わせて紹介します。

結論を先に書いておきます。

  • 通常のスマートコントラクトデプロイ (つまり create) の場合: keccak256(rlp([sender, nonce]))[12:]
  • create2 の場合:keccak256( 0xff ++ sender ++ salt ++ keccak256(init_code))[12:]

通常のスマートコントラクトデプロイの場合

スマートコントラクトをデプロイということは、特殊のトランザクションを作成してイーサリアムネットワークに公開することになります。

トランザクションの特別なところは、

  • to は、通常の場合送信先のアドレスになりますが、スマートコントラクトのデプロイの場合は、to は空になります
  • input は、通常の場合スマートコントラクトを実行するためのパラメータデータになりますが、スマートコントラクトのデプロイの場合は、スマートコントラクトをコンパイルしてできたバイトコードになります

このトランザクションが最終的にイーサリアムのノードでマイニングされる際に、新しいアドレスが計算され、そのアドレスとバイトコードと初期ステータスを state DB に保存します。

気になるアドレスの計算処理は、go-ethereum のソースは下記になっています。

// https://github.com/ethereum/go-ethereum/blob/master/core/vm/evm.go#L507-L511
// Create creates a new contract using code as deployment code.
func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {
	contractAddr = crypto.CreateAddress(caller.Address(), evm.StateDB.GetNonce(caller.Address()))
	return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr, CREATE)
}

// https://github.com/ethereum/go-ethereum/blob/master/crypto/crypto.go#L106C1-L110
// CreateAddress creates an ethereum address given the bytes and the nonce
func CreateAddress(b common.Address, nonce uint64) common.Address {
	data, _ := rlp.EncodeToBytes([]interface{}{b, nonce})
	return common.BytesToAddress(Keccak256(data)[12:])
}

つまり、通常デプロイの場合は、新しいスマートコントラクトのアドレスは、デプロイの送信者とその送信者の nonce によって決められています。

CREATE2 でデプロイする場合

create2 は EVM の opcode であり、EIP 1014 で導入されました。

create2 の場合、新しいスマートコントラクトのアドレスは、下記4つの要素によって決められます。

  • 0xff:固定値、create でデプロイするアドレスと区別するために必要
  • sender:デプロイの送信者
  • salt:任意の値、デプロイの送信者が任意の値を指定できる
  • code:新しいスマートコントラクトのバイトコード

create2 のソースは下記になっています。

// https://github.com/ethereum/go-ethereum/blob/master/core/vm/evm.go#L513-L521
func (evm *EVM) Create2(caller ContractRef, code []byte, gas uint64, endowment *big.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {
	codeAndHash := &codeAndHash{code: code}
	contractAddr = crypto.CreateAddress2(caller.Address(), salt.Bytes32(), codeAndHash.Hash().Bytes())
	return evm.create(caller, codeAndHash, gas, endowment, contractAddr, CREATE2)
}

// https://github.com/ethereum/go-ethereum/blob/master/crypto/crypto.go#L112-L116
func CreateAddress2(b common.Address, salt [32]byte, inithash []byte) common.Address {
	return common.BytesToAddress(Keccak256([]byte{0xff}, b.Bytes(), salt[:], inithash)[12:])
}

create2 を使ってスマートコントラクトをデプロイするサンプルは、下記が参考になります。

https://github.com/miguelmota/solidity-create2-example

まとめると、create2 の場合、code は分かるので、新しいスマートコントラクトのアドレスは、sendersalt を決めれば、予測可能になります。

create2 は何が嬉しいのか

予測可能と言われてもピンとこないかと思いますが、入力値が同じであれば、出力値が必ず同じ値になる ということです。言い換えると、入力値が同じであれば、異なる環境でも同じ結果が得られるということです。

応用例として、下記のようなものがあります。

  • 一番分かりやすいのは、 異なるネットワーク上で同じアドレスを使うことができる ということです
    • testnet と mainnet の間だけではなく、イーサリアム以外の、EVM サポートのあるブロックチェーンでも同じアドレスを使うことができます
    • さらに、L1 だけではなく、L2 でも EVM サポートされていれば、同じアドレスを使うことができます
    • DApp を作成した経験があれば分かると思いますが、異なるネットワーク上でのアドレス管理は、かなり面倒です。create2 を使うことで、アドレス管理の負担を軽減することができます
  • また、ストレージとガスの節約 もできます
    • 入力値を分かればアドレスを計算できるので、必要になるまでコントラクトをデプロイしないことで、ブロックチェーンのストレージとガスを節約することができます

特に1番目の ことなるネットワーク・チェーン上で同じアドレスを得られる ということは、最近の web3 のサービスは複数チェーンの展開が多くなってきたため、非常に重要な機能になると思います。

まとめ

create2 は、スマートコントラクトのアドレスをよりコントロールできるようになる opcode です。特に、異なるネットワーク・チェーン上で同じアドレスを得られるということは、非常に重要な機能になると思います。

それでは、Happy Hacking!

Discussion