Przejdź do głównej zawartości

Przewodnik po kontrakcie Uniswap-v2

Solidity
Średnio zaawansowany
Ori Pomerantz
1 maja 2021
54 minuta czytania

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:

  1. Zamiana pomiędzy różnymi tokenami
  2. Dodawanie płynności do rynku i otrzymywanie w nagrodę tokenów płynności ERC-20 giełdy par
  3. 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

  1. Zapewnij kontu peryferyjnemu uprawnienie na kwotę do zamiany.
  2. 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)

  1. Zidentyfikuj kwoty, które muszą być przedmiotem handlu na każdej giełdzie wzdłuż ścieżki.
  2. Iteruje po ścieżce. Dla każdej giełdy po drodze wysyła token wejściowy, a następnie wywołuje funkcję swap gieł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)

  1. Sprawdź, ile dodatkowych tokenów mamy oprócz znanych rezerw. Ta kwota to liczba tokenów wejściowych, które otrzymaliśmy do wymiany.
  2. Wyślij tokeny wyjściowe do miejsca docelowego.
  3. Wywołaj _update, aby zaktualizować kwoty rezerw

Z powrotem w kontrakcie peryferyjnym (UniswapV2Router02.sol)

  1. Wykonaj niezbędne czyszczenie (na przykład spal tokeny WETH, aby odzyskać ETH do wysłania handlowcowi)

Dodaj płynność

Wywołujący

  1. Zapewnij kontu peryferyjnemu uprawnienie na kwoty do dodania do puli płynności.
  2. Wywołaj jedną z funkcji addLiquidity kontraktu peryferyjnego.

W kontrakcie peryferyjnym (UniswapV2Router02.sol)

  1. W razie potrzeby utwórz nową giełdę par
  2. 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.
  3. Sprawdź, czy kwoty są dopuszczalne (wywołujący mogą określić minimalną kwotę, poniżej której woleliby nie dodawać płynności)
  4. Wywołaj kontrakt główny.

W kontrakcie głównym (UniswapV2Pair.sol)

  1. Wybij tokeny płynności i wyślij je do wywołującego
  2. Wywołaj _update, aby zaktualizować kwoty rezerw

Usuń płynność

Wywołujący

  1. Zapewnij kontu peryferyjnemu uprawnienie na tokeny płynności do spalenia w zamian za tokeny bazowe.
  2. Wywołaj jedną z funkcji removeLiquidity kontraktu peryferyjnego.

W kontrakcie peryferyjnym (UniswapV2Router02.sol)

  1. Wyślij tokeny płynności do giełdy par

W kontrakcie głównym (UniswapV2Pair.sol)

  1. 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.
  2. Spal tokeny płynności
  3. 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 getReserves
2 uint112 private reserve1; // uses single storage slot, accessible via getReserves

Rezerwy, 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 getReserves

Znacznik 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 event

Sposó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.

Zdarzeniereserve0reserve1reserve0 * reserve1Średni kurs wymiany (token1 / token0)
Ustawienie początkowe1 000,0001 000,0001 000 000
Handlowiec A zamienia 50 token0 na 47.619 token11 050,000952,3811 000 0000,952
Handlowiec B zamienia 10 token0 na 8.984 token11 060,000943,3961 000 0000,898
Handlowiec C zamienia 40 token0 na 34.305 token11 100,000909,0901 000 0000,858
Handlowiec D zamienia 100 token1 na 109,01 token0990,9901 009,0901 000 0000,917
Handlowiec E zamienia 10 token0 na 10,079 token11 000,990999,0101 000 0001,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:

  1. Odwrócenie. Jeśli wywołanie do kontraktu zewnętrznego zostanie odwrócone, to wartość logiczna zwracana jest false
  2. 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 to
8 );

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 deployment
2 function initialize(address _token0, address _token1) external {
3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
4 token0 = _token0;
5 token1 = _token1;
6 }

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 accumulators
2 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 desired
3 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 desired
2 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ń:

Zdarzeniereserve0reserve1znacznik czasuMarginalny kurs wymiany (reserve1 / reserve0)price0CumulativeLast
Ustawienie początkowe1 000,0001 000,0005 0001,0000
Handlowiec A wpłaca 50 tokenów0 i otrzymuje z powrotem 47,619 tokenów11 050,000952,3815 0200,90720
Handlowiec B wpłaca 10 tokenów0 i otrzymuje z powrotem 8,984 tokenów11 060,000943,3965 0300,89020+10*0.907 = 29.07
Handlowiec C wpłaca 40 tokenów0 i otrzymuje z powrotem 34,305 tokenów11 100,000909,0905 1000,82629.07+70*0.890 = 91.37
Handlowiec D wpłaca 100 tokenów1 i otrzymuje z powrotem 109,01 tokenów0990,9901 009,0905 1101,01891.37+10*0.826 = 99.63
Handlowiec E wpłaca 10 tokenów0 i otrzymuje z powrotem 10,079 tokenów11 000,990999,0105 1500,99899.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 savings

Zmienna 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 checks
2 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 savings

Jest 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 _mintFee
2 if (_totalSupply == 0) {
3 liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
4 _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens

Jeś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ść.

Zdarzeniereserve0reserve1reserve0 * reserve1Wartość puli (reserve0 + reserve1)
Ustawienie początkowe83225640
Handlowiec wpłaca 8 tokenów Token0, otrzymuje z powrotem 16 Token1161625632

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).

Zdarzeniereserve0reserve1reserve0 * reserve1Wartość puli (reserve0 + reserve1)Tokeny płynności wybite dla tego depozytuCałkowita liczba tokenów płynnościwartość każdego tokena płynności
Ustawienie początkowe8,0008,0006416,000882,000
Zdeponuj cztery z każdego typu12,00012,00014424,0004122,000
Zdeponuj dwa z każdego typu14,00014,00019628,0002142,000
Nierówny depozyt18,00014,00025232,000014~2,286
Po arbitrażu~15,874~15,874252~31,748014~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-date
4 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 checks
2 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 savings
2 address _token0 = token0; // gas savings
3 address _token1 = token1; // gas savings
4 uint balance0 = IERC20(_token0).balanceOf(address(this));
5 uint balance1 = IERC20(_token1).balanceOf(address(this));
6 uint liquidity = balanceOf[address(this)];

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 _mintFee
3 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
4 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
5 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');

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-date
9 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 checks
2 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 savings
3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
4
5 uint balance0;
6 uint balance1;
7 { // scope for _token{0,1}, avoids stack too deep errors

Zmienne 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 tokens
5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens

Ten 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 errors
5 uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
6 uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
7 require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');

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 sald
  • skim, 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 reserves
2 function skim(address to) external lock {
3 address _token0 = token0; // gas savings
4 address _token1 = token1; // gas savings
5 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
6 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
7 }
8
9
10
11 // force reserves to match balances
12 function sync() external lock {
13 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
14 }
15}

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 sufficient

Duż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 direction
3 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 := chainid
5 }

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 contract
3 }

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 amountBMin

Są 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:

ParametrWartość
amountADesired1000
amountBDesired1000
amountAMin900
amountBMin800

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 yet
2 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.

Wykres

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 deadline

Ta 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 deadline
5 ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
6 (amountToken, amountETH) = _addLiquidity(
7 token,
8 WETH,
9 amountTokenDesired,
10 msg.value,
11 amountTokenMin,
12 amountETHMin
13 );
14 address pair = UniswapV2Library.pairFor(factory, token, WETH);
15 TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
16 IWETH(WETH).deposit{value: amountETH}();
17 assert(IWETH(WETH).transfer(pair, amountETH));

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 any
3 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 deadline
10 ) 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 pair
3 (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 deadline
8 ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
9 (amountToken, amountETH) = removeLiquidity(
10 token,
11 WETH,
12 liquidity,
13 amountTokenMin,
14 amountETHMin,
15 address(this),
16 deadline
17 );
18 TransferHelper.safeTransfer(token, to, amountToken);
19 IWETH(WETH).withdraw(amountETH);
20 TransferHelper.safeTransferETH(to, amountETH);
21 }

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 s
10 ) external virtual override returns (uint amountA, uint amountB) {
11 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
12 uint value = approveMax ? uint(-1) : liquidity;
13 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
14 (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
15 }
16
17
18 function removeLiquidityETHWithPermit(
19 address token,
20 uint liquidity,
21 uint amountTokenMin,
22 uint amountETHMin,
23 address to,
24 uint deadline,
25 bool approveMax, uint8 v, bytes32 r, bytes32 s
26 ) external virtual override returns (uint amountToken, uint amountETH) {
27 address pair = UniswapV2Library.pairFor(factory, token, WETH);
28 uint value = approveMax ? uint(-1) : liquidity;
29 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
30 (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);
31 }

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 deadline
10 ) public virtual override ensure(deadline) returns (uint amountETH) {
11 (, amountETH) = removeLiquidity(
12 token,
13 WETH,
14 liquidity,
15 amountTokenMin,
16 amountETHMin,
17 address(this),
18 deadline
19 );
20 TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
21 IWETH(WETH).withdraw(amountETH);
22 TransferHelper.safeTransferETH(to, amountETH);
23 }
24

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 s
11 ) external virtual override returns (uint amountETH) {
12 address pair = UniswapV2Library.pairFor(factory, token, WETH);
13 uint value = approveMax ? uint(-1) : liquidity;
14 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
15 amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(
16 token, liquidity, amountTokenMin, amountETHMin, to, deadline
17 );
18 }

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 pary
3 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.

  1. Sytuacja początkowa
  2. Handlarz sprzedaje 24,695 tokenów A i otrzymuje 25,305 tokenów B.
  3. Handlarz sprzedaje 24,695 tokenów B za 25,305 tokenów C, zatrzymując około 0,61 tokenów B jako zysk.
  4. 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).
KrokWymiana A-BWymiana B-CWymiana A-C
1A:1000 B:1050 A/B=1,05B:1000 C:1050 B/C=1,05A:1050 C:1000 C/A=1,05
2A:1024,695 B:1024,695 A/B=1B:1000 C:1050 B/C=1,05A:1050 C:1000 C/A=1,05
3A:1024,695 B:1024,695 A/B=1B:1024,695 C:1024,695 B/C=1A:1050 C:1000 C/A=1,05
4A:1024,695 B:1024,695 A/B=1B:1024,695 C:1024,695 B/C=1A:1024,695 C:1024,695 C/A=1
1 (address input, address output) = (path[i], path[i + 1]);
2 (address token0,) = UniswapV2Library.sortTokens(input, output);
3 uint amountOut = amounts[i + 1];

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 deadline
3 ) 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 deadline
7 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
8 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
9 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
10 TransferHelper.safeTransferFrom(
11 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
12 );
13 _swap(amounts, path, to);
14 }

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 external
3 virtual
4 override
5 payable
6 ensure(deadline)
7 returns (uint[] memory amounts)
8 {
9 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
10 amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
11 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
12 IWETH(WETH).deposit{value: amounts[0]}();
13 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
14 _swap(amounts, path, to);
15 }
16
17
18 function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
19 external
20 virtual
21 override
22 ensure(deadline)
23 returns (uint[] memory amounts)
24 {
25 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
26 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
27 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
28 TransferHelper.safeTransferFrom(
29 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
30 );
31 _swap(amounts, path, address(this));
32 IWETH(WETH).withdraw(amounts[amounts.length - 1]);
33 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
34 }
35
36
37
38 function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
39 external
40 virtual
41 override
42 ensure(deadline)
43 returns (uint[] memory amounts)
44 {
45 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
46 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
47 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
48 TransferHelper.safeTransferFrom(
49 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
50 );
51 _swap(amounts, path, address(this));
52 IWETH(WETH).withdraw(amounts[amounts.length - 1]);
53 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
54 }
55
56
57 function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
58 external
59 virtual
60 override
61 payable
62 ensure(deadline)
63 returns (uint[] memory amounts)
64 {
65 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
66 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
67 require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
68 IWETH(WETH).deposit{value: amounts[0]}();
69 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
70 _swap(amounts, path, to);
71 // zwróć resztę eth, jeśli istnieje
72 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 pary
3 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 stosu
8 (uint reserve0, uint reserve1,) = pair.getReserves();
9 (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
10 amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
11 amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);

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 deadline
15 ) external virtual override ensure(deadline) {
16 TransferHelper.safeTransferFrom(
17 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
18 );
19 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
20 _swapSupportingFeeOnTransferTokens(path, to);
21 require(
22 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
23 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
24 );
25 }
26
27
28 function swapExactETHForTokensSupportingFeeOnTransferTokens(
29 uint amountOutMin,
30 address[] calldata path,
31 address to,
32 uint deadline
33 )
34 external
35 virtual
36 override
37 payable
38 ensure(deadline)
39 {
40 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
41 uint amountIn = msg.value;
42 IWETH(WETH).deposit{value: amountIn}();
43 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));
44 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
45 _swapSupportingFeeOnTransferTokens(path, to);
46 require(
47 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
48 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
49 );
50 }
51
52
53 function swapExactTokensForETHSupportingFeeOnTransferTokens(
54 uint amountIn,
55 uint amountOutMin,
56 address[] calldata path,
57 address to,
58 uint deadline
59 )
60 external
61 virtual
62 override
63 ensure(deadline)
64 {
65 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
66 TransferHelper.safeTransferFrom(
67 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
68 );
69 _swapSupportingFeeOnTransferTokens(path, address(this));
70 uint amountOut = IERC20(WETH).balanceOf(address(this));
71 require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
72 IWETH(WETH).withdraw(amountOut);
73 TransferHelper.safeTransferETH(to, amountOut);
74 }

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 public
8 pure
9 virtual
10 override
11 returns (uint amountOut)
12 {
13 return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
14 }
15
16 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)
17 public
18 pure
19 virtual
20 override
21 returns (uint amountIn)
22 {
23 return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);
24 }
25
26 function getAmountsOut(uint amountIn, address[] memory path)
27 public
28 view
29 virtual
30 override
31 returns (uint[] memory amounts)
32 {
33 return UniswapV2Library.getAmountsOut(factory, amountIn, path);
34 }
35
36 function getAmountsIn(uint amountOut, address[] memory path)
37 public
38 view
39 virtual
40 override
41 returns (uint[] memory amounts)
42 {
43 return UniswapV2Library.getAmountsIn(factory, amountOut, path);
44 }
45}

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 matematycznych
4
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**112
7
8library UQ112x112 {
9 uint224 constant Q112 = 2**112;

Q112 to kodowanie jedynki.

1 // koduje uint112 jako UQ112x112
2 function encode(uint112 y) internal pure returns (uint224 z) {
3 z = uint224(y) * Q112; // nigdy się nie przepełnia
4 }

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 UQ112x112
2 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ści
11 function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
12 require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
13 (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
14 require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
15 }

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ętrznych
2 function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
3 (address token0, address token1) = sortTokens(tokenA, tokenB);
4 pair = address(uint(keccak256(abi.encodePacked(
5 hex'ff',
6 factory,
7 keccak256(abi.encodePacked(token0, token1)),
8 hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // hash kodu inicjującego
9 ))));
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 pary
2 function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
3 (address token0,) = sortTokens(tokenA, tokenB);
4 (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
5 (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
6 }

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 zasobu
2 function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
3 require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
4 require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
5 amountB = amountA.mul(reserveB) / reserveA;
6 }

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 zasobu
2 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 zasobu
2 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
3 require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
4 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
5 uint numerator = reserveIn.mul(amountOut).mul(1000);
6 uint denominator = reserveOut.sub(amountOut).mul(997);
7 amountIn = (numerator / denominator).add(1);
8 }

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 par
3 function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
4 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
5 amounts = new uint[](path.length);
6 amounts[0] = amountIn;
7 for (uint i; i < path.length - 1; i++) {
8 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
9 amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
10 }
11 }
12
13 // wykonuje łańcuchowe obliczenia getAmountIn na dowolnej liczbie par
14 function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
15 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
16 amounts = new uint[](path.length);
17 amounts[amounts.length - 1] = amountOut;
18 for (uint i = path.length - 1; i > 0; i--) {
19 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
20 amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
21 }
22 }
23}

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-later
2
3pragma solidity >=0.6.0;
4
5// metody pomocnicze do interakcji z tokenami ERC20 i wysyłania ETH, które nie zawsze zwracają true/false
6library TransferHelper {
7 function safeApprove(
8 address token,
9 address to,
10 uint256 value
11 ) internal {
12 // bytes4(keccak256(bytes('approve(address,uint256)')));
13 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));
14

Możemy wywołać inny kontrakt na jeden z dwóch sposobów:

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 value
7 ) internal {
8 // bytes4(keccak256(bytes('transfer(address,uint256)')));
9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
10 require(
11 success && (data.length == 0 || abi.decode(data, (bool))),
12 'TransferHelper::safeTransfer: transfer failed'
13 );
14 }

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 value
7 ) internal {
8 // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
10 require(
11 success && (data.length == 0 || abi.decode(data, (bool))),
12 'TransferHelper::transferFrom: transferFrom failed'
13 );
14 }

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

Czy ten samouczek był pomocny?