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):
| 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 | o endereço de token menor | o maior | 0 | 60 |
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
| Contrato | Evento | Quando |
|---|---|---|
| DuelFactory | PairCreated(pairId, creator, tokenA, tokenD) | Um duelo foi lançado |
| DuelHook | PairRegistered(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) |
| DuelHook | FeeTaken(pairId, fee, snipeTax) | Uma negociação contra ETH pagou a taxa |
| DuelHook | SwitchFeeBurned(pairId, token, amount) | Uma troca queimou a taxa |
| DuelHook | ArbitrageCaptured(pairId, profit) | O realinhamento encheu o pote |
| DuelHook | RoundSettled(pairId, roundIndex, averageTick, winner, ethSpent, tokensBurned) | Uma rodada foi fechada |
| DuelHook | CreatorFeesClaimed(pairId, to, amount) | Um criador foi pago |
| DuelFactory | CreatorTransferStarted(pairId, creator, newCreator) | Um criador ofereceu o papel (para o endereço zero: oferta retirada) |
| DuelHook | CreatorChanged(pairId, from, to) | O papel de criador de um duelo mudou de mãos |
| DuelHook | CreatorFeesGivenUp(pairId, creator) | O criador destinou permanentemente sua parte das taxas futuras aos potes |
| DuelHook | ProtocolFeesClaimed(to, amount) | A tesouraria foi paga |
| DuelHook | ProtocolRateSet(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 |
| DuelFactory | ConfigProposed(config, eta), ConfigApplied(config), ConfigCancelled() | Termos de lançamento propostos com 48 horas de antecedência, aplicados, retirados |
| DuelFactory | PausedSet(paused) | Lançamentos pausados ou retomados |
| PoolManager | Swap(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, queacceptCreator(pairId)chamado pornewCreatoraceita;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.