はじめに
初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回はスマートコントラクトのアップグレードについて解説していきます。
以下の勉強会で使用したスライドをもとに解説していきます。
勉強会の動画もあります。
また、より詳しいことは以下の記事にまとめています。
前置きは早々に早速中身を見ていきましょう!
コントラクトのアップグレードとは?
コントラクトは更新できないのではないか?
「コントラクトはアップデートできない。」
このように聞いたことはあると思います。
ではコントラクトをアップグレードするとはどういうことでしょうか?
先ほど言ったことは真実で、コントラクト自体を編集してアップグレードすることはできません。
ここで言っているアップグレードとは、「コントラクトを新しいコントラクトに置き換える」ということです。
つまり、1つのコントラクトを編集して新しい状態にするのではなく、新たにコントラクトを作成してそちらを使用するということです。
アップグレードする具体的な場面
ではアップグレードする場面としてはどのような場面があるでしょうか?
今回は具体例をもとに説明していきます。
例えば資金を入出金できるコントラクトがあるとします。
実はこのコントラクトには脆弱性がありますが、ユーザーや運営はそれを知らずコントラクトを使用してしまいます。
ある日、運営が脆弱性に気付きます。
しかし、コントラクトをアップグレードできない状態だと、ハッカーに資金を抜かれ放題になってしまいます。
ここでコントラクトのアップグレードが役立ちます。
コントラクトをアップグレードすることで、脆弱性を修正することができ資金を抜かれる心配がなくなります。
CEIパターン
ここでちょっとした補足なのですが、先ほどのコントラクトの脆弱性は「CEIパターン」に沿っていないことが原因となっています(小さくて読みにくいですが画像内のコントラクトはちゃんと脆弱性がある状態になっています)。
資金を引き出す処理をもとに「CEIパターン」を説明します。
Check
まずは「Check」、確認をする必要があります。
- 引き出せる資金はいくらか?
- 資金を預け入れているアドレスか?
上記のような確認をまずは行います。
Effect
次に「Effect」、データの書き換えを行います。
実際の処理を行う前にコントラクト内のデータを書き換えることが大切です。
Interaction
次に「Interaction」、具体的な処理を実行します。
CEIパターンに従わないとどうなる?
では「CEIパターン」に従わないとどうなるのでしょうか?
例えば「Check→Interaction→Effect」の順に実行するとします。
「条件を確認→資金を送金→データを書き換える」
一見良さそうですが、資金を送金した後に処理がそこで終わってしまったらどうでしょうか?
データが書きかわらないため、再び資金を引き出すことができてしまいます。
これが「リエントランシー攻撃」と言われる有名な攻撃手法です。
リエントランシー攻撃については以下の記事で詳しく説明しています。
コントラクトをアップグレードできるメリット・デメリット
コントラクトをアップグレードすることはもちろんメリットもありますが、デメリットも存在します。
メリット
メリットは上記4点が挙げられます。
機能追加
新機能の追加を行いたい時に役立ちます。
ユーザーからの信頼性
脆弱性があっても修正ができる状態ということで、ユーザーからの信頼性が高まります。
脆弱性の修正
先ほども説明しましたが、何かしら脆弱性があるときに修正できます。
コントラクトに長期的価値を与える
Web3という業界は進展が早いです。
そのため新しい技術がどんどん出てくるため、コントラクトをアップグレードできる状態にしておくと新技術の実装をすることができるようになります。
これによりコントラクトが古くならず長期的に使用できるようになります。
次にデメリットについて説明します。
デメリットは画像内の4点が挙げられます。
セキュリティリスク
コントラクトをアップグレードすることにより脆弱性が生まれる危険性ももちろんあります。
そのためアップグレードした際は監査に出したりして、脆弱性がないかチェックをしっかりする必要があります。
ガス代向上
コントラクトをアップグレードすることにより、以前と同じ機能部分のコード量が増えることがあります。
そのため若干ですがガス代が向上する可能性があります。
信頼性の低下
先ほどメリットでは信頼性が向上すると述べましたが、逆に信頼性が低下する可能性もあります。
理由としては、運営が好きなようにコントラクトを変更できてしまうからです。
こっそり運営にメリットを与えるようにコントラクトを変更できてしまいます(有名なプロジェクトであればすぐバレると思いますが...)。
互換性の問題
新しいコントラクトにしたことで、以前のコントラクト互換性がなくなると使用するユーザーやそのコントラクトを使用しているエンジニアが混乱する可能性があります。
そのため互換性は意識しておくことが大切です。
ストレージスロット
この部分では、コントラクト内のストレージスロットの説明をしていきます。
と思ったのですが、以下の記事でより詳しく説明しているため参考にしていただけると嬉しいです!
Dekegatecakk
次に「Delegatecall」について説明していきます。
こちらも以下の記事でより詳しく説明しているので、参考にしていただけると嬉しいです!
Proxyコントラクト
Proxyコントラクトとは、「Proxy」という意味の通り「代理のコントラクト」になります。
主な役割としては、他のコントラクトを呼び出す処理を行うコントラクトです。
アップグレードの仕組み
では次にアップグレードの仕組みを見ていきましょう。
Upgradableの仕組み
現在青色のコントラクトを使用しています。
Proxyコントラクトは青色のコントラクトを呼び出しています。
しかし、新機能の追加や脆弱性の修正のためにコントラクトを新しくしたいとします。
新機能の追加や脆弱性の修正を行った黄色のコントラクトを用意します。
そして、Proxyコントラクトが呼び出すコントラクトを黄色のコントラクトに差し替えます。
やっていることとしてはこれだけです!
だいぶシンプルですよね。
もう少し詳しく説明すると、Proxyコントラクトは「Storageコントラクト」とも呼ばれ、データはこのコントラクト内に保存されます(詳しくはDelegatecallの章を参考にしてくだい)。
そして呼び出すコントラクトは、「Logicコントラクト」とも呼ばれ、具体的な処理を行なっています。
「Storageコントラクト」と「Logicコントラクト」はそれぞれアドレスを持っていて、Proxyコントラクトはこのアドレスを使用して「Logicコントラクト」を呼び出しています。
ここで疑問が湧いてきます。
「いやいや、Proxyコントラクトなんて使用せずに直接コントラクト呼び出した方がシンプルでわかりやすくない?」
しかし、上記の理由からそれはあまり良いとは言えません。
「こっちのコントラクトを使ってください!」と告知しないといけない。
新しいコントラクトを公開した後に、ユーザーやエンジニアに「古いコントラクトは使っても意味ないので、新しいコントラクトを使用してください!」と伝える必要が出てきます。
しかし、これは現実的ではありません。
全ユーザーがその告知に気づくことはまずあり得ません。
古いコントラクトに資金を送ると、誰も取り出せなくなる可能性がある
間違って古いコントラクトに資金を送ってしまい一生取り出せなくなる心配もあります。
データの移行にガス代が結構かかる
古いコントラクト内のデータを全て新しいコントラクトに移すために、膨大なガス代と時間がかかりません。
これらの観点からProxyコントラクトを使用せず、直接コントラクト呼び出すのは難しいと理解できると思います。
Proxyコントラクトを使用することで、ユーザーは処理を実行するコントラクトがどれか考える必要がなくなり、データの移行も不要になります。
Upgradableの注意点
コントラクトをアップグレードするとき、いくつか注意点があります。
ストレージの衝突
まずはストレージの衝突です。
ストレージスロットの部分で説明しているように、コントラクト内の変数の定義順にデータがストレージに保存されていきます。
画像左がProxy(Storage)コントラクト内のストレージデータで、画像右が新しい(Logic)コントラクト内のストレージデータだと思ってください。
新しいコントラクト内で、既存のコントラクト内の変数定義順と異なる順番で変数を定義すると、予想外の挙動を起こしてしまいます。
例えば、新しいコントラクト内で「WalletAddress」という変数を呼び出すと「name」という変数の値が取得されるなどです。
これはデータが保存されているコントラクトと、処理を実行するコントラクトが異なるために起こる現象です。
データが保存されているProxyコントラクトの変数定義順と、処理を実行するコントラクトをの変数定義順は要注意です。
関数の衝突
変数のつぎは関数の衝突も問題となってきます。
Proxyコントラクトは、呼び出し先のコントラクト内の関数を判別するとき、関数名の先頭4Byteを使用しています。
(4Byteの理由はガス代の節約のためです)
しかし、先頭4Byteだと2つの関数で重複してしまう可能性があります。
そうなると、「Logicコントラクト内のAという関数を実行したはずなのに、実際にはProxyコントラクト内のBという関数が実行されてしまう。」、なんてことが起こり得ます。
constructorが使えない
constructorとは、コントラクトをデプロイされた時に1回だけ実行される関数です。
コントラクトのアップグレードでは、データはProxyコントラクト内の保存されているため、デプロイ時に何かしらのデータをLogicコントラクト内の保存しても意味がないということです。
その他のアップグレードパターン
ここまででコントラクトのアップグレードについて説明してきました。
実はコントラクトのアップグレードにはいくつかパターンが存在します。
気になる方は以下の記事を読んだり、ご自分でも調べてみてください。
最後に
今回はスマートコントラクトのアップグレードについて解説してきました。
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
採用強化中!
CryptoGamesでは一緒に働く仲間を大募集中です。
この記事で書いた自分の経験からもわかるように、裁量権を持って働くことができて一気に成長できる環境です。
「ブロックチェーンやWeb3、NFTに興味がある」、「スマートコントラクトの開発に携わりたい」など、少しでも興味を持っている方はまずはお話ししましょう!