Construir sobre Duel
Los pools de Duel son pools estándar de Uniswap V4 con un hook, y cada cifra que muestra la app sale de vistas y eventos públicos. Bots, terminales, dashboards y agregadores pueden construir sobre él sin pedir permiso.
Leer un duelo
DuelLens reúne todo sobre un duelo en una sola llamada:
function getPair(uint256 pairId) external view returns (PairView memory);
function getPairs(uint256 offset, uint256 limit) external view returns (PairView[] memory);
Un PairView contiene el id del duelo, su creador y la URI de sus metadatos, los dos bandos (token, nombre, símbolo, suministro, precio y tick del pool, ETH por token, ETH real en el pool), las tres claves de pool, el reparto en directo (gaugeBps, la parte del bando A en puntos básicos), la media de la ronda hasta ahora (roundGaugeBps), el bote, las comisiones del creador, el volumen y las recompras históricos, el impuesto antisnipe actual, el índice, el inicio y el fin de la ronda y si ha tenido operaciones (roundTraded), el momento y el tick de lanzamiento (launchTime, startTick), si es un duelo solidario, su categoría (category, según la numeración de la app, 0 para ninguna), sus términos de comisiones (fees) y la fracción de su parte que el protocolo toma ahora (protocolRateBps, sobre 10.000): el protocolo recibe fees.protocolShareBps × protocolRateBps / 10.000 de la comisión de swap, y el bote el resto de esa parte. En los contratos y sus ABI, el bando B es D: d, tokenD, keyD.
El hook tiene vistas más pequeñas: pairCount(), getPair(pairId), poolKeys(pairId), gaugeBps(pairId), snipeTaxBps(pairId), protocolRateBps(), protocolFees() y firstRoundEnd(launchTime).
Operar
Cualquier router de Uniswap V4 puede operar en los pools de un duelo. Construye las claves de pool a partir de poolKeys(pairId):
| Pool | currency0 | currency1 | fee | tickSpacing |
|---|---|---|---|---|
| ETH / A | ETH (address(0)) | token A | 0 | 200 |
| ETH / B | ETH (address(0)) | token B | 0 | 200 |
| A / B | la dirección de token más baja | la más alta | 0 | 60 |
El hook cobra sus comisiones mediante return deltas, así que cotiza con el V4 Quoter: sus respuestas incluyen todas las comisiones e impuestos. Un cambio a través de ETH es su quoteExactInput a lo largo de la ruta token → ETH → el otro token.
Un swap cuya comisión el hook cobra antes de ejecutarlo (una compra o un cambio de entrada exacta, una venta de salida exacta) debe completarse del todo: uno que su límite de precio, o un pool de ETH sin suficiente ETH, detendría antes se revierte con PartialFill. El PoolManager envuelve el error de un hook: PartialFill y ArbitrageStarved llegan dentro de WrappedError(address target, bytes4 selector, bytes reason, bytes details), y dentro de UnexpectedRevertBytes(bytes revertData) cuando es el V4 Quoter quien los comunica. Decodifica reason para leer el error del hook.
Cada operación termina con la realineación del hook, que necesita gas propio: un swap que le deja muy poco se revierte con ArbitrageStarved en lugar de omitirla. Envía los swaps con el gas que da una estimación, nunca menos, y con margen: el primer swap tras el cierre de una ronda también liquida la ronda (su recompra y luego una realineación, unos 170.000 de gas más), algo que una estimación hecha antes del cierre no cuenta. La app añade 500.000 de gas a cada estimación.
DuelRouter ofrece una llamada para un swap en un pool:
function swapExactIn(
PoolKey calldata key,
bool zeroForOne,
uint256 amountIn,
uint256 minAmountOut,
address recipient,
uint256 deadline
) external payable returns (uint256 amountOut);
y otra para un cambio a través de ETH, en la que el bando que dejas se vende en su pool de ETH y todo el ETH obtenido se gasta en el del otro:
function swapThroughEth(
PoolKey calldata keyIn,
PoolKey calldata keyOut,
uint256 amountIn,
uint256 minAmountOut,
address recipient,
uint256 deadline
) external returns (uint256 amountOut);
Una venta en un pool de ETH de Duel que se detiene en el precio de lanzamiento del pool, agotado su ETH, se envía de nuevo por el resto en la misma llamada, una vez que la realineación del hook ha recomprado al pool lo que absorbe el pool entre los bandos: hasta SALE_PASSES() (8) swaps, mientras el precio del pool esté por debajo del límite de los swaps. Cada uno paga la comisión del hook sobre su propio ETH, y minAmountOut los cuenta todos. El V4 Quoter de Uniswap cotiza un solo swap: más allá de lo que ejecuta, responde NotEnoughLiquidity. La app cotiza una venta así ejecutando el router como el trader (un eth_call con un state override, DuelSimulator).
Lo que la realineación ya no puede recomprar, sell lo lleva a través del pool entre los bandos hasta el pool de ETH del otro bando, adonde las realineaciones movieron el ETH que 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 en keyIn como lo hace swapExactIn, después lo que queda a través de keyCross, que lo absorbe todo (su comisión de cambio se quema, como en cualquier cambio), y en keyOther, también pasada tras pasada. minAmountOut cuenta el ETH de ambos pools. Lo que keyOther tampoco puede recomprar se paga a recipient en el token de ese pool, fuera del mínimo. La app vende con sell cuando el V4 Quoter rechaza una venta y DuelSimulator.simulateSale muestra que así se vende todo por ETH.
Otro router V4 hace un solo swap. El hook no rechaza una venta de entrada exacta que supere lo que el ETH del pool puede pagar (su comisión se cobra después del swap, sobre el ETH pagado): la venta se ejecuta en parte, hasta el precio de lanzamiento del pool, y el resto se queda con el vendedor. Fija el mínimo recibido para lo que puede ejecutarse y vende el resto en una transacción posterior, contra lo que la realineación haya recomprado.
Envía ETH con swapExactIn para comprar. Para vender o cambiar, aprueba el router para el token, o deja que un permit (EIP-2612) lo apruebe en la misma transacción: swapExactInWithPermit, swapThroughEthWithPermit y sellWithPermit reciben los mismos argumentos y el permit de quien llama por exactamente amountIn, firmado para el router:
struct Permit {
uint256 deadline;
uint8 v;
bytes32 r;
bytes32 s;
}
La billetera del usuario firma los permits para cada token de Duel en un dominio EIP-712 llamado Duel, versión 1, con la dirección del token (su eip712Domain(), ERC-5267, lo indica). El router usa primero el permit e ignora si falla: uno ya enviado por cualquiera ha fijado la autorización, y uno que falla no cambia nada; el swap toma entonces lo que cubre la autorización ya vigente y se revierte si no alcanza. Un swap que se completa solo en parte deja vigente la parte no utilizada de la autorización. El plazo del permit limita hasta cuándo puede presentarse su firma, pero no hace expirar la autorización ya concedida. El router solo retira tokens de quien llama. El ETH de un cambio se queda en el PoolManager para que lo gaste el segundo pool; un pool de Duel lo gasta todo o el hook revierte el swap (PartialFill), y en cualquier otro pool lo que no puede gastar se devuelve a quien llama.
Lanzar un duelo
DuelFactory.createPair(CreateParams) lanza un duelo; envía con la llamada la comisión de lanzamiento y las compras de lanzamiento, y el exceso se reembolsa. CreateParams contiene nameA, symbolA, nameD, symbolD (el bando B), metadataURI, buyA, buyD (el ETH gastado en cada bando en el lanzamiento, sin el impuesto antisnipe), supporter, category (según la numeración de categorías de la app, 0 para ninguna) y config, el configHash() de la fábrica tal como lo has leído: el lanzamiento se revierte con ConfigChanged si los términos han cambiado desde entonces. Un nombre ocupa de 1 a 128 bytes, un ticker de 1 a 40 y la URI de los metadatos 4.096 como máximo (InvalidMetadata); una compra de lanzamiento por encima del 5 % del suministro de un bando se revierte (CreatorBuyTooLarge).
La fábrica empieza con los lanzamientos en pausa. Mientras paused() sea true, solo owner() puede lanzar un duelo; las llamadas de los demás revierten con CreationPaused. El propietario abre los lanzamientos con setPaused(false). Los duelos existentes siguen negociándose.
Eventos
| Contrato | Evento | Cuándo |
|---|---|---|
| DuelFactory | PairCreated(pairId, creator, tokenA, tokenD) | Se lanzó un duelo |
| DuelHook | PairRegistered(pairId, tokenA, tokenD, creator, category) | Sus pools se registraron en el hook, en la misma transacción; category es su tema, según la numeración de categorías de la app (0 para ninguna) |
| DuelHook | FeeTaken(pairId, fee, snipeTax) | Una operación contra ETH pagó su comisión |
| DuelHook | SwitchFeeBurned(pairId, token, amount) | Un cambio quemó su comisión |
| DuelHook | ArbitrageCaptured(pairId, profit) | La realineación llenó el bote |
| DuelHook | RoundSettled(pairId, roundIndex, averageTick, winner, ethSpent, tokensBurned) | Se cerró una ronda |
| DuelHook | CreatorFeesClaimed(pairId, to, amount) | Se pagó a un creador |
| DuelFactory | CreatorTransferStarted(pairId, creator, newCreator) | Un creador ofreció su rol (a la dirección cero: oferta retirada) |
| DuelHook | CreatorChanged(pairId, from, to) | El rol de creador de un duelo cambió de manos |
| DuelHook | CreatorFeesGivenUp(pairId, creator) | El creador destinó permanentemente su parte de las comisiones futuras a los botes |
| DuelHook | ProtocolFeesClaimed(to, amount) | Se pagó a la tesorería |
| DuelHook | ProtocolRateSet(rateBps) | La parte del protocolo en la comisión de swap de todos los duelos cambió al instante: toma rateBps de ella (10.000 para toda), el resto va a los botes |
| DuelFactory | ConfigProposed(config, eta), ConfigApplied(config), ConfigCancelled() | Términos de lanzamiento propuestos con 48 horas de antelación, aplicados, retirados |
| DuelFactory | PausedSet(paused) | Lanzamientos pausados o reanudados |
| PoolManager | Swap(id, sender, amount0, amount1, sqrtPriceX96, liquidity, tick, fee) | Cada operación, incluida la propia realineación del hook (su sender es el hook) |
Las listas de operaciones leen los swaps de los tres pools; los gráficos de precio y de reparto solo leen los dos pools de ETH. Las listas de holders usan los eventos Transfer de los tokens. El subgraph indexa swaps, saldos, holders y rondas liquidadas para que la app pueda cargar el historial sin reproducir la cadena desde el lanzamiento.
Liquidar y reclamar
settle(pairId)liquida una ronda terminada. Cualquiera puede llamarla.claimCreatorFees(pairId)solo paga al creador del duelo, la billetera que lo lanzó o la dirección a la que cedió su rol;claimProtocolFees()paga a la tesorería configurada en el momento de reclamar. Cualquiera puede activar cualquiera de las dos. La tesorería puede cambiar tras el plazo de 48 horas de la fábrica; quien llama no puede elegir otro destinatario.transferCreator(pairId, newCreator)en la fábrica ofrece el rol de creador de un duelo, queacceptCreator(pairId)desdenewCreatoracepta;pendingCreator(pairId)lee la oferta. Solo el creador lo ofrece, y solo la dirección a la que se ofrece lo acepta.giveUpCreatorFees(pairId)en el hook envía permanentemente la parte del creador de las comisiones futuras a los botes de las rondas. Solo el creador actual puede llamarlo. Las comisiones ya acumuladas siguen siendo reclamables por el creador, y el rol de creador se puede seguir transfiriendo.