• Duel ist offen auf Robinhood Chain
  • Starte ein Duell zwei Token, drei Pools, eine Transaktion
  • Runde schließt in --:--:--
  • Runden von vier Stunden der Rückkauf des Gewinners hat ein Preislimit
  • Liquidität gesperrt für immer, keine Team-Token

Auf Duel bauen

Die Pools von Duel sind Standard-Uniswap-V4-Pools mit einem Hook, und jede Zahl, die die App zeigt, stammt aus öffentlichen Views und Events. Bots, Terminals, Dashboards und Aggregatoren können darauf aufbauen, ohne zu fragen.

Ein Duell lesen

DuelLens sammelt alles über ein Duell in einem Aufruf:

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

Ein PairView enthält die ID des Duells, seinen Creator und seine Metadaten-URI, beide Seiten (Token, Name, Symbol, Angebot, Pool-Preis und Tick, ETH pro Token, echtes ETH im Pool), die drei Pool-Keys, die Live-Verteilung (gaugeBps, der Anteil von Seite A in Basispunkten), den bisherigen Durchschnitt der Runde (roundGaugeBps), den Pot, die Creator-Gebühren, das Gesamtvolumen und die Rückkäufe, die aktuelle Anti-Snipe-Steuer, Index, Beginn und Ende der Runde und ob in ihr gehandelt wurde (roundTraded), Zeitpunkt und Tick des Launchs (launchTime, startTick), ob es ein Unterstützer-Duell ist, seine Kategorie (category, nach der Nummerierung der App, 0 für keine), seine Gebührenkonditionen (fees) und den Teil seines Anteils, den das Protokoll derzeit nimmt (protocolRateBps, von 10.000): Das Protokoll erhält fees.protocolShareBps × protocolRateBps / 10.000 der Swap-Gebühr, der Pot den Rest dieses Anteils. In den Verträgen und ihren ABIs heißt Seite B D: d, tokenD, keyD.

Der Hook hat kleinere Views: pairCount(), getPair(pairId), poolKeys(pairId), gaugeBps(pairId), snipeTaxBps(pairId), protocolRateBps(), protocolFees() und firstRoundEnd(launchTime).

Handeln

Jeder Uniswap-V4-Router kann in den Pools eines Duells handeln. Baue die Pool-Keys aus poolKeys(pairId):

Poolcurrency0currency1feetickSpacing
ETH / AETH (address(0))Token A0200
ETH / BETH (address(0))Token B0200
A / Bdie niedrigere Token-Adressedie höhere060

Der Hook erhebt seine Gebühren über Return-Deltas, also hol Angebote über den V4 Quoter ein: Seine Antworten enthalten alle Gebühren und Steuern. Ein Wechsel über ETH ist sein quoteExactInput entlang des Pfads Token → ETH → anderer Token.

Ein Swap, dessen Gebühr der Hook vor der Ausführung erhebt (ein Kauf oder Wechsel mit exaktem Input, ein Verkauf mit exaktem Output), muss vollständig ausgeführt werden: Einer, den sein Preislimit oder ein ETH-Pool mit zu wenig ETH vorzeitig stoppen würde, wird mit PartialFill rückgängig gemacht. Der PoolManager verpackt den Fehler eines Hooks: PartialFill und ArbitrageStarved kommen in WrappedError(address target, bytes4 selector, bytes reason, bytes details) an, und in UnexpectedRevertBytes(bytes revertData), wenn der V4 Quoter sie meldet. Dekodiere reason, um den Fehler des Hooks zu lesen.

Jeder Trade endet mit der Neuausrichtung des Hooks, die eigenes Gas braucht: Ein Swap, der ihr zu wenig lässt, wird mit ArbitrageStarved rückgängig gemacht, statt sie zu überspringen. Sende Swaps mit dem Gas einer Schätzung, nie mit weniger, und mit Reserve: Der erste Swap nach dem Schluss einer Runde rechnet auch die Runde ab (ihr Rückkauf, dann eine Neuausrichtung, rund 170.000 Gas mehr), was eine vor dem Schluss gemachte Schätzung nicht mitzählt. Die App schlägt auf jede Schätzung 500.000 Gas auf.

DuelRouter bietet einen Aufruf für einen Swap in einem Pool:

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

und einen für einen Wechsel über ETH, bei dem die verlassene Seite in ihrem ETH-Pool verkauft und das ganze erhaltene ETH im Pool der anderen ausgegeben wird:

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

Ein Verkauf in einen ETH-Pool von Duel, der beim Launch-Preis des Pools stoppt, weil sein ETH aufgebraucht ist, wird im selben Aufruf für den Rest erneut gesendet, sobald die Neuausrichtung des Hooks vom Pool zurückgekauft hat, was der Pool zwischen den Seiten aufnimmt: bis zu SALE_PASSES() (8) Swaps, solange der Preis des Pools unter dem Limit der Swaps liegt. Jeder zahlt die Gebühr des Hooks auf sein eigenes ETH, und minAmountOut zählt sie alle. Der V4 Quoter von Uniswap quotiert einen einzigen Swap: Über das hinaus, was dieser ausführt, antwortet er mit NotEnoughLiquidity. Die App quotiert einen solchen Verkauf, indem sie den Router als der Trader ausführt (ein eth_call mit State Override, DuelSimulator).

Was die Neuausrichtung nicht mehr zurückkaufen kann, führt sell über den Pool zwischen den Seiten weiter in den ETH-Pool der anderen Seite, in den die Neuausrichtungen das ETH verschoben haben, das dafür bezahlt:

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

sell verkauft in keyIn wie swapExactIn, dann den Rest über keyCross, der alles aufnimmt (seine Wechselgebühr wird verbrannt, wie bei jedem Wechsel), und in keyOther, ebenfalls Durchgang für Durchgang. minAmountOut zählt das ETH beider Pools. Was auch keyOther nicht zurückkaufen kann, erhält recipient in dessen Token, ohne dass es zum Mindestertrag zählt. Die App verkauft über sell, wenn der V4 Quoter einen Verkauf ablehnt und DuelSimulator.simulateSale zeigt, dass sell alles gegen ETH verkauft.

Ein anderer V4-Router macht einen einzigen Swap. Der Hook lehnt einen Verkauf mit exaktem Input nicht ab, der über das hinausgeht, was das ETH des Pools bezahlt (seine Gebühr wird nach dem Swap erhoben, auf das ausgezahlte ETH): Der Verkauf wird teilweise ausgeführt, bis hinunter zum Launch-Preis des Pools, und der Rest bleibt beim Verkäufer. Setze den Mindestertrag auf das, was ausgeführt werden kann, und verkaufe den Rest in einer späteren Transaktion, in das hinein, was die Neuausrichtung zurückgekauft hat.

Sende ETH mit swapExactIn, um zu kaufen. Zum Verkaufen oder Wechseln genehmige den Router für den Token oder lass einen Permit (EIP-2612) ihn in derselben Transaktion genehmigen: swapExactInWithPermit, swapThroughEthWithPermit und sellWithPermit nehmen dieselben Argumente und den Permit des Aufrufers über genau amountIn, für den Router signiert:

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

Die Wallet des Nutzers signiert die Permits eines Duel-Tokens in einer EIP-712-Domain namens Duel, Version 1, auf der Adresse des Tokens (dessen eip712Domain(), ERC-5267, sagt das). Der Router nutzt zuerst den Permit und ignoriert dessen Fehlschlag: Ein bereits von irgendwem gesendeter Permit hat die Allowance gesetzt, und ein fehlschlagender ändert nichts; der Swap nimmt dann, was die bestehende Allowance abdeckt, und wird rückgängig gemacht, wenn sie nicht reicht. Ein nur teilweise ausgeführter Swap lässt den ungenutzten Teil der durch den Permit erteilten Allowance bestehen. Die Deadline begrenzt die Gültigkeit der Permit-Signatur; eine bereits erteilte Allowance läuft dadurch nicht ab. Der Router zieht Token nur vom Aufrufer ab. Das ETH eines Wechsels bleibt im PoolManager, damit der zweite Pool es ausgibt; ein Duel-Pool gibt es ganz aus, oder der Hook macht den Swap rückgängig (PartialFill), und bei jedem anderen Pool wird dem Aufrufer zurückgezahlt, was er nicht ausgeben kann.

Ein Duell starten

DuelFactory.createPair(CreateParams) startet ein Duell; sende die Launch-Gebühr und die Launch-Käufe mit, ein Überschuss wird erstattet. CreateParams besteht aus nameA, symbolA, nameD, symbolD (Seite B), metadataURI, buyA, buyD (das ETH, das beim Launch auf jeder Seite ausgegeben wird, ohne Anti-Snipe-Steuer), supporter, category (nach der Nummerierung der Kategorien in der App, 0 für keine) und config, dem configHash() der Factory, wie du ihn gelesen hast: Der Launch wird mit ConfigChanged rückgängig gemacht, wenn sich die Konditionen seitdem geändert haben. Ein Name hat 1 bis 128 Bytes, ein Ticker 1 bis 40 und die Metadaten-URI höchstens 4096 (InvalidMetadata); ein Launch-Kauf über 5 % des Angebots einer Seite wird rückgängig gemacht (CreatorBuyTooLarge).

Die Factory startet mit pausierten Launches. Solange paused() true ist, kann nur owner() ein Duell starten; Aufrufe anderer werden mit CreationPaused rückgängig gemacht. Der Owner öffnet die Launches mit setPaused(false). Bestehende Duelle werden weiter gehandelt.

Events

VertragEventWann
DuelFactoryPairCreated(pairId, creator, tokenA, tokenD)Ein Duell wurde gestartet
DuelHookPairRegistered(pairId, tokenA, tokenD, creator, category)Seine Pools wurden beim Hook registriert, in derselben Transaktion; category ist sein Thema, nach der Nummerierung der Kategorien in der App (0 für keine)
DuelHookFeeTaken(pairId, fee, snipeTax)Ein Trade gegen ETH hat seine Gebühr bezahlt
DuelHookSwitchFeeBurned(pairId, token, amount)Ein Wechsel hat seine Gebühr verbrannt
DuelHookArbitrageCaptured(pairId, profit)Die Neuausrichtung hat den Pot gefüllt
DuelHookRoundSettled(pairId, roundIndex, averageTick, winner, ethSpent, tokensBurned)Eine Runde wurde geschlossen
DuelHookCreatorFeesClaimed(pairId, to, amount)Ein Creator wurde bezahlt
DuelFactoryCreatorTransferStarted(pairId, creator, newCreator)Ein Creator hat seine Rolle angeboten (an die Nulladresse: Angebot zurückgezogen)
DuelHookCreatorChanged(pairId, from, to)Die Creator-Rolle eines Duells hat den Besitzer gewechselt
DuelHookCreatorFeesGivenUp(pairId, creator)Der Creator hat seinen Anteil an künftigen Gebühren dauerhaft in die Pots umgeleitet
DuelHookProtocolFeesClaimed(to, amount)Die Treasury wurde bezahlt
DuelHookProtocolRateSet(rateBps)Der Anteil des Protokolls an der Swap-Gebühr aller Duelle hat sich sofort geändert: Es nimmt rateBps davon (10.000 für alles), der Rest geht in die Pots
DuelFactoryConfigProposed(config, eta), ConfigApplied(config), ConfigCancelled()Launch-Konditionen für 48 Stunden später vorgeschlagen, angewendet, zurückgezogen
DuelFactoryPausedSet(paused)Launches pausiert oder fortgesetzt
PoolManagerSwap(id, sender, amount0, amount1, sqrtPriceX96, liquidity, tick, fee)Jeder Trade, einschließlich der eigenen Neuausrichtung des Hooks (sein sender ist der Hook)

Trade-Listen lesen Swaps aus allen drei Pools; die Preis- und Verteilungscharts lesen nur die beiden ETH-Pools. Holder-Listen nutzen die Transfer-Events der Token. Der Subgraph indexiert Swaps, Salden, Holder und abgerechnete Runden, damit die App die Historie laden kann, ohne die Chain seit dem Launch erneut abzuspielen.

Abrechnen und Abholen

  • settle(pairId) rechnet eine beendete Runde ab. Jeder kann sie aufrufen.
  • claimCreatorFees(pairId) zahlt nur an den Creator des Duells, die Wallet, die es gestartet hat, oder die Adresse, an die sie ihre Rolle übergeben hat; claimProtocolFees() zahlt an die Treasury, die beim Abholen eingestellt ist. Jeder kann beide Abholungen auslösen. Die Treasury kann sich nach der 48-stündigen Wartezeit der Factory ändern; ein Aufrufer kann keinen anderen Empfänger wählen.
  • transferCreator(pairId, newCreator) auf der Factory bietet die Creator-Rolle eines Duells an, die acceptCreator(pairId) von newCreator übernimmt; pendingCreator(pairId) liest das Angebot. Nur der Creator bietet sie an, und nur die angebotene Adresse übernimmt sie.
  • giveUpCreatorFees(pairId) am Hook leitet den Creator-Anteil an künftigen Gebühren dauerhaft in die Pots um. Nur der aktuelle Creator kann die Funktion aufrufen. Bereits angesammelte Gebühren kann der Creator weiterhin abrufen, und die Creator-Rolle kann weiterhin übertragen werden.