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';10Afișați 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 {2Copiaț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;2Copiaț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;2Copiaț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;2Copiaț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)')));2Copiaț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;2Copiaț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;3Copiaț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 getReserves3Copiaț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 getReserves2Copiaț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;3Copiaț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 event2Copiaț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;2Copiaț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() {2Copiaț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;3Copiaț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 _;2Copiaț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 }3Copiaț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 }6Copiaț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));3Copiaț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 }3Copiaț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);3Copiaț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 );9Copiaț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);2Copiaț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 }4Copiaț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 }7Copiaț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 {3Copiaț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');2Copiaț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) {4