Analiza contractului Uniswap-v2
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:
- Efectuarea de operațiuni de schimb între diferite tokenuri
- Adăugarea de lichidităţi pe piață și primirea de recompense prin schimbul perechilor de tokenuri de lichidităţi ERC-20
- 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
- Furnizează o alocație contului periferic de valoare egală cu cea care trebuie schimbată.
- 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)
- Identifică suma care trebuie tranzacționată la fiecare schimb de-a lungul căii.
- 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)
- Verifică să nu se fraudeze contractul central și dacă acesta poate menține suficiente lichidităţi după efectuarea schimbului.
- 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.
- Trimite tokenurile rezultate la destinație.
- Apelează
_update
pentru a actualiza cantitatea de rezervă
Înapoi în contractul periferic (UniswapV2Router02.sol)
- 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
- Furnizează contului periferic o alocație egală cu sumele care trebuie adăugate la fondul comun de lichidităţi.
- Apelează una din funcțiile contractului periferic, și anume „addLiquidity”.
În contractul periferic (UniswapV2Router02.sol)
- Creează un nou schimb în pereche dacă este necesar
- 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.
- Verifică dacă sumele sunt rezonabile (apelanții pot specifica suma minimă sub care nu sunt dispuși să adauge lichidități)
- Apelează contractul central.
În contractul central (UniswapV2Pair.sol)
- Emite tokenurile de lichidităţi și le trimite către apelant
- Apelează
_update
pentru a actualiza cantitatea de rezervă
Eliminarea de lichidități
Apelantul
- Furnizează contului periferic o alocație de tokenuri de lichidităţi care trebuie arse în schimbul tokenurilor preexistente.
- Apelează una din funcțiile contractului periferic, și anume „removeLiquidity”.
În contractul periferic (UniswapV2Router02.sol)
- Trimite tokenurile de lichidităţi la schimbul în pereche
În contractul central (UniswapV2Pair.sol)
- 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.
- Arde tokenurile de lichidităţi
- 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;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';Afișează totCopiaț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 getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReservesCopiaț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 getReservesCopiaț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 eventCopiaț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.
Eveniment | reserve0 | reserve1 | reserve0 * reserve1 | Curs mediu de schimb (token1 / token0) |
---|---|---|---|---|
Situaţia inițială | 1.000,000 | 1.000,000 | 1.000.000 | |
Comerciantul A schimbă 50 de token0 pe 47,619 token1 | 1.050,000 | 952,381 | 1.000.000 | 0,952 |
Comerciantul B schimbă 10 token0 pentru 8,984 token1 | 1.060,000 | 943,396 | 1.000.000 | 0,898 |
Comerciantul C schimbă 40 de token0 pe 34,305 token1 | 1.100,000 | 909,090 | 1.000.000 | 0,858 |
Comerciantul D schimbă 100 de token1 pe 109,01 token0 | 990,990 | 1.009,090 | 1.000.000 | 0,917 |
Comerciantul E schimbă 10 token0 pe 10,079 token1 | 1.000,990 | 999,010 | 1.000.000 | 1,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:
- Revenirea. If a call to an external contract reverts, then the boolean return value is
false
- 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 to8 );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 deployment2 function initialize(address _token0, address _token1) external {3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check4 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 accumulators2 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 desired3 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 desired2 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:
Eveniment | reserve0 | reserve1 | marcă temporală | Cursul de schimb marginal (reserve1 / reserve0) | price0CumulativeLast |
---|---|---|---|---|---|
Situaţia inițială | 1.000,000 | 1.000,000 | 5.000 | 1,000 | 0 |
Comerciantul A depune 50 de token0 și obține 47,619 token1 înapoi | 1.050,000 | 952,381 | 5.020 | 0,907 | 20 |
Comerciantul B depune 10 token0 și obține 8,984 token1 înapoi | 1.060,000 | 943,396 | 5.030 | 0,890 | 20+10*0,907 = 29,07 |
Comerciantul C depune 40 de token0 și obține 34,305 token1 înapoi | 1.100,000 | 909,090 | 5.100 | 0,826 | 29,07+70*0,890 = 91,37 |
Comerciantul D depune 100 de token1 și obține 109,01 token0 înapoi | 990,990 | 1.009,090 | 5.110 | 1,018 | 91,37+10*0,826 = 99,63 |
Comerciantul E depune 10 token0 și obține 10,079 token1 înapoi | 1.000,990 | 999,010 | 5.150 | 0,998 | 99,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 savingsCopiaț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 checks2 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 savingsCopiaț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 _mintFee2 if (_totalSupply == 0) {3 liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);4 _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokensCopiaț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.
Eveniment | reserve0 | reserve1 | reserve0 * reserve1 | Valoarea fondului comun (reserve0 + reserve1) |
---|---|---|---|---|
Situaţia inițială | 8 | 32 | 256 | 40 |
Comerciantul depozitează 8 Token0, primește înapoi 16 Token1 | 16 | 16 | 256 | 32 |
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).
Eveniment | reserve0 | reserve1 | reserve0 * reserve1 | Valoarea fondului comun (reserve0 + reserve1) | Tokenuri de lichidităţi emise pentru acest depozit | Total tokenuri de lichidităţi | valoarea fiecărui token de lichidităţi |
---|---|---|---|---|---|---|---|
Situaţia inițială | 8,000 | 8,000 | 64 | 16,000 | 8 | 8 | 2,000 |
Depune patru din fiecare tip | 12,000 | 12,000 | 144 | 24,000 | 4 | 12 | 2,000 |
Depune două din fiecare tip | 14,000 | 14,000 | 196 | 28,000 | 2 | 14 | 2,000 |
Depozit de valoare inegală | 18,000 | 14,000 | 252 | 32,000 | 0 | 14 | ~2,286 |
După arbitraj | ~15,874 | ~15,874 | 252 | ~31,748 | 0 | 14 | ~2,267 |
1 }2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');3 _mint(to, liquidity);Copiați
Folosește funcția UniswapV2ERC20._mint
ca să creeze efectiv tokenurile de lichidităţi suplimentare și le distribuie în contul corect.
12 _update(balance0, balance1, _reserve0, _reserve1);3 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date4 emit Mint(msg.sender, amount0, amount1);5 }Copiaț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 checks2 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 savings2 address _token0 = token0; // gas savings3 address _token1 = token1; // gas savings4 uint balance0 = IERC20(_token0).balanceOf(address(this));5 uint balance1 = IERC20(_token1).balanceOf(address(this));6 uint liquidity = balanceOf[address(this)];Copiaț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 _mintFee3 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution4 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution5 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');Copiaț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));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 }11Afișează totCopiaț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 checks2 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 savings3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');45 uint balance0;6 uint balance1;7 { // scope for _token{0,1}, avoids stack too deep errorsCopiaț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 tokens5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokensCopiaț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 errors5 uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));6 uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));7 require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');Copiaț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 }23 _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 curenteskim
, retragerea sumei suplimentare. Rețineți că orice cont poate apelaskim
, 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 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}Afișează totCopiați
UniswapV2Factory.sol
Acest contract(opens in a new tab) creează schimbul în pereche.
1pragma solidity =0.5.16;23import './interfaces/IUniswapV2Factory.sol';4import './UniswapV2Pair.sol';56contract UniswapV2Factory is IUniswapV2Factory {7 address public feeTo;8 address public feeToSetter;Copiaț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 sufficientCopiaț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 direction3 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 }56 function setFeeToSetter(address _feeToSetter) external {7 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');8 feeToSetter = _feeToSetter;9 }10}Afișează totCopiaț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 := chainid5 }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ă totCopiaț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 }4Copiaț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;23import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';4import '@uniswap/lib/contracts/libraries/TransferHelper.sol';56import './interfaces/IUniswapV2Router02.sol';7import './libraries/UniswapV2Library.sol';8import './libraries/SafeMath.sol';9import './interfaces/IERC20.sol';10import './interfaces/IWETH.sol';Afișează totCopiaț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;34 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 contract3 }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.
12 // **** 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 amountBMinCopiaț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:
Parametru | Valoare |
---|---|
amountADesired | 1000 |
amountBDesired | 1000 |
amountAMin | 900 |
amountBMin | 800 |
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 yet2 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.
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 deadlineAfișează totCopiaț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 deadline5 ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {6 (amountToken, amountETH) = _addLiquidity(7 token,8 WETH,9 amountTokenDesired,10 msg.value,11 amountTokenMin,12 amountETHMin13 );14 address pair = UniswapV2Library.pairFor(factory, token, WETH);15 TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);16 IWETH(WETH).deposit{value: amountETH}();17 assert(IWETH(WETH).transfer(pair, amountETH));Afișează totCopiaț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 any3 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 deadline10 ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {Afișează totCopiaț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 pair3 (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 deadline8 ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {9 (amountToken, amountETH) = removeLiquidity(10 token,11 WETH,12 liquidity,13 amountTokenMin,14 amountETHMin,15 address(this),16 deadline17 );18 TransferHelper.safeTransfer(token, to, amountToken);19 IWETH(WETH).withdraw(amountETH);20 TransferHelper.safeTransferETH(to, amountETH);21 }Afișează totCopiaț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 s10 ) external virtual override returns (uint amountA, uint amountB) {11 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);12 uint value = approveMax ? uint(-1) : liquidity;13 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);14 (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);15 }161718 function removeLiquidityETHWithPermit(19 address token,20 uint liquidity,21 uint amountTokenMin,22 uint amountETHMin,23 address to,24 uint deadline,25 bool approveMax, uint8 v, bytes32 r, bytes32 s26 ) external virtual override returns (uint amountToken, uint amountETH) {27 address pair = UniswapV2Library.pairFor(factory, token, WETH);28 uint value = approveMax ? uint(-1) : liquidity;29 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);30 (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);31 }Afișează totCopiați
Aceste funcții transmit meta-tranzacții, care permit utilizatorilor fără ether să retragă din fondul comun folosind mecanismul de autorizare.
12 // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) ****3 function removeLiquidityETHSupportingFeeOnTransferTokens(4 address token,5 uint liquidity,6 uint amountTokenMin,7 uint amountETHMin,8 address to,9 uint deadline10 ) public virtual override ensure(deadline) returns (uint amountETH) {11 (, amountETH) = removeLiquidity(12 token,13 WETH,14 liquidity,15 amountTokenMin,16 amountETHMin,17 address(this),18 deadline19 );20 TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));21 IWETH(WETH).withdraw(amountETH);22 TransferHelper.safeTransferETH(to, amountETH);23 }24Afișează totCopiaț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.
123 function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(4 address token,5 uint liquidity,6 uint amountTokenMin,7 uint amountETHMin,8 address to,9 uint deadline,10 bool approveMax, uint8 v, bytes32 r, bytes32 s11 ) external virtual override returns (uint amountETH) {12 address pair = UniswapV2Library.pairFor(factory, token, WETH);13 uint value = approveMax ? uint(-1) : liquidity;14 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);15 amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(16 token, liquidity, amountTokenMin, amountETHMin, to, deadline17 );18 }Afișează totCopiaț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 pair3 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.
- Situația inițială
- Un comerciant vinde 24,695 tokenuri A și obține 25,305 tokenuri B.
- Comerciantul vinde 24,695 tokenuri B pentru 25,305 tokenuri C, păstrând aproximativ 0,61 token B ca profit.
- 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-B | Schimb B-C | Schimb A-C |
---|---|---|---|
1 | A:1000 B:1050 A/B=1.05 | B:1000 C:1050 B/C=1.05 | A:1050 C:1000 C/A=1.05 |
2 | A:1024.695 B:1024.695 A/B=1 | B:1000 C:1050 B/C=1.05 | A:1050 C:1000 C/A=1.05 |
3 | A:1024.695 B:1024.695 A/B=1 | B:1024.695 C:1024.695 B/C=1 | A:1050 C:1000 C/A=1.05 |
4 | A:1024.695 B:1024.695 A/B=1 | B:1024.695 C:1024.695 B/C=1 | A:1024.695 C:1024.695 C/A=1 |
1 (address input, address output) = (path[i], path[i + 1]);2 (address token0,) = UniswapV2Library.sortTokens(input, output);3 uint amountOut = amounts[i + 1];Copiaț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.
12 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 deadline3 ) 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 deadline7 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {8 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);9 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');10 TransferHelper.safeTransferFrom(11 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]12 );13 _swap(amounts, path, to);14 }Afișează totCopiaț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 external3 virtual4 override5 payable6 ensure(deadline)7 returns (uint[] memory amounts)8 {9 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');10 amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);11 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');12 IWETH(WETH).deposit{value: amounts[0]}();13 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));14 _swap(amounts, path, to);15 }161718 function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)19 external20 virtual21 override22 ensure(deadline)23 returns (uint[] memory amounts)24 {25 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');26 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);27 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');28 TransferHelper.safeTransferFrom(29 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]30 );31 _swap(amounts, path, address(this));32 IWETH(WETH).withdraw(amounts[amounts.length - 1]);33 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);34 }35363738 function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)39 external40 virtual41 override42 ensure(deadline)43 returns (uint[] memory amounts)44 {45 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');46 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);47 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');48 TransferHelper.safeTransferFrom(49 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]50 );51 _swap(amounts, path, address(this));52 IWETH(WETH).withdraw(amounts[amounts.length - 1]);53 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);54 }555657 function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)58 external59 virtual60 override61 payable62 ensure(deadline)63 returns (uint[] memory amounts)64 {65 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');66 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);67 require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');68 IWETH(WETH).deposit{value: amounts[0]}();69 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));70 _swap(amounts, path, to);71 // refund dust eth, if any72 if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);73 }Afișează totCopiaț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 pair3 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 errors8 (uint reserve0, uint reserve1,) = pair.getReserves();9 (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);10 amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);11 amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);Afișează totCopiaț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 }789 function swapExactTokensForTokensSupportingFeeOnTransferTokens(10 uint amountIn,11 uint amountOutMin,12 address[] calldata path,13 address to,14 uint deadline15 ) external virtual override ensure(deadline) {16 TransferHelper.safeTransferFrom(17 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn18 );19 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);20 _swapSupportingFeeOnTransferTokens(path, to);21 require(22 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,23 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'24 );25 }262728 function swapExactETHForTokensSupportingFeeOnTransferTokens(29 uint amountOutMin,30 address[] calldata path,31 address to,32 uint deadline33 )34 external35 virtual36 override37 payable38 ensure(deadline)39 {40 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');41 uint amountIn = msg.value;42 IWETH(WETH).deposit{value: amountIn}();43 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));44 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);45 _swapSupportingFeeOnTransferTokens(path, to);46 require(47 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,48 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'49 );50 }515253 function swapExactTokensForETHSupportingFeeOnTransferTokens(54 uint amountIn,55 uint amountOutMin,56 address[] calldata path,57 address to,58 uint deadline59 )60 external61 virtual62 override63 ensure(deadline)64 {65 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');66 TransferHelper.safeTransferFrom(67 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn68 );69 _swapSupportingFeeOnTransferTokens(path, address(this));70 uint amountOut = IERC20(WETH).balanceOf(address(this));71 require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');72 IWETH(WETH).withdraw(amountOut);73 TransferHelper.safeTransferETH(to, amountOut);74 }Afișează totCopiaț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 }56 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)7 public8 pure9 virtual10 override11 returns (uint amountOut)12 {13 return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);14 }1516 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)17 public18 pure19 virtual20 override21 returns (uint amountIn)22 {23 return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);24 }2526 function getAmountsOut(uint amountIn, address[] memory path)27 public28 view29 virtual30 override31 returns (uint[] memory amounts)32 {33 return UniswapV2Library.getAmountsOut(factory, amountIn, path);34 }3536 function getAmountsIn(uint amountOut, address[] memory path)37 public38 view39 virtual40 override41 returns (uint[] memory amounts)42 {43 return UniswapV2Library.getAmountsIn(factory, amountOut, path);44 }45}Afișează totCopiaț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;23// a library for performing various math operations45library Math {6 function min(uint x, uint y) internal pure returns (uint z) {7 z = x < y ? x : y;8 }910 // babylonian method (https://wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)11 function sqrt(uint y) internal pure returns (uint z) {12 if (y > 3) {13 z = y;14 uint x = y / 2 + 1;Afișează totCopiaț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;23// a library for handling binary fixed point numbers (https://wikipedia.org/wiki/Q_(number_format))45// range: [0, 2**112 - 1]6// resolution: 1 / 2**11278library UQ112x112 {9 uint224 constant Q112 = 2**112;Afișează totCopiați
Q112
este codificarea pentru unu.
1 // encode a uint112 as a UQ112x1122 function encode(uint112 y) internal pure returns (uint224 z) {3 z = uint224(y) * Q112; // never overflows4 }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 UQ112x1122 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;23import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';45import "./SafeMath.sol";67library UniswapV2Library {8 using SafeMath for uint;910 // returns sorted token addresses, used to handle return values from pairs sorted in this order11 function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {12 require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');13 (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);14 require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');15 }Afișează totCopiaț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 calls2 function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {3 (address token0, address token1) = sortTokens(tokenA, tokenB);4 pair = address(uint(keccak256(abi.encodePacked(5 hex'ff',6 factory,7 keccak256(abi.encodePacked(token0, token1)),8 hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash9 ))));10 }Afișează totCopiaț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 pair2 function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {3 (address token0,) = sortTokens(tokenA, tokenB);4 (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();5 (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);6 }Copiaț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 asset2 function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {3 require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');4 require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');5 amountB = amountA.mul(reserveB) / reserveA;6 }Copiaț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 asset2 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.
12 require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');3 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');4 uint amountInWithFee = amountIn.mul(997);5 uint numerator = amountInWithFee.mul(reserveOut);6 uint denominator = reserveIn.mul(1000).add(amountInWithFee);7 amountOut = numerator / denominator;8 }Copiaț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 asset2 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {3 require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');4 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');5 uint numerator = reserveIn.mul(amountOut).mul(1000);6 uint denominator = reserveOut.sub(amountOut).mul(997);7 amountIn = (numerator / denominator).add(1);8 }Copiați
Această funcție efectuează aproximativ același lucru, însă obține suma rezultantă și oferă datele de introdus.
12 // performs chained getAmountOut calculations on any number of pairs3 function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {4 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');5 amounts = new uint[](path.length);6 amounts[0] = amountIn;7 for (uint i; i < path.length - 1; i++) {8 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);9 amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);10 }11 }1213 // performs chained getAmountIn calculations on any number of pairs14 function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {15 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');16 amounts = new uint[](path.length);17 amounts[amounts.length - 1] = amountOut;18 for (uint i = path.length - 1; i > 0; i--) {19 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);20 amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);21 }22 }23}Afișează totCopiaț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-later23pragma solidity >=0.6.0;45// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false6library TransferHelper {7 function safeApprove(8 address token,9 address to,10 uint256 value11 ) internal {12 // bytes4(keccak256(bytes('approve(address,uint256)')));13 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));14Afișează totCopiați
Putem să apelăm un contract diferit în două moduri:
- Prin folosirea unei definiții de interfață pentru a crea un apel de funcție
- Prin folosirea interfeței binare a aplicației (ABI)(opens in a new tab) „manual” pentru a crea apelul. Acesta este modul în care autorul codului a decis să o facă.
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
).
123 function safeTransfer(4 address token,5 address to,6 uint256 value7 ) internal {8 // bytes4(keccak256(bytes('transfer(address,uint256)')));9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));10 require(11 success && (data.length == 0 || abi.decode(data, (bool))),12 'TransferHelper::safeTransfer: transfer failed'13 );14 }Afișează totCopiaț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.
12 function safeTransferFrom(3 address token,4 address from,5 address to,6 uint256 value7 ) internal {8 // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));10 require(11 success && (data.length == 0 || abi.decode(data, (bool))),12 'TransferHelper::transferFrom: transferFrom failed'13 );14 }Afișează totCopiaț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.
12 function safeTransferETH(address to, uint256 value) internal {3 (bool success, ) = to.call{value: value}(new bytes(0));4 require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');5 }6}Copiaț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: @wackerow(opens in a new tab), 2 aprilie 2024