Construiește pe Duel
Poolurile Duel sunt pooluri Uniswap V4 standard, cu un hook, iar fiecare cifră pe care o arată aplicația vine din vizualizări și evenimente publice. Boții, terminalele, dashboardurile și agregatoarele pot construi pe el fără să ceară voie.
Citirea unui duel
DuelLens adună totul despre un duel într-un singur apel:
function getPair(uint256 pairId) external view returns (PairView memory);
function getPairs(uint256 offset, uint256 limit) external view returns (PairView[] memory);
Un PairView conține ID-ul duelului, creatorul și URI-ul metadatelor, ambele tabere (token, nume, simbol, ofertă,
prețul și tickul poolului, ETH pe token, ETH-ul real din pool), cele trei chei de pool, repartiția live (gaugeBps,
partea taberei A în puncte de bază), media rundei de până acum (roundGaugeBps), potul, comisioanele de creator,
volumul și răscumpărările all-time, taxa anti-snipe curentă, indexul, începutul și sfârșitul rundei și dacă s-a
tranzacționat în ea (roundTraded), momentul și tickul lansării (launchTime, startTick), dacă este un duel
susținător, categoria lui (category, după numerotarea aplicației, 0 pentru niciuna), condițiile lui de comision
(fees) și cât din partea sa ia protocolul acum (protocolRateBps, din 10.000): protocolul primește
fees.protocolShareBps × protocolRateBps / 10.000 din comisionul de swap, iar potul restul acelei părți. În contracte
și în ABI-urile lor, tabăra B se numește D: d, tokenD, keyD.
Hookul are vizualizări mai mici: pairCount(), getPair(pairId), poolKeys(pairId), gaugeBps(pairId),
snipeTaxBps(pairId), protocolRateBps(), protocolFees() și firstRoundEnd(launchTime).
Tranzacționarea
Orice router Uniswap V4 poate tranzacționa poolurile unui duel. Construiește cheile de pool din poolKeys(pairId):
| Pool | currency0 | currency1 | fee | tickSpacing |
|---|---|---|---|---|
| ETH / A | ETH (address(0)) | tokenul A | 0 | 200 |
| ETH / B | ETH (address(0)) | tokenul B | 0 | 200 |
| A / B | adresa de token mai mică | cea mai mare | 0 | 60 |
Hookul își ia comisioanele prin return deltas, așa că cotează cu V4 Quoter: răspunsurile lui includ fiecare comision și
fiecare taxă. O schimbare de tabără prin ETH înseamnă quoteExactInput pe drumul token → ETH → celălalt token.
Un swap căruia hookul îi ia comisionul înainte să ruleze (o cumpărare sau o schimbare de tabără cu intrare exactă, o
vânzare cu ieșire exactă) trebuie să se execute integral: unul pe care limita lui de preț, sau un pool ETH fără destul
ETH, l-ar opri mai devreme e anulat cu PartialFill. PoolManager împachetează eroarea unui hook: PartialFill și
ArbitrageStarved sosesc în WrappedError(address target, bytes4 selector, bytes reason, bytes details), iar în
UnexpectedRevertBytes(bytes revertData) când le raportează V4 Quoter. Decodează reason ca să citești eroarea hookului.
Fiecare tranzacție se încheie cu realinierea hookului, care are nevoie de gaz propriu: un swap care îi lasă prea puțin se anulează cu ArbitrageStarved în loc să o sară. Trimite swapurile cu gazul dat de o estimare, niciodată mai puțin, și cu o marjă: primul swap de după închiderea unei runde decontează și runda (răscumpărarea ei, apoi o realiniere, cam 170.000 de gaz în plus), ceea ce o estimare făcută înainte de închidere nu socotește. Aplicația adaugă 500.000 de gaz la fiecare estimare.
DuelRouter oferă un apel pentru un swap într-un singur pool:
function swapExactIn(
PoolKey calldata key,
bool zeroForOne,
uint256 amountIn,
uint256 minAmountOut,
address recipient,
uint256 deadline
) external payable returns (uint256 amountOut);
și unul pentru o schimbare de tabără prin ETH, tabăra vândută în poolul ei ETH și tot ETH-ul obținut cheltuit în poolul celeilalte:
function swapThroughEth(
PoolKey calldata keyIn,
PoolKey calldata keyOut,
uint256 amountIn,
uint256 minAmountOut,
address recipient,
uint256 deadline
) external returns (uint256 amountOut);
O vânzare într-un pool ETH Duel care se oprește la prețul de lansare al poolului, cu ETH-ul epuizat, este trimisă din nou pentru rest în același apel, după ce realinierea hookului a răscumpărat din pool ceea ce preia poolul dintre tabere: până la SALE_PASSES() (8) swapuri, cât timp prețul poolului rămâne sub limita swapurilor. Fiecare plătește comisionul hookului pe propriul ETH, iar minAmountOut le numără pe toate. V4 Quoter de la Uniswap cotează un singur swap: dincolo de ce execută acesta, răspunde NotEnoughLiquidity. Aplicația cotează o asemenea vânzare rulând routerul în locul traderului (un eth_call cu state override, DuelSimulator).
Ce realinierea nu mai poate răscumpăra, sell duce mai departe prin poolul dintre tabere, până în poolul ETH al celeilalte tabere, unde realinierile au mutat ETH-ul care îl plătește:
function sell(
PoolKey calldata keyIn,
PoolKey calldata keyCross,
PoolKey calldata keyOther,
uint256 amountIn,
uint256 minAmountOut,
address recipient,
uint256 deadline
) external returns (uint256 amountOut);
Vinde în keyIn la fel ca swapExactIn, apoi ce rămâne prin keyCross, care preia tot (comisionul lui de schimbare a taberei se arde, ca la orice schimbare de tabără), și în keyOther, tot în mai multe treceri. minAmountOut numără ETH-ul ambelor pooluri. Ce nu poate răscumpăra nici keyOther este plătit către recipient în tokenul acelui pool, în afara sumei minime. Aplicația vinde prin sell când V4 Quoter refuză o vânzare și DuelSimulator.simulateSale arată că astfel se vinde totul pe ETH.
Un alt router V4 face un singur swap. Hookul nu refuză o vânzare cu intrare exactă care trece de ce plătește ETH-ul poolului (comisionul lui se ia după swap, din ETH-ul plătit): vânzarea se execută în parte, până la prețul de lansare al poolului, iar restul rămâne la vânzător. Setează suma minimă primită pentru ce se poate executa și vinde restul într-o tranzacție ulterioară, în ce a răscumpărat realinierea.
Trimite ETH cu swapExactIn ca să cumperi. Ca să vinzi sau să schimbi, aprobă routerul pentru token sau lasă un permit
(EIP-2612) să-l aprobe în aceeași tranzacție: swapExactInWithPermit, swapThroughEthWithPermit și sellWithPermit
primesc aceleași argumente, plus permitul apelantului pentru exact amountIn, semnat pentru router:
struct Permit {
uint256 deadline;
uint8 v;
bytes32 r;
bytes32 s;
}
Portofelul tău semnează permiturile unui token Duel în domeniul EIP-712 al tokenului, numit Duel, versiunea 1, folosind adresa tokenului
(eip712Domain()-ul său, ERC-5267, o spune). Routerul folosește întâi permitul și îi ignoră eșecul: unul trimis deja de
oricine a setat aprobarea, iar unul care eșuează nu schimbă nimic, swapul luând atunci cât acoperă aprobarea existentă și
eșuând dacă nu ajunge. Un swap executat doar în parte păstrează aprobarea neutilizată. Termenul permitului limitează momentul în
care poate fi prezentată semnătura, fără să expire aprobarea deja acordată. Ia tokenuri doar de la apelant.
ETH-ul unei schimbări de tabără rămâne în PoolManager ca să-l cheltuiască al doilea pool; un pool Duel îl cheltuiește în
întregime sau hookul anulează swapul (PartialFill), iar pe orice alt pool ce nu poate cheltui îi e returnat apelantului.
Lansarea unui duel
DuelFactory.createPair(CreateParams) lansează un duel; trimite odată cu apelul comisionul de lansare și cumpărările la lansare, orice surplus este returnat. CreateParams conține nameA, symbolA, nameD, symbolD (tabăra B), metadataURI, buyA, buyD (ETH-ul cheltuit pe fiecare tabără la lansare, fără taxa anti-snipe), supporter, category (după numerotarea categoriilor din aplicație, 0 pentru niciuna) și config, adică configHash() al factory-ului așa cum l-ai citit: lansarea se anulează cu ConfigChanged dacă între timp condițiile s-au schimbat. Un nume are între 1 și 128 de octeți, un ticker între 1 și 40, iar URI-ul metadatelor cel mult 4096 (InvalidMetadata); o cumpărare la lansare de peste 5% din oferta unei tabere se anulează (CreatorBuyTooLarge).
Fabrica pornește cu lansările suspendate. Cât timp paused() este true, doar owner() poate lansa un duel; apelurile celorlalți sunt anulate cu CreationPaused. Proprietarul deschide lansările prin setPaused(false). Duelurile existente se tranzacționează în continuare.
Evenimente
| Contract | Eveniment | Când |
|---|---|---|
| DuelFactory | PairCreated(pairId, creator, tokenA, tokenD) | S-a lansat un duel |
| DuelHook | PairRegistered(pairId, tokenA, tokenD, creator, category) | Poolurile lui s-au înregistrat la hook, în aceeași tranzacție; category este tema lui, după numerotarea categoriilor din aplicație (0 pentru niciuna) |
| DuelHook | FeeTaken(pairId, fee, snipeTax) | O tranzacție contra ETH și-a plătit comisionul |
| DuelHook | SwitchFeeBurned(pairId, token, amount) | O schimbare de tabără și-a ars comisionul |
| DuelHook | ArbitrageCaptured(pairId, profit) | Realinierea a alimentat potul |
| DuelHook | RoundSettled(pairId, roundIndex, averageTick, winner, ethSpent, tokensBurned) | S-a închis o rundă |
| DuelHook | CreatorFeesClaimed(pairId, to, amount) | Un creator a fost plătit |
| DuelFactory | CreatorTransferStarted(pairId, creator, newCreator) | Un creator și-a oferit rolul (adresei zero: oferta retrasă) |
| DuelHook | CreatorChanged(pairId, from, to) | Rolul de creator al unui duel a schimbat mâinile |
| DuelHook | CreatorFeesGivenUp(pairId, creator) | Creatorul și-a redirecționat definitiv partea din taxele viitoare către fonduri |
| DuelHook | ProtocolFeesClaimed(to, amount) | Trezoreria a fost plătită |
| DuelHook | ProtocolRateSet(rateBps) | Partea protocolului din comisionul de swap al tuturor duelurilor s-a schimbat imediat: ia rateBps din ea (10.000 pentru toată), restul merge în poturi |
| DuelFactory | ConfigProposed(config, eta), ConfigApplied(config), ConfigCancelled() | Condiții de lansare propuse pentru peste 48 de ore, aplicate, retrase |
| DuelFactory | PausedSet(paused) | Lansări suspendate sau reluate |
| PoolManager | Swap(id, sender, amount0, amount1, sqrtPriceX96, liquidity, tick, fee) | Fiecare tranzacție, inclusiv realinierea făcută de hook (sender-ul ei e hookul) |
Listele de tranzacții citesc swapurile tuturor celor trei pooluri; graficele prețurilor și repartiției citesc numai cele două pooluri ETH. Listele de deținători folosesc evenimentele Transfer ale tokenurilor. Subgraphul indexează swapurile, soldurile, deținătorii și rundele decontate pentru a încărca istoricul fără reluarea lanțului de la lansare.
Decontarea și revendicarea
settle(pairId)decontează o rundă încheiată. Oricine o poate apela.claimCreatorFees(pairId)plătește numai creatorul duelului, portofelul care l-a lansat sau adresa căreia i-a cedat rolul;claimProtocolFees()plătește trezoreria configurată la revendicare. Oricine poate declanșa apelurile. Trezoreria se poate schimba după termenul de 48 de ore al factory-ului; apelantul nu poate alege alt beneficiar.transferCreator(pairId, newCreator)pe factory oferă rolul de creator al unui duel, pe careacceptCreator(pairId)apelat denewCreatorîl preia;pendingCreator(pairId)citește oferta. Doar creatorul îl oferă și doar adresa căreia i s-a oferit îl preia.giveUpCreatorFees(pairId)pe hook trimite definitiv partea creatorului din taxele viitoare către fondurile rundelor. Doar creatorul actual poate apela funcția. Taxele deja acumulate pot fi revendicate în continuare de creator, iar rolul de creator poate fi în continuare transferat.