• Duel est ouvert sur Robinhood Chain
  • Lancez un duel deux tokens, trois pools, une transaction
  • Clôture du round dans --:--:--
  • Des rounds de quatre heures le rachat du gagnant a un plafond de prix
  • Liquidité bloquée à vie, aucun token pour l’équipe
Duelby

Construire sur Duel

Les pools de Duel sont des pools Uniswap V4 standards avec un hook, et chaque chiffre que montre l’app vient de vues et d’événements publics. Bots, terminaux, dashboards et agrégateurs peuvent construire dessus sans rien demander.

Lire un duel

DuelLens rassemble tout ce qui concerne un duel en un seul appel :

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

Un PairView contient l’identifiant du duel, son créateur et l’URI de ses métadonnées, les deux camps (token, nom, symbole, offre, prix et tick du pool, ETH par token, ETH réel dans le pool), les trois clés de pool, la répartition en direct (gaugeBps, la part du camp A en points de base), la moyenne du round jusqu’ici (roundGaugeBps), la cagnotte, les frais du créateur, le volume et les rachats depuis le lancement, la taxe anti-snipe en cours, l’index, le début et la fin du round et s’il a été tradé (roundTraded), l’heure et le tick de lancement (launchTime, startTick), s’il s’agit d’un duel supporter, sa catégorie (category, selon la numérotation de l’app, 0 pour aucune), ses conditions de frais (fees) et la fraction de sa part que le protocole prend en ce moment (protocolRateBps, sur 10 000) : le protocole reçoit fees.protocolShareBps × protocolRateBps / 10 000 des frais de swap, et la cagnotte le reste de cette part. Dans les contrats et leurs ABI, le camp B s’appelle D : d, tokenD, keyD.

Le hook a des vues plus petites : pairCount(), getPair(pairId), poolKeys(pairId), gaugeBps(pairId), snipeTaxBps(pairId), protocolRateBps(), protocolFees() et firstRoundEnd(launchTime).

Trader

N’importe quel router Uniswap V4 peut trader les pools d’un duel. Construisez les clés de pool à partir de poolKeys(pairId) :

Poolcurrency0currency1feetickSpacing
ETH / AETH (address(0))token A0200
ETH / BETH (address(0))token B0200
A / Bl’adresse de token la plus bassela plus haute060

Le hook prélève ses frais par des return deltas : demandez vos cotations au V4 Quoter, dont les réponses incluent tous les frais et toutes les taxes. Un changement de camp par l’ETH se cote avec son quoteExactInput, sur le chemin token → ETH → autre token.

Un swap dont le hook prend les frais avant son exécution (un achat ou un changement de camp à entrée exacte, une vente à sortie exacte) doit s’exécuter en entier : celui que sa limite de prix, ou un pool ETH à court d’ETH, arrêterait avant est annulé avec PartialFill. Le PoolManager enveloppe l’erreur d’un hook : PartialFill et ArbitrageStarved arrivent dans WrappedError(address target, bytes4 selector, bytes reason, bytes details), et dans UnexpectedRevertBytes(bytes revertData) quand c’est le V4 Quoter qui les rapporte. Décodez reason pour lire l’erreur du hook.

Chaque swap se termine par le réalignement du hook, qui a besoin de son propre gaz : un swap qui lui en laisse trop peu est annulé avec ArbitrageStarved au lieu de le sauter. Envoyez les swaps avec le gaz qu’une estimation donne, jamais moins, et avec de la marge : le premier swap après la clôture d’un round règle aussi ce round (son rachat, puis un réalignement, environ 170 000 de gaz en plus), ce qu’une estimation faite avant la clôture ne compte pas. L’app ajoute 500 000 de gaz à chaque estimation.

DuelRouter propose un appel pour un swap dans un pool :

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

et un pour un changement de camp par l’ETH, le camp vendu dans son pool ETH et tout l’ETH obtenu dépensé dans celui de l’autre :

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

Une vente dans un pool ETH de Duel qui s’arrête au prix de lancement du pool, son ETH épuisé, est relancée pour le reste dans le même appel, une fois que le réalignement du hook a racheté au pool ce que le pool entre les camps absorbe : jusqu’à SALE_PASSES() (8) swaps, tant que le prix du pool reste sous la limite des swaps. Chacun paie les frais du hook sur son propre ETH, et minAmountOut les compte tous. Le V4 Quoter d’Uniswap cote un seul swap : au-delà de ce qu’il exécute, il répond NotEnoughLiquidity. L’app cote une telle vente en exécutant le router comme le trader (un eth_call avec un state override, DuelSimulator).

Ce que le réalignement ne peut plus racheter, sell l’emmène par le pool entre les camps jusque dans le pool ETH de l’autre camp, où les réalignements ont déplacé l’ETH qui le paie :

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

Il vend dans keyIn comme le fait swapExactIn, puis ce qui reste par keyCross, qui l’absorbe en entier (ses frais de changement de camp brûlés, comme pour tout changement de camp), et dans keyOther, passe après passe là aussi. minAmountOut compte l’ETH des deux pools. Ce que keyOther ne peut pas racheter non plus est versé à recipient dans le token de ce pool, hors du minimum. L’app vend par sell quand le V4 Quoter refuse une vente et que DuelSimulator.simulateSale montre que cet appel vend tout contre de l’ETH.

Un autre router V4 fait un seul swap. Le hook ne refuse pas une vente à entrée exacte qui dépasse ce que l’ETH du pool peut payer (ses frais sont pris après le swap, sur l’ETH versé) : la vente s’exécute en partie, jusqu’au prix de lancement du pool, et le reste demeure chez le vendeur. Fixez le minimum reçu pour ce qui peut s’exécuter, et vendez le reste dans une transaction suivante, contre ce que le réalignement a racheté.

Envoyez de l’ETH avec swapExactIn pour acheter. Pour vendre ou changer de camp, approuvez le router pour le token, ou laissez un permit (EIP-2612) l’approuver dans la même transaction : swapExactInWithPermit, swapThroughEthWithPermit et sellWithPermit prennent les mêmes arguments, plus le permit de l’appelant pour exactement amountIn, signé pour le router :

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

Votre wallet signe les permits d’un token Duel dans le domaine EIP-712 de ce token, nommé Duel, version 1, à l’adresse du token (son eip712Domain(), ERC-5267, l’indique). Le router utilise le permit d’abord et ignore son échec : un permit déjà envoyé par n’importe qui a posé l’autorisation, et un permit qui échoue ne change rien, le swap prenant alors ce que couvre l’autorisation déjà en place, et échouant si elle ne suffit pas. Un swap qui ne s’exécute qu’en partie conserve l’autorisation inutilisée. L’échéance du permit limite le moment où sa signature peut être présentée ; elle ne fait pas expirer l’autorisation déjà accordée. Le router ne prend de tokens qu’à l’appelant. L’ETH d’un changement de camp reste dans le PoolManager pour que le second pool le dépense ; un pool Duel le dépense en entier ou le hook annule le swap (PartialFill), et sur tout autre pool, ce qu’il ne peut pas dépenser revient à l’appelant.

Lancer un duel

DuelFactory.createPair(CreateParams) lance un duel ; envoyez avec l’appel les frais de lancement et les achats de lancement, l’excédent est remboursé. CreateParams contient nameA, symbolA, nameD, symbolD (le camp B), metadataURI, buyA, buyD (l’ETH dépensé sur chaque camp au lancement, sans taxe anti-snipe), supporter, category (selon la numérotation des catégories de l’app, 0 pour aucune) et config, le configHash() de la factory tel que vous l’avez lu : le lancement est annulé avec ConfigChanged si les conditions ont changé depuis. Un nom fait de 1 à 128 octets, un ticker de 1 à 40 et l’URI des métadonnées 4 096 au plus (InvalidMetadata) ; un achat de lancement au-delà de 5 % de l’offre d’un camp est annulé (CreatorBuyTooLarge).

La factory démarre en pause. Tant que paused() est vrai, seul owner() peut lancer un duel ; les autres appels sont annulés avec CreationPaused. Le propriétaire ouvre les lancements avec setPaused(false). Les duels existants continuent de s’échanger.

Les événements

ContratÉvénementQuand
DuelFactoryPairCreated(pairId, creator, tokenA, tokenD)Un duel a été lancé
DuelHookPairRegistered(pairId, tokenA, tokenD, creator, category)Ses pools enregistrés auprès du hook, dans la même transaction ; category indique son sujet, selon la numérotation des catégories de l’app (0 pour aucune)
DuelHookFeeTaken(pairId, fee, snipeTax)Un trade contre l’ETH a payé ses frais
DuelHookSwitchFeeBurned(pairId, token, amount)Un changement de camp a brûlé ses frais
DuelHookArbitrageCaptured(pairId, profit)Le réalignement a rempli la cagnotte
DuelHookRoundSettled(pairId, roundIndex, averageTick, winner, ethSpent, tokensBurned)Un round s’est clôturé
DuelHookCreatorFeesClaimed(pairId, to, amount)Un créateur a été payé
DuelFactoryCreatorTransferStarted(pairId, creator, newCreator)Un créateur a proposé son rôle (à l’adresse zéro : proposition retirée)
DuelHookCreatorChanged(pairId, from, to)Le rôle de créateur d’un duel a changé de mains
DuelHookCreatorFeesGivenUp(pairId, creator)Le créateur a définitivement affecté sa part des frais futurs aux cagnottes
DuelHookProtocolFeesClaimed(to, amount)La trésorerie a été payée
DuelHookProtocolRateSet(rateBps)La part du protocole sur les frais de swap de tous les duels a changé d’un coup : il en prend rateBps (10 000 pour la totalité), le reste va aux cagnottes
DuelFactoryConfigProposed(config, eta), ConfigApplied(config), ConfigCancelled()Des conditions de lancement proposées pour 48 heures plus tard, appliquées, retirées
DuelFactoryPausedSet(paused)Lancements mis en pause ou rouverts
PoolManagerSwap(id, sender, amount0, amount1, sqrtPriceX96, liquidity, tick, fee)Chaque trade, y compris le réalignement du hook lui-même (son sender est le hook)

Les listes de trades lisent les swaps des trois pools ; les graphiques des prix et de la répartition lisent seulement les deux pools ETH. Les listes de détenteurs utilisent les événements Transfer des tokens. Le subgraph indexe les swaps, soldes, détenteurs et rounds réglés pour charger l’historique sans relire toute la chaîne depuis le lancement.

Régler et réclamer

  • settle(pairId) règle un round terminé. N’importe qui peut l’appeler.
  • claimCreatorFees(pairId) paie seulement le créateur du duel, le wallet qui l’a lancé ou l’adresse à qui il a cédé son rôle ; claimProtocolFees() paie la trésorerie configurée au moment de la réclamation. N’importe qui peut déclencher ces appels. La trésorerie peut changer après le délai de 48 heures de la factory ; l’appelant ne peut pas choisir un autre destinataire.
  • transferCreator(pairId, newCreator) sur la factory propose le rôle de créateur d’un duel, que acceptCreator(pairId) appelé par newCreator prend ; pendingCreator(pairId) lit la proposition. Seul le créateur le propose, et seule l’adresse proposée le prend.
  • giveUpCreatorFees(pairId) sur le hook envoie définitivement la part du créateur sur les frais futurs aux cagnottes des rounds. Seul le créateur actuel peut l’appeler. Les frais déjà accumulés restent réclamables par le créateur, et le rôle de créateur peut toujours être transféré.