Recentemente ho scritto un tutorial sullo sviluppo di uno scambio decentralizzato https://github.com/WTFAcademy/WTF-Dapp, ho consultato l'implementazione del codice di Uniswap V3 e ho appreso molti punti di conoscenza. In precedenza, avevo sviluppato contratti NFT semplici, questa è la mia prima volta nel tentare di sviluppare un contratto Defi, sono sicuro che questi piccoli trucchi saranno molto utili per i principianti che vogliono imparare a sviluppare contratti.
I grandi sviluppatori di contratti possono direttamente visitare https://github.com/WTFAcademy/WTF-Dapp per contribuire al codice e costruire insieme il Web3~
Ora diamo un'occhiata a questi piccoli trucchi, alcuni dei quali possono essere considerati veri e propri colpi di genio.
L'indirizzo del contratto distribuito può essere reso prevedibile.
Di solito, l'indirizzo che otteniamo distribuendo un contratto appare casuale, poiché è legato al « nonce », quindi l'indirizzo del contratto non è facilmente prevedibile. Ma in Uniswap, abbiamo una tale esigenza: dobbiamo poter dedurre l'indirizzo del contratto tramite la coppia di scambi e informazioni correlate. Questo è molto utile in molti casi, ad esempio per determinare i diritti di transazione o per ottenere l'indirizzo della pool.
In Uniswap, la creazione di contratti avviene tramite il codice « pool = address(new Uniswap V3 Pool{salt: keccak 256(abi.encode(token 0, token 1, fee))}()); ». Aggiungendo il « salt » si utilizza il metodo CREATE 2 (https://github.com/AmazingAng/WTF-Solidity/blob/main/25_Create2/readme.md) per creare il contratto, il vantaggio di questo approccio è che l'indirizzo del contratto creato è prevedibile, la logica di generazione dell'indirizzo è « Nuovo indirizzo = hash("0x FF", indirizzo creatore, salt, initcode)».
Puoi consultare questa parte del corso WTF-DApp https://github.com/WTFAcademy/WTF-Dapp/blob/main/P103_Factory/readme.md per saperne di più.
Usa bene le funzioni di callback.
In Solidity, i contratti possono chiamarsi a vicenda. Un possibile scenario è A che chiama B in un certo metodo, e B richiama A nel metodo chiamato; questo è molto utile in alcune situazioni.
In Uniswap, quando chiami il metodo « swap » del contratto « Uniswap V3 Pool » per eseguire uno scambio, esso richiama la funzione « swapCallback », che passerà il « Token » necessario per la transazione calcolato, e il chiamante deve trasferire il Token necessario per la transazione nel « Uniswap V3 Pool » durante la callback, invece di dividere il metodo « swap » in due parti da chiamare, assicurando così la sicurezza del metodo « swap » e garantendo che l'intera logica venga eseguita completamente, senza necessità di registri di variabili complessi per garantire la sicurezza.
Il frammento di codice è il seguente:
Puoi studiare di più sulla parte relativa agli scambi nel corso qui https://github.com/WTFAcademy/WTF-Dapp/blob/main/P106_PoolSwap/readme.md.
Usa le eccezioni per comunicare informazioni, usa try catch per implementare la stima delle transazioni.
Consultando il codice di Uniswap, abbiamo notato che nel suo contratto https://github.com/Uniswap/v3-periphery/blob/main/contracts/lens/Quoter.sol, il metodo « swap » del « Uniswap V3 Pool » è stato eseguito avvolto in un « try catch »:
Perché accade questo? Perché dobbiamo simulare il metodo « swap » per stimare il Token necessario per la transazione, ma poiché durante la stima non avviene realmente uno scambio di Token, si verificherà un errore. In Uniswap, viene lanciato un errore speciale nella callback della transazione e poi catturato, così che le informazioni necessarie possano essere estratte dal messaggio di errore.
Sembra piuttosto un hack, ma è anche molto pratico. In questo modo non è necessario modificare il metodo di swap per soddisfare le esigenze di stima delle transazioni, e la logica è più semplice. Nel nostro corso, abbiamo anche fatto riferimento a questa logica implementando il contratto https://github.com/WTFAcademy/WTF-Dapp/blob/main/demo-contract/contracts/wtfswap/SwapRouter.sol.
Utilizza numeri grandi per risolvere i problemi di precisione.
Nel codice di Uniswap, ci sono molte logiche di calcolo, ad esempio calcolare i Token scambiati in base al prezzo e alla liquidità attuali, e in questo processo dobbiamo evitare di perdere precisione durante le operazioni di divisione. In Uniswap, il processo di calcolo utilizza frequentemente l'operazione « << FixedPoint 96.RESOLUTION », che rappresenta uno spostamento a sinistra di 96 bit, equivalente a moltiplicare per « 2 ^ 96 ». Dopo lo spostamento a sinistra, eseguiamo l'operazione di divisione, in modo da garantire la precisione senza traboccare durante le normali transazioni (generalmente calcoliamo usando « uint 256 », che è sufficiente).
Il codice è il seguente (calcola il numero di Token necessari per lo scambio in base al prezzo e alla liquidità):
Come puoi vedere, innanzitutto in Uniswap i prezzi sono calcolati usando la radice quadrata moltiplicata per « 2 ^ 96 » (corrispondente a « sqrtRatioAX 96 » e « sqrtRatioBX 96 » nel codice sopra), poi la liquidità « liquidity » viene spostata a sinistra per calcolare « numerator 1 ». Nel calcolo sottostante, « 2 ^ 96 » verrà eliminato nel processo di calcolo, ottenendo il risultato finale.
Naturalmente, in ogni caso, in teoria ci sarà una perdita di precisione, ma questa situazione implica solo una perdita della più piccola unità, che è accettabile.
Per ulteriori informazioni, puoi studiare di più qui https://github.com/WTFAcademy/WTF-Dapp/blob/main/P106_PoolSwap/readme.md.
Utilizza il metodo Share per calcolare i profitti
In Uniswap, dobbiamo registrare i profitti delle commissioni per i LP (fornitori di liquidità). Chiaramente, non possiamo registrare le commissioni per ogni LP in ogni transazione, poiché ciò consumerebbe una grande quantità di Gas. Come possiamo gestirlo?
In Uniswap, possiamo vedere che nella « Position » è definita la seguente struttura:
Contiene « feeGrowthInside0LastX128 e feeGrowthInside1LastX128 », che registrano le commissioni che ogni liquidità dovrebbe ricevere l'ultima volta che è stata prelevata la commissione per ogni posizione (Position).
In parole semplici, devo solo registrare le commissioni totali e quanto ciascuna liquidità dovrebbe ricevere in commissioni, in modo che, quando un LP preleva le commissioni, possa calcolare quante commissioni può prelevare in base alla liquidità in suo possesso. È come se possedessi azioni di una società, quando devi prelevare i profitti dalle azioni, devi solo conoscere i profitti storici per azione dell'azienda e i profitti dell'ultima volta che hai prelevato.
In precedenza, abbiamo introdotto nel nostro articolo (Design intelligente del contratto, come stETH distribuisce automaticamente i profitti ogni giorno? Fai partecipare il tuo ETH per ottenere interessi stabili) il metodo di calcolo dei profitti di stETH, che è una logica simile.
Non tutte le informazioni devono essere ottenute dalla blockchain.
Lo storage on-chain è relativamente costoso, quindi non tutte le informazioni devono essere on-chain o ottenute dalla blockchain. Ad esempio, molte delle interfacce chiamate dal sito front-end di Uniswap sono interfacce tradizionali Web2.
Le liste delle pool di scambio, le informazioni sulle pool di scambio, ecc. possono essere memorizzate in normali database, alcune potrebbero dover essere sincronizzate periodicamente dalla blockchain, ma non abbiamo bisogno di chiamare in tempo reale le interfacce PRC fornite dai servizi blockchain o nodi per ottenere dati rilevanti.
Naturalmente, ora molti fornitori di blockchain PRC offrono alcune interfacce avanzate, che ti consentono di ottenere alcuni dati in modo più rapido e a basso costo, il che è simile. Ad esempio, ZAN offre un'interfaccia simile per ottenere tutti gli NFT di un determinato utente; queste informazioni possono evidentemente essere migliorate in termini di prestazioni ed efficienza tramite caching, puoi visitare https://zan.top/service/advance-api per ottenere ulteriori informazioni.
Naturalmente, le transazioni cruciali devono avvenire on-chain.
Impara a suddividere i contratti e impara a utilizzare contratti standard esistenti come l'ERC 721.
Un progetto può contenere più contratti effettivamente distribuiti, anche se effettivamente è distribuito solo un contratto, il nostro codice può essere diviso in più contratti tramite ereditarietà per la manutenzione.
Ad esempio, in Uniswap, il contratto https://github.com/Uniswap/v3-periphery/blob/main/contracts/NonfungiblePositionManager.sol è ereditato da molti contratti, il codice è il seguente:
Inoltre, quando guardi l'implementazione del contratto « ERC 721 Permit », noterai che utilizza direttamente il contratto « @openzeppelin/contracts/token/ERC 721/ERC 721.sol », il che facilita la gestione delle posizioni tramite NFT e, dall'altra parte, consente di utilizzare contratti standard esistenti per migliorare l'efficienza dello sviluppo dei contratti.
Nel nostro corso, puoi imparare a sviluppare un semplice contratto ERC 721 per gestire le posizioni qui https://github.com/WTFAcademy/WTF-Dapp/blob/main/P108_PositionManager/readme.md.
Riassunto
Leggere tanti articoli non è utile come mettersi a sviluppare, provare a implementare una versione semplificata di uno scambio decentralizzato ti permetterà di comprendere più a fondo l'implementazione del codice di Uniswap e di apprendere ulteriori punti di conoscenza che si possono incontrare in progetti reali.
Il corso WTF-DApp è un corso open source completato dalla comunità degli sviluppatori di ZAN e dai membri della comunità degli sviluppatori di WTF Academy. Se sei interessato a Web3 e allo sviluppo di progetti Defi, puoi consultare il nostro corso pratico https://github.com/WTFAcademy/WTF-Dapp, per completare passo dopo passo una versione semplificata di uno scambio, crediamo che ti sarà di grande aiuto~
Questo articolo è stato scritto da Fisher del team ZAN (X account @zan_team) (X account @yudao 1024 ).