• Duel è aperto su Robinhood Chain
  • Lancia un duello due token, tre pool, una transazione
  • Il round chiude tra --:--:--
  • Round di quattro ore il riacquisto del vincitore ha un limite di prezzo
  • Liquidità bloccata per sempre, nessun token del team

Costruire su Duel

I pool di Duel sono pool Uniswap V4 standard con un hook, e ogni numero che l'app mostra viene da viste ed eventi pubblici. Bot, terminali, dashboard e aggregatori possono costruirci sopra senza chiedere permesso.

Leggere un duello

DuelLens raccoglie tutto su un duello in una sola chiamata:

function getPair(uint256 pairId) external view returns (PairView memory);
function getPairs(uint256 offset, uint256 limit) external view returns (PairView[] memory);

Un PairView contiene l'id del duello, il suo creatore e l'URI dei suoi metadati, le due fazioni (token, nome, simbolo, offerta, prezzo e tick del pool, ETH per token, ETH reale nel pool), le tre chiavi dei pool, la ripartizione dal vivo (gaugeBps, la quota della fazione A in punti base), la media del round finora (roundGaugeBps), il piatto, le commissioni del creatore, il volume e i riacquisti totali, la tassa anti-snipe attuale, l'indice, l'inizio e la fine del round e se ha avuto operazioni (roundTraded), l'ora e il tick di lancio (launchTime, startTick), se è un duello solidale, la sua categoria (category, secondo la numerazione dell'app, 0 per nessuna), i suoi termini di commissione (fees) e la parte della sua quota che il protocollo prende ora (protocolRateBps, su 10.000): il protocollo riceve fees.protocolShareBps × protocolRateBps / 10.000 della commissione di swap, e il piatto il resto di quella quota. Nei contratti e nelle loro ABI, la fazione B è D: d, tokenD, keyD.

L'hook ha viste più piccole: pairCount(), getPair(pairId), poolKeys(pairId), gaugeBps(pairId), snipeTaxBps(pairId), protocolRateBps(), protocolFees() e firstRoundEnd(launchTime).

Fare trading

Qualsiasi router Uniswap V4 può fare trading sui pool di un duello. Costruisci le chiavi dei pool a partire da poolKeys(pairId):

Poolcurrency0currency1feetickSpacing
ETH / AETH (address(0))token A0200
ETH / BETH (address(0))token B0200
A / Bl'indirizzo di token più bassoquello più alto060

L'hook preleva le sue commissioni tramite return delta, quindi quota con il V4 Quoter: le sue risposte includono ogni commissione e tassa. Un passaggio tramite ETH è il suo quoteExactInput lungo il percorso token → ETH → l'altro token.

Uno swap di cui l'hook preleva la commissione prima dell'esecuzione (un acquisto o un passaggio a input esatto, una vendita a output esatto) deve completarsi interamente: uno che il suo limite di prezzo, o un pool ETH a corto di ETH, fermerebbe prima fa revert con PartialFill. Il PoolManager incapsula l'errore di un hook: PartialFill e ArbitrageStarved arrivano dentro WrappedError(address target, bytes4 selector, bytes reason, bytes details), e dentro UnexpectedRevertBytes(bytes revertData) quando è il V4 Quoter a riportarli. Decodifica reason per leggere l'errore dell'hook.

Ogni operazione termina con il riallineamento dell'hook, che richiede gas proprio: uno swap che gliene lascia troppo poco fa revert con ArbitrageStarved invece di saltarlo. Invia gli swap con il gas di una stima, mai meno, e con un margine: il primo swap dopo la chiusura di un round regola anche il round (il suo riacquisto, poi un riallineamento, circa 170.000 di gas in più), cosa che una stima fatta prima della chiusura non conta. L'app aggiunge 500.000 di gas a ogni stima.

DuelRouter offre una chiamata per uno swap in un pool:

function swapExactIn(
    PoolKey calldata key,
    bool zeroForOne,
    uint256 amountIn,
    uint256 minAmountOut,
    address recipient,
    uint256 deadline
) external payable returns (uint256 amountOut);

e una per un passaggio tramite ETH, in cui la fazione che lasci viene venduta nel suo pool ETH e tutto l'ETH ottenuto viene speso in quello dell'altra:

function swapThroughEth(
    PoolKey calldata keyIn,
    PoolKey calldata keyOut,
    uint256 amountIn,
    uint256 minAmountOut,
    address recipient,
    uint256 deadline
) external returns (uint256 amountOut);

Una vendita in un pool ETH di Duel che si ferma al prezzo di lancio del pool, esaurito il suo ETH, viene inviata di nuovo per il resto nella stessa chiamata, una volta che il riallineamento dell'hook ha riacquistato dal pool ciò che assorbe il pool tra le fazioni: fino a SALE_PASSES() (8) swap, finché il prezzo del pool resta sotto il limite degli swap. Ognuno paga la commissione dell'hook sul proprio ETH, e minAmountOut li conta tutti. Il V4 Quoter di Uniswap quota un solo swap: oltre ciò che esegue, risponde NotEnoughLiquidity. L'app quota una vendita simile eseguendo il router come il trader (una eth_call con uno state override, DuelSimulator).

Ciò che il riallineamento non può più riacquistare, sell lo porta attraverso il pool tra le fazioni fino al pool ETH dell'altra fazione, dove i riallineamenti hanno spostato l'ETH che lo paga:

function sell(
    PoolKey calldata keyIn,
    PoolKey calldata keyCross,
    PoolKey calldata keyOther,
    uint256 amountIn,
    uint256 minAmountOut,
    address recipient,
    uint256 deadline
) external returns (uint256 amountOut);

Vende in keyIn come fa swapExactIn, poi ciò che resta attraverso keyCross, che lo assorbe tutto (la sua commissione di passaggio viene bruciata, come in ogni passaggio), e in keyOther, anche qui a più riprese. minAmountOut conta l'ETH di entrambi i pool. Ciò che nemmeno keyOther può riacquistare viene pagato a recipient nel token di quel pool, fuori dal minimo. L'app vende tramite sell quando il V4 Quoter rifiuta una vendita e DuelSimulator.simulateSale mostra che così si vende tutto contro ETH.

Un altro router V4 fa un solo swap. L'hook non rifiuta una vendita a input esatto che supera ciò che l'ETH del pool può pagare (la sua commissione è prelevata dopo lo swap, sull'ETH pagato): la vendita viene eseguita in parte, fino al prezzo di lancio del pool, e il resto rimane al venditore. Imposta il minimo ricevuto su ciò che può essere eseguito, e vendi il resto in una transazione successiva, contro ciò che il riallineamento ha riacquistato.

Invia ETH con swapExactIn per comprare. Per vendere o passare, approva il router per il token, oppure lascia che un permit (EIP-2612) lo approvi nella stessa transazione: swapExactInWithPermit, swapThroughEthWithPermit e sellWithPermit prendono gli stessi argomenti e il permit del chiamante per esattamente amountIn, firmato per il router:

struct Permit {
    uint256 deadline;
    uint8 v;
    bytes32 r;
    bytes32 s;
}

Il wallet dell'utente firma i permit per ogni token di Duel in un dominio EIP-712 chiamato Duel, versione 1, con l'indirizzo del token (il suo eip712Domain(), ERC-5267, lo dichiara). Il router usa prima il permit e ne ignora il fallimento: uno già inviato da chiunque ha impostato l'allowance, e uno che fallisce non cambia nulla; lo swap prende allora ciò che copre l'allowance già esistente e fa revert se non basta. Uno swap che si completa solo in parte lascia valida la quota non utilizzata dell'allowance. La scadenza del permit limita il termine entro cui la firma può essere presentata, ma non fa scadere l'allowance già concessa. Il router preleva token solo dal chiamante. L'ETH di un passaggio resta nel PoolManager perché lo spenda il secondo pool; un pool di Duel lo spende tutto o l'hook fa revert dello swap (PartialFill), e su qualsiasi altro pool ciò che non riesce a spendere viene restituito al chiamante.

Lanciare un duello

DuelFactory.createPair(CreateParams) lancia un duello; invia con la chiamata la commissione di lancio e gli acquisti al lancio, l'eventuale eccedenza viene rimborsata. CreateParams contiene nameA, symbolA, nameD, symbolD (la fazione B), metadataURI, buyA, buyD (l'ETH speso su ogni fazione al lancio, senza la tassa anti-snipe), supporter, category (secondo la numerazione delle categorie dell'app, 0 per nessuna) e config, il configHash() della factory così come l'hai letto: il lancio fa revert con ConfigChanged se nel frattempo i termini sono cambiati. Un nome occupa da 1 a 128 byte, un ticker da 1 a 40 e l'URI dei metadati al massimo 4.096 (InvalidMetadata); un acquisto al lancio oltre il 5% dell'offerta di una fazione fa revert (CreatorBuyTooLarge).

La factory parte con i lanci in pausa. Finché paused() è true, solo owner() può lanciare un duello; le chiamate degli altri vengono annullate con CreationPaused. Il proprietario apre i lanci con setPaused(false). I duelli esistenti continuano a essere scambiati.

Eventi

ContrattoEventoQuando
DuelFactoryPairCreated(pairId, creator, tokenA, tokenD)È stato lanciato un duello
DuelHookPairRegistered(pairId, tokenA, tokenD, creator, category)I suoi pool sono stati registrati presso l'hook, nella stessa transazione; category è il suo tema, secondo la numerazione delle categorie dell'app (0 per nessuna)
DuelHookFeeTaken(pairId, fee, snipeTax)Un'operazione contro ETH ha pagato la sua commissione
DuelHookSwitchFeeBurned(pairId, token, amount)Un passaggio ha bruciato la sua commissione
DuelHookArbitrageCaptured(pairId, profit)Il riallineamento ha riempito il piatto
DuelHookRoundSettled(pairId, roundIndex, averageTick, winner, ethSpent, tokensBurned)Un round si è chiuso
DuelHookCreatorFeesClaimed(pairId, to, amount)Un creatore è stato pagato
DuelFactoryCreatorTransferStarted(pairId, creator, newCreator)Un creatore ha offerto il suo ruolo (all'indirizzo zero: offerta ritirata)
DuelHookCreatorChanged(pairId, from, to)Il ruolo di creatore di un duello è passato di mano
DuelHookCreatorFeesGivenUp(pairId, creator)Il creatore ha destinato definitivamente la propria quota delle commissioni future ai montepremi
DuelHookProtocolFeesClaimed(to, amount)La tesoreria è stata pagata
DuelHookProtocolRateSet(rateBps)La quota del protocollo sulla commissione di swap di tutti i duelli è cambiata subito: ne prende rateBps (10.000 per tutta), il resto va ai piatti
DuelFactoryConfigProposed(config, eta), ConfigApplied(config), ConfigCancelled()Termini di lancio proposti con 48 ore di anticipo, applicati, ritirati
DuelFactoryPausedSet(paused)Lanci sospesi o ripresi
PoolManagerSwap(id, sender, amount0, amount1, sqrtPriceX96, liquidity, tick, fee)Ogni operazione, compreso il riallineamento dell'hook stesso (il suo sender è l'hook)

Gli elenchi delle operazioni leggono gli swap di tutti e tre i pool; i grafici del prezzo e della ripartizione leggono solo i due pool ETH. Gli elenchi degli holder usano gli eventi Transfer dei token. Il subgraph indicizza swap, saldi, holder e round regolati, così l'app può caricare la storia senza rileggere la chain dal lancio.

Regolare e riscuotere

  • settle(pairId) regola un round finito. Chiunque può chiamarla.
  • claimCreatorFees(pairId) paga solo il creatore del duello, il wallet che l'ha lanciato o l'indirizzo a cui ha ceduto il ruolo; claimProtocolFees() paga la tesoreria configurata al momento della riscossione. Chiunque può avviare l'una o l'altra riscossione. La tesoreria può cambiare dopo il periodo di 48 ore della factory; chi chiama non può scegliere un altro destinatario.
  • transferCreator(pairId, newCreator) sulla factory offre il ruolo di creatore di un duello, che acceptCreator(pairId) chiamata da newCreator accetta; pendingCreator(pairId) legge l'offerta. Solo il creatore la offre, e solo l'indirizzo a cui è offerta la accetta.
  • giveUpCreatorFees(pairId) sull’hook invia definitivamente la quota del creatore sulle commissioni future ai montepremi dei round. Solo il creatore attuale può chiamarla. Le commissioni già accumulate restano riscuotibili dal creatore e il ruolo di creatore può ancora essere trasferito.