Přeskočit na hlavní obsah

Procházení kontraktu Uniswap v2

solidity
Středně pokročilý
Ori Pomerantz
1. května 2021
54 minuta čtení

Úvod

Uniswap v2opens in a new tab umí vytvořit směnný trh mezi libovolnými dvěma tokeny ERC-20. V tomto článku si projdeme zdrojový kód kontraktů, které implementují tento protokol, a podíváme se, proč jsou napsány právě tímto způsobem.

Co dělá Uniswap?

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

Poskytovatelé likvidity poskytují poolu dva tokeny, které lze směňovat (nazvěme je Token0 a Token1). Na oplátku obdrží třetí token, který představuje částečné vlastnictví poolu, nazývaný token likvidity.

Obchodníci posílají jeden typ tokenu do poolu a přijímají druhý (například odešlou Token0 a obdrží Token1) z poolu poskytnutého poskytovateli likvidity. Směnný kurz je určen relativním počtem tokenů Token0 a Token1, které má pool k dispozici. Kromě toho si pool bere malé procento jako odměnu pro pool likvidity.

Když poskytovatelé likvidity chtějí svá aktiva zpět, mohou spálit tokeny poolu a obdržet zpět své tokeny, včetně jejich podílu na odměnách.

Podrobnější popis najdete zdeopens in a new tab.

Proč v2? Proč ne v3?

Uniswap v3opens in a new tab je vylepšení, které je mnohem komplikovanější než v2. Je jednodušší se nejprve naučit v2 a pak přejít na v3.

Hlavní kontrakty vs. periferní kontrakty

Uniswap v2 je rozdělen na dvě části: hlavní a periferní. Toto rozdělení umožňuje, aby hlavní kontrakty, které drží aktiva, a proto musí být bezpečné, byly jednodušší a snadněji auditovatelné. Všechny další funkce požadované obchodníky pak mohou poskytovat periferní kontrakty.

Datové a řídicí toky

Toto je tok dat a řízení, který nastává při provádění tří hlavních akcí Uniswapu:

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

Směna

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

Volající

  1. Poskytněte perifernímu účtu povolenku v částce, která má být směněna.
  2. Zavolejte jednu z mnoha směnných funkcí periferního kontraktu (která závisí na tom, zda se jedná o ETH, zda obchodník specifikuje množství tokenů k vkladu nebo množství tokenů k získání zpět atd.). Každá funkce směny přijímá path, pole směnáren, kterými se má projít.

V periferním kontraktu (UniswapV2Router02.sol)

  1. Identifikujte částky, které je třeba obchodovat na každé směnárně podél cesty.
  2. Iteruje přes cestu. Pro každou směnárnu po cestě odešle vstupní token a poté zavolá směnnou funkci swap směnárny. Ve většině případů je cílová adresa pro tokeny další párovou směnárnou v cestě. V konečné směnárně se jedná o adresu poskytnutou obchodníkem.

V hlavním kontraktu (UniswapV2Pair.sol) {#in-the-core-contract-uniswapv2pairsol-2}5. Ověřte, že hlavní kontrakt není podváděn a že si po směně dokáže udržet dostatečnou likviditu.

  1. Podívejte se, kolik dalších tokenů máme kromě známých rezerv. Tato částka je počet vstupních tokenů, které jsme obdrželi ke směně.
  2. Odešlete výstupní tokeny do cíle.
  3. Zavolejte _update pro aktualizaci výše rezerv

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

  1. Proveďte veškeré potřebné vyčištění (například spálení tokenů WETH, abyste získali zpět ETH pro odeslání obchodníkovi)

Přidat likviditu

Volající

  1. Poskytněte perifernímu účtu povolenku v částkách, které se mají přidat do poolu likvidity.
  2. Zavolejte jednu z funkcí addLiquidity periferního kontraktu.

V periferním kontraktu (UniswapV2Router02.sol)

  1. V případě potřeby vytvořte novou párovou směnárnu
  2. Pokud existuje párová směnárna, vypočítejte množství tokenů k přidání. Předpokládá se, že se jedná o stejnou hodnotu pro oba tokeny, tedy stejný poměr nových tokenů k existujícím tokenům.
  3. Zkontrolujte, zda jsou částky přijatelné (volající mohou zadat minimální částku, pod kterou by raději nepřidávali likviditu)
  4. Zavolejte hlavní kontrakt.

V hlavním kontraktu (UniswapV2Pair.sol)

  1. Vyražte tokeny likvidity a odešlete je volajícímu
  2. Zavolejte _update pro aktualizaci výše rezerv

Odebrat likviditu

Volající

  1. Poskytněte perifernímu účtu povolenku na tokeny likvidity, které mají být spáleny výměnou za podkladové tokeny.
  2. Zavolejte jednu z funkcí removeLiquidity periferního kontraktu.

V periferním kontraktu (UniswapV2Router02.sol)

  1. Odešlete tokeny likvidity do párové směnárny

V hlavním kontraktu (UniswapV2Pair.sol)

  1. Odešlete na cílovou adresu podkladové tokeny v poměru ke spáleným tokenům. Například pokud je v poolu 1 000 tokenů A, 500 tokenů B a 90 tokenů likvidity a obdržíme 9 tokenů ke spálení, pálíme 10 % tokenů likvidity, takže uživateli vrátíme 100 tokenů A a 50 tokenů B.
  2. Spalte tokeny likvidity
  3. Zavolejte _update pro aktualizaci výše rezerv

Hlavní kontrakty

Jedná se o bezpečné kontrakty, které drží likviditu.

UniswapV2Pair.sol

Tento kontraktopens in a new tab implementuje skutečný pool, který směňuje tokeny. Jedná se o hlavní funkci Uniswapu.

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';
Zobrazit vše

Jedná se o všechna rozhraní, o kterých kontrakt potřebuje vědět, buď proto, že je kontrakt implementuje (IUniswapV2Pair a UniswapV2ERC20), nebo proto, že volá kontrakty, které je implementují.

1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {

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

1 using SafeMath for uint;

Knihovna SafeMathopens in a new tab se používá k zamezení přetečení a podtečení. To je důležité, protože jinak bychom mohli skončit v situaci, kdy by hodnota měla být -1, ale místo toho je 2^256-1.

1 using UQ112x112 for uint224;

Mnoho výpočtů v kontraktu poolu vyžaduje zlomky. Zlomky však nejsou v EVM podporovány. Řešení, které Uniswap nalezl, je použití 224bitových hodnot, se 112 bity pro celočíselnou část a 112 bity pro zlomkovou část. Takže 1.0 je reprezentováno jako 2^112, 1.5 je reprezentováno jako 2^112 + 2^111 atd.

Další podrobnosti o této knihovně jsou k dispozici později v tomto dokumentu.

Proměnné

1 uint public constant MINIMUM_LIQUIDITY = 10**3;

Aby se zabránilo dělení nulou, existuje minimální počet tokenů likvidity, které vždy existují (ale jsou vlastněny nulovým účtem). Tento počet je MINIMUM_LIQUIDITY, tisíc.

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

Toto je ABI selektor pro přenosovou funkci ERC-20. Používá se k převodu tokenů ERC-20 na dva tokenové účty.

1 address public factory;

Jedná se o factory kontrakt, který vytvořil tento pool. Každý pool je směnárnou mezi dvěma tokeny ERC-20, factory je centrální bod, který spojuje všechny tyto pooly.

1 address public token0;
2 address public token1;

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

1 uint112 private reserve0; // používá jeden slot úložiště, dostupný přes getReserves
2 uint112 private reserve1; // používá jeden slot úložiště, dostupný přes getReserves

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

1 uint32 private blockTimestampLast; // používá jeden slot úložiště, dostupný přes getReserves

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

Jedním z největších nákladů na palivo v ethereových kontraktech je úložiště, které přetrvává od jednoho volání kontraktu k druhému. Každá buňka úložiště je 256 bitů dlouhá. Takže tři proměnné, reserve0, reserve1 a blockTimestampLast, jsou alokovány tak, že jedna hodnota úložiště může obsahovat všechny tři (112+112+32=256).

1 uint public price0CumulativeLast;
2 uint public price1CumulativeLast;

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

1 uint public kLast; // reserve0 * reserve1, stav bezprostředně po poslední události likvidity

Způsob, jakým párová směnárna rozhoduje o směnném kurzu mezi token0 a token1, je udržovat násobek obou rezerv konstantní během obchodů. kLast je tato hodnota. Mění se, když poskytovatel likvidity vkládá nebo vybírá 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 obchodní poplatek ve výši 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 smění 50 token0 za 47,619 token11 050,000952,3811 000 0000,952
Obchodník B smění 10 token0 za 8,984 token11 060,000943,3961 000 0000,898
Obchodník C smění 40 token0 za 34,305 token11 100,000909,0901 000 0000,858
Obchodník D smění 100 token1 za 109,01 token0990,9901 009,0901 000 0000,917
Obchodník E smění 10 token0 za 10,079 token11 000,990999,0101 000 0001,008

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

Zámek

1 uint private unlocked = 1;

Existuje třída bezpečnostních zranitelností, které jsou založeny na zneužití reentranceopens in a new tab. Uniswap musí 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á. Díky proměnné unlocked jako součásti kontraktu můžeme zabránit volání funkcí, které již běží (v rámci stejné transakce).

1 modifier lock() {

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

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

Pokud se unlocked rovná jedné, nastavte ji na nulu. Pokud je již nula, vraťte volání zpět, aby se nezdařilo.

1 _;

V modifikátoru _; je původní volání funkce (se všemi parametry). Zde to znamená, že volání funkce proběhne pouze v případě, že unlocked bylo jedna, když byla volána, a během jejího běhu je hodnota unlocked nula.

1 unlocked = 1;
2 }

Po návratu hlavní funkce uvolněte zámek.

Různé funkce

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

Tato funkce poskytuje volajícím aktuální stav směnárny. Všimněte si, že funkce v Solidity mohou vracet více hodnotopens 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));

Tato interní funkce převádí množství tokenů ERC20 ze směnárny někomu jinému. SELECTOR specifikuje, že volaná funkce je transfer(address,uint) (viz definice výše).

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

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

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

  1. Návrat. Pokud se volání externího kontraktu vrátí, pak je návratová hodnota boolean false
  2. Ukončit normálně, ale nahlásit chybu. V takovém případě má buffer návratové hodnoty nenulovou délku a po dekódování jako booleovská hodnota je false

Pokud nastane kterákoli z těchto podmínek, vraťte zpět.

Události

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

Tyto dvě události se vygenerují, když poskytovatel likvidity buď vloží likviditu (Mint), nebo ji vybere (Burn). V obou případech jsou součástí události množství tokenu0 a tokenu1, které jsou vloženy nebo vybrány, a také identita účtu, který nás volal (sender). V případě výběru událost obsahuje také cíl, který obdržel tokeny (to), který se nemusí shodovat s odesílatelem.

1 event Swap(
2 address indexed sender,
3 uint amount0In,
4 uint amount1In,
5 uint amount0Out,
6 uint amount1Out,
7 address indexed to
8 );

Tato událost se vygeneruje, když obchodník smění 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 do směnárny, nebo z ní přijat.

1 event Sync(uint112 reserve0, uint112 reserve1);

Nakonec se Sync vygeneruje pokaždé, když jsou přidány nebo vybrány tokeny, bez ohledu na důvod, aby se poskytly nejnovější informace o rezervách (a tedy o směnném kurzu).

Funkce nastavení

Tyto funkce mají být volány jednou, když je nastavena nová párová směnárna.

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

Konstruktor zajišťuje, že budeme sledovat adresu kontraktu factory, který pár vytvořil. Tato informace je nutná pro initialize a pro poplatek kontraktu factory (pokud existuje).

1 // voláno jednou z kontraktu factory v době nasazení
2 function initialize(address _token0, address _token1) external {
3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // dostatečná kontrola
4 token0 = _token0;
5 token1 = _token1;
6 }

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

Interní funkce aktualizace

_update
1 // aktualizovat rezervy a při prvním volání za blok akumulátory cen
2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {

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

1 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ři převodu na uint112 dojde k přetečení a návratu k 0), odmítněte pokračovat v _update, aby se zabránilo 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 každého tokenu. Zatím to nebyl problém.

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

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

1 // * nikdy nedojde k přetečení a + přetečení je žádoucí
2 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
3 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
4 }

Každý akumulátor nákladů je aktualizován o nejnovější náklady (rezerva druhého tokenu/rezerva tohoto tokenu) vynásobené 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 časovým rozdílem mezi nimi. Předpokládejme například tuto sekvenci 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 získá zpět 47,619 token11 050,000952,3815 0200,90720
Obchodník B vloží 10 token0 a získá zpět 8,984 token11 060,000943,3965 0300,89020+10*0,907 = 29,07
Obchodník C vloží 40 token0 a získá zpět 34,305 token11 100,000909,0905 1000,82629,07+70*0,890 = 91,37
Obchodník D vloží 100 token1 a získá zpět 109,01 token0990,9901 009,0905 1101,01891,37+10*0,826 = 99,63
Obchodník E vloží 10 token0 a získá 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 Tokenu0 mezi časovými razítky 5 030 a 5 150. Rozdíl v hodnotě price0Cumulative je 143,702-29,07=114,632. To 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ůvod, proč potřebujeme znát staré velikosti rezerv.

1 reserve0 = uint112(balance0);
2 reserve1 = uint112(balance1);
3 blockTimestampLast = blockTimestamp;
4 emit Sync(reserve0, reserve1);
5 }

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

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

V Uniswapu 2.0 platí obchodníci za použití trhu poplatek ve výši 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 určenou kontraktem factory jako poplatek za protokol, který platí Uniswapu za jeho vývojové úsilí.

Aby se snížily výpočty (a tedy náklady na palivo), tento poplatek se počítá pouze při přidání nebo odebrání likvidity z poolu, nikoliv při každé transakci.

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

Přečtěte si cíl poplatku kontraktu factory. Pokud je nula, neexistuje žádný protokolární poplatek a není třeba ho počítat.

1 uint _kLast = kLast; // úspora paliva

Stavová proměnná kLast se nachází 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 ukončení volání funkce kontraktu, takže pro úsporu paliva používáme interní proměnnou.

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

Poskytovatelé likvidity získávají svůj podíl jednoduše zhodnocením svých tokenů likvidity. Protokolární poplatek však vyžaduje, aby byly vyraženy nové tokeny likvidity a poskytnuty na adresu feeTo.

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

Pokud existuje nová likvidita, na kterou se má vybírat poplatek za protokol. Funkci druhé odmocniny najdete později v tomto článku

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

Tento složitý výpočet poplatků je vysvětlen v bílé knizeopens in a new tab na straně 5. Víme, že mezi dobou, kdy byl kLast vypočítán, 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, předtím, 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í).

1 if (liquidity > 0) _mint(feeTo, liquidity);
2 }
3 }

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

1 } else if (_kLast != 0) {
2 kLast = 0;
3 }
4 }

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

Externě dostupné funkce

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

vyrazit
1 // tuto funkci na nízké úrovni by měl volat kontrakt, který provádí důležité bezpečnostní kontroly
2 function mint(address to) external lock returns (uint liquidity) {

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

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

Toto je způsob, jak číst výsledky funkce Solidity, která vrací více hodnot. Zahazujeme poslední vrácené hodnoty, časové razítko bloku, protože ho nepotřebujeme.

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

Získejte aktuální zůstatky a zjistěte, kolik bylo přidáno z každého typu tokenu.

1 bool feeOn = _mintFee(_reserve0, _reserve1);

Vypočítejte poplatky za protokol, které mají být vybrány, pokud existují, a podle toho vyražte tokeny likvidity. Protože parametry _mintFee jsou staré hodnoty rezerv, poplatek se vypočítá přesně pouze na základě změn v poolu způsobených poplatky.

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

Pokud se jedná o první vklad, vytvořte MINIMUM_LIQUIDITY tokenů a odešlete je na nulovou adresu, abyste je uzamkli. Nikdy nemohou být vykoupeny, což znamená, že pool nikdy nebude zcela vyprázdněn (to nás na některých místech ušetří dělení 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, jako je ETH rozdělen na wei, je 10^-15 hodnoty jediného tokenu. Nejsou to vysoké náklady.

V době prvního vkladu neznáme relativní hodnotu obou tokenů, takže jednoduše vynásobíme částky a vezmeme druhou odmocninu, za předpokladu, že vklad nám poskytne 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 arbitráží. Řekněme, že hodnota obou tokenů je stejná, ale náš vkladatel vložil čtyřikrát více Tokenu1 než Tokenu0. Obchodník může využít toho, že si párová směnárna myslí, že Token0 je cennější, a vytěžit z něj hodnotu.

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

Jak vidíte, obchodník získal navíc 8 tokenů, které pocházejí ze snížení hodnoty poolu, což poškozuje vkladatele, který ho vlastní.

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

S každým dalším vkladem 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 tokeny likvidity na základě nižší hodnoty, kterou poskytli, jako trest.

Ať už se jedná o počáteční vklad nebo 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 nevytváří žádné tokeny likvidity).

Událostreserve0reserve1reserve0 * reserve1Hodnota poolu (reserve0 + reserve1)Tokeny likvidity vyražené pro tento vkladCelkový počet tokenů likvidityhodnota každého tokenu likvidity
Počáteční nastavení8,0008,0006416,000882,000
Vložte čtyři z každého typu12,00012,00014424,0004122,000
Vložte dva z každého typu14,00014,00019628,0002142,000
Vklad nerovné hodnoty18,00014,00025232,000014~2,286
Po arbitráži~15,874~15,874252~31,748014~2,267
1 }
2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
3 _mint(to, liquidity);

Použijte funkci UniswapV2ERC20._mint k vytvoření dalších tokenů likvidity a jejich předání na správný účet.

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

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

spálit
1 // tuto funkci na nízké úrovni by měl volat kontrakt, který provádí důležité bezpečnostní kontroly
2 function burn(address to) external lock returns (uint amount0, uint amount1) {

Tato funkce je volána při výběru likvidity a je třeba spálit příslušné tokeny likvidity. Měl by být také volán z periferního účtu.

1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // úspora paliva
2 address _token0 = token0; // úspora paliva
3 address _token1 = token1; // úspora paliva
4 uint balance0 = IERC20(_token0).balanceOf(address(this));
5 uint balance1 = IERC20(_token1).balanceOf(address(this));
6 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.

1 bool feeOn = _mintFee(_reserve0, _reserve1);
2 uint _totalSupply = totalSupply; // úspora paliva, musí být definována zde, protože totalSupply se může aktualizovat v _mintFee
3 amount0 = liquidity.mul(balance0) / _totalSupply; // použití zůstatků zajišťuje poměrné rozdělení
4 amount1 = liquidity.mul(balance1) / _totalSupply; // použití zůstatků zajišťuje poměrné rozdělení
5 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.

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 a reserve1 jsou aktuální
9 emit Burn(msg.sender, amount0, amount1, to);
10 }
11
Zobrazit vše

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

směna
1 // tuto funkci na nízké úrovni by měl volat kontrakt, který provádí důležité bezpečnostní kontroly
2 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {

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

1 require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
2 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // úspora paliva
3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
4
5 uint balance0;
6 uint balance1;
7 { // rozsah pro _token{0,1}, vyhýbá se 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. Pokud můžeme omezit počet tak, abychom použili zásobník, spotřebujeme méně paliva. Více podrobností naleznete v Yellow Paper, formální specifikaci Ethereaopens in a new tab, str. 26, rovnice 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); // optimisticky převeďte tokeny
5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimisticky převeďte tokeny

Tento převod je optimistický, protože převádíme dříve, než jsme si jisti, že jsou splněny všechny podmínky. To je v Ethereu v pořádku, protože pokud podmínky nejsou splněny později ve volání, vrátíme se z něj a všech změn, které vytvořilo.

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

Informujte příjemce o směně, pokud je to požadováno.

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

Získejte aktuální zůstatky. Periferní kontrakt nám posílá tokeny předtím, než nás zavolá k provedení směny. To usnadňuje kontraktu kontrolu, zda není podváděn, což je kontrola, která musí proběhnout v hlavním kontraktu (protože nás mohou volat i jiné entity než náš periferní kontrakt).

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 { // rozsah pro reserve{0,1}Adjusted, zabraňuje chybám „stack too deep“
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');

Jedná se o kontrolu integrity, abychom se ujistili, že při směně neztratíme. V žádném případě by směna neměla snížit reserve0*reserve1. Zde také zajišťujeme, aby byl při směně uplatněn poplatek ve výši 0,3 %; před kontrolou hodnoty K vynásobíme oba zůstatky 1000 a odečteme částky vynásobené 3, což znamená, že 0,3 % (3/1000 = 0,003 = 0,3 %) se odečte ze zůstatku před porovnáním jeho hodnoty K s hodnotou K aktuálních rezerv.

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

Aktualizujte reserve0 a reserve1, a pokud je to nutné, akumulátory cen a časové razítko a vygenerujte událost.

Sync nebo Skim

Je možné, že se skutečné zůstatky dostanou mimo synchronizaci s rezervami, které si směnárna párů myslí, že má. Nelze vybírat tokeny bez souhlasu kontraktu, ale vklady jsou jiná věc. Účet může převádět tokeny na směnárnu bez volání mint nebo swap.

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

  • sync, aktualizujte rezervy na aktuální zůstatky
  • skim, vyberte přebytečnou částku. Všimněte si, že jakýkoli účet může volat skim, protože nevíme, kdo tokeny vložil. Tato informace se generuje v události, ale události nejsou dostupné z blockchainu.
1 // vynutit, aby se zůstatky shodovaly s rezervami
2 function skim(address to) external lock {
3 address _token0 = token0; // úspora paliva
4 address _token1 = token1; // úspora paliva
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 // vynutit, aby se rezervy shodovaly se zůstatky
12 function sync() external lock {
13 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
14 }
15}
Zobrazit vše

UniswapV2Factory.sol

Tento kontraktopens in a new tab vytváří párové směnárny.

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;

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

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

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

První, getPair, je mapování, které identifikuje kontrakt párové směnárny 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 i hodnota jsou adresy. Chcete-li získat adresu párové směnárny, která vám umožní převést z tokenA na tokenB, použijete getPair[<adresa tokenuA>][<adresa tokenuB>] (nebo naopak).

Druhá proměnná, allPairs, je pole, které obsahuje všechny adresy párových směnáren vytvořených tímto kontraktem factory. V Ethereu nelze iterovat obsah mapování ani získat seznam všech klíčů, takže tato proměnná je jediný způsob, jak zjistit, které směnárny tento kontrakt factory spravuje.

Poznámka: Důvod, proč nelze iterovat všechny klíče mapování, je ten, že úložiště dat kontraktu je drahé, takže čím méně ho používáme, tím lépe, a čím méně ho měníme, tím lépe. Můžete vytvořit mapování, která podporují iteraciopens in a new tab, ale vyžadují další úložiště pro seznam klíčů. Ve většině aplikací to nepotřebujete.

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

Tato událost se vygeneruje, když je vytvořena nová párová směnárna. Zahrnuje adresy tokenů, adresu párové směnárny a celkový počet směnáren spravovaných kontraktem factory.

1 constructor(address _feeToSetter) public {
2 feeToSetter = _feeToSetter;
3 }

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

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

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

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

Toto je hlavní funkce kontraktu factory, vytvořit párovou směnárnu mezi dvěma tokeny ERC-20. Všimněte si, že tuto funkci může volat kdokoli. Nepotřebujete povolení od Uniswapu k vytvoření nové párové směnárny.

1 require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
2 (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 off-chain (to může být užitečné pro transakce druhé vrstvy). K tomu potřebujeme mít konzistentní pořadí adres tokenů, bez ohledu na pořadí, v jakém jsme je obdrželi, takže je zde seřadíme.

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

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

1 bytes memory bytecode = type(UniswapV2Pair).creationCode;

K vytvoření nového kontraktu potřebujeme kód, který ho vytvoří (jak funkci konstruktoru, tak kód, který zapíše do paměti bytecode EVM skutečného kontraktu). Normálně v Solidity použijeme addr = new <název kontraktu>(<parametry konstruktoru>) a kompilátor se o vše postará za nás, ale abychom měli deterministickou adresu kontraktu, musíme použít opcode CREATE2opens in a new tab. Když byl tento kód napsán, tento opcode ještě nebyl podporován v Solidity, takže bylo nutné ručně získat kód. To již není problém, protože Solidity nyní podporuje CREATE2opens 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 }

Když opcode ještě není podporován v Solidity, můžeme ho zavolat pomocí inline assemblyopens in a new tab.

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

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

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

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

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}
Zobrazit vše

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

UniswapV2ERC20.sol

Tento kontraktopens in a new tab implementuje token likvidity ERC-20. Je podobný kontraktu OpenZeppelin ERC-20, takže vysvětlím pouze část, která je odlišná, funkci 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í tohoto problému jsou meta-transakceopens in a new tab. Vlastník tokenů podepíše transakci, která umožňuje někomu jinému vybrat tokeny off-chain, a pošle ji přes internet příjemci. Příjemce, který má ETH, pak odešle povolení jménem vlastníka.

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;

Tento haš je identifikátor typu transakceopens in a new tab. Jediný, který zde podporujeme, je Permit s těmito parametry.

1 mapping(address => uint) public nonces;

Není možné, aby příjemce zfalšoval digitální podpis. Je však triviální poslat stejnou transakci dvakrát (jedná se o formu replay attackuopens in a new tab). Abychom tomu zabránili, používáme nonceopens in a new tab. Pokud nonce nového Permit není o jedno vyšší než poslední použité, předpokládáme, že je neplatné.

1 constructor() public {
2 uint chainId;
3 assembly {
4 chainId := chainid
5 }

Toto je kód pro získání identifikátoru řetězceopens in a new tab. Používá EVM asemblerový dialekt zvaný Yulopens in a new tab. Všimněte si, že v aktuální verzi Yulu musíte použít chainid(), nikoli 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 }
Zobrazit vše

Vypočítejte oddělovač doményopens in a new tab pro EIP-712.

1 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á relevantní pole a tři skalární hodnoty pro podpisopens in a new tab (v, r a s).

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

Nepřijímejte transakce po termínu.

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(...) je zpráva, kterou očekáváme. Víme, jaká by měla být nonce, takže ji nemusíme dostávat jako parametr.

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

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

Z digestu a podpisu můžeme získat adresu, která ho podepsala, pomocí ecrecoveropens in a new tab.

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

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

Periferní kontrakty

Periferní kontrakty jsou API (rozhraní pro programování aplikací) pro Uniswap. Jsou k dispozici pro externí volání, a to buď z jiných kontraktů, nebo z decentralizovaných aplikací. Mohli byste volat hlavní kontrakty přímo, ale je to složitější a pokud uděláte chybu, můžete ztratit hodnotu. Hlavní kontrakty obsahují pouze testy, aby se ujistily, že nejsou podváděny, nikoli kontroly integrity pro kohokoli jiného. Ty jsou v periferii, takže je lze podle potřeby aktualizovat.

UniswapV2Router01.sol

Tento kontraktopens in a new tab má problémy a již by se neměl používatopens in a new tab. Naštěstí jsou periferní kontrakty bezstavové a nedrží žádná aktiva, takže je snadné je zastarat a navrhnout lidem, aby místo toho používali náhradu, UniswapV2Router02.

UniswapV2Router02.sol

Ve většině případů byste používali Uniswap prostřednictvím tohoto kontraktuopens in a new tab. Jak ho používat, se můžete podívat zdeopens 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';
Zobrazit vše

Většinu z nich jsme již potkali, nebo jsou zcela zřejmé. Jedinou výjimkou je IWETH.sol. Uniswap v2 umožňuje směnárny pro jakýkoli pár tokenů ERC-20, ale samotný ether (ETH) není token ERC-20. Předchází standardu a je přenášen jedinečnými mechanismy. Aby bylo možné používat ETH v kontraktech, které se vztahují na tokeny ERC-20, lidé přišli s kontraktem 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 spálit WETH a získat zpět ETH.

1contract UniswapV2Router02 is IUniswapV2Router02 {
2 using SafeMath for uint;
3
4 address public immutable override factory;
5 address public immutable override WETH;

Směrovač potřebuje vědět, který kontrakt factory použít, a pro transakce, které vyžadují WETH, který 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, aby odkazovaly na méně poctivé kontrakty.

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

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

1 constructor(address _factory, address _WETH) public {
2 factory = _factory;
3 WETH = _WETH;
4 }

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

1 receive() external payable {
2 assert(msg.sender == WETH); // přijmout ETH pouze prostřednictvím fallbacku z kontraktu WETH
3 }

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

Přidat likviditu

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

1
2 // **** PŘIDAT LIKVIDITU ****
3 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.

1 address tokenA,
2 address tokenB,

Jedná se o adresy kontraktů tokenů ERC-20.

1 uint amountADesired,
2 uint amountBDesired,

Jedná se o částky, které chce poskytovatel likvidity vložit. Jsou to také maximální částky A a B, které mají být vloženy.

1 uint amountAMin,
2 uint amountBMin

Jedná se o minimální přijatelné částky k vkladu. Pokud se transakce nemůže uskutečnit s těmito částkami nebo více, vraťte se z ní. Pokud tuto funkci nechcete, stačí zadat nulu.

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

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

ParametrHodnota
amountADesired1000
amountBDesired1000
amountAMin900
amountBMin800

Dokud směnný kurz zůstane mezi 0,9 a 1,25, transakce proběhne. Pokud se směnný kurz dostane mimo tento rozsah, transakce se zruší.

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

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

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

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

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

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

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

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

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

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

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

Pokud je amountBOptimal menší než částka, kterou chce poskytovatel likvidity vložit, znamená to, že token B je aktuálně cennější, než si vkladatel likvidity myslí, takže je vyžadována menší částka.

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

Pokud je optimální částka B větší než požadovaná částka B, znamená to, že tokeny B jsou aktuálně méně cenné, než si vkladatel likvidity myslí, takže je vyžadována 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 dáme dohromady, 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 je směnný kurz, A/B. Pokud x=1, mají stejnou hodnotu a vložíte tisíc od každého. Pokud x=2, A má 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.

Graf

Likviditu byste mohli vložit přímo do hlavního kontraktu (pomocí UniswapV2Pair::mintopens in a new tab), ale hlavní kontrakt kontroluje pouze to, zda není sám podváděn, takže riskujete ztrátu hodnoty, pokud se směnný kurz změní mezi dobou, kdy odešlete transakci, a dobou, kdy je provedena. Pokud použijete periferní kontrakt, ten zjistí částku, kterou byste měli vložit, a okamžitě ji vloží, takže se směnný kurz nezmění a nic neztratíte.

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
Zobrazit vše

Tuto funkci může volat transakce k vkladu likvidity. Většina parametrů je stejná jako v _addLiquidity výše, se dvěma výjimkami:

. to je adresa, která získá nové tokeny likvidity vyražené, aby ukázaly podíl poskytovatele likvidity v poolu . deadline je časový limit transakce

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

Vypočítáme částky, které skutečně vložíme, a pak najdeme adresu poolu likvidity. Abychom ušetřili palivo, neděláme to tak, že bychom se ptali kontraktu factory, ale pomocí funkce knihovny pairFor (viz níže v knihovnách)

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

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

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

Na oplátku dejte na adresu to tokeny likvidity za částečné vlastnictví poolu. Funkce mint hlavního kontraktu zjistí, kolik má navíc tokenů (oproti tomu, kolik měl naposledy, když se změnila likvidita), a podle toho razí likviditu.

1 function addLiquidityETH(
2 address token,
3 uint amountTokenDesired,

Když chce poskytovatel likvidity poskytnout likviditu párové směnárně Token/ETH, existuje několik rozdílů. Kontrakt zařizuje 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 dostupná v 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));
Zobrazit vše

K vkladu ETH kontrakt nejprve zabalí ETH 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 se zabalení ve skutečnosti nestane.

1 liquidity = IUniswapV2Pair(pair).mint(to);
2 // vrátit zbytek eth, pokud nějaký je
3 if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
4 }

Uživatel nám již poslal ETH, takže pokud zbývá nějaký přebytek (protože druhý token je méně cenný, než si uživatel myslel), musíme provést refundaci.

Odebrat likviditu

Tyto funkce odstraní likviditu a vyplatí zpět poskytovatele likvidity.

1 // **** ODEBRAT LIKVIDITU ****
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) {
Zobrazit vše

Nejjednodušší případ odstranění likvidity. Existuje minimální množství každého tokenu, které poskytovatel likvidity souhlasí přijmout, a musí se to stát před termínem.

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

Funkce burn hlavního kontraktu se stará o vrácení tokenů uživateli.

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

Když funkce vrací více hodnot, ale zajímají nás pouze některé z nich, takto získáme pouze tyto hodnoty. Je to poněkud levnější z hlediska paliva než číst hodnotu a nikdy ji nepoužít.

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

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

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

Je v pořádku provést převod nejprve a poté ověřit, zda je legitimní, protože pokud není, vrátíme se ze všech změn stavu.

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 }
Zobrazit vše

Odstranění likvidity pro ETH je téměř stejné, s výjimkou toho, že obdržíme tokeny WETH a poté je vykoupíme za ETH, abychom je vrátili poskytovateli likvidity.

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 }
Zobrazit vše

Tyto funkce přenášejí meta-transakce, aby umožnily uživatelům bez etheru vybrat z poolu pomocí mechanismu povolení.

1
2 // **** ODSTRANIT LIKVIDITU (podpora tokenů s poplatkem za převod) ****
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
Zobrazit vše

Tuto funkci lze použít pro tokeny, které mají poplatky za převod nebo úložiště. Když má token takové poplatky, nemůžeme se spolehnout na funkci removeLiquidity, aby nám řekla, kolik tokenu dostaneme zpět, takže musíme nejprve vybrat a poté získat zůstatek.

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 }
Zobrazit vše

Poslední funkce kombinuje poplatky za úložiště s meta-transakcemi.

Obchodování

1 // **** SMĚNA ****
2 // vyžaduje, aby byla počáteční částka již odeslána prvnímu páru
3 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, které jsou vystaveny obchodníkům.

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

V době, kdy toto píšu, existuje 388 160 tokenů ERC-20opens in a new tab. Kdyby existovala párová směnárna pro každý pár tokenů, bylo by to přes 150 miliard párových směnáren. Celý řetězec má v tuto chvíli pouze 0,1 % tohoto počtu účtůopens in a new tab. Namísto toho funkce směny podporují koncept cesty. 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 synchronizované, 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. Počáteční 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 jako zisk ponechá přibližně 0,61 tokenů B.
  4. Poté obchodník prodá 24,695 tokenů C za 25,305 tokenů A a jako zisk si ponechá přibližně 0,61 tokenů C. Obchodník má také navíc 0,61 tokenů A (25,305, které obchodník nakonec získá, mínus 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
1 (address input, address output) = (path[i], path[i + 1]);
2 (address token0,) = UniswapV2Library.sortTokens(input, output);
3 uint amountOut = amounts[i + 1];

Získejte pár, se kterým právě pracujeme, seřaďte jej (pro použití s párem) a získejte očekávané výstupní množství.

1 (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 očekává párová směnárna.

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

Je to poslední směnárna? Pokud ano, odešlete tokeny obdržené za obchod na cílovou adresu. Pokud ne, odešlete je na další párovou směnárnu.

1
2 IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
3 amount0Out, amount1Out, to, new bytes(0)
4 );
5 }
6 }

Skutečně zavolejte párovou směnárnu, aby směnila tokeny. Nepotřebujeme zpětné volání, které by nás informovalo o směně, takže v tomto poli neposíláme žádné bajty.

1 function swapExactTokensForTokens(

Tuto funkci obchodníci přímo používají ke směně jednoho tokenu za druhý.

1 uint amountIn,
2 uint amountOutMin,
3 address[] calldata path,

Tento parametr obsahuje adresy ERC-20 kontraktů. Jak je 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 kontraktu, volaným přímo uživatelem (pomocí transakce) nebo z jiného kontraktu, pak lze hodnotu parametru převzít přímo z dat volání. Pokud je funkce volána interně, jako _swap výše, pak musí být parametry uloženy v memory. Z pohledu volaného kontraktu je calldata pouze pro čtení.

U skalárních typů, jako je uint nebo address, řeší výběr úložiště kompilátor za nás, ale u polí, která jsou delší a nákladnější, určujeme typ úložiště, které se má použít.

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

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

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

Vypočítejte částku, která se má zakoupit při každé směně. Pokud je výsledek menší než minimum, které je obchodník ochoten přijmout, vraťte transakci zpět.

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

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 v rámci jedné transakce, takže párová směnárna ví, že všechny neočekávané tokeny jsou součástí tohoto převodu.

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 }
Zobrazit vše

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

V obou případech musí obchodník nejprve udělit tomuto perifernímu kontraktu povolení k jejich převodu.

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 // vrátit prachové eth, pokud nějaké zbylo
72 if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
73 }
Zobrazit vše

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 je k ražbě WETH, nebo obdržíme WETH z poslední směnárny na cestě a spálíme je, čímž obchodníkovi vrátíme výsledné ETH.

1 // **** SMĚNA (podpora tokenů s poplatkem za převod) ****
2 // vyžaduje, aby byla počáteční částka již odeslána prvnímu páru
3 function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {

Jedná se o interní funkci pro směnu tokenů, které mají poplatky za převod nebo úložiště, která řeší (tento problémopens 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 { // rozsah pro zamezení chybám příliš hlubokého zásobníku
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);
Zobrazit vše

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

Poznámka: Teoreticky bychom mohli tuto funkci použít místo _swap, ale v určitých případech (například pokud je převod nakonec vrácen, protože na konci není dost na splnění požadovaného minima) by to nakonec stálo více paliva. Tokeny s poplatkem za převod jsou poměrně vzácné, takže i když je musíme zohlednit, není nutné u všech směn předpokládat, že projdou alespoň jednou 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 }
Zobrazit vše

Toto jsou stejné varianty jako u běžných tokenů, ale místo toho volají _swapSupportingFeeOnTransferTokens.

1 // **** FUNKCE KNIHOVNY ****
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}
Zobrazit vše

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

UniswapV2Migrator.sol

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

Knihovny

Knihovna SafeMathopens in a new tab je dobře zdokumentovaná, takže ji zde není třeba dokumentovat.

Math

Tato knihovna obsahuje některé matematické funkce, které se v kódu Solidity běžně nevyžadují, takže nejsou součástí jazyka.

1pragma solidity =0.5.16;
2
3// knihovna pro provádění různých matematických operací
4
5library Math {
6 function min(uint x, uint y) internal pure returns (uint z) {
7 z = x < y ? x : y;
8 }
9
10 // babylónská metoda (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;
Zobrazit vše

Začněte s x jako odhadem, který je vyšší než druhá odmocnina (to je důvod, proč musíme 1-3 považovat za zvláštní případy).

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

Získejte bližší odhad, průměr předchozího odhadu a čísla, jehož druhou odmocninu se snažíme najít, dělený předchozím odhadem. Opakujte, dokud nový odhad nebude nižší než ten stávající. Další podrobnosti naleznete zdeopens in a new tab.

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

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

1 }
2 }
3}

Zlomky s pevnou desetinnou čárkou (UQ112x112)

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

1pragma solidity =0.5.16;
2
3// knihovna pro zpracování binárních čísel s pevnou desetinnou čárkou (https://wikipedia.org/wiki/Q_(number_format))
4
5// rozsah: [0, 2**112 - 1]
6// rozlišení: 1 / 2**112
7
8library UQ112x112 {
9 uint224 constant Q112 = 2**112;
Zobrazit vše

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

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

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

1 // vydělí UQ112x112 hodnotou uint112 a vrátí UQ112x112
2 function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
3 z = x / uint224(y);
4 }
5}

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

UniswapV2Library

Tato knihovna se používá pouze pro periferní kontrakty

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 // vrátí seřazené adresy tokenů, které se používají ke zpracování návratových hodnot z párů seřazených v tomto pořadí
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 }
Zobrazit vše

Seřaďte oba 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é.

1 // vypočítá adresu CREATE2 pro pár bez externích volání
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' // haš init kódu
9 ))));
10 }
Zobrazit vše

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

1 // načte a seřadí rezervy pro pár
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 }

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í je seřadí.

1 // na základě určitého množství aktiva a rezerv páru vrátí ekvivalentní množství druhého aktiva
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 }

Tato funkce vám udává množství tokenu B, které získáte výměnou za token A, pokud se neplatí žádný poplatek. Tento výpočet zohledňuje, že převod mění směnný kurz.

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

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

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 nativně nezpracovává zlomky, takže nemůžeme jen tak vynásobit výstupní částku 0,997. Místo toho vynásobíme čitatel 997 a jmenovatel 1000, čímž dosáhneme stejného efektu.

1 // na základě výstupního množství aktiva a rezerv páru vrátí požadované vstupní množství druhého aktiva
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 }

Tato funkce dělá zhruba totéž, ale získává výstupní částku a poskytuje vstupní.

1
2 // provádí zřetězené výpočty getAmountOut na libovolném počtu párů
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 // provádí zřetězené výpočty getAmountIn na libovolném počtu párů
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}
Zobrazit vše

Tyto dvě funkce zajišťují identifikaci hodnot, když je nutné projít několika párovými směnárnami.

Pomocník pro převody

Tato knihovnaopens in a new tab přidává kontroly úspěšnosti kolem převodů ERC-20 a Etherea, aby se vrácení transakce a vrácená hodnota false považovaly za totéž.

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3pragma solidity >=0.6.0;
4
5// pomocné metody pro interakci s tokeny ERC20 a odesílání ETH, které ne vždy vrací 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
Zobrazit vše

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

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

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ď vrácením (v takovém případě je success false), nebo úspěšným provedením a vrácením hodnoty false (v takovém případě existují výstupní data a pokud je dekódujete jako boolean, získáte 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 }
Zobrazit vše

Tato funkce implementuje funkci transfer standardu ERC-20opens in a new tab, která umožňuje účtu utratit povolenou částku poskytnutou jiným účtem.

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 }
Zobrazit vše

Tato funkce implementuje funkci transferFrom standardu ERC-20opens in a new tab, která umožňuje účtu utratit povolenou částku poskytnutou jiným účtem.

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}

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

Závěr

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

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

Více z mé práce najdete zdeopens in a new tab.

Stránka naposledy aktualizována: 14. února 2026

Byl tento tutoriál užitečný?