引き続きスマートコントラクトを変更できるいくつかのアプローチを分析します。
この記事は、少なくとも基本的な堅牢性プログラミングスキルを持ち、イーサリアムネットワークの基本原則を理解している人を対象としています。
スマートコントラクトをいくつかの関連するコントラクトに分割する
この場合、現在アクティブな契約のアドレスを任意の契約のストレージに保存できます。 多くの場合、単一のコントラクトが選択され、システム全体の一部へのリンクを保存および変更します。
例としては、トークン販売契約があります。これは、Eteraの出所であるウォレットに送信する必要があるトークンの数を計算するためのルールを明確に記述していません。 数量の計算は別の契約で行うことができ、必要に応じて置き換えることができます。 同様のアプローチが堅実さだけでなく使用されることが多いため、このオプションについては長い間検討しません。
このアプローチの主な欠点の1つは、システム全体の外部にあるコントラクトのインターフェイスを変更できないことです。 関数を追加または削除することはできません。
delegatecallを使用して、別のコントラクトへの呼び出しをプロキシする
EIP-7では、別のコントラクトからコードを呼び出すことができる命令が提案および実装されましたが、呼び出しコンテキストは現在のコントラクトと同じままです。 つまり、呼び出されたコントラクトは呼び出しコントラクトのストレージに書き込みます。msg.senderとmsg.valueは最初と同じままです。
ネットワーク上で、このメカニズムの実装のいくつかの例を見つけることができます。 すべてには、ソリッドアセンブリの使用が含まれます。 アセンブリなしでは、delegatecallから値を取得することはできません。
proxycallに委任呼び出しを使用するすべてのメソッドの主なアイデアは、フォールバック関数を実装することです。 その中のcalldataを読み取り、delegatecallを介して渡す必要があります。
いくつかの実装例を詳しく見てみましょう。
- アップグレード可能な場合、戻り値のサイズがマッピングに保存されます。
ここからフォールバック関数の実装は次のとおりです。
bytes4 sig; assembly { sig := calldataload(0) } var len = _sizes[sig]; var target = _dest; assembly { // return _dest.delegatecall(msg.data) calldatacopy(0x0, 0x0, calldatasize) delegatecall(sub(gas, 10000), target, 0x0, calldatasize, 0, len) return(0, len) }
戻り値のサイズ(バイト単位)は、mapping_sizesに格納されます。 ストレージのこのフィールドは、契約を更新するときに入力する必要があります。
このアプローチの欠点は、戻り値のサイズが呼び出される関数のシグネチャに厳密に関連付けられていることです。つまり、任意のサイズの文字列またはバイト配列が返されないということです。
さらに、ストレージへのアクセスは非常に高価です。 この場合、ストレージへの2つの呼び出しがあります。_destフィールドにアクセスするときと、_sizeフィールドにアクセスするときです。
- EVMアセンブリのトリック :常に32バイトの応答サイズを使用します。
コードは前の例と非常に似ていますが、応答サイズは常に32バイトです。 これはかなりバランスの取れた決定です。 第一に、堅牢性のほとんどのタイプは正確に32バイトに適合し、第二に、再びストレージに頼ることなく、かなりの量のガスを節約します。 後で、異なる実装で消費されるガスの量を推定します。 - 新しい resultdatasizeおよびresultdatacopy命令を使用する
これらの指示は、最後のハードフォーク(Byzantium-2017年10月17日)の後にのみ、Ethereumコアネットワークに現れました。
この指示により、呼び出し/デリゲート呼び出しから返される応答のサイズを取得したり、応答自体をメモリにコピーしたりできます。 つまり、任意のreturndataサイズに対して本格的なプロキシを実装する機会がありました。
最終的なアセンブリコードは次のとおりです。
アセンブリ{ let _target:= sload(0) calldatacopy(0x0、0x0、calldatasize) let retval:= delegatecall(gas、_target、0x0、calldatasize、0x0、0) returnizeizeをしましょう:= returndatasize returndatacopy(0x0、0x0、returnizeize) スイッチretval case 0 {revert(0、0)} default {return(0、Returnsize)} }
ガスの使用を検討してください。 テストでは、上記の3つの方法すべてで、ガスの使用量が1000から1500に増加することが示されています。 これは多かれ少なかれ平均トランザクションコストの約2%であり、ストレージを変更します。
使用の難しさ
残念ながら、これらの手法の使用は制限されています。 まず、契約のこのような更新が機能するためには、契約内のデータストレージの構造を変更できません(フィールドの再配置、フィールドの削除はできません)。 契約の新しいバージョンにフィールドを追加できます。
また、アクティブな契約の住所を変更する機能へのアクセスを非常に慎重に記述する必要があります。
重要な事実は、契約におけるユーザーの信頼は、同じ不変のものよりも低いということです。 一方、契約の新しいバージョンをロールバックでき、その後契約のバージョンが修正されて変更できなくなるテスト期間を提供できます。
実装例の更新
更新をより簡単で信頼性の高いものにするためのいくつかの契約。
アップグレード可能 -このコントラクトは、ターゲットフィールド(コントラクトのアクティブバージョンのアドレス)が現在のバージョンと同じスロットに格納されていることの検証を実装します。
同様に、他のストレージフィールドにチェックを実装できます(例はTarget.solにあります )
アップグレード可能な契約の実装を計画している場合は、アップグレード可能な契約のテストを必ず確認してください。
そのような契約をネットワークに展開する前に、すべてのオプションを確実にテストする必要があります。 それ以外の場合、次の更新後、機能する契約がなくても更新の可能性なしに留まることができます。