シミュレートから理解するuniswapV4の流動性提供
はじめに
Uniswap V4(以下「Uniswap」と表記)では、V3同様に「集中流動性」という仕組みが採用され、ユーザは流動性を提供する価格範囲(レンジ)を細かく設定できるようになっています。
本記事では、あるトークン(今回は MYTKN と呼称)と USDT とのプールにおいて、下限価格を設定した際に、どのような売買挙動(価格変動・プール残高の推移)が起こるかをシミュレーションした結果を紹介します。下限を設定しない「フルレンジ」と比較して、メリット・デメリットがどのように変化するのかを可視化したい方の参考になれば幸いです。
また、今回作成したシミュレーションツール(foundry)も公開、使用方法の説明しています。
オリジナルのパラメータでシミュレートをしていただけます。
シミュレーション概要
- 対象プール
- MYTKN / USDT
- 初期価格は 1 MYTKN = 0.0002 USDT
- 流動性提供時の主なパラメータ
- 入れるUSDT量(今回は70000USDTを基軸に設定)
- 下限価格
- 上限価格(今回は 0.002USDT に固定)
- シミュレーション内容
- 「MYTKN⇒USDT」方向にトークンが売却されるスワップが何回か起こる想定の下、プール内価格や残高がどのように推移するかを計算
- 一度のスワップ量を一定(例:1,000USDT分)とし、同方向のスワップを連続させる
- 価格やプール内残高、累計で売却されたMYTKN量などを可視化
価格レンジとは
価格レンジとは「流動性を提供する価格の範囲」
指定した価格レンジ内でのみ流動性が提供される(レンジ内のみのswap提供)
※左:uniswapV2 右:uniswapV3・4
出典: https://uniswap.org/whitepaper-v3.pdf
なぜ下限価格を設定するのか
フルレンジのメリット・デメリット
- メリット
- 価格がいくら上下しようと常に流動性を提供可能。
- スワップによるトークンの交換が制限されない(枯渇しない限りはMYTKN⇔USDTが常時可能)。
- デメリット
- 最低価格が担保されず、売却圧が高まればMYTKNの価格がほぼ0になるケースも起こり得る。
下限価格を設定するメリット・デメリット
- メリット
- 価格が一定以下に落ちるのを防ぎたい場合に、その「下限」を担保できる。
- 極端な暴落を防ぐために使える。
- デメリット
- 下限価格に到達してしまうと、MYTKNをさらに売却(⇒USDT)するスワップは行えない(プールのUSDTが枯渇またはレンジ外のため)。
- レンジが狭いほど、少ない売却量でも価格が大きく下がりやすくなる(資金効率が悪い)。
シミュレーション結果の概要
下記は一例として、MYTKNの下限を「なし」「現在価格の1/2」「1/5」「1/10」…といった複数パターンで試算した結果です。
- 下限なし(フルレンジ)
- どれだけ売りが来ても常にスワップが可能。
- ただし、売り圧に対して価格が限りなく下がるリスクがある。
- 下限1/2(例:0.0001USDT)
- 下限価格0.0001USDTを割った状態にはならず、一定の値を保つ。
- 一方で、MYTKNの売却が想定以上に増えると、USDTが早めに枯渇する可能性がある。
- 下限1/5、1/10…
- 下限をより低く設定すれば、より多くの売りを吸収できるが、価格下落幅が大きくなるため注意が必要。
- 例:下限1/10に設定すると、同じMYTKN売却量でも価格が1/4程度まで落ち込むケースがある。
各パターンでは「どのくらいの売り量に耐えられるか」や「プールから流出するUSDT量」などを比較し、実際に何億枚のMYTKNを売却されても問題ないか等を判断材料にします。
グラフ・考察ポイント
- グラフで見るポイント
- 売却累計MYTKN量 vs. 価格推移(MYTKN / USDT)
- 売却累計MYTKN量 vs. USDT流出量(プールから減るUSDTの累計)
-
このグラフの見方について
縦軸:MYTKN累計売却量(万)、他ユーザがUSDTを得るためにプールに投入したMYTKN量
横軸= 価格(1MYTKN= 0.000xx USDT)
各パターンの下限設定における、ほぼ全量(約95%)のUSDT分、MYTKNがプールに投入(売却)された地点 -
グラフからわかること
- 下限を設定すると、ある売却量を超えた段階で取引(MYTKN→USDT)が停止するため、価格の暴落を防ぎやすい。
- ただし、下限付近で一気に価格が下がるので、資金効率(少額のMTYKN売りで多くのUSDTがプールから持っていかれる)が悪化する。
- 下限を設定せずフルレンジにすると、売りが際限なく入ってしまい、場合によってはMYTKNが極端に値下がりするリスクがある。
-
例として下限1/2について解析
- 0.0001(現在価格の1/2)を割っていない
- 約5億MYTKNの売却でUSDTがなくなってしまう
- 下限を広くとったパターンに比べて、MYTKN売却による価格下落は低い。例えば下限を1/10に設定したパターンでは5億枚のMYTKN売却でMYTKNが0.00005(1/4)まで下がってしまう
シミュレーション結果のまとめと今後の展望
- 下限価格をどこに設定するか
- 価格を一定水準以下にしたくない(防衛ライン)
- どの程度の売りが想定されるか(例えば流通枚数が35億枚で、そのうち10億枚売られる可能性を想定するなど)といった条件に左右されます。
- フルレンジ であれば手間なく流動性を提供できますが、極端な売りに晒されるリスクを考慮すると、防衛ラインの設定も一案。
- USDT預入量を増やす ことでスリッページが緩和され、下落幅が低くなる反面、用意するMYTKN量も増大します。
今回記事を書く前にまとめた資料がこちらです。
グラフ作成の基となったシミュレート結果を表に加工しているのも載せています。
シミュレータ解説
以下は作成したシミュレータの解説です。使用したい方、ソースの内容について気になる方は御覧ください。
1. プロジェクト全体の概要
本コードは、Foundry を使ってUniswap v4相当のプールをローカルテスト環境で構築し、以下をシミュレートするものです。
- トークン (token0, token1) の用意 & ミント
- PoolManager・PositionManagerのデプロイ (Uniswap v4コア & v4-periphery相当)
- 下限価格(レンジの下側)、上限価格(レンジの上側)の指定
- 流動性提供(Liquidity Mint)
- 複数回のスワップ (token0 → token1 または token1 → token0)
- スワップ実行後の価格やプール残高を Foundry の console.log で出力
これにより、Uniswap v4で実際に行うような集中流動性の提供(上限/下限価格)とスワップ時の価格推移を簡易的に検証できます。
2. ソースコードの場所と構成
v-4template
リポジトリを流用し、SimulateTest.solを作成し追加しました。
-
setUp()
- テスト環境の初期化
- トークンの発行や、Router類(ModifyLiquidity, Swap, Donateなど)のデプロイ
- Provider / Investorなどのアドレスにトークンを配布し、Approve設定
- PoolKey(プールの識別情報)の生成
-
testSim()
- 実際にテストを走らせるメイン関数
- Token0/Token1のミント量やスワップ回数を定義
- simulateLiquidityAndSwaps() を呼び出している
-
simulateLiquidityAndSwaps(...)
- 実際にプールを初期化し、流動性を供給して、複数回のスワップを実行
- スワップのたびに「現在の価格 (sqrtPriceX96)」「プール内token0, token1の残高」をログ出力
- 下限価格・上限価格のtick計算や、供給したいトークン量などもここで制御可能
-
補助的な内部関数
- deployPoolManager(), deployPosm(), deployRouters() など
- getCurrentSqrtPrice(), encodePriceSqrt(), getTick() など
- logSimulationState() (スワップ結果のログ出力)
3. 使用方法
READMEに従い操作することでセットアップから実行まで行えます。
4. SimulateTest内の内容について
こちらでは各種変数について解説します。
値を変更して実行することでシミュレートしたい設定値にできます。
プール作成パラメータ
uint160 _initialSqrtPriceX96 = encodePriceSqrt(2, 1e4); // 0.0002
uint160 sqrtPriceLower = encodePriceSqrt(0, 0); // 0
uint160 sqrtPriceUpper = encodePriceSqrt(2, 1e3); // 0.002
uint256 token0Liquidity = type(uint96).max;
uint256 token1Liquidity = 70000 ether;
Uniswap V3 / V4 では、流動性提供時に「下限価格 (Lower Tick)」「上限価格 (Upper Tick)」を設定します。本テストコードでは、実数というよりsqrtPriceX96
形式で指定し、そこから Tick を計算しています。そのためencodePriceSqrt
という関数を作成し、uniswapフロント上で設定する値を直感的に入力できるようにしています。
-
encodePriceSqrt(amount1, amount0)
- ここでは「(amount1 << 192) / amount0 の平方根」を求め、
sqrtPriceX96
を返します。 - 例として encodePriceSqrt(2, 1e4) と書くと、これは「1 MYTKN = 0.0002 USDT 相当の価格」を表す
sqrtPriceX96
になります。
- ここでは「(amount1 << 192) / amount0 の平方根」を求め、
-
_initialSqrtPriceX96
: 初期価格 -
sqrtPriceLower
: 価格下限 -
sqrtPriceUpper
: 価格上限 -
token0Liquidity
: token0のプール預入許容量。今回はMYTKNが潤沢にあり、USDT(token1)をベースに決定するためtype(uint96).max
としてUSDTをベースに計算された額に応じて範囲内になるように大きい値を指定しています。 -
token0Liquidity
: token1のプール預入許容量。70,000USDT(70000e18)を指定しています。
swapパラメータ
uint256 swapCount = 10;
bool zeroForOne = true;
IPoolManager.SwapParams memory params = IPoolManager.SwapParams({
zeroForOne: zeroForOne,
amountSpecified: 1000 ether, // 1000USDT相当
sqrtPriceLimitX96: zeroForOne
? TickMath.MIN_SQRT_PRICE + 1
: TickMath.MAX_SQRT_PRICE - 1
});
-
swapCount
: swapする回数 -
zeroForOne
: トークンの交換方向指定- zeroForOne = true → 「token0 を売って token1 を買う」
- zeroForOne = false → 「token1 を売って token0 を買う」
-
amountSpecified
: swap量指定。int型であり、正と負で意味合いが変わる。- 正の値(+)
- 「与えるトークン量(Input Tokenの量)を指定」 する方式
- 例として amountSpecified = 1000 ether であれば、「1000 ether の token0を投入し、得られるもう片方のトークンの量は実際のプール流動性と価格次第」という意味になります。
- 負の値(-)
- 「受け取りたいトークン量(Output Tokenの量)を指定」 する方式
- 例として amountSpecified = -1000 ether と書いた場合、「1000 ether の token1を 受け取りたい」という指定です。その場合、実際に必要となる Input 側トークン量はプール価格や流動性によって変動し、最終的にスワップで確定します。
- 正の値(+)
-
sqrtPriceLimitX96
: スリッページの許容範囲。今回は無制限となるように指定しています。
出力ログの例
0,0.00012345,70000,350000
1,0.00012012,69900,349500
2,0.00011789,69800,349000
swap回数,swap後価格,プール内token0残高(MYTKN),プール内token1残高(USDT)
と表示しています。
最後に
今回、さまざまな設定値が実際にどのような挙動を示すのかを確認したく、シミュレーターを作成しシミュレーションを実施しました。
ソースコードを公開し、記事としてまとめるのは初めての試みとなりますので、もしお気づきの点がありましたらぜひご意見をいただけますと幸いです。
ソースコード
メンバー
- yamu
- kai adachi
Discussion