Vai al contenuto principale

Aiuta ad aggiornare questa pagina

🌏

C'è una nuova versione di questa pagina, ma al momento è solo in inglese. Aiutaci a tradurre l'ultima versione.

Traduci la pagina
Visualizza in inglese

Nessun bug qui!🐛

Questa pagina non è stata tradotta. Per il momento, è stata intenzionalmente lasciata in inglese.

Guida dettagliata al contratto Uniswap-v2

solidity
uniswap
Intermedio
✍Ori Pomerantz
📆1 maggio 2021
⏱️59 minuti letti

Introduzione

Uniswap v2 può creare un mercato di scambio tra due token ERC-20. In questo articolo analizzeremo il codice sorgente per i contratti che implementano questo protocollo e vedremo perché sono scritti in questo modo.

Cosa fa Uniswap?

Fondamentalmente, esistono due tipi di utenti: fornitori di liquidità e trader.

I fornitori di liquidità forniscono al pool i due token scambiabili (li chiameremo Token0 e Token1). In cambio, ricevono un terzo token che rappresenta la proprietà parziale del pool, detto token di liquidità.

I trader inviano un tipo di token al pool e ricevono l'altro (ad esempio, invii Token0 e ricevi Token1) dal pool fornito dai fornitori di liquidità. Il tasso di cambio è determinato dal numero relativo di Token0 e Token1 che il pool possiede. Inoltre, il pool prende una piccola percentuale come ricompensa per il pool di liquidità.

Quando i fornitori di liquidità rivogliono indietro le loro risorse, possono bruciare i token del pool e ricevere i token, compresa la quota di ricompense.

Clicca qui per una descrizione completa.

Perché v2? Perché non v3?

Mentre scrivo il presente articolo, Uniswap v3 è quasi pronto. Tuttavia, è un aggiornamento molto più complicato dell'originale. È più facile imparare prima la v2 e poi passare alla v3.

Contratti principali e contratti periferici

Uniswap v2 è diviso in due componenti, una principale e una periferica. Questa divisione consente ai contratti principali, che detengono le risorse e dunque devono essere sicuri, di essere più semplici e facili da controllare. Tutte le funzionalità aggiuntive richieste dai trader possono essere fornite dai contratti periferici.

Flussi di dati e di controllo

Questo è il flusso di dati e controllo che si verifica quando esegui le tre azioni principali di Uniswap:

  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 al conto perifierico un'allowance nell'importo da scambiare.
  2. Chiama una delle tante funzioni di scambio del contratto periferico (la funzione chiamata dipende dal fatto che siano o meno coinvolti ETH, dal fatto che il trader specifichi l'importo di token da depositare o da prelevare, ecc.). Ogni funzione di scambio accetta un path, un insieme di scambi da attraversare.

Nel contratto periferico (UniswapV2Router02.sol)

  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 è 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 al conto periferico un'allowance negli importi da aggiungere al pool di liquidità.
  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 tra token nuovi ed esistenti.
  3. Controlla se gli importi sono accettabili (i chiamanti possono specificare un importo minimo al di sotto del quale preferirebbero 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 al conto periferico un'allowance di token di liquidità da bruciare in cambio dei token sottostanti.
  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 1.000 token A nel pool, 500 token B e 90 token di liquidità e ne riceviamo 9 da bruciare, bruciamo il 10% dei token di liquidità, quindi reinviamo all'utente 100 token A e 50 token B.
  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 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';
10
Mostra tutto
📋 Copia

Queste sono tutte le interfacce di cui il contratto deve essere a conoscenza, perché le implementa (IUniswapV2Pair and UniswapV2ERC20) o perché chiama i contratti che le implementano.

1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
2
📋 Copia

Questo contratto eredita da UniswapV2ERC20, che fornisce le funzioni di ERC-20 per i token di liquidità.

1 using SafeMath for uint;
2
📋 Copia

La libreria SafeMath è usata per evitare flussi eccessivi o insufficienti. È un aspetto importante perché altrimenti potremmo finire in una situazione in cui il valore dovrebbe essere -1, ma invece è 2^256-1.

1 using UQ112x112 for uint224;
2
📋 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 a 224 bit, con 112 bit per la parte intera e 112 bit per la frazione. Quindi 1.0 è rappresentato come 2^112, 1.5 è rappresentato come 2^112 + 2^111, ecc.

Ulteriori dettagli su questa libreria sono disponibili più in avanti nel documento.

Variabili

1 uint public constant MINIMUM_LIQUIDITY = 10**3;
2
📋 Copia

Per evitare casi di divisione per zero, esiste un numero minimo di token di liquidità che esiste sempre (ma è posseduto dal conto zero). Quel numero è MINIMUM_LIQUIDITY, mille.

1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
2
📋 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;
2
📋 Copia

Questo è il contratto factory che ha creato questo pool. Ogni pool rappresenta uno scambio tra due token ERC-20, dove factory è un punto centrale che connette tutti questi pool.

1 address public token0;
2 address public token1;
3
📋 Copia

Sono gli indirizzi dei contratti per due tipi di token ERC-20, scambiabili da questo pool.

1 uint112 private reserve0; // uses single storage slot, accessible via getReserves
2 uint112 private reserve1; // uses single storage slot, accessible via getReserves
3
📋 Copia

Le riserve che il pool ha per ogni tipo di token. Supponiamo che i due rappresentino la stessa quantità di valore e dunque che ogni token0 valga token1 di reserve1/reserve0.

1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves
2
📋 Copia

La marca oraria dall'ultimo blocco in cui si è verificato uno scambio, usata per tracciare i tassi di cambio nel tempo.

Una delle più grandi spese di gas dei contratti di Ethereum è la memoria, che persiste da una chiamata del contratto alla successiva. Ogni cella di memoria è lunga 256 bit. Tre variabili, reserve0, reserve1 e blockTimestampLast, sono quindi allocate in modo che un solo valore di memoria possa comprenderle tutte e tre (112+112+32=256).

1 uint public price0CumulativeLast;
2 uint public price1CumulativeLast;
3
📋 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 periodo di tempo.

1 uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event
2
📋 Copia

Il modo in cui lo scambio in pari decide sul tasso di cambio tra token0 e token1 consiste nel mantenere il multiplo delle due riserve costanti durante gli scambi. kLast è questo valore. Cambia quando un fornitore di liquidità deposita o preleva token e aumenta lievemente a causa della commissione di mercato dello 0,3%.

Ecco un semplice esempio. Nota che per semplicità la tabella ha solo tre cifre dopo la virgola decimale e che ignoriamo la commissione di gas dello 0,3%, quindi i numeri non sono accurati.

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;
2
📋 Copia

Esiste una classe di vulnerabilità di sicurezza basata sull'abuso di rientrata. Uniswap deve trasferire token ERC-20 arbitrari, ovvero chiamare i contratti ERC-20 che potrebbero tentare di abusare del mercato di Uniswap che li chiama. Avendo una variabile unlocked inserita nel contratto, possiamo evitare che le funzioni vengano chiamate mentre sono in esecuzione (nella stessa transazione).

1 modifier lock() {
2
📋 Copia

Questa funzione è un modificatore, una funzione che avvolge una funzione normale per cambiarne in qualche modo il comportamento.

1 require(unlocked == 1, 'UniswapV2: LOCKED');
2 unlocked = 0;
3
📋 Copia

Se unlocked è pari a uno, impostalo su zero. Se è già zero, ripristina la chiamata, facendola fallire.

1 _;
2
📋 Copia

In un modificatore _; è la chiamata alla funzione originale (con tutti i parametri). Significa che la chiamata della funzione ha luogo solo se unlocked era su uno quando è stata chiamata e, mentre è in esecuzione, il valore di unlocked è zero.

1 unlocked = 1;
2 }
3
📋 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 }
6
📋 Copia

Questa funzione fornisce ai chiamanti lo stato corrente dello scambio. Nota che le funzioni di Solidity possono restituire valori multipli.

1 function _safeTransfer(address token, address to, uint value) private {
2 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
3
📋 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.

1 require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
2 }
3
📋 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);
3
📋 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 fanno parte dell'evento, così come l'identità del conto che ci ha chiamati (sender). Nel caso di un prelievo, l'evento include anche la destinazione che ha ricevuto i token (to), che potrebbe non essere pari al mittente.

1 event Swap(
2 address indexed sender,
3 uint amount0In,
4 uint amount1In,
5 uint amount0Out,
6 uint amount1Out,
7 address indexed to
8 );
9
📋 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);
2
📋 Copia

Infine, Sync viene emesso ogni volta che i token sono aggiunti o prelevati, indipendentemente dal motivo, per fornire le ultime informazioni sulla riserva (e dunque il tasso di cambio).

Funzioni di configurazione

Queste funzioni dovrebbero essere chiamate una volta che il nuovo scambio in pari è configurato.

1 constructor() public {
2 factory = msg.sender;
3 }
4
📋 Copia

Il costruttore si assicura che terremo traccia dell'indirizzo della factory che ha creato la coppia. Queste informazioni sono necessarie per initialize e per la commissione di factory (se presente)

1 // called once by the factory at time of 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 }
7
📋 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 {
3
📋 Copia

Questa funzione è chiamata ogni volta che i token vengono depositati o prelevati.

1 require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
2
📋 Copia

Se balance0 o balance1 (uint256) è maggiore di uint112(-1) (=2^112-1) (quindi va in overflow e torna a 0 quando viene convertita in uint112), rifiuta di continuare _update per prevenire l'overflow. Con un token normale suddivisibile in 10^18 unità, questo significa che ogni scambio è limitato a circa 5,1*10^15 di ogni token. Finora non è stato un problema.

1 uint32 blockTimestamp = uint32(block.timestamp % 2**32);
2 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
3 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
4
📋 Copia

Se il tempo trascorso è diverso da zero, significa che la nostra è la prima transazione di scambio su questo blocco. In tal caso, dobbiamo aggiornare gli accumulatori del costo.

1 // * never overflows, and + overflow is desired
2 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
3 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
4 }
5
📋 Copia

Ogni accumulatore del costo è aggiornato con l'ultimo costo (riserva dell'altro token/riserva di questo token) per il tempo trascorso in secondi. Per ottenere un prezzo medio leggi il prezzo cumulativo di due punti nel tempo e dividi per la differenza di tempo tra loro. Ad esempio, supponiamo questa sequenza di eventi:

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 }
6
📋 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) {
3
📋 Copia

In Uniswap 2.0 i trader pagano una commissione dello 0,30% per usare il mercato. Gran parte di tale commissione (0,25% della transazione) va sempre ai fornitori di liquidità. Il rimanente 0,05% può andare ai fornitori di liquidità o a un indirizzo specificato dalla factory come commissione di protocollo, che paga Uniswap per l'attività di sviluppo.

Per ridurre i calcoli (e dunque i costi del carburante), tale commissione è calcolata solo quando la liquidità viene aggiunta o rimossa dal pool, anziché ad ogni operazione.

1 address feeTo = IUniswapV2Factory(factory).feeTo();
2 feeOn = feeTo != address(0);
3
📋 Copia

Leggi la destinazione della commissione della factory. Se è zero, allora non esiste alcuna commissione di protocollo e non serve calcolarla.

1 uint _kLast = kLast; // gas savings
2
📋 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 rilasciata quando termina la chiamata alla funzione al contratto, quindi usiamo una variabile interna per risparmiare sul carburante.

1 if (feeOn) {
2 if (_kLast != 0) {
3
📋 Copia

I fornitori di liquidità ottengono la loro parte semplicemente mediante l'apprezzamento dei loro token di liquidità. Ma la commissione di protocollo richiede che i nuovi token di liquidità siano coniati e forniti all'indirizzo feeTo.

1 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
2 uint rootKLast = Math.sqrt(_kLast);
3 if (rootK > rootKLast) {
4
📋 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;
4
📋 Copia

Questo complicato calcolo delle commissioni è spiegato nel whitepaper a pagina 5. Sappiamo che, tra il momento in cui kLast è stato calcolato e il presente, non è stata aggiunta o rimossa liquidità (perché eseguiamo questo calcolo ogni volta che la liquidità viene aggiunta o rimossa, prima che cambi effettivamente), quindi qualunque modifica a reserve0 * reserve1 deve provenire dalle commissioni di transazione (senza di esse manterremmo reserve0 * reserve1 costante).

1 if (liquidity > 0) _mint(feeTo, liquidity);
2 }
3 }
4
📋 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 }
5
📋 Copia

Se non c'è alcuna commissione con kLast impostato a zero (se non è già così). Alla scrittura di questo contratto, esisteva una funzionalità di rimborso del carburante che incoraggiava i contratti a ridurre la dimensione generale dello stato di Ethereum azzerando l'archiviazione non necessaria. Questo codice ottiene quel rimborso, se possibile.

Funzioni accessibili esternamente

Nota che, anche se ogni transazione o contratto può chiamare queste funzioni, esse sono progettate per essere chiamate dal contratto periferico. Se le chiami direttamente, non potrai barare sullo scambio in pari, ma potresti perdere valore a causa di un errore.

mint
1 // this low-level function should be called from a contract which performs important safety checks
2 function mint(address to) external lock returns (uint liquidity) {
3
📋 Copia

Questa funzione viene chiamata quando un fornitore di liquidità aggiunge liquidità al pool. Essa conia token di liquidità aggiuntivi a titolo di ricompensa. Dovrebbe essere chiamata da un contratto periferico che la chiama dopo aver aggiunto la liquidità alla stessa transazione (quindi nessun altro può inviare una transazione che rivendichi la nuova liquidità prima del proprietario legittimo).

1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
2
📋 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 ci servono più.

1 uint balance0 = IERC20(token0).balanceOf(address(this));
2 uint balance1 = IERC20(token1).balanceOf(address(this));
3 uint amount0 = balance0.sub(_reserve0);
4 uint amount1 = balance1.sub(_reserve1);
5
📋 Copia

Ottiene i saldi correnti e vede quanto è stato aggiunto di ogni tipo di token.

1 bool feeOn = _mintFee(_reserve0, _reserve1);
2
📋 Copia

Calcola le commissioni di protocollo da raccogliere, se presenti, e conia token di liquidità di conseguenza. Poiché i parametri relativi a _mintFee sono valori di riserva vecchi, la commissione viene calcolata accuratamente solo secondo le modifiche al pool causate dalle commissioni.

1 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _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
5
📋 Copia

Se questo è il primo deposito, crea MINIMUM_LIQUIDITY token e inviali all'indirizzo zero per bloccarli. Non saranno mai riscattabili, il che significa che il pool non sarà mai svuotato completamente (questo ci salva dalla divisione per zero in alcuni punti). Il valore di MINIMUM_LIQUIDITY è mille e, considerando che gran parte dell'ERC-20 è suddivisa in unità di 10^-18 di un token, come ETH è diviso in wei, corrisponde a 10^-15 al valore di un singolo token. Non è quindi un costo elevato.

Al momento del primo deposito non conosciamo il valore relativo dei due token, quindi ci limitiamo a moltiplicare gli importi e calcoliamo la radice quadrata, supponendo che il deposito ci fornisca un valore pari in entrambi i token.

Possiamo fidarci perché è nell'interesse del depositante fornire un valore pari, così da evitare di perdere valore all'arbitraggio. Diciamo che il valore dei due token è identico, ma il nostro depositante ha depositato quattro volte tanto di Token1 rispetto al Token0. Un trader può avvalersi del fatto che lo scambio in pari pensi che Token0 sia più prezioso per ricavarne valore.

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);
3
📋 Copia

Con ogni deposito successivo, conosciamo già il tasso di cambio tra i due attivi e prevediamo che i fornitori di liquidità forniranno pari valore per entrambe. Altrimenti, diamo loro token di liquidità in base al valore inferiore che hanno fornito, a titolo di punizione.

Che sia il deposito iniziale o uno successivo, il numero di token di liquidità che forniamo è pari alla radice quadrata del cambio in reserve0*reserve1 e il valore del token di liquidità non cambia (salvo in presenza di un deposito che non abbia valori pari per entrambi i tipi, nel qual caso la "multa" viene distribuita). Ecco un altro esempio con due token dallo stesso valore, con tre depositi validi e uno inadeguato (deposito di un solo tipo di token, quindi non produce alcun token di liquidità).

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);
4
📋 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 }
6
📋 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) {
3
📋 Copia

Questa funzione viene chiamata quando la liquidità è prelevata e i token di liquidità appropriati devono essere bruciati. Dovrebbe essere chiamata anche da un conto periferico.

1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas 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)];
7
📋 Copia

Il contratto periferico ha trasferito la liquidità da bruciare a questo contratto prima della chiamata. Così sappiamo quanta liquidità bruciare e ci possiamo assicurare che sia bruciata.

1 bool feeOn = _mintFee(_reserve0, _reserve1);
2 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _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');
6
📋 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
12
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 {
3
📋 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
8
📋 Copia

Le variabili locali sono memorizzabili in memoria o, se sono troppe, direttamente sullo stack. Se possiamo limitare il numero in modo da usare lo stack consumeremo meno carburante. Per ulteriori dettagli vedi lo yellow paper, le specifiche formali di Ethereum, 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
6
📋 Copia

Questo trasferimento è ottimistico, perché trasferiamo prima di essere sicuri che tutte le condizioni siano soddisfatte. Su Ethereum possiamo farlo perché, se le condizioni non sono risultassero soddisfatte nella chiamata, potremo sempre ripristinare allo stato prima di essa e di eventuali modifiche.

1 if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
2
📋 Copia

Informa il ricevente dello scambio, se richiesto.

1 balance0 = IERC20(_token0).balanceOf(address(this));
2 balance1 = IERC20(_token1).balanceOf(address(this));
3 }
4
📋 Copia

Ottieni i saldi correnti. Il contratto periferico ci invia i token prima di chiamarci per lo scambio. In questo modo il contratto può verificare facilmente di non essere oggetto di truffe; tale controllo deve verificarsi nel contratto principale (perché possiamo essere chiamati da altre entità oltre che dal nostro contratto periferico).

1 uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
2 uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
3 require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
4 { // scope for reserve{0,1}Adjusted, avoids stack too deep 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');
8
📋 Copia

Questo è un controllo di sicurezza, per assicurarsi di non perdere in seguito allo scambio. Non esiste alcuna circostanza in cui uno scambio dovrebbe ridurre reserve0*reserve1. Questo è anche il punto in cui garantiamo che una commissione dello 0,3% è inviata allo scambio; prima di verificare lo stato di salute di K, moltiplichiamo entrambi i saldi per 1000 meno gli importi moltiplicati per 3, questo significa che lo 0,3% (3/1000 = 0,003 = 0,3%) viene dedotto dal saldo prima di confrontare il suo valore K con il valore K delle riserve correnti.

1 }
2
3 _update(balance0, balance1, _reserve0, _reserve1);
4 emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
5 }
6
📋 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 ogni conto ha la facoltà di chiamare skim perché 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}
16
Mostra tutto
📋 Copia

UniswapV2Factory.sol

Questo contratto 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;
9
📋 Copia

Queste variabili di stato sono necessarie per implementare la commissione di protocollo (vedi il whitepaper, p. 5). L'indirizzo feeTo accumulata i token di liquidità per la commissione di protocollo e feeToSetter è l'indirizzo che può modificare feeTo con un indirizzo diverso.

1 mapping(address => mapping(address => address)) public getPair;
2 address[] public allPairs;
3
📋 Copia

Queste variabili tracciano le coppie, gli scambi tra due tipi di token.

Il primo, getPair, è una mappatura che identifica uno contratto di scambio in pari basato sui due token ERC-20 scambiati. I token ERC-20 sono identificati dagli indirizzi dei contratti che li implementano, quindi le chiavi e il valore sono tutti indirizzi. Per ottenere l'indirizzo dello scambio in pari che ti consente di convertire da tokenA a tokenB, usi getPair[<tokenA address>][<tokenB address>] (o viceversa).

La seconda variabile, allPairs, è un insieme che include tutti gli indirizzi di scambi in pari creati da questa factory. In Ethereum non puoi iterare sul contenuto di una mappatura od ottenere un elenco di tutte le chiavi, quindi questa variabile è l'unico modo per sapere quali scambi sono gestiti da questa factory.

Nota: Il motivo per cui non puoi iterare su tutte le chiavi di una mappatura è che l'archiviazione dei dati del contratto è costosa, quindi meno la modifichiamo, meglio è. Puoi creare delle mappature che supportano l'iterazione, ma richiedono memoria aggiuntiva per un elenco di chiavi. In gran parte delle applicazioni, non ne hai bisogno.

1 event PairCreated(address indexed token0, address indexed token1, address pair, uint);
2
📋 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 }
4
📋 Copia

La sola cosa che fa il costruttore è specificare feeToSetter. Le factory iniziano senza una commissione e solo feeSetter può modificare questo punto.

1 function allPairsLength() external view returns (uint) {
2 return allPairs.length;
3 }
4
📋 Copia

Questa funzione restituisce il numero di scambi in pari.

1 function createPair(address tokenA, address tokenB) external returns (address pair) {
2
📋 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 occorre l'autorizzazione di Uniswap per creare un nuovo scambio in pari.

1 require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
2 (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
3
📋 Copia

Vogliamo che l'indirizzo del nuovo scambio sia deterministico, così che sia calcolabile in anticipo al di fuori della catena (questo può essere utile per le transazioni di livello 2). Per farlo dobbiamo avere un ordine coerente degli indirizzi del token, indipendentemente dall'ordine in cui le abbiamo ricevute, per cui le smistiamo qui.

1 require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
2 require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
3
📋 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 solo pool di liquidità per coppia di token. Se esiste già uno scambio, non serve crearne un altro per la stessa coppia.

1 bytes memory bytecode = type(UniswapV2Pair).creationCode;
2
📋 Copia

Per creare un nuovo contratto ci serve il codice che lo crea (sia la funzione del costruttore sia il codice che scrive sulla memoria il bytecode dell'EVM del contratto reale). Normalmente, in Solidity, è sufficiente usare addr = new <name of contract>(<constructor parameters>) e il compilatore pensa a tutto il resto, mentre per avere un indirizzo del contratto deterministico, dobbiamo usare l'opcode CREATE2. Quando questo codice è stato scritto, quell'opcode non era ancora supportato da Solidity, quindi occorreva ottenere manualmente il codice. Questo aspetto non è più un problema, perché ora Solidity supporta CREATE2.

1 bytes32 salt = keccak256(abi.encodePacked(token0, token1));
2 assembly {
3 pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
4 }
5
📋 Copia

Quando un opcode non è ancora supportato da Solidity, possiamo chiamarlo usando l'assemblaggio in linea.

1 IUniswapV2Pair(pair).initialize(token0, token1);
2
📋 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 }
6
📋 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}
11
Mostra tutto
📋 Copia

Queste due funzioni consentono a feeSetter di controllare il destinatario della commissione (se presente) e di modificare feeSetter impostando un nuovo indirizzo.

UniswapV2ERC20.sol

Questo contratto implementa il token di liquidità ERC-20. È simile al contratto ERC-20 di OpenWhisk, quindi spiegherò solo la parte diversa, la funzionalità permit.

Le transazioni su Ethereum costano ether (ETH), equivalente al denaro reale. Se hai token ERC-20 ma non ETH, non puoi inviare transazioni, quindi non puoi farci nulla. Una soluzione per evitare questo problema sono le meta-transazioni. Il proprietario dei token firma una transazione che consenta ad altri di prelevare i token al di fuori della catena e la invia al destinatario usando Internet. Il destinatario, che ha ETH a disposizione, invia il permesso per conto del proprietario.

1 bytes32 public DOMAIN_SEPARATOR;
2 // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
3 bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
4
📋 Copia

Questo hash è l'identificativo per il tipo di transazione. Il solo che supportiamo qui è Permit con questi parametri.

1 mapping(address => uint) public nonces;
2
📋 Copia

Un destinatario non può falsificare una firma digitale. Tuttavia, un'azione piuttosto banale consiste nell'inviare la stessa transazione due volte (è una forma di replay attack). Per impedirlo, usiamo un nonce. Se il nonce di un nuovo Permit non è uno in più dell'ultimo usato, supponiamo che non sia valido.

1 constructor() public {
2 uint chainId;
3 assembly {
4 chainId := chainid
5 }
6
📋 Copia

Questo è il codice per recuperare l'identificativo della catena. Esso utilizza un dialetto d'assemblaggio dell'EVM chiamato Yul. Nota che nella versione corrente di Yul devi usare chainid(), non chainid.

1 DOMAIN_SEPARATOR = keccak256(
2 abi.encode(
3 keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
4 keccak256(bytes(name)),
5 keccak256(bytes('1')),
6 chainId,
7 address(this)
8 )
9 );
10 }
11
Mostra tutto
📋 Copia

Calcola il separatore di dominio per EIP-712.

1 function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
2
📋 Copia

Questa è la funzione che implementa le autorizzazioni. Riceve come parametri i campi pertinenti e i tre valori scalari per la firma (v, r e s).

1 require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
2
📋 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 );
8
📋 Copia

abi.encodePacked(...) è il messaggio che ci aspettiamo di ricevere. Sappiamo quale dovrebbe essere il valore del nonce, quindi non serve ottenerlo come parametro.

L'algoritmo di firma di Ethereum prevede di ottenere 256 bit da firmare, quindi usiamo la funzione di hash keccak256.

1 address recoveredAddress = ecrecover(digest, v, r, s);
2
📋 Copia

Dal digest e la firma, possiamo ottenere l'indirizzo che lo ha firmato usando ecrecover.

1 require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
2 _approve(owner, spender, value);
3 }
4
5
📋 Copia

Se è tutto corretto, trattala come un'approvazione di ERC-20.

I contratti periferici

I contratti periferici sono l'API (interfaccia del programma applicativo) per Uniswap. Sono disponibili per le chiamate esterne, da altri contratti o applicazioni decentralizzate. I contratti principali possono essere chiamati direttamente, ma è più complicato e si rischia di perdere valore in caso di errore. I contratti principali contengono solo test, al fine di evitare truffe, e non controlli di sicurezza per chiunque altro. Questi ultimi sono contenuti nei contratti periferici, in modo da essere aggiornabili all'occorrenza.

UniswapV2Router01.sol

Questo contratto è problematico e non dovrebbe essere più usato. Fortunatamente, i contratti periferici sono privi di stato e non detengono alcuna risorsa, quindi è facile deprecarli e suggerire alle persone di usare una soluzione alternativa, ad esempio UniswapV2Router02.

UniswapV2Router02.sol

In gran parte dei casi puoi usare Uniswap tramite questo contratto. Puoi vedere come usarlo qui.

1pragma solidity =0.6.6;
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';
11
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 unici. Per consentire l'uso di ETH nei contratti che applicano i token ERC-20, è stato escogitato il contratto wrapped ether (WETH). Inviando questo contratto ETH viene coniato un importo equivalente di WETH. Oppure è possibile bruciare WETH per riottenere ETH.

1contract UniswapV2Router02 is IUniswapV2Router02 {
2 using SafeMath for uint;
3
4 address public immutable override factory;
5 address public immutable override WETH;
6
📋 Copia

Il router deve sapere quale factory usare e, per le transazioni che richiedono WETH, quale contratto WETH usare. Questi valori sono immutabili, a significare che sono impostabili solo nel costruttore. Questo rassicura gli utenti circa il fatto che nessuno potrebbe modificarli in modo da farli diventare contratti meno onesti.

1 modifier ensure(uint deadline) {
2 require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
3 _;
4 }
5
📋 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 }
5
📋 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 }
4
📋 Copia

Questa funzione viene chiamata quando riscattiamo i token dal contratto WETH in ETH. Solo il contratto WETH che utilizziamo è autorizzato a farlo.

Aggiungere liquidità

Queste funzioni aggiungono token allo scambio in pari, il che accresce il pool di liquidità.

1
2 // **** ADD LIQUIDITY ****
3 function _addLiquidity(
4
📋 Copia

Questa funzione serve a calcolare l'importo di token A e B che dovrebbero essere depositati nello scambio in pari.

1 address tokenA,
2 address tokenB,
3
📋 Copia

Questi sono gli indirizzi dei contratti del token ERC-20.

1 uint amountADesired,
2 uint amountBDesired,
3
📋 Copia

Questi sono gli importi che il fornitore di liquidità vuole depositare. Sono anche gli importi massimi da depositare di A e B.

1 uint amountAMin,
2 uint amountBMin
3
📋 Copia

Questi sono gli importi minimi accettabili da depositare. Se la transazione non può essere effettuata con questi importi o con importi superiori, ripristinala. Se non vuoi questa funzionalità, specifica semplicemente zero.

I fornitori di liquidità specificano un minimo, generalmente perché vogliono limitare la transazione al tasso di cambio vicino a quello corrente. Se il tasso di cambio fluttua troppo, ciò potrebbe essere dovuto a notizie che modificano i valori sottostanti, in tal caso i fornitori di liquidità vogliono poter decidere manualmente come agire.

Per esempio, immagina un caso in cui il tasso di cambio è uno a uno e il fornitore di liquidità specifica questi valori:

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 dall'intervallo, la transazione viene annullata.

Il motivo per questa precauzione è che le transazioni non sono immediate. Dopo che le hai inviate, a un certo punto un miner le includerà in un blocco (a meno che il tuo prezzo del carburante non sia molto basso, nel qual caso dovrai inviare un'altra transazione con lo stesso nonce e un prezzo del carburante maggiore per sovrascriverla). Non puoi controllare ciò che succede durante l'intervallo tra l'invio e l'inclusione.

1 ) internal virtual returns (uint amountA, uint amountB) {
2
📋 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 }
5
📋 Copia

Se non vi è ancora alcuno scambio per questa coppia di token, crealo.

1 (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
2
📋 Copia

Ottieni le riserve correnti nella coppia.

1 if (reserveA == 0 && reserveB == 0) {
2 (amountA, amountB) = (amountADesired, amountBDesired);
3
📋 Copia

Se le riserve correnti sono vuote, significa che questo è un nuovo scambio in pari. Gli importi da depositare dovrebbero essere esattamente gli stessi che il fornitore di liquidità vuole fornire.

1 } else {
2 uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
3
📋 Copia

Se ci serve vedere quali importi saranno, otteniamo l'importo ottimale usando questa funzione. Vogliamo ottenere lo stesso rapporto delle riserve correnti.

1 if (amountBOptimal <= amountBDesired) {
2 require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
3 (amountA, amountB) = (amountADesired, amountBOptimal);
4
📋 Copia

Se amountBOptimal è inferiore all'importo che il fornitore di liquidità vuole depositare, significa che al momento il token B è più prezioso di quanto il depositante della liquidità pensi, quindi è richiesto un importo inferiore.

1 } else {
2 uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
3 assert(amountAOptimal <= amountADesired);
4 require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
5 (amountA, amountB) = (amountAOptimal, amountBDesired);
6
📋 Copia

Se l'importo ottimale di B è superiore all'importo di B desiderato, significa che al momento i token sono meno preziosi di quanto il depositante della liquidità pensi, quindi è necessario un importo maggiore. Tuttavia, l'importo desiderato rappresenta un massimo, quindi non possiamo farlo. Calcoliamo invece il numero ottimale di token A per l'importo desiderato di token B.

Mettendo tutto insieme otteniamo questo grafico. Supponiamo che stai cercando di depositare mille token A (riga blu) e mille token B (linea rossa). L'asse delle x è il tasso di cambio, A/B. Se x=1, significa che hanno pari valore e depositi mille unità di ciascuno. Se x=2, A è il doppio del valore di B (ottieni due token B per ogni token A), quindi depositi mille token B, ma solo 500 token A. Se x=0,5, la situazione è invertita, mille token A e cinquecento token B.

Grafico

1 }
2 }
3 }
4
📋 Copia

Potresti depositare la liquidità direttamente nel contratto principale (usando UniswapV2Pair::mint), ma il contratto principale verifica solo che non abbia luogo una truffa, quindi corri il rischio di perdere valore se il tasso di cambio cambia tra quando invii la transazione e quando viene eseguita. Con il contratto periferico verrà calcolato l'importo da depositare, che verrà depositato immediatamente. In questo modo il tasso di cambio non cambia e non perdi nulla.

1 function addLiquidity(
2 address tokenA,
3 address tokenB,
4 uint amountADesired,
5 uint amountBDesired,
6 uint amountAMin,
7 uint amountBMin,
8 address to,
9 uint deadline
10
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);
4
📋 Copia

Calcoliamo gli importi da depositare realmente e poi troviamo l'indirizzo del pool di liquidità. Per risparmiare sul gas, non ocorre chiedere alla factory, bensì bisogna utilizzare la funzione della libreria pairFor (vedi sotto nelle librerie)

1 TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
2 TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
3
📋 Copia

Trasferisci gli importi corretti di token dall'utente nello scambio in pari.

1 liquidity = IUniswapV2Pair(pair).mint(to);
2 }
3
📋 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 ciò che aveva l'ultima volta che la liquidità è cambiata) e conia liquidità di conseguenza.

1 function addLiquidityETH(
2 address token,
3 uint amountTokenDesired,
4
📋 Copia

Quando un fornitore di liquidità vuole fornire liquidità a uno scambio in pari di Token/ETH, vi sono alcune differenze. Il contratto gestisce l'avvolgimento di ETH per il fornitore di liquidità. Non serve specificare quanti ETH l'utente vuole depositare, perché l'utente li invia insieme alla transazione (l'importo è disponibile in msg.value).

1 uint amountTokenMin,
2 uint amountETHMin,
3 address to,
4 uint 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));
18
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 è avvolto in un assert. Ciò significa che se il trasferimento fallisce, anche la chiamata di questo contratto fallisce e, dunque, l'avvolgimento non si verifica effettivamente.

1 liquidity = IUniswapV2Pair(pair).mint(to);
2 // refund dust eth, if any
3 if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
4 }
5
📋 Copia

L'utente ci ha già inviato l'ETH, quindi se esiste una rimanenza aggiuntiva (poiché l'altro token è meno prezioso di quanto l'utente pensasse), dobbiamo emettere un rimborso.

Rimuovere liquidità

Queste funzioni rimuoveranno la liquidità e ripagheranno il fornitore di liquidità.

1 // **** REMOVE LIQUIDITY ****
2 function removeLiquidity(
3 address tokenA,
4 address tokenB,
5 uint liquidity,
6 uint amountAMin,
7 uint amountBMin,
8 address to,
9 uint deadline
10 ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
11
Mostra tutto
📋 Copia

Il caso più semplice di rimozione di liquidità. Esiste un importo minimo di ogni token che il fornitore di liquidità acconsente di accettare e deve verificarsi prima della scadenza.

1 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
2 IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
3 (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
4
📋 Copia

La funzione di burn del contratto principale gestisce il rimborso dei token all'utente.

1 (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
2
📋 Copia

Quando una funzione restituisce valori multipli, ma siamo interessati solo in alcuni di essi, questo è l'unico modo per ottenere quei valori. In una certa misura è più economico in termini di carburante rispetto a leggere un valore e non usarlo mai.

1 (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
2
📋 Copia

Traduci gli importi dal modo in cui il contratto principale li restituisce (prima il token dell'indirizzo inferiore) al modo atteso dall'utente (corrispondenti a tokenA e tokenB).

1 require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
2 require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
3 }
4
📋 Copia

Va bene effettuare prima il trasferimento e quindi verificare che sia legittimo, perché in caso contrario ripristineremo tutti i cambiamenti di stato.

1 function removeLiquidityETH(
2 address token,
3 uint liquidity,
4 uint amountTokenMin,
5 uint amountETHMin,
6 address to,
7 uint 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 }
22
Mostra tutto
📋 Copia

Rimuovere la liquidità per ETH è quasi la stessa cosa, con la differenza che riceviamo i token WETH e poi li riscattiamo per gli ETH da restituire al fornitore di liquidità.

1 function removeLiquidityWithPermit(
2 address tokenA,
3 address tokenB,
4 uint liquidity,
5 uint amountAMin,
6 uint amountBMin,
7 address to,
8 uint deadline,
9 bool approveMax, uint8 v, bytes32 r, bytes32 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 }
32
Mostra tutto
📋 Copia

Queste funzioni utilizzano le meta-transazioni per consentire agli utenti senza ether di prelevare dal pool, usando il meccanismo di 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
25
Mostra tutto
📋 Copia

Questa funzione è utilizzabile per token aventi commissioni di trasferimento o archiviazione. Quando un token ha tali commissioni, non possiamo utilizzare la funzione removeLiquidity per capire quanti token otterremo indietro, quindi dobbiamo prima prelevare e ottenere il saldo.

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 }
19
Mostra tutto
📋 Copia

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 pair
3 function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
4
📋 Copia

Questa funzione esegue le elaborazioni interne necessarie per le funzioni esposte ai trader.

1 for (uint i; i < path.length - 1; i++) {
2
📋 Copia

Mentre scrivo il presente articolo, esistono 388.160 token ERC-20. Se ci fosse stato uno scambio in pari per ogni coppia di token, avremmo oltre 150 miliardi di scambi in pari. L'intera catena, al momento, ha solo lo 0,1% di quel numero di conti. Invece, le funzioni di scambio supportano il concetto di percorso. Il trader A può scambiare A per B, B per C e C per D. In tal modo non serve uno scambio in pari diretto A-D.

I prezzi su questi mercati tendono a essere sincronizzati, perché quando sono desincronizzati si creano opportunità per l'arbitraggio. Immagina, ad esempio, tre token: A, B e C. Ci sono tre scambi in pari, uno per coppia.

  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. A questo punto il trader vende 24,695 token C per 25,305 token A, mantenendo approssimativamente 0,61 token C come profitto. Il trader ha anche 0,61 token A in più (i 25,305 che finiscono in mano al trader, meno l'investimento originale di 24,695).
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];
4
📋 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));
2
📋 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;
2
📋 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 }
7
📋 Copia

In realtà, chiami lo scambio in pari per scambiare i token. Non ci serve una richiamata sullo scambio, quindi non inviamo alcun byte in quel campo.

1 function swapExactTokensForTokens(
2
📋 Copia

Questa funzione è usata direttamente dai trader per scambiare un token con un altro.

1 uint amountIn,
2 uint amountOutMin,
3 address[] calldata path,
4
📋 Copia

Questo parametro contiene gli indirizzi dei contratti ERC-20. Come spiegato sopra, questo è un array perché potresti avere bisogno di passare attraverso diversi scambi in pari per passare dalla risorsa che hai in mano a quella che desideri.

Un parametro della funzione in Solidity è memorizzabile in memory o in calldata. Se la funzione è un punto d'accesso al contratto, chiamato direttamente da un utente (usando una transazione) o da un contratto diverso, allora il valore del parametro può essere tratto direttamente dai dati della chiamata. Se la funzione è chiamata internamente, come _swap sopra, allora i parametri devono essere memorizzati in memory. Dalla prospettiva del contratto chiamato, calldata è in sola lettura.

Con tipi scalari come uint o address, il compilatore gestisce la scelta dell'archiviazione per noi, mentre con gli array, più lunghi e costosi, specifichiamo il tipo di memoria da usare.

1 address to,
2 uint deadline
3 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
4
📋 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');
3
📋 Copia

Calcola l'importo da acquistare a ogni scambio. Se il risultato è inferiore al minimo che il trader è disposto ad accettare, ripristina la transazione.

1 TransferHelper.safeTransferFrom(
2 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
3 );
4 _swap(amounts, path, to);
5 }
6
📋 Copia

Infine, trasferisci il token ERC-20 iniziale al conto per il primo scambio in pari e chiama _swap. Tutto questo accade nell'ambito della stessa transazione, quindi lo scambio in pari sa che ogni token imprevisto è parte di questo trasferimento.

1 function swapTokensForExactTokens(
2 uint amountOut,
3 uint amountInMax,
4 address[] calldata path,
5 address to,
6 uint 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 }
15
Mostra tutto
📋 Copia

La funzione precedente, swapTokensForTokens, consente a un trader di specificare il numero esatto di token di input che desidera dare e il numero minimo di token di output che vuole ricevere in cambio. Questa funzione effettua lo scambio inverso, consentendo al trader di specificare il numero di token in uscita che vuole ricevere e il numero massimo di token in entrata che è disposto a pagare.

In entrambi i casi, il trader deve dare innanzi tutto a questo contratto periferico un'allowance per consentirgli di effettuare il trasferimento.

1 function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
2 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 }
74
Mostra tutto
📋 Copia

Queste quattro varianti comportano tutte lo scambio tra ETH e token. La sola differenza è che riceviamo ETH dal trader e lo usiamo per coniare WETH o ricviamo WETH dall'ultimo scambio nel percorso e li bruciamo, reinviando al trader l'ETH risultante.

1 // **** SWAP (supporting fee-on-transfer tokens) ****
2 // requires the initial amount to have already been sent to the first pair
3 function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {
4
📋 Copia

Questa è la funzione interna per scambiare token che hanno commissioni di trasferimento o archiviazione da risolvere (questo problema).

1 for (uint i; i < path.length - 1; i++) {
2 (address input, address output) = (path[i], path[i + 1]);
3 (address token0,) = UniswapV2Library.sortTokens(input, output);
4 IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
5 uint amountInput;
6 uint amountOutput;
7 { // scope to avoid stack too deep 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);
12
Mostra tutto
📋 Copia

A causa delle commissioni di trasferimento, non possiamo fare affidamento sulla funzione getAmountsOut per capire quanto otteniamo da ogni trasferimento (come facciamo prima di chiamare lo _swap originale). Dobbiamo prima trasferire e poi vedere quanti token riceviamo indietro.

Nota: In teoria, potremmo semplicemente usare questa funzione al posto di _swap, ma in certi casi (ad esempio, se il trasferimento viene ripristinato perché al termine dell'operazione non vi è abbastanza per soddisfare il minimo richiesto) finiremmo per spendere più carburante. I token della commissione di trasferimento sono abbastanza rari; di conseguenza, benché sia necessario accomodarli, non serve effettuare tutti gli scambi per supporre che passimo attraverso almeno uno di essi.

1 }
2 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));
3 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
4 pair.swap(amount0Out, amount1Out, to, new bytes(0));
5 }
6 }
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 }
75
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}
46
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 SafeMath è 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 // 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;
15
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;
4
📋 Copia

Ottieni una stima più vicina, la media della stima precedente e il numero la cui radice quadrata stiamo provando a trovare, diviso per la stima precedente. Ripeti finché la nuova stima non è inferiore a quella esistente. Per ulteriori dettagli, vedi qui.

1 }
2 } else if (y != 0) {
3 z = 1;
4
📋 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}
4
📋 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;
10
Mostra tutto
📋 Copia

Q112 è la codifica per uno.

1 // encode a uint112 as a UQ112x112
2 function encode(uint112 y) internal pure returns (uint224 z) {
3 z = uint224(y) * Q112; // never overflows
4 }
5
📋 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 UQ112x112
2 function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
3 z = x / uint224(y);
4 }
5}
6
📋 Copia

Se dividiamo due valori UQ112x112, il risultato non è più moltiplicato per 2^112. Prendiamo invece un intero per il denominatore. Avremmo dovuto usare un trucco simile per la moltiplicazione, ma non dobbiamo eseguire la moltiplicazione dei valori 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 }
16
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 l'altra per i parametri B, A, conducendo a 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 }
11
Mostra tutto
📋 Copia

Questa funzione calcola l'indirizzo dello scambio in pari per i due token. Questo contratto è creato usando l'opcode CREATE2; in questo modo 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 }
7
📋 Copia

Questa funzione restituisce le riserve dei due token dello scambio in pari. Tieni presente che può ricevere i token in qualsiasi ordine e ordinarli per 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 }
7
📋 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 del fatto 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 asset
2 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
3
📋 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 scambio dello 0,3%, l'importo effettivamente ottenuto è 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 }
9
📋 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 }
9
📋 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}
24
Mostra tutto
📋 Copia

Queste due funzioni gestiscono l'identificazione dei valori quando è necessario passare per diversi scambi in pari.

Transfer Helper

Questa libreria aggiunge controlli di successo intorno all'ERC-20 e i trasferimenti di Ethereum per trattare un ripristino e un valore restituito 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
15
Mostra tutto
📋 Copia

Esisono due modi per chiamare un contratto diverso:

  • Usare una definizione d'interfaccia per creare la chiamata di una funzione.
  • Usare l'Interfaccia Binaria dell'Applicazione (ABI) "manualmente" 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 }
6
📋 Copia

Per motividi di retrocompatibilità con il token creato prima dello standard ERC-20, una chiamata all'ERC-20 può fallire ripristinando (nel caso in cui success è false) oppure andando a buon fine ma restituendo un valore false (nel qual caso esistono dati in uscita e se li decodifichi come 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 }
15
Mostra tutto
📋 Copia

Questa funzione implementa la funzionalità di trasferimento dell'ERC-20, che consente a un conto di spendere l'allowance fornita da un conto diverso.

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 }
15
Mostra tutto
📋 Copia

Questa funzione implementa la funzionalità di transferFrom dell'ERC-20, che consente a un conto di spendere l'allowance 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}
7
📋 Copia

Questa funzione trasferisce ether a un conto. Ogni chiamata a un contratto diverso può tentare di inviare ether. Poiché non abbiamo bisogno di chiamare concretamente alcuna funzione, non inviamo alcun dato con la chiamata.

Conclusione

Questo è un articolo lungo di circa 50 pagine. Se sei arrivato fin qui, congratulazioni! Ci auguriamo che tu abbia compreso le considerazioni connesse alla scrittura di un'applicazione reale (a differenza dei brevi programmi campione) e sia ora in grado di scrivere più efficacemente i contratti per i tuoi casi d'uso.

Ora vai a scrivere qualcosa di utile e facci sognare.

Ultima modifica: , Invalid DateTime
Modifica la pagina

Questa pagina è stata utile?