🔍

スマートコントラクトの Proxy 先の取得

2022/03/26に公開

Ethereum など EVM 系で動くスマートコントラクトについてのお話です。
スマートコントラクトには Upgradable とか Proxy とか言われる手法があります。ここでは詳しい説明は省かせて頂きますが、簡単にいうと、実際に呼び出されるコントラクトの裏に実装のコントラクトがあるという状態です。

今お手伝いさせて頂いている N Suite という鍵管理のプロダクトの改善の一環で「コントラクトのアドレスを与えられたら、それが Proxy ならその先の情報も自動取得する」という必要が出てきました。

そのために行った調査の結果をせっかくなのでここで共有したいと思います。

動機と前提

その前に少しだけ、N Suite でなぜこの調査を行ったかの説明をしておきます。興味のない方は読み飛ばして 調査結果 だけ読んで頂いて結構です。

動機

N Suite はコントラクトの管理や実行をチームで扱うためのプロダクトで、作成したコントラクトを任意のネットワークにデプロイしたり、既にネットワークに存在しているコントラクトを登録して任意のメソッドを呼び出したりできます。
この時、登録しようとしているコントラクトが Proxy だった場合に、その Proxy 先の情報までユーザが取ってきて登録するのは不便です。
また、Etherscan などで verify 済ならそこの API で Proxy 先の取得は簡単ですが、企業のクローズドなコントラクトで verify されていない場合でも、その Proxy 先のアドレスぐらいは出来れば自動で表示してあげたいです。

前提

という訳で、verify されていないコントラクトが Proxy かどうか、そしてその Proxy 先のアドレスを自動でどこまで判別できるかの調査です。ユーザがソースコードなどをまるっと build-info の状態でアップしてもらうのは避けたいですが ABI を提供するのはありとします。

まとめるこうなります。

  • 目的
    • Proxy かどうか判別すること。
    • Proxy ならその先のアドレスを取得すること。
  • 制限
    • コントラクトは verify されていない。
    • ソースコードはない。
    • ABI ぐらいはアップしてもらってもよい。

調査結果

(以下、調査結果は「である」調でいきます)

分類

Proxy 自体は delegatecall を使えばいくらでも独自実装できる。
ここでは、およそ一般的であろう実装パターンを調査した。
大きく分けると特定の SLOT に Proxy 先を格納するパターンと、そうでないパターンがある。

特定の SLOT に Proxy 先を格納するパターン

SLOT とはアドレスに紐付いた32バイトの格納場所のことで、JSON-RPC の eth_getStorageAt で取得が可能になっている。

EIP-1822: Universal Upgradeable Proxy Standard (UUPS)

古いが、UUPS と呼称されアイデア自体は引き継がれている。

Proxy 先を変更する際の互換性を確認するための proxiable などを提唱している。

Proxy 先のコントラクトにも「Proxy されている」ことを意識する実装を求める。

SLOT = keccak256("PROXIABLE")
0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7

EIP-1967: Standard Proxy Storage Slots

おそらく現時点で最も広く一般的に使われており、OpenZeppelin を使うと自動的にこれになる。

SLOT として implementation, beacon, admin のアドレス用の3つを定義している。

SLOT への値の更新は event で通知することが求められている。

Proxy 先として implementation と beacon のいずれかを選択できる仕様になっており、beacon を使う場合には implementation を空にすることになっている。

  • implementation の場合はそのアドレスが直接 Proxy 先となる。(これが OpenZeppelin でのデフォルト)
  • beacon の場合はそのアドレスの Read メソッド implementation() を呼び出して Proxy 先を得る必要がある。
SLOT = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1))
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
SLOT = bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)
0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50

OpenZeppelin’s Unstructured Storage Proxies

定義としては SLOT に格納するものを指すので、EIP-1822 や EIP-1967 の SLOT に格納してもこの規格に含まれることになる。

OpenZeppelin がかつて使っていた SLOT はこれ

SLOT = keccak256("org.zeppelinos.proxy.implementation")
0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3

0x4Fabb145d64652a948d72533023f6E7A623C7C53

SLOT に格納しないパターン

OpenZeppelin’s Eternal Storage

古い。最新の OpenZeppelin のドキュメントには載ってない。

Proxy 先の取得が標準化されているわけではないので汎用的には対応不可。

EIP-897: DelegateProxy

古いし、Draft のままになっていて、普及しているとは言えない。

Read メソッドの implementation が Proxy 先のアドレスを返す仕様になっている。

その他

EIP-2535: Diamonds, Multi-Facet Proxy

現時点(2022年3月)ではまだ協議中。

協議中とはいえ採用しているコントラクトがあるかもしれないが、考慮に含めないことにする。

Poxy 先の取得

SLOT に格納されている場合

SLOT が分かれば取得は容易だが、ABI からはどの SLOT かなどは判定できない。

メジャーであると思われる SLOT にアクセスしてみるのが現実的と思われる。

SLOT の優先度としては以下の通り

  1. eip1967.proxy.implementation
  2. eip1967.proxy.beacon
  3. org.zeppelinos.proxy.implementation
  4. PROXIABLE (EIP-1822)

ただし、最後の PROXIABLE は具体的な事例に遭遇してから導入を検討しても十分だと思われる。

SLOT に格納されていない場合

標準化されていないものに関しては対応はまず無理と考えていい。

DelegateProxy パターンの implementation の Read メソッドを呼ぶ手法は実施が容易であり、かつ、自前実装でも対応している可能性があるので、試してみる価値はある。

ただし ABI に implementation の Read メソッドがあったとしても Proxy のそれを指している確証は無い。

Proxy か否かの識別

etherscan では delegatecall がバイトコードに含まれているかどうかでチェックしていると思われる。
(根拠はこの medium の記事)
https://medium.com/etherscan-blog/and-finally-proxy-contract-support-on-etherscan-693e3da0714b

バイトコードの取得は可能ではあるがそれの解析なども含めて考えると、ほとんどのコントラクトが Proxy ではないことを考えると効率的ではない。それに Uniswap V3: Router 2 のように delegatecall を使っているだけで Proxy であると誤判定する可能性もある。

OpenZeppelin を素直に使っているものであれば ABI で判定はできるが、EIP-1967 などの規格ではメソッド名を定義しているわけではないため確実ではない。

実用面で考えると Proxy 先がわからなければ Proxy ではないと判定して進めても問題ないと考えられる。ただし、その場合 ユーザが Proxy 先を指定するという選択肢がある事が望ましい。

結論

コントラクトアドレスが与えられたら、SLOT の優先度 1〜3 をチェックして、Proxy 先が見つかればそのコントラクトは Proxy であると判断する。ABI はこの時考慮しない。

Proxy であるにも関わらず、Proxy ではないと誤判定である可能性もある。しかしその逆はない。

Proxy ではないと判定してもなお、ユーザに Proxy であるとして Proxy 先を指定させる方法の検討が求められる。

最後に

以上までが調査結果です。
結果的には ABI を必要とせずにアドレスだけでここまで出来ることが判りました。逆に ABI だけでは判定は難しい事もわかりました。

個人的には EIP-2535 が整備されてより複雑なコントラクトが増えていくのかなぁと感じています。

もし、お気づきの点があればご指摘戴けると非常に助かります。
皆さんが Proxy コントラクト関係で悩んだ時に、この資料が何かの参考になればとても嬉しいです。


N Suite を開発している double jump.tokyo株式会社 では仲間を募集中です。
https://www.wantedly.com/projects/721528

Discussion