T20 · DEX アーキテクチャ
一回の Swap は、実際に何層を通るか?
- 練習する能力
- BuilderFinancial Literacy
- 実践
- 2 ホップのルーターを実装し、直接交換した場合と中間資産を経由した場合の最終約定価格を比べる。
- AI Lab
- AI にルーティングアルゴリズムを設計させ、実際のプールデータで最適経路か検証する。
現実の問題
T19 のプールをフロントエンドへ接続しました。
両準備金から見積もり、「1 ETH は約 3000 USDC」と表示し、確認後に swap へ計算済み出力を渡します。
ローカルでは正常でも、翌日の苦情は二種類でした。
- 取引の失敗。署名後に revert し、Gas は消費します。
- 成功したが表示額と入金額が違う。3000 と表示して、2890 しか届きません。
攻撃もコントラクトのバグも見積もり誤算もないのに、110 はどこへ行きましたか?
見積もりを見てから、実際にチェーンで実行するまで、どれだけ時間があるかに答えがあります。
十数秒から数分です。その間、他ユーザーの通常取引や、見た人の先回りで準備金が変わります。
見積もり、経路選択、実行を一体化し、最も弱い見積もりをオフチェーンへ置いていました。本章で三つを分けます。
思考実験
空港の両替所を考えます。
三工程を別の役が担います。
- 入口の相場表示:定期更新し、参考値と明示します。
- 窓口担当:直接か、ドル経由か、二つへ分割するか選びます。
- 金庫:実際に数えて渡します。
空港では一分以内なので、分かれているとは感じません。
チェーンでは、異なる三時点です。
t0 ブラウザーが準備金を読み、見積もって表示 (オフチェーン、読み取り)
t1 署名してメモリプールへ入る (オフチェーン、公開)
t2 ブロック生成者が収録して実行 (チェーン上、状態は変わり得る)t0〜t2 は誰でも変えられ、特に t1〜t2 は、取引が公開されています。金額と対象を見て先に約定できます。
窓口で相場表示に従うと言われても、歩く間に表示が変われば、どの価格になりますか?
お金を渡す時に、「これ未満なら交換しない」と言うしかありません。
それが本章の最重要パラメータです。
あなたが決める
表示と違う金額を受け取らせないため、どう変えますか?
結果を観察する
三パラメータは別の問題を防ぎます。
| パラメータ | 防ぐこと | 決める人 | 欠落の結果 |
|---|---|---|---|
| 最低受取量 | 先回り、サンドイッチ、変動の金額ずれ | 発信者 | 任意価格で約定 |
| 有効期限 | 長い滞留の時間ずれ | 発信者 | 古い意図を数時間後に実行 |
| 受取アドレス | 資産の行き先 | 発信者 | 中間コントラクトへ残る |
共通するのは、三つとも発信者が署名することです。
ユーザーが制約を付ける唯一の場所です。収録時には、フロントエンド、バックエンド、RPC、Router はユーザーの制御外にあります。
直感に反する点もあります。
許容スリッページは、自分で公開する懸賞額です。
5% を許せば、その範囲の先回り利益を取られても正常に成功し、限界で約定するだけです。
| 許容幅 | 失敗率 | 奪われ得る上限 |
|---|---|---|
| 0.1% | 高い、少しの変動で失敗 | 0.1% |
| 0.5% | 中 | 0.5% |
| 5% | 低い | 5% |
| 50% | ほぼ失敗しない | 50% |
失敗を減らすため大きな既定値を付ける製品もあります。体験の設定でなく、リスク予算です。
モデルを作る
責任、場所、費用の違う三層です。
- Quote:見積もり
- Route:経路選択
- Execute:実行
| 層 | 場所 | 仕事 | 信頼できるか |
|---|---|---|---|
| Quote | オフチェーン | 複数準備金で経路別結果を予測 | 不可、数秒前の値 |
| Route | オフチェーン | 候補選択、分割 | 不可、提案にすぎない |
| Execute | チェーン | 経路を進み、最終下限を確認 | 可、算術と検証だけ |
経路は外で計算し、チェーンは結果だけ検証します。
全プールを列挙して最適を探すのは、Gas が高く、さらに読む準備金自体を操作できます。同取引内で状態を変え、攻撃者に有利な経路を選ばせられます。
経路を署名へ固定すれば、残るのはその経路で実際にいくら受け取ったかという検証です。
Router の骨格です。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin, // 発信者の署名に含む下限
address[] calldata path, // オフチェーンで計算した経路
address to, // 発信者が指定する受取人
uint256 deadline // 発信者が指定する有効期限
) external returns (uint256 amountOut) {
require(block.timestamp <= deadline, "EXPIRED");
require(path.length >= 2, "BAD_PATH");
_transferFrom(path[0], msg.sender, _pairFor(path[0], path[1]), amountIn);
uint256 amount = amountIn;
for (uint256 i = 0; i + 1 < path.length; i++) {
amount = _swapOneHop(path[i], path[i + 1], amount, i + 2 == path.length ? to : _pairFor(path[i + 1], path[i + 2]));
}
amountOut = amount;
require(amountOut >= amountOutMin, "INSUFFICIENT_OUTPUT");
}四点に注意します。
deadlineは最初に確認します。 期限切れなら何もしません。- 途中の受取先は次プールです。 Router に留めず、転送回数も減らします。
amountOutMinは最後に確認します。 最終額が達すれば、途中一段の大きなずれがあっても成功すべきです。- Router 自体は資金を持ちません。 取引間の残高はゼロが適切です。
二段が一段より良い場合の数学です。
直接:A -> C
深度 D1 の一プールが全量を担う
二段:A -> B -> C
深度 D2、D3 で、それぞれのずれが小さい
代償:手数料が約 0.3% から 0.6%、Gas も増える
有利な条件:
A/C が浅く、A/B と B/C が深い
直接の余分なスリッページ > 追加 0.3% と Gas経路アルゴリズムの問いはこれです。 最短でなく、ずれと手数料を比較します。
何と呼ぶか
二資産を保有して曲線で値付けし、外の世界を知りません。
T19 のコントラクトです。Router を知る必要はありません。
経路を受け、各 Pool を呼び、最後にスリッページを確認します。
資金を持たず、価格判断をせず、全制約を呼び出し元から受けます。 違反すれば道具からリスクになります。
オフチェーンの予測結果です。
常に過去の値です。 読んだ時の状態で、実行時ではありません。約束と見なすのが事故の原因です。
署名パラメータで結果の境界を設けます。
最低受取量と期限の価値は、ユーザーの署名にあることから生まれます。
複数 DEX で比較、分割するシステムです。
経路選択を広げますが、任意の対象を呼ぶ実行コントラクトの権限は、設計を誤れば承認額を使う穴になります。
受け入れる最大のずれで、最低量の計算を決めます。
体験の切り替えでなく、リスク予算です。その範囲を取られることを許します。
実践
三プールをデプロイします。 T19 のコードで A、B、C を作り、深度を変えます。
A/C 片側 10 万 (浅い)
A/B 片側 500 万 (深い)
B/C 片側 500 万 (深い)二関数を書きます。各段結果を読む getAmountsOut(amountIn, path) と、実行する swapExactTokensForTokens です。
getAmountsOut は view とし、同じ見積もり式を共有します。近似と厳密を混在させません。
二経路を比較します。 1000、10000、100000 を、A/C と A/B/C へ通します。
| 金額 | 直接の受取 | 二段の受取 | 追加手数料 | 有利な経路 |
|---|---|---|---|---|
| 1000 | ||||
| 10000 | ||||
| 100000 |
境目を探します。どれほど小さいと追加 0.3% が割に合わないかが、実際の計算対象です。
保護を三テストで検証します。
amountOutMinを予測より 0.5% 低くし、成功を確認。- 別アドレスの大口取引で準備金を変え、
INSUFFICIENT_OUTPUTを確認。 - 過去の
deadlineでEXPIREDを確認。
第二が中心です。戻らなければ、確認位置が誤っています。途中の結果を下限と比べていないか調べます。
攻撃費用を逆側から計算します。 定量演習です。
攻撃者の必要額だけでなく、設定が残す利益幅を計算します。
取引額 V、許容幅 s、片側深度 D
1. 許容境界で約定するまで価格を動かす
2. 元本は T19 から、D * (sqrt(1+s) - 1) の程度
3. 粗利は約 V * s
4. 費用は二取引手数料(元本の約 0.6%)、二回 Gas、先回りの入札
5. 純利益 = 粗利 - 費用。正なら実行するV = 10000、s = 0.5% と 5% で全項を求めます。
どれほどの取引から攻撃する価値が出ますか? それ未満は対象にならず、リスク開始点を知る重要な数値です。
T24 で攻撃側コードも含めて計算します。
ローカルのみで実資金は接続しません。 卒業課題でも、第五段階の安全性確認後に使います。
AI Lab
第二が重要な二段階です。
第 1 段階:
準備金と手数料の分かる定積プール群から、
入力、出力資産と入力額に対し、最大受取の経路を求めてください。
重み関数と、最短経路が使えない理由を示してください。
第 2 段階:
最適でない経路を出す三場面の、構造、誤り、代償を示してください。
分割して複数経路を使うなら、アルゴリズムをどう変えますか?
追加 Gas が、どの金額以下で割に合わなくなりますか?第二は境界を認めさせます。難しさは探索でなく、流す額がスリッページを変え、辺の重みを変えることです。100 と 100 万で同経路の最適性が変わり、多くの回答が漏らします。
実数値で検算します。表を作ったので、手元に正解があります。それが検証できる理由です。
AI の回答後、自分で必ず検証すること
- 深度順だけでなく、重みに手数料を含めている
- 最短経路を最良約定と誤解していない
- Lab の三プールで、手計算の最適経路と一致する
- 経路長の上限と、無限に増やせない理由がある
- 経路も署名へ含め、バックエンドが実行時に決めない
- インターフェース、関数、引数順が自分のコントラクトに存在する
実例
deadline に type(uint256).max を渡すか、現在の時刻へ巨大な値を足します。
第一は永久有効で、何か月後の有利な条件まで待って収録できます。
さらに、deadline をチェーン上で block.timestamp から計算すれば、実行時の block.timestamp は自分自身を超えず、いつも成立します。
署名前に値が固定されたかで判断します。そうでなければ保護しません。
失敗を減らすため広げると、ユーザーは成功を見て、境界の損失に気付きません。
期待値を知らないので苦情も少なく、合法でログも正常です。
現在の価格インパクトへ結び付け、0.3% なら 0.5% などにし、一律にしません。数値を見せ、予測額より最低受取を中心に表示します。
to を Router や中間コントラクトにし、後で取得するつもりでした。
取り出す処理がなければ、最初に見つけた人が取ることになります。残高を探す専用ボットもあります。
受取先は、確実に取り出せるアドレスにします。 取引終了後の中間残高はゼロにします。
多様なプロトコルを使うため、任意アドレスへの任意呼び出しを持ちます。
ユーザーの承認があり、攻撃者が transferFrom を呼ばせれば、全承認者の残高を取れます。
許可リスト、引数検証、必要額だけの承認で防ぎます。承認は取引と独立した持続リスクです。 不要なものを定期的に取り消します。
変数を一つ変える
新しいデータで近い下限を出し、失敗率が減ります。
しかし署名外なら、誠実さ、侵害、バグに依存します。署名は、その後誰も変えられない境界です。外へ出せば境界を失います。
関与するなら、バックエンドは提案し、ユーザーが署名で確認します。
理論価格は改善しても、三点が悪化します。
Gas が線形増加し、失敗率が増し、攻撃面が増します。各段の呼び出し、変化、異常が全体へ影響します。
3〜4 段が多いのは技術上限でなく、逓減する効果です。第四段の改善が Gas より小さくなります。
公開の隙間が消え、先回りやサンドイッチの機会が減ります。
代わりに特定中継者が内容を見て、収録を決め、他へ機会を渡せるという信頼を置きます。
全員が見えるリスクを、一人が見えるリスクへ変えます。 T24 で代償を計算します。
Gas を失い、価格が動き、再試行します。
失敗率も利用され得ます。即再試行すると知られれば、条件を作って広い許容へ追い込めます。
金額と深度で動的に変え、大きければ分割を勧めます。価格を大きく動かす取引は、許容幅だけで解決できません。
持ち帰る問い
何を解決しますか?外での意図をチェーンで安全に実現します。どの制約を署名へ含めたかで、取引画面を判断します。
ユーザーは誰ですか?一般取引者は下限で約定し、裁定者やボットは広すぎる下限を探します。同 Router の意味が、二者で逆です。
誰が支払いますか?取引者が手数料、Gas、スリッページを払います。第三は通常大きく、請求書にないので漏れます。
誰がリスクを負いますか?署名者です。誤見積もり、悪い経路、広い許容の損失を負い、チェーンは正常に見えます。保護を署名に入れる理由です。
章末チェック
署名が最後の制約地点で、その後の各者はユーザーの制御外だからです。
実行時に計算すると操作済み準備金を使います。攻撃前に固定して初めて保護します。
期限と受取人も同じです。
別の軸なので、両方必要です。
金額下限は古い意図の実行を止めず、3 時間前でも現在の価格が合えば実行します。
期限は時間内の任意価格を止めません。
両方を署名へ含めます。
二つあり、第二が重要です。
費用は、各準備金の外部照会と組み合わせ探索が高いことです。
操作可能性は、前段で変えた準備金へ誘導されることです。
署名で経路を固定し、結果だけ確認します。外でできる判断は外へ置き、チェーンで必要な検証は省きません。
手数料は線形、スリッページは非線形だからです。
二回の 0.3% に対し、浅いプールの大口交換のずれは数% を簡単に超えます。
直接の追加ずれと、0.3% と Gas の比較です。大口と浅い直接プールほど有利で、小口では Gas が効果を消します。
唯一の正解はありません。次を確認します。
- 固定値でなく、現在の価格インパクトから計算するか?
- 最低受取と予測受取のどちらを中心に表示するか?期待が変わります。
- 大口で追加説明や分割提案が出るか?
- 再試行で自動拡大するか?それは反復利用され得ます。
- 典型取引で最大いくら取られ得るか説明できるか?
第五へ答えられなければ、最初の四つも十分に考えていません。
要点
価格提示、経路選択、実行を分けなければ、スリッページも先回り取引も防げない。