Přejít na hlavní obsah

Průvodce kontraktem Uniswap v2

Solidity
dapps
Středně pokročilý
Ori Pomerantz
1. května 2021
54 minut čtení

Úvod

Uniswap v2 (opens in a new tab) dokáže vytvořit směnný trh mezi libovolnými dvěma tokeny ERC-20. V tomto článku si projdeme zdrojový kód kontraktů, které tento protokol implementují, a podíváme se, proč jsou napsány právě takto.

Co dělá Uniswap?

V zásadě existují dva typy uživatelů: poskytovatelé likvidity a obchodníci.

Poskytovatelé likvidity dodávají do fondu dva tokeny, které lze směňovat (budeme je nazývat Token0 a Token1). Na oplátku obdrží třetí token, který představuje částečné vlastnictví fondu a nazývá se token likvidity.

Obchodníci posílají do fondu jeden typ tokenu a získávají druhý (například pošlou Token0 a obdrží Token1) z fondu, který poskytli poskytovatelé likvidity. Směnný kurz je určen relativním množstvím tokenů Token0 a Token1, které má fond k dispozici. Kromě toho si fond bere malé procento jako odměnu pro fond likvidity.

Když chtějí poskytovatelé likvidity svá aktiva zpět, mohou spálit tokeny fondu a získat zpět své tokeny, včetně svého podílu na odměnách.

Klikněte sem pro podrobnější popis (opens in a new tab).

Proč v2? Proč ne v3?

Uniswap v3 (opens in a new tab) je aktualizace, která je mnohem složitější než v2. Je snazší se nejprve naučit v2 a poté přejít na v3.

Kontrakty jádra vs. periferní kontrakty

Uniswap v2 je rozdělen na dvě komponenty, jádro a periferii. Toto rozdělení umožňuje, aby kontrakty jádra, které drží aktiva a proto musí být bezpečné, byly jednodušší a snáze auditovatelné. Veškerou dodatečnou funkcionalitu požadovanou obchodníky pak mohou poskytovat periferní kontrakty.

Datové a řídicí toky

Toto je tok dat a řízení, ke kterému dochází, když provádíte tři hlavní akce na Uniswapu:

  1. Swap mezi různými tokeny
  2. Přidání likvidity na trh a získání odměny ve formě párových ERC-20 tokenů likvidity
  3. Spálení ERC-20 tokenů likvidity a získání zpět ERC-20 tokenů, které párová burza umožňuje obchodníkům směňovat

Swap

Toto je nejběžnější tok, který používají obchodníci:

Volající

  1. Poskytnout perifernímu účtu povolený limit ve výši částky, která má být swapována.
  2. Zavolat jednu z mnoha swapovacích funkcí periferního kontraktu (kterou přesně, závisí na tom, zda je zapojeno ETH či nikoli, zda obchodník specifikuje množství tokenů k vložení nebo množství tokenů k získání zpět atd.). Každá swapovací funkce přijímá path, pole burz, kterými se má projít.

V periferním kontraktu (UniswapV2Router02.sol)

  1. Identifikovat částky, které je třeba zobchodovat na každé burze podél cesty.
  2. Iteruje přes cestu. Pro každou burzu po cestě odešle vstupní token a poté zavolá funkci swap dané burzy. Ve většině případů je cílovou adresou pro tokeny další párová burza na cestě. V konečné burze je to adresa poskytnutá obchodníkem.

V hlavním kontraktu (UniswapV2Pair.sol)

  1. Ověřit, že hlavní kontrakt není podváděn a může si po swapu udržet dostatečnou likviditu.
  2. Zjistit, kolik tokenů navíc máme kromě známých rezerv. Tato částka představuje počet vstupních tokenů, které jsme obdrželi ke směně.
  3. Odeslat výstupní tokeny do cíle.
  4. Zavolat _update pro aktualizaci částek rezerv

Zpět v periferním kontraktu (UniswapV2Router02.sol)

  1. Provést jakýkoli nezbytný úklid (například spálit WETH tokeny pro získání zpět ETH k odeslání obchodníkovi)

Přidání likvidity

Volající

  1. Poskytnout perifernímu účtu povolený limit ve výši částek, které mají být přidány do fondu likvidity.
  2. Zavolat jednu z funkcí addLiquidity periferního kontraktu.

V periferním kontraktu (UniswapV2Router02.sol)

  1. V případě potřeby vytvořit novou párovou burzu
  2. Pokud existuje stávající párová burza, vypočítat množství tokenů k přidání. To by mělo mít stejnou hodnotu pro oba tokeny, tedy stejný poměr nových tokenů ke stávajícím tokenům.
  3. Zkontrolovat, zda jsou částky přijatelné (volající mohou specifikovat minimální částku, pod kterou by raději likviditu nepřidávali)
  4. Zavolat hlavní kontrakt.

V hlavním kontraktu (UniswapV2Pair.sol)

  1. Vyrazit tokeny likvidity a odeslat je volajícímu
  2. Zavolat _update pro aktualizaci částek rezerv

Odebrání likvidity

Volající

  1. Poskytnout perifernímu účtu povolený limit tokenů likvidity, které mají být spáleny výměnou za podkladové tokeny.
  2. Zavolat jednu z funkcí removeLiquidity periferního kontraktu.

V periferním kontraktu (UniswapV2Router02.sol)

  1. Odeslat tokeny likvidity na párovou burzu

V hlavním kontraktu (UniswapV2Pair.sol)

  1. Odeslat na cílovou adresu podkladové tokeny v poměru ke spáleným tokenům. Například pokud je ve fondu 1000 tokenů A, 500 tokenů B a 90 tokenů likvidity a my obdržíme 9 tokenů ke spálení, pálíme 10 % tokenů likvidity, takže uživateli pošleme zpět 100 tokenů A a 50 tokenů B.
  2. Spálit tokeny likvidity
  3. Zavolat _update pro aktualizaci částek rezerv

Hlavní kontrakty

Toto jsou bezpečné kontrakty, které drží likviditu.

UniswapV2Pair.sol

Tento kontrakt (opens in a new tab) implementuje samotný fond, který směňuje tokeny. Je to základní funkcionalita Uniswapu.

Toto jsou všechna rozhraní, o kterých kontrakt potřebuje vědět, ať už proto, že je sám implementuje (IUniswapV2Pair a UniswapV2ERC20), nebo proto, že volá kontrakty, které je implementují.

contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {

Tento kontrakt dědí z UniswapV2ERC20, což poskytuje funkce ERC-20 pro tokeny likvidity.

    using SafeMath  for uint;

Knihovna SafeMath (opens in a new tab) se používá k zabránění přetečení a podtečení (overflow a underflow). To je důležité, protože jinak bychom se mohli dostat do situace, kdy by hodnota měla být -1, ale místo toho je 2^256-1.

    using UQ112x112 for uint224;

Mnoho výpočtů v kontraktu fondu vyžaduje zlomky. Zlomky však EVM nepodporuje. Řešení, které Uniswap našel, je použít 224bitové hodnoty, kde 112 bitů je pro celočíselnou část a 112 bitů pro zlomkovou část. Takže 1.0 je reprezentováno jako 2^112, 1.5 je reprezentováno jako 2^112 + 2^111 atd.

Více podrobností o této knihovně je k dispozici dále v dokumentu.

Proměnné

    uint public constant MINIMUM_LIQUIDITY = 10**3;

Aby se předešlo případům dělení nulou, existuje minimální počet tokenů likvidity, které vždy existují (ale jsou vlastněny účtem nula). Toto číslo je MINIMUM_LIQUIDITY, tedy tisíc.

    bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));

Toto je selektor ABI pro funkci převodu ERC-20. Používá se k převodu tokenů ERC-20 na dvou účtech tokenů.

    address public factory;

Toto je tovární kontrakt (factory), který vytvořil tento fond. Každý fond je směnárnou mezi dvěma tokeny ERC-20, továrna je centrálním bodem, který všechny tyto fondy spojuje.

    address public token0;
    address public token1;

Zde jsou adresy kontraktů pro dva typy tokenů ERC-20, které lze v tomto fondu směňovat.

    uint112 private reserve0;           // využívá jeden úložný slot, přístupný přes getReserves
    uint112 private reserve1;           // využívá jeden úložný slot, přístupný přes getReserves

Rezervy, které má fond pro každý typ tokenu. Předpokládáme, že oba představují stejnou hodnotu, a proto má každý token0 hodnotu reserve1/reserve0 tokenů1.

    uint32  private blockTimestampLast; // využívá jeden úložný slot, přístupný přes getReserves

Časové razítko (timestamp) posledního bloku, ve kterém došlo ke směně, používané ke sledování směnných kurzů v čase.

Jedním z největších výdajů za gas u kontraktů Etherea je úložiště (storage), které přetrvává z jednoho volání kontraktu do dalšího. Každá buňka úložiště je dlouhá 256 bitů. Takže tři proměnné, reserve0, reserve1 a blockTimestampLast, jsou alokovány takovým způsobem, že jedna hodnota úložiště může obsahovat všechny tři (112+112+32=256).

    uint public price0CumulativeLast;
    uint public price1CumulativeLast;

Tyto proměnné uchovávají kumulativní náklady pro každý token (každý vyjádřený v tom druhém). Lze je použít k výpočtu průměrného směnného kurzu za určité časové období.

    uint public kLast; // reserve0 * reserve1, bezprostředně po nejnovější události likvidity

Způsob, jakým směnárna párů rozhoduje o směnném kurzu mezi token0 a token1, spočívá v udržování konstantního násobku obou rezerv během obchodů. kLast je tato hodnota. Mění se, když poskytovatel likvidity vloží nebo vybere tokeny, a mírně se zvyšuje kvůli tržnímu poplatku 0,3 %.

Zde je jednoduchý příklad. Všimněte si, že pro zjednodušení má tabulka pouze tři číslice za desetinnou čárkou a ignorujeme poplatek za obchodování 0,3 %, takže čísla nejsou přesná.

Událostreserve0reserve1reserve0 * reserve1Průměrný směnný kurz (token1 / token0)
Počáteční nastavení1,000.0001,000.0001,000,000
Obchodník A swapne 50 token0 za 47,619 token11,050.000952.3811,000,0000.952
Obchodník B swapne 10 token0 za 8,984 token11,060.000943.3961,000,0000.898
Obchodník C swapne 40 token0 za 34,305 token11,100.000909.0901,000,0000.858
Obchodník D swapne 100 token1 za 109,01 token0990.9901,009.0901,000,0000.917
Obchodník E swapne 10 token0 za 10,079 token11,000.990999.0101,000,0001.008

Jak obchodníci poskytují více token0, relativní hodnota token1 se zvyšuje a naopak, na základě nabídky a poptávky.

Zámek

    uint private unlocked = 1;

Existuje třída bezpečnostních zranitelností, které jsou založeny na zneužití reentrance (opens in a new tab). Uniswap potřebuje převádět libovolné tokeny ERC-20, což znamená volání kontraktů ERC-20, které se mohou pokusit zneužít trh Uniswap, který je volá. Tím, že máme proměnnou unlocked jako součást kontraktu, můžeme zabránit volání funkcí, zatímco běží (v rámci stejné transakce).

    modifier lock() {

Tato funkce je modifikátor (opens in a new tab), funkce, která obaluje normální funkci, aby nějakým způsobem změnila její chování.

        require(unlocked == 1, 'UniswapV2: LOCKED');
        unlocked = 0;

Pokud se unlocked rovná jedné, nastavte ji na nulu. Pokud je již nula, zvrátit volání, nechat ho selhat.

        _;

V modifikátoru je _; původní volání funkce (se všemi parametry). Zde to znamená, že k volání funkce dojde pouze tehdy, pokud byla hodnota unlocked jedna, když byla volána, a zatímco běží, hodnota unlocked je nula.

        unlocked = 1;
    }

Poté, co se hlavní funkce vrátí, uvolněte zámek.

Různé funkce

    function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
        _reserve0 = reserve0;
        _reserve1 = reserve1;
        _blockTimestampLast = blockTimestampLast;
    }

Tato funkce poskytuje volajícím aktuální stav směnárny. Všimněte si, že funkce v Solidity mohou vracet více hodnot (opens in a new tab).

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

Tato interní funkce převede určité množství tokenů ERC-20 ze směnárny někomu jinému. SELECTOR specifikuje, že funkce, kterou voláme, je transfer(address,uint) (viz definice výše).

Abychom nemuseli importovat rozhraní pro funkci tokenu, vytvoříme volání „ručně“ pomocí jedné z funkcí ABI (opens in a new tab).

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

Existují dva způsoby, jak může volání převodu ERC-20 nahlásit selhání:

  1. Zvrácení (revert). Pokud se volání externího kontraktu zvrátí, pak je návratová hodnota typu boolean false
  2. Normální ukončení, ale nahlášení selhání. V takovém případě má buffer návratové hodnoty nenulovou délku a při dekódování jako hodnota typu boolean je to false

Pokud nastane kterákoli z těchto podmínek, zvrátit.

Události

    event Mint(address indexed sender, uint amount0, uint amount1);
    event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);

Tyto dvě události jsou emitovány, když poskytovatel likvidity buď vloží likviditu (Mint), nebo ji vybere (Burn). V obou případech jsou součástí události částky token0 a token1, které jsou vloženy nebo vybrány, a také identita účtu, který nás volal (sender). V případě výběru událost zahrnuje také cíl, který tokeny obdržel (to), což nemusí být stejné jako odesílatel.

    event Swap(
        address indexed sender,
        uint amount0In,
        uint amount1In,
        uint amount0Out,
        uint amount1Out,
        address indexed to
    );

Tato událost je emitována, když obchodník swapne jeden token za druhý. Opět platí, že odesílatel a cíl nemusí být stejní. Každý token může být buď odeslán na směnárnu, nebo z ní přijat.

    event Sync(uint112 reserve0, uint112 reserve1);

Nakonec je Sync emitováno pokaždé, když jsou tokeny přidány nebo vybrány, bez ohledu na důvod, aby poskytlo nejnovější informace o rezervách (a tím i směnný kurz).

Funkce nastavení

Tyto funkce by měly být volány jednou při nastavení nové směnárny párů.

    constructor() public {
        factory = msg.sender;
    }

Konstruktor zajišťuje, že budeme sledovat adresu továrny, která pár vytvořila. Tato informace je vyžadována pro initialize a pro tovární poplatek (pokud existuje).

    // voláno jednou factory při nasazení
    function initialize(address _token0, address _token1) external {
        require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // dostatečná kontrola
        token0 = _token0;
        token1 = _token1;
    }

Tato funkce umožňuje továrně (a pouze továrně) specifikovat dva tokeny ERC-20, které bude tento pár směňovat.

Interní funkce aktualizace

_update
    // aktualizuje rezervy a při prvním volání v bloku i cenové akumulátory
    function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {

Tato funkce je volána pokaždé, když jsou tokeny vloženy nebo vybrány.

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

Pokud je balance0 nebo balance1 (uint256) vyšší než uint112(-1) (=2^112-1) (takže přeteče a při převodu na uint112 se vrátí na 0), odmítněte pokračovat v _update, abyste zabránili přetečení. U normálního tokenu, který lze rozdělit na 10^18 jednotek, to znamená, že každá směnárna je omezena na přibližně 5,1*10^15 od každého tokenu. Zatím to nebyl problém.

        uint32 blockTimestamp = uint32(block.timestamp % 2**32);
        uint32 timeElapsed = blockTimestamp - blockTimestampLast; // přetečení je žádoucí
        if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {

Pokud uplynulý čas není nula, znamená to, že jsme první transakcí směny v tomto bloku. V takovém případě musíme aktualizovat akumulátory nákladů.

            // * nikdy nepřeteče a u + je přetečení žádoucí
            price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
            price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
        }

Každý akumulátor nákladů je aktualizován nejnovějšími náklady (rezerva druhého tokenu/rezerva tohoto tokenu) vynásobenými uplynulým časem v sekundách. Chcete-li získat průměrnou cenu, přečtete kumulativní cenu ve dvou časových bodech a vydělíte ji časovým rozdílem mezi nimi. Předpokládejme například tuto posloupnost událostí:

Událostreserve0reserve1časové razítkoMezní směnný kurz (reserve1 / reserve0)price0CumulativeLast
Počáteční nastavení1,000.0001,000.0005,0001.0000
Obchodník A vloží 50 token0 a dostane zpět 47,619 token11,050.000952.3815,0200.90720
Obchodník B vloží 10 token0 a dostane zpět 8,984 token11,060.000943.3965,0300.89020+10*0.907 = 29.07
Obchodník C vloží 40 token0 a dostane zpět 34,305 token11,100.000909.0905,1000.82629.07+70*0.890 = 91.37
Obchodník D vloží 100 token1 a dostane zpět 109,01 token0990.9901,009.0905,1101.01891.37+10*0.826 = 99.63
Obchodník E vloží 10 token0 a dostane zpět 10,079 token11,000.990999.0105,1500.99899.63+40*1.1018 = 143.702

Řekněme, že chceme vypočítat průměrnou cenu Token0 mezi časovými razítky 5 030 a 5 150. Rozdíl v hodnotě price0Cumulative je 143,702-29,07=114,632. Toto je průměr za dvě minuty (120 sekund). Takže průměrná cena je 114,632/120 = 0,955.

Tento výpočet ceny je důvodem, proč potřebujeme znát staré velikosti rezerv.

        reserve0 = uint112(balance0);
        reserve1 = uint112(balance1);
        blockTimestampLast = blockTimestamp;
        emit Sync(reserve0, reserve1);
    }

Nakonec aktualizujte globální proměnné a emitujte událost Sync.

_mintFee
    // pokud je poplatek zapnutý, razit likviditu odpovídající 1/6 růstu sqrt(k)
    function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {

V Uniswap 2.0 platí obchodníci za používání trhu poplatek 0,30 %. Většina tohoto poplatku (0,25 % z obchodu) jde vždy poskytovatelům likvidity. Zbývajících 0,05 % může jít buď poskytovatelům likvidity, nebo na adresu specifikovanou továrnou jako poplatek za protokol, který platí Uniswapu za jejich úsilí při vývoji.

Aby se snížil počet výpočtů (a tím i náklady na gas), tento poplatek se počítá pouze při přidání nebo odebrání likvidity z fondu, nikoli při každé transakci.

        address feeTo = IUniswapV2Factory(factory).feeTo();
        feeOn = feeTo != address(0);

Přečtěte si cíl poplatku továrny. Pokud je nula, pak neexistuje žádný poplatek za protokol a není třeba tento poplatek počítat.

        uint _kLast = kLast; // úspora gasu

Stavová proměnná kLast je umístěna v úložišti, takže bude mít hodnotu mezi různými voláními kontraktu. Přístup k úložišti je mnohem dražší než přístup k volatilní paměti, která se uvolní po skončení volání funkce kontraktu, takže používáme interní proměnnou, abychom ušetřili gas.

        if (feeOn) {
            if (_kLast != 0) {

Poskytovatelé likvidity získají svůj podíl jednoduše zhodnocením svých tokenů likvidity. Poplatek za protokol však vyžaduje, aby byly vyraženy nové tokeny likvidity a poskytnuty na adresu feeTo.

                uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
                uint rootKLast = Math.sqrt(_kLast);
                if (rootK > rootKLast) {

Pokud existuje nová likvidita, ze které se má vybrat poplatek za protokol. Funkci druhé odmocniny můžete vidět dále v tomto článku

                    uint numerator = totalSupply.mul(rootK.sub(rootKLast));
                    uint denominator = rootK.mul(5).add(rootKLast);
                    uint liquidity = numerator / denominator;

Tento složitý výpočet poplatků je vysvětlen v bílé knize (opens in a new tab) na straně 5. Víme, že mezi dobou, kdy bylo vypočítáno kLast, a současností nebyla přidána ani odebrána žádná likvidita (protože tento výpočet spouštíme pokaždé, když je likvidita přidána nebo odebrána, než se skutečně změní), takže jakákoli změna v reserve0 * reserve1 musí pocházet z transakčních poplatků (bez nich bychom udržovali reserve0 * reserve1 konstantní).

                    if (liquidity > 0) _mint(feeTo, liquidity);
                }
            }

Použijte funkci UniswapV2ERC20._mint ke skutečnému vytvoření dalších tokenů likvidity a jejich přiřazení k feeTo.

        } else if (_kLast != 0) {
            kLast = 0;
        }
    }

Pokud není nastaven žádný poplatek, nastavte kLast na nulu (pokud to tak již není). Když byl tento kontrakt napsán, existovala funkce vrácení gasu (opens in a new tab), která povzbuzovala kontrakty ke snížení celkové velikosti stavu Etherea vynulováním úložiště, které nepotřebovaly. Tento kód získá tuto náhradu, kdykoli je to možné.

Externě přístupné funkce

Všimněte si, že ačkoli jakákoli transakce nebo kontrakt může tyto funkce volat, jsou navrženy tak, aby byly volány z periferního kontraktu. Pokud je zavoláte přímo, nebudete moci směnárnu párů podvést, ale můžete ztratit hodnotu kvůli chybě.

mint
    // tato nízkoúrovňová funkce by měla být volána z kontraktu, který provádí důležité bezpečnostní kontroly
    function mint(address to) external lock returns (uint liquidity) {

Tato funkce je volána, když poskytovatel likvidity přidá likviditu do fondu. Jako odměnu razí další tokeny likvidity. Měla by být volána z periferního kontraktu, který ji zavolá po přidání likvidity ve stejné transakci (takže nikdo jiný by nemohl odeslat transakci, která by si nárokovala novou likviditu před legitimním vlastníkem).

        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // úspora gasu

Toto je způsob, jak číst výsledky funkce v Solidity, která vrací více hodnot. Poslední vrácenou hodnotu, časové razítko bloku, zahodíme, protože ji nepotřebujeme.

        uint balance0 = IERC20(token0).balanceOf(address(this));
        uint balance1 = IERC20(token1).balanceOf(address(this));
        uint amount0 = balance0.sub(_reserve0);
        uint amount1 = balance1.sub(_reserve1);

Získejte aktuální zůstatky a podívejte se, kolik bylo přidáno od každého typu tokenu.

        bool feeOn = _mintFee(_reserve0, _reserve1);

Vypočítejte poplatky za protokol, které se mají vybrat, pokud nějaké jsou, a podle toho vyrazte tokeny likvidity. Protože parametry pro _mintFee jsou staré hodnoty rezerv, poplatek se počítá přesně pouze na základě změn fondu způsobených poplatky.

        uint _totalSupply = totalSupply; // úspora gasu, musí být definováno zde, protože totalSupply se může aktualizovat v _mintFee
        if (_totalSupply == 0) {
            liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
           _mint(address(0), MINIMUM_LIQUIDITY); // trvale uzamknout prvních MINIMUM_LIQUIDITY tokenů

Pokud se jedná o první vklad, vytvořte MINIMUM_LIQUIDITY tokenů a pošlete je na adresu nula, abyste je uzamkli. Nikdy je nelze vyplatit, což znamená, že fond nebude nikdy zcela vyprázdněn (to nás na některých místech zachrání před dělením nulou). Hodnota MINIMUM_LIQUIDITY je tisíc, což vzhledem k tomu, že většina ERC-20 je rozdělena na jednotky 10^-18 tokenu, stejně jako je ETH rozděleno na Wei, je 10^-15 hodnoty jednoho tokenu. Není to vysoký náklad.

V době prvního vkladu neznáme relativní hodnotu obou tokenů, takže částky jednoduše vynásobíme a odmocníme za předpokladu, že nám vklad poskytuje stejnou hodnotu v obou tokenech.

Můžeme tomu věřit, protože je v zájmu vkladatele poskytnout stejnou hodnotu, aby se vyhnul ztrátě hodnoty kvůli arbitráži. Řekněme, že hodnota obou tokenů je stejná, ale náš vkladatel vložil čtyřikrát více Token1 než Token0. Obchodník může využít skutečnosti, že si směnárna párů myslí, že Token0 je cennější, k tomu, aby z ní získal hodnotu.

Událostreserve0reserve1reserve0 * reserve1Hodnota fondu (reserve0 + reserve1)
Počáteční nastavení83225640
Obchodník vloží 8 tokenů Token0, dostane zpět 16 Token1161625632

Jak vidíte, obchodník vydělal dalších 8 tokenů, které pocházejí ze snížení hodnoty fondu, což poškozuje vkladatele, který jej vlastní.

        } else {
            liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);

Při každém dalším vkladu již známe směnný kurz mezi oběma aktivy a očekáváme, že poskytovatelé likvidity poskytnou stejnou hodnotu v obou. Pokud tak neučiní, dáme jim jako trest tokeny likvidity na základě nižší hodnoty, kterou poskytli.

Ať už se jedná o počáteční vklad nebo o následný, počet tokenů likvidity, které poskytujeme, se rovná druhé odmocnině změny v reserve0*reserve1 a hodnota tokenu likvidity se nemění (pokud nedostaneme vklad, který nemá stejné hodnoty obou typů, v takovém případě se „pokuta“ rozdělí). Zde je další příklad se dvěma tokeny, které mají stejnou hodnotu, se třemi dobrými vklady a jedním špatným (vklad pouze jednoho typu tokenu, takže neprodukuje žádné tokeny likvidity).

Událostreserve0reserve1reserve0 * reserve1Hodnota fondu (reserve0 + reserve1)Tokeny likvidity vyražené pro tento vkladCelkem tokenů likvidityhodnota každého tokenu likvidity
Počáteční nastavení8.0008.0006416.000882.000
Vklad čtyř od každého typu12.00012.00014424.0004122.000
Vklad dvou od každého typu14.00014.00019628.0002142.000
Vklad nestejné hodnoty18.00014.00025232.000014~2.286
Po arbitráži~15.874~15.874252~31.748014~2.267
        }
        require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
        _mint(to, liquidity);

Použijte funkci UniswapV2ERC20._mint ke skutečnému vytvoření dalších tokenů likvidity a dejte je na správný účet.


        _update(balance0, balance1, _reserve0, _reserve1);
        if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 a reserve1 jsou aktuální
        emit Mint(msg.sender, amount0, amount1);
    }

Aktualizujte stavové proměnné (reserve0, reserve1 a v případě potřeby kLast) a emitujte příslušnou událost.

burn
    // tato nízkoúrovňová funkce by měla být volána z kontraktu, který provádí důležité bezpečnostní kontroly
    function burn(address to) external lock returns (uint amount0, uint amount1) {

Tato funkce je volána, když je likvidita vybrána a příslušné tokeny likvidity je třeba spálit. Měla by být také volána z periferního účtu.

        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // úspora gasu
        address _token0 = token0;                                // úspora gasu
        address _token1 = token1;                                // úspora gasu
        uint balance0 = IERC20(_token0).balanceOf(address(this));
        uint balance1 = IERC20(_token1).balanceOf(address(this));
        uint liquidity = balanceOf[address(this)];

Periferní kontrakt převedl likviditu, která má být spálena, do tohoto kontraktu před voláním. Tímto způsobem víme, kolik likvidity spálit, a můžeme se ujistit, že bude spálena.

        bool feeOn = _mintFee(_reserve0, _reserve1);
        uint _totalSupply = totalSupply; // úspora gasu, musí být definováno zde, protože totalSupply se může aktualizovat v _mintFee
        amount0 = liquidity.mul(balance0) / _totalSupply; // použití zůstatků zajišťuje poměrné rozdělení
        amount1 = liquidity.mul(balance1) / _totalSupply; // použití zůstatků zajišťuje poměrné rozdělení
        require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');

Poskytovatel likvidity obdrží stejnou hodnotu obou tokenů. Tímto způsobem neměníme směnný kurz.

Zbytek funkce burn je zrcadlovým obrazem funkce mint výše.

swap
    // tato nízkoúrovňová funkce by měla být volána z kontraktu, který provádí důležité bezpečnostní kontroly
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {

Tato funkce by měla být také volána z periferního kontraktu.

        require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // úspora gasu
        require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

        uint balance0;
        uint balance1;
        { // rozsah pro _token{0,1}, zabraňuje chybám stack too deep

Lokální proměnné mohou být uloženy buď v paměti, nebo, pokud jich není příliš mnoho, přímo na zásobníku (stack). Pokud můžeme omezit počet tak, abychom použili zásobník, spotřebujeme méně gasu. Další podrobnosti naleznete v yellow paper, formálních specifikacích Etherea (opens in a new tab), str. 26, rovnice 298.

            address _token0 = token0;
            address _token1 = token1;
            require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
            if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimisticky převést tokeny
            if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimisticky převést tokeny

Tento převod je optimistický, protože převádíme dříve, než si budeme jisti, že jsou splněny všechny podmínky. V Ethereu je to v pořádku, protože pokud podmínky nebudou splněny později ve volání, zvrátíme ho a jakékoli změny, které vytvořil.

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

Na požádání informujte příjemce o swapu.

            balance0 = IERC20(_token0).balanceOf(address(this));
            balance1 = IERC20(_token1).balanceOf(address(this));
        }

Získejte aktuální zůstatky. Periferní kontrakt nám pošle tokeny předtím, než nás zavolá pro swap. To usnadňuje kontraktu zkontrolovat, že není podváděn, což je kontrola, která se musí stát v hlavním kontraktu (protože můžeme být voláni jinými subjekty než naším periferním kontraktem).

        uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
        uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
        require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
        { // rozsah pro reserve{0,1}Adjusted, zabraňuje chybám stack too deep
            uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
            uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
            require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');

Toto je kontrola správnosti (sanity check), abychom se ujistili, že na swapu neproděláme. Neexistuje žádná okolnost, za které by swap měl snížit reserve0*reserve1. Zde také zajišťujeme, že je při swapu odeslán poplatek 0,3 %; před kontrolou správnosti hodnoty K vynásobíme oba zůstatky 1000 a odečteme částky vynásobené 3, to znamená, že 0,3 % (3/1000 = 0,003 = 0,3 %) je odečteno ze zůstatku před porovnáním jeho hodnoty K s hodnotou K aktuálních rezerv.

        }

        _update(balance0, balance1, _reserve0, _reserve1);
        emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
    }

Aktualizujte reserve0 a reserve1 a v případě potřeby akumulátory cen a časové razítko a emitujte událost.

Synchronizace nebo Skim

Je možné, že se skutečné zůstatky dostanou mimo synchronizaci s rezervami, o kterých si směnárna párů myslí, že je má. Neexistuje způsob, jak vybrat tokeny bez souhlasu kontraktu, ale vklady jsou jiná věc. Účet může převést tokeny na směnárnu, aniž by zavolal mint nebo swap.

V takovém případě existují dvě řešení:

  • sync, aktualizovat rezervy na aktuální zůstatky
  • skim, vybrat částku navíc. Všimněte si, že jakýkoli účet má povoleno volat skim, protože nevíme, kdo tokeny vložil. Tato informace je emitována v události, ale události nejsou z blockchainu přístupné.

UniswapV2Factory.sol

Tento kontrakt (opens in a new tab) vytváří směnárny párů.

pragma solidity =0.5.16;

import './interfaces/IUniswapV2Factory.sol';
import './UniswapV2Pair.sol';

contract UniswapV2Factory is IUniswapV2Factory {
    address public feeTo;
    address public feeToSetter;

Tyto stavové proměnné jsou nezbytné k implementaci poplatku za protokol (viz bílá kniha (opens in a new tab), str. 5). Adresa feeTo shromažďuje tokeny likvidity pro poplatek za protokol a feeToSetter je adresa, která má povoleno změnit feeTo na jinou adresu.

    mapping(address => mapping(address => address)) public getPair;
    address[] public allPairs;

Tyto proměnné sledují páry, směnárny mezi dvěma typy tokenů.

První z nich, getPair, je mapování, které identifikuje kontrakt směnárny párů na základě dvou tokenů ERC-20, které směňuje. Tokeny ERC-20 jsou identifikovány adresami kontraktů, které je implementují, takže klíče a hodnota jsou všechny adresy. Chcete-li získat adresu směnárny párů, která vám umožní převést z tokenA na tokenB, použijete getPair[<tokenA address>][<tokenB address>] (nebo naopak).

Druhá proměnná, allPairs, je pole, které obsahuje všechny adresy směnáren párů vytvořených touto továrnou. V Ethereu nemůžete iterovat přes obsah mapování nebo získat seznam všech klíčů, takže tato proměnná je jediným způsobem, jak zjistit, které směnárny tato továrna spravuje.

Poznámka: Důvodem, proč nemůžete iterovat přes všechny klíče mapování, je to, že ukládání dat kontraktu je drahé, takže čím méně ho používáme, tím lépe, a čím méně často ho měníme, tím lépe. Můžete vytvořit mapování, která podporují iteraci (opens in a new tab), ale vyžadují další úložiště pro seznam klíčů. Ve většině aplikací to nepotřebujete.

    event PairCreated(address indexed token0, address indexed token1, address pair, uint);

Tato událost je emitována při vytvoření nové směnárny párů. Zahrnuje adresy tokenů, adresu směnárny párů a celkový počet směnáren spravovaných továrnou.

    constructor(address _feeToSetter) public {
        feeToSetter = _feeToSetter;
    }

Jediná věc, kterou konstruktor dělá, je specifikace feeToSetter. Továrny začínají bez poplatku a pouze feeSetter to může změnit.

    function allPairsLength() external view returns (uint) {
        return allPairs.length;
    }

Tato funkce vrací počet směnných párů.

    function createPair(address tokenA, address tokenB) external returns (address pair) {

Toto je hlavní funkce továrny, vytvořit směnárnu párů mezi dvěma tokeny ERC-20. Všimněte si, že tuto funkci může zavolat kdokoli. K vytvoření nové směnárny párů nepotřebujete povolení od Uniswapu.

        require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);

Chceme, aby adresa nové směnárny byla deterministická, aby ji bylo možné vypočítat předem offchain (to může být užitečné pro transakce na vrstvě 2 (l2)). K tomu potřebujeme mít konzistentní pořadí adres tokenů, bez ohledu na pořadí, ve kterém jsme je obdrželi, takže je zde seřadíme.

        require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
        require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // jediná kontrola je dostatečná

Velké fondy likvidity jsou lepší než malé, protože mají stabilnější ceny. Nechceme mít více než jeden fond likvidity na pár tokenů. Pokud již směnárna existuje, není třeba vytvářet další pro stejný pár.

        bytes memory bytecode = type(UniswapV2Pair).creationCode;

K vytvoření nového kontraktu potřebujeme kód, který jej vytvoří (jak funkci konstruktoru, tak kód, který zapíše do paměti bajtkód EVM samotného kontraktu). Normálně v Solidity používáme pouze addr = new <name of contract>(<constructor parameters>) a kompilátor se o vše postará za nás, ale abychom měli deterministickou adresu kontraktu, musíme použít operační kód CREATE2 (opens in a new tab). Když byl tento kód napsán, tento operační kód ještě nebyl v Solidity podporován, takže bylo nutné kód získat ručně. To už není problém, protože Solidity nyní podporuje CREATE2 (opens in a new tab).

        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        assembly {
            pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
        }

Když operační kód ještě není v Solidity podporován, můžeme jej zavolat pomocí inline assembly (opens in a new tab).

        IUniswapV2Pair(pair).initialize(token0, token1);

Zavolejte funkci initialize, abyste nové směnárně řekli, jaké dva tokeny směňuje.

        getPair[token0][token1] = pair;
        getPair[token1][token0] = pair; // naplnit mapování v opačném směru
        allPairs.push(pair);
        emit PairCreated(token0, token1, pair, allPairs.length);
    }

Uložte informace o novém páru do stavových proměnných a emitujte událost, abyste informovali svět o nové směnárně párů.

Tyto dvě funkce umožňují feeSetter ovládat příjemce poplatku (pokud existuje) a změnit feeSetter na novou adresu.

UniswapV2ERC20.sol

Tento kontrakt (opens in a new tab) implementuje token likvidity ERC-20. Je podobný kontraktu ERC-20 od OpenZeppelin, takže vysvětlím pouze část, která se liší, funkcionalitu permit.

Transakce na Ethereu stojí ether (ETH), což je ekvivalent skutečných peněz. Pokud máte tokeny ERC-20, ale nemáte ETH, nemůžete odesílat transakce, takže s nimi nemůžete nic dělat. Jedním z řešení, jak se tomuto problému vyhnout, jsou metatransakce (opens in a new tab). Vlastník tokenů podepíše transakci, která umožňuje někomu jinému vybrat tokeny offchain, a odešle ji pomocí internetu příjemci. Příjemce, který má ETH, pak odešle povolení jménem vlastníka.

    bytes32 public DOMAIN_SEPARATOR;
    // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
    bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;

Tento hash je identifikátor typu transakce (opens in a new tab). Jediný, který zde podporujeme, je Permit s těmito parametry.

    mapping(address => uint) public nonces;

Pro příjemce není proveditelné zfalšovat digitální podpis. Je však triviální odeslat stejnou transakci dvakrát (to je forma útoku přehráním (replay attack) (opens in a new tab)). Abychom tomu zabránili, používáme nonce (opens in a new tab). Pokud nonce nového Permit není o jedna větší než poslední použitá, předpokládáme, že je neplatná.

    constructor() public {
        uint chainId;
        assembly {
            chainId := chainid
        }

Toto je kód pro načtení identifikátoru řetězce (opens in a new tab). Používá dialekt EVM assembly zvaný Yul (opens in a new tab). Všimněte si, že v aktuální verzi Yul musíte použít chainid(), nikoli chainid.

Vypočítejte oddělovač domény (opens in a new tab) pro EIP-712.

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

Toto je funkce, která implementuje oprávnění. Jako parametry přijímá příslušná pole a tři skalární hodnoty pro podpis (opens in a new tab) (v, r a s).

        require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');

Nepřijímejte transakce po termínu.

        bytes32 digest = keccak256(
            abi.encodePacked(
                '\x19\x01',
                DOMAIN_SEPARATOR,
                keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
            )
        );

abi.encodePacked(...) je zpráva, kterou očekáváme, že dostaneme. Víme, jaká by měla být nonce, takže není nutné, abychom ji dostali jako parametr.

Algoritmus podpisu Etherea očekává, že k podpisu dostane 256 bitů, takže používáme hashovací funkci keccak256.

        address recoveredAddress = ecrecover(digest, v, r, s);

Z hashe (digest) a podpisu můžeme získat adresu, která jej podepsala, pomocí ecrecover (opens in a new tab).

        require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
        _approve(owner, spender, value);
    }

Pokud je vše v pořádku, považujte to za schválení (approve) ERC-20 (opens in a new tab).

Periferní kontrakty

Periferní kontrakty jsou API (rozhraní pro programování aplikací) pro Uniswap. Jsou dostupné pro externí volání, ať už z jiných kontraktů nebo decentralizovaných aplikací. Mohli byste volat hlavní (core) kontrakty přímo, ale to je složitější a v případě chyby byste mohli přijít o hodnotu. Hlavní kontrakty obsahují pouze testy, které zajišťují, že nebudou podvedeny, nikoli kontroly smysluplnosti (sanity checks) pro kohokoli jiného. Ty se nacházejí v periferních kontraktech, aby mohly být podle potřeby aktualizovány.

UniswapV2Router01.sol

Tento kontrakt (opens in a new tab) má problémy a už by se neměl používat (opens in a new tab). Naštěstí jsou periferní kontrakty bezstavové (stateless) a nedrží žádná aktiva, takže je snadné jej označit za zastaralý a doporučit lidem, aby místo něj použili náhradu UniswapV2Router02.

UniswapV2Router02.sol

Ve většině případů byste Uniswap používali prostřednictvím tohoto kontraktu (opens in a new tab). Jak jej používat, se můžete podívat zde (opens in a new tab).

S většinou z nich jsme se už setkali, nebo jsou celkem zřejmé. Jedinou výjimkou je IWETH.sol. Uniswap v2 umožňuje směnu jakéhokoli páru tokenů ERC-20, ale samotný ether (ETH) není token ERC-20. Vznikl před tímto standardem a převádí se pomocí unikátních mechanismů. Aby bylo možné používat ETH v kontraktech, které pracují s tokeny ERC-20, přišli lidé s kontraktem pro zabalený ether (WETH) (opens in a new tab). Pošlete tomuto kontraktu ETH a on vám vyrazí ekvivalentní množství WETH. Nebo můžete WETH spálit a získat ETH zpět.

contract UniswapV2Router02 is IUniswapV2Router02 {
    using SafeMath for uint;

    address public immutable override factory;
    address public immutable override WETH;

Router potřebuje vědět, jakou továrnu (factory) použít, a pro transakce, které vyžadují WETH, jaký kontrakt WETH použít. Tyto hodnoty jsou neměnné (opens in a new tab), což znamená, že je lze nastavit pouze v konstruktoru. To dává uživatelům jistotu, že je nikdo nebude moci změnit tak, aby ukazovaly na méně poctivé kontrakty.

    modifier ensure(uint deadline) {
        require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
        _;
    }

Tento modifikátor zajišťuje, že časově omezené transakce („udělej X před časem Y, pokud můžeš“) neproběhnou po uplynutí jejich časového limitu.

    constructor(address _factory, address _WETH) public {
        factory = _factory;
        WETH = _WETH;
    }

Konstruktor pouze nastavuje neměnné stavové proměnné.

    receive() external payable {
        assert(msg.sender == WETH); // přijímat ETH pouze přes fallback z kontraktu WETH
    }

Tato funkce se volá, když vybíráme tokeny z kontraktu WETH zpět na ETH. K tomu je oprávněn pouze kontrakt WETH, který používáme.

Přidání likvidity

Tyto funkce přidávají tokeny do párové směnárny, což zvětšuje fond likvidity.


    // **** PŘIDAT LIKVIDITU ****
    function _addLiquidity(

Tato funkce se používá k výpočtu množství tokenů A a B, které by měly být vloženy do párové směnárny.

        address tokenA,
        address tokenB,

Toto jsou adresy kontraktů tokenů ERC-20.

        uint amountADesired,
        uint amountBDesired,

Toto jsou částky, které chce poskytovatel likvidity vložit. Jsou to zároveň maximální částky A a B, které mají být vloženy.

        uint amountAMin,
        uint amountBMin

Toto jsou minimální přijatelné částky pro vklad. Pokud transakce nemůže proběhnout s těmito nebo vyššími částkami, dojde k jejímu zvrácení (revert). Pokud tuto funkci nechcete, jednoduše zadejte nulu.

Poskytovatelé likvidity obvykle specifikují minimum, protože chtějí omezit transakci na směnný kurz, který se blíží tomu aktuálnímu. Pokud směnný kurz příliš kolísá, může to znamenat novinky, které mění podkladové hodnoty, a oni se chtějí manuálně rozhodnout, co dělat.

Představte si například případ, kdy je směnný kurz jedna ku jedné a poskytovatel likvidity zadá tyto hodnoty:

ParameterValue
amountADesired1000
amountBDesired1000
amountAMin900
amountBMin800

Dokud se směnný kurz drží mezi 0,9 a 1,25, transakce proběhne. Pokud se směnný kurz dostane mimo toto rozpětí, transakce se zruší.

Důvodem tohoto opatření je, že transakce nejsou okamžité; odešlete je a validátor je nakonec zahrne do bloku (pokud není vaše cena plynu velmi nízká, v takovém případě budete muset odeslat další transakci se stejnou hodnotou nonce a vyšší cenou plynu, abyste ji přepsali). Nemůžete ovlivnit, co se stane v intervalu mezi odesláním a zahrnutím.

    ) internal virtual returns (uint amountA, uint amountB) {

Funkce vrací částky, které by měl poskytovatel likvidity vložit, aby měl poměr rovný aktuálnímu poměru mezi rezervami.

        // vytvořit pár, pokud ještě neexistuje
        if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
            IUniswapV2Factory(factory).createPair(tokenA, tokenB);
        }

Pokud pro tento pár tokenů ještě neexistuje směnárna, vytvořte ji.

        (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);

Získejte aktuální rezervy v páru.

        if (reserveA == 0 && reserveB == 0) {
            (amountA, amountB) = (amountADesired, amountBDesired);

Pokud jsou aktuální rezervy prázdné, jedná se o novou párovou směnárnu. Částky k vložení by měly být přesně stejné jako ty, které chce poskytovatel likvidity poskytnout.

        } else {
            uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);

Pokud potřebujeme zjistit, jaké budou částky, získáme optimální částku pomocí této funkce (opens in a new tab). Chceme stejný poměr, jako mají aktuální rezervy.

            if (amountBOptimal <= amountBDesired) {
                require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
                (amountA, amountB) = (amountADesired, amountBOptimal);

Pokud je amountBOptimal menší než částka, kterou chce poskytovatel likvidity vložit, znamená to, že token B je v současnosti cennější, než si vkladatel likvidity myslí, takže je potřeba menší částka.

            } else {
                uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
                assert(amountAOptimal <= amountADesired);
                require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
                (amountA, amountB) = (amountAOptimal, amountBDesired);

Pokud je optimální částka B vyšší než požadovaná částka B, znamená to, že tokeny B jsou v současnosti méně cenné, než si vkladatel likvidity myslí, takže je potřeba vyšší částka. Požadovaná částka je však maximum, takže to nemůžeme udělat. Místo toho vypočítáme optimální počet tokenů A pro požadované množství tokenů B.

Když to všechno spojíme, dostaneme tento graf. Předpokládejme, že se snažíte vložit tisíc tokenů A (modrá čára) a tisíc tokenů B (červená čára). Osa x představuje směnný kurz, A/B. Pokud x=1, mají stejnou hodnotu a vložíte od každého tisíc. Pokud x=2, má A dvojnásobnou hodnotu než B (za každý token A dostanete dva tokeny B), takže vložíte tisíc tokenů B, ale pouze 500 tokenů A. Pokud x=0,5, situace je opačná, tisíc tokenů A a pět set tokenů B.

Graph

Likviditu byste mohli vložit přímo do hlavního kontraktu (pomocí UniswapV2Pair::mint (opens in a new tab)), ale hlavní kontrakt pouze kontroluje, zda není sám podveden, takže se vystavujete riziku ztráty hodnoty, pokud se směnný kurz změní mezi okamžikem odeslání transakce a jejím provedením. Pokud použijete periferní kontrakt, ten vypočítá částku, kterou byste měli vložit, a okamžitě ji vloží, takže se směnný kurz nezmění a vy o nic nepřijdete.

Tuto funkci lze zavolat transakcí pro vložení likvidity. Většina parametrů je stejná jako u _addLiquidity výše, se dvěma výjimkami:

. to je adresa, na kterou se vyrazí nové tokeny likvidity, aby ukazovaly podíl poskytovatele likvidity ve fondu . deadline je časový limit transakce

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

Vypočítáme částky, které se mají skutečně vložit, a poté najdeme adresu fondu likvidity. Abychom ušetřili gas, neděláme to dotazem na továrnu, ale pomocí funkce knihovny pairFor (viz níže v knihovnách).

        TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
        TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);

Převeďte správné částky tokenů od uživatele do párové směnárny.

        liquidity = IUniswapV2Pair(pair).mint(to);
    }

Na oplátku dejte adrese to tokeny likvidity za částečné vlastnictví fondu. Funkce mint hlavního kontraktu zjistí, kolik má tokenů navíc (v porovnání s tím, co měl při poslední změně likvidity), a podle toho vyrazí likviditu.

    function addLiquidityETH(
        address token,
        uint amountTokenDesired,

Když chce poskytovatel likvidity poskytnout likviditu do párové směnárny Token/ETH, existuje několik rozdílů. Kontrakt se postará o zabalení ETH pro poskytovatele likvidity. Není třeba specifikovat, kolik ETH chce uživatel vložit, protože je uživatel jednoduše pošle s transakcí (částka je k dispozici v msg.value).

Pro vložení ETH jej kontrakt nejprve zabalí do WETH a poté převede WETH do páru. Všimněte si, že převod je zabalen v assert. To znamená, že pokud převod selže, selže i toto volání kontraktu, a proto k zabalení ve skutečnosti nedojde.

        liquidity = IUniswapV2Pair(pair).mint(to);
        // vrátit zbytkový ether, pokud nějaký je
        if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
    }

Uživatel nám již poslal ETH, takže pokud nějaké zbyde (protože druhý token je méně cenný, než si uživatel myslel), musíme provést vrácení peněz.

Odebrání likvidity

Tyto funkce odeberou likviditu a vyplatí poskytovatele likvidity.

Nejjednodušší případ odebrání likvidity. Existuje minimální množství každého tokenu, které poskytovatel likvidity souhlasí přijmout, a musí k tomu dojít před uplynutím lhůty.

        address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
        IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // odeslat likviditu do páru
        (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);

Funkce burn hlavního kontraktu se stará o vyplacení tokenů zpět uživateli.

        (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);

Když funkce vrací více hodnot, ale nás zajímají jen některé z nich, takto získáme pouze tyto hodnoty. Z hlediska gasu je to o něco levnější než přečíst hodnotu a nikdy ji nepoužít.

        (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);

Převeďte částky ze způsobu, jakým je vrací hlavní kontrakt (token s nižší adresou jako první), na způsob, jaký očekává uživatel (odpovídající tokenA a tokenB).

        require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
        require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
    }

Je v pořádku provést převod jako první a poté ověřit, zda je legitimní, protože pokud není, zvrátíme (revert) všechny změny stavu.

Odebrání likvidity pro ETH je téměř stejné, s tím rozdílem, že obdržíme tokeny WETH a poté je směníme za ETH, které vrátíme poskytovateli likvidity.

Tyto funkce předávají meta-transakce, aby umožnily uživatelům bez etheru vybírat z fondu pomocí mechanismu povolení (permit).

Tuto funkci lze použít pro tokeny, které mají poplatky za převod nebo uložení. Když má token takové poplatky, nemůžeme se spoléhat na to, že nám funkce removeLiquidity řekne, kolik tokenu dostaneme zpět, takže musíme nejprve provést výběr a poté zjistit zůstatek.

Poslední funkce kombinuje poplatky za uložení s meta-transakcemi.

Obchodování

    // **** SWAP ****
    // vyžaduje, aby počáteční částka již byla odeslána do prvního páru
    function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {

Tato funkce provádí interní zpracování, které je vyžadováno pro funkce vystavené obchodníkům.

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

V době psaní tohoto textu existuje 388 160 tokenů ERC-20 (opens in a new tab). Kdyby pro každý pár tokenů existovala párová směnárna, bylo by to přes 150 miliard párových směnáren. Celý řetězec má v současné době pouze 0,1 % tohoto počtu účtů (opens in a new tab). Místo toho funkce swapu podporují koncept cesty (path). Obchodník může směnit A za B, B za C a C za D, takže není potřeba přímá párová směnárna A-D.

Ceny na těchto trzích bývají synchronizované, protože když nejsou, vytváří to příležitost pro arbitráž. Představte si například tři tokeny, A, B a C. Existují tři párové směnárny, jedna pro každý pár.

  1. Výchozí situace
  2. Obchodník prodá 24,695 tokenů A a získá 25,305 tokenů B.
  3. Obchodník prodá 24,695 tokenů B za 25,305 tokenů C, přičemž si ponechá přibližně 0,61 tokenů B jako zisk.
  4. Poté obchodník prodá 24,695 tokenů C za 25,305 tokenů A, přičemž si ponechá přibližně 0,61 tokenů C jako zisk. Obchodník má také 0,61 tokenů A navíc (25,305, se kterými obchodník skončí, minus původní investice 24,695).
KrokSměnárna A-BSměnárna B-CSměnárna 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
            (address input, address output) = (path[i], path[i + 1]);
            (address token0,) = UniswapV2Library.sortTokens(input, output);
            uint amountOut = amounts[i + 1];

Získejte pár, který právě zpracováváme, seřaďte jej (pro použití s párem) a získejte očekávanou výstupní částku.

            (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));

Získejte očekávané výstupní částky, seřazené tak, jak je párová směnárna očekává.

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

Je toto poslední směna? Pokud ano, pošlete tokeny získané za obchod na cílovou adresu. Pokud ne, pošlete je do další párové směnárny.


            IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
                amount0Out, amount1Out, to, new bytes(0)
            );
        }
    }

Skutečně zavolejte párovou směnárnu pro swap tokenů. Nepotřebujeme zpětné volání (callback), které by nás o směně informovalo, takže v tomto poli neposíláme žádné bajty.

    function swapExactTokensForTokens(

Tuto funkci používají přímo obchodníci ke swapu jednoho tokenu za jiný.

        uint amountIn,
        uint amountOutMin,
        address[] calldata path,

Tento parametr obsahuje adresy kontraktů ERC-20. Jak bylo vysvětleno výše, jedná se o pole, protože možná budete muset projít několika párovými směnárnami, abyste se dostali od aktiva, které máte, k aktivu, které chcete.

Parametr funkce v Solidity může být uložen buď v memory nebo v calldata. Pokud je funkce vstupním bodem do kontraktu, volaným přímo od uživatele (pomocí transakce) nebo z jiného kontraktu, pak lze hodnotu parametru převzít přímo z dat volání (call data). Pokud je funkce volána interně, jako _swap výše, pak musí být parametry uloženy v memory. Z pohledu volaného kontraktu jsou calldata pouze pro čtení.

U skalárních typů, jako je uint nebo address, řeší kompilátor volbu úložiště za nás, ale u polí, která jsou delší a dražší, specifikujeme typ úložiště, které se má použít.

        address to,
        uint deadline
    ) external virtual override ensure(deadline) returns (uint[] memory amounts) {

Návratové hodnoty se vždy vracejí v paměti.

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

Vypočítejte částku, která má být nakoupena v každém swapu. Pokud je výsledek menší než minimum, které je obchodník ochoten přijmout, zvrátí (revert) se transakce.

        TransferHelper.safeTransferFrom(
            path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
        );
        _swap(amounts, path, to);
    }

Nakonec převeďte počáteční token ERC-20 na účet pro první párovou směnárnu a zavolejte _swap. To vše se děje ve stejné transakci, takže párová směnárna ví, že jakékoli neočekávané tokeny jsou součástí tohoto převodu.

Předchozí funkce, swapTokensForTokens, umožňuje obchodníkovi specifikovat přesný počet vstupních tokenů, které je ochoten dát, a minimální počet výstupních tokenů, které je ochoten za ně přijmout. Tato funkce provádí reverzní swap, umožňuje obchodníkovi specifikovat počet výstupních tokenů, které chce, a maximální počet vstupních tokenů, které je za ně ochoten zaplatit.

V obou případech musí obchodník nejprve poskytnout tomuto perifernímu kontraktu povolený limit (allowance), aby mu umožnil jejich převod.

Všechny tyto čtyři varianty zahrnují obchodování mezi ETH a tokeny. Jediný rozdíl je v tom, že buď obdržíme ETH od obchodníka a použijeme jej k vyražení WETH, nebo obdržíme WETH z poslední směnárny na cestě a spálíme jej, přičemž obchodníkovi pošleme zpět výsledné ETH.

    // **** SWAP (s podporou tokenů s poplatkem při převodu) ****
    // vyžaduje, aby počáteční částka již byla odeslána do prvního páru
    function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {

Toto je interní funkce pro swap tokenů, které mají poplatky za převod nebo uložení, k vyřešení (tohoto problému (opens in a new tab)).

Kvůli poplatkům za převod se nemůžeme spoléhat na to, že nám funkce getAmountsOut řekne, kolik získáme z každého převodu (jak to děláme před voláním původní _swap). Místo toho musíme nejprve provést převod a poté zjistit, kolik tokenů jsme dostali zpět.

Poznámka: Teoreticky bychom mohli použít tuto funkci místo _swap, ale v určitých případech (například pokud je převod nakonec zvrácen, protože na konci není dostatek k dosažení požadovaného minima) by to nakonec stálo více gasu. Tokeny s poplatkem za převod jsou poměrně vzácné, takže i když se jim musíme přizpůsobit, není nutné, aby všechny swapy předpokládaly, že projdou alespoň jedním z nich.

Toto jsou stejné varianty používané pro normální tokeny, ale místo toho volají _swapSupportingFeeOnTransferTokens.

Tyto funkce jsou pouze proxy, které volají funkce UniswapV2Library.

UniswapV2Migrator.sol

Tento kontrakt se používal k migraci směnáren ze staré v1 na v2. Nyní, když byly migrovány, již není relevantní.

Knihovny

Knihovna SafeMath (opens in a new tab) je dobře zdokumentovaná, takže není potřeba ji zde dokumentovat.

Math

Tato knihovna obsahuje některé matematické funkce, které v kódu Solidity běžně nejsou potřeba, a proto nejsou součástí jazyka.

Začněte s x jako odhadem, který je vyšší než druhá odmocnina (to je důvod, proč musíme s 1-3 zacházet jako se speciálními případy).

            while (x < z) {
                z = x;
                x = (y / x + x) / 2;

Získejte přesnější odhad, průměr předchozího odhadu a čísla, jehož druhou odmocninu se snažíme najít, vyděleného předchozím odhadem. Opakujte, dokud nový odhad nebude nižší než ten stávající. Pro více podrobností se podívejte sem (opens in a new tab).

            }
        } else if (y != 0) {
            z = 1;

Nikdy bychom neměli potřebovat druhou odmocninu z nuly. Druhé odmocniny z jedné, dvou a tří jsou zhruba jedna (používáme celá čísla, takže zlomky ignorujeme).

        }
    }
}

Zlomky s pevnou řádovou čárkou (UQ112x112)

Tato knihovna zpracovává zlomky, které běžně nejsou součástí aritmetiky Etherea. Dělá to tak, že zakóduje číslo x jako x*2^112. To nám umožňuje používat původní operační kódy pro sčítání a odčítání beze změny.

Q112 je kódování pro jedničku.

    // zakódovat uint112 jako UQ112x112
    function encode(uint112 y) internal pure returns (uint224 z) {
        z = uint224(y) * Q112; // nikdy nepřeteče
    }

Protože y je uint112, může být maximálně 2^112-1. Toto číslo lze stále zakódovat jako UQ112x112.

    // vydělit UQ112x112 pomocí uint112, vrací UQ112x112
    function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
        z = x / uint224(y);
    }
}

Pokud vydělíme dvě hodnoty UQ112x112, výsledek už není vynásoben 2^112. Místo toho tedy vezmeme celé číslo pro jmenovatele. Podobný trik bychom museli použít i pro násobení, ale násobení hodnot UQ112x112 nepotřebujeme provádět.

UniswapV2Library

Tato knihovna je používána pouze periferními kontrakty

Seřaďte dva tokeny podle adresy, abychom pro ně mohli získat adresu párové směnárny. To je nutné, protože jinak bychom měli dvě možnosti, jednu pro parametry A,B a druhou pro parametry B,A, což by vedlo ke dvěma směnárnám místo jedné.

Tato funkce vypočítá adresu párové směnárny pro dva tokeny. Tento kontrakt je vytvořen pomocí operačního kódu CREATE2 (opens in a new tab), takže můžeme vypočítat adresu pomocí stejného algoritmu, pokud známe parametry, které používá. To je mnohem levnější než se ptát továrny (factory), a

    // načte a seřadí rezervy pro pár
    function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
        (address token0,) = sortTokens(tokenA, tokenB);
        (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
        (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
    }

Tato funkce vrací rezervy dvou tokenů, které má párová směnárna. Všimněte si, že může přijímat tokeny v libovolném pořadí a pro interní použití si je seřadí.

    // na základě určitého množství aktiva a rezerv páru vrací ekvivalentní množství druhého aktiva
    function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
        require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
        require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
        amountB = amountA.mul(reserveB) / reserveA;
    }

Tato funkce vám poskytne množství tokenu B, které získáte výměnou za token A, pokud není účtován žádný poplatek. Tento výpočet bere v úvahu, že převod mění směnný kurz.

    // na základě vstupního množství aktiva a rezerv páru vrací maximální výstupní množství druhého aktiva
    function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {

Výše uvedená funkce quote funguje skvěle, pokud za použití párové směnárny není žádný poplatek. Pokud je však poplatek za směnu 0,3 %, částka, kterou skutečně dostanete, je nižší. Tato funkce vypočítá částku po odečtení poplatku za směnu.


        require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
        require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
        uint amountInWithFee = amountIn.mul(997);
        uint numerator = amountInWithFee.mul(reserveOut);
        uint denominator = reserveIn.mul(1000).add(amountInWithFee);
        amountOut = numerator / denominator;
    }

Solidity nativně nezpracovává zlomky, takže nemůžeme částku jednoduše vynásobit 0,997. Místo toho vynásobíme čitatele 997 a jmenovatele 1000, čímž dosáhneme stejného efektu.

    // na základě výstupního množství aktiva a rezerv páru vrací požadované vstupní množství druhého aktiva
    function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
        require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
        require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
        uint numerator = reserveIn.mul(amountOut).mul(1000);
        uint denominator = reserveOut.sub(amountOut).mul(997);
        amountIn = (numerator / denominator).add(1);
    }

Tato funkce dělá zhruba to samé, ale získá výstupní částku a poskytne vstupní.

Tyto dvě funkce se starají o identifikaci hodnot, když je nutné projít několika párovými směnárnami.

Transfer Helper

Tato knihovna (opens in a new tab) přidává kontroly úspěšnosti kolem převodů ERC-20 a Etherea, aby se zvrácení (revert) a vrácení hodnoty false zpracovávaly stejným způsobem.

Jiný kontrakt můžeme zavolat jedním ze dvou způsobů:

        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            'TransferHelper::safeApprove: approve failed'
        );
    }

Z důvodu zpětné kompatibility s tokeny, které byly vytvořeny před standardem ERC-20, může volání ERC-20 selhat buď zvrácením (v takovém případě je success false), nebo tím, že je úspěšné a vrátí hodnotu false (v takovém případě existují výstupní data, a pokud je dekódujete jako boolean, získáte false).

Tato funkce implementuje funkcionalitu převodu ERC-20 (opens in a new tab), která umožňuje účtu utratit povolený limit poskytnutý jiným účtem.

Tato funkce implementuje funkcionalitu transferFrom ERC-20 (opens in a new tab), která umožňuje účtu utratit povolený limit poskytnutý jiným účtem.


    function safeTransferETH(address to, uint256 value) internal {
        (bool success, ) = to.call{value: value}(new bytes(0));
        require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');
    }
}

Tato funkce převádí ether na účet. Jakékoli volání jiného kontraktu se může pokusit odeslat ether. Protože ve skutečnosti nepotřebujeme volat žádnou funkci, neposíláme s voláním žádná data.

Závěr

Toto je dlouhý článek o zhruba 50 stranách. Pokud jste se dostali až sem, gratulujeme! Doufejme, že jste nyní pochopili, co je třeba zvážit při psaní reálné aplikace (na rozdíl od krátkých ukázkových programů), a jste lépe připraveni psát kontrakty pro své vlastní případy použití.

Nyní běžte, napište něco užitečného a ohromte nás.

Zde najdete více z mé práce (opens in a new tab).