Guida dettagliata al contratto Uniswap-v2
Introduzione
Uniswap v2(opens in a new tab) può creare un mercato di scambio tra due token ERC-20. In questo articolo guarderemo al codice sorgente per i contratti che implementano questo protocollo e vedremo perché sono scritti così.
Cosa fa Uniswap?
Fondamentalmente, esistono due tipi di utenti: fornitori di liquidità e trader.
I fornitori di liquidità forniscono il pool con i due token scambiabili (li chiameremo Token0 e Token1). In cambio, ricevono un terzo token rappresentante la proprietà parziale del pool, detto token di liquidità.
I trader inviano un tipo di token al pool e ricevono l'altro (ad esempio, inviano Token0 e ricevono 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à vogliono restituite le proprie risorse, possono bruciare i token del pool e riprendersi i propri token, inclusa la propria quota delle ricompense.
Fai clic qui per una descrizione completa(opens in a new tab).
Perché v2? Perché non v3?
Uniswap v3(opens in a new tab) è un aggiornamento molto più complicato della v2. È 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 un'indennità nell'importo da scambiare al conto periferico.
- Chiama una delle tante funzioni di scambio del contratto periferico (che dipende dal fatto che ETH sia o meno coinvolto, se il trader specifica l'importo di token da depositare o l'importo di token da riprendere, etc.). 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
swap
dello scambio. In gran parte dei casi, l'indirizzo di destinazione per i token è lo scambio in pari successivo nel percorso. Nello scambio finale è presente 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
_update
per 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 un'indennità negli importi da aggiungere al pool di liquidità al conto periferico.
- 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 di token nuovi a quelli esistenti.
- Controlla se gli importi sono accettabili (i chiamanti possono specificare un importo minimo al di sotto del quale preferiscono non aggiungere liquidità)
- Chiama il contratto principale.
Nel contratto principale (UniswapV2Pair.sol)
- Conia token di liquditià e li invia al chiamante
- Chiama
_update
per aggiornare gli importi della riserva
Rimuovere la liquidità
Chiamante
- Fornisce un'indennità dei token di liquidità da bruciare in cambio dei token sottostanti al conto periferico.
- 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 1000 token A nel pool, 500 token B e 90 token di liquidità e riceviamo 9 token da bruciare, stiamo bruciando il 10% dei token di liquidità, quindi restituiamo all'utente 100 token A e 50 token B.
- Brucia i token di liquidità
- Chiama
_update
per aggiornare gli importi della riserva
I contratti principali
Questi sono i contratti sicuri che detengono la liquidità.
UniswapV2Pair.sol
Questo contratto(opens in a new tab) 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';Mostra tuttoCopia
Queste sono tutte le interfacce che il contratto deve conoscere, perché il contratto le implementa (IUniswapV2Pair
e UniswapV2ERC20
) o perché chiama dei contratti per implementarle.
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {Copia
Questo contratto eredita da UniswapV2ERC20
, che fornisce le funzioni di ERC-20 per i token di liquidità.
1 using SafeMath for uint;Copia
La libreria SafeMath(opens in a new tab) è usata per evitare overflow e underflow. Questo è 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;Copia
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 da 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;Copia
Per evitare i casi di divisione per zero, esiste un numero minimo di token di liquidità sempre esistenti (ma posseduti dal conto zero). Quel numero è MINIMUM_LIQUIDITY, mille.
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));Copia
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;Copia
Questo è il contratto factory che ha creato questo pool. Ogni pool è uno scambio tra due token ERC-20, la factory è il punto centrale che connette tutti questi pool.
1 address public token0;2 address public token1;Copia
Esistono degli indirizzi dei contratti per i 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 getReservesCopia
Le riserve che il pool ha per ogni tipo di token. Supponiamo che i due rappresentino la stessa quantità di valore e che, dunque, ogni token0 valga reserve1/reserve0 token1.
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReservesCopia
La marca oraria dall'ultimo blocco in cui si è verificato uno scambio, usata per tracciare i tassi di cambio nel tempo.
Uno dei maggiori consumi di gas dei contratti di Ethereum è l'archiviazione, che persiste da una chiamata del contratto alla successiva. Ogni cella di memoria è lunga 256 bit. Quindi tre variabili, reserve0
, reserve1
e blockTimestampLast
, sono allocate in modo che un singolo valore d'archiviazione possa includerle tutte e tre (112+112+32=256).
1 uint public price0CumulativeLast;2 uint public price1CumulativeLast;Copia
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 dato periodo di tempo.
1 uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity eventCopia
Il modo in cui lo scambio in pari decide sul tasso di cambio tra token0 e token1, è mantenere il multiplo delle due riserve costante 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 contiene solo tre cifre dopo il punto decimale e ignoriamo la commissione di trading dello 0,3%, quindi i numeri non sono esatti.
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;Copia
Esiste una classe di vulnerabilità di sicurezza basate sull'abuso di rientranza(opens in a new tab). 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
come parte del contratto, possiamo impedire alle funzioni di essere chiamate mentre sono in esecuzione (all'interno della stessa transazione).
1 modifier lock() {Copia
Questa funzione è un modificatore(opens in a new tab), una funzione che avvolge una funzione normale per cambiarne il comportamento in qualche modo.
1 require(unlocked == 1, 'UniswapV2: LOCKED');2 unlocked = 0;Copia
Se unlocked
è pari a uno, impostalo su zero. Se è già zero, ripristina la chiamata, facendola fallire.
1 _;Copia
In un modificatore _;
è la chiamata alla funzione originale (con tutti i parametri). Questo significa che la chiamata alla funzione si verifica solo se unlocked
era pari a uno al momento della chiamata, e se mentre è in esecuzione il valore di unlocked
è zero.
1 unlocked = 1;2 }Copia
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 }Copia
Questa funzione fornisce ai chiamanti lo stato corrente dello scambio. Nota che le funzioni di Solidity possono restituire valori multipli(opens in a new tab).
1 function _safeTransfer(address token, address to, uint value) private {2 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));Copia
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(opens in a new tab).
1 require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');2 }Copia
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);Copia
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 sono parte dell'evento, così come l'identità del conto chiamante (sender
). Nel caso di un prelievo, l'evento include anche la destinazione che ha ricevuto i token (to
), che potrebbe non essere identica 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 );Copia
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);Copia
Infine, Sync
è emesso ogni volta che i token sono aggiunti o prelevati, indipendentemente dal motivo, per fornire le ultime informazioni sulla riserva (e dunque sul 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 }Copia
Il costruttore si assicura che terremo traccia dell'indirizzo della factory che ha creato la coppia. Queste informazioni sono richieste per initialize
e per la commissione della factory (se esistente)
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 }Copia
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 {Copia
Questa funzione è chiamata ogni volta che i token vengono depositati o prelevati.
1 require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');Copia
Se balance0 o balance1 (uint256) è superiore a uint112(-1) (=2^112-1) (quindi se eccede e torna a 0 quando convertito in uint112) si rifiuta di continuare lo _update per impedire 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) {Copia
Se il tempo trascorso è diverso da zero, significa che la nostra è la prima transazione di scambio su questo blocco. In quel caso, dobbiamo aggiornare gli accumulatori di 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 }Copia
Ogni accumulatore di costo è aggiornato con l'ultimo costo (riserva dell'altro token/riserva di questo token), moltiplicato per il tempo passato in secondi. Per avere un prezzo medio, si prende il prezzo cumulativo in due punti nel tempo e si divide per la differenza di tempo tra questi. 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 }Copia
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) {Copia
In Uniswap 2.0 i trader pagano una commissione dello 0,30% per usare il mercato. Gran parte di questa commissione (0,25% dello scambio) 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 i suoi sforzi di sviluppo.
Per ridurre i calcoli (e dunque i costi del gas), questa commissione è calcolata solo quando la liquidità è aggiunta o rimossa dal pool, piuttosto che a ogni transazione.
1 address feeTo = IUniswapV2Factory(factory).feeTo();2 feeOn = feeTo != address(0);Copia
Leggi la destinazione della commissione della factory. Se è zero, allora non vi è alcuna commissione di protocollo e non serve calcolarla.
1 uint _kLast = kLast; // gas savingsCopia
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, rilasciato quando la chiamata al contratto della funzione termina, quindi utilizziamo una variabile interna per risparmiare sul gas.
1 if (feeOn) {2 if (_kLast != 0) {Copia
I fornitori di liquidità ottengono la loro parte semplicemente mediante l'apprezzamento dei loro token di liquidità. Ma la commissione di protocollo richiede di coniare e fornire nuovi token di liquidità all'indirizzo feeTo
.
1 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));2 uint rootKLast = Math.sqrt(_kLast);3 if (rootK > rootKLast) {Copia
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;Copia
Questo complicato calcolo delle commissioni è spiegato nel whitepaper(opens in a new tab) a pagina 5. Sappiamo che, tra il momento del calcolo di kLast
e il presente, non è stata aggiunta o rimossa alcuna liquidità (perché eseguiamo questo calcolo ogni volta che la liquidità è aggiunta o rimossa, prima che cambi realmente), quindi ogni modifica in reserve0 * reserve1
deve provenire dalle commissioni di transazione (senza di esse manterremmo reserve0 * reserve1
costante).
1 if (liquidity > 0) _mint(feeTo, liquidity);2 }3 }Copia
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 }Copia
Se non c'è alcuna commissione con kLast
impostato a zero (se non è già così). Alla scrittura di questo contratto, era presente una funzionalità di rimborso del gas(opens in a new tab), che incoraggiava i contratti a ridurre le dimensioni complessive dello stato di Ethereum, azzerando l'archiviazione non necessaria. Questo codice ottiene quel rimborso, se possibile.
Funzioni accessibili esternamente
Nota che, sebbene ogni transazione o contratto possa chiamare queste funzioni, 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) {Copia
Questa funzione viene chiamata quando un fornitore di liquidità aggiunge liquidità al pool. Conia token di liquidità aggiuntivi come premio. Dovrebbe essere chiamata da un contratto periferico che la chiami dopo aver aggiunto la liquidità alla stessa transazione (così che nessun altro potrebbe inviare una transazione che rivendichi nuova liquidità prima del legittimo proprietario).
1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savingsCopia
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 li necessitiamo.
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);Copia
Ottiene i saldi correnti e vede quanto è stato aggiunto di ogni tipo di token.
1 bool feeOn = _mintFee(_reserve0, _reserve1);Copia
Calcola le commissioni di protocollo da raccogliere, se presenti, e conia token di liquidità di conseguenza. Poiché i parametri relativi a _mintFee
sono i vecchi valori della riserva, la commissione è calcolata accuratamente solo secondo le modifiche al pool dovute alle 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 tokensCopia
Se questo è il primo deposito, crea MINIMUM_LIQUIDITY
token e inviali all'indirizzo zero per bloccarli. Non possono essere riscattati, 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 che, considerando che gran parte dell'ERC-20 sia suddiviso in unità di 10^-18 di un token, poiché ETH è diviso in wei, equivale a 10^-15 del valore di un singolo token. Non è quindi un costo elevato.
Al momento del primo deposito non conosciamo il valore relativo dei due token, quindi moltiplichiamo semplicemente 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 una quantità di Token1 quattro volte superiore rispetto a quella di 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);Copia
A ogni deposito successivo, conosciamo già il tasso di cambio tra le due risorse e prevediamo che i fornitori di liquidità forniranno un valore pari per entrambe. Altrimenti, diamo loro token di liquidità in base al valore inferiore che hanno fornito, a titolo di punizione.
Sia nel deposito iniziale sia in 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 (a meno che non otteniamo un deposito che non ha valori pari di entrambi i tipi, nel qual caso viene distribuita la "multa"). Ecco un altro esempio con due token aventi lo stesso valore, con tre depositi validi e uno non valido (deposito di solo un 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);Copia
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 }Copia
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) {Copia
Questa funzione viene chiamata quando la liquidità è prelevata e i token di liquidità appropriati devono essere bruciati. Anch'essa dovrebbe essere chiamata 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)];Copia
Il contratto periferico ha trasferito la liquidità da bruciare a questo contratto prima della chiamata. Così sappiamo quanta liquidità bruciare e possiamo assicurarci 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');Copia
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 }11Mostra 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 {Copia
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 errorsCopia
Le variabili locali sono memorizzabili in memoria o, se sono troppe, direttamente sullo stack. Se possiamo limitare il numero, così da usare lo stack, consumeremo meno gas. Per ulteriori dettagli vedi lo yellow paper, le specifiche formali di Ethereum(opens in a new tab), 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 tokensCopia
Questo trasferimento è ottimistico, perché trasferiamo prima di essere sicuri che tutte le condizioni siano soddisfatte. Questo va bene su Ethereum perché se le condizioni non sono soddisfatte in seguito nella chiamata, ripristiniamo allo stato prima di esso ed eventuali modifiche che ha creato.
1 if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);Copia
Informa il ricevente dello scambio, se richiesto.
1 balance0 = IERC20(_token0).balanceOf(address(this));2 balance1 = IERC20(_token1).balanceOf(address(this));3 }Copia
Ottieni i saldi correnti. Il contratto periferico ci invia i token prima di chiamarci per lo scambio. Questo rende più semplice al contratto verificare che non sia oggetto di truffe, un controllo che deve verificarsi nel contratto principali (perché possiamo essere chiamati da altre entità oltre al 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');Copia
Questo è un controllo di sicurezza, per assicurarsi di non perdere in seguito allo scambio. Non vi è 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 }Copia
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 qualsiasi conto può chiamareskim
, poiché 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}Mostra tuttoCopia
UniswapV2Factory.sol
Questo contratto(opens in a new tab) 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;Copia
Queste variabili di stato sono necessarie per implementare la commissione di protocollo (vedi il whitepaper(opens in a new tab), p. 5). L'indirizzo feeTo
accumula i token di liquidità per la commissione di protocollo e feeToSetter
è l'indirizzo che può modificare feeTo
a un indirizzo differente.
1 mapping(address => mapping(address => address)) public getPair;2 address[] public allPairs;Copia
Queste variabili tracciano le coppie, gli scambi tra due tipi di token.
La prima, getPair
, è una mappatura che identifica un contratto di scambio in pari basato sui due token ERC-20 che scambia. 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 consente di convertire da tokenA
a tokenB
, si usa getPair[<tokenA address>][<tokenB address>]
(o viceversa).
La seconda variabile, allPairs
, è un insieme che include tutti gli indirizzi degli scambi in pari creati da questa factory. In Ethereum, non puoi iterare sul contenuto di una mappatura od ottenere un elenco di tutte le chiavi, quindi questa variabile è il solo 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 ne usiamo, meglio è, e meno spesso la cambiamo, meglio è. Puoi creare delle mappature che supportano l'iterazione(opens in a new tab), ma richiedono archiviazione aggiuntiva per un elenco di chiavi. Nella maggior parte delle applicazioni, non ne hai bisogno.
1 event PairCreated(address indexed token0, address indexed token1, address pair, uint);Copia
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 }Copia
La sola cosa che fa il costruttore è specificare feeToSetter
. Le fabbriche iniziano senza una commissione e solo feeSetter
può modificare questa condizione.
1 function allPairsLength() external view returns (uint) {2 return allPairs.length;3 }Copia
Questa funzione restituisce il numero di scambi in pari.
1 function createPair(address tokenA, address tokenB) external returns (address pair) {Copia
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 servono autorizzazioni da 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);Copia
Vogliamo che l'indirizzo del nuovo scambio sia deterministico, quindi 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 li abbiamo ricevuti, quindi li ordiniamo qui.
1 require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');2 require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficientCopia
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 singolo 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;Copia
Per creare un nuovo contratto necessitiamo del codice che lo crea (sia la funzione del costruttore che il codice che scrive il codice del byte dell'EVM alla memoria del contratto effettivo). Normalmente, in Solidity, usiamo semplicemente addr = new <name of contract>(<constructor parameters>)
e il compilatore si occupa di tutto per noi, ma per avere un indirizzo del contratto deterministico, dobbiamo usare l'opcode CREATE2(opens in a new tab). Alla scrittura di questo codice, quell'opcode non era ancora supportato da Solidity, quindi era necessario ottenerlo manualmente. Questo non è più un problema, perché Solidity supporta ora CREATE2(opens in a new tab).
1 bytes32 salt = keccak256(abi.encodePacked(token0, token1));2 assembly {3 pair := create2(0, add(bytecode, 32), mload(bytecode), salt)4 }Copia
Quando un opcode non è ancora supportato da Solidity, possiamo chiamarlo usando l'assemblaggio in linea(opens in a new tab).
1 IUniswapV2Pair(pair).initialize(token0, token1);Copia
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 }Copia
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}Mostra tuttoCopia
Queste due funzioni consentono a feeSetter
di controllare il destinatario della commissione (se presente) e di modificare feeSetter
a un nuovo indirizzo.
UniswapV2ERC20.sol
Questo contratto(opens in a new tab) implementa il token di liquidità ERC-20. È simile al contratto ERC-20 di OpenZeppelin, quindi spiegherò solo le differenze: la funzionalità permit
.
Le transazioni su Ethereum costano ether (ETH), equivalente al denaro reale. Se hai dei 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(opens in a new tab). Il proprietario dei token firma una transazione che consente a chiunque altro di prelevare token al di fuori della catena e li invia al destinatario tramite 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;Copia
Questo hash è l'identificativo per il tipo di transazione(opens in a new tab). L'unico supportato qui è Permit
con questi parametri.
1 mapping(address => uint) public nonces;Copia
Un destinatario non può falsificare una firma digitale. Tuttavia, è facile inviare due volte la stessa transazione (questa è una forma di attacco replay(opens in a new tab)). Per impedirlo, usiamo un nonce(opens in a new tab). Se il nonce di un nuovo Permit
non è uno in più dell'ultimo usato, presumiamo che non sia valido.
1 constructor() public {2 uint chainId;3 assembly {4 chainId := chainid5 }Copia
Questo è il codice per recuperare l'identificativo della catena(opens in a new tab). Usa un dialetto dell'assembly EVM detto Yul(opens in a new tab). 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 }Mostra tuttoCopia
Calcola il separatore di dominio(opens in a new tab) per EIP-712.
1 function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {Copia
Questa è la funzione che implementa le autorizzazioni. Riceve i campi rilevanti come parametri e i tre valori scalari per la firma(opens in a new tab) (v, r e s).
1 require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');Copia
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 );Copia
abi.encodePacked(...)
è il messaggio che ci aspettiamo di ricevere. Sappiamo quale dovrebbe essere il nonce, quindi non serve che lo otteniamo come un parametro.
L'algoritmo di firma di Ethereum prevede di ottenere 256 bit da firmare, quindi usiamo la funzione di hash keccak256
.
1 address recoveredAddress = ecrecover(digest, v, r, s);Copia
Dal digest e la firma, otteniamo l'indirizzo firmato usando ecrecover(opens in a new tab).
1 require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');2 _approve(owner, spender, value);3 }4Copia
Se è tutto corretto, trattala come un'approvazione di ERC-20(opens in a new tab).
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 dalle applicazioni decentralizzate. Potresti chiamare i contratti principali direttamente, ma è più complicato e potresti perdere del valore se commetti un errore. I contratti principali contengono prove solo per assicurarsi di evitare truffe, non controlli di integrità per chiunque altro. Questi ultimi sono contenuti nei contratti periferici, in modo da essere aggiornabili all'occorrenza.
UniswapV2Router01.sol
Questo contratto(opens in a new tab) presenta dei problemi e non deve più essere usato(opens in a new tab). Fortunatamente, i contratti periferici sono privi di stato e non detengono alcuna risorsa, quindi è facile deprecarli e suggerire alle persone di usare invece il sostituto, UniswapV2Router02
.
UniswapV2Router02.sol
In gran parte dei casi puoi usare Uniswap tramite questo contratto(opens in a new tab). Puoi vedere come usarlo qui(opens in a new tab).
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';Mostra 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 univoci. Per consentire l'uso di ETH nei contratti che si applicano ai token ERC-20, è stato ideato il contratto wrapped ether (WETH)(opens in a new tab). Inviando ETH a questo contratto e 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;Copia
Il router deve sapere quale fabbrica usare e, per le transazioni che richiedono WETH, quale contratto WETH usare. Questi valori sono immutabili(opens in a new tab), a significare che sono impostabili solo nel costruttore. Questo rassicura gli utenti che nessuno possa modificarli per farli puntare a contratti meno onesti.
1 modifier ensure(uint deadline) {2 require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');3 _;4 }Copia
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 }Copia
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 }Copia
Questa funzione viene chiamata quando riscattiamo i token dal contratto WETH in ETH. Solo il contratto WTH che usiamo può farlo.
Aggiungere liquidità
Queste funzioni aggiungono token allo scambio in pari, il che accresce il pool di liquidità.
12 // **** ADD LIQUIDITY ****3 function _addLiquidity(Copia
Questa funzione è usata per calcolare l'importo di token A e B che dovrebbero essere depositati nello scambio in pari.
1 address tokenA,2 address tokenB,Copia
Questi sono gli indirizzi dei contratti del token ERC-20.
1 uint amountADesired,2 uint amountBDesired,Copia
Questi sono gli importi che il fornitore di liquidità vuole depositare. Sono inoltre gli importi massimi di A e B da depositare.
1 uint amountAMin,2 uint amountBMinCopia
Questi sono gli importi minimi accettabili da depositare. Se la transazione non può avvenire con questi importi o con importi superiori, si ripristina. Se non vuoi questa funzionalità, specifica semplicemente zero.
I fornitori di liquidità specificano un minimo, solitamente perché vogliono limitare la transazione a un tasso di cambio vicino a quello corrente. Se il tasso di cambio fluttua troppo, potrebbe indicare novità che modificano i valori sottostanti, e vogliono quindi decidere manualmente cosa fare.
Ad 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 da quell'intervallo, la transazione viene annullata.
Il motivo di questa precauzione è che le transazioni non sono immediate: le invii e, prima o poi, un validatore le includerà in un blocco (a meno che il tuo prezzo del gas non sia molto basso, nel qual caso dovrai inviare un'altra transazione con lo stesso nonce e un prezzo del gas maggiore, per sovrascriverla). Non puoi controllare cosa succede nel periodo di tempo tra l'invio e l'inclusione.
1 ) internal virtual returns (uint amountA, uint amountB) {Copia
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 }Copia
Se non vi è ancora alcuno scambio per questa coppia di token, crealo.
1 (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);Copia
Ottieni le riserve correnti nella coppia.
1 if (reserveA == 0 && reserveB == 0) {2 (amountA, amountB) = (amountADesired, amountBDesired);Copia
Se le riserve correnti sono vuote, significa che questo è un nuovo scambio in pari. Gli importi da depositare dovrebbero essere esattamente gli stessi di quelli che il fornitore di liquidità desidera fornire.
1 } else {2 uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);Copia
Se dobbiamo vedere quali importi saranno, otteniamo l'importo ottimale usando questa funzione(opens in a new tab). 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);Copia
Se amountBOptimal
è inferiore all'importo che il fornitore di liquidità desidera depositare, significa che il token B ha in questo momento un valore superiore di quanto pensi il depositante di liquidità, quindi è necessario 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);Copia
Se l'importo B ottimale è superiore all'importo B desiderato, significa che i token B hanno un valore correntemente inferiore a quanto il depositante di liquidità pensi, quindi è necessario un importo maggiore. Ma 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 di provare a depositare mille token A (riga blu) e mille token B (riga rossa). L'asse delle x è il tasso di cambio, A/B. Se x=1, sono pari in valore e ne depositi mille 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.
Potresti depositare la liquidità direttamente nel contratto principale (usando UniswapV2Pair::mint(opens in a new tab)), ma il contratto principale effettua solo un controllo per evitare le truffe, quindi corri il rischio di perdere valore se il tasso di cambio cambia tra il momento in cui invii la tua transazione e quando è eseguita. Se usi il contratto periferico, esso identifica l'importo che dovresti depositare e lo deposita immediatamente, così 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 deadlineMostra 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);Copia
Calcoliamo gli importi da depositare realmente e poi troviamo l'indirizzo del pool di liquidità. Per risparmiare sul gas, non lo facciamo chiedendo alla factory, ma usando la funzione della libreria pairFor
(vedi sotto, in librerie)
1 TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);2 TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);Copia
Trasferisci gli importi corretti di token dall'utente nello scambio in pari.
1 liquidity = IUniswapV2Pair(pair).mint(to);2 }Copia
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 quanto aveva all'ultimo cambiamento di liquidità) e conia liquidità di conseguenza.
1 function addLiquidityETH(2 address token,3 uint amountTokenDesired,Copia
Quando un fornitore di liquidità vuole fornire liquidità a uno scambio in pari di Token/ETH, vi sono alcune differenze. Il contratto gestisce il wrapping di ETH per il fornitore di liquidità. Non serve specificare quanti ETH l'utente desidera depositare, perché l'utente li invia semplicemente con la 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));Mostra tuttoCopia
Per depositare l'ETH, il contratto lo avvolge prima in WETH e quindi trasferisce i WETH nella coppia. Nota che il trasferimento è wrapped in un assert
. Questo significa che se il trasferimento fallisce, anche la chiamata di questo contratto fallisce e dunque, il wrapping non si verifica.
1 liquidity = IUniswapV2Pair(pair).mint(to);2 // refund dust eth, if any3 if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);4 }Copia
L'utente ci ha già inviato gli ETH, quindi se ne avanzano (perché l'altro token ha un valore inferiore 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) {Mostra tuttoCopia
Il caso più semplice di rimozione di liquidità. Esiste un importo minimo di ogni token che il fornitore di liquidità acconsente ad accettare e che 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);Copia
La funzione di burn
del contratto principale gestisce il rimborso dei token all'utente.
1 (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);Copia
Quando una funzione restituisce diversi valori, ma siamo interessati a solo alcuni di essi, questo è l'unico modo per ottenere questi valori. In una certa misura è più economico in termini di gas, piuttosto che leggere un valore senza mai utilizzarlo.
1 (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);Copia
Tradurre gli importi da come sono restituiti dal contratto principale (token dell'indirizzo inferiore per primo) al modo in cui l'utente se li aspetta (corrispondenti a tokenA
e tokenB
).
1 require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');2 require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');3 }Copia
Va bene eseguire prima il trasferimento e poi verificare che sia legittimo, perché, altrimenti, 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 }Mostra tuttoCopia
Rimuovere liquidità per ETH è quasi la stessa cosa, tranne per il fatto che riceviamo i token WETH e poi li riscattiamo in ETH per restituirli 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 }Mostra tuttoCopia
Queste funzioni ritrasmettono le meta-transazioni per consentire agli utenti di prelevare dal pool, usando il meccanismo del 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 }24Mostra tuttoCopia
Questa funzione è utilizzabile per token aventi commissioni di trasferimento o archiviazione. Quando un token ha tali commissioni, non possiamo affidarci alla funzione removeLiquidity
affinché ci dica quanti token riceviamo indietro, quindi dobbiamo prima prelevare e poi 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 }Mostra 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 {Copia
Questa funzione esegue l'elaborazione interna necessaria per le funzioni esposte ai trader.
1 for (uint i; i < path.length - 1; i++) {Copia
Mentre scrivo il presente articolo, esistono 388.160 token ERC-20(opens in a new tab). Se ci fosse stato uno scambio in pari per ogni coppia di token, avremmo avuto oltre 150 miliardi di scambi in pari. L'intera catena, al momento, contiene solo lo 0,1% di quel numero di conti(opens in a new tab). Invece, le funzioni di scambio supportano il concetto di percorso. Un trader può scambiare A per B, B per C e C per D, quindi non serve uno scambio in pari A-D diretto.
I prezzi su questi mercati tendono a essere sincronizzati, perché quando non sono sincronizzati, si crea un'opportunità d'arbitraggio. Immagina, ad esempio, tre token, A, B e C. Esistono tre scambi in pari, uno per ogni 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.
- Poi il trader vende 24,695 token C per 23,305 token A, mantenendo approssimativamente 0,61 token C come profitto. Il trader ha anche 0,61 token A in più (i 25,305 con cui il trader finisce, 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];Copia
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));Copia
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;Copia
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 }Copia
In realtà, chiami lo scambio in pari per scambiare i token. Non serve una richiamata sullo scambio, quindi non inviamo alcun byte a quel campo.
1 function swapExactTokensForTokens(Copia
Questa funzione è usata direttamente dai trader per scambiare un token con un altro.
1 uint amountIn,2 uint amountOutMin,3 address[] calldata path,Copia
Questo parametro contiene gli indirizzi dei contratti ERC-20. Come spiegato sopra, è un array perché potresti dover passare per diversi scambi in pari per andare dalla risorsa che hai 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, chiamata direttamente da un utente (usando una transazione) o da un contratto differente, allora il valore del parametro può essere preso direttamente dai dati della chiamata. Se la funzione è chiamata internamente, come la suddetta _swap
, allora i parametri devono essere memorizzati in memory
. Dalla prospettiva del contratto chiamato, calldata
è in sola lettura.
Con i tipi scalari come uint
o address
, il compilatore gestisce la scelta d'archiviazione per noi ma con gli array, che sono più lunghi e costosi, specifichiamo il tipo di archiviazione da usare.
1 address to,2 uint deadline3 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {Copia
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');Copia
Calcola l'importo da acquistare a ogni scambio. Se il risultato è inferiore al minimo che il trader desidera 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 }Copia
Infine, trasferisci il token ERC-20 iniziale al conto per il primo scambio in pari e chiama _swap
. Tutto ciò si verifica nella 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 }Mostra tuttoCopia
La funzione precedente, swapTokensForTokens
, consente a un trader di specificare un numero esatto di token di input che desidera dare e il numero minimo di token di output che desidera ricevere in cambio. Questa funzione effettua lo scambio inverso, consente a un trader di specificare il numero di token di output che desidera e il numero massimo di token di input che desidera pagare per essi.
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 }Mostra tuttoCopia
Queste quattro varianti comportano tutte lo scambio tra ETH e token. La sola differenza è che riceviamo ETH dal trader e li usiamo per coniare WETH, o riceviamo WETH dall'ultimo scambio nel percorso e li bruciamo, reinviando al trader gli ETH risultanti.
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 {Copia
Questa è la funzione interna per scambiare i token che hanno commissioni di trasferimento o archiviazione da risolvere (questo problema(opens in a new tab)).
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);Mostra tuttoCopia
A causa delle commissioni di trasferimento non possiamo chiedere alla funzione getAmountsOut
di dirci quanto otteniamo da ogni trasferimento (come facciamo prima di chiamare l'originale _swap
). Al contrario, dobbiamo prima trasferire e poi vedere quanti token riprendiamo.
Nota: In teoria, potremmo semplicemente usare questa funzione invece di _swap
, ma in certi casi (ad esempio, se il trasferimento è ripristinato perché non c'è abbastanza gas al termine, per soddisfare il minimo richiesto), questa finirebbe per costare più gas. I token della commissione di trasferimento sono piuttosto rari, quindi pur dovendoli prevedere, non vi è bisogno di presupporre che tutti gli scambi passino per 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 }Mostra 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}Mostra tuttoCopia
Queste funzioni sono solo proxy che chiamano le funzioni UniswapV2Library.
UniswapV2Migrator.sol
Questo contratto era usato per migare gli scambi dalla vecchia v1 alla v2. Ora che sono stati migrati, non è più rilevante.
Le librerie
La libreria di SafeMath(opens in a new tab) è ben documentata, quindi non serve documentarla qui.
Math
Questa libreria contiene alcune funzioni matematiche che normalmente non sono necessarie nel codice di Solidity e che quindi non fanno parte del linguaggio.
1pragma solidity =0.5.16;23// a library for performing various math operations45library Math {6 function min(uint x, uint y) internal pure returns (uint z) {7 z = x < y ? x : y;8 }910 // babylonian method (https://wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)11 function sqrt(uint y) internal pure returns (uint z) {12 if (y > 3) {13 z = y;14 uint x = y / 2 + 1;Mostra tuttoCopia
Inizia con x come una stima che è superiore alla radice quadrata (questo è il motivo per cui dobbiamo trattare 1-3 come casi speciali).
1 while (x < z) {2 z = x;3 x = (y / x + x) / 2;Copia
Ottieni una stima più vicina, la media della stima precedente e il numero di cui stiamo provando a trovare la radice quadrata, diviso per la stima precedente. Ripeti finché la nuova stima non è inferiore a quella esistente. Per ulteriori dettagli, vedi qui(opens in a new tab).
1 }2 } else if (y != 0) {3 z = 1;Copia
Non dovremmo mai avere bisogno della radice quadrata di zero. Le radici quadrate di uno, due e tre sono approssimativamente uno (usiamo gli interi, quindi ignoriamo la frazione).
1 }2 }3}Copia
Frazioni a punto fisso (UQ112x112)
Questa libreria gestisce le frazioni, che normalmente non sono parte dell'aritmetica di Ethereum. Lo fa codificando il numero x come x*2^112. Questo ci permette di usare gli opcode di addizione e sottrazione originali senza alcuna modifica.
1pragma solidity =0.5.16;23// a library for handling binary fixed point numbers (https://wikipedia.org/wiki/Q_(number_format))45// range: [0, 2**112 - 1]6// resolution: 1 / 2**11278library UQ112x112 {9 uint224 constant Q112 = 2**112;Mostra tuttoCopia
Q112
è la codifica per uno.
1 // encode a uint112 as a UQ112x1122 function encode(uint112 y) internal pure returns (uint224 z) {3 z = uint224(y) * Q112; // never overflows4 }Copia
Poiché y è uint112
, il suo valore massimo può essere 2^112-1. Quel numero è ancora codificabile come un UQ112x112
.
1 // divide a UQ112x112 by a uint112, returning a UQ112x1122 function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {3 z = x / uint224(y);4 }5}Copia
Se dividiamo due valori UQ112x112
, il risultato non è più moltiplicato per 2^112. Quindi, invece, prendiamo un intero come denominatore. Avremmo dovuto usare un trucco simile per la moltiplicazione, ma non abbiamo necessità di moltiplicare i valori di UQ112x112
.
UniswapV2Library
Questa libreria è usata solo per i contratti periferici
1pragma solidity >=0.5.0;23import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';45import "./SafeMath.sol";67library UniswapV2Library {8 using SafeMath for uint;910 // returns sorted token addresses, used to handle return values from pairs sorted in this order11 function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {12 require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');13 (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);14 require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');15 }Mostra tuttoCopia
Ordina i due token per indirizzo, in modo da ottenere l'indirizzo dello scambio in pari per gli stessi. Questo è necessario perché altrimenti avremmo due possibilità, una per i parametri A,B e un'altra per i parametri B,A, creando due scambi invece di uno.
1 // calculates the CREATE2 address for a pair without making any external calls2 function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {3 (address token0, address token1) = sortTokens(tokenA, tokenB);4 pair = address(uint(keccak256(abi.encodePacked(5 hex'ff',6 factory,7 keccak256(abi.encodePacked(token0, token1)),8 hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash9 ))));10 }Mostra tuttoCopia
Questa funzione calcola l'indirizzo dello scambio in pari per i due token. Questo contratto è creato usando l'opcode CREATE2(opens in a new tab), quindi possiamo calcolare l'indirizzo usando lo stesso algoritmo, se conosciamo i parametri che utilizza. È molto più economico rispetto a chiedere alla fabbrica.
1 // fetches and sorts the reserves for a pair2 function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {3 (address token0,) = sortTokens(tokenA, tokenB);4 (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();5 (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);6 }Copia
Questa funzione restituisce le riserve dei due token dello scambio in pari. Nota che può ricevere i token in qualsiasi ordine e ordinarli per l'uso interno.
1 // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset2 function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {3 require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');4 require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');5 amountB = amountA.mul(reserveB) / reserveA;6 }Copia
Questa funzione ti da l'importo di token B che riceverai in cambio del token A se non vengono applicate commissioni. Questo calcolo tiene conto che il trasferimento modifica il tasso di cambio.
1 // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset2 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {Copia
La funzione quote
di cui sopra funziona molto bene se non c'è alcuna commissione per utilizzare lo scambio in pari. Tuttavia, se esiste una commissione di cambio dello 0,3%, l'importo che ottieni è in realtà inferiore. Questa funzione calcola l'importo dopo la commissione di scambio.
12 require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');3 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');4 uint amountInWithFee = amountIn.mul(997);5 uint numerator = amountInWithFee.mul(reserveOut);6 uint denominator = reserveIn.mul(1000).add(amountInWithFee);7 amountOut = numerator / denominator;8 }Copia
Solidity non gestisce nativamente le frazioni, quindi non possiamo semplicemente moltiplicare l'importo per 0,997. Invece, moltiplichiamo il numeratore per 997 e il denominatore per 1000, ottenendo lo stesso effetto.
1 // given an output amount of an asset and pair reserves, returns a required input amount of the other asset2 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {3 require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');4 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');5 uint numerator = reserveIn.mul(amountOut).mul(1000);6 uint denominator = reserveOut.sub(amountOut).mul(997);7 amountIn = (numerator / denominator).add(1);8 }Copia
Questa funzione fa approssimativamente la stessa cosa, ma ottiene l'importo in uscita e fornisce l'input.
12 // performs chained getAmountOut calculations on any number of pairs3 function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {4 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');5 amounts = new uint[](path.length);6 amounts[0] = amountIn;7 for (uint i; i < path.length - 1; i++) {8 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);9 amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);10 }11 }1213 // performs chained getAmountIn calculations on any number of pairs14 function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {15 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');16 amounts = new uint[](path.length);17 amounts[amounts.length - 1] = amountOut;18 for (uint i = path.length - 1; i > 0; i--) {19 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);20 amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);21 }22 }23}Mostra tuttoCopia
Queste due funzioni gestiscono l'identificazione dei valori quando è necessario passare per diversi scambi in pari.
Transfer Helper
Questa libreria(opens in a new tab) aggiunge controlli di successo relativi ai trasferimenti di ERC-20 ed Ethereum per trattare un ripristino e una restituzione di valore false
allo stesso modo.
1// SPDX-License-Identifier: GPL-3.0-or-later23pragma solidity >=0.6.0;45// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false6library TransferHelper {7 function safeApprove(8 address token,9 address to,10 uint256 value11 ) internal {12 // bytes4(keccak256(bytes('approve(address,uint256)')));13 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));14Mostra tuttoCopia
Esisono due modi per chiamare un contratto diverso:
- Usare una definizione d'interfaccia per creare la chiamata di una funzione.
- Usa "manualmente" l'interfaccia binaria dell'applicazione (ABI)(opens in a new tab) per creare la chiamata. Questo è ciò che l'autore del codice ha deciso di fare.
1 require(2 success && (data.length == 0 || abi.decode(data, (bool))),3 'TransferHelper::safeApprove: approve failed'4 );5 }Copia
Per motivi di retrocompatibilità con il token creato prima dello standard ERC-20, una chiamata all'ERC-20 può fallire ripristinandosi (nel qual caso success
è false
), oppure concludersi con successo restituendo un valore false
(nel qual caso vi sono dei dati di output e se li decodifichi come un booleano, ottieni false
).
123 function safeTransfer(4 address token,5 address to,6 uint256 value7 ) internal {8 // bytes4(keccak256(bytes('transfer(address,uint256)')));9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));10 require(11 success && (data.length == 0 || abi.decode(data, (bool))),12 'TransferHelper::safeTransfer: transfer failed'13 );14 }Mostra tuttoCopia
Questa funzione implementa la funzionalità di trasferimento dell'ERC-20(opens in a new tab), che consente a un conto di spendere la disponibilità fornita da un altro conto.
12 function safeTransferFrom(3 address token,4 address from,5 address to,6 uint256 value7 ) internal {8 // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));10 require(11 success && (data.length == 0 || abi.decode(data, (bool))),12 'TransferHelper::transferFrom: transferFrom failed'13 );14 }Mostra tuttoCopia
Questa funzione implementa la funzionalità transferFrom dell'ERC-20(opens in a new tab), che consente a un conto di spendere la disponibilità fornita da un altro conto.
12 function safeTransferETH(address to, uint256 value) internal {3 (bool success, ) = to.call{value: value}(new bytes(0));4 require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');5 }6}Copia
Questa funzione trasferisce ether a un conto. Ogni chiamata a un contratto diverso può tentare di inviare ether. Poiché non dobbiamo realmente chiamare alcuna funzione, non inviamo alcun dato con la chiamata.
Conclusioni
Questo è un articolo lungo di circa 50 pagine. Se sei arrivato fin qui, congratulazioni! Speriamo di aver esposto chiaramente alcuni aspetti relativi alla scrittura di un'applicazione reale (e non brevi programmi campione) e che ora tu sia in grado di scrivere contratti per le tue esigenze.
Ora vai a scrivere qualcosa di utile e facci sognare.
Ultima modifica: @wackerow(opens in a new tab), 2 aprile 2024