T12 · テストとデプロイ
実際のお金を使う前に、どうすれば問題を見つけられるか?
- 練習する能力
- Builder
- 実践
- メインネットをローカルにフォークし、実在するプロトコルとやり取りして、テスト経路を最後まで通す。
- AI Lab
- AI にコントラクトのテストケースを生成させ、カバーした分岐と見落とした境界条件を数える。
現実の問題
ユーザーからあるステーブルコインを預かり、レンディングプロトコルへ預け入れて、預入証明をユーザーへ返すコントラクトを作ったとします。
テストは入念に書きました。偽のトークンと偽のレンディングプロトコルを用意し、34 ケース、カバレッジ 97%、すべて成功です。
ところがメインネットへデプロイすると、最初の実取引が revert します。
半日調べてようやく原因が分かりました。実際のステーブルコインは、既存の承認額がゼロでない状態で再度承認すると失敗します。 標準に沿って作った偽トークンにはその挙動がありません。実物は標準に完全には従っていなかったのです(T9 で扱った種類の落とし穴です)。
修正して再デプロイすると、2 件目の取引も revert しました。実際のレンディングプロトコルでは、この資産に最低預入額が設定されており、それ未満は拒否されます。偽プロトコルにはその挙動もありませんでした。
3 回目のデプロイ後、最もつらい問題に気付きます。管理権限が、デプロイスクリプトで使った一時アドレスに残っていました。 権限移管の処理は書いたものの、成功したかを何も検証していなかったのです。
3 つの問題には共通点があります。
問題は自分のコードではなく、世界についての自分の想定にありました。そしてテストも、その想定に沿って書かれていました。
97% というカバレッジは、コードが実行されたことを示します。想定が正しいことは何一つ証明していません。
では、想像した世界ではなく、実際の世界に対してテストを実行するにはどうすればよいでしょうか?
思考実験
公開前の準備を、4 段階の飛行訓練に置き換えて考えます。
第 1 段階:紙の上で計算する。 風速、重量、滑走路の長さから離陸距離を計算します。速く、安く、数式の正しさを確認できます。ただし数値の出所は検証できません。計算が正しくても、入力した風速が違えば山に衝突します。
第 2 段階:シミュレーターで飛ぶ。 飛行機、滑走路、管制塔を作り、手順を通して実行します。「脚を格納するとフラップが格納できなくなる」といった、部品同士の問題を発見できます。ただし滑走路も自作なので、実際の滑走路にある穴は再現されていません。
第 3 段階:実際の空港をシミュレーターへ取り込む。 自分で滑走路を作らず、実在する空港の滑走路、地形、航法施設をそのまま取り込み、そこで飛びます。
この段階が本章の中心です。 コードはローカルで実行されますが、やり取りする相手は現実の状態です。実際のトークン、プロトコル、パラメータ、独特な挙動を扱います。最初の 2 段階で見つけられなかった問題が、ここで姿を現します。
第 4 段階:砂袋を載せて、本当に一度飛ぶ。 実際のお金を使わない環境で、デプロイ、検証、権限移管まで、公開手順を一通り実行します。
4 段階の役割は明確です。
- 最初の 2 段階は、自分のロジックを検証します。
- 第 3 段階は、自分の想定を検証します。
- 第 4 段階は、自分の手順を検証します。
それぞれ異なる種類の誤りなので、一つの手段ですべてを検証することはできません。
さらに、どの段階よりも重要で、段階とは無関係な点があります。
コントラクトの入口はすべて公開されています。そのため「正しく実行できるか」以上に、「正しく拒否できるか」が重要です。
正常に実行する経路は一つで、必ずテストします。正しく拒否すべき経路は何十もあり、一つでも漏れると、誰もが直接呼び出せる穴になります。
あなたが決める
コントラクトが実際のプロトコルとやり取りします。公開前にどうテストしますか?
結果を観察する
4 つの選択肢は 4 段階の優劣ではなく、互いに独立した 2 つの軸です。
| ロジック分岐の網羅 | 依存先の現実性 | 速度 | 冒頭のどの問題を発見できるか | |
|---|---|---|---|---|
| すべて代役 | 最高 | 最低 | 最速 | 一つも発見できない |
| 本物の依存先をローカルへデプロイ | 高い | 中程度 | 中程度 | 最初の問題(場合による) |
| 実際の状態を取り込む | 中程度 | 最高 | 遅い | 最初と 2 番目 |
| テストネットで手動操作 | 最低 | 高い | 最も遅い | 3 番目 |
この表から持ち帰ってほしいのは、次の一文です。
カバレッジはコードがどれだけ実行されたかを測り、想定がどれだけ検証されたかは測りません。
代役のみで 100% を達成したテスト群は、実際の世界の挙動を何も知らないかもしれません。一方、実際の状態で動く一経路は、分岐の 20% しか網羅しなくても、損失につながる種類の問題を防げます。したがって一つを選ぶのではなく、段階ごとに役割を分けます。
- ロジックと拒否経路は代役で検証する。速いため、多くのケースを書けます。
- 統合は実際の状態で検証する。それだけが想定を確かめられます。
- デプロイは一度の完全なリハーサルで検証する。デプロイも、普段はテストされないコードだからです。
もう一つ、明記すべき帰結があります。書き忘れた拒否ケースこそ、他人が呼び出す入口です。
モデルを作る
公開までのパイプライン
- 失敗ケースを書く
- ロジックテスト(代役)
- 統合テスト(実際の状態)
- ランダム入力と不変条件
- デプロイスクリプトのリハーサル
- テストネットで全手順
- メインネット
最初の工程に注目してください。失敗ケースを先に、成功ケースを後に書きます。 好みの問題ではなく、順序の問題です。成功ケースから書くと、それを書き終えた時点で完了した気になるからです。
拒否リスト:制限を一つずつテストケースにする
コントラクトを開き、「条件を満たさないと拒否する」箇所をすべて列挙し、それぞれに一つのケースを対応させます。
| コントラクトの制限 | 防ぐこと | 必須の失敗ケース |
|---|---|---|
| 管理者のみ呼び出せる | 誰かによる乗っ取り | 一般アドレスから呼び出し、拒否を確認 |
| 初期化は一度のみ | 先回りや繰り返し初期化 | 2 回初期化し、2 回目の拒否を確認 |
| 金額は正である | ゼロ額操作による記帳の汚染 | 0 を渡し、拒否を確認 |
| 残高が足りる | 残高を超える引き出し | 残高に 1 を足して引き出し、拒否を確認 |
| 承認額が足りる | 他人の資金の消費 | 承認せず送金し、拒否を確認 |
| 停止中は操作禁止 | 緊急停止の不作動 | 停止後に呼び出し、拒否を確認 |
| 期限切れ | 期限切れ注文の実行 | 時刻を期限後へ進め、拒否を確認 |
| 再入防止 | コールバック中の再進入 | コールバックする偽コントラクトで発生させ、拒否を確認 |
最後は特殊なケースです。テスト道具として「悪意あるコントラクト」を専用に作ります。コールバックを受けたとき、禁止される操作を行うことだけが役目です。再入の全体モデルは T28 で学びますが、このケースは今から必要です。
見落としやすい確認点が 2 つあります。
- 確認するのは「拒否」であり、「何かのエラー」ではありません。 具体的なエラー型やメッセージを確認します。そうしないと、別の原因で失敗しても成功扱いになる、見せかけの成功ケースが生まれます。これは非常に多い問題です。
- 拒否後に状態が変わっていないこと。 revert は取引の変更をすべて巻き戻すはずです。主要な状態が変わらないことも確認すると、見つけにくい種類の誤りを捕捉できます。
実際の状態でのテスト:正しく行うべき 3 点
第一に、高さを固定します。 実ネットワークから状態を取得する際は、ブロック高を設定に固定します。固定しないと、十数秒ごとに変わる「現在のチェーン状態」へ依存します。今日成功したケースが来週失敗し、自分のコードには存在しない問題の調査に 2 日かかることになります。
第二に、身元と残高を用意します。 テスト用アカウントは、実際の状態では資金を持っていません。方法は 2 つあります。
- 対象資産を大量に持つ実在アドレスを見つけ、その身元で取引します。大半のテストフレームワークに、任意アドレスとして呼び出す機能があります。
- ストレージを書き換えて残高を設定します。速い方法ですが、残高がどのスロットにあるかを知る必要があります(レイアウトは T8)。
第三に、エラーがないことではなく、実際の結果を確認します。 「revert しなかった」だけでは、ほとんど情報がありません。自分の残高がいくら減ったか、何の証明が返されたか、プロトコルの記帳がどう変わったかを確認します。
完全な統合テスト経路は次のようになります。
1. 実ネットワークの固定ブロック高から状態を取得する
2. 自分のコントラクトをデプロイする
3. テストアカウントへ実際の資産を用意する
4. 操作前の関係する残高と状態をすべて記録する
5. コントラクトを呼び出し、一連の業務経路を通す
6. 自分の残高、相手の残高、証明の数量、プロトコルの記帳を個別に照合する
7. 逆方向の経路(引き出し、償還)も通し、開始点へ戻れることを確認する第 7 工程は省略されやすく、問題にもなりやすいものです。預けられることと引き出せることは別です。実際に試して初めて、引き出せないと分かります。
ランダム入力と不変条件:「すべての入力について」を実行可能な確認にする
通常のケースは「この入力なら、この出力になる」というものです。ランダムテストは逆に、任意の入力でも、ある性質が常に成立するかを問います。
資金を保有するほぼすべてのコントラクトで使える、代表的な性質があります。
コントラクトが実際に持つ資産 が 全ユーザーの記帳額の合計以上
全保有者の残高の合計 が 総供給量と等しい
任意の操作列の後にアカウントが引き出せる額 が 預けた額以下
「預入後すぐ引き出す」純効果 が 何もしない場合より有利ではない
管理者だけが変えられるパラメータ が 1 万回のランダム呼び出し後も変わらない最後の性質は過小評価されています。すべての公開入口へ 1 万回のランダム呼び出しを行い、その後で不変条件を調べます。 非常に安く効果の高いテストで、想像しなかった呼び出し順序の問題が見つかります。
実務上の注意点は 2 つです。
- 失敗を再現できるようにする。 失敗させた乱数シードと入力列を記録し、通常のテストケースとして固定します。
- 不変条件の範囲を明確にする。 「コントラクトの残高が記帳合計以上」という条件は、入口を経由しない直接送金で破れる場合があります。その際は条件を調整するか、実際の欠陥だと認める必要があります。
デプロイも、普段テストされないコードである
デプロイスクリプトは、プロジェクトで唯一「一度だけ実行し、間違えても直せず、テストもない」コードになりがちです。冒頭の 3 番目の問題はここで起きました。
適切なスクリプトには、次の要件があります。
| デプロイスクリプトの必須事項 | 省くとどうなるか |
|---|---|
| 全パラメータを設定から読み、コードに固定しない | チェーンを変えるたびに、気付きにくい誤りが起きる |
| 各工程の生成アドレスをファイルに記録する | 後からどのアドレスがどの版なのか分からなくなる |
| デプロイ直後にチェーンの状態を確認する | 権限移管漏れやパラメータ誤りに数か月後に気付く |
| 権限移管を独立工程にし、確認する | 一時秘密鍵に管理権限が残る |
| 再実行可能で、冪等である | 途中の失敗後に最初からやり直す、または重複デプロイする |
| ソース検証に必要な情報をすべて出力する | チェーンのコードとリポジトリの同一性を照合できない |
最も重要なのは 3 行目です。スクリプトの末尾には、一連のアサーションを書くべきです。「デプロイ完了」と表示するだけでは足りません。
assert コントラクトの owner == 設定したマルチシグアドレス
assert コントラクトの admin != デプロイ者アドレス
assert 主要パラメータ == 設定値
assert コントラクトは停止中(停止してから公開する手順の場合)
assert 外部依存のアドレス == 設定アドレス、かつそのアドレスにコードがある最後は特に見落としやすい点です。渡した外部アドレスには、そのチェーン上でコントラクトが存在しないかもしれません。 空アドレスへの依存は、デプロイ時にエラーにならず、最初のユーザー取引で初めて壊れます。
手順上のルールもあります。デプロイスクリプトは、実際の状態を取り込んだローカル環境で、先にリハーサルします。 同じ設定とスクリプトで、デプロイからアサーションまで完走させます。お金を使う前に、特に高くつく設定、アドレス、権限の誤りを発見できます。
何と呼ぶか
一つの関数やモジュールのロジックだけを検証し、外部依存はすべて代役に置き換えます。
速いので何百ケースも書けますが、代役はすべて自分の想定です。コントラクトでの最大の価値は、成功経路よりも、拒否経路を一つずつ検証することです。
挙動を制御できる、偽の依存先です。
その代償は、「実際の依存先はどう動くか」を「自分はどう動くと思うか」に置き換えることです。 両者が異なる箇所は、テストが永久に照らせない死角になります。
複数の実際の部品を組み合わせて、インターフェースと順序を検証します。
コントラクトでは、重要なのは部品が複数あることよりも、部品が本物かどうかです。代役を組み合わせた統合テストは、結局大きな単体テストにすぎません。
実ネットワークの固定ブロック高の状態を開始点にして、ローカルチェーンを起動します。読もうとした情報を、そのネットワークから取得します。
本章で最も重要な能力です。 一銭も使わず、実際のトークン、プロトコル、流動性とやり取りできます。絶対のルールは、高さを固定することです。そうしないと結果が時間とともに変わります。
ランダム入力で関数を繰り返し呼び出し、誤動作する入力がないか調べます。
通常のケースが「この入力は正しいか」を問うのに対し、「誤る入力が何か存在するか」を問います。反例が見つかったら、まずその入力を恒久的なテストケースにします。
任意の操作列の後にも成立すべき性質です。例えば「コントラクトの資産が、全員の記帳額の合計以上である」です。
不変条件テストはランダムテストを広げたものです。一つの関数だけでなく、すべての公開入口をランダムに大量に呼び出し、その性質がまだ成立するかを確認します。想定外の呼び出し順序の問題が見つかります。
デプロイ、設定、権限移管、結果確認、アドレス記録を、再実行可能なコードにまとめます。
適切かどうかの判断基準は一つです。末尾に一連のアサーションがあるか。 「デプロイ完了」と表示するだけなら、何も検証していません。
実践
この Lab は費用不要です。フォークは実際の状態をローカルへ読むだけで、取引はすべてローカルに留まります。 最後の 2 工程ではテストネットを使い、テスト用コインはフォーセットで取得します。この演習専用の鍵を生成し、実資産を持つウォレットは使わないでください。
コマンドはすべて一般的な形で示します。フレームワークによって引数名が異なるので、各工程の前にコマンドのヘルプを実行し、引数を確認してください。
フォーク環境を起動し、高さを固定します。
<テストフレームワークのフォーク引数> --fork-url <アーカイブ RPC アドレス> --fork-block-number <選んだブロック高>高さを設定ファイルや環境変数へ記録し、空欄にしないでください。空欄だと実行ごとに異なる状態を取得します。
起動したら、既知の実コントラクトアドレスでコード長を読み、ゼロでないことを確かめます。ゼロならフォークが有効になっておらず、以後のテストは空のチェーンで実行されます。
テストアカウントへ実際の資産を用意します。
次のどちらかを選びます。
方法 1:保有者の身元で呼び出す
対象トークンを大量に持つ実在アドレスを探す
フレームワークの「任意アドレスとして取引する」機能を使う
そこからテストアカウントへ一部を送る
方法 2:ストレージを直接書き換える
残高マッピングのスロットを見つける(配置規則は T8)
フレームワークの「任意スロットへ書き込む」機能を使う方法 1 は現実に近く、方法 2 は速い方法です。初回は方法 1 を勧めます。実際の状態へフォークできているかも確認できるからです。
成功ケースの前に、3 つの失敗ケースを書きます。
正常なロジックを書く前に、次を用意します。
ケース 1:金額 0 で呼び出し、拒否と状態不変を確認
ケース 2:承認額なしで呼び出し、拒否を確認
ケース 3:非管理者アドレスから管理関数を呼び出し、拒否を確認すべてで具体的なエラーを確認し、単に「失敗した」とだけ確認しないでください。
次に拒否リストと照合し、コントラクトの制限ごとにケースを追加します。ここまでできれば、多くの公開済みコントラクトよりも細かく検証できています。
実際の相手との完全な操作経路を通します。
「モデルを作る」の 7 工程に沿って、操作前の残高と状態をすべて記録し、呼び出し、個別に確認し、逆経路で開始点へ戻ります。
特に第 7 工程を忘れないでください。預入後は引き出します。 償還経路でだけ問題が出るコントラクトも多くあります。
一度か何度かは失敗するでしょう。しかし、失敗のたびに現実の独特な挙動を見つけています。それがこの Lab の価値です。すべて記録すると、公開前に対応すべきリストになります。
カバレッジを測り、その数字をうのみにしません。
レポートを実行して数値を記録します。その後、さらに重要な作業をします。
コントラクトを開き、「条件を満たさないと拒否する」箇所を数える。
テストの中で、拒否を確認するケースを数える。
二つの数は近いはず。拒否ケースが拒否条件よりはるかに少なければ、高いカバレッジは見せかけです。その分岐は通過されただけで、検証されていません。
不変条件を一つ追加し、1 万回ランダムに呼び出します。
最も重要な性質を選びます。通常は次のものです。
コントラクトが実際に持つ資産数量 が 全ユーザーの記帳額合計以上フレームワークのランダム呼び出しで、すべての公開入口をランダムな順序と引数で呼び、各回の後に性質を確認します。
反例が出たら、入力を通常のケースとして固定します。 成果は「実行した」ことではなく、見つかった入力群です。
デプロイスクリプトを書き、フォーク環境で先にリハーサルします。
前の表に沿って、アサーションで終わるスクリプトを書き、フォーク環境で一度実行します。
権限確認の 2 条件は必須です。owner が設定アドレスであり、デプロイ者アドレスではないことです。冒頭の 3 番目の問題をここで防ぎます。
テストネットで全手順を一度実行します。
同じスクリプトと設定を使い、パラメータをテストネット用に変えて実際にデプロイします。アサーションをすべて実行し、ソースも検証します。
テスト用コインはフォーセットで取得し、実際のお金は使いません。
終わったら、フォーク環境では分からなかった工程を振り返ります。例えば依存アドレスがテストネットに存在しないなど、通常一つか二つは見つかります。それがこの段階を設ける理由です。
AI Lab
3 段階で問いかけます。実際の成果になるのは第 2、第 3 段階です。
第 1 段階:
これは私のコントラクトのソースです。テスト群を生成してください。
すべての拒否条件を先に列挙し、それぞれ拒否を確認するケースを作り、
その後で成功経路を書いてください。各ケースの検証対象も説明してください。
第 2 段階:
生成したケースをソースと一行ずつ照合し、表にしてください。
各 require / revert / 権限確認に対応するケースはどれですか?
対応ケースがない箇所は別に列挙してください。
第 3 段階:
このコントラクトが実際の外部プロトコルとやり取りし、テストでは代役を使うとします。
代役と実際の依存先のどの種類の挙動の違いが、テスト成功と本番失敗を招きますか?
種類ごとに具体例を一つ示し、フォーク環境での検証方法を説明してください。第 1 段階では、モデルは速く多くのケースを出します。しかし、ケースの分布が大きく偏っています。成功経路は詳しく、拒否経路は少数です。学習データのテストコードがそうなっているためです。数量ではなく「拒否ケースの割合」で評価してください。
第 2 段階が、偏りを修正する鍵です。モデル自身にソースと照合して漏れを列挙させる方が、「何を見落としたか」と直接聞くより効果的です。検証可能な列挙作業になるからです。表が完成すれば、具体的な追加テストリストが得られます。
第 3 段階が最も価値の高い部分です。これはモデルが得意な、正確な記憶を要しない帰納推論です。T9 と T11 の AI Lab でも学んだ境界です。正確な記憶が必要なら照合し、帰納推論ならモデルに任せます。
最後は自分で行います。挙げられた違いを一つずつ、フォーク環境の実物で試します。 「特定のステーブルコインの承認は特殊」とモデルが述べても、正しい場合も、2 つのトークンの特徴を混同している場合もあります。フォーク環境は、無料で実際の答えを確かめる場になります。
AI の回答後、自分で必ず検証すること
- テストフレームワークの全 API とアサーションを、ヘルプや文書で実在するか確認した。モデルはもっともらしい架空のテスト補助関数を作りがちです
- 拒否を確認するケースを数えた。成功経路しかなければ、書き直しを求める
- コントラクトの拒否条件を一つずつ照合し、対応ケースがないものは自分で追加した
- 具体的なエラー型を確認しているか確認した。単なる取引失敗の確認は、見せかけの成功の主因です
- 拒否後に状態が変わっていないことも確認している
- 0、最大値、小数桁数 0 と 18 のトークン、同一ブロック内の反復呼び出し、必要額と一致する残高という境界を網羅した
- 代役と実際の依存先の挙動の一致を、フォーク環境で自分が実測した
- フォークテストのブロック高が固定されている
- モデルが報告したカバレッジを信用せず、自分でツールを実行した
実例
アップグレード可能なコントラクトの実装に、繰り返し呼び出しを防ぐ保護のない初期化関数が公開されていました。テストでは当然デプロイ手順から呼ぶので、正常です。本番では他人がそのアドレスへ直接同じ関数を呼び、自分を管理者にしました。
共通点は、テストが「想定した呼び出し元」の経路しか通っていないことです。「初期化は一度のみで、誰が呼べるか」という拒否リストのケースが一つあれば防げます。
冒頭の場面です。代役は標準に沿い、実物は承認や戻り値で標準から外れています。
さらに一般的なのは、戻り値の問題です。標準では送金関数が真偽値を返しますが、一部の古いトークンは何も返しません。標準通りの呼び出し側は戻り値の解析に失敗し、代役では問題が現れません。
T9 と同じ問題ですが、本章では解決策を得ました。フォーク環境の実物で一度実行すれば、違いが現れます。
スクリプトには「owner をマルチシグへ移す」処理がありましたが、パラメータ誤りで静かに失敗しました。後続処理は続き、最後に「デプロイ完了」と表示しました。数週間後の権限確認で、管理権限がずっとホットウォレットの秘密鍵に残っていたと判明しました。その間、鍵を得た人なら誰でもプロトコルのパラメータを変更できました。
教訓:末尾にはログではなくアサーションが必要です。権限は 2 条件で確認します。owner が誰かと、owner がデプロイ者ではないことの両方です。
シェアと資産を換算するコントラクトは、手書きケースをすべて通過しました。数十万回のランダムテスト後、ごく小さな金額の入力が見つかりました。ある除算の丸めがユーザーに有利なため、預入と引き出しを繰り返すとプロトコルの資金が少しずつ減ります。1 回の損失は無視できても、無限に反復できます。
教訓:丸め方向は、人には網羅しにくく、機械には見つけやすい問題です。 不変条件テストの典型的な成果でもあります。「何もしない場合より有利にならない」という条件を、ランダム呼び出しが破る経路を見つけました。
変数を一つ変える
コードを一行も変えなくても、今日成功したケースが来週失敗します。依存プールの流動性が変わった、ガバナンスがパラメータを変更した、残高を借りた大口保有者が資金を移した、といった理由があり得ます。
調査が苦痛なのは、証拠がすべて自分のコードを指すのに、原因はそこにないからです。高さの固定は設定一行で、2 日の調査を省けます。
既に存在しないロジックでテストしています。すべて成功しても、メインネットの挙動はまったく違います。
フォーク高を定期的な保守対象とし、定期的に先へ進めて再実行し、失敗がないか確認します。失敗は依存先が変わった合図です。何が変わったか調べます。
さらに、外部依存のアップグレード可能性自体が、リスクモデルの一部です。T28 と T29 で詳しく扱います。
手順のリハーサルで最もよく遭遇する壁です。メインネットにしかないプロトコルも多く、テストネットにはないか、不完全な版しかありません。
方法は 3 つです。手順を試せる簡易版を自分でテストネットへデプロイする、機能テストは全面的にフォークへ任せる、またはテストネットでは業務ロジックを試さずデプロイと権限の手順だけを練習します。
3 番目が最も現実的です。各段階に得意な役割だけを任せます。テストネットの価値は手順の検証です。
十分にあり得ます。全分岐は実行されても、「許されない操作を拒否したか」を確認するケースがない状態です。
カバレッジレポートが、積極的に判断を誤らせる信号になります。十分に検証したと信じ、考えるのをやめてしまうからです。
より良い指標は、拒否条件数と拒否ケース数の比率です。完璧ではありませんが、少なくとも重要なことを測っています。
持ち帰る問い
何を解決するのでしょうか?本章は「まだ代償が小さい時点で誤りを発見する」問題を扱います。コントラクトでは特に切実です。デプロイ後は修正費用が増すだけでなく、修正という選択肢そのものがなくなることもあるからです。
誰がリスクを負いますか?拒否経路をテストしていないコントラクトでは、ユーザーがすべてのリスクを負い、事前に知る手段もありません。「テスト済み」とだけ対外的に伝えられない理由です。伝えるべきなのは、検証した拒否条件、使った実際の状態、デプロイスクリプトで確認した項目です。
AI が間違えたとき、誰が損失を負いますか?AI Lab はテストを生成させますが、重要な拒否ケースが漏れたテスト群は、テストがない場合より危険です。疑うのをやめさせるからです。各ケースを自分で実行し、エラー型を照合してください。責任はデプロイボタンを押す人にあります。
どの判断に Human-in-the-loop が必要ですか?デプロイと権限移管は明確な対象です。スクリプトの実行は自動化できても、「この設定は正しいか」は人が確認すべきです。 アサーションは考え終えた条件を機械に確認させるもので、考えること自体を任せるものではありません。
章末チェック
カバレッジは行の実行を測り、行の意図が検証されたかは測らないためです。
典型的な漏れ方は 2 つです。分岐を通過しただけで、正しく拒否したかを確認していない場合。そして、外部依存がすべて自作の代役で、カバレッジが高くても、自分の想定にコードが合うことしか示さない場合です。
代わりに役立つのは、コントラクトの拒否条件数と、拒否を確認するテストケース数です。両者は近いはずです。
「外部依存がどう動くか」を、想定から事実へ変えます。
代役は自分で書くので、自分の理解に必ず従います。実物のトークンやプロトコルには、承認制限、最低金額、戻り値、停止スイッチ、手数料など、標準にない挙動があります。実際の状態で実行して初めて現れます。
絶対のルールは、フォークのブロック高を固定することです。 固定しないと、時間とともに不規則に失敗するテストになります。
入口はすべて公開され、誰でも任意の引数で呼べるからです。正常実行の経路は一つでも、正しく拒否する経路は何十もあります。
順序も重要です。成功ケースから書くと、完了した気になり、明らかな拒否経路だけを追加しがちです。逆に拒否リストを先に完成させれば、成功経路も自然に書きます。
具体的には、条件を満たさないと拒否する箇所をすべて列挙し、それぞれにケースを作ります。具体的なエラーと、状態が変わらないことを確認します。
一度しか実行せず、誤っても直せず、通常テストもないコードだからです。しかも失敗は静かです。一工程が失敗しても、完走して成功を表示することがあります。
少なくとも、owner が設定したマルチシグでデプロイ者ではないこと、主要パラメータが設定値に等しいこと、外部依存の全アドレスにコードがあることを確認します。
また、同じ設定でフォーク環境において先にリハーサルします。最も高くつく設定、アドレス、権限の誤りは、いずれも無料の環境で発見できます。
唯一の正解はありませんが、次を確認します。
- 拒否条件すべてに、具体的なエラーを確認するケースがあるか?
- 拒否後の状態不変を確認するケースがあるか?
- コールバックする悪意あるコントラクトを、テスト道具として用意したか?
- 統合テストは実際の状態で動くか?ブロック高は固定したか?
- 逆経路も試し、預けた資産を引き出せることを確認したか?
- 少なくとも一つの不変条件を 1 万回のランダム呼び出しで検証したか?反例をケースとして固定したか?
- スクリプトの全パラメータは設定から取得するか?末尾にアサーションがあるか?
- 権限は owner が誰かと、デプロイ者ではないことの 2 条件を確認したか?
- フォーク環境でリハーサルし、テストネットでも全手順を実行したか?
- ソース検証に必要な情報と、生成したアドレスをどこへ記録したか?
第 6、第 8 項は特に省略されやすく、本章で最も持ち帰ってほしい項目です。
要点
メインネットの状態をフォークしたテストは、純粋な単体テストよりずっと実態に近い。