Appearance
L3-4: Shorting(ショート市場)
1. 問題
ショートは金融市場で弱気の見解を表明するための基本的な操作ですが、オンチェーンでの実装は伝統的金融よりも複雑です。伝統的なショートの流れは、株式を借りる → 売却する → 価格下落を待つ → 買い戻す → 返却する、であり、利益は価格差から生じます。DeFiでは、このプロセスを完全にオンチェーンで実行する必要があります。ユーザーは担保(USDCなど)を預け入れ、対象トークン(ETHなど)を借り入れ、DEXで売却し、価格が下落したら買い戻して返済します。
中核的な設計課題は、(1) 借り入れたトークンが返済可能であることをいかに保証するか(過剰担保)、(2) 価格が不利に変動した場合にいかに清算をトリガーするか、(3) 不良債権を防ぐために清算メカニズムをいかに設計するか、です。これらの問題はパーペチュアルコントラクトプロトコル(GMX、dYdXなど)にも同様に存在し、ショートメカニズムの中核を成します。
2. 理由
ショートは投機のツールであるだけでなく、市場効率性に不可欠な要素です。ショート売り手は流動性を提供し、価格発見を加速し、バブルを抑制します。DeFiにおいて、ショートメカニズムを理解することで、ヘッジ戦略の構築、パーペチュアルコントラクト市場への参加、さらには合成資産の設計が可能になります。Aaveでのレバレッジショート、GMXのパーペチュアルコントラクト、Ethenaのデルタニュートラル戦略は、すべてショートプリミティブの上に構築されています。
コントラクト設計の観点から見ると、ショート市場は優れたポジション管理の演習です。担保管理、価格オラクル統合、清算閾値計算、損益のリアルタイム決済が含まれます。これらのパターンは、すべての証拠金取引およびパーペチュアルコントラクトプロトコルの基礎です。
3. 解決策
ShortingMarket.solは完全なショート市場を実装しており、中核的な流れは以下のとおりです。
ポジションを建てる(Open Short):openShort()は2つのパラメータを受け取ります。担保数量(_collateral)と借入数量(_borrowAmount)です。その後、担保率が最低要件(MIN_COLLATERAL_RATIO = 150%)を満たしているかを検証します。計算式:
borrowValue = borrowAmount * currentPrice / 1e18
minCollateral = borrowValue * 150 / 100
検証通過後、ユーザーは担保を預け入れ、借り入れたトークンを受け取ります(これらのトークンは外部DEXで売却されます)。
ポジションを閉じる(Close Short):closeShort()は損益を計算します。価格が下落した場合、ショート売り手は利益を得ます:
borrowValueAtOpen = borrowAmount * openPrice / 1e18
borrowValueNow = borrowAmount * currentPrice / 1e18
profit = borrowValueAtOpen - borrowValueNow // 価格下落時に正
ユーザーは借り入れたトークンを返済し、その後担保+利益(または担保-損失)を引き出します。
清算(Liquidate):liquidate()は担保率がLIQUIDATION_COLLATERAL_RATIO = 120%を下回ったときにトリガーされます。清算人はポジションをカバーするために借入トークンを提供し、担保に10%の清算ボーナスを加えて受け取ります。
オラクル:currentPriceはコントラクトownerが手動で設定します(setPrice())。簡略化された設計です。本番環境ではChainlinkなどの分散型オラクルを使用する必要があります。
参考ソースファイル:src/level3/ShortingMarket.sol
4. 遭遇した落とし穴
- 価格操作とオラクルの選択:
currentPriceは手動設定の単一価格であり、TWAPや分散型検証がありません。攻撃者が価格設定の瞬間のウィンドウを悪用する可能性があります - 清算時の担保不足:価格が激しく変動した場合(フラッシュクラッシュなど)、担保が清算ボーナスを支払うのに不十分となり、不良債権が発生する可能性があります
- 借入トークンが取得できない:
openShort()でコントラクトが直接borrowTokenをユーザーに転送しますが、コントラクトアドレスが十分な借入トークンを保有している必要があります。これは設計上、事前の資金注入または外部流動性プールへの依存を意味します - ポジションIDの操作:
positions.lengthをIDとして使用しており、極端なケース(delegatecall経由など)でIDの衝突が発生する可能性があります - 損益計算における精度喪失:
closeShort()で1e18による除算が複数回発生し、小さな丸め誤差が蓄積する可能性があります
5. 落とし穴の原因
価格オラクルはショート市場で最も脆弱な部分です。ShortingMarket.solの現在の設計では、currentPriceはownerがsetPrice()を呼び出して更新します。これにより中央集権的な信頼が導入されます。ownerが悪意を持って誤った価格を設定した場合(例:暴落中の資産価格を過大に設定する)、正当な清算を阻止したり、不健全なポジションを人為的に健全に見せかけたりすることができます。
より深層的な問題は流動性です。市場が激しく変動する際、オンチェーンDEXのスリッページは極めて大きくなる可能性があります。コントラクトが清算価格を正しく計算しても、清算人はDEX上の流動性不足により実際に裁定取引を実現できない可能性があります。本番レベルのショート市場では通常、この問題を軽減するためにオラクルとオフチェーンマーケットメイカーの組み合わせを使用します。
借入トークンの出所も注目に値します。コントラクトはopenShort()で直接transfer(borrowToken)をユーザーに送信します。つまり、コントラクトアドレスがこれらのトークンを事前に保有している必要があります。実際の運用では流動性プール(AaveのaTokenプールのようなもの)が必要です。ショートの「借入」は通常2つの実装方法があります:(1) レンディングプールから借り入れる(Aaveのフラッシュローンパターン);(2) 合成資産モデル(GMXのGLPプール)、ユーザーは実際の市場で売却するのではなく、プロトコルと対賭します。
6. 落とし穴の解決方法
オラクルの安全性:手動のsetPrice()をChainlink価格フィードに置き換えます:
solidity
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
AggregatorV3Interface public priceFeed;
function getCurrentPrice() public view returns (uint256) {
(, int256 price,,,) = priceFeed.latestRoundData();
return uint256(price);
}
清算における不良債権の防止:清算に「カバー残り」ロジックを追加します。清算後の担保が債務を完全にカバーするのに不十分な場合、差額は不良債権として記録されます。実際の設計では、保険基金(Insurance Fund)を使用して不良債権を吸収するか、ポジションが清算ラインに近づいた時点で部分清算を許可する段階的清算(partial liquidation)メカニズムを導入できます。
流動性の確保:借入トークンの出所については、資産プールモデルの使用を推奨します。ユーザーの預入担保がプールを形成し、ショート売り手がプールからトークンを借り入れます。これにより自然な貸借市場が生まれ、借入金利は市場の需給によって決定されます。
精度の一貫性:すべての価格計算で精度単位を統一します。現在の実装では1e18スケーリングを使用しています。乗除算の過程で同じスケーリング係数が使用されることを確認し、(a * b / c) * d / eのような複数回の除算による精度損失を回避します。推奨される方法は、先に乗算してから除算することです:(a * b * d) / (c * e)。
7. 技術ポイント
| 技術ポイント | 説明 |
|---|---|
| ショートの本質 | 借入 → 売却 → 価格下落 → 買戻し → 返済 |
| 最低担保率 | 150%(MIN_COLLATERAL_RATIO) |
| 清算担保率 | 120%(LIQUIDATION_COLLATERAL_RATIO) |
| 清算ボーナス | 10%(LIQUIDATION_BONUS) |
| 損益計算 | borrowValueAtOpen - borrowValueNow |
| オラクル | 手動設定(簡略化)、本番ではChainlink |
| ポジション追跡 | userPositionsマッピング+positions配列 |
| 状態管理 | activeフラグ+PositionNotActiveエラー |