• O Duel está aberto na Robinhood Chain
  • Lance um duelo dois tokens, três pools, uma transação
  • A rodada fecha em --:--:--
  • Rodadas de quatro horas a recompra do vencedor tem limite de preço
  • Liquidez travada para sempre, sem tokens da equipe

Construir no Duel

Os pools do Duel são pools padrão da Uniswap V4 com um hook, e todo número que o app mostra vem de visões e eventos públicos. Bots, terminais, dashboards e agregadores podem construir em cima dele sem pedir permissão.

Ler um duelo

DuelLens reúne tudo sobre um duelo em uma única chamada:

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

Um PairView contém o id do duelo, o criador e a URI dos metadados dele, os dois lados (token, nome, símbolo, supply, preço e tick do pool, ETH por token, ETH real no pool), as três chaves de pool, a divisão ao vivo (gaugeBps, a parte do lado A em pontos-base), a média da rodada até agora (roundGaugeBps), o pote, as taxas do criador, o volume e as recompras acumulados, o imposto antissnipe atual, o índice, o início e o fim da rodada e se ela teve negociações (roundTraded), o horário e o tick de lançamento (launchTime, startTick), se é um duelo solidário, a categoria dele (category, conforme a numeração do app, 0 para nenhuma), os termos de taxa dele (fees) e a fração da própria parte com que o protocolo fica agora (protocolRateBps, sobre 10.000): o protocolo recebe fees.protocolShareBps × protocolRateBps / 10.000 da taxa de swap, e o pote o resto dessa parte. Nos contratos e nas ABIs deles, o lado B é D: d, tokenD, keyD.

O hook tem visões menores: pairCount(), getPair(pairId), poolKeys(pairId), gaugeBps(pairId), snipeTaxBps(pairId), protocolRateBps(), protocolFees() e firstRoundEnd(launchTime).

Negociar

Qualquer router da Uniswap V4 pode negociar nos pools de um duelo. Monte as chaves de pool a partir de poolKeys(pairId):

Poolcurrency0currency1feetickSpacing
ETH / AETH (address(0))token A0200
ETH / BETH (address(0))token B0200
A / Bo endereço de token menoro maior060

O hook cobra as taxas por meio de return deltas, então cote com o V4 Quoter: as respostas dele incluem todas as taxas e impostos. Uma troca via ETH é o quoteExactInput dele ao longo do caminho token → ETH → o outro token.

Um swap cuja taxa o hook cobra antes de rodar (uma compra ou troca de entrada exata, uma venda de saída exata) precisa se completar por inteiro: um swap que o limite de preço dele, ou um pool de ETH sem ETH suficiente, interromperia antes reverte com PartialFill. O PoolManager encapsula o erro de um hook: PartialFill e ArbitrageStarved chegam dentro de WrappedError(address target, bytes4 selector, bytes reason, bytes details), e dentro de UnexpectedRevertBytes(bytes revertData) quando é o V4 Quoter que os relata. Decodifique reason para ler o erro do hook.

Cada operação termina com o realinhamento do hook, que precisa de gás próprio: um swap que deixa pouco gás para ele reverte com ArbitrageStarved em vez de pulá-lo. Envie os swaps com o gás que uma estimativa dá, nunca menos, e com folga: o primeiro swap depois do fechamento de uma rodada também liquida a rodada (a recompra dela e depois um realinhamento, cerca de 170.000 de gás a mais), o que uma estimativa feita antes do fechamento não conta. O app soma 500.000 de gás a cada estimativa.

DuelRouter oferece uma chamada para um swap em um pool:

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

e outra para uma troca via ETH, em que o lado que você deixa é vendido no pool de ETH dele e todo o ETH obtido é gasto no do outro:

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

Uma venda em um pool de ETH do Duel que para no preço de lançamento do pool, com seu ETH esgotado, é enviada de novo pelo resto na mesma chamada, depois que o realinhamento do hook recompra do pool o que o pool entre os lados absorve: até SALE_PASSES() (8) swaps, enquanto o preço do pool estiver abaixo do limite dos swaps. Cada um paga a taxa do hook sobre o próprio ETH, e minAmountOut conta todos eles. O V4 Quoter da Uniswap cota um único swap: além do que ele executa, responde NotEnoughLiquidity. O app cota uma venda assim executando o router como o trader (um eth_call com state override, DuelSimulator).

O que o realinhamento já não consegue recomprar, sell leva pelo pool entre os lados até o pool de ETH do outro lado, para onde os realinhamentos moveram o ETH que paga por isso:

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

Vende em keyIn como faz swapExactIn, depois o que sobra por keyCross, que absorve tudo (a taxa de troca dele é queimada, como em qualquer troca), e em keyOther, também passagem após passagem. minAmountOut conta o ETH dos dois pools. O que keyOther também não consegue recomprar é pago a recipient no token desse pool, fora do mínimo. O app vende com sell quando o V4 Quoter recusa uma venda e DuelSimulator.simulateSale mostra que assim tudo é vendido por ETH.

Outro router V4 faz um único swap. O hook não recusa uma venda de entrada exata que passe do que o ETH do pool consegue pagar (a taxa dele é cobrada depois do swap, sobre o ETH pago): a venda é executada em parte, até o preço de lançamento do pool, e o resto fica com o vendedor. Defina o mínimo recebido para o que pode ser executado e venda o resto em uma transação posterior, contra o que o realinhamento tiver recomprado.

Envie ETH com swapExactIn para comprar. Para vender ou trocar, aprove o router para o token, ou deixe um permit (EIP-2612) aprová-lo na mesma transação: swapExactInWithPermit, swapThroughEthWithPermit e sellWithPermit recebem os mesmos argumentos e o permit de quem chama para exatamente amountIn, assinado para o router:

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

A carteira do usuário assina os permits de cada token do Duel em um domínio EIP-712 chamado Duel, versão 1, com o endereço do token (o eip712Domain() dele, ERC-5267, indica isso). O router usa o permit primeiro e ignora a falha dele: um permit já enviado por qualquer pessoa já definiu a autorização, e um que falha não muda nada; o swap então usa o que a autorização já existente cobre e reverte se não for suficiente. Um swap que se completa só em parte mantém válida a parte não utilizada da autorização. O prazo do permit limita até quando a assinatura pode ser apresentada, mas não faz expirar a autorização já concedida. O router só puxa tokens de quem chama. O ETH de uma troca fica no PoolManager para o segundo pool gastar; um pool do Duel gasta tudo ou o hook reverte o swap (PartialFill), e em qualquer outro pool o que ele não consegue gastar é devolvido a quem chama.

Lançar um duelo

DuelFactory.createPair(CreateParams) lança um duelo; envie com a chamada a taxa de lançamento e as compras de lançamento, e o excedente é reembolsado. CreateParams contém nameA, symbolA, nameD, symbolD (o lado B), metadataURI, buyA, buyD (o ETH gasto em cada lado no lançamento, sem o imposto antissnipe), supporter, category (conforme a numeração de categorias do app, 0 para nenhuma) e config, o configHash() da fábrica como você o leu: o lançamento reverte com ConfigChanged se os termos mudaram desde então. Um nome ocupa de 1 a 128 bytes, um ticker de 1 a 40 e a URI dos metadados no máximo 4.096 (InvalidMetadata); uma compra de lançamento acima de 5% do supply de um lado reverte (CreatorBuyTooLarge).

A fábrica começa com os lançamentos pausados. Enquanto paused() for true, só owner() pode lançar um duelo; as chamadas dos demais revertem com CreationPaused. O dono abre os lançamentos com setPaused(false). Os duelos existentes continuam sendo negociados.

Eventos

ContratoEventoQuando
DuelFactoryPairCreated(pairId, creator, tokenA, tokenD)Um duelo foi lançado
DuelHookPairRegistered(pairId, tokenA, tokenD, creator, category)Os pools dele foram registrados no hook, na mesma transação; category é o tema dele, conforme a numeração de categorias do app (0 para nenhuma)
DuelHookFeeTaken(pairId, fee, snipeTax)Uma negociação contra ETH pagou a taxa
DuelHookSwitchFeeBurned(pairId, token, amount)Uma troca queimou a taxa
DuelHookArbitrageCaptured(pairId, profit)O realinhamento encheu o pote
DuelHookRoundSettled(pairId, roundIndex, averageTick, winner, ethSpent, tokensBurned)Uma rodada foi fechada
DuelHookCreatorFeesClaimed(pairId, to, amount)Um criador foi pago
DuelFactoryCreatorTransferStarted(pairId, creator, newCreator)Um criador ofereceu o papel (para o endereço zero: oferta retirada)
DuelHookCreatorChanged(pairId, from, to)O papel de criador de um duelo mudou de mãos
DuelHookCreatorFeesGivenUp(pairId, creator)O criador destinou permanentemente sua parte das taxas futuras aos potes
DuelHookProtocolFeesClaimed(to, amount)A tesouraria foi paga
DuelHookProtocolRateSet(rateBps)A parte do protocolo na taxa de swap de todos os duelos mudou na hora: ele fica com rateBps dela (10.000 para toda), o resto vai para os potes
DuelFactoryConfigProposed(config, eta), ConfigApplied(config), ConfigCancelled()Termos de lançamento propostos com 48 horas de antecedência, aplicados, retirados
DuelFactoryPausedSet(paused)Lançamentos pausados ou retomados
PoolManagerSwap(id, sender, amount0, amount1, sqrtPriceX96, liquidity, tick, fee)Cada negociação, incluindo o próprio realinhamento do hook (o sender dele é o hook)

As listas de negociações leem os swaps dos três pools; os gráficos de preço e de divisão leem só os dois pools de ETH. As listas de holders usam os eventos Transfer dos tokens. O subgraph indexa swaps, saldos, holders e rodadas liquidadas para que o app possa carregar o histórico sem reprocessar a rede desde o lançamento.

Liquidar e resgatar

  • settle(pairId) liquida uma rodada encerrada. Qualquer pessoa pode chamá-la.
  • claimCreatorFees(pairId) paga só o criador do duelo, a carteira que o lançou ou o endereço para o qual ela passou o papel; claimProtocolFees() paga a tesouraria configurada no momento do resgate. Qualquer pessoa pode acionar qualquer um dos dois resgates. A tesouraria pode mudar depois do prazo de 48 horas da fábrica; quem chama não pode escolher outro destinatário.
  • transferCreator(pairId, newCreator) na fábrica oferece o papel de criador de um duelo, que acceptCreator(pairId) chamado por newCreator aceita; pendingCreator(pairId) lê a oferta. Só o criador oferece, e só o endereço a quem foi oferecido aceita.
  • giveUpCreatorFees(pairId) no hook envia permanentemente a parte do criador nas taxas futuras para os potes. Só o criador atual pode chamá-lo. As taxas já acumuladas continuam disponíveis para o criador resgatar, e o papel de criador ainda pode ser passado adiante.