1.スマートコントラクトのアップグレードついて
はじめに
本連載では、EthereumのSolidityを使ったスマートコントラクトのアップグレーダビリティ(アップグレード可能なこと:Upgradeability)に焦点を当てています。スマートコントラクトは、普通のアプリケーションとは違い、一度デプロイしたコントラクトのコードは変更できないという不変性を持っています。この不変性を特殊な方法を用いて、回避することで、開発の柔軟性の向上を図ることができる、「Upgradeability」のコンセプトをわかりやすく解説するのが本連載の目的です。今回は、「1.スマートコントラクトのアップグレードついて」です。アップグレードの概要・メリット・デメリットを解説し、アップグレードを担うProxy Contractの仕組みを紹介していきます。
関連する記事一覧
第2回 SolidityのUpgradeabilityとOpenZeppelinSDKとは
第3回 SolidityのUpgradeabilityとOpenZeppelinSDKとは
第4回 SolidityのUpgradeabilityとOpenZeppelinSDKとは
コントラクトのアップグレードとは
概要
イーサリアムのスマートコントラクトは、一度ブロックチェーンにデプロイすると後からコントラクトの中身に変更を加えることができません。これを「immutability(不変性)」と呼びます。コントラクトの不変性とは、コードの中身を勝手に変更できないという性質のことです。この不変性によりユーザーは安心してコントラクトに資産を預けたり、トークンを購入したり、DAppsを利用したりすることができます。コントラクトの不変性は、ユーザーに信頼を与える一方で、DApps開発には大きな制約を加えます。一般的に開発者は開発したアプリをその後も保守・運用していきます。アプリにバグが発生すれば、何かしらの対応をしなければなりません。またアプリの利便性を向上させるために、リリース後も追加の機能を実装していきます。しかしコントラクトのコードに変更を加えることができないため、開発者は普通のコードとは違うアプローチでDAppsを設計していかなければなりません。
Solidityには、バグ対応や新規機能の追加をするために「Upgradeability」というデザインパターンが存在します。コントラクトをアップグレード可能にすることで、上記の問題を解決できます。
メリット
追加による変更が可能
コントラクトがアップグレードできることで、開発者はコントラクトのバグ対応や新規機能の実装をデプロイ後も継続的に行うことが可能です。そのため、DAppsを長期的に開発して、UXを向上させることができます。
またDeFiやSTOのようにユーザーの資産を預かるサービスでは、新しい規制にサービスリリース後も柔軟に対応するために、このデザインパターンが実装されているケースがあります。またデプロイ後に脆弱性が発見された場合に、ハッカーに攻撃される前に取り除くことで、ユーザーの資産を保護することが可能です。
デメリット
コントラクトがアップグレードできてしまうことには、複数のデメリットを伴います。
ガバナンス
コントラクトの管理者が新しいコントラクトなどに変更をしてしまうことで、利用者がそのコントラクトを信用することができない場合があります。つまり不変性を損なうことで、ブロックチェーンの特徴である「管理者不要」、「Trustless(信用不要)」という特徴を失ってしまいます。
proxy contractは誰でも操作できると困るため、管理者をおく必要があります。この集権的なアプローチで、信頼を損なわないために、proxy contractの管理者をマルチシグウォレットに移譲して、m-of-n(複数人の中の数人による合意) による意思決定でアップグレードを実行するという手法があります。また投票を行うという手段も想定されます。アップグレードによって信用を失わない工夫が必要になります。
設計の複雑さ
イーサリアムは、もともとコントラクトのアップグレードに対応していないため、upgradeableに設計するには、複雑さを伴います。そして複雑なコードは脆弱性やバグを産みやすくなりがちです。またアップグレードに失敗する可能性もあります。
proxy contractについて
proxy contractとは
upgradeable contractには、複数のデザインパターンがありますが、それらの共通部分であり、基礎となる部分が「proxy contract」です。
「proxy」とは代理を意味しています。
今回は、ユーザの代理をして呼び出し先のコントラクトを呼び出します。 普通コントラクトに何か関数を実行させる場合、ユーザーのEOA(Externaly Owned Account)から対象のCA(Contract Account)に直接、トランザクションを実行します。
proxy contract はこのユーザーとコントラクトの間に入って、代理としてトランザクションを実行します。
Delegatecallについて
以下は、OpenzeppelinSDKで利用されているproxy contractの実装例です。
pragma solidity ^0.5.0;
contract Proxy {
function () payable external { //①
_fallback();
}
function _implementation() internal view returns (address);
function _delegate(address implementation) internal { //③
assembly {
calldatacopy(0, 0, calldatasize) // a
let result := delegatecall(gas, implementation, 0, calldatasize, 0, 0) // b
returndatacopy(0, 0, returndatasize) // c
switch result // d
case 0 { revert(0, returndatasize) }
default { return(0, returndatasize) }
}
}
function _willFallback() internal {
}
function _fallback() internal { //②
_willFallback();
_delegate(_implementation());
}
}
proxy contractが他のコントラクトの関数を呼び出すためには、「Delegatecall」を実装している必要があります。③で利用されているdelegatecallとは、呼び出し元のコントラクトのコンテキスト(この例では、proxy contract)で、外部のコントラクトの関数(この例では、proxy contractの先の呼び出し先contract)を実行することが可能になるものです。呼び出し先のcontractの実行者は、呼び出したEOA アドレス(msg.sender)であり、参照や変更を加える状態はproxy contract(呼び出し元コントラクト)のものになります。この方法は、主に外部のライブラリのメソッドの実行時に利用されます。 この例のdelegatecallの実装はAssembly言語が使われていて、実装の難易度は高いため、安易に利用すると脆弱性の元になります。
assembly部分のコード解説
a
calldatasizeを使用してmsg.dataのサイズを取得し、calldatacopyを使用してmsg.dataをcalldata領域の0の位置にコピーします。
b
関数の実行に必要なガス、呼び出し先コントラクトのアドレス(address implementationに格納されている。)、呼び出すメソッドのデータ(3つ目の引数場所で0となっている部分には、msg.dataが格納されている。)、渡すデータのサイズをdelegatecallの引数として渡し、外部呼び出しを行います。4・5番目の引数であるoutとoutsizeは、サイズがわからないため0です。
c
returndatasizeで返されたデータのサイズを利用して、返されたデータの中身をcalldata領域の0の位置にコピーします。
d
データを返します。エラー時に0を返します。
参考
Proxy Patternshttps://solidity.readthedocs.io/en/v0.4.21/assembly.html
Fallback関数について
そしてDelegatecallは、外部コントラクトの任意の関数を呼び出せる必要があります。そのために①にFallback 関数が実装されています。コントラクトはサポートしていな関数の呼び出しが行われると、fallback 関数を実行するようになっています。proxy contractでは特別なfallback 関数を用意して、外部コントラクトへの呼び出しをリダイレクトします。
fallback関数には名前がありません。主にコントラクトにETHを送金するために実装されます。引数を明示的に渡すことはできませんが、msg.dataを利用してfallbackのpayloadに直接任意のデータを渡すことが可能です。これにより、delegatecallに呼び出したい関数やその引数の情報が渡せます。またfallback関数は返り値が存在しないため、実行の成功の有無を論理値で返すように設定されています。
Fallback関数①、_fallback②、_delegate③の順に呼び出されます。
solidity document Fallback Function
https://solidity.readthedocs.io/en/v0.5.13/contracts.html#fallback-function
ストレージコントラクトとロジックコントラクトについて
一般的にスマートコントラクトがデプロイされると、コントラクト内の状態は、そのコントラクトアドレスにひもづく形でブロックチェーン上に保存されます。
一方でdelegatecallを利用して他のコントラクトのロジックを呼び出すと、呼び出し元のコントラクトのコンテキストで処理が実行されます。つまり、コントラクト状態は、呼び出し元のproxy contractが保持することになります。なのでproxy contractを「ストレージコントラクト」、関数などのロジックをもつコントラクトを「ロジックコントラクト」と呼ぶことがあります。
コントラクトのアップグレード時は、このロジックコントラクトを変更します。ストレージコントラクトはdelegatecallをするためにデプロイ済みのロジックコントラクトのコントラクトアドレスを保持しておく必要があります。アップグレード時は、ストレージコントラクトが保持するコントラクトアドレスを変更することで、ロジックのアップグレードが実行できます。但し、ストレージコントラクト(proxy contract)はアップグレードできないため、完全なupgradeabilityは持ち得ません。
まとめ
『第1回 UpgradeabilityとOpenZeppelinSDKとは』では、イーサリアムで本来実現不可能なスマートコントラクトのUpgradeabilityの仕組みを技術的に詳しく解説しました。『第2回 UpgradeabilityとOpenZeppelinSDKとは』では、3つのアップグレードパターンを掘り下げて解説していき、アップグレードを簡単に行うことが可能になる2つのツールについて、紹介していきます。
参考
アップグレード可能なスマートコントラクトを実現する具体的なアプローチ