Guida dettagliata al contratto Uniswap-v2
Introduzione
Uniswap v2(opens in a new tab) può creare un mercato di scambio tra due token ERC-20. In questo articolo 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(opens in a new tab).
Perché v2? Perché non v3?
Mentre scrivo il presente articolo, Uniswap v3(opens in a new tab) è quasi pronto. Tuttavia, è un aggiornamento molto più complicato dell'originale. È più facile imparare prima la v2 e poi passare alla v3.
Contratti principali e contratti periferici
Uniswap v2 è diviso in due componenti, una principale e una periferica. Questa divisione consente ai contratti principali, che detengono le risorse e dunque devono essere sicuri, di essere più semplici e facili da controllare. Tutte le funzionalità aggiuntive richieste dai trader possono essere fornite dai contratti periferici.
Flussi di dati e di controllo
Questo è il flusso di dati e controllo che si verifica quando esegui le tre azioni principali di Uniswap:
- Scambi token diversi
- Aggiungi liquidità al mercato e vieni ricompensato con token di liquidità ERC-20 di pari valore
- Bruci token di liquidità di ERC-20 e ne ottieni altri, con uno scambio in pari che consente lo scambio ai trader
Scambio
Questo è il flusso più comune, usato dai trader:
Chiamante
- Fornisce al conto perifierico un'allowance nell'importo da scambiare.
- Chiama una delle tante funzioni di scambio del contratto periferico (la funzione chiamata dipende dal fatto che siano o meno coinvolti ETH, dal fatto che il trader specifichi l'importo di token da depositare o da prelevare, ecc.). Ogni funzione di scambio accetta un
path
, un insieme di scambi da attraversare.
Nel contratto periferico (UniswapV2Router02.sol)
- Identifica l'importo necessario da scambiare su ogni scambio lungo il percorso.
- Itera sul percorso. Per ogni scambio lungo il percorso, invia il token di input e poi chiama la funzione di
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)
- Verifica che il contratto principale non raggiri il sistema e possa mantenere liquidità sufficiente dopo lo scambio.
- Vede quanti token aggiuntivi abbiamo, in aggiunta alle riserve note. Quell'importo è il numero di token di input ricevuti da scambiare.
- Invia i token d'output alla destinazione.
- Chiama
_update
per aggiornare gli importi della riserva
Di nuovo nel contratto periferico (UniswapV2Router02.sol)
- Esegue ogni pulizia necessaria (ad esempio, brucia i token di WET per riottenere ETH da inviare al trader)
Aggiungere liquidità
Chiamante
- Fornisce al conto periferico un'allowance negli importi da aggiungere al pool di liquidità.
- Chiama una delle funzioni addLiquidity del contratto periferico.
Nel contratto periferico (UniswapV2Router02.sol)
- Crea un nuovo scambio in pari se necessario
- Se c'è uno scambio in pari esistente, calcola l'importo di token da aggiungere. Questo dovrebbe essere un valore identico per entrambi i token, quindi lo stesso rapporto tra token nuovi ed esistenti.
- Controlla se gli importi sono accettabili (i chiamanti possono specificare un importo minimo al di sotto del quale preferirebbero non aggiungere liquidità)
- Chiama il contratto principale.
Nel contratto principale (UniswapV2Pair.sol)
- Conia token di liquditià e li invia al chiamante
- Chiama
_update
per aggiornare gli importi della riserva
Rimuovere la liquidità
Chiamante
- Fornisce al conto periferico un'allowance di token di liquidità da bruciare in cambio dei token sottostanti.
- Chiama una delle funzioni removeLiquidity del contratto periferico.
Nel contratto periferico (UniswapV2Router02.sol)
- Invia i token di liquidità allo scambio in pari
Nel contratto principale (UniswapV2Pair.sol)
- Invia all'indirizzo di destinazione i token sottostanti in proporzione ai token bruciati. Ad esempio, se ci sono 1.000 token A nel pool, 500 token B e 90 token di liquidità e ne riceviamo 9 da bruciare, bruciamo il 10% dei token di liquidità, quindi reinviamo all'utente 100 token A e 50 token B.
- Brucia i token di liquidità
- Chiama
_update
per aggiornare gli importi della riserva
I contratti principali
Questi sono i contratti sicuri che detengono la liquidità.
UniswapV2Pair.sol
Questo contratto(opens in a new tab) implementa il pool reale che scambia i token. È la funzionalità principale di Uniswap.
1pragma solidity =0.5.16;23import './interfaces/IUniswapV2Pair.sol';4import './UniswapV2ERC20.sol';5import './libraries/Math.sol';6import './libraries/UQ112x112.sol';7import './interfaces/IERC20.sol';8import './interfaces/IUniswapV2Factory.sol';9import './interfaces/IUniswapV2Callee.sol';10Mostra tuttoCopia
Queste sono tutte le interfacce di cui il contratto deve essere a conoscenza, perché le implementa (IUniswapV2Pair
and UniswapV2ERC20
) o perché chiama i contratti che le implementano.
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {2Copia
Questo contratto eredita da UniswapV2ERC20
, che fornisce le funzioni di ERC-20 per i token di liquidità.
1 using SafeMath for uint;2Copia
La libreria SafeMath(opens in a new tab) è usata per evitare flussi eccessivi o insufficienti. È un aspetto importante perché altrimenti potremmo finire in una situazione in cui il valore dovrebbe essere -1
, ma invece è 2^256-1
.
1 using UQ112x112 for uint224;2Copia
Molti calcoli nel contratto di pool richiedono l'uso di frazioni. Tuttavia, le frazioni non sono supportate dall'EVM. La soluzione che Uniswap ha trovato è usare valori a 224 bit, con 112 bit per la parte intera e 112 bit per la frazione. Quindi 1.0
è rappresentato come 2^112
, 1.5
è rappresentato come 2^112 + 2^111
, ecc.
Ulteriori dettagli su questa libreria sono disponibili più in avanti nel documento.
Variabili
1 uint public constant MINIMUM_LIQUIDITY = 10**3;2Copia
Per evitare casi di divisione per zero, esiste un numero minimo di token di liquidità che esiste sempre (ma è posseduto dal conto zero). Quel numero è MINIMUM_LIQUIDITY, mille.
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));2Copia
Questo è il selettore dell'ABI per la funzione di trasferimento dell'ERC-20. È usato per trasferire i token ERC-20 nei due conti di token.
1 address public factory;2Copia
Questo è il contratto factory che ha creato questo pool. Ogni pool rappresenta uno scambio tra due token ERC-20, dove factory è un punto centrale che connette tutti questi pool.
1 address public token0;2 address public token1;3Copia
Sono gli indirizzi dei contratti per due tipi di token ERC-20, scambiabili da questo pool.
1 uint112 private reserve0; // uses single storage slot, accessible via getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReserves3Copia
Le riserve che il pool ha per ogni tipo di token. Supponiamo che i due rappresentino la stessa quantità di valore e dunque che ogni token0 valga token1 di reserve1/reserve0.
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves2Copia
La marca oraria dall'ultimo blocco in cui si è verificato uno scambio, usata per tracciare i tassi di cambio nel tempo.
Una delle più grandi spese di gas dei contratti di 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;3Copia
Queste variabili contengono i costi cumulativi per ogni token (ognuna nel termine dell'altra). Sono utilizzabili per calcolare il tasso di cambio medio su un periodo di tempo.
1 uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event2Copia
Il modo in cui lo scambio in pari decide sul tasso di cambio tra token0 e token1 consiste nel mantenere il multiplo delle due riserve costanti durante gli scambi. kLast
è questo valore. Cambia quando un fornitore di liquidità deposita o preleva token e aumenta lievemente a causa della commissione di mercato dello 0,3%.
Ecco un semplice esempio. Nota che per semplicità la tabella ha solo tre cifre dopo la virgola decimale e che ignoriamo la commissione di gas dello 0,3%, quindi i numeri non sono accurati.
Evento | reserve0 | reserve1 | reserve0 * reserve1 | Tasso di cambio medio (token1/token0) |
---|---|---|---|---|
Configurazione iniziale | 1.000,000 | 1.000,000 | 1.000.000 | |
Trader A scambia 50 token0 per 47,619 token1 | 1.050,000 | 952,381 | 1.000.000 | 0,952 |
Trader B scambia 10 token0 per 8,984 token1 | 1.060,000 | 943,396 | 1.000.000 | 0,898 |
Trader C scambia 40 token0 per 34,305 token1 | 1.100,000 | 909,090 | 1.000.000 | 0,858 |
Trader D scambia 100 token1 per 109,01 token0 | 990,990 | 1.009,090 | 1.000.000 | 0,917 |
Trader E scambia 10 token0 per 10,079 token1 | 1.000,990 | 999,010 | 1.000.000 | 1,008 |
Man mano che i trader forniscono più token0, il valore relativo di token1 aumenta, e viceversa, in basel all'offerta e alla domanda.
Bloccare
1 uint private unlocked = 1;2Copia
Esiste una classe di vulnerabilità di sicurezza basata sull'abuso di rientrata(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
inserita nel contratto, possiamo evitare che le funzioni vengano chiamate mentre sono in esecuzione (nella stessa transazione).
1 modifier lock() {2Copia
Questa funzione è un modificatore(opens in a new tab), una funzione che avvolge una funzione normale per cambiarne in qualche modo il comportamento.
1 require(unlocked == 1, 'UniswapV2: LOCKED');2 unlocked = 0;3Copia
Se unlocked
è pari a uno, impostalo su zero. Se è già zero, ripristina la chiamata, facendola fallire.
1 _;2Copia
In un modificatore _;
è la chiamata alla funzione originale (con tutti i parametri). Significa che la chiamata della funzione ha luogo solo se unlocked
era su uno quando è stata chiamata e, mentre è in esecuzione, il valore di unlocked
è zero.
1 unlocked = 1;2 }3Copia
Al ritorno della funzione principale, rilascia il blocco.
Funzioni varie
1 function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {2 _reserve0 = reserve0;3 _reserve1 = reserve1;4 _blockTimestampLast = blockTimestampLast;5 }6Copia
Questa funzione fornisce ai chiamanti lo stato corrente dello scambio. Nota che le funzioni di Solidity possono restituire valori multipli(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));3Copia
Questa funzione interna trasferisce un importo di token ERC20 dallo scambio a qualcun altro. SELECTOR
specifica che la funzione che stiamo chiamando è transfer(address,uint)
(vedi la definizione sopra).
Per evitare di dover importare un'interfaccia per la funzione del token, creiamo "manualmente" la chiamata usando una delle funzioni dell'ABI(opens in a new tab).
1 require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');2 }3Copia
Esistono due modi in cui una chiamata di trasferimento ERC-20 può segnalare il fallimento:
- Ripristina. Se una chiamata a un contratto esterno si annulla, allora il valore di restituzione booleano è
false
- Termina normalmente ma segnala un guasto. In quel caso il buffer del valore restituito ha una lunghezza diversa da zero e, quando viene codificato come valore booleano, è
false
Se una di queste condizioni si verifica, ripristina.
Eventi
1 event Mint(address indexed sender, uint amount0, uint amount1);2 event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);3Copia
Questi due eventi sono emessi quando un fornitore di liquidità deposita (Mint
) o preleva (Burn
) liquidità. In ogni caso, gli importi di token0 e token1 depositati o prelevati fanno parte dell'evento, così come l'identità del conto che ci ha chiamati (sender
). Nel caso di un prelievo, l'evento include anche la destinazione che ha ricevuto i token (to
), che potrebbe non essere pari al mittente.
1 event Swap(2 address indexed sender,3 uint amount0In,4 uint amount1In,5 uint amount0Out,6 uint amount1Out,7 address indexed to8 );9Copia
Questo evento è emesso quando un trader scambia un token per l'altro. Ancora, il mittente e la destinazione potrebbero non corrispondere. Ogni token potrebe essere inviato allo scambio o ricevuto da esso.
1 event Sync(uint112 reserve0, uint112 reserve1);2Copia
Infine, Sync
viene emesso ogni volta che i token sono aggiunti o prelevati, indipendentemente dal motivo, per fornire le ultime informazioni sulla riserva (e dunque il tasso di cambio).
Funzioni di configurazione
Queste funzioni dovrebbero essere chiamate una volta che il nuovo scambio in pari è configurato.
1 constructor() public {2 factory = msg.sender;3 }4Copia
Il costruttore si assicura che terremo traccia dell'indirizzo della factory che ha creato la coppia. Queste informazioni sono necessarie per initialize
e per la commissione di factory (se presente)
1 // called once by the factory at time of deployment2 function initialize(address _token0, address _token1) external {3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check4 token0 = _token0;5 token1 = _token1;6 }7Copia
Questa funzione consente alla factory (e solo a essa) di specificare i due token ERC-20 che questa coppia scambierà.
Funzioni di aggiornamento interno
_update
1 // update reserves and, on the first call per block, price accumulators2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {3Copia
Questa funzione è chiamata ogni volta che i token vengono depositati o prelevati.
1 require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');2Copia
Se balance0 o balance1 (uint256) è maggiore di uint112(-1) (=2^112-1) (quindi va in overflow e torna a 0 quando viene convertita in uint112), rifiuta di continuare _update per prevenire l'overflow. Con un token normale suddivisibile in 10^18 unità, questo significa che ogni scambio è limitato a circa 5,1*10^15 di ogni token. Finora non è stato un problema.
1 uint32 blockTimestamp = uint32(block.timestamp % 2**32);2 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired3 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {4Copia
Se il tempo trascorso è diverso da zero, significa che la nostra è la prima transazione di scambio su questo blocco. In tal caso, dobbiamo aggiornare gli accumulatori del costo.
1 // * never overflows, and + overflow is desired2 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;3 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;4 }5Copia
Ogni accumulatore del costo è aggiornato con l'ultimo costo (riserva dell'altro token/riserva di questo token) per il tempo trascorso in secondi. Per ottenere un prezzo medio leggi il prezzo cumulativo di due punti nel tempo e dividi per la differenza di tempo tra loro. Ad esempio, supponiamo questa sequenza di eventi:
Evento | reserve0 | reserve1 | marca oraria | Tasso di cambio marginale (reserve1/reserve0) | price0CumulativeLast |
---|---|---|---|---|---|
Configurazione iniziale | 1.000,000 | 1.000,000 | 5.000 | 1,000 | 0 |
Trader A deposita 50 token0 e ottiene 47,619 token1 | 1.050,000 | 952,381 | 5.020 | 0,907 | 20 |
Trader B deposita 10 token0 e ottiene 8,984 token1 | 1.060,000 | 943,396 | 5.030 | 0,890 | 20+10*0,907 = 29,07 |
Trader C deposita 40 token0 e ottiene 34,305 token1 | 1.100,000 | 909,090 | 5.100 | 0,826 | 29,07+70*0,890 = 91,37 |
Trader D deposita 100 token0 e ottiene 109,01 token1 | 990,990 | 1.009,090 | 5.110 | 1,018 | 91,37+10*0,826 = 99,63 |
Trader E deposita 10 token0 e ottiene 10,079 token1 | 1.000,990 | 999,010 | 5.150 | 0,998 | 99,63+40*1.1018 = 143,702 |
Diciamo che vogliamo calcolare il prezzo medio di Token0 tra le marche orarie 5.030 e 5.150. La differenza nel valore di price0Cumulative
è 143,702-29,07=114,632. Questa è la media in due minuti (120 secondi). Quindi il prezzo medio è 114,632/120 = 0,955.
Questo calcolo del prezzo è il motivo per cui dobbiamo conoscere le dimensioni della vecchia riserva.
1 reserve0 = uint112(balance0);2 reserve1 = uint112(balance1);3 blockTimestampLast = blockTimestamp;4 emit Sync(reserve0, reserve1);5 }6Copia
Infine, aggiorna le variabili globali ed emetti un evento Sync
.
_mintFee
1 // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)2 function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {3Copia
In Uniswap 2.0 i trader pagano una commissione dello 0,30% per usare il mercato. Gran parte di tale commissione (0,25% della transazione) va sempre ai fornitori di liquidità. Il rimanente 0,05% può andare ai fornitori di liquidità o a un indirizzo specificato dalla factory come commissione di protocollo, che paga Uniswap per l'attività di sviluppo.
Per ridurre i calcoli (e dunque i costi del carburante), tale commissione è calcolata solo quando la liquidità viene aggiunta o rimossa dal pool, anziché ad ogni operazione.
1 address feeTo = IUniswapV2Factory(factory).feeTo();2 feeOn = feeTo != address(0);3Copia
Leggi la destinazione della commissione della factory. Se è zero, allora non esiste alcuna commissione di protocollo e non serve calcolarla.
1 uint _kLast = kLast; // gas savings2Copia
La variabile di stato kLast
si trova in memoria, quindi avrà un valore tra diverse chiamate al contratto. L'accesso all'archiviazione è molto più costoso dell'accesso alla memoria volatile rilasciata quando termina la chiamata alla funzione al contratto, quindi usiamo una variabile interna per risparmiare sul carburante.
1 if (feeOn) {2 if (_kLast != 0) {3Copia
I fornitori di liquidità ottengono la loro parte semplicemente mediante l'apprezzamento dei loro token di liquidità. Ma la commissione di protocollo richiede che i nuovi token di liquidità siano coniati e forniti all'indirizzo feeTo
.
1 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));2 uint rootKLast = Math.sqrt(_kLast);3 if (rootK > rootKLast) {4Copia
Se c'è nuova liquidità su cui raccogliere una commissione di protocollo. Puoi vedere la funzione della radice quadrata più avanti in questo articolo
1 uint numerator = totalSupply.mul(rootK.sub(rootKLast));2 uint denominator = rootK.mul(5).add(rootKLast);3 uint liquidity = numerator / denominator;4Copia
Questo complicato calcolo delle commissioni è spiegato nel whitepaper(opens in a new tab) a pagina 5. Sappiamo che, tra il momento in cui kLast
è stato calcolato e il presente, non è stata aggiunta o rimossa liquidità (perché eseguiamo questo calcolo ogni volta che la liquidità viene aggiunta o rimossa, prima che cambi effettivamente), quindi qualunque modifica a reserve0 * reserve1
deve provenire dalle commissioni di transazione (senza di esse manterremmo reserve0 * reserve1
costante).
1 if (liquidity > 0) _mint(feeTo, liquidity);2 }3 }4Copia
Usa la funzione UniswapV2ERC20._mint
per creare realmente i token aggiuntivi di liquidità e assegnali a feeTo
.
1 } else if (_kLast != 0) {2 kLast = 0;3 }4 }5Copia
Se non c'è alcuna commissione con kLast
impostato a zero (se non è già così). Alla scrittura di questo contratto, esisteva una funzionalità di rimborso del carburante(opens in a new tab) 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 checks2 function mint(address to) external lock returns (uint liquidity) {3Copia
Questa funzione viene chiamata quando un fornitore di liquidità aggiunge liquidità al pool. Essa conia token di liquidità aggiuntivi a titolo di ricompensa. Dovrebbe essere chiamata da un contratto periferico che la chiama dopo aver aggiunto la liquidità alla stessa transazione (quindi nessun altro può inviare una transazione che rivendichi la nuova liquidità prima del proprietario legittimo).
1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings2Copia
Questo è il modo per leggere i risultati di una funzione di Solidity che restituisce valori multipli. Scartiamo gli ultimi valori restituiti, la marca oraria del blocco, perché non ci servono più.
1 uint balance0 = IERC20(token0).balanceOf(address(this));2 uint balance1 = IERC20(token1).balanceOf(address(this));3 uint amount0 = balance0.sub(_reserve0);4 uint amount1 = balance1.sub(_reserve1);5Copia
Ottiene i saldi correnti e vede quanto è stato aggiunto di ogni tipo di token.
1 bool feeOn = _mintFee(_reserve0, _reserve1);2Copia
Calcola le commissioni di protocollo da raccogliere, se presenti, e conia token di liquidità di conseguenza. Poiché i parametri relativi a _mintFee
sono valori di riserva vecchi, la commissione viene calcolata accuratamente solo secondo le modifiche al pool causate dalle commissioni.
1 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee2 if (_totalSupply == 0) {3 liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);4 _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens5Copia
Se questo è il primo deposito, crea MINIMUM_LIQUIDITY
token e inviali all'indirizzo zero per bloccarli. Non saranno mai riscattabili, il che significa che il pool non sarà mai svuotato completamente (questo ci salva dalla divisione per zero in alcuni punti). Il valore di MINIMUM_LIQUIDITY
è mille e, considerando che gran parte dell'ERC-20 è suddivisa in unità di 10^-18 di un token, come ETH è diviso in wei, corrisponde a 10^-15 al valore di un singolo token. Non è quindi un costo elevato.
Al momento del primo deposito non conosciamo il valore relativo dei due token, quindi ci limitiamo a moltiplicare gli importi e calcoliamo la radice quadrata, supponendo che il deposito ci fornisca un valore pari in entrambi i token.
Possiamo fidarci perché è nell'interesse del depositante fornire un valore pari, così da evitare di perdere valore all'arbitraggio. Diciamo che il valore dei due token è identico, ma il nostro depositante ha depositato quattro volte tanto di Token1 rispetto al Token0. Un trader può avvalersi del fatto che lo scambio in pari pensi che Token0 sia più prezioso per ricavarne valore.
Evento | reserve0 | reserve1 | reserve0 * reserve1 | Valore del pool (reserve0 + reserve1) |
---|---|---|---|---|
Configurazione iniziale | 8 | 32 | 256 | 40 |
Il trader deposita 8 token Token0 e ottiene 16 Token1 | 16 | 16 | 256 | 32 |
Come puoi vedere, il trader ha guadagnato 8 token extra, che provengono da una riduzione nel valore del pool, danneggiando il depositante che li possiede.
1 } else {2 liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);3Copia
Con ogni deposito successivo, conosciamo già il tasso di cambio tra i due attivi e prevediamo che i fornitori di liquidità forniranno pari valore per entrambe. Altrimenti, diamo loro token di liquidità in base al valore inferiore che hanno fornito, a titolo di punizione.
Che sia il deposito iniziale o uno successivo, il numero di token di liquidità che forniamo è pari alla radice quadrata del cambio in reserve0*reserve1
e il valore del token di liquidità non cambia (salvo in presenza di un deposito che non abbia valori pari per entrambi i tipi, nel qual caso la "multa" viene distribuita). Ecco un altro esempio con due token dallo stesso valore, con tre depositi validi e uno inadeguato (deposito di un solo tipo di token, quindi non produce alcun token di liquidità).
Evento | reserve0 | reserve1 | reserve0 * reserve1 | Valore del pool (reserve0 + reserve1) | Token di liquidità coniati per questo deposito | Token di liquidità totali | Valore di ciascun token di liquidità |
---|---|---|---|---|---|---|---|
Configurazione iniziale | 8,000 | 8,000 | 64 | 16,000 | 8 | 8 | 2,000 |
Deposita quattro per tipo | 12,000 | 12,000 | 144 | 24,000 | 4 | 12 | 2,000 |
Deposita due per tipo | 14,000 | 14,000 | 196 | 28,000 | 2 | 14 | 2,000 |
Deposito di valore non pari | 18,000 | 14,000 | 252 | 32,000 | 0 | 14 | ~2,286 |
Dopo l'arbitraggio | ~15,874 | ~15,874 | 252 | ~31,748 | 0 | 14 | ~2,267 |
1 }2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');3 _mint(to, liquidity);4Copia
Usa la funzione UniswapV2ERC20._mint
per creare realmente i token aggiuntivi di liquidità e dare loro l'importo corretto.
12 _update(balance0, balance1, _reserve0, _reserve1);3 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date4 emit Mint(msg.sender, amount0, amount1);5 }6Copia
Aggiorna le variabili di stato (reserve0
, reserve1
e, all'occorrenza, kLast
) ed emetti l'evento appropriato.
burn
1 // this low-level function should be called from a contract which performs important safety checks2 function burn(address to) external lock returns (uint amount0, uint amount1) {3Copia
Questa funzione viene chiamata quando la liquidità è prelevata e i token di liquidità appropriati devono essere bruciati. Dovrebbe essere chiamata anche da un conto periferico.
1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings2 address _token0 = token0; // gas savings3 address _token1 = token1; // gas savings4 uint balance0 = IERC20(_token0).balanceOf(address(this));5 uint balance1 = IERC20(_token1).balanceOf(address(this));6 uint liquidity = balanceOf[address(this)];7Copia
Il contratto periferico ha trasferito la liquidità da bruciare a questo contratto prima della chiamata. Così sappiamo quanta liquidità bruciare e ci possiamo assicurare che sia bruciata.
1 bool feeOn = _mintFee(_reserve0, _reserve1);2 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee3 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution4 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution5 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');6Copia
Il fornitore della liquidità riceve pari valore di entrambi i token. In questo modo non modifichiamo il tasso di cambio.
1 _burn(address(this), liquidity);2 _safeTransfer(_token0, to, amount0);3 _safeTransfer(_token1, to, amount1);4 balance0 = IERC20(_token0).balanceOf(address(this));5 balance1 = IERC20(_token1).balanceOf(address(this));67 _update(balance0, balance1, _reserve0, _reserve1);8 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date9 emit Burn(msg.sender, amount0, amount1, to);10 }1112Mostra tuttoCopia
Il resto della funzione burn
è speculare alla funzione mint
di cui sopra.
swap
1 // this low-level function should be called from a contract which performs important safety checks2 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {3Copia
Anche questa funzione dovrebbe essere chiamata da un contratto periferico.
1 require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');2 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');45 uint balance0;6 uint balance1;7 { // scope for _token{0,1}, avoids stack too deep errors8Copia
Le variabili locali sono memorizzabili in memoria o, se sono troppe, direttamente sullo stack. Se possiamo limitare il numero in modo da usare lo stack consumeremo meno carburante. Per ulteriori dettagli vedi lo yellow paper, le specifiche formali di Ethereum(opens in a new tab), p. 26, equazione 298.
1 address _token0 = token0;2 address _token1 = token1;3 require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');4 if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens6Copia
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);2Copia
Informa il ricevente dello scambio, se richiesto.
1 balance0 = IERC20(_token0).balanceOf(address(this));2 balance1 = IERC20(_token1).balanceOf(address(this));3 }4Copia
Ottieni i saldi correnti. Il contratto periferico ci invia i token prima di chiamarci per lo scambio. In questo modo il contratto può verificare facilmente di non essere oggetto di truffe; tale controllo deve verificarsi nel contratto principale (perché possiamo essere chiamati da altre entità oltre che dal nostro contratto periferico).
1 uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;2 uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;3 require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');4 { // scope for reserve{0,1}Adjusted, avoids stack too deep errors5 uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));6 uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));7 require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');8Copia
Questo è un controllo di sicurezza, per assicurarsi di non perdere in seguito allo scambio. Non esiste alcuna circostanza in cui uno scambio dovrebbe ridurre reserve0*reserve1
. Questo è anche il punto in cui garantiamo che una commissione dello 0,3% è inviata allo scambio; prima di verificare lo stato di salute di K, moltiplichiamo entrambi i saldi per 1000 meno gli importi moltiplicati per 3, questo significa che lo 0,3% (3/1000 = 0,003 = 0,3%) viene dedotto dal saldo prima di confrontare il suo valore K con il valore K delle riserve correnti.
1 }23 _update(balance0, balance1, _reserve0, _reserve1);4 emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);5 }6Copia
Aggiorna reserve0
e reserve1
e, se necessario, gli accumulatori di prezzo e la marca oraria ed emetti un evento.
Sincronizzazione o Skim
È possibile che i saldi reali si desincronizzino rispetto alle riserve considerate dallo scambio in pari. Non c'è modo di prelevare i token senza il consenso del contratto, ma i depositi sono una questione diversa. Un conto può trasferire i token allo scambio senza chiamare mint
o swap
.
In quel caso ci sono due soluzioni:
sync
, che aggiorna le riserve in base ai saldi correntiskim
, che preleva l'importo aggiuntivo. Nota che ogni conto ha la facoltà di chiamareskim
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 reserves2 function skim(address to) external lock {3 address _token0 = token0; // gas savings4 address _token1 = token1; // gas savings5 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));6 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));7 }891011 // force reserves to match balances12 function sync() external lock {13 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);14 }15}16Mostra tuttoCopia
UniswapV2Factory.sol
Questo contratto(opens in a new tab) crea gli scambi in pari.
1pragma solidity =0.5.16;23import './interfaces/IUniswapV2Factory.sol';4import './UniswapV2Pair.sol';56contract UniswapV2Factory is IUniswapV2Factory {7 address public feeTo;8 address public feeToSetter;9