• Duel jest otwarty na Robinhood Chain
  • Uruchom pojedynek dwa tokeny, trzy pule, jedna transakcja
  • Runda zamyka się za --:--:--
  • Czterogodzinne rundy odkup zwycięzcy ma limit ceny
  • Płynność zablokowana na zawsze, bez tokenów zespołu

Budowanie na Duel

Pule Duel to standardowe pule Uniswap V4 z hookiem, a każda liczba, którą pokazuje aplikacja, pochodzi z publicznych widoków i zdarzeń. Boty, terminale, dashboardy i agregatory mogą na tym budować bez pytania o zgodę.

Odczyt pojedynku

DuelLens zbiera wszystko o pojedynku w jednym wywołaniu:

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

PairView zawiera ID pojedynku, jego twórcę i URI metadanych, obie strony (token, nazwę, symbol, podaż, cenę i tick puli, ETH za token, prawdziwy ETH w puli), trzy klucze pul, bieżący podział (gaugeBps, udział strony A w punktach bazowych), dotychczasową średnią rundy (roundGaugeBps), pulę nagród, opłaty twórcy, łączny wolumen i odkupy, bieżący podatek antysnajperski, numer, początek i koniec rundy oraz to, czy w niej handlowano (roundTraded), czas i tick startu (launchTime, startTick), to, czy jest pojedynkiem wspierającym, jego kategorię (category, według numeracji aplikacji, 0 oznacza brak), jego warunki opłat (fees) i to, jaką część swojego udziału protokół teraz bierze (protocolRateBps, na 10 000): protokół dostaje fees.protocolShareBps × protocolRateBps / 10 000 opłaty za swap, a pula nagród resztę tego udziału. W kontraktach i ich ABI strona B to D: d, tokenD, keyD.

Hook ma mniejsze widoki: pairCount(), getPair(pairId), poolKeys(pairId), gaugeBps(pairId), snipeTaxBps(pairId), protocolRateBps(), protocolFees() i firstRoundEnd(launchTime).

Handel

Dowolny router Uniswap V4 może handlować w pulach pojedynku. Zbuduj klucze pul z poolKeys(pairId):

Pulacurrency0currency1feetickSpacing
ETH / AETH (address(0))token A0200
ETH / BETH (address(0))token B0200
A / Bniższy adres tokenawyższy060

Hook pobiera opłaty przez return deltas, więc wyceniaj przez V4 Quoter: jego odpowiedzi uwzględniają wszystkie opłaty i podatki. Zmiana strony przez ETH to jego quoteExactInput na ścieżce token → ETH → drugi token.

Swap, którego opłatę hook pobiera przed wykonaniem (kupno lub zmiana strony z dokładnym wejściem, sprzedaż z dokładnym wyjściem), musi wypełnić się w całości: swap, który jego limit ceny albo pula ETH z niedoborem ETH zatrzymałyby wcześniej, jest cofany z PartialFill. PoolManager opakowuje błąd hooka: PartialFill i ArbitrageStarved przychodzą wewnątrz WrappedError(address target, bytes4 selector, bytes reason, bytes details), a wewnątrz UnexpectedRevertBytes(bytes revertData), gdy zgłasza je V4 Quoter. Zdekoduj reason, aby odczytać błąd hooka.

Każda transakcja kończy się wyrównaniem przez hook, które potrzebuje własnego gazu: swap, który zostawia mu go za mało, jest cofany z ArbitrageStarved zamiast je pominąć. Wysyłaj swapy z gazem z oszacowania, nigdy mniejszym, i z zapasem: pierwszy swap po zamknięciu rundy rozlicza też rundę (jej odkup, potem wyrównanie, około 170 000 gazu więcej), czego oszacowanie zrobione przed zamknięciem nie uwzględnia. Aplikacja dodaje 500 000 gazu do każdego oszacowania.

DuelRouter oferuje jedno wywołanie dla swapu w jednej puli:

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

i jedno dla zmiany strony przez ETH, w której opuszczana strona jest sprzedawana w swojej puli ETH, a cały uzyskany ETH wydawany w puli drugiej:

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

Sprzedaż do puli ETH Duel, która zatrzymuje się na cenie startowej puli po wyczerpaniu jej ETH, jest wysyłana ponownie po resztę w tym samym wywołaniu, gdy wyrównanie hooka odkupi od puli to, co przyjmie pula między stronami: do SALE_PASSES() (8) swapów, dopóki cena puli jest poniżej limitu swapów. Każdy płaci opłatę hooka od własnego ETH, a minAmountOut liczy je wszystkie. V4 Quoter Uniswapa wycenia jeden swap: ponad to, co on wykona, odpowiada NotEnoughLiquidity. Aplikacja wycenia taką sprzedaż, wykonując router jako trader (eth_call z nadpisaniem stanu, DuelSimulator).

To, czego wyrównanie nie może już odkupić, sell kieruje dalej przez pulę między stronami do puli ETH drugiej strony, do której wyrównania przeniosły ETH, który za to płaci:

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

sell sprzedaje do puli keyIn tak jak swapExactIn, potem to, co zostało, przez pulę keyCross, która przyjmuje wszystko (jej opłata za zmianę strony jest spalana, jak przy każdej zmianie strony), i do puli keyOther, również w kolejnych przebiegach. minAmountOut liczy ETH z obu pul. To, czego pula keyOther też nie może odkupić, trafia do recipient w jej tokenie i nie wlicza się do minimalnego wyniku. Aplikacja sprzedaje przez sell, gdy V4 Quoter odrzuca sprzedaż, a DuelSimulator.simulateSale pokazuje, że sell sprzeda wszystko za ETH.

Inny router V4 wykonuje jeden swap. Hook nie odrzuca sprzedaży z dokładnym wejściem wykraczającej poza to, za co płaci ETH puli (jego opłata jest pobierana po swapie, od wypłaconego ETH): sprzedaż wykonuje się częściowo, aż do ceny startowej puli, a reszta zostaje u sprzedającego. Ustaw minimalny wynik na to, co może się wykonać, a resztę sprzedaj w późniejszej transakcji, do tego, co odkupiło wyrównanie.

Aby kupić, wyślij ETH z swapExactIn. Aby sprzedać lub zmienić stronę, zatwierdź router dla tokena albo pozwól, by permit (EIP-2612) zatwierdził go w tej samej transakcji: swapExactInWithPermit, swapThroughEthWithPermit i sellWithPermit przyjmują te same argumenty oraz permit wywołującego na dokładnie amountIn, podpisany dla routera:

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

Dla każdego tokena Duel właściciel portfela podpisuje permity w domenie EIP-712 o nazwie Duel, wersji 1, powiązanej z adresem tego tokena (mówi o tym jego eip712Domain(), ERC-5267). Router najpierw używa permitu i ignoruje jego niepowodzenie: permit wysłany już przez kogokolwiek ustawił zgodę, a nieudany niczego nie zmienia; swap bierze wtedy to, co pokrywa już istniejąca zgoda, i jest cofany, jeśli to za mało. Po częściowym wypełnieniu swapa niewykorzystana część zgody pozostaje aktywna: termin permitu ogranicza, kiedy można użyć podpisu, ale nie wygasza już udzielonej zgody. Router pobiera tokeny tylko od wywołującego. ETH ze zmiany strony zostaje w PoolManagerze, aby wydała go druga pula; pula Duel wydaje go w całości albo hook cofa swap (PartialFill), a w każdej innej puli to, czego nie może ona wydać, wraca do wywołującego.

Uruchomienie pojedynku

DuelFactory.createPair(CreateParams) uruchamia pojedynek; wyślij razem z wywołaniem opłatę za start i zakupy przy starcie, nadwyżka jest zwracana. CreateParams to nameA, symbolA, nameD, symbolD (strona B), metadataURI, buyA, buyD (ETH wydawany na każdą stronę przy starcie, bez podatku antysnajperskiego), supporter, category (według numeracji kategorii w aplikacji, 0 oznacza brak) i config, czyli configHash() fabryki w odczytanej przez ciebie postaci: start jest cofany z ConfigChanged, jeśli warunki zmieniły się od tego czasu. Nazwa ma od 1 do 128 bajtów, ticker od 1 do 40, a URI metadanych najwyżej 4096 (InvalidMetadata); zakup przy starcie powyżej 5% podaży strony jest cofany (CreatorBuyTooLarge).

Fabryka rozpoczyna działanie ze wstrzymanym uruchamianiem pojedynków. Dopóki paused() ma wartość true, tylko owner() może uruchomić pojedynek; wywołania innych osób są wycofywane z błędem CreationPaused. Właściciel otwiera uruchamianie przez setPaused(false). Handel istniejącymi pojedynkami trwa nadal.

Zdarzenia

KontraktZdarzenieKiedy
DuelFactoryPairCreated(pairId, creator, tokenA, tokenD)Uruchomiono pojedynek
DuelHookPairRegistered(pairId, tokenA, tokenD, creator, category)Jego pule zarejestrowano w hooku, w tej samej transakcji; category to jego temat, według numeracji kategorii w aplikacji (0 oznacza brak)
DuelHookFeeTaken(pairId, fee, snipeTax)Transakcja z ETH zapłaciła opłatę
DuelHookSwitchFeeBurned(pairId, token, amount)Zmiana strony spaliła swoją opłatę
DuelHookArbitrageCaptured(pairId, profit)Wyrównanie zasiliło pulę nagród
DuelHookRoundSettled(pairId, roundIndex, averageTick, winner, ethSpent, tokensBurned)Runda się zamknęła
DuelHookCreatorFeesClaimed(pairId, to, amount)Twórca otrzymał wypłatę
DuelFactoryCreatorTransferStarted(pairId, creator, newCreator)Twórca zaproponował swoją rolę (adresowi zerowemu: propozycja wycofana)
DuelHookCreatorChanged(pairId, from, to)Rola twórcy pojedynku przeszła w inne ręce
DuelHookCreatorFeesGivenUp(pairId, creator)Twórca na stałe przekierował swój udział w przyszłych opłatach do pul
DuelHookProtocolFeesClaimed(to, amount)Skarbiec otrzymał wypłatę
DuelHookProtocolRateSet(rateBps)Udział protokołu w opłacie za swap wszystkich pojedynków zmienił się od razu: bierze z niego rateBps (10 000 to całość), reszta trafia do pul nagród
DuelFactoryConfigProposed(config, eta), ConfigApplied(config), ConfigCancelled()Warunki startu zaproponowane z 48-godzinnym wyprzedzeniem, zastosowane, wycofane
DuelFactoryPausedSet(paused)Starty wstrzymane lub wznowione
PoolManagerSwap(id, sender, amount0, amount1, sqrtPriceX96, liquidity, tick, fee)Każda transakcja, łącznie z wyrównaniem samego hooka (jego sender to hook)

Listy transakcji odczytują swapy ze wszystkich trzech pul; wykresy ceny i podziału odczytują tylko dwie pule ETH. Listy posiadaczy korzystają ze zdarzeń Transfer tokenów. Subgraph indeksuje swapy, salda, posiadaczy i rozliczone rundy, aby aplikacja mogła wczytać historię bez odtwarzania sieci od startu.

Rozliczanie i odbiór

  • settle(pairId) rozlicza zakończoną rundę. Może ją wywołać każdy.
  • claimCreatorFees(pairId) płaci tylko twórcy pojedynku: portfelowi, który go uruchomił, lub adresowi, któremu przekazał rolę; claimProtocolFees() płaci skarbcowi ustawionemu w chwili odbioru. Każdy może wywołać każdy z tych odbiorów. Skarbiec może się zmienić po 48-godzinnym terminie fabryki; wywołujący nie może wybrać innego odbiorcy.
  • transferCreator(pairId, newCreator) w fabryce proponuje rolę twórcy pojedynku, którą przejmuje acceptCreator(pairId) wywołane przez newCreator; pendingCreator(pairId) odczytuje propozycję. Proponuje ją tylko twórca, a przejmuje tylko adres, któremu ją zaproponowano.
  • giveUpCreatorFees(pairId) na hooku na stałe przekazuje udział twórcy w przyszłych opłatach do pul rund. Funkcję może wywołać tylko obecny twórca. Zgromadzone wcześniej opłaty nadal może odebrać twórca, a jego rolę nadal można przekazać.