Duel で開発する
Duel のプールは hook 付きの標準的な Uniswap V4 プールで、アプリに表示される数値はすべて公開のビューとイベントから得られます。ボット、ターミナル、ダッシュボード、アグリゲーターは、許可を求めることなくその上に構築できます。
デュエルを読む
DuelLens は、1回の呼び出しでデュエルのすべてを集めます:
function getPair(uint256 pairId) external view returns (PairView memory);
function getPairs(uint256 offset, uint256 limit) external view returns (PairView[] memory);
PairView には、デュエルの ID、クリエイター、メタデータ URI、両陣営(トークン、名前、シンボル、供給量、プールの価格と tick、トークンあたりの ETH、プール内の実際の ETH)、3つのプールキー、ライブの比率(gaugeBps、ベーシスポイントで表したA陣営の取り分)、ここまでのラウンドの平均(roundGaugeBps)、ポット、クリエイター手数料、累計の出来高と買い戻し、現在のアンチスナイプ税、ラウンドの番号・開始・終了と取引があったかどうか(roundTraded)、ローンチの時刻と tick(launchTime、startTick)、サポーターデュエルかどうか、カテゴリー(category、アプリの番号付けによるもので、なしは 0)、手数料条件(fees)、そしてプロトコルが現在その取り分のうち受け取る割合(protocolRateBps、すべてなら 10,000)が入っています。プロトコルはスワップ手数料の fees.protocolShareBps × protocolRateBps / 10,000 を受け取り、その取り分の残りはポットに入ります。コントラクトとその ABI では、B陣営は D です:d、tokenD、keyD。
hook には、より小さなビューもあります:pairCount()、getPair(pairId)、poolKeys(pairId)、gaugeBps(pairId)、snipeTaxBps(pairId)、protocolRateBps()、protocolFees()、firstRoundEnd(launchTime)。
取引する
どの Uniswap V4 ルーターでもデュエルのプールで取引できます。プールキーは poolKeys(pairId) から組み立てます:
| プール | currency0 | currency1 | fee | tickSpacing |
|---|---|---|---|---|
| ETH / A | ETH(address(0)) | トークン A | 0 | 200 |
| ETH / B | ETH(address(0)) | トークン B | 0 | 200 |
| A / B | アドレスの小さい方のトークン | 大きい方のトークン | 0 | 60 |
hook はリターンデルタで手数料を徴収するため、見積もりには V4 Quoter を使ってください。その回答にはすべての手数料と税が含まれます。ETH 経由の乗り換えは、トークン → ETH → もう一方のトークン という経路での quoteExactInput です。
実行前に hook が手数料を取るスワップ(入力額指定の買いや乗り換え、出力額指定の売り)は、完全に成立しなければなりません。価格の上限や、ETH が足りない ETH プールによって途中で止まるスワップは、PartialFill でリバートします。PoolManager は hook のエラーを包みます。PartialFill と ArbitrageStarved は WrappedError(address target, bytes4 selector, bytes reason, bytes details) の中に入って届き、V4 Quoter が報告するときは UnexpectedRevertBytes(bytes revertData) の中に入って届きます。hook のエラーを読むには reason をデコードしてください。
すべての取引は hook の再調整で終わり、再調整にはそれ自体のガスが必要です。再調整に残すガスが少なすぎるスワップは、再調整をスキップせずに ArbitrageStarved でリバートします。スワップは見積もられたガスを決して減らさず、余裕を持たせて送ってください。ラウンド終了後の最初のスワップはそのラウンドの精算も行い(買い戻し、続いて再調整で、約170,000ガス多くかかります)、終了前に行った見積もりにはそれが含まれません。アプリは各見積もりに500,000ガスを上乗せします。
DuelRouter には、1つのプールでのスワップ用の呼び出しがあります:
function swapExactIn(
PoolKey calldata key,
bool zeroForOne,
uint256 amountIn,
uint256 minAmountOut,
address recipient,
uint256 deadline
) external payable returns (uint256 amountOut);
さらに、ETH 経由の乗り換え用の呼び出しもあります。手放す陣営をその ETH プールで売り、得た ETH をすべてもう一方のプールで使います:
function swapThroughEth(
PoolKey calldata keyIn,
PoolKey calldata keyOut,
uint256 amountIn,
uint256 minAmountOut,
address recipient,
uint256 deadline
) external returns (uint256 amountOut);
Duel の ETH プールへの売却が、ETH を使い切ってプールのローンチ価格で止まった場合、hook の再調整が両陣営の間のプールが引き受ける分をプールから買い戻した後、残りについて同じ呼び出しの中で再び送られます。プールの価格がスワップの上限より下にある間、最大 SALE_PASSES()(8)回のスワップです。各スワップは自身の ETH に対して hook の手数料を払い、minAmountOut はその合計に適用されます。Uniswap の V4 Quoter は1回のスワップを見積もり、それが約定する分を超えると NotEnoughLiquidity を返します。アプリはこうした売却を、ルーターをトレーダーとして実行して見積もります(ステートオーバーライド付きの eth_call、DuelSimulator)。
再調整がもう買い戻せない分は、sell が引き継ぎ、両陣営の間のプールを通して、もう一方の陣営の ETH プールへ売ります。その代金を支払う ETH は、これまでの再調整がそこへ移したものです:
function sell(
PoolKey calldata keyIn,
PoolKey calldata keyCross,
PoolKey calldata keyOther,
uint256 amountIn,
uint256 minAmountOut,
address recipient,
uint256 deadline
) external returns (uint256 amountOut);
sell は swapExactIn と同じように keyIn へ売り、残りを keyCross に通してから(keyCross はそのすべてを引き受け、どの乗り換えとも同じく乗り換え手数料はバーンされます)、keyOther へ、こちらも何回かに分けて売ります。minAmountOut は両方のプールの ETH の合計に適用されます。keyOther でも買い戻せない分は、そのプールのトークンで recipient に支払われ、最低受取額には含まれません。アプリは、V4 Quoter が売却の見積もりを拒み、DuelSimulator.simulateSale が sell ならすべてを ETH に売れると示したときに、sell で売ります。
ほかの V4 ルーターは1回のスワップしか行いません。hook は、プールの ETH で支払える分を超える入力額指定の売りを拒否しません(手数料はスワップの後に、支払われた ETH から取られます)。売りはプールのローンチ価格まで一部だけ約定し、残りは売り手の手元に残ります。最低受取額は約定できる分に合わせて設定し、残りは後のトランザクションで、再調整が買い戻した分に対して売ってください。
買うときは swapExactIn に ETH を付けて送ります。売りや乗り換えでは、ルーターにトークンを approve するか、同じトランザクションの中で permit(EIP-2612)に approve させます。swapExactInWithPermit、swapThroughEthWithPermit、sellWithPermit は同じ引数に加えて、ルーター宛てに署名された、ちょうど amountIn 分の呼び出し元の permit を受け取ります:
struct Permit {
uint256 deadline;
uint8 v;
bytes32 r;
bytes32 s;
}
ユーザーのウォレットは、各 Duel トークンのアドレスを使い、そのトークンの Duel という名前、バージョン 1 の EIP-712 ドメインで permit に署名します(その eip712Domain()、ERC-5267 がそれを示します)。ルーターはまず permit を使い、その失敗は無視します。誰かがすでに送った permit は allowance を設定済みで、失敗した permit は何も変えません。その場合スワップは既存の allowance でまかなえる分を使い、足りなければリバートします。一部だけ成立したスワップでは、使われなかった allowance が残ります。permit の期限は署名を提出できる期間を制限しますが、すでに設定された allowance を失効させるものではありません。ルーターがトークンを引き出すのは呼び出し元からだけです。乗り換えの ETH は、2つ目のプールが使えるように PoolManager にとどまります。Duel のプールはそれを使い切り、使い切れなければ hook がスワップをリバートします(PartialFill)。それ以外のプールでは、使い切れなかった分は呼び出し元に返されます。
ローンチする
DuelFactory.createPair(CreateParams) がデュエルをローンチします。ローンチ手数料とローンチ購入の ETH を一緒に送ってください。超過分は返金されます。CreateParams は nameA、symbolA、nameD、symbolD(B陣営)、metadataURI、buyA、buyD(ローンチ時に各陣営に使う ETH で、アンチスナイプ税はかかりません)、supporter、category(アプリのカテゴリー番号で、なしは 0)、そして config からなります。config は、あなたが読み取った時点のファクトリーの configHash() です。その後に条件が変わっていれば、ローンチは ConfigChanged でリバートします。名前は1〜128バイト、ティッカーは1〜40バイト、メタデータ URI は最大4,096バイトです(InvalidMetadata)。陣営の供給量の5%を超えるローンチ購入はリバートします(CreatorBuyTooLarge)。
ファクトリーはローンチを一時停止した状態で開始します。paused() が true の間は owner() だけがデュエルをローンチでき、他の呼び出し元は CreationPaused でリバートします。オーナーは setPaused(false) でローンチを開放します。既存デュエルの取引は継続します。
イベント
| コントラクト | イベント | タイミング |
|---|---|---|
| DuelFactory | PairCreated(pairId, creator, tokenA, tokenD) | デュエルがローンチされた |
| DuelHook | PairRegistered(pairId, tokenA, tokenD, creator, category) | そのプールが同じトランザクションで hook に登録された。category はデュエルのテーマを表す、アプリのカテゴリー番号(なしは 0) |
| DuelHook | FeeTaken(pairId, fee, snipeTax) | ETH での取引が手数料を支払った |
| DuelHook | SwitchFeeBurned(pairId, token, amount) | 乗り換えが手数料をバーンした |
| DuelHook | ArbitrageCaptured(pairId, profit) | 再調整がポットを満たした |
| DuelHook | RoundSettled(pairId, roundIndex, averageTick, winner, ethSpent, tokensBurned) | ラウンドが終了した |
| DuelHook | CreatorFeesClaimed(pairId, to, amount) | クリエイターに支払われた |
| DuelFactory | CreatorTransferStarted(pairId, creator, newCreator) | クリエイターが役割を譲る提案をした(ゼロアドレス宛ては提案の取り下げ) |
| DuelHook | CreatorChanged(pairId, from, to) | デュエルのクリエイターの役割が移った |
| DuelHook | CreatorFeesGivenUp(pairId, creator) | クリエイターが今後の手数料の取り分をポットへ恒久的に振り向けました |
| DuelHook | ProtocolFeesClaimed(to, amount) | トレジャリーに支払われた |
| DuelHook | ProtocolRateSet(rateBps) | すべてのデュエルのスワップ手数料におけるプロトコルの取り分がすぐに変わった。プロトコルはその rateBps を受け取り(すべてなら 10,000)、残りはポットに入る |
| DuelFactory | ConfigProposed(config, eta), ConfigApplied(config), ConfigCancelled() | 48時間後に向けてローンチ条件が提案された、適用された、取り下げられた |
| DuelFactory | PausedSet(paused) | ローンチが一時停止された、または再開された |
| PoolManager | Swap(id, sender, amount0, amount1, sqrtPriceX96, liquidity, tick, fee) | すべての取引。hook 自身の再調整も含む(その sender は hook) |
取引一覧は3つのプールすべてのスワップを読み、価格と比率のチャートは2つの ETH プールだけを読みます。ホルダー一覧はトークンの Transfer イベントを使います。サブグラフはスワップ、残高、ホルダー、精算されたラウンドをインデックスするので、アプリはローンチからチェーンを再生しなくても履歴を読み込めます。
精算と受け取り
settle(pairId)は終わったラウンドを精算します。誰でも呼び出せます。claimCreatorFees(pairId)が支払う相手は、デュエルのクリエイター(ローンチしたウォレット、または役割を譲られたアドレス)だけです。claimProtocolFees()は受け取り時点で設定されているトレジャリーに支払います。どちらの受け取りも誰でも実行できます。トレジャリーはファクトリーの48時間の待機期間の後に変わることがありますが、呼び出し元が別の受取先を選ぶことはできません。- ファクトリーの
transferCreator(pairId, newCreator)はデュエルのクリエイターの役割を提案し、newCreatorからのacceptCreator(pairId)がそれを受け取ります。pendingCreator(pairId)で提案を読めます。提案できるのはクリエイターだけで、受け取れるのは提案されたアドレスだけです。 - フックの
giveUpCreatorFees(pairId)は、今後発生する手数料のクリエイター取り分をラウンドのポットへ恒久的に送ります。呼び出せるのは現在のクリエイターだけです。すでに蓄積された手数料は引き続きクリエイターが請求でき、クリエイターの役割も引き続き譲渡できます。