Guida dettagliata al contratto Uniswap-v2
Introduzione
Uniswap v2 può creare un mercato di scambio tra due token ERC-20. In questo articolo analizzeremo il codice sorgente per i contratti che implementano questo protocollo e vedremo perché sono scritti in questo modo.
Cosa fa Uniswap?
Fondamentalmente, esistono due tipi di utenti: fornitori di liquidità e trader.
I fornitori di liquidità forniscono al pool i due token scambiabili (li chiameremo Token0 e Token1). In cambio, ricevono un terzo token che rappresenta la proprietà parziale del pool, detto token di liquidità.
I trader inviano un tipo di token al pool e ricevono l'altro (ad esempio, invii Token0 e ricevi Token1) dal pool fornito dai fornitori di liquidità. Il tasso di cambio è determinato dal numero relativo di Token0 e Token1 che il pool possiede. Inoltre, il pool prende una piccola percentuale come ricompensa per il pool di liquidità.
Quando i fornitori di liquidità rivogliono indietro le loro risorse, possono bruciare i token del pool e ricevere i token, compresa la quota di ricompense.
Clicca qui per una descrizione completa.
Perché v2? Perché non v3?
Mentre scrivo il presente articolo, Uniswap v3 è quasi pronto. Tuttavia, è un aggiornamento molto più complicato dell'originale. È più facile imparare prima la v2 e poi passare alla v3.
Contratti principali e contratti periferici
Uniswap v2 è diviso in due componenti, una principale e una periferica. Questa divisione consente ai contratti principali, che detengono le risorse e dunque devono essere sicuri, di essere più semplici e facili da controllare. Tutte le funzionalità aggiuntive richieste dai trader possono essere fornite dai contratti periferici.
Flussi di dati e di controllo
Questo è il flusso di dati e controllo che si verifica quando esegui le tre azioni principali di Uniswap:
- Scambi token diversi
- Aggiungi liquidità al mercato e vieni ricompensato con token di liquidità ERC-20 di pari valore
- Bruci token di liquidità di ERC-20 e ne ottieni altri, con uno scambio in pari che consente lo scambio ai trader
Scambio
Questo è il flusso più comune, usato dai trader:
Chiamante
- Fornisce al conto perifierico un'allowance nell'importo da scambiare.
- Chiama una delle tante funzioni di scambio del contratto periferico (la funzione chiamata dipende dal fatto che siano o meno coinvolti ETH, dal fatto che il trader specifichi l'importo di token da depositare o da prelevare, ecc.). Ogni funzione di scambio accetta un
path, un insieme di scambi da attraversare.
Nel contratto periferico (UniswapV2Router02.sol)
- Identifica l'importo necessario da scambiare su ogni scambio lungo il percorso.
- Itera sul percorso. Per ogni scambio lungo il percorso, invia il token di input e poi chiama la funzione di
swapdello scambio. In gran parte dei casi, l'indirizzo di destinazione per i token è lo scambio in pari successivo nel percorso. Nello scambio finale è l'indirizzo fornito dal trader.
Nel contratto principale (UniswapV2Pair.sol)
- Verifica che il contratto principale non raggiri il sistema e possa mantenere liquidità sufficiente dopo lo scambio.
- Vede quanti token aggiuntivi abbiamo, in aggiunta alle riserve note. Quell'importo è il numero di token di input ricevuti da scambiare.
- Invia i token d'output alla destinazione.
- Chiama
_updateper aggiornare gli importi della riserva
Di nuovo nel contratto periferico (UniswapV2Router02.sol)
- Esegue ogni pulizia necessaria (ad esempio, brucia i token di WET per riottenere ETH da inviare al trader)
Aggiungere liquidità
Chiamante
- Fornisce al conto periferico un'allowance negli importi da aggiungere al pool di liquidità.
- Chiama una delle funzioni addLiquidity del contratto periferico.
Nel contratto periferico (UniswapV2Router02.sol)
- Crea un nuovo scambio in pari se necessario
- Se c'è uno scambio in pari esistente, calcola l'importo di token da aggiungere. Questo dovrebbe essere un valore identico per entrambi i token, quindi lo stesso rapporto tra token nuovi ed esistenti.
- Controlla se gli importi sono accettabili (i chiamanti possono specificare un importo minimo al di sotto del quale preferirebbero non aggiungere liquidità)
- Chiama il contratto principale.
Nel contratto principale (UniswapV2Pair.sol)
- Conia token di liquditià e li invia al chiamante
- Chiama
_updateper aggiornare gli importi della riserva
Rimuovere la liquidità
Chiamante
- Fornisce al conto periferico un'allowance di token di liquidità da bruciare in cambio dei token sottostanti.
- Chiama una delle funzioni removeLiquidity del contratto periferico.
Nel contratto periferico (UniswapV2Router02.sol)
- Invia i token di liquidità allo scambio in pari
Nel contratto principale (UniswapV2Pair.sol)
- Invia all'indirizzo di destinazione i token sottostanti in proporzione ai token bruciati. Ad esempio, se ci sono 1.000 token A nel pool, 500 token B e 90 token di liquidità e ne riceviamo 9 da bruciare, bruciamo il 10% dei token di liquidità, quindi reinviamo all'utente 100 token A e 50 token B.
- Brucia i token di liquidità
- Chiama
_updateper aggiornare gli importi della riserva
I contratti principali
Questi sono i contratti sicuri che detengono la liquidità.
UniswapV2Pair.sol
Questo contratto implementa il pool reale che scambia i token. È la funzionalità principale di Uniswap.
1pragma solidity =0.5.16;23import './interfaces/IUniswapV2Pair.sol';4import './UniswapV2ERC20.sol';5import './libraries/Math.sol';6import './libraries/UQ112x112.sol';7import './interfaces/IERC20.sol';8import './interfaces/IUniswapV2Factory.sol';9import './interfaces/IUniswapV2Callee.sol';10Mostra tuttoCopia
Queste sono tutte le interfacce di cui il contratto deve essere a conoscenza, perché le implementa (IUniswapV2Pair and UniswapV2ERC20) o perché chiama i contratti che le implementano.
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {2Copia
Questo contratto eredita da UniswapV2ERC20, che fornisce le funzioni di ERC-20 per i token di liquidità.
1 using SafeMath for uint;2Copia
La libreria SafeMath è usata per evitare flussi eccessivi o insufficienti. È un aspetto importante perché altrimenti potremmo finire in una situazione in cui il valore dovrebbe essere -1, ma invece è 2^256-1.
1 using UQ112x112 for uint224;2Copia
Molti calcoli nel contratto di pool richiedono l'uso di frazioni. Tuttavia, le frazioni non sono supportate dall'EVM. La soluzione che Uniswap ha trovato è usare valori a 224 bit, con 112 bit per la parte intera e 112 bit per la frazione. Quindi 1.0 è rappresentato come 2^112, 1.5 è rappresentato come 2^112 + 2^111, ecc.
Ulteriori dettagli su questa libreria sono disponibili più in avanti nel documento.
Variabili
1 uint public constant MINIMUM_LIQUIDITY = 10**3;2Copia
Per evitare casi di divisione per zero, esiste un numero minimo di token di liquidità che esiste sempre (ma è posseduto dal conto zero). Quel numero è MINIMUM_LIQUIDITY, mille.
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));2Copia
Questo è il selettore dell'ABI per la funzione di trasferimento dell'ERC-20. È usato per trasferire i token ERC-20 nei due conti di token.
1 address public factory;2Copia
Questo è il contratto factory che ha creato questo pool. Ogni pool rappresenta uno scambio tra due token ERC-20, dove factory è un punto centrale che connette tutti questi pool.
1 address public token0;2 address public token1;3Copia
Sono gli indirizzi dei contratti per due tipi di token ERC-20, scambiabili da questo pool.
1 uint112 private reserve0; // uses single storage slot, accessible via getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReserves3Copia
Le riserve che il pool ha per ogni tipo di token. Supponiamo che i due rappresentino la stessa quantità di valore e dunque che ogni token0 valga token1 di reserve1/reserve0.
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves2Copia
La marca oraria dall'ultimo blocco in cui si è verificato uno scambio, usata per tracciare i tassi di cambio nel tempo.
Una delle più grandi spese di gas dei contratti di nexus è la memoria, che persiste da una chiamata del contratto alla successiva. Ogni cella di memoria è lunga 256 bit. Tre variabili, reserve0, reserve1 e blockTimestampLast, sono quindi allocate in modo che un solo valore di memoria possa comprenderle tutte e tre (112+112+32=256).
1 uint public price0CumulativeLast;2 uint public price1CumulativeLast;3Copia
Queste variabili contengono i costi cumulativi per ogni token (ognuna nel termine dell'altra). Sono utilizzabili per calcolare il tasso di cambio medio su un periodo di tempo.
1 uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event2Copia
Il modo in cui lo scambio in pari decide sul tasso di cambio tra token0 e token1 consiste nel mantenere il multiplo delle due riserve costanti durante gli scambi. kLast è questo valore. Cambia quando un fornitore di liquidità deposita o preleva token e aumenta lievemente a causa della commissione di mercato dello 0,3%.
Ecco un semplice esempio. Nota che per semplicità la tabella ha solo tre cifre dopo la virgola decimale e che ignoriamo la commissione di gas dello 0,3%, quindi i numeri non sono accurati.
| Evento | reserve0 | reserve1 | reserve0 * reserve1 | Tasso di cambio medio (token1/token0) |
|---|---|---|---|---|
| Configurazione iniziale | 1.000,000 | 1.000,000 | 1.000.000 | |
| Trader A scambia 50 token0 per 47,619 token1 | 1.050,000 | 952,381 | 1.000.000 | 0,952 |
| Trader B scambia 10 token0 per 8,984 token1 | 1.060,000 | 943,396 | 1.000.000 | 0,898 |
| Trader C scambia 40 token0 per 34,305 token1 | 1.100,000 | 909,090 | 1.000.000 | 0,858 |
| Trader D scambia 100 token1 per 109,01 token0 | 990,990 | 1.009,090 | 1.000.000 | 0,917 |
| Trader E scambia 10 token0 per 10,079 token1 | 1.000,990 | 999,010 | 1.000.000 | 1,008 |
Man mano che i trader forniscono più token0, il valore relativo di token1 aumenta, e viceversa, in basel all'offerta e alla domanda.
Bloccare
1 uint private unlocked = 1;2Copia
Esiste una classe di vulnerabilità di sicurezza basata sull'abuso di rientrata. Uniswap deve trasferire token ERC-20 arbitrari, ovvero chiamare i contratti ERC-20 che potrebbero tentare di abusare del mercato di Uniswap che li chiama. Avendo una variabile unlocked inserita nel contratto, possiamo evitare che le funzioni vengano chiamate mentre sono in esecuzione (nella stessa transazione).
1 modifier lock() {2Copia
Questa funzione è un modificatore, una funzione che avvolge una funzione normale per cambiarne in qualche modo il comportamento.
1 require(unlocked == 1, 'UniswapV2: LOCKED');2 unlocked = 0;3Copia
Se unlocked è pari a uno, impostalo su zero. Se è già zero, ripristina la chiamata, facendola fallire.
1 _;2Copia
In un modificatore _; è la chiamata alla funzione originale (con tutti i parametri). Significa che la chiamata della funzione ha luogo solo se unlocked era su uno quando è stata chiamata e, mentre è in esecuzione, il valore di unlocked è zero.
1 unlocked = 1;2 }3Copia
Al ritorno della funzione principale, rilascia il blocco.
Funzioni varie
1 function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {2 _reserve0 = reserve0;3 _reserve1 = reserve1;4 _blockTimestampLast = blockTimestampLast;5 }6Copia
Questa funzione fornisce ai chiamanti lo stato corrente dello scambio. Nota che le funzioni di Solidity possono restituire valori multipli.
1 function _safeTransfer(address token, address to, uint value) private {2 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));3Copia
Questa funzione interna trasferisce un importo di token ERC20 dallo scambio a qualcun altro. SELECTOR specifica che la funzione che stiamo chiamando è transfer(address,uint) (vedi la definizione sopra).
Per evitare di dover importare un'interfaccia per la funzione del token, creiamo "manualmente" la chiamata usando una delle funzioni dell'ABI.
1 require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');2 }3Copia
Esistono due modi in cui una chiamata di trasferimento ERC-20 può segnalare il fallimento:
- Ripristina. Se una chiamata a un contratto esterno si annulla, allora il valore di restituzione booleano è
false - Termina normalmente ma segnala un guasto. In quel caso il buffer del valore restituito ha una lunghezza diversa da zero e, quando viene codificato come valore booleano, è
false
Se una di queste condizioni si verifica, ripristina.
Eventi
1 event Mint(address indexed sender, uint amount0, uint amount1);2 event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);3Copia
Questi due eventi sono emessi quando un fornitore di liquidità deposita (Mint) o preleva (Burn) liquidità. In ogni caso, gli importi di token0 e token1 depositati o prelevati fanno parte dell'evento, così come l'identità del conto che ci ha chiamati (sender). Nel caso di un prelievo, l'evento include anche la destinazione che ha ricevuto i token (to), che potrebbe non essere pari al mittente.
1 event Swap(2 address indexed sender,3 uint amount0In,4 uint amount1In,5 uint amount0Out,6 uint amount1Out,7 address indexed to8 );9Copia
Questo evento è emesso quando un trader scambia un token per l'altro. Ancora, il mittente e la destinazione potrebbero non corrispondere. Ogni token potrebe essere inviato allo scambio o ricevuto da esso.
1 event Sync(uint112 reserve0, uint112 reserve1);2Copia
Infine, Sync viene emesso ogni volta che i token sono aggiunti o prelevati, indipendentemente dal motivo, per fornire le ultime informazioni sulla riserva (e dunque il tasso di cambio).
Funzioni di configurazione
Queste funzioni dovrebbero essere chiamate una volta che il nuovo scambio in pari è configurato.
1 constructor() public {2 factory = msg.sender;3 }4Copia
Il costruttore si assicura che terremo traccia dell'indirizzo della factory che ha creato la coppia. Queste informazioni sono necessarie per initialize e per la commissione di factory (se presente)
1 // called once by the factory at time of deployment2 function initialize(address _token0, address _token1) external {3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check4 token0 = _token0;5 token1 = _token1;6 }7Copia
Questa funzione consente alla factory (e solo a essa) di specificare i due token ERC-20 che questa coppia scambierà.
Funzioni di aggiornamento interno
_update
1 // update reserves and, on the first call per block, price accumulators2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {3Copia
Questa funzione è chiamata ogni volta che i token vengono depositati o prelevati.
1 require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');2Copia
Se balance0 o balance1 (uint256) è maggiore di uint112(-1) (=2^112-1) (quindi va in overflow e torna a 0 quando viene convertita in uint112), rifiuta di continuare _update per prevenire l'overflow. Con un token normale suddivisibile in 10^18 unità, questo significa che ogni scambio è limitato a circa 5,1*10^15 di ogni token. Finora non è stato un problema.
1 uint32 blockTimestamp = uint32(block.timestamp % 2**32);2 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired3 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {4Copia
Se il tempo trascorso è diverso da zero, significa che la nostra è la prima transazione di scambio su questo blocco. In tal caso, dobbiamo aggiornare gli accumulatori del costo.
1 // * never overflows, and + overflow is desired2 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;3 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;4 }5Copia
Ogni accumulatore del costo è aggiornato con l'ultimo costo (riserva dell'altro token/riserva di questo token) per il tempo trascorso in secondi. Per ottenere un prezzo medio leggi il prezzo cumulativo di due punti nel tempo e dividi per la differenza di tempo tra loro. Ad esempio, supponiamo questa sequenza di eventi:
| Evento | reserve0 | reserve1 | marca oraria | Tasso di cambio marginale (reserve1/reserve0) | price0CumulativeLast |
|---|---|---|---|---|---|
| Configurazione iniziale | 1.000,000 | 1.000,000 | 5.000 | 1,000 | 0 |
| Trader A deposita 50 token0 e ottiene 47,619 token1 | 1.050,000 | 952,381 | 5.020 | 0,907 | 20 |
| Trader B deposita 10 token0 e ottiene 8,984 token1 | 1.060,000 | 943,396 | 5.030 | 0,890 | 20+10*0,907 = 29,07 |
| Trader C deposita 40 token0 e ottiene 34,305 token1 | 1.100,000 | 909,090 | 5.100 | 0,826 | 29,07+70*0,890 = 91,37 |
| Trader D deposita 100 token0 e ottiene 109,01 token1 | 990,990 | 1.009,090 | 5.110 | 1,018 | 91,37+10*0,826 = 99,63 |
| Trader E deposita 10 token0 e ottiene 10,079 token1 | 1.000,990 | 999,010 | 5.150 | 0,998 | 99,63+40*1.1018 = 143,702 |
Diciamo che vogliamo calcolare il prezzo medio di Token0 tra le marche orarie 5.030 e 5.150. La differenza nel valore di price0Cumulative è 143,702-29,07=114,632. Questa è la media in due minuti (120 secondi). Quindi il prezzo medio è 114,632/120 = 0,955.
Questo calcolo del prezzo è il motivo per cui dobbiamo conoscere le dimensioni della vecchia riserva.
1 reserve0 = uint112(balance0);2 reserve1 = uint112(balance1);3 blockTimestampLast = blockTimestamp;4 emit Sync(reserve0, reserve1);5 }6Copia
Infine, aggiorna le variabili globali ed emetti un evento Sync.
_mintFee
1 // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)2 function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {3Copia
In Uniswap 2.0 i trader pagano una commissione dello 0,30% per usare il mercato. Gran parte di tale commissione (0,25% della transazione) va sempre ai fornitori di liquidità. Il rimanente 0,05% può andare ai fornitori di liquidità o a un indirizzo specificato dalla factory come commissione di protocollo, che paga Uniswap per l'attività di sviluppo.
Per ridurre i calcoli (e dunque i costi del carburante), tale commissione è calcolata solo quando la liquidità viene aggiunta o rimossa dal pool, anziché ad ogni operazione.
1 address feeTo = IUniswapV2Factory(factory).feeTo();2 feeOn = feeTo != address(0);3Copia
Leggi la destinazione della commissione della factory. Se è zero, allora non esiste alcuna commissione di protocollo e non serve calcolarla.
1 uint _kLast = kLast; // gas savings2Copia
La variabile di stato kLast si trova in memoria, quindi avrà un valore tra diverse chiamate al contratto. L'accesso all'archiviazione è molto più costoso dell'accesso alla memoria volatile rilasciata quando termina la chiamata alla funzione al contratto, quindi usiamo una variabile interna per risparmiare sul carburante.
1 if (feeOn) {2 if (_kLast != 0) {3Copia
I fornitori di liquidità ottengono la loro parte semplicemente mediante l'apprezzamento dei loro token di liquidità. Ma la commissione di protocollo richiede che i nuovi token di liquidità siano coniati e forniti all'indirizzo feeTo.
1 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));2 uint rootKLast = Math.sqrt(_kLast);3 if (rootK > rootKLast) {4Copia
Se c'è nuova liquidità su cui raccogliere una commissione di protocollo. Puoi vedere la funzione della radice quadrata più avanti in questo articolo
1 uint numerator = totalSupply.mul(rootK.sub(rootKLast));2 uint denominator = rootK.mul(5).add(rootKLast);3 uint liquidity = numerator / denominator;4Copia
Questo complicato calcolo delle commissioni è spiegato nel whitepaper a pagina 5. Sappiamo che, tra il momento in cui kLast è stato calcolato e il presente, non è stata aggiunta o rimossa liquidità (perché eseguiamo questo calcolo ogni volta che la liquidità viene aggiunta o rimossa, prima che cambi effettivamente), quindi qualunque modifica a reserve0 * reserve1 deve provenire dalle commissioni di transazione (senza di esse manterremmo reserve0 * reserve1 costante).
1 if (liquidity > 0) _mint(feeTo, liquidity);2 }3 }4Copia
Usa la funzione UniswapV2ERC20._mint per creare realmente i token aggiuntivi di liquidità e assegnali a feeTo.
1 } else if (_kLast != 0) {2 kLast = 0;3 }4 }5Copia
Se non c'è alcuna commissione con kLast impostato a zero (se non è già così). Alla scrittura di questo contratto, esisteva una funzionalità di rimborso del carburante che incoraggiava i contratti a ridurre la dimensione generale dello stato di nexus azzerando l'archiviazione non necessaria. Questo codice ottiene quel rimborso, se possibile.
Funzioni accessibili esternamente
Nota che, anche se ogni transazione o contratto può chiamare queste funzioni, esse sono progettate per essere chiamate dal contratto periferico. Se le chiami direttamente, non potrai barare sullo scambio in pari, ma potresti perdere valore a causa di un errore.
mint
1 // this low-level function should be called from a contract which performs important safety checks2 function mint(address to) external lock returns (uint liquidity) {3Copia
Questa funzione viene chiamata quando un fornitore di liquidità aggiunge liquidità al pool. Essa conia token di liquidità aggiuntivi a titolo di ricompensa. Dovrebbe essere chiamata da un contratto periferico che la chiama dopo aver aggiunto la liquidità alla stessa transazione (quindi nessun altro può inviare una transazione che rivendichi la nuova liquidità prima del proprietario legittimo).
1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings2Copia
Questo è il modo per leggere i risultati di una funzione di Solidity che restituisce valori multipli. Scartiamo gli ultimi valori restituiti, la marca oraria del blocco, perché non ci servono più.
1 uint balance0 = IERC20(token0).balanceOf(address(this));2 uint balance1 = IERC20(token1).balanceOf(address(this));3 uint amount0 = balance0.sub(_reserve0);4 uint amount1 = balance1.sub(_reserve1);5Copia
Ottiene i saldi correnti e vede quanto è stato aggiunto di ogni tipo di token.
1 bool feeOn = _mintFee(_reserve0, _reserve1);2Copia
Calcola le commissioni di protocollo da raccogliere, se presenti, e conia token di liquidità di conseguenza. Poiché i parametri relativi a _mintFee sono valori di riserva vecchi, la commissione viene calcolata accuratamente solo secondo le modifiche al pool causate dalle commissioni.
1 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee2 if (_totalSupply == 0) {3 liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);4 _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens5Copia
Se questo è il primo deposito, crea MINIMUM_LIQUIDITY token e inviali all'indirizzo zero per bloccarli. Non saranno mai riscattabili, il che significa che il pool non sarà mai svuotato completamente (questo ci salva dalla divisione per zero in alcuni punti). Il valore di MINIMUM_LIQUIDITY è mille e, considerando che gran parte dell'ERC-20 è suddivisa in unità di 10^-18 di un token, come ETH è diviso in wei, corrisponde a 10^-15 al valore di un singolo token. Non è quindi un costo elevato.
Al momento del primo deposito non conosciamo il valore relativo dei due token, quindi ci limitiamo a moltiplicare gli importi e calcoliamo la radice quadrata, supponendo che il deposito ci fornisca un valore pari in entrambi i token.
Possiamo fidarci perché è nell'interesse del depositante fornire un valore pari, così da evitare di perdere valore all'arbitraggio. Diciamo che il valore dei due token è identico, ma il nostro depositante ha depositato quattro volte tanto di Token1 rispetto al Token0. Un trader può avvalersi del fatto che lo scambio in pari pensi che Token0 sia più prezioso per ricavarne valore.
| Evento | reserve0 | reserve1 | reserve0 * reserve1 | Valore del pool (reserve0 + reserve1) |
|---|---|---|---|---|
| Configurazione iniziale | 8 | 32 | 256 | 40 |
| Il trader deposita 8 token Token0 e ottiene 16 Token1 | 16 | 16 | 256 | 32 |
Come puoi vedere, il trader ha guadagnato 8 token extra, che provengono da una riduzione nel valore del pool, danneggiando il depositante che li possiede.
1 } else {2 liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);3Copia
Con ogni deposito successivo, conosciamo già il tasso di cambio tra i due attivi e prevediamo che i fornitori di liquidità forniranno pari valore per entrambe. Altrimenti, diamo loro token di liquidità in base al valore inferiore che hanno fornito, a titolo di punizione.
Che sia il deposito iniziale o uno successivo, il numero di token di liquidità che forniamo è pari alla radice quadrata del cambio in reserve0*reserve1 e il valore del token di liquidità non cambia (salvo in presenza di un deposito che non abbia valori pari per entrambi i tipi, nel qual caso la "multa" viene distribuita). Ecco un altro esempio con due token dallo stesso valore, con tre depositi validi e uno inadeguato (deposito di un solo tipo di token, quindi non produce alcun token di liquidità).
| Evento | reserve0 | reserve1 | reserve0 * reserve1 | Valore del pool (reserve0 + reserve1) | Token di liquidità coniati per questo deposito | Token di liquidità totali | Valore di ciascun token di liquidità |
|---|---|---|---|---|---|---|---|
| Configurazione iniziale | 8,000 | 8,000 | 64 | 16,000 | 8 | 8 | 2,000 |
| Deposita quattro per tipo | 12,000 | 12,000 | 144 | 24,000 | 4 | 12 | 2,000 |
| Deposita due per tipo | 14,000 | 14,000 | 196 | 28,000 | 2 | 14 | 2,000 |
| Deposito di valore non pari | 18,000 | 14,000 | 252 | 32,000 | 0 | 14 | ~2,286 |
| Dopo l'arbitraggio | ~15,874 | ~15,874 | 252 | ~31,748 | 0 | 14 | ~2,267 |
1 }2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');3 _mint(to, liquidity);4Copia
Usa la funzione UniswapV2ERC20._mint per creare realmente i token aggiuntivi di liquidità e dare loro l'importo corretto.
12 _update(balance0, balance1, _reserve0, _reserve1);3 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date4 emit Mint(msg.sender, amount0, amount1);5 }6Copia
Aggiorna le variabili di stato (reserve0, reserve1 e, all'occorrenza, kLast) ed emetti l'evento appropriato.
burn
1 // this low-level function should be called from a contract which performs important safety checks2 function burn(address to) external lock returns (uint amount0, uint amount1) {3Copia
Questa funzione viene chiamata quando la liquidità è prelevata e i token di liquidità appropriati devono essere bruciati. Dovrebbe essere chiamata anche da un conto periferico.
1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings2 address _token0 = token0; // gas savings3 address _token1 = token1; // gas savings4 uint balance0 = IERC20(_token0).balanceOf(address(this));5 uint balance1 = IERC20(_token1).balanceOf(address(this));6 uint liquidity = balanceOf[address(this)];7Copia
Il contratto periferico ha trasferito la liquidità da bruciare a questo contratto prima della chiamata. Così sappiamo quanta liquidità bruciare e ci possiamo assicurare che sia bruciata.
1 bool feeOn = _mintFee(_reserve0, _reserve1);2 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee3 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution4 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution5 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');6Copia
Il fornitore della liquidità riceve pari valore di entrambi i token. In questo modo non modifichiamo il tasso di cambio.
1 _burn(address(this), liquidity);2 _safeTransfer(_token0, to, amount0);3 _safeTransfer(_token1, to, amount1);4 balance0 = IERC20(_token0).balanceOf(address(this));5 balance1 = IERC20(_token1).balanceOf(address(this));67 _update(balance0, balance1, _reserve0, _reserve1);8 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date9 emit Burn(msg.sender, amount0, amount1, to);10 }1112Mostra tuttoCopia
Il resto della funzione burn è speculare alla funzione mint di cui sopra.
swap
1 // this low-level function should be called from a contract which performs important safety checks2 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {3Copia
Anche questa funzione dovrebbe essere chiamata da un contratto periferico.
1 require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');2 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');45 uint balance0;6 uint balance1;7 { // scope for _token{0,1}, avoids stack too deep errors8Copia
Le variabili locali sono memorizzabili in memoria o, se sono troppe, direttamente sullo stack. Se possiamo limitare il numero in modo da usare lo stack consumeremo meno carburante. Per ulteriori dettagli vedi lo yellow paper, le specifiche formali di nexus, p. 26, equazione 298.
1 address _token0 = token0;2 address _token1 = token1;3 require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');4 if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens6Copia
Questo trasferimento è ottimistico, perché trasferiamo prima di essere sicuri che tutte le condizioni siano soddisfatte. Su nexus possiamo farlo perché, se le condizioni non sono risultassero soddisfatte nella chiamata, potremo sempre ripristinare allo stato prima di essa e di eventuali modifiche.
1 if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);2Copia
Informa il ricevente dello scambio, se richiesto.
1 balance0 = IERC20(_token0).balanceOf(address(this));2 balance1 = IERC20(_token1).balanceOf(address(this));3 }4Copia
Ottieni i saldi correnti. Il contratto periferico ci invia i token prima di chiamarci per lo scambio. In questo modo il contratto può verificare facilmente di non essere oggetto di truffe; tale controllo deve verificarsi nel contratto principale (perché possiamo essere chiamati da altre entità oltre che dal nostro contratto periferico).
1 uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;2 uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;3 require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');4 { // scope for reserve{0,1}Adjusted, avoids stack too deep errors5 uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));6 uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));7 require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');8Copia
Questo è un controllo di sicurezza, per assicurarsi di non perdere in seguito allo scambio. Non esiste alcuna circostanza in cui uno scambio dovrebbe ridurre reserve0*reserve1. Questo è anche il punto in cui garantiamo che una commissione dello 0,3% è inviata allo scambio; prima di verificare lo stato di salute di K, moltiplichiamo entrambi i saldi per 1000 meno gli importi moltiplicati per 3, questo significa che lo 0,3% (3/1000 = 0,003 = 0,3%) viene dedotto dal saldo prima di confrontare il suo valore K con il valore K delle riserve correnti.
1 }23 _update(balance0, balance1, _reserve0, _reserve1);4 emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);5 }6Copia
Aggiorna reserve0 e reserve1 e, se necessario, gli accumulatori di prezzo e la marca oraria ed emetti un evento.
Sincronizzazione o Skim
È possibile che i saldi reali si desincronizzino rispetto alle riserve considerate dallo scambio in pari. Non c'è modo di prelevare i token senza il consenso del contratto, ma i depositi sono una questione diversa. Un conto può trasferire i token allo scambio senza chiamare mint o swap.
In quel caso ci sono due soluzioni:
sync, che aggiorna le riserve in base ai saldi correntiskim, che preleva l'importo aggiuntivo. Nota che ogni conto ha la facoltà di chiamareskimperché non sappiamo chi ha depositato i token. Queste informazioni sono emesse in un evento, ma gli eventi non sono accessibili dalla blockchain.
1 // force balances to match reserves2 function skim(address to) external lock {3 address _token0 = token0; // gas savings4 address _token1 = token1; // gas savings5 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));6 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));7 }891011 // force reserves to match balances12 function sync() external lock {13 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);14 }15}16Mostra tuttoCopia
UniswapV2Factory.sol
Questo contratto crea gli scambi in pari.
1pragma solidity =0.5.16;23import './interfaces/IUniswapV2Factory.sol';4import './UniswapV2Pair.sol';56contract UniswapV2Factory is IUniswapV2Factory {7 address public feeTo;8 address public feeToSetter;9Copia
Queste variabili di stato sono necessarie per implementare la commissione di protocollo (vedi il whitepaper, p. 5). L'indirizzo feeTo accumulata i token di liquidità per la commissione di protocollo e feeToSetter è l'indirizzo che può modificare feeTo con un indirizzo diverso.
1 mapping(address => mapping(address => address)) public getPair;2 address[] public allPairs;3Copia
Queste variabili tracciano le coppie, gli scambi tra due tipi di token.
Il primo, getPair, è una mappatura che identifica uno contratto di scambio in pari basato sui due token ERC-20 scambiati. I token ERC-20 sono identificati dagli indirizzi dei contratti che li implementano, quindi le chiavi e il valore sono tutti indirizzi. Per ottenere l'indirizzo dello scambio in pari che ti consente di convertire da tokenA a tokenB, usi getPair[<tokenA address>][<tokenB address>] (o viceversa).
La seconda variabile, allPairs, è un insieme che include tutti gli indirizzi di scambi in pari creati da questa factory. In nexus non puoi iterare sul contenuto di una mappatura od ottenere un elenco di tutte le chiavi, quindi questa variabile è l'unico modo per sapere quali scambi sono gestiti da questa factory.
Nota: Il motivo per cui non puoi iterare su tutte le chiavi di una mappatura è che l'archiviazione dei dati del contratto è costosa, quindi meno la modifichiamo, meglio è. Puoi creare delle mappature che supportano l'iterazione, ma richiedono memoria aggiuntiva per un elenco di chiavi. In gran parte delle applicazioni, non ne hai bisogno.
1 event PairCreated(address indexed token0, address indexed token1, address pair, uint);2Copia
Questo evento è emesso quando viene creato un nuovo scambio in pari. Include gli indirizzi dei token, l'indirizzo dello scambio in pari e il numero totale di scambi gestiti dalla factory.
1 constructor(address _feeToSetter) public {2 feeToSetter = _feeToSetter;3 }4Copia
La sola cosa che fa il costruttore è specificare feeToSetter. Le factory iniziano senza una commissione e solo feeSetter può modificare questo punto.
1 function allPairsLength() external view returns (uint) {2 return allPairs.length;3 }4Copia
Questa funzione restituisce il numero di scambi in pari.
1 function createPair(address tokenA, address tokenB) external returns (address pair) {2Copia
Questa è la funzione principale della factory, per creare uno scambio in pari tra due token ERC-20. Nota che chiunque può chiamare questa funzione. Non occorre l'autorizzazione di Uniswap per creare un nuovo scambio in pari.
1 require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');2 (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);3Copia
Vogliamo che l'indirizzo del nuovo scambio sia deterministico, così che sia calcolabile in anticipo al di fuori della catena (questo può essere utile per le transazioni di livello 2). Per farlo dobbiamo avere un ordine coerente degli indirizzi del token, indipendentemente dall'ordine in cui le abbiamo ricevute, per cui le smistiamo qui.
1 require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');2 require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient3Copia
I pool di liquidità di grandi dimensioni sono meglio rispetto a quelli piccoli, in quanto hanno prezzi più stabili. Non vogliamo avere più di un solo pool di liquidità per coppia di token. Se esiste già uno scambio, non serve crearne un altro per la stessa coppia.
1 bytes memory bytecode = type(UniswapV2Pair).creationCode;2Copia
Per creare un nuovo contratto ci serve il codice che lo crea (sia la funzione del costruttore sia il codice che scrive sulla memoria il bytecode dell'EVM del contratto reale). Normalmente, in Solidity, è sufficiente usare addr = new <name of contract>(<constructor parameters>) e il compilatore pensa a tutto il resto, mentre per avere un indirizzo del contratto deterministico, dobbiamo usare l'opcode CREATE2. Quando questo codice è stato scritto, quell'opcode non era ancora supportato da Solidity, quindi occorreva ottenere manualmente il codice. Questo aspetto non è più un problema, perché ora Solidity supporta CREATE2.
1 bytes32 salt = keccak256(abi.encodePacked(token0, token1));2 assembly {3 pair := create2(0, add(bytecode, 32), mload(bytecode), salt)4 }5Copia
Quando un opcode non è ancora supportato da Solidity, possiamo chiamarlo usando l'assemblaggio in linea.
1 IUniswapV2Pair(pair).initialize(token0, token1);2Copia
Chiama la funzione initialize per dire al nuovo scambio quale coppia di token scambiare.
1 getPair[token0][token1] = pair;2 getPair[token1][token0] = pair; // populate mapping in the reverse direction3 allPairs.push(pair);4 emit PairCreated(token0, token1, pair, allPairs.length);5 }6Copia
Salva le informazioni della nuova copia nelle variabili di stato ed emetti un evento per informare tutti dello scambio della nuova coppia.
1 function setFeeTo(address _feeTo) external {2 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');3 feeTo = _feeTo;4 }56 function setFeeToSetter(address _feeToSetter) external {7 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');8 feeToSetter = _feeToSetter;9 }10}11Mostra tuttoCopia
Queste due funzioni consentono a feeSetter di controllare il destinatario della commissione (se presente) e di modificare feeSetter impostando un nuovo indirizzo.
UniswapV2ERC20.sol
Questo contratto implementa il token di liquidità ERC-20. È simile al contratto ERC-20 di OpenWhisk, quindi spiegherò solo la parte diversa, la funzionalità permit.
Le transazioni su nexus costano ether (ETH), equivalente al denaro reale. Se hai token ERC-20 ma non ETH, non puoi inviare transazioni, quindi non puoi farci nulla. Una soluzione per evitare questo problema sono le meta-transazioni. Il proprietario dei token firma una transazione che consenta ad altri di prelevare i token al di fuori della catena e la invia al destinatario usando Internet. Il destinatario, che ha ETH a disposizione, invia il permesso per conto del proprietario.
1 bytes32 public DOMAIN_SEPARATOR;2 // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");3 bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;4Copia
Questo hash è l'identificativo per il tipo di transazione. Il solo che supportiamo qui è Permit con questi parametri.
1 mapping(address => uint) public nonces;2Copia
Un destinatario non può falsificare una firma digitale. Tuttavia, un'azione piuttosto banale consiste nell'inviare la stessa transazione due volte (è una forma di replay attack). Per impedirlo, usiamo un nonce. Se il nonce di un nuovo Permit non è uno in più dell'ultimo usato, supponiamo che non sia valido.
1 constructor() public {2 uint chainId;3 assembly {4 chainId := chainid5 }6Copia
Questo è il codice per recuperare l'identificativo della catena. Esso utilizza un dialetto d'assemblaggio dell'EVM chiamato Yul. Nota che nella versione corrente di Yul devi usare chainid(), non chainid.
1 DOMAIN_SEPARATOR = keccak256(2 abi.encode(3 keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),4 keccak256(bytes(name)),5 keccak256(bytes('1')),6 chainId,7 address(this)8 )9 );10 }11Mostra tuttoCopia
Calcola il separatore di dominio per EIP-712.
1 function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {2Copia
Questa è la funzione che implementa le autorizzazioni. Riceve come parametri i campi pertinenti e i tre valori scalari per la firma (v, r e s).
1 require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');2Copia
Non accettare le transazioni dopo la scadenza.
1 bytes32 digest = keccak256(2 abi.encodePacked(3 '\x19\x01',4 DOMAIN_SEPARATOR,5 keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))6 )7 );8Copia
abi.encodePacked(...) è il messaggio che ci aspettiamo di ricevere. Sappiamo quale dovrebbe essere il valore del nonce, quindi non serve ottenerlo come parametro.
L'algoritmo di firma di nexus prevede di ottenere 256 bit da firmare, quindi usiamo la funzione di hash keccak256.
1 address recoveredAddress = ecrecover(digest, v, r, s);2Copia
Dal digest e la firma, possiamo ottenere l'indirizzo che lo ha firmato usando ecrecover.
1 require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');2 _approve(owner, spender, value);3 }45Copia
Se è tutto corretto, trattala come un'approvazione di ERC-20.
I contratti periferici
I contratti periferici sono l'API (interfaccia del programma applicativo) per Uniswap. Sono disponibili per le chiamate esterne, da altri contratti o applicazioni decentralizzate. I contratti principali possono essere chiamati direttamente, ma è più complicato e si rischia di perdere valore in caso di errore. I contratti principali contengono solo test, al fine di evitare truffe, e non controlli di sicurezza per chiunque altro. Questi ultimi sono contenuti nei contratti periferici, in modo da essere aggiornabili all'occorrenza.
UniswapV2Router01.sol
Questo contratto è problematico e non dovrebbe essere più usato. Fortunatamente, i contratti periferici sono privi di stato e non detengono alcuna risorsa, quindi è facile deprecarli e suggerire alle persone di usare una soluzione alternativa, ad esempio UniswapV2Router02.
UniswapV2Router02.sol
In gran parte dei casi puoi usare Uniswap tramite questo contratto. Puoi vedere come usarlo qui.
1pragma solidity =0.6.6;23import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';4import '@uniswap/lib/contracts/libraries/TransferHelper.sol';56import './interfaces/IUniswapV2Router02.sol';7import './libraries/UniswapV2Library.sol';8import './libraries/SafeMath.sol';9import './interfaces/IERC20.sol';10import './interfaces/IWETH.sol';11Mostra tuttoCopia
Gran parte di questi li abbiamo incontrati precedentemente oppure sono piuttosto ovvi. L'unica eccezione è IWETH.sol. Uniswap v2 consente scambi per ogni coppia di token ERC-20, ma l'ether (ETH) stesso non è un token ERC-20. Precede lo standard ed è trasferito da meccanismi unici. Per consentire l'uso di ETH nei contratti che applicano i token ERC-20, è stato escogitato il contratto wrapped ether (WETH). Inviando questo contratto ETH viene coniato un importo equivalente di WETH. Oppure è possibile bruciare WETH per riottenere ETH.
1contract UniswapV2Router02 is IUniswapV2Router02 {2 using SafeMath for uint;34 address public immutable override factory;5 address public immutable override WETH;6Copia
Il router deve sapere quale factory usare e, per le transazioni che richiedono WETH, quale contratto WETH usare. Questi valori sono immutabili, a significare che sono impostabili solo nel costruttore. Questo rassicura gli utenti circa il fatto che nessuno potrebbe modificarli in modo da farli diventare contratti meno onesti.
1 modifier ensure(uint deadline) {2 require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');3 _;4 }5Copia
Questo modificatore si assicura che le transazioni a tempo limitato ("Se puoi, fai X prima di Y") non si verifichino dopo il tempo limite.
1 constructor(address _factory, address _WETH) public {2 factory = _factory;3 WETH = _WETH;4 }5Copia
Il costruttore si limita a impostare le variabili di stato immutabili.
1 receive() external payable {2 assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract3 }4Copia
Questa funzione viene chiamata quando riscattiamo i token dal contratto WETH in ETH. Solo il contratto WETH che utilizziamo è autorizzato a farlo.
Aggiungere liquidità
Queste funzioni aggiungono token allo scambio in pari, il che accresce il pool di liquidità.
12 // **** ADD LIQUIDITY ****3 function _addLiquidity(4Copia
Questa funzione serve a calcolare l'importo di token A e B che dovrebbero essere depositati nello scambio in pari.
1 address tokenA,2 address tokenB,3Copia
Questi sono gli indirizzi dei contratti del token ERC-20.
1 uint amountADesired,2 uint amountBDesired,3Copia
Questi sono gli importi che il fornitore di liquidità vuole depositare. Sono anche gli importi massimi da depositare di A e B.
1 uint amountAMin,2 uint amountBMin3Copia
Questi sono gli importi minimi accettabili da depositare. Se la transazione non può essere effettuata con questi importi o con importi superiori, ripristinala. Se non vuoi questa funzionalità, specifica semplicemente zero.
I fornitori di liquidità specificano un minimo, generalmente perché vogliono limitare la transazione al tasso di cambio vicino a quello corrente. Se il tasso di cambio fluttua troppo, ciò potrebbe essere dovuto a notizie che modificano i valori sottostanti, in tal caso i fornitori di liquidità vogliono poter decidere manualmente come agire.
Per esempio, immagina un caso in cui il tasso di cambio è uno a uno e il fornitore di liquidità specifica questi valori:
| Parametro | Valore |
|---|---|
| amountADesired | 1000 |
| amountBDesired | 1000 |
| amountAMin | 900 |
| amountBMin | 800 |
Finché il tasso di cambio resta tra 0,9 e 1,25, la transazione viene eseguita. Se il tasso di cambio esce dall'intervallo, la transazione viene annullata.
Il motivo per questa precauzione è che le transazioni non sono immediate. Dopo che le hai inviate, a un certo punto un miner le includerà in un blocco (a meno che il tuo prezzo del carburante non sia molto basso, nel qual caso dovrai inviare un'altra transazione con lo stesso nonce e un prezzo del carburante maggiore per sovrascriverla). Non puoi controllare ciò che succede durante l'intervallo tra l'invio e l'inclusione.
1 ) internal virtual returns (uint amountA, uint amountB) {2Copia
La funzione restituisce gli importi che il fornitore di liquidità dovrebbe depositare per avere un rapporto pari al rapporto corrente tra le riserve.
1 // create the pair if it doesn't exist yet2 if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {3 IUniswapV2Factory(factory).createPair(tokenA, tokenB);4 }5Copia
Se non vi è ancora alcuno scambio per questa coppia di token, crealo.
1 (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);2Copia
Ottieni le riserve correnti nella coppia.
1 if (reserveA == 0 && reserveB == 0) {2 (amountA, amountB) = (amountADesired, amountBDesired);3Copia
Se le riserve correnti sono vuote, significa che questo è un nuovo scambio in pari. Gli importi da depositare dovrebbero essere esattamente gli stessi che il fornitore di liquidità vuole fornire.
1 } else {2 uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);3Copia
Se ci serve vedere quali importi saranno, otteniamo l'importo ottimale usando questa funzione. Vogliamo ottenere lo stesso rapporto delle riserve correnti.
1 if (amountBOptimal <= amountBDesired) {2 require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');3 (amountA, amountB) = (amountADesired, amountBOptimal);4Copia
Se amountBOptimal è inferiore all'importo che il fornitore di liquidità vuole depositare, significa che al momento il token B è più prezioso di quanto il depositante della liquidità pensi, quindi è richiesto un importo inferiore.
1 } else {2 uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);3 assert(amountAOptimal <= amountADesired);4 require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');5 (amountA, amountB) = (amountAOptimal, amountBDesired);6Copia
Se l'importo ottimale di B è superiore all'importo di B desiderato, significa che al momento i token sono meno preziosi di quanto il depositante della liquidità pensi, quindi è necessario un importo maggiore. Tuttavia, l'importo desiderato rappresenta un massimo, quindi non possiamo farlo. Calcoliamo invece il numero ottimale di token A per l'importo desiderato di token B.
Mettendo tutto insieme otteniamo questo grafico. Supponiamo che stai cercando di depositare mille token A (riga blu) e mille token B (linea rossa). L'asse delle x è il tasso di cambio, A/B. Se x=1, significa che hanno pari valore e depositi mille unità di ciascuno. Se x=2, A è il doppio del valore di B (ottieni due token B per ogni token A), quindi depositi mille token B, ma solo 500 token A. Se x=0,5, la situazione è invertita, mille token A e cinquecento token B.
1 }2 }3 }4Copia
Potresti depositare la liquidità direttamente nel contratto principale (usando UniswapV2Pair::mint), ma il contratto principale verifica solo che non abbia luogo una truffa, quindi corri il rischio di perdere valore se il tasso di cambio cambia tra quando invii la transazione e quando viene eseguita. Con il contratto periferico verrà calcolato l'importo da depositare, che verrà depositato immediatamente. In questo modo il tasso di cambio non cambia e non perdi nulla.
1 function addLiquidity(2 address tokenA,3 address tokenB,4 uint amountADesired,5 uint amountBDesired,6 uint amountAMin,7 uint amountBMin,8 address to,9 uint deadline10Mostra tuttoCopia
Questa funione può essere chiamata da una transazione per depositare la liquidità. Gran parte dei parametri sono gli stessi di _addLiquidity sopra, con due eccezioni:
. to è l'indirizzo che ottiene la nuova liquidità coniata per mostrare la porzione del pool del fornitore di liquidità. deadline è un limite di tempo sulla transazione.
1 ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {2 (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);3 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);4Copia
Calcoliamo gli importi da depositare realmente e poi troviamo l'indirizzo del pool di liquidità. Per risparmiare sul gas, non ocorre chiedere alla factory, bensì bisogna utilizzare la funzione della libreria pairFor (vedi sotto nelle librerie)
1 TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);2 TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);3Copia
Trasferisci gli importi corretti di token dall'utente nello scambio in pari.
1 liquidity = IUniswapV2Pair(pair).mint(to);2 }3Copia
In cambio, dai i token di liquidità dell'indirizzo to per la proprietà parziale del pool. La funzione mint del contratto principale vede quanti token aggiuntivi ha (rispetto a ciò che aveva l'ultima volta che la liquidità è cambiata) e conia liquidità di conseguenza.
1 function addLiquidityETH(2 address token,3 uint amountTokenDesired,4Copia
Quando un fornitore di liquidità vuole fornire liquidità a uno scambio in pari di Token/ETH, vi sono alcune differenze. Il contratto gestisce l'avvolgimento di ETH per il fornitore di liquidità. Non serve specificare quanti ETH l'utente vuole depositare, perché l'utente li invia insieme alla transazione (l'importo è disponibile in msg.value).
1 uint amountTokenMin,2 uint amountETHMin,3 address to,4 uint deadline5 ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {6 (amountToken, amountETH) = _addLiquidity(7 token,8 WETH,9 amountTokenDesired,10 msg.value,11 amountTokenMin,12 amountETHMin13 );14 address pair = UniswapV2Library.pairFor(factory, token, WETH);15 TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);16 IWETH(WETH).deposit{value: amountETH}();17 assert(IWETH(WETH).transfer(pair, amountETH));18Mostra tuttoCopia
Per depositare l'ETH, il contratto lo avvolge prima in WETH e quindi trasferisce i WETH nella coppia. Nota che il trasferimento è avvolto in un assert. Ciò significa che se il trasferimento fallisce, anche la chiamata di questo contratto fallisce e, dunque, l'avvolgimento non si verifica effettivamente.
1 liquidity = IUniswapV2Pair(pair).mint(to);2 // refund dust eth, if any3 if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);4 }5Copia
L'utente ci ha già inviato l'ETH, quindi se esiste una rimanenza aggiuntiva (poiché l'altro token è meno prezioso di quanto l'utente pensasse), dobbiamo emettere un rimborso.
Rimuovere liquidità
Queste funzioni rimuoveranno la liquidità e ripagheranno il fornitore di liquidità.
1 // **** REMOVE LIQUIDITY ****2 function removeLiquidity(3 address tokenA,4 address tokenB,5 uint liquidity,6 uint amountAMin,7 uint amountBMin,8 address to,9 uint deadline10 ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {11Mostra tuttoCopia
Il caso più semplice di rimozione di liquidità. Esiste un importo minimo di ogni token che il fornitore di liquidità acconsente di accettare e deve verificarsi prima della scadenza.
1 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);2 IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair3 (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);4Copia
La funzione di burn del contratto principale gestisce il rimborso dei token all'utente.
1 (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);2Copia
Quando una funzione restituisce valori multipli, ma siamo interessati solo in alcuni di essi, questo è l'unico modo per ottenere quei valori. In una certa misura è più economico in termini di carburante rispetto a leggere un valore e non usarlo mai.
1 (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);2Copia
Traduci gli importi dal modo in cui il contratto principale li restituisce (prima il token dell'indirizzo inferiore) al modo atteso dall'utente (corrispondenti a tokenA e tokenB).
1 require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');2 require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');3 }4Copia
Va bene effettuare prima il trasferimento e quindi verificare che sia legittimo, perché in caso contrario ripristineremo tutti i cambiamenti di stato.
1 function removeLiquidityETH(2 address token,3 uint liquidity,4 uint amountTokenMin,5 uint amountETHMin,6 address to,7 uint deadline8 ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {9 (amountToken, amountETH) = removeLiquidity(10 token,11 WETH,12 liquidity,13 amountTokenMin,14 amountETHMin,15 address(this),16 deadline17 );18 TransferHelper.safeTransfer(token, to, amountToken);19 IWETH(WETH).withdraw(amountETH);20 TransferHelper.safeTransferETH(to, amountETH);21 }22Mostra tuttoCopia
Rimuovere la liquidità per ETH è quasi la stessa cosa, con la differenza che riceviamo i token WETH e poi li riscattiamo per gli ETH da restituire al fornitore di liquidità.
1 function removeLiquidityWithPermit(2 address tokenA,3 address tokenB,4 uint liquidity,5 uint amountAMin,6 uint amountBMin,7 address to,8 uint deadline,9 bool approveMax, uint8 v, bytes32 r, bytes32 s10 ) external virtual override returns (uint amountA, uint amountB) {11 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);12 uint value = approveMax ? uint(-1) : liquidity;13 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);14 (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);15 }161718 function removeLiquidityETHWithPermit(19 address token,20 uint liquidity,21 uint amountTokenMin,22 uint amountETHMin,23 address to,24 uint deadline,25 bool approveMax, uint8 v, bytes32 r, bytes32 s26 ) external virtual override returns (uint amountToken, uint amountETH) {27 address pair = UniswapV2Library.pairFor(factory, token, WETH);28 uint value = approveMax ? uint(-1) : liquidity;29 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);30 (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);31 }32Mostra tuttoCopia
Queste funzioni utilizzano le meta-transazioni per consentire agli utenti senza ether di prelevare dal pool, usando il meccanismo di permesso.
12 // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) ****3 function removeLiquidityETHSupportingFeeOnTransferTokens(4 address token,5 uint liquidity,6 uint amountTokenMin,7 uint amountETHMin,8 address to,9 uint deadline10 ) public virtual override ensure(deadline) returns (uint amountETH) {11 (, amountETH) = removeLiquidity(12 token,13 WETH,14 liquidity,15 amountTokenMin,16 amountETHMin,17 address(this),18 deadline19 );20 TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));21 IWETH(WETH).withdraw(amountETH);22 TransferHelper.safeTransferETH(to, amountETH);23 }2425Mostra tuttoCopia
Questa funzione è utilizzabile per token aventi commissioni di trasferimento o archiviazione. Quando un token ha tali commissioni, non possiamo utilizzare la funzione removeLiquidity per capire quanti token otterremo indietro, quindi dobbiamo prima prelevare e ottenere il saldo.
123 function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(4 address token,5 uint liquidity,6 uint amountTokenMin,7 uint amountETHMin,8 address to,9 uint deadline,10 bool approveMax, uint8 v, bytes32 r, bytes32 s11 ) external virtual override returns (uint amountETH) {12 address pair = UniswapV2Library.pairFor(factory, token, WETH);13 uint value = approveMax ? uint(-1) : liquidity;14 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);15 amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(16 token, liquidity, amountTokenMin, amountETHMin, to, deadline17 );18 }19Mostra tuttoCopia
La funzione finale combina le commissioni di archiviazione con le meta-transazioni.
Scambio
1 // **** SWAP ****2 // requires the initial amount to have already been sent to the first pair3 function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {4Copia
Questa funzione esegue le elaborazioni interne necessarie per le funzioni esposte ai trader.
1 for (uint i; i < path.length - 1; i++) {2Copia
Mentre scrivo il presente articolo, esistono 388.160 token ERC-20. Se ci fosse stato uno scambio in pari per ogni coppia di token, avremmo oltre 150 miliardi di scambi in pari. L'intera catena, al momento, ha solo lo 0,1% di quel numero di conti. Invece, le funzioni di scambio supportano il concetto di percorso. Il trader A può scambiare A per B, B per C e C per D. In tal modo non serve uno scambio in pari diretto A-D.
I prezzi su questi mercati tendono a essere sincronizzati, perché quando sono desincronizzati si creano opportunità per l'arbitraggio. Immagina, ad esempio, tre token: A, B e C. Ci sono tre scambi in pari, uno per coppia.
- La situazione iniziale
- Un trader vende 24,695 token A e riceve 23,305 token B.
- Il trader vende 24,695 token B per 25,305 token C, mantenendo approssimativamente 0,61 token B come profitto.
- A questo punto il trader vende 24,695 token C per 25,305 token A, mantenendo approssimativamente 0,61 token C come profitto. Il trader ha anche 0,61 token A in più (i 25,305 che finiscono in mano al trader, meno l'investimento originale di 24,695).
| Fase | Scambio A-B | Scambio B-C | Scambio A-C |
|---|---|---|---|
| 1 | A:1000 B:1050 A/B=1,05 | B:1000 C:1050 B/C=1,05 | A:1050 C:1000 C/A=1,05 |
| 2 | A:1024,695 B:1024,695 A/B=1 | B:1000 C:1050 B/C=1,05 | A:1050 C:1000 C/A=1,05 |
| 3 | A:1024,695 B:1024,695 A/B=1 | B:1024,695 C:1024,695 B/C=1 | A:1050 C:1000 C/A=1,05 |
| 4 | A:1024,695 B:1024,695 A/B=1 | B:1024,695 C:1024,695 B/C=1 | A:1024,695 C:1024,695 C/A=1 |
1 (address input, address output) = (path[i], path[i + 1]);2 (address token0,) = UniswapV2Library.sortTokens(input, output);3 uint amountOut = amounts[i + 1];4Copia
Ottieni la coppia che stiamo gestendo, ordinala (per usarla con la coppia) e ottieni l'importo di output previsto.
1 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));2Copia
Ottieni gli importi di output previsti, ordinati nel modo previsto dallo scambio in pari.
1 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;2Copia
Questo è l'ultimo scambio? Se sì, invia i token ricevuti per lo scambio alla destinazione. Altrimenti, inviali al prossimo scambio in pari.
12 IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(3 amount0Out, amount1Out, to, new bytes(0)4 );5 }6 }7Copia
In realtà, chiami lo scambio in pari per scambiare i token. Non ci serve una richiamata sullo scambio, quindi non inviamo alcun byte in quel campo.
1 function swapExactTokensForTokens(2Copia
Questa funzione è usata direttamente dai trader per scambiare un token con un altro.
1 uint amountIn,2 uint amountOutMin,3 address[] calldata path,4Copia
Questo parametro contiene gli indirizzi dei contratti ERC-20. Come spiegato sopra, questo è un array perché potresti avere bisogno di passare attraverso diversi scambi in pari per passare dalla risorsa che hai in mano a quella che desideri.
Un parametro della funzione in Solidity è memorizzabile in memory o in calldata. Se la funzione è un punto d'accesso al contratto, chiamato direttamente da un utente (usando una transazione) o da un contratto diverso, allora il valore del parametro può essere tratto direttamente dai dati della chiamata. Se la funzione è chiamata internamente, come _swap sopra, allora i parametri devono essere memorizzati in memory. Dalla prospettiva del contratto chiamato, calldata è in sola lettura.
Con tipi scalari come uint o address, il compilatore gestisce la scelta dell'archiviazione per noi, mentre con gli array, più lunghi e costosi, specifichiamo il tipo di memoria da usare.
1 address to,2 uint deadline3 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {4Copia
I valori di ritorno sono sempre restituiti in memoria.
1 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);2 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');3Copia
Calcola l'importo da acquistare a ogni scambio. Se il risultato è inferiore al minimo che il trader è disposto ad accettare, ripristina la transazione.
1 TransferHelper.safeTransferFrom(2 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]3 );4 _swap(amounts, path, to);5 }6Copia
Infine, trasferisci il token ERC-20 iniziale al conto per il primo scambio in pari e chiama _swap. Tutto questo accade nell'ambito della stessa transazione, quindi lo scambio in pari sa che ogni token imprevisto è parte di questo trasferimento.
1 function swapTokensForExactTokens(2 uint amountOut,3 uint amountInMax,4 address[] calldata path,5 address to,6 uint deadline7 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {8 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);9 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');10 TransferHelper.safeTransferFrom(11 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]12 );13 _swap(amounts, path, to);14 }15Mostra tuttoCopia
La funzione precedente, swapTokensForTokens, consente a un trader di specificare il numero esatto di token di input che desidera dare e il numero minimo di token di output che vuole ricevere in cambio. Questa funzione effettua lo scambio inverso, consentendo al trader di specificare il numero di token in uscita che vuole ricevere e il numero massimo di token in entrata che è disposto a pagare.
In entrambi i casi, il trader deve dare innanzi tutto a questo contratto periferico un'allowance per consentirgli di effettuare il trasferimento.
1 function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)2 external3 virtual4 override5 payable6 ensure(deadline)7 returns (uint[] memory amounts)8 {9 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');10 amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);11 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');12 IWETH(WETH).deposit{value: amounts[0]}();13 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));14 _swap(amounts, path, to);15 }161718 function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)19 external20 virtual21 override22 ensure(deadline)23 returns (uint[] memory amounts)24 {25 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');26 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);27 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');28 TransferHelper.safeTransferFrom(29 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]30 );31 _swap(amounts, path, address(this));32 IWETH(WETH).withdraw(amounts[amounts.length - 1]);33 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);34 }35363738 function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)39 external40 virtual41 override42 ensure(deadline)43 returns (uint[] memory amounts)44 {45 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');46 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);47 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');48 TransferHelper.safeTransferFrom(49 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]50 );51 _swap(amounts, path, address(this));52 IWETH(WETH).withdraw(amounts[amounts.length - 1]);53 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);54 }555657 function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)58 external59 virtual60 override61 payable62 ensure(deadline)63 returns (uint[] memory amounts)64 {65 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');66 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);67 require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');68 IWETH(WETH).deposit{value: amounts[0]}();69 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));70 _swap(amounts, path, to);71 // refund dust eth, if any72 if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);73 }74Mostra tuttoCopia
Queste quattro varianti comportano tutte lo scambio tra ETH e token. La sola differenza è che riceviamo ETH dal trader e lo usiamo per coniare WETH o ricviamo WETH dall'ultimo scambio nel percorso e li bruciamo, reinviando al trader l'ETH risultante.
1 // **** SWAP (supporting fee-on-transfer tokens) ****2 // requires the initial amount to have already been sent to the first pair3 function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {4Copia
Questa è la funzione interna per scambiare token che hanno commissioni di trasferimento o archiviazione da risolvere (questo problema).
1 for (uint i; i < path.length - 1; i++) {2 (address input, address output) = (path[i], path[i + 1]);3 (address token0,) = UniswapV2Library.sortTokens(input, output);4 IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));5 uint amountInput;6 uint amountOutput;7 { // scope to avoid stack too deep errors8 (uint reserve0, uint reserve1,) = pair.getReserves();9 (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);10 amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);11 amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);12Mostra tuttoCopia
A causa delle commissioni di trasferimento, non possiamo fare affidamento sulla funzione getAmountsOut per capire quanto otteniamo da ogni trasferimento (come facciamo prima di chiamare lo _swap originale). Dobbiamo prima trasferire e poi vedere quanti token riceviamo indietro.
Nota: In teoria, potremmo semplicemente usare questa funzione al posto di _swap, ma in certi casi (ad esempio, se il trasferimento viene ripristinato perché al termine dell'operazione non vi è abbastanza per soddisfare il minimo richiesto) finiremmo per spendere più carburante. I token della commissione di trasferimento sono abbastanza rari; di conseguenza, benché sia necessario accomodarli, non serve effettuare tutti gli scambi per supporre che passimo attraverso almeno uno di essi.
1 }2 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));3 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;4 pair.swap(amount0Out, amount1Out, to, new bytes(0));5 }6 }789 function swapExactTokensForTokensSupportingFeeOnTransferTokens(10 uint amountIn,11 uint amountOutMin,12 address[] calldata path,13 address to,14 uint deadline15 ) external virtual override ensure(deadline) {16 TransferHelper.safeTransferFrom(17 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn18 );19 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);20 _swapSupportingFeeOnTransferTokens(path, to);21 require(22 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,23 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'24 );25 }262728 function swapExactETHForTokensSupportingFeeOnTransferTokens(29 uint amountOutMin,30 address[] calldata path,31 address to,32 uint deadline33 )34 external35 virtual36 override37 payable38 ensure(deadline)39 {40 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');41 uint amountIn = msg.value;42 IWETH(WETH).deposit{value: amountIn}();43 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));44 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);45 _swapSupportingFeeOnTransferTokens(path, to);46 require(47 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,48 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'49 );50 }515253 function swapExactTokensForETHSupportingFeeOnTransferTokens(54 uint amountIn,55 uint amountOutMin,56 address[] calldata path,57 address to,58 uint deadline59 )60 external61 virtual62 override63 ensure(deadline)64 {65 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');66 TransferHelper.safeTransferFrom(67 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn68 );69 _swapSupportingFeeOnTransferTokens(path, address(this));70 uint amountOut = IERC20(WETH).balanceOf(address(this));71 require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');72 IWETH(WETH).withdraw(amountOut);73 TransferHelper.safeTransferETH(to, amountOut);74 }75Mostra tuttoCopia
Queste sono le stesse varianti usate per i token normali, che però chiamano _swapSupportingFeeOnTransferTokens.
1 // **** LIBRARY FUNCTIONS ****2 function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {3 return UniswapV2Library.quote(amountA, reserveA, reserveB);4 }56 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)7 public8 pure9 virtual10 override11 returns (uint amountOut)12 {13 return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);14 }1516 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)17 public18 pure19 virtual20 override21 returns (uint amountIn)22 {23 return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);24 }2526 function getAmountsOut(uint amountIn, address[] memory path)27 public28 view29 virtual30 override31 returns (uint[] memory amounts)32 {33 return UniswapV2Library.getAmountsOut(factory, amountIn, path);34 }3536 function getAmountsIn(uint amountOut, address[] memory path)37 public38 view39 virtual40 override41 returns (uint[] memory amounts)42 {43 return UniswapV2Library.getAmountsIn(factory, amountOut, path);44 }45}46Mostra tutto