Przewodnik po kontrakcie Uniswap-v2
Wprowadzenie
Uniswap v2 (opens in a new tab) może tworzyć rynek wymiany pomiędzy dowolnymi dwoma tokenami ERC-20. W tym artykule przeanalizujemy kod źródłowy kontraktów, które implementują ten protokół i zobaczymy, dlaczego są one napisane w ten sposób.
Co robi Uniswap?
Zasadniczo istnieją dwa typy użytkowników: dostawcy płynności i handlowcy.
Dostawcy płynności zapewniają puli dwa tokeny, które można wymieniać (nazwiemy je Token0 i Token1). W zamian otrzymują trzeci token, który reprezentuje częściową własność puli, nazywany tokenem płynności.
Handlowcy wysyłają do puli jeden rodzaj tokena i otrzymują drugi (na przykład wysyłają Token0 i otrzymują Token1) z puli zapewnionej przez dostawców płynności. Kurs wymiany jest określany przez względną liczbę tokenów Token0 i Token1, które posiada pula. Dodatkowo, pula pobiera niewielki procent jako nagrodę dla puli płynności.
Gdy dostawcy płynności chcą odzyskać swoje aktywa, mogą spalić tokeny puli i otrzymać z powrotem swoje tokeny, w tym swój udział w nagrodach.
Kliknij tutaj, aby uzyskać pełniejszy opis (opens in a new tab).
Dlaczego v2? Dlaczego nie v3?
Uniswap v3 (opens in a new tab) jest aktualizacją, która jest znacznie bardziej skomplikowana niż v2. Łatwiej jest najpierw nauczyć się v2, a następnie przejść do v3.
Kontrakty Główne a Kontrakty Peryferyjne
Uniswap v2 jest podzielony na dwa komponenty, główny i peryferyjny. Ten podział pozwala, aby kontrakty główne, które przechowują aktywa i dlatego muszą być bezpieczne, były prostsze i łatwiejsze do audytu. Cała dodatkowa funkcjonalność wymagana przez handlowców może być następnie zapewniona przez kontrakty peryferyjne.
Przepływy danych i kontroli
Jest to przepływ danych i kontroli, który ma miejsce podczas wykonywania trzech głównych działań Uniswap:
- Zamiana pomiędzy różnymi tokenami
- Dodawanie płynności do rynku i otrzymywanie w nagrodę tokenów płynności ERC-20 giełdy par
- Spalanie tokenów płynności ERC-20 i odzyskiwanie tokenów ERC-20, którymi giełda par pozwala handlowcom handlować
Zamiana
Jest to najczęstszy przepływ, używany przez handlowców:
Wywołujący
- Zapewnij kontu peryferyjnemu uprawnienie na kwotę do zamiany.
- Wywołaj jedną z wielu funkcji zamiany kontraktu peryferyjnego (która z nich zależy od tego, czy zaangażowane jest ETH, czy handlowiec określa ilość tokenów do zdeponowania lub ilość tokenów do odzyskania itp.).
Każda funkcja zamiany akceptuje
path, czyli tablicę giełd do przejścia.
W kontrakcie peryferyjnym (UniswapV2Router02.sol)
- Zidentyfikuj kwoty, które muszą być przedmiotem handlu na każdej giełdzie wzdłuż ścieżki.
- Iteruje po ścieżce. Dla każdej giełdy po drodze wysyła token wejściowy, a następnie wywołuje funkcję
swapgiełdy. W większości przypadków adresem docelowym dla tokenów jest następna giełda par na ścieżce. W ostatniej giełdzie jest to adres podany przez handlowca.
W kontrakcie głównym (UniswapV2Pair.sol)
- Sprawdź, ile dodatkowych tokenów mamy oprócz znanych rezerw. Ta kwota to liczba tokenów wejściowych, które otrzymaliśmy do wymiany.
- Wyślij tokeny wyjściowe do miejsca docelowego.
- Wywołaj
_update, aby zaktualizować kwoty rezerw
Z powrotem w kontrakcie peryferyjnym (UniswapV2Router02.sol)
- Wykonaj niezbędne czyszczenie (na przykład spal tokeny WETH, aby odzyskać ETH do wysłania handlowcowi)
Dodaj płynność
Wywołujący
- Zapewnij kontu peryferyjnemu uprawnienie na kwoty do dodania do puli płynności.
- Wywołaj jedną z funkcji
addLiquiditykontraktu peryferyjnego.
W kontrakcie peryferyjnym (UniswapV2Router02.sol)
- W razie potrzeby utwórz nową giełdę par
- Jeśli istnieje giełda par, oblicz ilość tokenów do dodania. Ma to być identyczna wartość dla obu tokenów, więc taki sam stosunek nowych tokenów do istniejących.
- Sprawdź, czy kwoty są dopuszczalne (wywołujący mogą określić minimalną kwotę, poniżej której woleliby nie dodawać płynności)
- Wywołaj kontrakt główny.
W kontrakcie głównym (UniswapV2Pair.sol)
- Wybij tokeny płynności i wyślij je do wywołującego
- Wywołaj
_update, aby zaktualizować kwoty rezerw
Usuń płynność
Wywołujący
- Zapewnij kontu peryferyjnemu uprawnienie na tokeny płynności do spalenia w zamian za tokeny bazowe.
- Wywołaj jedną z funkcji
removeLiquiditykontraktu peryferyjnego.
W kontrakcie peryferyjnym (UniswapV2Router02.sol)
- Wyślij tokeny płynności do giełdy par
W kontrakcie głównym (UniswapV2Pair.sol)
- Wyślij na adres docelowy tokeny bazowe w proporcji do spalonych tokenów. Na przykład, jeśli w puli jest 1000 tokenów A, 500 tokenów B i 90 tokenów płynności, a my otrzymujemy 9 tokenów do spalenia, spalamy 10% tokenów płynności, więc odsyłamy użytkownikowi 100 tokenów A i 50 tokenów B.
- Spal tokeny płynności
- Wywołaj
_update, aby zaktualizować kwoty rezerw
Kontrakty główne
Są to bezpieczne kontrakty, które przechowują płynność.
UniswapV2Pair.sol
Ten kontrakt (opens in a new tab) implementuje faktyczną pulę, która wymienia tokeny. Jest to podstawowa funkcjonalność Uniswap.
1pragma solidity =0.5.16;2
3import './interfaces/IUniswapV2Pair.sol';4import './UniswapV2ERC20.sol';5import './libraries/Math.sol';6import './libraries/UQ112x112.sol';7import './interfaces/IERC20.sol';8import './interfaces/IUniswapV2Factory.sol';9import './interfaces/IUniswapV2Callee.sol';Są to wszystkie interfejsy, o których kontrakt musi wiedzieć, albo dlatego, że kontrakt je implementuje (IUniswapV2Pair i UniswapV2ERC20), albo dlatego, że wywołuje kontrakty, które je implementują.
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {Ten kontrakt dziedziczy po UniswapV2ERC20, który zapewnia funkcje ERC-20 dla tokenów płynności.
1 using SafeMath for uint;Biblioteka SafeMath (opens in a new tab) jest używana do unikania przepełnień i niedomiarów. Jest to ważne, ponieważ w przeciwnym razie możemy skończyć w sytuacji, w której wartość powinna wynosić -1, ale zamiast tego wynosi 2^256-1.
1 using UQ112x112 for uint224;Wiele obliczeń w kontrakcie puli wymaga ułamków. Jednak ułamki nie są obsługiwane przez EVM.
Rozwiązaniem, które znalazł Uniswap, jest użycie 224-bitowych wartości, z 112 bitami na część całkowitą i 112 bitami na część ułamkową. Tak więc 1.0 jest reprezentowane jako 2^112, 1.5 jest reprezentowane jako 2^112 + 2^111 itd.
Więcej szczegółów na temat tej biblioteki jest dostępnych w dalszej części dokumentu.
Zmienne
1 uint public constant MINIMUM_LIQUIDITY = 10**3;Aby uniknąć przypadków dzielenia przez zero, istnieje minimalna liczba tokenów płynności, które zawsze istnieją (ale są własnością konta zero). Ta liczba to MINIMUM_LIQUIDITY, czyli tysiąc.
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));Jest to selektor ABI dla funkcji transferu ERC-20. Służy do transferu tokenów ERC-20 na dwóch kontach tokenów.
1 address public factory;Jest to kontrakt fabryki, który stworzył tę pulę. Każda pula jest giełdą pomiędzy dwoma tokenami ERC-20, fabryka jest centralnym punktem, który łączy wszystkie te pule.
1 address public token0;2 address public token1;Są to adresy kontraktów dla dwóch typów tokenów ERC-20, które mogą być wymieniane przez tę pulę.
1 uint112 private reserve0; // uses single storage slot, accessible via getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReservesRezerwy, które pula posiada dla każdego typu tokena. Zakładamy, że oba reprezentują tę samą wartość, a zatem każdy token0 jest wart reserve1/reserve0 tokenów1.
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReservesZnacznik czasu dla ostatniego bloku, w którym nastąpiła wymiana, używany do śledzenia kursów wymiany w czasie.
Jednym z największych wydatków na gaz w kontraktach Ethereum jest pamięć masowa (storage), która utrzymuje się od jednego wywołania kontraktu do następnego. Każda komórka pamięci masowej ma długość 256 bitów. Tak więc trzy zmienne, reserve0, reserve1 i blockTimestampLast, są przydzielane w taki sposób, aby pojedyncza wartość pamięci masowej mogła zawierać wszystkie trzy z nich (112+112+32=256).
1 uint public price0CumulativeLast;2 uint public price1CumulativeLast;Te zmienne przechowują skumulowane koszty dla każdego tokena (każdy w odniesieniu do drugiego). Można ich użyć do obliczenia średniego kursu wymiany w danym okresie.
1 uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity eventSposób, w jaki giełda par decyduje o kursie wymiany między tokenem0 a tokenem1, polega na utrzymywaniu stałej wielokrotności obu rezerw podczas transakcji. kLast jest tą wartością. Zmienia się, gdy dostawca płynności wpłaca lub wypłaca tokeny, i nieznacznie wzrasta z powodu 0,3% opłaty rynkowej.
Oto prosty przykład. Należy zauważyć, że dla uproszczenia tabela ma tylko trzy cyfry po przecinku i ignorujemy opłatę handlową w wysokości 0,3%, więc liczby nie są dokładne.
| Zdarzenie | reserve0 | reserve1 | reserve0 * reserve1 | Średni kurs wymiany (token1 / token0) |
|---|---|---|---|---|
| Ustawienie początkowe | 1 000,000 | 1 000,000 | 1 000 000 | |
| Handlowiec A zamienia 50 token0 na 47.619 token1 | 1 050,000 | 952,381 | 1 000 000 | 0,952 |
| Handlowiec B zamienia 10 token0 na 8.984 token1 | 1 060,000 | 943,396 | 1 000 000 | 0,898 |
| Handlowiec C zamienia 40 token0 na 34.305 token1 | 1 100,000 | 909,090 | 1 000 000 | 0,858 |
| Handlowiec D zamienia 100 token1 na 109,01 token0 | 990,990 | 1 009,090 | 1 000 000 | 0,917 |
| Handlowiec E zamienia 10 token0 na 10,079 token1 | 1 000,990 | 999,010 | 1 000 000 | 1,008 |
Gdy handlowcy dostarczają więcej tokenów0, względna wartość tokenów1 wzrasta i odwrotnie, w oparciu o podaż i popyt.
Blokada
1 uint private unlocked = 1;Istnieje klasa luk w zabezpieczeniach, które opierają się na nadużyciu reentrancy (opens in a new tab). Uniswap musi transferować dowolne tokeny ERC-20, co oznacza wywoływanie kontraktów ERC-20, które mogą próbować nadużyć rynek Uniswap, który je wywołuje.
Posiadając zmienną unlocked jako część kontraktu, możemy zapobiec wywoływaniu funkcji podczas ich działania (w ramach tej samej transakcji).
1 modifier lock() {Ta funkcja jest modyfikatorem (opens in a new tab), funkcją, która opakowuje normalną funkcję, aby w jakiś sposób zmienić jej zachowanie.
1 require(unlocked == 1, 'UniswapV2: LOCKED');2 unlocked = 0;Jeśli unlocked jest równe jeden, ustaw je na zero. Jeśli jest już równe zero, odwróć wywołanie, spraw, by zakończyło się niepowodzeniem.
1 _;W modyfikatorze _; jest oryginalnym wywołaniem funkcji (ze wszystkimi parametrami). Tutaj oznacza to, że wywołanie funkcji ma miejsce tylko wtedy, gdy unlocked było równe jeden, gdy zostało wywołane, a podczas jego działania wartość unlocked jest równa zero.
1 unlocked = 1;2 }Po powrocie funkcji głównej zwolnij blokadę.
Różne funkcje
1 function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {2 _reserve0 = reserve0;3 _reserve1 = reserve1;4 _blockTimestampLast = blockTimestampLast;5 }Ta funkcja dostarcza wywołującym aktualny stan giełdy. Zauważ, że funkcje Solidity mogą zwracać wiele wartości (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));Ta funkcja wewnętrzna transferuje ilość tokenów ERC20 z giełdy do kogoś innego. SELECTOR określa, że funkcja, którą wywołujemy, to transfer(address,uint) (zobacz definicję powyżej).
Aby uniknąć konieczności importowania interfejsu dla funkcji tokenu, „ręcznie” tworzymy wywołanie za pomocą jednej z funkcji ABI (opens in a new tab).
1 require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');2 }Istnieją dwa sposoby, w jakie wywołanie transferu ERC-20 może zgłosić niepowodzenie:
- Odwrócenie. Jeśli wywołanie do kontraktu zewnętrznego zostanie odwrócone, to wartość logiczna zwracana jest
false - Zakończenie normalnie, ale zgłoszenie niepowodzenia. W takim przypadku bufor wartości zwracanej ma niezerową długość, a po zdekodowaniu jako wartość logiczna jest
false
Jeśli którykolwiek z tych warunków wystąpi, odwróć.
Zdarzenia
1 event Mint(address indexed sender, uint amount0, uint amount1);2 event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);Te dwa zdarzenia są emitowane, gdy dostawca płynności wpłaca płynność (Mint) lub ją wypłaca (Burn). W obu przypadkach ilości token0 i token1, które są wpłacane lub wypłacane, są częścią zdarzenia, podobnie jak tożsamość konta, które nas wywołało (sender). W przypadku wypłaty zdarzenie obejmuje również cel, który otrzymał tokeny (to), który może nie być taki sam jak nadawca.
1 event Swap(2 address indexed sender,3 uint amount0In,4 uint amount1In,5 uint amount0Out,6 uint amount1Out,7 address indexed to8 );To zdarzenie jest emitowane, gdy handlowiec zamienia jeden token na drugi. Ponownie, nadawca i miejsce docelowe mogą nie być takie same. Każdy token może być albo wysłany do giełdy, albo z niej otrzymany.
1 event Sync(uint112 reserve0, uint112 reserve1);Wreszcie, Sync jest emitowany za każdym razem, gdy tokeny są dodawane lub wypłacane, niezależnie od powodu, aby zapewnić najnowsze informacje o rezerwach (a zatem o kursie wymiany).
Funkcje konfiguracji
Te funkcje powinny być wywoływane raz, podczas konfigurowania nowej giełdy par.
1 constructor() public {2 factory = msg.sender;3 }Konstruktor zapewnia, że będziemy śledzić adres fabryki, która stworzyła parę. Ta informacja jest wymagana dla initialize i dla opłaty fabrycznej (jeśli istnieje)
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 }Ta funkcja pozwala fabryce (i tylko fabryce) określić dwa tokeny ERC-20, które ta para będzie wymieniać.
Wewnętrzne funkcje aktualizacji
_update
1 // update reserves and, on the first call per block, price accumulators2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {Ta funkcja jest wywoływana za każdym razem, gdy tokeny są wpłacane lub wypłacane.
1 require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');Jeśli saldo0 lub saldo1 (uint256) jest wyższe niż uint112(-1) (=2^112-1) (więc przepełnia się i zawija z powrotem do 0 po konwersji na uint112), odmawia kontynuacji _update, aby zapobiec przepełnieniom. W przypadku normalnego tokena, który można podzielić na jednostki 10^18, oznacza to, że każda wymiana jest ograniczona do około 5,1*10^15 każdego z tokenów. Jak dotąd nie stanowiło to problemu.
1 uint32 blockTimestamp = uint32(block.timestamp % 2**32);2 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired3 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {Jeśli upłynął czas niezerowy, oznacza to, że jesteśmy pierwszą transakcją wymiany w tym bloku. W takim przypadku musimy zaktualizować akumulatory kosztów.
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 }Każdy akumulator kosztów jest aktualizowany o najnowszy koszt (rezerwa drugiego tokena/rezerwa tego tokena) pomnożony przez upływający czas w sekundach. Aby uzyskać średnią cenę, odczytujesz skumulowaną cenę w dwóch punktach w czasie i dzielisz przez różnicę czasu między nimi. Na przykład, załóżmy następującą sekwencję zdarzeń:
| Zdarzenie | reserve0 | reserve1 | znacznik czasu | Marginalny kurs wymiany (reserve1 / reserve0) | price0CumulativeLast |
|---|---|---|---|---|---|
| Ustawienie początkowe | 1 000,000 | 1 000,000 | 5 000 | 1,000 | 0 |
| Handlowiec A wpłaca 50 tokenów0 i otrzymuje z powrotem 47,619 tokenów1 | 1 050,000 | 952,381 | 5 020 | 0,907 | 20 |
| Handlowiec B wpłaca 10 tokenów0 i otrzymuje z powrotem 8,984 tokenów1 | 1 060,000 | 943,396 | 5 030 | 0,890 | 20+10*0.907 = 29.07 |
| Handlowiec C wpłaca 40 tokenów0 i otrzymuje z powrotem 34,305 tokenów1 | 1 100,000 | 909,090 | 5 100 | 0,826 | 29.07+70*0.890 = 91.37 |
| Handlowiec D wpłaca 100 tokenów1 i otrzymuje z powrotem 109,01 tokenów0 | 990,990 | 1 009,090 | 5 110 | 1,018 | 91.37+10*0.826 = 99.63 |
| Handlowiec E wpłaca 10 tokenów0 i otrzymuje z powrotem 10,079 tokenów1 | 1 000,990 | 999,010 | 5 150 | 0,998 | 99.63+40*1.1018 = 143.702 |
Powiedzmy, że chcemy obliczyć średnią cenę Token0 między znacznikami czasu 5030 i 5150. Różnica w wartości price0Cumulative wynosi 143,702-29,07=114,632. Jest to średnia z dwóch minut (120 sekund). Więc średnia cena wynosi 114,632/120 = 0,955.
To obliczenie ceny jest powodem, dla którego musimy znać stare rozmiary rezerw.
1 reserve0 = uint112(balance0);2 reserve1 = uint112(balance1);3 blockTimestampLast = blockTimestamp;4 emit Sync(reserve0, reserve1);5 }Na koniec zaktualizuj zmienne globalne i wyemituj zdarzenie 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) {W Uniswap 2.0 handlowcy płacą 0,30% opłaty za korzystanie z rynku. Większość tej opłaty (0,25% transakcji) zawsze trafia do dostawców płynności. Pozostałe 0,05% może trafić albo do dostawców płynności, albo na adres określony przez fabrykę jako opłata za protokół, która płaci Uniswap za ich pracę rozwojową.
Aby zmniejszyć obliczenia (a tym samym koszty gazu), opłata ta jest obliczana tylko wtedy, gdy płynność jest dodawana lub usuwana z puli, a nie przy każdej transakcji.
1 address feeTo = IUniswapV2Factory(factory).feeTo();2 feeOn = feeTo != address(0);Odczytaj miejsce docelowe opłaty w fabryce. Jeśli jest to zero, to nie ma opłaty za protokół i nie ma potrzeby jej obliczać.
1 uint _kLast = kLast; // gas savingsZmienna stanu kLast znajduje się w pamięci masowej, więc będzie miała wartość między różnymi wywołaniami kontraktu.
Dostęp do pamięci masowej jest znacznie droższy niż dostęp do pamięci ulotnej, która jest zwalniana po zakończeniu wywołania funkcji do kontraktu, więc używamy zmiennej wewnętrznej, aby zaoszczędzić na gazie.
1 if (feeOn) {2 if (_kLast != 0) {Dostawcy płynności otrzymują swój udział po prostu dzięki aprecjacji ich tokenów płynności. Ale opłata za protokół wymaga wybicia nowych tokenów płynności i dostarczenia ich na adres feeTo.
1 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));2 uint rootKLast = Math.sqrt(_kLast);3 if (rootK > rootKLast) {Jeśli istnieje nowa płynność, od której można pobrać opłatę za protokół. Funkcję pierwiastka kwadratowego można zobaczyć w dalszej części tego artykułu
1 uint numerator = totalSupply.mul(rootK.sub(rootKLast));2 uint denominator = rootK.mul(5).add(rootKLast);3 uint liquidity = numerator / denominator;To skomplikowane obliczenie opłat jest wyjaśnione w dokumentacji (opens in a new tab) na stronie 5. Wiemy, że między czasem, w którym obliczono kLast, a teraźniejszością nie dodano ani nie usunięto żadnej płynności (ponieważ wykonujemy to obliczenie za każdym razem, gdy płynność jest dodawana lub usuwana, zanim faktycznie się zmieni), więc każda zmiana w reserve0 * reserve1 musi pochodzić z opłat transakcyjnych (bez nich utrzymywalibyśmy stałą wartość reserve0 * reserve1).
1 if (liquidity > 0) _mint(feeTo, liquidity);2 }3 }Użyj funkcji UniswapV2ERC20._mint, aby faktycznie utworzyć dodatkowe tokeny płynności i przypisać je do feeTo.
1 } else if (_kLast != 0) {2 kLast = 0;3 }4 }Jeśli nie ma ustawionej opłaty, ustaw kLast na zero (jeśli jeszcze tak nie jest). Kiedy ten kontrakt był pisany, istniała funkcja zwrotu gazu (opens in a new tab), która zachęcała kontrakty do zmniejszania ogólnego rozmiaru stanu Ethereum poprzez zerowanie pamięci masowej, której nie potrzebowały.
Ten kod otrzymuje ten zwrot, gdy jest to możliwe.
Funkcje dostępne zewnętrznie
Należy pamiętać, że chociaż każda transakcja lub kontrakt może wywołać te funkcje, są one zaprojektowane do wywoływania z kontraktu peryferyjnego. Jeśli wywołasz je bezpośrednio, nie będziesz w stanie oszukać giełdy par, ale możesz stracić wartość przez pomyłkę.
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) {Ta funkcja jest wywoływana, gdy dostawca płynności dodaje płynność do puli. Wybija dodatkowe tokeny płynności jako nagrodę. Powinna być wywoływana z kontraktu peryferyjnego, który wywołuje ją po dodaniu płynności w tej samej transakcji (aby nikt inny nie mógł złożyć transakcji, która rości sobie prawo do nowej płynności przed prawowitym właścicielem).
1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savingsJest to sposób na odczytanie wyników funkcji Solidity, która zwraca wiele wartości. Odrzucamy ostatnie zwrócone wartości, znacznik czasu bloku, ponieważ go nie potrzebujemy.
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);Pobierz bieżące salda i zobacz, ile dodano każdego typu tokena.
1 bool feeOn = _mintFee(_reserve0, _reserve1);Oblicz opłaty za protokół do pobrania, jeśli istnieją, i odpowiednio wybij tokeny płynności. Ponieważ parametry dla _mintFee są starymi wartościami rezerw, opłata jest obliczana dokładnie na podstawie tylko zmian w puli wynikających z opłat.
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 tokensJeśli jest to pierwszy depozyt, utwórz MINIMUM_LIQUIDITY tokenów i wyślij je na adres zero, aby je zablokować. Nigdy nie można ich wykupić, co oznacza, że pula nigdy nie zostanie całkowicie opróżniona (co chroni nas przed dzieleniem przez zero w niektórych miejscach). Wartość MINIMUM_LIQUIDITY to tysiąc, co biorąc pod uwagę, że większość tokenów ERC-20 jest podzielona na jednostki 10^-18 tokena, tak jak ETH jest podzielone na wei, jest to 10^-15 wartości pojedynczego tokena. Niewielki koszt.
W momencie pierwszego depozytu nie znamy względnej wartości obu tokenów, więc po prostu mnożymy kwoty i bierzemy pierwiastek kwadratowy, zakładając, że depozyt zapewnia nam równą wartość w obu tokenach.
Możemy temu ufać, ponieważ w interesie deponenta jest zapewnienie równej wartości, aby uniknąć utraty wartości na arbitrażu. Powiedzmy, że wartość obu tokenów jest identyczna, ale nasz deponent zdeponował cztery razy więcej Token1 niż Token0. Handlowiec może wykorzystać fakt, że giełda par uważa, że Token0 jest bardziej wartościowy, aby wydobyć z niego wartość.
| Zdarzenie | reserve0 | reserve1 | reserve0 * reserve1 | Wartość puli (reserve0 + reserve1) |
|---|---|---|---|---|
| Ustawienie początkowe | 8 | 32 | 256 | 40 |
| Handlowiec wpłaca 8 tokenów Token0, otrzymuje z powrotem 16 Token1 | 16 | 16 | 256 | 32 |
Jak widać, handlowiec zarobił dodatkowe 8 tokenów, które pochodzą ze zmniejszenia wartości puli, szkodząc deponentowi, który jest jej właścicielem.
1 } else {2 liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);Przy każdym kolejnym depozycie znamy już kurs wymiany między dwoma aktywami i oczekujemy, że dostawcy płynności zapewnią równą wartość w obu. Jeśli tego nie zrobią, dajemy im tokeny płynności w oparciu o mniejszą wartość, którą dostarczyli, jako karę.
Niezależnie od tego, czy jest to depozyt początkowy, czy kolejny, liczba tokenów płynności, które dostarczamy, jest równa pierwiastkowi kwadratowemu ze zmiany reserve0*reserve1, a wartość tokena płynności nie zmienia się (chyba że otrzymamy depozyt, który nie ma równych wartości obu typów, w którym to przypadku „kara” jest rozdzielana). Oto kolejny przykład z dwoma tokenami o tej samej wartości, z trzema dobrymi depozytami i jednym złym (depozyt tylko jednego typu tokena, więc nie produkuje żadnych tokenów płynności).
| Zdarzenie | reserve0 | reserve1 | reserve0 * reserve1 | Wartość puli (reserve0 + reserve1) | Tokeny płynności wybite dla tego depozytu | Całkowita liczba tokenów płynności | wartość każdego tokena płynności |
|---|---|---|---|---|---|---|---|
| Ustawienie początkowe | 8,000 | 8,000 | 64 | 16,000 | 8 | 8 | 2,000 |
| Zdeponuj cztery z każdego typu | 12,000 | 12,000 | 144 | 24,000 | 4 | 12 | 2,000 |
| Zdeponuj dwa z każdego typu | 14,000 | 14,000 | 196 | 28,000 | 2 | 14 | 2,000 |
| Nierówny depozyt | 18,000 | 14,000 | 252 | 32,000 | 0 | 14 | ~2,286 |
| Po arbitrażu | ~15,874 | ~15,874 | 252 | ~31,748 | 0 | 14 | ~2,267 |
1 }2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');3 _mint(to, liquidity);Użyj funkcji UniswapV2ERC20._mint, aby faktycznie utworzyć dodatkowe tokeny płynności i przekazać je na właściwe konto.
1
2 _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 }Zaktualizuj zmienne stanu (reserve0, reserve1 i w razie potrzeby kLast) i wyemituj odpowiednie zdarzenie.
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) {Ta funkcja jest wywoływana, gdy płynność jest wypłacana i odpowiednie tokeny płynności muszą zostać spalone. Powinna być również wywoływana z konta peryferyjnego.
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)];Kontrakt peryferyjny przetransferował płynność do spalenia do tego kontraktu przed wywołaniem. W ten sposób wiemy, ile płynności spalić i możemy upewnić się, że zostanie ona spalona.
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');Dostawca płynności otrzymuje równą wartość obu tokenów. W ten sposób nie zmieniamy kursu wymiany.
1 _burn(address(this), liquidity);2 _safeTransfer(_token0, to, amount0);3 _safeTransfer(_token1, to, amount1);4 balance0 = IERC20(_token0).balanceOf(address(this));5 balance1 = IERC20(_token1).balanceOf(address(this));6
7 _update(balance0, balance1, _reserve0, _reserve1);8 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date9 emit Burn(msg.sender, amount0, amount1, to);10 }11
Reszta funkcji burn jest lustrzanym odbiciem funkcji mint powyżej.
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 {Ta funkcja również powinna być wywoływana z kontraktu peryferyjnego.
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');4
5 uint balance0;6 uint balance1;7 { // scope for _token{0,1}, avoids stack too deep errorsZmienne lokalne mogą być przechowywane w pamięci lub, jeśli nie jest ich zbyt wiele, bezpośrednio na stosie. Jeśli możemy ograniczyć liczbę, aby użyć stosu, zużywamy mniej gazu. Więcej szczegółów można znaleźć w żółtej księdze, formalnych specyfikacjach Ethereum (opens in a new tab), s. 26, równanie 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 tokensTen transfer jest optymistyczny, ponieważ transferujemy, zanim jesteśmy pewni, że wszystkie warunki są spełnione. Jest to w porządku w Ethereum, ponieważ jeśli warunki nie zostaną spełnione później w wywołaniu, wycofujemy się z niego i wszelkich zmian, które spowodował.
1 if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);Poinformuj odbiorcę o zamianie, jeśli jest to wymagane.
1 balance0 = IERC20(_token0).balanceOf(address(this));2 balance1 = IERC20(_token1).balanceOf(address(this));3 }Pobierz bieżące salda. Kontrakt peryferyjny wysyła nam tokeny przed wywołaniem nas do zamiany. Ułatwia to kontraktowi sprawdzenie, czy nie jest oszukiwany, co musi nastąpić w kontrakcie głównym (ponieważ możemy być wywoływani przez inne jednostki niż nasz kontrakt peryferyjny).
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');Jest to kontrola poprawności, aby upewnić się, że nie stracimy na zamianie. W żadnym wypadku zamiana nie powinna zmniejszać reserve0*reserve1. To również tutaj zapewniamy, że opłata w wysokości 0,3% jest wysyłana przy zamianie; przed sprawdzeniem poprawności wartości K, mnożymy oba salda przez 1000 odjęte od kwot pomnożonych przez 3, co oznacza, że 0,3% (3/1000 = 0,003 = 0,3%) jest odliczane od salda przed porównaniem jego wartości K z bieżącą wartością K rezerw.
1 }2
3 _update(balance0, balance1, _reserve0, _reserve1);4 emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);5 }Zaktualizuj reserve0 i reserve1, a w razie potrzeby akumulatory cen i znacznik czasu oraz wyemituj zdarzenie.
Sync lub Skim
Możliwe jest, że rzeczywiste salda nie będą zsynchronizowane z rezerwami, które giełda par uważa, że ma.
Nie ma sposobu na wypłatę tokenów bez zgody kontraktu, ale depozyty to inna sprawa. Konto może przetransferować tokeny do giełdy bez wywoływania mint lub swap.
W takim przypadku istnieją dwa rozwiązania:
sync, zaktualizuj rezerwy do bieżących saldskim, wypłać nadwyżkę. Zauważ, że każde konto może wywołaćskim, ponieważ nie wiemy, kto zdeponował tokeny. Ta informacja jest emitowana w zdarzeniu, ale zdarzenia nie są dostępne z blockchaina.
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 }8
9
10
11 // 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}UniswapV2Factory.sol
Ten kontrakt (opens in a new tab) tworzy giełdy par.
1pragma solidity =0.5.16;2
3import './interfaces/IUniswapV2Factory.sol';4import './UniswapV2Pair.sol';5
6contract UniswapV2Factory is IUniswapV2Factory {7 address public feeTo;8 address public feeToSetter;Te zmienne stanu są niezbędne do wdrożenia opłaty za protokół (patrz dokumentacja (opens in a new tab), s. 5).
Adres feeTo gromadzi tokeny płynności dla opłaty za protokół, a feeToSetter to adres upoważniony do zmiany feeTo na inny adres.
1 mapping(address => mapping(address => address)) public getPair;2 address[] public allPairs;Te zmienne śledzą pary, giełdy między dwoma typami tokenów.
Pierwsza z nich, getPair, to mapowanie, które identyfikuje kontrakt giełdy par na podstawie dwóch tokenów ERC-20, które wymienia. Tokeny ERC-20 są identyfikowane przez adresy kontraktów, które je implementują, więc klucze i wartość są wszystkie adresami. Aby uzyskać adres giełdy par, która pozwala na konwersję z tokenA na tokenB, używasz getPair[<adres tokenA>][<adres tokenB>] (lub odwrotnie).
Druga zmienna, allPairs, to tablica, która zawiera wszystkie adresy giełd par utworzonych przez tę fabrykę. W Ethereum nie można iterować po zawartości mapowania ani uzyskać listy wszystkich kluczy, więc ta zmienna jest jedynym sposobem, aby wiedzieć, którymi giełdami zarządza ta fabryka.
Uwaga: Powodem, dla którego nie można iterować po wszystkich kluczach mapowania, jest to, że przechowywanie danych kontraktu jest drogie, więc im mniej go używamy, tym lepiej, i im rzadziej je zmieniamy, tym lepiej. Można tworzyć mapowania, które obsługują iterację (opens in a new tab), ale wymagają one dodatkowej pamięci masowej na listę kluczy. W większości aplikacji nie jest to potrzebne.
1 event PairCreated(address indexed token0, address indexed token1, address pair, uint);To zdarzenie jest emitowane, gdy tworzona jest nowa giełda par. Obejmuje ono adresy tokenów, adres giełdy par i całkowitą liczbę giełd zarządzanych przez fabrykę.
1 constructor(address _feeToSetter) public {2 feeToSetter = _feeToSetter;3 }Jedyną rzeczą, jaką robi konstruktor, jest określenie feeToSetter. Fabryki zaczynają bez opłaty i tylko feeSetter może to zmienić.
1 function allPairsLength() external view returns (uint) {2 return allPairs.length;3 }Ta funkcja zwraca liczbę par giełdowych.
1 function createPair(address tokenA, address tokenB) external returns (address pair) {Jest to główna funkcja fabryki, tworząca giełdę par między dwoma tokenami ERC-20. Zauważ, że każdy może wywołać tę funkcję. Nie potrzebujesz pozwolenia od Uniswap, aby utworzyć nową giełdę par.
1 require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');2 (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);Chcemy, aby adres nowej giełdy był deterministyczny, aby można go było obliczyć z góry poza łańcuchem (może to być przydatne w przypadku transakcji warstwy 2). Aby to zrobić, musimy mieć spójną kolejność adresów tokenów, niezależnie od kolejności, w jakiej je otrzymaliśmy, więc sortujemy je tutaj.
1 require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');2 require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficientDuże pule płynności są lepsze niż małe, ponieważ mają bardziej stabilne ceny. Nie chcemy mieć więcej niż jednej puli płynności na parę tokenów. Jeśli giełda już istnieje, nie ma potrzeby tworzenia kolejnej dla tej samej pary.
1 bytes memory bytecode = type(UniswapV2Pair).creationCode;Aby utworzyć nowy kontrakt, potrzebujemy kodu, który go tworzy (zarówno funkcji konstruktora, jak i kodu, który zapisuje w pamięci kod bajtowy EVM właściwego kontraktu). Zazwyczaj w Solidity używamy po prostu addr = new <nazwa kontraktu>(<parametry konstruktora>), a kompilator zajmuje się wszystkim za nas, ale aby mieć deterministyczny adres kontraktu, musimy użyć kodu operacyjnego CREATE2 (opens in a new tab).
Kiedy ten kod był pisany, ten kod operacyjny nie był jeszcze obsługiwany przez Solidity, więc konieczne było ręczne pobranie kodu. To już nie jest problemem, ponieważ Solidity teraz obsługuje 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 }Gdy kod operacyjny nie jest jeszcze obsługiwany przez Solidity, możemy go wywołać za pomocą asembler wbudowany (opens in a new tab).
1 IUniswapV2Pair(pair).initialize(token0, token1);Wywołaj funkcję initialize, aby poinformować nową giełdę, jakie dwa tokeny wymienia.
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);Zapisz nowe informacje o parze w zmiennych stanu i wyemituj zdarzenie, aby poinformować świat o nowej giełdzie par.
1 function setFeeTo(address _feeTo) external {2 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');3 feeTo = _feeTo;4 }5
6 function setFeeToSetter(address _feeToSetter) external {7 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');8 feeToSetter = _feeToSetter;9 }10}Te dwie funkcje pozwalają feeSetter kontrolować odbiorcę opłaty (jeśli istnieje) i zmienić feeSetter na nowy adres.
UniswapV2ERC20.sol
Ten kontrakt (opens in a new tab) implementuje token płynności ERC-20. Jest podobny do kontraktu OpenZeppelin ERC-20, więc wyjaśnię tylko tę część, która jest inna, funkcjonalność permit.
Transakcje w Ethereum kosztują ether (ETH), który jest równowartością prawdziwych pieniędzy. Jeśli masz tokeny ERC-20, ale nie masz ETH, nie możesz wysyłać transakcji, więc nie możesz nic z nimi zrobić. Jednym z rozwiązań tego problemu są metatransakcje (opens in a new tab). Właściciel tokenów podpisuje transakcję, która pozwala komuś innemu wypłacić tokeny poza łańcuchem i wysyła ją przez Internet do odbiorcy. Odbiorca, który ma ETH, następnie składa pozwolenie w imieniu właściciela.
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;Ten hasz jest identyfikatorem typu transakcji (opens in a new tab). Jedynym, który tu obsługujemy, jest Permit z tymi parametrami.
1 mapping(address => uint) public nonces;Sfałszowanie podpisu cyfrowego przez odbiorcę jest niewykonalne. Jednak trywialne jest dwukrotne wysłanie tej samej transakcji (jest to forma ataku typu replay (opens in a new tab)). Aby temu zapobiec, używamy nonce (opens in a new tab). Jeśli nonce nowego Permit nie jest o jeden większy niż ostatnio użyty, zakładamy, że jest on nieważny.
1 constructor() public {2 uint chainId;3 assembly {4 chainId := chainid5 }Jest to kod do pobrania identyfikatora łańcucha (opens in a new tab). Używa dialektu asemblera EVM o nazwie Yul (opens in a new tab). Zauważ, że w bieżącej wersji Yul musisz używać chainid(), a nie 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 }Oblicz separator domeny (opens in a new tab) dla EIP-712.
1 function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {Jest to funkcja, która implementuje uprawnienia. Otrzymuje jako parametry odpowiednie pola oraz trzy wartości skalarne dla podpisu (opens in a new tab) (v, r i s).
1 require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');Nie akceptuj transakcji po terminie.
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 );abi.encodePacked(...) to komunikat, którego oczekujemy. Wiemy, jakie powinno być nonce, więc nie ma potrzeby, abyśmy otrzymywali je jako parametr.
Algorytm podpisu Ethereum oczekuje do podpisania 256 bitów, więc używamy funkcji haszującej keccak256.
1 address recoveredAddress = ecrecover(digest, v, r, s);Z digestu i podpisu możemy uzyskać adres, który go podpisał, używając ecrecover (opens in a new tab).
1 require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');2 _approve(owner, spender, value);3 }4
Jeśli wszystko jest w porządku, traktuj to jako zatwierdzenie ERC-20 (opens in a new tab).
Kontrakty peryferyjne
Kontrakty peryferyjne to API (interfejs programowania aplikacji) dla Uniswap. Są one dostępne dla wywołań zewnętrznych, zarówno z innych kontraktów, jak i ze zdecentralizowanych aplikacji. Można by wywoływać kontrakty główne bezpośrednio, ale jest to bardziej skomplikowane i można stracić wartość, jeśli popełni się błąd. Kontrakty główne zawierają tylko testy, aby upewnić się, że nie są oszukiwane, a nie kontrole poprawności dla nikogo innego. Te są w peryferiach, więc można je aktualizować w razie potrzeby.
UniswapV2Router01.sol
Ten kontrakt (opens in a new tab) ma problemy i nie powinien być już używany (opens in a new tab). Na szczęście kontrakty peryferyjne są bezstanowe i nie przechowują żadnych aktywów, więc łatwo jest je wycofać i zasugerować ludziom, aby zamiast tego używali zamiennika, UniswapV2Router02.
UniswapV2Router02.sol
W większości przypadków używałbyś Uniswap za pośrednictwem tego kontraktu (opens in a new tab). Możesz zobaczyć, jak go używać tutaj (opens in a new tab).
1pragma solidity =0.6.6;2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';4import '@uniswap/lib/contracts/libraries/TransferHelper.sol';5
6import './interfaces/IUniswapV2Router02.sol';7import './libraries/UniswapV2Library.sol';8import './libraries/SafeMath.sol';9import './interfaces/IERC20.sol';10import './interfaces/IWETH.sol';Większość z nich spotkaliśmy już wcześniej lub są dość oczywiste. Jedynym wyjątkiem jest IWETH.sol. Uniswap v2 pozwala na wymianę dowolnej pary tokenów ERC-20, ale sam ether (ETH) nie jest tokenem ERC-20. Poprzedza on standard i jest transferowany za pomocą unikalnych mechanizmów. Aby umożliwić wykorzystanie ETH w kontraktach, które dotyczą tokenów ERC-20, ludzie wymyślili kontrakt opakowanego etheru (WETH) (opens in a new tab). Wysyłasz do tego kontraktu ETH, a on wybija Ci równoważną ilość WETH. Możesz też spalić WETH i odzyskać ETH.
1contract UniswapV2Router02 is IUniswapV2Router02 {2 using SafeMath for uint;3
4 address public immutable override factory;5 address public immutable override WETH;Router musi wiedzieć, jakiej fabryki użyć, a dla transakcji, które wymagają WETH, jakiego kontraktu WETH użyć. Te wartości są niezmienne (opens in a new tab), co oznacza, że można je ustawić tylko w konstruktorze. Daje to użytkownikom pewność, że nikt nie będzie w stanie ich zmienić, aby wskazywały na mniej uczciwe kontrakty.
1 modifier ensure(uint deadline) {2 require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');3 _;4 }Ten modyfikator zapewnia, że transakcje o ograniczonym czasie („zrób X przed czasem Y, jeśli możesz”) nie mają miejsca po upływie ich terminu.
1 constructor(address _factory, address _WETH) public {2 factory = _factory;3 WETH = _WETH;4 }Konstruktor ustawia tylko niezmienne zmienne stanu.
1 receive() external payable {2 assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract3 }Ta funkcja jest wywoływana, gdy wymieniamy tokeny z kontraktu WETH z powrotem na ETH. Tylko kontrakt WETH, którego używamy, jest do tego upoważniony.
Dodaj płynność
Te funkcje dodają tokeny do giełdy par, co zwiększa pulę płynności.
1
2 // **** ADD LIQUIDITY ****3 function _addLiquidity(Ta funkcja służy do obliczania ilości tokenów A i B, które powinny zostać zdeponowane na giełdzie par.
1 address tokenA,2 address tokenB,Są to adresy kontraktów tokenów ERC-20.
1 uint amountADesired,2 uint amountBDesired,Są to kwoty, które dostawca płynności chce zdeponować. Są to również maksymalne kwoty A i B do zdeponowania.
1 uint amountAMin,2 uint amountBMinSą to minimalne dopuszczalne kwoty do zdeponowania. Jeśli transakcja nie może się odbyć z tymi kwotami lub większymi, wycofaj się z niej. Jeśli nie chcesz tej funkcji, po prostu podaj zero.
Dostawcy płynności określają minimum, zazwyczaj dlatego, że chcą ograniczyć transakcję do kursu wymiany zbliżonego do bieżącego. Jeśli kurs wymiany zbytnio się waha, może to oznaczać wiadomości, które zmieniają podstawowe wartości, a oni chcą ręcznie zdecydować, co robić.
Na przykład wyobraź sobie przypadek, w którym kurs wymiany wynosi jeden do jednego, a dostawca płynności określa te wartości:
| Parametr | Wartość |
|---|---|
| amountADesired | 1000 |
| amountBDesired | 1000 |
| amountAMin | 900 |
| amountBMin | 800 |
Dopóki kurs wymiany pozostaje między 0,9 a 1,25, transakcja ma miejsce. Jeśli kurs wymiany wyjdzie poza ten zakres, transakcja zostanie anulowana.
Powodem tego środka ostrożności jest to, że transakcje nie są natychmiastowe, przesyłasz je i ostatecznie walidator uwzględni je w bloku (chyba że cena gazu jest bardzo niska, w którym to przypadku będziesz musiał przesłać kolejną transakcję z tym samym nonce i wyższą ceną gazu, aby ją nadpisać). Nie możesz kontrolować tego, co dzieje się w okresie między złożeniem a włączeniem.
1 ) internal virtual returns (uint amountA, uint amountB) {Funkcja zwraca kwoty, które dostawca płynności powinien zdeponować, aby mieć stosunek równy bieżącemu stosunkowi między rezerwami.
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 }Jeśli nie ma jeszcze giełdy dla tej pary tokenów, utwórz ją.
1 (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);Pobierz bieżące rezerwy w parze.
1 if (reserveA == 0 && reserveB == 0) {2 (amountA, amountB) = (amountADesired, amountBDesired);Jeśli bieżące rezerwy są puste, to jest to nowa giełda par. Kwoty do zdeponowania powinny być dokładnie takie same, jak te, które chce dostarczyć dostawca płynności.
1 } else {2 uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);Jeśli musimy zobaczyć, jakie będą kwoty, otrzymujemy optymalną kwotę za pomocą tej funkcji (opens in a new tab). Chcemy tego samego stosunku co bieżące rezerwy.
1 if (amountBOptimal <= amountBDesired) {2 require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');3 (amountA, amountB) = (amountADesired, amountBOptimal);Jeśli amountBOptimal jest mniejsza niż kwota, którą dostawca płynności chce zdeponować, oznacza to, że token B jest obecnie bardziej wartościowy, niż myśli deponent płynności, więc wymagana jest mniejsza kwota.
1 } else {2 uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);3 assert(amountAOptimal <= amountADesired);4 require(amountAOptimal >= amountAMin, 'UniswapV2Router: NIEWYSTARCZAJĄCA_ILOŚĆ_A');5 (amountA, amountB) = (amountAOptimal, amountBDesired);Jeśli optymalna kwota B jest większa niż pożądana kwota B, oznacza to, że tokeny B są obecnie mniej wartościowe, niż myśli deponent płynności, więc wymagana jest wyższa kwota. Jednak pożądana kwota jest maksimum, więc nie możemy tego zrobić. Zamiast tego obliczamy optymalną liczbę tokenów A dla pożądanej ilości tokenów B.
Łącząc wszystko razem, otrzymujemy ten wykres. Załóżmy, że próbujesz zdeponować tysiąc tokenów A (niebieska linia) i tysiąc tokenów B (czerwona linia). Oś x to kurs wymiany, A/B. Jeśli x=1, mają równą wartość i wpłacasz po tysiąc każdego. Jeśli x=2, A ma dwukrotnie większą wartość niż B (otrzymujesz dwa tokeny B za każdy token A), więc wpłacasz tysiąc tokenów B, ale tylko 500 tokenów A. Jeśli x=0,5, sytuacja jest odwrotna, tysiąc tokenów A i pięćset tokenów B.
Można zdeponować płynność bezpośrednio w kontrakcie głównym (używając UniswapV2Pair::mint (opens in a new tab)), ale kontrakt główny sprawdza tylko, czy sam nie jest oszukiwany, więc ryzykujesz utratę wartości, jeśli kurs wymiany zmieni się między czasem złożenia transakcji a czasem jej wykonania. Jeśli używasz kontraktu peryferyjnego, oblicza on kwotę, którą powinieneś wpłacić, i wpłaca ją natychmiast, więc kurs wymiany się nie zmienia i nic nie tracisz.
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 deadlineTa funkcja może być wywoływana przez transakcję w celu zdeponowania płynności. Większość parametrów jest taka sama jak w _addLiquidity powyżej, z dwoma wyjątkami:
. to to adres, który otrzymuje nowe tokeny płynności wybite, aby pokazać udział dostawcy płynności w puli
. deadline to limit czasowy transakcji
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);Obliczamy kwoty do faktycznego zdeponowania, a następnie znajdujemy adres puli płynności. Aby zaoszczędzić gaz, nie robimy tego, pytając fabryki, ale używając funkcji bibliotecznej pairFor (patrz poniżej w bibliotekach)
1 TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);2 TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);Przenieś odpowiednie ilości tokenów od użytkownika na giełdę par.
1 liquidity = IUniswapV2Pair(pair).mint(to);2 }W zamian przekaż na adres to tokeny płynności za częściową własność puli. Funkcja mint kontraktu głównego sprawdza, ile dodatkowych tokenów posiada (w porównaniu z tym, co miała ostatnim razem, gdy płynność się zmieniła) i odpowiednio wybija płynność.
1 function addLiquidityETH(2 address token,3 uint amountTokenDesired,Gdy dostawca płynności chce zapewnić płynność dla giełdy par Token/ETH, jest kilka różnic. Kontrakt obsługuje opakowywanie ETH dla dostawcy płynności. Nie ma potrzeby określania, ile ETH użytkownik chce zdeponować, ponieważ użytkownik po prostu wysyła je z transakcją (kwota jest dostępna w 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));Aby zdeponować ETH, kontrakt najpierw opakowuje je w WETH, a następnie transferuje WETH do pary. Zauważ, że transfer jest opakowany w assert. Oznacza to, że jeśli transfer się nie powiedzie, to wywołanie kontraktu również się nie powiedzie, a zatem opakowanie tak naprawdę się nie odbywa.
1 liquidity = IUniswapV2Pair(pair).mint(to);2 // refund dust eth, if any3 if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);4 }Użytkownik już wysłał nam ETH, więc jeśli zostanie jakaś nadwyżka (ponieważ drugi token jest mniej wartościowy, niż myślał użytkownik), musimy dokonać zwrotu.
Usuń płynność
Te funkcje usuną płynność i zwrócą pieniądze dostawcy płynności.
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) {Najprostszy przypadek usunięcia płynności. Istnieje minimalna ilość każdego tokena, którą dostawca płynności zgadza się zaakceptować, i musi to nastąpić przed terminem.
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);Funkcja burn kontraktu głównego obsługuje zwrot tokenów użytkownikowi.
1 (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);Gdy funkcja zwraca wiele wartości, ale interesują nas tylko niektóre z nich, w ten sposób uzyskujemy tylko te wartości. Jest to nieco tańsze pod względem gazu niż odczytanie wartości i nigdy jej nieużywanie.
1 (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);Przetłumacz kwoty ze sposobu, w jaki zwraca je kontrakt główny (najpierw token o niższym adresie) na sposób, w jaki oczekuje ich użytkownik (odpowiadający tokenA i tokenB).
1 require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');2 require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');3 }Można najpierw dokonać transferu, a następnie zweryfikować jego legalność, ponieważ jeśli nie jest legalny, wycofamy się ze wszystkich zmian stanu.
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 }Usunięcie płynności dla ETH jest prawie takie samo, z tym wyjątkiem, że otrzymujemy tokeny WETH, a następnie wymieniamy je na ETH, aby oddać je dostawcy płynności.
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 }16
17
18 function removeLiquidityETHWithPermit(19 address token,20 uint liquidity,21 uint amountTokenMin,22 uint amountETHMin,23 address to,24 uint deadline,25 bool approveMax, uint8 v, bytes32 r, bytes32 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 }Te funkcje przekazują metatransakcje, aby umożliwić użytkownikom bez etheru wypłatę z puli, używając mechanizmu pozwolenia.
1
2 // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) ****3 function removeLiquidityETHSupportingFeeOnTransferTokens(4 address token,5 uint liquidity,6 uint amountTokenMin,7 uint amountETHMin,8 address to,9 uint 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 }24
Tej funkcji można używać dla tokenów, które mają opłaty za transfer lub przechowywanie. Gdy token ma takie opłaty, nie możemy polegać na funkcji removeLiquidity, aby powiedziała nam, ile tokena odzyskamy, więc musimy najpierw wypłacić, a następnie sprawdzić saldo.
1
2
3 function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(4 address token,5 uint liquidity,6 uint amountTokenMin,7 uint amountETHMin,8 address to,9 uint deadline,10 bool approveMax, uint8 v, bytes32 r, bytes32 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 }Ostatnia funkcja łączy opłaty za przechowywanie z meta-transakcjami.
Handel
1 // **** WYMIANA ****2 // wymaga, aby początkowa kwota została już wysłana do pierwszej pary3 function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {Ta funkcja wykonuje wewnętrzne przetwarzanie, które jest wymagane dla funkcji udostępnianych handlarzom.
1 for (uint i; i < path.length - 1; i++) {W chwili, gdy to piszę, istnieje 388 160 tokenów ERC-20 (opens in a new tab). Gdyby istniała wymiana dla każdej pary tokenów, byłoby to ponad 150 miliardów wymian par. Cały łańcuch w tym momencie ma tylko 0,1% tej liczby kont (opens in a new tab). Zamiast tego funkcje wymiany obsługują koncepcję ścieżki. Handlarz może wymienić A na B, B na C i C na D, więc nie ma potrzeby bezpośredniej wymiany pary A-D.
Ceny na tych rynkach mają tendencję do synchronizacji, ponieważ gdy nie są zsynchronizowane, stwarza to okazję do arbitrażu. Wyobraźmy sobie na przykład trzy tokeny: A, B i C. Istnieją trzy wymiany par, po jednej dla każdej pary.
- Sytuacja początkowa
- Handlarz sprzedaje 24,695 tokenów A i otrzymuje 25,305 tokenów B.
- Handlarz sprzedaje 24,695 tokenów B za 25,305 tokenów C, zatrzymując około 0,61 tokenów B jako zysk.
- Następnie handlarz sprzedaje 24,695 tokenów C za 25,305 tokenów A, zatrzymując około 0,61 tokenów C jako zysk. Handlarz ma również 0,61 dodatkowych tokenów A (25,305, które handlarz otrzymuje na koniec, minus pierwotna inwestycja w wysokości 24,695).
| Krok | Wymiana A-B | Wymiana B-C | Wymiana 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];Pobierz parę, którą obecnie obsługujemy, posortuj ją (do użytku z parą) i pobierz oczekiwaną kwotę wyjściową.
1 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));Pobierz oczekiwane kwoty wyjściowe, posortowane w sposób, w jaki oczekuje ich wymiana par.
1 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;Czy to ostatnia wymiana? Jeśli tak, wyślij tokeny otrzymane w ramach transakcji do miejsca docelowego. Jeśli nie, wyślij je do następnej wymiany par.
1
2 IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(3 amount0Out, amount1Out, to, new bytes(0)4 );5 }6 }Faktycznie wywołaj wymianę par, aby zamienić tokeny. Nie potrzebujemy wywołania zwrotnego, aby zostać poinformowanym o wymianie, więc nie wysyłamy żadnych bajtów w tym polu.
1 function swapExactTokensForTokens(Ta funkcja jest używana bezpośrednio przez handlarzy do zamiany jednego tokena na inny.
1 uint amountIn,2 uint amountOutMin,3 address[] calldata path,Ten parametr zawiera adresy kontraktów ERC-20. Jak wyjaśniono powyżej, jest to tablica, ponieważ może być konieczne przejście przez kilka wymian par, aby przejść od posiadanego zasobu do zasobu, który chcesz uzyskać.
Parametr funkcji w Solidity może być przechowywany w memory lub calldata. Jeśli funkcja jest punktem wejścia do kontraktu, wywoływanym bezpośrednio przez użytkownika (za pomocą transakcji) lub z innego kontraktu, wartość parametru można pobrać bezpośrednio z danych wywołania. Jeśli funkcja jest wywoływana wewnętrznie, jak _swap powyżej, parametry muszą być przechowywane w memory. Z perspektywy wywoływanego kontraktu calldata jest tylko do odczytu.
W przypadku typów skalarnych, takich jak uint lub address, kompilator sam wybiera dla nas sposób przechowywania, ale w przypadku tablic, które są dłuższe i droższe, to my określamy rodzaj używanego miejsca do przechowywania.
1 address to,2 uint deadline3 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {Zwracane wartości są zawsze zwracane w pamięci.
1 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);2 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');Oblicz kwotę do zakupu w każdej wymianie. Jeśli wynik jest mniejszy niż minimum, które handlarz jest gotów zaakceptować, wycofaj transakcję.
1 TransferHelper.safeTransferFrom(2 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]3 );4 _swap(amounts, path, to);5 }Na koniec przelej początkowy token ERC-20 na konto pierwszej wymiany par i wywołaj _swap. Wszystko to dzieje się w tej samej transakcji, więc wymiana par wie, że wszelkie nieoczekiwane tokeny są częścią tego transferu.
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 }Poprzednia funkcja, swapTokensForTokens, pozwala handlarzowi określić dokładną liczbę tokenów wejściowych, które jest gotów oddać, oraz minimalną liczbę tokenów wyjściowych, które jest gotów otrzymać w zamian. Ta funkcja wykonuje odwrotną wymianę, pozwala handlarzowi określić liczbę tokenów wyjściowych, które chce, oraz maksymalną liczbę tokenów wejściowych, które jest gotów za nie zapłacić.
W obu przypadkach handlarz musi najpierw udzielić temu kontraktowi peryferyjnemu zezwolenia na ich transfer.
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 }16
17
18 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 }35
36
37
38 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 }55
56
57 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 // zwróć resztę eth, jeśli istnieje72 if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);73 }Te cztery warianty dotyczą handlu między ETH a tokenami. Jedyną różnicą jest to, że albo otrzymujemy ETH od handlarza i używamy go do wybicia WETH, albo otrzymujemy WETH z ostatniej wymiany na ścieżce i spalamy go, odsyłając handlarzowi powstałe ETH.
1 // **** WYMIANA (obsługa tokenów z opłatą za transfer) ****2 // wymaga, aby początkowa kwota została już wysłana do pierwszej pary3 function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {Jest to funkcja wewnętrzna do wymiany tokenów, które mają opłaty za transfer lub przechowywanie, w celu rozwiązania (tego problemu (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 { // zakres w celu uniknięcia błędów zbyt głębokiego stosu8 (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);Z powodu opłat za transfer nie możemy polegać na funkcji getAmountsOut, która informuje nas, ile otrzymujemy z każdego transferu (tak jak robimy to przed wywołaniem oryginalnej funkcji _swap). Zamiast tego musimy najpierw dokonać transferu, a następnie zobaczyć, ile tokenów otrzymaliśmy z powrotem.
Uwaga: teoretycznie moglibyśmy po prostu użyć tej funkcji zamiast _swap, ale w niektórych przypadkach (na przykład, jeśli transfer zostanie ostatecznie wycofany, ponieważ na końcu nie ma wystarczającej ilości, aby spełnić wymagane minimum) kosztowałoby to więcej gazu. Tokeny z opłatą za transfer są dość rzadkie, więc chociaż musimy je uwzględnić, nie ma potrzeby, aby wszystkie wymiany zakładały, że przechodzą przez co najmniej jeden z nich.
1 }2 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));3 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;4 pair.swap(amount0Out, amount1Out, to, new bytes(0));5 }6 }7
8
9 function swapExactTokensForTokensSupportingFeeOnTransferTokens(10 uint amountIn,11 uint amountOutMin,12 address[] calldata path,13 address to,14 uint 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 }26
27
28 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 }51
52
53 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 }Są to te same warianty, które są używane dla normalnych tokenów, ale zamiast tego wywołują _swapSupportingFeeOnTransferTokens.
1 // **** FUNKCJE BIBLIOTEKI ****2 function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {3 return UniswapV2Library.quote(amountA, reserveA, reserveB);4 }5
6 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)7 public8 pure9 virtual10 override11 returns (uint amountOut)12 {13 return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);14 }15
16 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 }25
26 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 }35
36 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}Te funkcje są tylko pośrednikami, które wywołują funkcje UniswapV2Library.
UniswapV2Migrator.sol
Ten kontrakt był używany do migracji wymian ze starej wersji v1 do v2. Teraz, gdy zostały zmigrowane, nie jest to już istotne.
Biblioteki
Biblioteka SafeMath (opens in a new tab) jest dobrze udokumentowana, więc nie ma potrzeby jej tutaj dokumentować.
Math
Ta biblioteka zawiera kilka funkcji matematycznych, które normalnie nie są potrzebne w kodzie Solidity, więc nie są częścią języka.
1pragma solidity =0.5.16;2
3// biblioteka do wykonywania różnych operacji matematycznych4
5library Math {6 function min(uint x, uint y) internal pure returns (uint z) {7 z = x < y ? x : y;8 }9
10 // metoda babilońska (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;Zacznij od x jako oszacowania, które jest wyższe niż pierwiastek kwadratowy (dlatego musimy traktować 1-3 jako przypadki specjalne).
1 while (x < z) {2 z = x;3 x = (y / x + x) / 2;Uzyskaj bliższe oszacowanie, średnią z poprzedniego oszacowania i liczby, której pierwiastek kwadratowy próbujemy znaleźć, podzieloną przez poprzednie oszacowanie. Powtarzaj, aż nowe oszacowanie nie będzie niższe od istniejącego. Więcej szczegółów znajdziesz tutaj (opens in a new tab).
1 }2 } else if (y != 0) {3 z = 1;Nigdy nie powinniśmy potrzebować pierwiastka kwadratowego z zera. Pierwiastki kwadratowe z jednego, dwóch i trzech to w przybliżeniu jeden (używamy liczb całkowitych, więc ignorujemy część ułamkową).
1 }2 }3}Ułamki stałoprzecinkowe (UQ112x112)
Ta biblioteka obsługuje ułamki, które normalnie nie są częścią arytmetyki Ethereum. Robi to poprzez kodowanie liczby x jako x*2^112. Pozwala nam to na użycie oryginalnych kodów operacyjnych dodawania i odejmowania bez zmian.
1pragma solidity =0.5.16;2
3// biblioteka do obsługi binarnych liczb stałoprzecinkowych (https://wikipedia.org/wiki/Q_(number_format))4
5// zakres: [0, 2**112 - 1]6// rozdzielczość: 1 / 2**1127
8library UQ112x112 {9 uint224 constant Q112 = 2**112;Q112 to kodowanie jedynki.
1 // koduje uint112 jako UQ112x1122 function encode(uint112 y) internal pure returns (uint224 z) {3 z = uint224(y) * Q112; // nigdy się nie przepełnia4 }Ponieważ y to uint112, największa możliwa wartość to 2^112-1. Ta liczba nadal może być zakodowana jako UQ112x112.
1 // dzieli UQ112x112 przez uint112, zwracając UQ112x1122 function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {3 z = x / uint224(y);4 }5}Jeśli podzielimy dwie wartości UQ112x112, wynik nie będzie już pomnożony przez 2^112. Zamiast tego bierzemy liczbę całkowitą jako mianownik. Musielibyśmy użyć podobnej sztuczki, aby wykonać mnożenie, ale nie musimy wykonywać mnożenia wartości UQ112x112.
UniswapV2Library
Ta biblioteka jest używana tylko przez kontrakty peryferyjne
1pragma solidity >=0.5.0;2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';4
5import "./SafeMath.sol";6
7library UniswapV2Library {8 using SafeMath for uint;9
10 // zwraca posortowane adresy tokenów, używane do obsługi wartości zwracanych z par posortowanych w tej kolejności11 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 }Posortuj dwa tokeny według adresu, abyśmy mogli uzyskać adres wymiany par dla nich. Jest to konieczne, ponieważ w przeciwnym razie mielibyśmy dwie możliwości, jedną dla parametrów A,B, a drugą dla parametrów B,A, co prowadziłoby do dwóch wymian zamiast jednej.
1 // oblicza adres CREATE2 dla pary bez wykonywania żadnych wywołań zewnętrznych2 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' // hash kodu inicjującego9 ))));10 }Ta funkcja oblicza adres wymiany par dla dwóch tokenów. Ten kontrakt jest tworzony przy użyciu kodu operacyjnego CREATE2 (opens in a new tab), więc możemy obliczyć adres za pomocą tego samego algorytmu, jeśli znamy parametry, których używa. Jest to o wiele tańsze niż zapytanie kontraktu factory i
1 // pobiera i sortuje rezerwy dla pary2 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 }Ta funkcja zwraca rezerwy dwóch tokenów, które posiada wymiana par. Zauważ, że może odbierać tokeny w dowolnej kolejności i sortuje je do użytku wewnętrznego.
1 // biorąc pod uwagę pewną ilość zasobu i rezerwy pary, zwraca równowartość drugiego zasobu2 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 }Ta funkcja podaje ilość tokena B, którą otrzymasz w zamian za token A, jeśli nie ma żadnej opłaty. To obliczenie uwzględnia fakt, że transfer zmienia kurs wymiany.
1 // biorąc pod uwagę kwotę wejściową zasobu i rezerwy pary, zwraca maksymalną kwotę wyjściową drugiego zasobu2 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {Powyższa funkcja quote działa świetnie, jeśli nie ma opłaty za korzystanie z wymiany par. Jeśli jednak istnieje opłata za wymianę w wysokości 0,3%, kwota, którą faktycznie otrzymujesz, jest niższa. Ta funkcja oblicza kwotę po uwzględnieniu opłaty za wymianę.
1
2 require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');3 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');4 uint amountInWithFee = amountIn.mul(997);5 uint numerator = amountInWithFee.mul(reserveOut);6 uint denominator = reserveIn.mul(1000).add(amountInWithFee);7 amountOut = numerator / denominator;8 }Solidity nie obsługuje natywnie ułamków, więc nie możemy po prostu pomnożyć kwoty wyjściowej przez 0,997. Zamiast tego mnożymy licznik przez 997, a mianownik przez 1000, uzyskując ten sam efekt.
1 // biorąc pod uwagę kwotę wyjściową zasobu i rezerwy pary, zwraca wymaganą kwotę wejściową drugiego zasobu2 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 }Ta funkcja robi mniej więcej to samo, ale pobiera kwotę wyjściową i podaje kwotę wejściową.
1
2 // wykonuje łańcuchowe obliczenia getAmountOut na dowolnej liczbie par3 function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {4 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');5 amounts = new uint[](path.length);6 amounts[0] = amountIn;7 for (uint i; i < path.length - 1; i++) {8 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);9 amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);10 }11 }12
13 // wykonuje łańcuchowe obliczenia getAmountIn na dowolnej liczbie par14 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}Te dwie funkcje obsługują identyfikację wartości, gdy konieczne jest przejście przez kilka wymian par.
Pomocnik transferu
Ta biblioteka (opens in a new tab) dodaje sprawdzenia powodzenia wokół transferów ERC-20 i Ethereum, aby traktować wycofanie i zwrócenie wartości false w ten sam sposób.
1// SPDX-License-Identifier: GPL-3.0-or-later2
3pragma solidity >=0.6.0;4
5// metody pomocnicze do interakcji z tokenami ERC20 i wysyłania ETH, które nie zawsze zwracają 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));14
Możemy wywołać inny kontrakt na jeden z dwóch sposobów:
- Użyj definicji interfejsu, aby utworzyć wywołanie funkcji
- Użyj binarnego interfejsu aplikacji (ABI) (opens in a new tab) "ręcznie", aby utworzyć wywołanie. Tak właśnie zdecydował się zrobić autor kodu.
1 require(2 success && (data.length == 0 || abi.decode(data, (bool))),3 'TransferHelper::safeApprove: approve failed'4 );5 }Ze względu na kompatybilność wsteczną z tokenami, które zostały utworzone przed standardem ERC-20, wywołanie ERC-20 może zakończyć się niepowodzeniem albo przez wycofanie (w którym to przypadku success ma wartość false), albo przez pomyślne wykonanie i zwrócenie wartości false (w którym to przypadku istnieją dane wyjściowe, a jeśli zdekodujesz je jako wartość logiczną, otrzymasz false).
1
2
3 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 }Ta funkcja implementuje funkcjonalność transferu ERC-20 (opens in a new tab), która pozwala kontu na wydanie zezwolenia udzielonego przez inne konto.
1
2 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 }Ta funkcja implementuje funkcjonalność transferFrom ERC-20 (opens in a new tab), która pozwala kontu na wydanie zezwolenia udzielonego przez inne konto.
1
2 function safeTransferETH(address to, uint256 value) internal {3 (bool success, ) = to.call{value: value}(new bytes(0));4 require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');5 }6}Ta funkcja przesyła ether na konto. Każde wywołanie do innego kontraktu może próbować wysłać ether. Ponieważ nie musimy faktycznie wywoływać żadnej funkcji, nie wysyłamy żadnych danych z wywołaniem.
Wnioski
To długi artykuł, liczący około 50 stron. Jeśli dotarliście tutaj, gratulacje! Mamy nadzieję, że do tej pory zrozumieliście już kwestie związane z pisaniem prawdziwej aplikacji (w przeciwieństwie do krótkich programów przykładowych) i jesteście lepiej przygotowani do pisania kontraktów dla własnych przypadków użycia.
Teraz idźcie, napiszcie coś pożytecznego i zadziwcie nas.
Zobacz więcej mojej pracy tutaj (opens in a new tab).
Strona ostatnio zaktualizowana: 3 marca 2026
