Vai al contenuto principale

Guida dettagliata al contratto Uniswap-v2

solidity
Intermedio
Ori Pomerantz
1 maggio 2021
59 minuti letti minute read

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:

  1. Scambi token diversi
  2. Aggiungi liquidità al mercato e vieni ricompensato con token di liquidità ERC-20 di pari valore
  3. 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

  1. Fornisce un'indennità nell'importo da scambiare al conto periferico.
  2. 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)

  1. Identifica l'importo necessario da scambiare su ogni scambio lungo il percorso.
  2. 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)

  1. Verifica che il contratto principale non raggiri il sistema e possa mantenere liquidità sufficiente dopo lo scambio.
  2. Vede quanti token aggiuntivi abbiamo, in aggiunta alle riserve note. Quell'importo è il numero di token di input ricevuti da scambiare.
  3. Invia i token d'output alla destinazione.
  4. Chiama _update per aggiornare gli importi della riserva

Di nuovo nel contratto periferico (UniswapV2Router02.sol)

  1. Esegue ogni pulizia necessaria (ad esempio, brucia i token di WET per riottenere ETH da inviare al trader)

Aggiungere liquidità

Chiamante

  1. Fornisce un'indennità negli importi da aggiungere al pool di liquidità al conto periferico.
  2. Chiama una delle funzioni addLiquidity del contratto periferico.

Nel contratto periferico (UniswapV2Router02.sol)

  1. Crea un nuovo scambio in pari se necessario
  2. 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.
  3. Controlla se gli importi sono accettabili (i chiamanti possono specificare un importo minimo al di sotto del quale preferiscono non aggiungere liquidità)
  4. Chiama il contratto principale.

Nel contratto principale (UniswapV2Pair.sol)

  1. Conia token di liquditià e li invia al chiamante
  2. Chiama _update per aggiornare gli importi della riserva

Rimuovere la liquidità

Chiamante

  1. Fornisce un'indennità dei token di liquidità da bruciare in cambio dei token sottostanti al conto periferico.
  2. Chiama una delle funzioni removeLiquidity del contratto periferico.

Nel contratto periferico (UniswapV2Router02.sol)

  1. Invia i token di liquidità allo scambio in pari

Nel contratto principale (UniswapV2Pair.sol)

  1. 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.
  2. Brucia i token di liquidità
  3. 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;
2
3import './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 tutto
Copia

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 getReserves
2 uint112 private reserve1; // uses single storage slot, accessible via getReserves
Copia

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 getReserves
Copia

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 event
Copia

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.

Eventoreserve0reserve1reserve0 * reserve1Tasso di cambio medio (token1/token0)
Configurazione iniziale1.000,0001.000,0001.000.000
Trader A scambia 50 token0 per 47,619 token11.050,000952,3811.000.0000,952
Trader B scambia 10 token0 per 8,984 token11.060,000943,3961.000.0000,898
Trader C scambia 40 token0 per 34,305 token11.100,000909,0901.000.0000,858
Trader D scambia 100 token1 per 109,01 token0990,9901.009,0901.000.0000,917
Trader E scambia 10 token0 per 10,079 token11.000,990999,0101.000.0001,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:

  1. Ripristina. Se una chiamata a un contratto esterno si annulla, allora il valore di restituzione booleano è false
  2. 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 to
8 );
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 deployment
2 function initialize(address _token0, address _token1) external {
3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
4 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 accumulators
2 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 desired
3 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 desired
2 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:

Eventoreserve0reserve1marca orariaTasso di cambio marginale (reserve1/reserve0)price0CumulativeLast
Configurazione iniziale1.000,0001.000,0005.0001,0000
Trader A deposita 50 token0 e ottiene 47,619 token11.050,000952,3815.0200,90720
Trader B deposita 10 token0 e ottiene 8,984 token11.060,000943,3965.0300,89020+10*0,907 = 29,07
Trader C deposita 40 token0 e ottiene 34,305 token11.100,000909,0905.1000,82629,07+70*0,890 = 91,37
Trader D deposita 100 token0 e ottiene 109,01 token1990,9901.009,0905.1101,01891,37+10*0,826 = 99,63
Trader E deposita 10 token0 e ottiene 10,079 token11.000,990999,0105.1500,99899,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 savings
Copia

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 checks
2 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 savings
Copia

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 _mintFee
2 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 tokens
Copia

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.

Eventoreserve0reserve1reserve0 * reserve1Valore del pool (reserve0 + reserve1)
Configurazione iniziale83225640
Il trader deposita 8 token Token0 e ottiene 16 Token1161625632

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à).

Eventoreserve0reserve1reserve0 * reserve1Valore del pool (reserve0 + reserve1)Token di liquidità coniati per questo depositoToken di liquidità totaliValore di ciascun token di liquidità
Configurazione iniziale8,0008,0006416,000882,000
Deposita quattro per tipo12,00012,00014424,0004122,000
Deposita due per tipo14,00014,00019628,0002142,000
Deposito di valore non pari18,00014,00025232,000014~2,286
Dopo l'arbitraggio~15,874~15,874252~31,748014~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.

1
2 _update(balance0, balance1, _reserve0, _reserve1);
3 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
4 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 checks
2 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 savings
2 address _token0 = token0; // gas savings
3 address _token1 = token1; // gas savings
4 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 _mintFee
3 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
4 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
5 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));
6
7 _update(balance0, balance1, _reserve0, _reserve1);
8 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
9 emit Burn(msg.sender, amount0, amount1, to);
10 }
11
Mostra tutto
Copia

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 checks
2 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 savings
3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
4
5 uint balance0;
6 uint balance1;
7 { // scope for _token{0,1}, avoids stack too deep errors
Copia

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 tokens
5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
Copia

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 errors
5 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 }
2
3 _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 correnti
  • skim, che preleva l'importo aggiuntivo. Nota che qualsiasi conto può chiamare skim, 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 reserves
2 function skim(address to) external lock {
3 address _token0 = token0; // gas savings
4 address _token1 = token1; // gas savings
5 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
6 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
7 }
8
9
10
11 // force reserves to match balances
12 function sync() external lock {
13 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
14 }
15}
Mostra tutto
Copia

UniswapV2Factory.sol

Questo contratto(opens in a new tab) crea gli scambi in pari.

1pragma solidity =0.5.16;
2
3import './interfaces/IUniswapV2Factory.sol';
4import './UniswapV2Pair.sol';
5
6contract 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 sufficient
Copia

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 direction
3 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 }
5
6 function setFeeToSetter(address _feeToSetter) external {
7 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
8 feeToSetter = _feeToSetter;
9 }
10}
Mostra tutto
Copia

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 := chainid
5 }
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 tutto
Copia

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 }
4
Copia

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;
2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
4import '@uniswap/lib/contracts/libraries/TransferHelper.sol';
5
6import './interfaces/IUniswapV2Router02.sol';
7import './libraries/UniswapV2Library.sol';
8import './libraries/SafeMath.sol';
9import './interfaces/IERC20.sol';
10import './interfaces/IWETH.sol';
Mostra tutto
Copia

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;
3
4 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 contract
3 }
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à.

1
2 // **** 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 amountBMin
Copia

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:

ParametroValore
amountADesired1000
amountBDesired1000
amountAMin900
amountBMin800

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 yet
2 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.

Grafico

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 deadline
Mostra tutto
Copia

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 deadline
5 ) 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 amountETHMin
13 );
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 tutto
Copia

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 any
3 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 deadline
10 ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
Mostra tutto
Copia

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); // invia liquidità al paro
3 (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 deadline
8 ) 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 deadline
17 );
18 TransferHelper.safeTransfer(token, to, amountToken);
19 IWETH(WETH).withdraw(amountETH);
20 TransferHelper.safeTransferETH(to, amountETH);
21 }
Mostra tutto
Copia

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 s
10 ) 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 }
16
17
18 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 s
26 ) 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 tutto
Copia

Queste funzioni ritrasmettono le meta-transazioni per consentire agli utenti di prelevare dal pool, usando il meccanismo del permesso.

1
2 // **** 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 deadline
10 ) 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 deadline
19 );
20 TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
21 IWETH(WETH).withdraw(amountETH);
22 TransferHelper.safeTransferETH(to, amountETH);
23 }
24
Mostra tutto
Copia

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.

1
2
3 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 s
11 ) 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, deadline
17 );
18 }
Mostra tutto
Copia

La funzione finale combina le commissioni di archiviazione con le meta-transazioni.

Scambio

1 // **** SCAMBIO ****
2 // richiede l'importo iniziale già inviato alla prima coppia
3 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.

  1. La situazione iniziale
  2. Un trader vende 24,695 token A e riceve 23,305 token B.
  3. Il trader vende 24,695 token B per 25,305 token C, mantenendo approssimativamente 0,61 token B come profitto.
  4. 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).
FaseScambio A-BScambio B-CScambio A-C
1A:1000 B:1050 A/B=1,05B:1000 C:1050 B/C=1,05A:1050 C:1000 C/A=1,05
2A:1024,695 B:1024,695 A/B=1B:1000 C:1050 B/C=1,05A:1050 C:1000 C/A=1,05
3A:1024,695 B:1024,695 A/B=1B:1024,695 C:1024,695 B/C=1A:1050 C:1000 C/A=1,05
4A:1024,695 B:1024,695 A/B=1B:1024,695 C:1024,695 B/C=1A: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.

1
2 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 deadline
3 ) 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 deadline
7 ) 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 tutto
Copia

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 external
3 virtual
4 override
5 payable
6 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 }
16
17
18 function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
19 external
20 virtual
21 override
22 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 }
35
36
37
38 function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
39 external
40 virtual
41 override
42 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 }
55
56
57 function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
58 external
59 virtual
60 override
61 payable
62 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 any
72 if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
73 }
Mostra tutto
Copia

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 // **** SCAMBIO (supporta i token della commissione al trasferimento) ****
2 // richiede che l'importo iniziale sia già stato inviato alla prima coppia
3 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 errors
8 (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 tutto
Copia

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 }
7
8
9 function swapExactTokensForTokensSupportingFeeOnTransferTokens(
10 uint amountIn,
11 uint amountOutMin,
12 address[] calldata path,
13 address to,
14 uint deadline
15 ) external virtual override ensure(deadline) {
16 TransferHelper.safeTransferFrom(
17 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
18 );
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 }
26
27
28 function swapExactETHForTokensSupportingFeeOnTransferTokens(
29 uint amountOutMin,
30 address[] calldata path,
31 address to,
32 uint deadline
33 )
34 external
35 virtual
36 override
37 payable
38 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 }
51
52
53 function swapExactTokensForETHSupportingFeeOnTransferTokens(
54 uint amountIn,
55 uint amountOutMin,
56 address[] calldata path,
57 address to,
58 uint deadline
59 )
60 external
61 virtual
62 override
63 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]), amountIn
68 );
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 tutto
Copia

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 }
5
6 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)
7 public
8 pure
9 virtual
10 override
11 returns (uint amountOut)
12 {
13 return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
14 }
15
16 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)
17 public
18 pure
19 virtual
20 override
21 returns (uint amountIn)
22 {
23 return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);
24 }
25
26 function getAmountsOut(uint amountIn, address[] memory path)
27 public
28 view
29 virtual
30 override
31 returns (uint[] memory amounts)
32 {
33 return UniswapV2Library.getAmountsOut(factory, amountIn, path);
34 }
35
36 function getAmountsIn(uint amountOut, address[] memory path)
37 public
38 view
39 virtual
40 override
41 returns (uint[] memory amounts)
42 {
43 return UniswapV2Library.getAmountsIn(factory, amountOut, path);
44 }
45}
Mostra tutto
Copia

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;
2
3// a library for performing various math operations
4
5library Math {
6 function min(uint x, uint y) internal pure returns (uint z) {
7 z = x < y ? x : y;
8 }
9
10 // metodo babilonese (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 tutto
Copia

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;
2
3// a library for handling binary fixed point numbers (https://wikipedia.org/wiki/Q_(number_format))
4
5// range: [0, 2**112 - 1]
6// resolution: 1 / 2**112
7
8library UQ112x112 {
9 uint224 constant Q112 = 2**112;
Mostra tutto
Copia

Q112 è la codifica per uno.

1 // codifica un uint112 come un UQ112x112
2 function encode(uint112 y) internal pure returns (uint224 z) {
3 z = uint224(y) * Q112; // mai in eccesso
4 }
Copia

Poiché y è uint112, il suo valore massimo può essere 2^112-1. Quel numero è ancora codificabile come un UQ112x112.

1 // divide un UQ112x112 per un uint112, restituendo un UQ112x112
2 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;
2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
4
5import "./SafeMath.sol";
6
7library UniswapV2Library {
8 using SafeMath for uint;
9
10 // returns sorted token addresses, used to handle return values from pairs sorted in this order
11 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 tutto
Copia

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 calls
2 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 hash
9 ))));
10 }
Mostra tutto
Copia

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 pair
2 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 asset
2 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 // dato un importo in entrata di una risorsa e riserve della coppia, restituisce l'importo risultante massimo dell'altra risorsa
2 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.

1
2 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 asset
2 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.

1
2 // performs chained getAmountOut calculations on any number of pairs
3 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 }
12
13 // performs chained getAmountIn calculations on any number of pairs
14 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 tutto
Copia

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-later
2
3pragma solidity >=0.6.0;
4
5// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false
6library TransferHelper {
7 function safeApprove(
8 address token,
9 address to,
10 uint256 value
11 ) internal {
12 // bytes4(keccak256(bytes('approve(address,uint256)')));
13 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));
14
Mostra tutto
Copia

Esisono due modi per chiamare un contratto diverso:

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).

1
2
3 function safeTransfer(
4 address token,
5 address to,
6 uint256 value
7 ) 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 tutto
Copia

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.

1
2 function safeTransferFrom(
3 address token,
4 address from,
5 address to,
6 uint256 value
7 ) 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 tutto
Copia

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.

1
2 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: @Herbie_23(opens in a new tab), 2 aprile 2024

Questo tutorial è stato utile?