メインコンテンツへスキップ

コントラクトのサイズ制限に対処するためのコントラクトのサイズ削減

Solidityスマートコントラクトストレージtruffle
中級
Markus Waas
soliditydeveloper.com(opens in a new tab)
2020年6月26日
12 分の読書 minute read

制限がある理由

2016年11月22日(opens in a new tab)、Spurious DragonのハードフォークでEIP-170(opens in a new tab)が導入され、24.576 KBのスマートコントラクトのサイズ制限が追加されました。 Solidityデベロッパーにとって、これはコントラクトに機能をどんどん追加していくうちに、ある時点でサイズ制限に達し、デプロイした際に以下のエラーが表示されてしまうということを意味します。

Warning: Contract code size exceeds 24576 bytes ( Spurious Dragonで追加された制限). This contract may not be deployable on Mainnet. オプティマイザを有効にすることを検討してください (効率的に実行されます!)、文字列の取り消しをオフにするか、ライブラリを使用します。

この制限は、サービス拒否(DOS)攻撃を防ぐために導入されました。 コントラクトの呼び出しは、ガスの観点では比較的安価です。 しかし、イーサリアムノードのコントラクト呼び出しの影響は、(ディスクからのコードの読み込み、コードの前処理、マークルプルーフへのデータの追加の対象となる)呼び出されたコントラクトコードのサイズによっては、過度に増加することになります。 攻撃者がリソースをほとんど必要とせずに、他のノードでの大量の処理を生じさせるそうした状況では、DOS攻撃を受ける可能性が常に存在します。

コントラクトコードの固有のサイズ制限が、ブロックのガスリミットとなるため、本来これは問題ではありませんでした。 コントラクトは、コントラクトのバイトコードをすべて含むトランザクション内でデプロイされる必要があることは言うまでもありません。 ブロックに1つのトランザクションのみを含めると、そのガスのすべてを使うことができますが、無限ではありません。 ロンドンアップグレード以降、ブロックのガスリミットは、ネットワークの需要に応じて15M~30M間で変えられるようになりました。

サイズ制限への対処

残念ながら、コントラクトのバイトコードサイズを取得する簡単な方法はありません。 Truffleをご使用の場合は、サイズの取得に役立つtruffle-contract-size(opens in a new tab)プラグインという素晴らしいツールがあります。

  1. npm install truffle-contract-size
  2. truffle-config.jsにプラグインを追加します。plugins: ["truffle-contract-size"]
  3. truffle run contract-sizeを実行します。

これにより、変更内容がコントラクトの合計サイズにどのような影響を与えているかを把握することができます。

次に、いくつかの方法を、効果が大きいものから順に見ていきます。 減量の観点から考えてみましょう。 目標体重(この場合は24 KB)を達成するための最良の戦略は、まず効果が大きい方法に集中して取り組むことです。 ほとんどの場合、食生活を改善するだけで解決しますが、もう少し何かが必要な場合もあります。 その場合は、運動(中程度の効果)やサプリメント(小さな効果)を加えるとよいでしょう。

サイズ削減効果: 大

コントラクトの分割

これは常に最初のアプローチであるべきです。 コントラクトを複数の小さなまとまりに分割するにはどうすればよいでしょうか? 一般的には、コントラクトのための良いアーキテクチャを考えなければなりません。 コードの読みやすさの観点からは、常に、小さなコントラクトコードが好まれます。 コントラクトの分割については、以下の質問を自問してください。

  • どの関数がセットになっていますか? 関数の各セットは、そのコントラクト内にあることが最善策となる場合があります。
  • コントラクトの状態や、状態の特定のサブセットのみの読み取りを必要としないのは、どの関数ですか?
  • ストレージと機能を分けることはできますか?

ライブラリ

機能コードをストレージから移動させる簡単な方法としては、ライブラリ(opens in a new tab)の使用が挙げられます。 コンパイル中に直接コントラクトに追加(opens in a new tab)されるので、ライブラリ関数をinternalで宣言しないでください。 しかし、public関数を使用する場合、それらは実際には別のライブラリコントラクトに含まれることになります。 ライブラリをより便利に利用するには、using for(opens in a new tab)の使用を検討してください。

プロキシ

より高度な戦略としては、プロキシシステムが挙げられます。 このシステムではライブラリが、呼び出し元のコントラクトの状態で単に別のコントラクトの関数を実行するDELEGATECALLを裏で使用します。 プロキシシステムの詳細については、こちらのブログ(opens in a new tab)をご覧ください。 これで機能性が向上します。例えば、アップグレード可能になりますが、複雑さも増します。 何らかの理由によりプロキシシステムが唯一の選択肢でない限り、コントラクトサイズを減らすためだけにプロキシシステムを追加することはお勧めしません。

サイズ削減効果: 中

関数の削除

これは当然実行すべきことです。 関数はコントラクトサイズをかなり増大させます。

  • external: 利便性の理由から、多くのview関数が頻繁に追加されます。 サイズ制限に達するまでは、追加してもかまいません。 その後で、絶対に必要なもの以外のすべての関数を削除することを真剣に検討します。
  • internal: internal関数やprivate関数を削除し、関数が一度だけ呼び出される場合に限り、コードを単にインライン化することもできます。

変数の追加を回避

以下のような簡単な変更をするだけで、

1function get(uint id) returns (address,address) {
2 MyStruct memory myStruct = myStructs[id];
3 return (myStruct.addr1, myStruct.addr2);
4}
コピー
1function get(uint id) returns (address,address) {
2 return (myStructs[id].addr1, myStructs[id].addr2);
3}
コピー

0.28 KBもの差が出ます。 コントラクトでは類似の状況が多く見られます。結果的に、それらがサイズをかなり増大させています。

エラーメッセージの短縮

長いリバート(元に戻す)メッセージ、特に多くの異なるリバートメッセージは、コントラクトを肥大化させる可能性があります。 代わりに、短いエラーコードを使用し、コントラクト内でそれらをデコードします。 そうすると、以下のように、長いメッセージをかなり短くすることができます。

1require(msg.sender == owner, "Only the owner of this contract can call this function");
2
コピー
1require(msg.sender == owner, "OW1");
コピー

エラーメッセージのかわりにカスタムエラーを使用

Solidity 0.8.4(opens in a new tab)で、カスタムエラーが導入されました。 カスタムエラーは、コントラクトのサイズを削減するのに効果的な方法です。なぜなら、(関数と同様に)セレクターとしてABIエンコードされるためです。

1error Unauthorized();
2
3if (msg.sender != owner) {
4 revert Unauthorized();
5}
コピー

オプティマイザでの低い実行値を検討

オプティマイザの設定も変更できます。 デフォルト値の200は、関数が200回呼び出される場合と同様にバイトコードを最適化しようとしていることを意味します。 これを1に変更すると、通常、各関数を1回だけ実行するケースに最適化するよう、オプティマイザに指示します。 1回だけ実行するように最適化された関数とは、その関数自体のデプロイのために最適化されていることを意味します。 ただし、1に設定すると関数の実行にかかるガス代が高くなることに注意してください。

サイズ削減効果: 小

関数への構造体渡しを回避

ABIEncoderV2(opens in a new tab)を使用している場合は、関数に構造体を渡さないようにすることができます。 以下のように、パラメータを構造体として渡す代わりに、

1function get(uint id) returns (address,address) {
2 return _get(myStruct);
3}
4
5function _get(MyStruct memory myStruct) private view returns(address,address) {
6 return (myStruct.addr1, myStruct.addr2);
7}
コピー
1function get(uint id) returns(address,address) {
2 return _get(myStructs[id].addr1, myStructs[id].addr2);
3}
4
5function _get(address addr1, address addr2) private view returns(address,address) {
6 return (addr1, addr2);
7}
コピー

必要なパラメータを直接渡すようにします。 この例では、さらに0.1 KBを節約しました。

関数と変数の正しい可視性の宣言

  • 外部からのみ呼び出される関数や変数ですか? その場合は、publicではなくexternalとして宣言します。
  • コントラクト内からのみ呼び出される関数または変数ですか? その場合は、publicではなくprivateあるいはinternalとして宣言します。

modifierの削除

modifier修飾子を過剰に使用すると、コントラクトのサイズに大きな影響を与える可能性があります。 そのため、modifierの代わりに関数を使用することを検討してください。

1modifier checkStuff() {}
2
3function doSomething() checkStuff {}
コピー
1function checkStuff() private {}
2
3function doSomething() { checkStuff(); }
コピー

これらのヒントを実践することで、コントラクトのサイズを大幅に削減することができます。 繰り返しになりますが、最大の効果を得るためには、可能な限りコントラクトを分割することが重要です。

最終編集者: @sekisanchi(opens in a new tab), 2024年1月18日

このチュートリアルは役に立ちましたか?