Salt la conținutul principal

Analiza contractului Uniswap-v2

solidity
Intermediar
Ori Pomerantz
1 mai 2021
61 minute de citit minute read

Introducere

Uniswap v2(opens in a new tab) poate crea o piață de schimb între oricare două tokenuri ERC-20. În acest articol vom trece în revistă codul sursă al contractelor care implementează acest protocol și vom vedea de ce sunt scrise în acest fel.

Ce anume face Uniswap?

În principiu, există două tipuri de utilizatori: furnizorii de lichidităţi și comercianții.

Furnizorii de lichidităţi pun la dispoziția fondului comun cele două tokenuri care pot fi schimbate (le vom numi Token0 și Token1). În schimb, aceștia primesc un al treilea token, ce reprezintă o parte din proprietatea asupra fondului comun și este numit token de lichidităţi.

Comercianții trimit un tip de token la fondul comun și îl primesc pe celălalt (de exemplu, trimit Token0 și primesc Token1) din fondul comun pus la dispoziție de furnizorii de lichidităţi. Cursul de schimb este determinat de numărul relativ de Token0 și Token1 care există în fondul comun. În plus, fondul încasează un mic procent ca recompensă pentru fondul comun de lichiditate.

Când furnizorii de lichidităţi își vor înapoi activele, aceștia pot arde tokenurile fondului comun și îşi pot primi înapoi tokenurile, inclusiv partea lor de recompense.

Faceți clic aici pentru a vedea o descriere completă(opens in a new tab).

De ce v2? De ce nu v3?

În momentul în care scriu această prezentare, Uniswap v3(opens in a new tab) este aproape gata. Pe de altă parte, este o actualizare mult mai complicată decât originalul. Este mult mai ușor să învățați v2 și apoi să treceți la v3.

Contracte centrale faţă de contracte periferice

Uniswap v2 este divizat în două componente, una centrală și una periferică. Această diviziune permite contractelor centrale, care dețin activele și ca urmare trebuie să fie sigure, să fie mai simple și mai ușor de auditat. Toate celelalte funcționalități suplimentare cerute de comercianți pot fi atunci furnizate de contractele periferice.

Fluxurile de date și de control

Acesta este fluxul de date și de control ce are loc atunci când efectuați cele trei acțiuni principale ale Uniswap:

  1. Efectuarea de operațiuni de schimb între diferite tokenuri
  2. Adăugarea de lichidităţi pe piață și primirea de recompense prin schimbul perechilor de tokenuri de lichidităţi ERC-20
  3. Arderea de tokenuri de lichidităţi ERC-20 și primirea înapoi a tokenurilor ERC-20 pe care îl permite comercianţilor schimbul în pereche

Schimburile

Acesta este cel mai obișnuit flux folosit de comercianți:

Apelantul

  1. Furnizează o alocație contului periferic de valoare egală cu cea care trebuie schimbată.
  2. Apelează una dintre numeroasele funcții de schimb ale contractului periferic (care depinde fie de faptul că implică ETH sau nu, fie de specificarea de către comerciant a numărului de tokenuri de depus sau de luat înapoi etc.). Orice funcție de schimb acceptă o „cale” path, o matrice de schimburi prin care să treacă.

În contractul periferic (UniswapV2Router02.sol)

  1. Identifică suma care trebuie tranzacționată la fiecare schimb de-a lungul căii.
  2. Se repetă de-a lungul căii. Pentru fiecare schimb de pe parcurs, trimite tokenul introdus și apoi apelează funcția swap a schimbului. În cele mai multe cazuri, adresa de destinație pentru tokenuri este următorul schimb în pereche de pe cale. La schimbul final, aceasta este adresa furnizată de comerciant.

În contractul central (UniswapV2Pair.sol)

  1. Verifică să nu se fraudeze contractul central și dacă acesta poate menține suficiente lichidităţi după efectuarea schimbului.
  2. Vede câte tokenuri suplimentare avem în plus față de rezervele cunoscute. Această valoare reprezintă numărul de tokenuri introduse pe care le-am primit pentru schimb.
  3. Trimite tokenurile rezultate la destinație.
  4. Apelează _update pentru a actualiza cantitatea de rezervă

Înapoi în contractul periferic (UniswapV2Router02.sol)

  1. Efectuează orice activitate de curățire este necesară (de exemplu, arde tokenurile WETH pentru a primi înapoi ETH, pe care să îl trimită comerciantului)

Adăugarea de lichidități

Apelantul

  1. Furnizează contului periferic o alocație egală cu sumele care trebuie adăugate la fondul comun de lichidităţi.
  2. Apelează una din funcțiile contractului periferic, și anume „addLiquidity”.

În contractul periferic (UniswapV2Router02.sol)

  1. Creează un nou schimb în pereche dacă este necesar
  2. Dacă un astfel de schimb în pereche există deja, calculează suma de tokenuri de adăugat. Deoarece se presupune că valorile ambelor tokenuri sunt identice, se va adăuga aceeaşi proporţie de tokenuri la cele existente.
  3. Verifică dacă sumele sunt rezonabile (apelanții pot specifica suma minimă sub care nu sunt dispuși să adauge lichidități)
  4. Apelează contractul central.

În contractul central (UniswapV2Pair.sol)

  1. Emite tokenurile de lichidităţi și le trimite către apelant
  2. Apelează _update pentru a actualiza cantitatea de rezervă

Eliminarea de lichidități

Apelantul

  1. Furnizează contului periferic o alocație de tokenuri de lichidităţi care trebuie arse în schimbul tokenurilor preexistente.
  2. Apelează una din funcțiile contractului periferic, și anume „removeLiquidity”.

În contractul periferic (UniswapV2Router02.sol)

  1. Trimite tokenurile de lichidităţi la schimbul în pereche

În contractul central (UniswapV2Pair.sol)

  1. Trimite la adresa de destinație tokenurile preexistente corespunzătoare, proporțional cu jetoanele arse. De exemplu, dacă în fondul comun există 1000 de tokenuri A, 500 de tokenuri B și 90 de tokenuri de llichidităţi și primim 9 tokenuri de lichidităţi pentru a fi arse, ardem 10% din tokenurile de lichidităţi și trimitem înapoi utilizatorului 100 de tokenuri A și 50 de tokenuri B.
  2. Arde tokenurile de lichidităţi
  3. Apelează _update pentru a actualiza cantitatea de rezervă

Contractele centrale

Acestea sunt contractele securizate care dețin lichidități.

UniswapV2Pair.sol

Acest contract(opens in a new tab) implementează fondul comun propriu-zis care face schimbul de tokenuri. Aceasta este funcționalitatea centrală a 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';
Afișează tot
Copiați

Toate acestea sunt interfețele despre care contractul trebuie să știe, fie deoarece sunt implementate de contract (IUniswapV2Pair și UniswapV2ERC20), fie pentru că apelează la contracte care le implementează.

1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
Copiați

Acest contract moștenește de la UniswapV2ERC20, care furnizează funcțiile ERC-20 pentru tokenurile de lichidităţi.

1 using SafeMath for uint;
Copiați

Biblioteca SafeMath library(opens in a new tab) este folosită pentru depășiri și subdepășiri. Este important deoarece altfel am putea să ajungem la o situație în care o valoare ar trebui să fie -1, în schimb ea este 2^256-1.

1 using UQ112x112 for uint224;
Copiați

O mulțime de calcule din cadrul contractului fondului de lichidităţi necesită fracții. Însă EVM nu acceptă fracțiile. Soluția pe care a găsit-o Uniswap a fost utilizarea de valori de 224 de biţi, cu 112 de biţi pentru partea întreagă și 112 de biţi pentru fracție. Astfel, 1.0 este reprezentat ca 2^112, 1.5 este reprezentat ca 2^112 + 2^111 etc.

Mai multe detalii despre această bibliotecă sunt disponibile mai departe în acest document.

Variabile

1 uint public constant MINIMUM_LIQUIDITY = 10**3;
Copiați

Pentru a se evita cazuri de împărțire la zero, există întotdeauna un minimum de tokenuri de lichidităţi (al căror proprietar este contul zero). Acest număr este MINIMUM_LIQUIDITY, o mie.

1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
Copiați

Acesta este selectorul ABI pentru funcția de transfer a ERC-20. Este folosit pentru transferul de tokenuri ERC-20 în cele două conturi de tokenuri.

1 address public factory;
Copiați

Acesta este contractul fabrică, cel care a creat acest fond comun. Fiecare fond comun este un schimb între două tokenuri ERC-20, iar fabrica este punctul central care unește toate aceste fonduri comune.

1 address public token0;
2 address public token1;
Copiați

Acestea sunt adresele contractelor pentru cele două tipuri de tokenuri ERC-20 care pot fi schimbate prin fondul comun.

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

Iar acestea sunt rezervele pe care le are fondul comun pentru fiecare tip de token. Presupunem că cele două rezerve reprezintă cantitativ aceeași valoare, prin urmare „token0” valorează „reserve1/reserve0” a „token1”.

1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves
Copiați

Aici avem marca temporală a ultimului bloc în care a avut loc un schimb și care este folosită pentru a urmări evoluţia ratelor de schimb în timp.

Una dintre cele mai mari cheltuieli de gaz în contractele Ethereum este stocarea, care persistă de la un apel al contractului la următorul. Fiecare celulă de stocare are o lungime de 256 de biţi. So three variables, reserve0, reserve1, and blockTimestampLast, are allocated in such a way a single storage value can include all three of them (112+112+32=256).

1 uint public price0CumulativeLast;
2 uint public price1CumulativeLast;
Copiați

Aceste variabile dețin costurile cumulative ale fiecărui token (fiecare prin raportare la celălalt). They can be used to calculate the average exchange rate over a period of time.

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

Modul în care schimbul în pereche stabilește cursul de schimb între „token0” și „token1” este de a menține multiplul dintre cele două rezerve constant în timpul tranzacțiilor. kLast este această valoare. Acesta se modifică atunci când un furnizor de lichidități depune sau retrage tokenuri și crește ușor datorită unui comision de piață de 0,3%.

Aici avem un exemplu simplu. Observați că, din motive de simplitate, tabloul are numai trei zecimale după virgulă și nu ținem cont de comisionul de tranzacționare de 0,3%, de aceea valorile nu sunt exacte.

Evenimentreserve0reserve1reserve0 * reserve1Curs mediu de schimb (token1 / token0)
Situaţia inițială1.000,0001.000,0001.000.000
Comerciantul A schimbă 50 de token0 pe 47,619 token11.050,000952,3811.000.0000,952
Comerciantul B schimbă 10 token0 pentru 8,984 token11.060,000943,3961.000.0000,898
Comerciantul C schimbă 40 de token0 pe 34,305 token11.100,000909,0901.000.0000,858
Comerciantul D schimbă 100 de token1 pe 109,01 token0990,9901.009,0901.000.0000,917
Comerciantul E schimbă 10 token0 pe 10,079 token11.000,990999,0101.000.0001,008

Pe măsură ce comercianții furnizează mai multe „token0”, valoarea relativă a „token1” crește și viceversa, în funcție de ofertă și cerere.

Blocarea

1 uint private unlocked = 1;
Copiați

Există o clasă de vulnerabilități de securitate care se bazează pe reentrancy abuse(opens in a new tab) (abuzul de reintrare). Uniswap are nevoie să transfere jetoane ERC-20 în mod arbitrar, adică prin apelarea contractelor ERC-20, care pot încerca să abuzeze de piața Uniswap care le apelează. Având o variabilă unlocked ce face parte din contract, putem evita ca funcțiile să fie apelate în timp ce sunt executate (în cadrul aceleiași tranzacții).

1 modifier lock() {
Copiați

Această funcție este un modificator(opens in a new tab), o funcție care se înfășoară în jurul unei funcții normale pentru a-i schimba comportamentul într-un anumit fel.

1 require(unlocked == 1, 'UniswapV2: LOCKED');
2 unlocked = 0;
Copiați

Dacă variabila unlocked este egală cu unu, setați-o la zero. Dacă aceasta este deja zero, întoarceți apelul, faceți-l să eșueze.

1 _;
Copiați

Într-un modificator _; este apelul funcției originale (cu toți parametrii). Aici înseamnă că apelul funcției are loc numai dacă unlocked avea valoarea unu când a fost apelată, iar în timp ce se execută valoarea unlocked este zero.

1 unlocked = 1;
2 }
Copiați

După revenirea funcției principale, eliberați blocajul.

Diverse funcții

1 function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
2 _reserve0 = reserve0;
3 _reserve1 = reserve1;
4 _blockTimestampLast = blockTimestampLast;
5 }
Copiați

Această funcție informează apelanții care este starea actuală a schimbului. Observați că funcțiile Solidity pot răspunde prin mai multe valori(opens in a new tab).

1 function _safeTransfer(address token, address to, uint value) private {
2 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
Copiați

Această funcție internă transferă o sumă de tokenuri ERC20 din schimb altcuiva. SELECTOR specifies that the function we are calling is transfer(address,uint) (see definition above).

Pentru a evita importul unei interfețe pentru funcția „token”, vom crea „manual” apelul folosind una dintre funcțiile ABI(opens in a new tab).

1 require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
2 }
Copiați

Există două modalități prin care un apel de transfer ERC-20 poate raporta un eșec:

  1. Revenirea. If a call to an external contract reverts, then the boolean return value is false
  2. Se termină normal, dar raportează un eșec. În cazul acesta, bufferul valorii de retur are o lungime diferită de zero, iar când este decodat ca valoare booleană este false

Dacă apar oricare dintre aceste condiții, reveniți.

Evenimente

1 event Mint(address indexed sender, uint amount0, uint amount1);
2 event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
Copiați

Aceste două evenimente sunt emise atunci când un furnizor de lichidități fie depune lichidităţi (Mint), fie le retrage (Burn). În oricare din cazuri, sumele de token0 și token1 care sunt depuse sau retrase fac parte din eveniment, precum și identitatea contului care ne-a apelat (sender). În cazul unei retrageri, evenimentul include și ținta care a primit tokenurile (to), care poate să nu fie aceeași cu expeditorul.

1 event Swap(
2 address indexed sender,
3 uint amount0In,
4 uint amount1In,
5 uint amount0Out,
6 uint amount1Out,
7 address indexed to
8 );
Copiați

Acest eveniment este emis atunci când un comerciant schimbă un token pe un altul. Repet, expeditorul și destinatarul pot să nu fie aceiași. Fiecare token poate fi ori trimis la schimb, ori primit prin acesta.

1 event Sync(uint112 reserve0, uint112 reserve1);
Copiați

În final, funcția Sync este emisă de fiecare dată când se depun sau se retrag tokenuri, indiferent de motiv, pentru a furniza cea mai recentă informație despre rezervă (și implicit cursul de schimb).

Funcțiile de configurare

Se presupune că aceste funcții vor fi apelate o singură dată, atunci când se creează un nou schimb în pereche.

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

Constructorul se asigură că vom păstra evidența adresei fabricii care a creat perechea. Această informație este necesară pentru inițializare initialize și pentru taxa de fabrică (dacă există vreuna)

1 // called once by the factory at time of deployment
2 function initialize(address _token0, address _token1) external {
3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
4 token0 = _token0;
5 token1 = _token1;
6 }
Copiați

Această funcție permite fabricii (și numai fabricii) să specifice cele două tokenuri ERC-20 pe care le va schimba acestă pereche.

Funcții interne de actualizare

_update
1 // update reserves and, on the first call per block, price accumulators
2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
Copiați

Această funcție este apelată de fiecare dată când sunt depozitate sau retrase tokenurile.

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

If either balance0 or balance1 (uint256) is higher than uint112(-1) (=2^112-1) (so it overflows & wraps back to 0 when converted to uint112) refuse to continue the _update to prevent overflows. With a normal token that can be subdivided into 10^18 units, this means each exchange is limited to about 5.1*10^15 of each tokens. Până acum, aceasta nu a fost o problemă.

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

Dacă timpul scurs nu este zero, înseamnă că suntem prima tranzacție de schimb din acest bloc. În acest caz, trebuie să actualizăm acumulatorii de costuri.

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

Fiecare acumulator de costuri este actualizat cu cel mai recent cost (rezerva celuilalt token/rezerva acestui token) înmulțit cu timpul scurs în secunde. Pentru a obține un preț mediu, citiți prețul cumulat la două momente și împărțiți la diferența de timp dintre ele. De exemplu, să presupunem această succesiune de evenimente:

Evenimentreserve0reserve1marcă temporalăCursul de schimb marginal (reserve1 / reserve0)price0CumulativeLast
Situaţia inițială1.000,0001.000,0005.0001,0000
Comerciantul A depune 50 de token0 și obține 47,619 token1 înapoi1.050,000952,3815.0200,90720
Comerciantul B depune 10 token0 și obține 8,984 token1 înapoi1.060,000943,3965.0300,89020+10*0,907 = 29,07
Comerciantul C depune 40 de token0 și obține 34,305 token1 înapoi1.100,000909,0905.1000,82629,07+70*0,890 = 91,37
Comerciantul D depune 100 de token1 și obține 109,01 token0 înapoi990,9901.009,0905.1101,01891,37+10*0,826 = 99,63
Comerciantul E depune 10 token0 și obține 10,079 token1 înapoi1.000,990999,0105.1500,99899,63+40*1,1018 = 143,702

Să spunem că vrem să calculăm prețul mediu al Token0 între mărcile temporale 5.030 și 5.150. Diferența de valoare dintre price0Cumulative este 143,702-29,07=114,632. Aceasta este media pe două minute (120 de secunde). Așa că prețul mediu este 114,632/120 = 0,955.

Acest calcul de preț este motivul pentru care trebuie să cunoaștem mărimea vechilor rezerve.

1 reserve0 = uint112(balance0);
2 reserve1 = uint112(balance1);
3 blockTimestampLast = blockTimestamp;
4 emit Sync(reserve0, reserve1);
5 }
Copiați

În sfârșit, actualizează variabilele globale și emite un eveniment Sync.

_mintFee
1 // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)
2 function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
Copiați

În Uniswap 2.0, comercianții plătesc o taxă de 0,30% pentru a utiliza piața. Cea mai mare parte a acestei taxe (0,25% din tranzacție) ajunge totdeauna la furnizorii de lichidități. Restul de 0,05% poate ajunge fie la furnizorii de lichidități, fie la o adresă specificată de fabrică drept taxă de protocol, care plătește Uniswap pentru munca de dezvoltare.

Pentru reducerea calculelor (și prin urmare a costurilor de gaz), această taxă este calculată doar când lichidităţile sunt adăugate sau scoase din fondul comun, și nu la fiecare tranzacție.

1 address feeTo = IUniswapV2Factory(factory).feeTo();
2 feeOn = feeTo != address(0);
Copiați

Citește adresa de destinație a fabricii. Dacă este zero, atunci nu există nicio taxă de protocol și nu este nevoie să se calculeze această taxă.

1 uint _kLast = kLast; // gas savings
Copiați

Variabila de stare kLast se află în memorie și de aceea va avea o valoare între diferite apeluri la contract. Accesul la memorie este cu mult mai scump decât accesul la memoria volatilă eliberată când se termină apelul funcției la contract, de aceea vom utiliza o variabilă internă pentru a economisi gaz.

1 if (feeOn) {
2 if (_kLast != 0) {
Copiați

Furnizorii de lichidităţi își obțin partea lor prin simpla apreciere a tokenurilor lor de lichidităţi. Însă taxa de protocol necesită emiterea de noi tokenuri de lichidităţi și furnizarea acestora la adresa feeTo.

1 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
2 uint rootKLast = Math.sqrt(_kLast);
3 if (rootK > rootKLast) {
Copiați

În cazul în care există noi lichidități pentru care să colecteze o taxă de protocol. Puteți vedea funcția „sqrt” (rădăcină pătrată) mai departe în acest articol

1 uint numerator = totalSupply.mul(rootK.sub(rootKLast));
2 uint denominator = rootK.mul(5).add(rootKLast);
3 uint liquidity = numerator / denominator;
Copiați

Acest calcul complicat de taxe este explicat în Whitepaper(opens in a new tab), la pagina 5. Știm că între momentul în care a fost calculat kLast și momentul actual nu s-au adăugat sau retras lichidităţi (pentru că acest calcul se execută de fiecare dată când sunt adăugate sau retrase lichidități, înainte ca acesta să se modifice efectiv), așa că orice schimbare în reserve0 * reserve1 trebuie că provine din taxele de tranzacții (fără de care reserve0 * reserve1 ar rămâne constantă).

1 if (liquidity > 0) _mint(feeTo, liquidity);
2 }
3 }
Copiați

Folosește funcția UniswapV2ERC20._mint pentru a crea efectiv tokenurile de lichidităţi suplimentare și pentru a le atribui la feeTo.

1 } else if (_kLast != 0) {
2 kLast = 0;
3 }
4 }
Copiați

În caz că nu există nicio taxă, setează kLast la zero (dacă nu este deja setat astfel). Când a fost scris acest contract, exista o funcție de rambursare a gazului(opens in a new tab) care încuraja contractele să reducă dimensiunea totală a stării Ethereum, prin reducerea la zero a stocării de care nu aveau nevoie. Acest cod obține această rambursare atunci când este posibil.

Funcții accesibile din exterior

Rețineți că, deși orice tranzacție sau contract poate să apeleze aceste funcții, ele au fost proiectate să fie apelate din contractul periferic. Dacă le apelați direct, nu veți putea frauda schimbul în pereche, dar s-ar putea să pierdeți din valoare din cauza unei greșeli.

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

Această funcție este apelată atunci când un furnizor de lichidităţi adaugă lichidități într-un fond comun. Aceasta emite tokenuri de lichidităţi suplimentare ca recompensă. Ar trebui să fie apelată dintr-un contract de periferie care să o apeleze după adăugarea lichidităților în cadrul aceleiași tranzacții (astfel încât nimeni să nu poată trimite o tranzacție care să revendice noua lichiditate înaintea proprietarului legitim).

1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
Copiați

Acesta este modul în care citim rezultatele unei funcții Solidity care răspund prin mai multe valori. Eliminăm ultima valoare de răspuns, marca temporală a blocului, deoarece nu avem nevoie de aceasta.

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

Obține soldurile curente și vede cât s-a adăugat din fiecare tip de token.

1 bool feeOn = _mintFee(_reserve0, _reserve1);
Copiați

Calculează taxele de protocol care trebuie colectate, dacă este cazul, și emite tokenurile de lichidităţi în consecință. Deoarece parametrii pentru _mintFee sunt vechile valori ale rezervei, taxa este calculată cu exactitate numai pe baza schimbărilor din fondul comun datorate taxelor.

1 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
2 if (_totalSupply == 0) {
3 liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
4 _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
Copiați

Dacă acesta este prima depunere, creează jetoanele MINIMUM_LIQUIDITY și le trimite la adresa zero pentru a fi blocate. Acestea nu pot fi niciodată revendicate, adică fondul de lichidităţi nu va fi golit complet niciodată (ceea ce ne va salva de împărțirea la zero în anumite locuri). Valoarea MINIMUM_LIQUIDITY este de o mie de tokenuri, iar având în vedere că majoritatea tokenurilor ERC-20 sunt subdivizate în 10^-18 unități, așa cum ETH este divizat în wei, valoarea unui singur astfel de token poate fi divizată în 10^-15 unități. Costurile nu sunt mari.

În momentul primei depuneri, nu cunoaștem valoarea relativă a celor două tokenuri, de aceea înmulțim cele două sume și scoatem rădăcina lor pătrată, presupunând că depozitul ne va oferi o valoare egală din ambele tokenuri.

Putem avea încredere în acest lucru, deoarece este în interesul deponentului să ofere o valoare egală, pentru a evita pierderea de valoare în urma arbitrajului. Să presupunem că valoarea celor două tokenuri este identică, dar deponentul nostru a depus de 4 ori mai multe Token1 decât Token0. Un comerciant poate utiliza faptul că un schimb în pereche consideră că Token0 este mai preţios pentru a extrage valoare din el.

Evenimentreserve0reserve1reserve0 * reserve1Valoarea fondului comun (reserve0 + reserve1)
Situaţia inițială83225640
Comerciantul depozitează 8 Token0, primește înapoi 16 Token1161625632

După cum puteți vedea, comerciantul câștigă 8 tokenuri suplimentare, care provin dintr-o reducere a valorii fondului comun, aducând pagube deponentului care le deține.

1 } else {
2 liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
Copiați

Cu fiecare depunere ulterioară, cunoaștem deja rata de schimb dintre cele două active și ne așteptăm ca furnizorii de lichidităţi să ne ofere o valoare egală în ambele. Dacă nu o fac, le dăm tokenuri de lichidităţi calculate în funcție de valoarea cea mai mică pe care au oferit-o, ca pedeapsă.

Fie că este vorba de depunerea inițială sau de una ulterioară, numărul de tokenuri de lichidităţi pe care le oferim este egal cu rădăcina pătrată a modificării în reserve0*reserve1, iar valoarea tokenului de lichidităţi nu se modifică (în afara cazului când primim un depozit care nu are valori egale pentru ambele tipuri, caz în care „amenda” este distribuită). Iată un alt exemplu cu două tokenuri care au aceeași valoare, cu trei depuneri bune și una rea (depunerea unui singur tip de token deci nu produce niciun token de lichidităţi).

Evenimentreserve0reserve1reserve0 * reserve1Valoarea fondului comun (reserve0 + reserve1)Tokenuri de lichidităţi emise pentru acest depozitTotal tokenuri de lichidităţivaloarea fiecărui token de lichidităţi
Situaţia inițială8,0008,0006416,000882,000
Depune patru din fiecare tip12,00012,00014424,0004122,000
Depune două din fiecare tip14,00014,00019628,0002142,000
Depozit de valoare inegală18,00014,00025232,000014~2,286
După arbitraj~15,874~15,874252~31,748014~2,267
1 }
2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
3 _mint(to, liquidity);
Copiați

Folosește funcția UniswapV2ERC20._mint ca să creeze efectiv tokenurile de lichidităţi suplimentare și le distribuie în contul corect.

1
2 _update(balance0, balance1, _reserve0, _reserve1);
3 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
4 emit Mint(msg.sender, amount0, amount1);
5 }
Copiați

Actualizează variabilele de stare (reserve0, reserve1 și de asemenea kLast) dacă este necesar, și emite evenimentul corespunzător.

„burn”
1 // this low-level function should be called from a contract which performs important safety checks
2 function burn(address to) external lock returns (uint amount0, uint amount1) {
Copiați

Această funcție este apelată atunci când se retrag lichidităţi, iar tokenurile de lichidităţi corespunzătoare trebuie să fie arse. De asemenea, ar trebui să fie apelată dintr-un contract periferic.

1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
2 address _token0 = token0; // gas savings
3 address _token1 = token1; // gas savings
4 uint balance0 = IERC20(_token0).balanceOf(address(this));
5 uint balance1 = IERC20(_token1).balanceOf(address(this));
6 uint liquidity = balanceOf[address(this)];
Copiați

Contractul periferic a transferat lichidităţile care trebuie arse în acest contract înainte de apel. În acest fel, știm câte lichidităţi rebuie să ardem și ne putem asigura că vor fi arse.

1 bool feeOn = _mintFee(_reserve0, _reserve1);
2 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
3 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
4 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
5 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
Copiați

Furnizorul de lichidităţi primește o valoare egală din ambele tokenuri. În acest fel nu modificăm rata de schimb.

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
Afișează tot
Copiați

Restul funcției burn este o imagine în oglindă a funcției mint de mai sus.

„swap”
1 // this low-level function should be called from a contract which performs important safety checks
2 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
Copiați

Această funcție se presupune şi că trebuie să fie apelată de la un contract periferic.

1 require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
2 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
4
5 uint balance0;
6 uint balance1;
7 { // scope for _token{0,1}, avoids stack too deep errors
Copiați

Variabilele locale pot fi stocate în memorie sau, dacă nu sunt prea multe, direct pe stivă. Dacă putem limita numărul lor pentru a folosi stiva, folosim mai puțin gaz. Pentru mai multe detalii, a se vedea cartea galbenă, specificațiile formale Ethereum(opens in a new tab), pag. 26, ecuația 298.

1 address _token0 = token0;
2 address _token1 = token1;
3 require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
4 if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
Copiați

Acest transfer este optimist, deoarece transferăm înainte de a fi siguri că toate condițiile sunt îndeplinite. În Ethereum acest lucru este în regulă, deoarece, dacă aceste condiții nu sunt îndeplinite mai târziu în apel, atunci ne retragem din apel şi din modificările create de acesta.

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

Informează destinatarul despre swap dacă i se solicită.

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

Obține soldurile curente. Contractul periferic ne trimite tokenurile înainte de a ne apela pentru swap. Aceasta facilitează verificarea de către contract dacă nu a fost fraudat, verificare ce trebuie să aibă loc în contractul central (pentru că putem fi apelați de alte entități decât contractul nostru periferic).

1 uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
2 uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
3 require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
4 { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
5 uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
6 uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
7 require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
Copiați

Acesta este un control al sănătăţii, pentru a ne asigura că nu pierdem în urma swap-ului. Sub nicio formă swap-ul nu ar trebui să reducă reserve0*reserve1. This is also where we ensure a fee of 0.3% is being sent on the swap; before sanity checking the value of K, we multiply both balances by 1000 subtracted by the amounts multiplied by 3, this means 0.3% (3/1000 = 0.003 = 0.3%) is being deducted from the balance before comparing its K value with the current reserves K value.

1 }
2
3 _update(balance0, balance1, _reserve0, _reserve1);
4 emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
5 }
Copiați

Actualizează reserve0 și reserve1, iar dacă este necesar acumulatorii de preț și marca temporală, apoi emite un eveniment.

Sync or Skim

Este posibil ca soldurile reale să fie desincronizate de rezervele pe care crede că le are schimbul în pereche. Nu există nicio posibilitate de a retrage tokenuri fără aprobarea contractului, însă depozitele sunt o cu totul altă problemă. Un cont poate transfera tokenuri către schimb fără să apeleze nici la mint, nici la swap.

În acest caz, există două soluții:

  • sync, actualizarea rezervelor la soldurile curente
  • skim, retragerea sumei suplimentare. Rețineți că orice cont poate apela skim, deoarece nu știm cine a depus tokenurile. Această informație este emisă într-un eveniment, dar evenimentele nu sunt accesibile din 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}
Afișează tot
Copiați

UniswapV2Factory.sol

Acest contract(opens in a new tab) creează schimbul în pereche.

1pragma solidity =0.5.16;
2
3import './interfaces/IUniswapV2Factory.sol';
4import './UniswapV2Pair.sol';
5
6contract UniswapV2Factory is IUniswapV2Factory {
7 address public feeTo;
8 address public feeToSetter;
Copiați

Aceste variabile de stare sunt necesare pentru implementarea taxei de protocol, (a se vedea cartea albă(opens in a new tab), pag. 5). Adresa feeTo acumulează tokenurile de lichidităţi pentru taxa de protocol, iar adresa feeToSetter este adresa care permite schimbarea feeTo la o adresă diferită.

1 mapping(address => mapping(address => address)) public getPair;
2 address[] public allPairs;
Copiați

Aceste variabile țin evidența perechilor, și anume a schimburilor dintre cele două tipuri de tokenuri.

Prima, getPair, este o mapare care identifică un contract de schimb în perechi, bazată pe cele două tokenuri ERC-20 pe care le schimbă. Tokenurile ERC-20 sunt identificate prin adresele contractelor care le implementează, așadar cheile și valoarea sunt toate adrese. Pentru a obține adresa schimbului în pereche care vă permite să convertiți din tokenA în tokenB, utilizați getPair[<tokenA address>][<tokenB address>] (sau viceversa).

A doua variabilă, allPairs, este o matrice care include toate adresele schimbului în pereche create de această fabrică. În Ethereum, nu este posibilă iterarea peste conținutul unei mapări, sau obținerea unei liste cu toate cheile, deci această variabilă este singura modalitate de a afla ce schimburi gestionează această fabrică.

Observaţie: Motivul pentru care nu puteți itera peste toate cheile unei mapări este acela că stocarea datelor contractului este costisitoare, de aceea, cu cât utilizăm mai puţin din ea, cu atât mai bine și cu cât o modificăm mai rar, cu atât mai bine. Puteți să creați mapări care suportăacceptă iterația(opens in a new tab), dar acestea necesită stocarea suplimentară a unei liste de chei. În majoritatea aplicațiilor nu aveți nevoie de aceasta.

1 event PairCreated(address indexed token0, address indexed token1, address pair, uint);
Copiați

Acest eveniment este emis atunci când se creează un nou schimb în pereche. Acesta include adresa tokenurilor, adresa schimbului în pereche și numărul total de schimburi gestionate de fabrică.

1 constructor(address _feeToSetter) public {
2 feeToSetter = _feeToSetter;
3 }
Copiați

Singurul lucru pe care îl face constructorul este să specifice feeToSetter. Fabricile încep fără taxă și numai feeToSetter poate să schimbe acest lucru.

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

Această funcție răspunde prin numărul de perechi de schimb.

1 function createPair(address tokenA, address tokenB) external returns (address pair) {
Copiați

Aceasta este funcția principală a fabricii, să creeze un schimb în pereche între două tokenuri ERC-20. Rețineți că oricine poate apela acestă funcție. Nu aveți nevoie de autorizația Uniswap pentru a crea un nou schimb în pereche.

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

Vrem ca adresa noului schimb să fie deterministă, încât să poată fi calculată în avans off-chain (acest lucrul poate fi util pentru tranzacțiile de nivel 2). Pentru a face aceasta, avem nevoie de o ordine consecventă a adreselor tokenurilor, indiferent de ordinea în care le-am primit, de aceea le sortăm aici.

1 require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
2 require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
Copiați

Fondurile comune de lichidități mai mari sunt mai bune decât cele mici, pentru că au prețuri mai stabile. Nu dorim să avem mai mult de un singur fond comun de lichidități pe fiecare pereche de tokenuri. Dacă un schimb există deja, nu este nevoie să creați un altul pentru aceeași pereche.

1 bytes memory bytecode = type(UniswapV2Pair).creationCode;
Copiați

Pentru a crea un nou contract, avem nevoie de codul care îl creează (atât funcția constructorului, cât și codul care scrie în memorie bytecode-ul EVM al contractului real). În mod normal, în Solidity, folosim numai addr = new <name of contract>(<constructor parameters>), iar compilatorul se ocupă de toate în locul nostru, dar pentru a avea o adresă de contract deterministă, trebuie să folosim opcode-ul CREATE2(opens in a new tab). Când a fost scris acest cod, opcode-ul nu era încă acceptat de Solidity, de aceea era necesar să se obțină manual codul. Această problemă nu mai există, deoarece Solidity acceptă acum CREATE2(opens in a new tab).

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

Atunci când un opcode nu este încă acceptat de Solidity, îl putem apela folosind inline assembly(opens in a new tab).

1 IUniswapV2Pair(pair).initialize(token0, token1);
Copiați

Apelează funcția initialize pentru a indica noului schimb care anume sunt cele două tokenuri pe care le schimbă.

1 getPair[token0][token1] = pair;
2 getPair[token1][token0] = pair; // populate mapping in the reverse direction
3 allPairs.push(pair);
4 emit PairCreated(token0, token1, pair, allPairs.length);
5 }
Copiați

Salvează noua pereche de informații în variabilele de stare, și emite un eveniment pentru a informa lumea despre noul schimb în pereche.

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}
Afișează tot
Copiați

Aceste două funcții permit feeToSetter să controleze destinatarul taxei (în caz că există vreunul) și să schimbe feeToSetter la o nouă adresă.

UniswapV2ERC20.sol

Acest contract(opens in a new tab) implementează tokenul de lichidităţi ERC-20. Este similar cu contractul OpenWhisk ERC-20, așa că voi explica numai partea care este diferită, funcționalitatea permit.

Tranzacțiile pe Ethereum costă ether (ETH), care este echivalent cu banii reali. Dacă aveți tokenuri ERC-20, şi nu ETH, nu puteți trimite tranzacții, deci nu puteți face nimic cu ele. O soluție pentru a evita această problemă constă în meta-tranzacții(opens in a new tab). Proprietarul tokenurilor semnează o tranzacție care permite altcuiva să retragă jetoanele din lanț și să le trimită prin internet destinatarului. Destinatarul, care posedă ETH, transmite apoi autorizația în numele proprietarului.

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

Acest hash este identificatorul pentru tipul de tranzacție(opens in a new tab). Singurul pe care îl acceptăm aici este Permit cu acești parametri.

1 mapping(address => uint) public nonces;
Copiați

Nu este posibilă falsificarea unei semnături digitale de către un destinatar. Totuși, este banală trimiterea unei tranzacții de două ori (este o formă de atac prin reluare(opens in a new tab) „replay attack”). Pentru a preveni aceasta, folosim un nonce(opens in a new tab). Dacă nonce-ul unui nou Permit nu este mai mare cu unu decât ultimul folosit, presupunem că acesta este nevalid.

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

Acesta este codul pentru a recupera identificatorul lanțului(opens in a new tab). Folosește un dialect de asamblare EVM numit Yul(opens in a new tab). Rețineți că în versiunea curentă a Yul trebuie să folosiți chainid(), și nu 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 }
Afișează tot
Copiați

Calculează separatorul de domeniu(opens in a new tab) pentru EIP-712.

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

Aceasta este funcția care implementează permisiunile. Ea primește ca parametri câmpurile relevante și cele trei valori scalare pentru semnătură(opens in a new tab) (v, r și s).

1 require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
Copiați

Nu acceptă tranzacții după data limită.

1 bytes32 digest = keccak256(
2 abi.encodePacked(
3 '\x19\x01',
4 DOMAIN_SEPARATOR,
5 keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
6 )
7 );
Copiați

abi.encodePacked(...) este mesajul pe care ne așteptăm să îl primim. Știm care ar trebui să fie nonce-ul, de aceea nu avem nevoie să îl obținem ca parametru

Algoritmul de semnătură în Ethereum așteaptă să primească 256 de biți pentru a semna, de aceea folosim funcția hash keccak256.

1 address recoveredAddress = ecrecover(digest, v, r, s);
Copiați

Din „digest” și din semnătură, putem obține adresa care a semnat-o folosind ecrecover(opens in a new tab).

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

Dacă totul este în regulă, tratați aceasta ca o aprobare ERC-20(opens in a new tab).

Contractele periferice

Contractele periferice sunt API-uri (interfață de program de aplicație) pentru Uniswap. Acestea sunt disponibile pentru apelurile externe, fie din alte contracte, fie din aplicațiile descentralizate. Ați putea apela contractele centrale direct, dar este mai complicat și s-ar putea să pierdeți valoare dacă faceți vreo greșeală. Contractele centrale conțin numai teste pentru a garanta că nu sunt fraudate, şi nu pentru a verifica starea de sănătate pentru oricine altcineva. Acestea sunt la periferie, de aceea pot fi actualizate după cum este nevoie.

UniswapV2Router01.sol

Acest contract(opens in a new tab) are probleme și ar trebui să nu mai fie utilizat(opens in a new tab). Din fericire, contractele periferice sunt fără stare și nu dețin niciun activ, de aceea este ușor să fie eliminate; se recomandă în schimb utilizarea înlocuitorului lor, UniswapV2Router02.

UniswapV2Router02.sol

În cele mai multe cazuri, veți utiliza Uniswap prin intermediul acestui contract(opens in a new tab). Puteți vedea cum să îl utilizați aici(opens in a new tab).

1pragma solidity =0.6.6;
2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
4import '@uniswap/lib/contracts/libraries/TransferHelper.sol';
5
6import './interfaces/IUniswapV2Router02.sol';
7import './libraries/UniswapV2Library.sol';
8import './libraries/SafeMath.sol';
9import './interfaces/IERC20.sol';
10import './interfaces/IWETH.sol';
Afișează tot
Copiați

Cele mai multe dintre acestea fie le-am mai întâlnit, fie sunt destul de evidente. Singura excepție este IWETH.sol. Uniswap v2 permite schimburi pentru orice pereche de tokenuri ERC-20, dar ether -ul (ETH) propriu-zis nu este un token ERC-20. El precedă standardul și este transferat prin mecanisme unice. Pentru a permite folosirea de ETH în contractele care se aplică la tokenurile ERC-20, a fost creat contractul de wrapped ether (WETH)(opens in a new tab) (ether înfășurat). Trimiteți ETH acestui contract, iar acesta vă emite o sumă echivalentă în WETH. Sau puteți arde WETH și primiți înapoi ETH-ul.

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

Routerul trebuie să știe ce fabrică să utilizeze, iar pentru tranzacțiile care au nevoie de WETH, ce contract WETH să utilizeze. Aceste valori sunt imuabile(opens in a new tab), în sensul că pot fi stabilite numai în constructor. Aceasta le garantează utilizatorilor că nimeni nu ar putea să le modifice ca să conducă spre contracte mai puțin oneste.

1 modifier ensure(uint deadline) {
2 require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
3 _;
4 }
Copiați

Acest modificator se asigură că tranzacțiile cu limită de timp („faceți X înainte de momentul Y, dacă se poate”) nu au loc după expirarea termenului limită.

1 constructor(address _factory, address _WETH) public {
2 factory = _factory;
3 WETH = _WETH;
4 }
Copiați

Constructorul stabilește pur și simplu variabilele de stare imuabile.

1 receive() external payable {
2 assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract
3 }
Copiați

Această funcție este apelată când răscumpărăm tokenuri din contractul în WETH înapoi în ETH. Contractul în WETH pe care îl folosim este singurul autorizat să facă acest lucru.

Adăugarea de lichidități

Aceste funcții adaugă tokenuri la schimbul în pereche, sporind astfel fondul comun de lichidități.

1
2 // **** ADD LIQUIDITY ****
3 function _addLiquidity(
Copiați

Această funcție este folosită pentru a calcula cantitatea de tokenuri A și B care trebuie depozitate în schimbul în pereche.

1 address tokenA,
2 address tokenB,
Copiați

Acestea sunt adresele contractelor de tokenuri ERC-20.

1 uint amountADesired,
2 uint amountBDesired,
Copiați

Acestea sunt sumele pe care vrea să le depună furnizorul de lichidităţi. De asemenea, acestea sunt sumele maxime de A și B de depus.

1 uint amountAMin,
2 uint amountBMin
Copiați

Acestea sunt sumele minime acceptabile pentru depunere. Dacă tranzacția nu poate avea loc cu aceste sume sau mai mari, renunțați la aceasta. Dacă nu doriți această funcție, trebuie numai să specificaţi „zero”.

Furnizorii de lichidităţi specifică de regulă un minim, deoarece vor să limiteze tranzacția la o rată de schimb cât mai apropiată de cea actuală. Dacă rata de schimb fluctuează prea mult, acest lucru ar însemna că există noutăți care schimbă valorile preexistente și aceștia vor să decidă manual ce să facă.

De exemplu, imaginați-vă un caz în care rata de schimb este de unu la unu, iar furnizorul de lichidități specifică următoarele valori:

ParametruValoare
amountADesired1000
amountBDesired1000
amountAMin900
amountBMin800

Atâta timp cât rata de schimb se menține între 0,9 și 1,25, tranzacția are loc. În cazul în care rata de schimb nu se încadrează în acest interval, tranzacția este anulată.

Motivul acestei precauții este că tranzacțiile nu au loc imediat, le trimiteți și în final un miner le va include într-un bloc (în afară de cazul în care prețul gazului este prea coborât - atunci va fi nevoie să trimiteți o altă tranzacție cu același nonce și cu un preț mai mare al gazului, pentru a o suprascrie). Dvs. nu puteți controla ce se întâmplă în intervalul de timp dintre trimitere și includere.

1 ) internal virtual returns (uint amountA, uint amountB) {
Copiați

Funcția răspunde prin sumele pe care furnizorul de lichidități ar trebui să le depună pentru a avea un raport egal cu actualul raport dintre rezerve.

1 // create the pair if it doesn't exist yet
2 if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
3 IUniswapV2Factory(factory).createPair(tokenA, tokenB);
4 }
Copiați

Dacă nu există încă un schimb pentru această pereche de tokenuri, atunci va fi creat.

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

Obține rezervele curente din pereche.

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

În cazul în care rezervele curente sunt goale, atunci acesta este un nou schimb în pereche. Sumele care trebuie depuse trebuie să fie identice cu cele pe care furnizorul de lichidități vrea să le furnizeze.

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

Dacă trebuie să vedem ce sume vor fi, obținem suma optimă folosind această funcție(opens in a new tab). Vrem același raport ca și rezervele curente.

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

Dacă amountBOptimal este mai mică decât suma pe care furnizorul de lichidităţi vrea să o depună, aceasta înseamnă că jetonul B are o valoare actuală mai mare decât crede depunătorul de lichidități, așadar este necesară o sumă mai mică.

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

Dacă suma optimă B este mai mare decât suma B dorită, acesta înseamnă că tokenurile B au o valoare curentă mai mică decât crede depunătorul de lichidităţi, așadar este necesară o sumă mai mare. Totuși, suma dorită este cea maximă, de aceea nu putem face aceasta. În schimb, calculăm numărul optim de tokenuri A pentru cantitatea dorită de tokenuri B.

Punând totul cap la cap, obținem acest grafic. Să presupunem că încercați să depuneți o mie de tokenuri A (linia albastră) și o mie de tokenuri B (linia roșie). Axa „x” reprezintă rata de schimb, A/B. Dacă x=1, acestea au valoare egală și depuneți o mie din fiecare. Dacă x=2, A este de două ori mai valoros decât B (obțineți două tokenuri B pentru fiecare token A), de aceea depuneți o mie de tokenuri B, dar numai 500 de tokenuri A. Dacă x=0,5, atunci situația se inversează, este vorba de o mie de tokenuri A și cinci sute de tokenuri B.

Grafic

1 }
2 }
3 }
Copiați

Ați putea depune lichidități direct în contractul central (folosind UniswapV2Pair::mint(opens in a new tab)), dar contractul central verifică numai dacă el însuși este fraudat astfel încât riscați să pierdeți din valoare dacă se modifică rata de schimb între momentul trimiterii și executării tranzacției. Dacă utilizați contractul periferic, acesta calculează suma pe care ar trebui să o depuneți și o depune imediat, în așa fel încât rata de schimb nu se modifică și în acest fel nu pierdeți nimic.

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
Afișează tot
Copiați

Această funcție poate fi apelată de o tranzacție pentru a depune lichidități. Cei mai mulți parametri sunt la fel ca în _addLiquidity de mai sus, cu două excepții:

. to adresa care obține noile tokenuri de lichidităţi emise, pentru a arăta partea furnizorului de lichidităţi din fondul comun. dealine este o limită de timp pentru tranzacție

1 ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
2 (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
3 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
Copiați

Calculăm sumele care trebuie depuse efectiv, iar apoi găsim adresa fondului de lichidități. Pentru a economisi gaz, nu cerem acest lucru fabricii, ci folosim funcția de bibliotecă pairFor (a se vedea mai jos în biblioteci)

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

Transferă cantitățile corecte de tokenuri de la utilizator în schimbul în pereche.

1 liquidity = IUniswapV2Pair(pair).mint(to);
2 }
Copiați

În schimb dă tokenurile de lichidităţi la adresa to pentru proprietatea parțială a fondului comun. Funcția mint a contractului central vede câte tokenuri suplimentare are (în comparație cu ce a avut când s-au modificat ultima dată lichidităţile) și emite lichidităţile în consecință.

1 function addLiquidityETH(
2 address token,
3 uint amountTokenDesired,
Copiați

Atunci când un furnizor de lichidităţi vrea să furnizeze lichidităţi unui schimb în pereche Jeton/ETH, există câteva diferențe. Contractul se ocupă de învelirea („wrapping”) de ETH pentru furnizorul de lichidități. Nu este nevoie să se specifice cât ETH doreşte să depună utilizatorul, deoarece acesta pur şi simplu îi trimite cu tranzacția (suma este disponibilă în 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));
Afișează tot
Copiați

Pentru a depune ETH, contractul îl înfășoară („wrap”) in WETH, apoi transferă WETH către pereche. Observați că transferul este înfășurat într-un assert. Deci dacă transferul nu reușește, nu reușește nici acest apel de contract, și prin urmare, învelirea („wrapping”) nu are loc cu adevărat.

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

Utilizatorul ne-a trimis deja ETH-ul, deci dacă a mai rămas ceva în plus (pentru că celălalt token are valoare mai mică decât a crezut utilizatorul), trebuie să emitem o rambursare.

Eliminarea de lichidități

Aceste funcții vor elimina lichidităţile și vor rambursa furnizorul de lichiditate.

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) {
Afișează tot
Copiați

Cazul cel mai simplu de eliminare de lichidităţi. Există o cantitate minimă din fiecare token pe care furnizorul de lichidităţi este dispus să o accepte și aceasta trebuie să se întâmple înainte de termenul limită.

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);
Copiați

Funcția burn a contractului central se ocupă de returnarea tokenurilor către utilizator.

1 (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
Copiați

Atunci când o funcție răspunde prin mai multe valori, însă ne interesează numai unele dintre acestea, iată cum putem obține doar acele valori. Este oarecum mai ieftin în privinţa gazului decât să citim o valoare pe care să nu o folosim niciodată.

1 (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
Copiați

Traduce sumele din modul cum răspunde prin acestea contractul central (tokenul cu adresă inferioară mai întâi) în modul în care le așteaptă utilizatorul (care să corespundă cu tokenA și tokenB).

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

Este corect să efectuăm transferul mai întâi, apoi să verificăm dacă este legitim, deoarece dacă nu este, vom anula toate modificările de stare.

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 }
Afișează tot
Copiați

Eliminarea lichidităților de ETH este aproape la fel, cu excepția faptului că primim tokenuri WETH și apoi le răscumpărăm pe ETH pentru a-l da înapoi furnizorului de lichidități.

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 }
Afișează tot
Copiați

Aceste funcții transmit meta-tranzacții, care permit utilizatorilor fără ether să retragă din fondul comun folosind mecanismul de autorizare.

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
Afișează tot
Copiați

Această funcție poate fi utilizată pentru tokenuri care au taxe de transfer sau de stocare. Când un token are astfel de taxe, nu ne putem baza pe funcția removeLiquidity pentru a ne spune cât de mult din token vom primi înapoi, așa că trebuie să retragem mai întâi, iar apoi să obținem soldul.

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 }
Afișează tot
Copiați

Ultima funcție combină taxele de stocare cu meta-tranzacțiile.

Tranzacţionare

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 {
Copiați

Această funcție efectuează procesarea internă necesară funcțiilor care sunt expuse comercianților.

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

În momentul în care scriu aceste rânduri, există 388.160 tokenuri ERC-20(opens in a new tab). Dacă ar exista un schimb în pereche pentru fiecare pereche de tokenuri, s-ar ajunge la peste 150 miliarde de schimburi în pereche. În acest moment, întregul lanț are numai 0,1% din acest număr de conturi(opens in a new tab). În schimb, funcțiile swap acceptă conceptul de cale („path”). Un comerciant poate schimba A cu B, B cu C, C cu D, astfel încât nu este nevoie de un schimb în pereche direct A-D.

Prețurile pe aceste piețe au tendința să fie sincronizate, deoarece când nu sunt sincronizate, se creează o oportunitate de arbitraj. Imaginați-vă, de exemplu, trei jetoane A, B și C. Există trei schimburi pereche, unul pentru fiecare pereche.

  1. Situația inițială
  2. Un comerciant vinde 24,695 tokenuri A și obține 25,305 tokenuri B.
  3. Comerciantul vinde 24,695 tokenuri B pentru 25,305 tokenuri C, păstrând aproximativ 0,61 token B ca profit.
  4. Apoi comerciantul vinde 24,695 tokenuri C pe 25,305 tokenuri A, păstrând aproximativ 0,61 tokenuri C ca profit. Comerciantul are de asemenea 0,61 tokenuri A în plus (cele 25,305 pe care comerciantul le are în final minus investiția originală de 24,695).
EtapăSchimb A-BSchimb B-CSchimb A-C
1A:1000 B:1050 A/B=1.05B:1000 C:1050 B/C=1.05A:1050 C:1000 C/A=1.05
2A:1024.695 B:1024.695 A/B=1B:1000 C:1050 B/C=1.05A:1050 C:1000 C/A=1.05
3A:1024.695 B:1024.695 A/B=1B:1024.695 C:1024.695 B/C=1A:1050 C:1000 C/A=1.05
4A:1024.695 B:1024.695 A/B=1B:1024.695 C:1024.695 B/C=1A:1024.695 C:1024.695 C/A=1
1 (address input, address output) = (path[i], path[i + 1]);
2 (address token0,) = UniswapV2Library.sortTokens(input, output);
3 uint amountOut = amounts[i + 1];
Copiați

Obține perechea pe care o gestionăm actualmente, o sortează (pentru a fi utilizată cu perechea) și obține suma rezultantă preconizată.

1 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
Copiați

Obține valorile rezultante preconizate, sortate așa cum anticipează schimbul în pereche.

1 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
Copiați

Acesta este ultimul schimb? În caz afirmativ, trimite la destinație tokenurile primite pentru tranzacţionare. În caz contrar, le trimite la următorul schimb în pereche.

1
2 IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
3 amount0Out, amount1Out, to, new bytes(0)
4 );
5 }
6 }
Copiați

Apelează de fapt schimbul în pereche pentru a schimba tokenurile. Nu avem nevoie de un retur de apel pentru a fi informați despre schimb, de aceea nu trimitem niciun octet în acest câmp.

1 function swapExactTokensForTokens(
Copiați

Această funcție este folosită direct de către comercianți pentru a schimba un token pe altul.

1 uint amountIn,
2 uint amountOutMin,
3 address[] calldata path,
Copiați

Acest parametru conține adresele contractelor ERC-20. Așa cum s-a explicat mai sus, aceasta este o matrice, deoarece ar putea fi necesar să treceţi prin mai multe schimburi în pereche pentru a obține activul dorit din activul pe care îl aveţi.

Parametrul unei funcții în solidity poate fi stocat fie în memory, fie în calldata. Dacă funcția este un punct de intrare în contract, apelat direct de la un utilizator (folosind o tranzacție) sau de la un alt contract, atunci valoarea parametrului poate fi obținută direct din datele apelului. Dacă funcția este apelată intern, ca _swap de mai sus, atunci parametrii trebuie să fie stocați în memory. Din perspectiva contractului apelat, calldata este numai pentru citire.

Cu tipurile scalare cum ar fi uint sau address, compilatorul ne alege tipul de stocare, însă cu matricele, care sunt mai lungi și mai scumpe, trebuie să specificăm tipul de stocare care va fi utilizat.

1 address to,
2 uint deadline
3 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
Copiați

Valorile de răspuns sunt returnate întotdeauna în memorie.

1 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
2 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
Copiați

Calculează cantitatea care trebuie cumpărată la fiecare swap. În cazul în care rezultatul este mai mic decât minimul pe care comerciantul este dispus să-l accepte, tranzacția este anulată.

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

În cele din urmă, transferă tokenurile inițiale ERC-20 în contul primului schimb în pereche și apelează _swap. Toate acestea au loc în cadrul aceleiași tranzacții, de aceea schimbul în pereche știe că orice tokenuri neașteptate fac parte din acest transfer.

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 }
Afișează tot
Copiați

Funcția anterioară, swapTokensForTokens, permite comerciantului să specifice un număr exact de tokenuri introduse pe care este dispus să le dea și numărul minim de tokenuri rezultante pe care este dispus să le primească în schimb. Această funcție realizează swap-ul inversat, îi permite comerciantului să specifice numărul dorit de tokenuri rezultante și numărul maxim de tokenuri introduse pe care este dispus să le plătească pentru acestea.

În ambele cazuri, comerciantul trebuie să acorde mai întâi contractului periferic o alocație care să-i permită să le transfere.

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 }
Afișează tot
Copiați

Toate aceste variante implică tranzacționarea între ETH și tokenuri. Singura diferență este că fie primim ETH de la comerciant și îl folosim a emite WETH, fie primim WETH de la ultimul schimb de pe cale și îl ardem, trimițând comerciantului ETH-ul rezultat.

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 {
Copiați

Aceasta este funcția internă de swapping al tokenurilor care au de rezolvat taxe de transfer sau de stocare (această problemă(opens in a new tab)).

1 for (uint i; i < path.length - 1; i++) {
2 (address input, address output) = (path[i], path[i + 1]);
3 (address token0,) = UniswapV2Library.sortTokens(input, output);
4 IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
5 uint amountInput;
6 uint amountOutput;
7 { // scope to avoid stack too deep errors
8 (uint reserve0, uint reserve1,) = pair.getReserves();
9 (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
10 amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
11 amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
Afișează tot
Copiați

Datorită taxelor de transfer, nu putem să ne bazăm pe funcția getAmountsOut pentru a ne arăta cât de mult putem obține din fiecare transfer (în modul în care o făceam înainte, prin apelarea _swap original). În schimb, trebuie să transferăm mai întâi și apoi să vedem câte tokenuri am primit înapoi.

Observaţie: Teoretic am putea pur şi simplu să folosim această funcție în loc de _swap, dar în anumite cazuri (dacă transferul ajunge să fie anulat deoarece în final nu sunt suficiente tokenuri pentru a satisface minimul necesar) aceasta ar ajunge să ne coste mai mult gaz. Tokenurile cu taxe de transfer sunt destul de rare, de aceea, deși trebuie să le luăm în considerare, nu este nevoie să presupunem că la toate schimburile vom întâlni cel puțin unul dintre ele.

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 }
Afișează tot
Copiați

Acestea sunt aceleași variante folosite pentru tokenurile normale, dar ele apelează în schimb _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}
Afișează tot
Copiați

Aceste funcții sunt numai proxy-uri care apelează funcțiile UniswapV2Library.

UniswapV2Migrator.sol

Acest contract a fost utilizat pentru migrarea schimburilor de la vechiul v1 la v2. Acum, odată ce acestea au fost migrate, nu mai este relevantă.

Bibliotecile

Biblioteca SafeMath(opens in a new tab) este bine documentată, așa că nu este necesar să o documentăm aici.

„Math”

Această bibliotecă include câteva funcții matematice care în mod normal nu sunt necesare în codul Solidity, de aceea nu fac parte din limbaj.

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;
Afișează tot
Copiați

Începe cu x ca o estimare care este mai mare decât rădăcina pătrată (acesta este motivul pentru care tratăm 1-3 ca pe niște cazuri speciale).

1 while (x < z) {
2 z = x;
3 x = (y / x + x) / 2;
Copiați

Obține o estimare mai apropiată, media dintre estimarea anterioară și numărul a cărui rădăcină pătrată încercăm să o găsim împărțit la estimarea anterioară. Repetă până când noua estimare nu este mai mică decât cea existentă. Pentru mai multe detalii, uitaţi-vă aici(opens in a new tab).

1 }
2 } else if (y != 0) {
3 z = 1;
Copiați

Nu ar trebui să avem nevoie niciodată de rădăcina pătrată a lui zero. Rădăcinile pătrate ale lui unu, doi și trei sunt cu aproximație unu (cum noi folosim numerele întregi, deci ignorăm fracțiunile).

1 }
2 }
3}
Copiați

Fracțiunile cu virgulă fixă (UQ112x112)

Această bibliotecă gestionează fracțiunile, care în mod normal nu fac parte din aritmetica lui Ethereum. Realizează aceasta prin codificarea numărului x ca x*2^112. Aceasta ne permite să folosim opcodurile originale de adunare și scădere fără nicio modificare.

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;
Afișează tot
Copiați

Q112 este codificarea pentru unu.

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 }
Copiați

Because y is uint112, the most it can be is 2^112-1. Acest număr poate fi codificat în continuare ca 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}
Copiați

Dacă împărțim două valori UQ112x112, rezultatul nu mai este înmulțit cu 2^112. Deci în schimb luăm un număr întreg ca numitor. Ar fi trebuit să folosim un artificiu similar pentru a face înmulțiri, dar nu avem nevoie să facem înmulțirea valorilor UQ112x112.

UniswapV2Library

Această bibliotecă este folosită numai de contractele periferice

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 }
Afișează tot
Copiați

Sortează cele două tokenuri după adresă, astfel încât să putem obține din ele adresa schimbului în pereche. Acest lucru este necesar, întrucât altfel am avea două posibilități, una pentru parametrii A,B și alta pentru parametrii B,A, ceea ce ar conduce la două schimburi în loc de unul.

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 }
Afișează tot
Copiați

Această funcție calculează adresa schimbului în pereche pentru cele două tokenuri. Acest contract este creat folosind opcode CREATE2(opens in a new tab), astfel încât să putem calcula adresa folosind același algoritm dacă știm parametrii pe care îi folosește. Este mult mai ieftin decât să întrebați fabrica și

1 // fetches and sorts the reserves for a pair
2 function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
3 (address token0,) = sortTokens(tokenA, tokenB);
4 (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
5 (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
6 }
Copiați

Această funcție răspunde prin rezervele celor două tokenuri pe care le are schimbul în pereche. Observați că acesta poate primi tokenurile în orice ordine și le sortează pentru uz intern.

1 // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset
2 function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
3 require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
4 require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
5 amountB = amountA.mul(reserveB) / reserveA;
6 }
Copiați

Această funcție vă oferă cantitatea tokenului B pe care o veți primi în schimbul tokenului A dacă nu presupune nicio taxă. Acest calcul ia în considerare faptul că transferul modifică rata de schimb.

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) {
Copiați

Funcția quote de mai sus funcționează excelent dacă pentru a folosi schimbul în pereche nu se percepe nicio taxă. Totuși, dacă există o taxă de schimb de 0,3%, suma pe care o obțineți cu adevărat este mai mică. Această funcție calculează suma după aplicarea taxei de schimb.

1
2 require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
3 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
4 uint amountInWithFee = amountIn.mul(997);
5 uint numerator = amountInWithFee.mul(reserveOut);
6 uint denominator = reserveIn.mul(1000).add(amountInWithFee);
7 amountOut = numerator / denominator;
8 }
Copiați

Solidity nu operează nativ cu fracțiuni, așa că nu putem înmulți suma cu 0,997. De aceea, înmulțim numărătorul cu 997 și numitorul cu 1000, ceea ce are același efect.

1 // given an output amount of an asset and pair reserves, returns a required input amount of the other asset
2 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
3 require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
4 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
5 uint numerator = reserveIn.mul(amountOut).mul(1000);
6 uint denominator = reserveOut.sub(amountOut).mul(997);
7 amountIn = (numerator / denominator).add(1);
8 }
Copiați

Această funcție efectuează aproximativ același lucru, însă obține suma rezultantă și oferă datele de introdus.

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}
Afișează tot
Copiați

Aceste două funcții se ocupă de identificarea valorilor atunci când este nevoie să se treacă prin mai multe schimburi în pereche.

„TransferHelper”

Această bibliotecă(opens in a new tab) adaugă verificări ale succesului transferurilor ERC-20 și Ethereum, pentru a trata o revenire și o valoare de răspuns false în același fel.

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
Afișează tot
Copiați

Putem să apelăm un contract diferit în două moduri:

1 require(
2 success && (data.length == 0 || abi.decode(data, (bool))),
3 'TransferHelper::safeApprove: approve failed'
4 );
5 }
Copiați

Pentru asigurarea unei compatibilități din urmă cu un jeton creat înainte de standardul ERC-20, un apel ERC-20 poate eșua fie prin revenire (caz în care success este false), fie prin reușită, dar răspunzând printr-o valoare false (caz în care există date rezultante și dacă le decodați ca valoare booleană, obțineți 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 }
Afișează tot
Copiați

Această funcție implementează funcționalitatea „transfer” a ERC-20(opens in a new tab), care permite unui cont să cheltuiască alocația furnizată de un alt cont.

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 }
Afișează tot
Copiați

Această funcție implementează funcționalitatea „transferFrom” a ERC-20(opens in a new tab), care permite unui cont să cheltuiască alocația furnizată de un alt cont.

1
2 function safeTransferETH(address to, uint256 value) internal {
3 (bool success, ) = to.call{value: value}(new bytes(0));
4 require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');
5 }
6}
Copiați

Această funcție transferă ether într-un cont. Orice apel către un contract diferit poate încerca să trimită ether. Deoarece nu avem nevoie să apelăm vreo funcție, nu trimitem niciun fel de date cu apelul.

Concluzie

Acesta este un articol lung de aproape 50 de pagini. Dacă ați ajuns până aici, felicitări! Să sperăm că până acum ați înțeles considerațiile legate de scrierea unei aplicații din viața reală (spre deosebire de scurtele exemple de programe) și că sunteți mai bine pregătit de a scrie contracte pentru propriile cazuri de utilizare.

Iar acum, apucaţi-vă să scrieți ceva util și uimiți-ne.

Ultima modificare: @nicklcanada(opens in a new tab), 23 februarie 2024

A fost util acest tutorial?