Перейти до основного контенту

Огляд контракту Uniswap v2

мова програмування
Середнячок
Ori Pomerantz
1 травня 2021 р.
54 читається за хвилину

Вступ

Uniswap v2opens in a new tab може створювати ринок обміну між будь-якими двома токенами ERC-20. У цій статті ми розглянемо вихідний код контрактів, які реалізують цей протокол, і з’ясуємо, чому вони написані саме так.

Що робить Uniswap?

Загалом існує два типи користувачів: постачальники ліквідності та трейдери.

Постачальники ліквідності надають пулу два токени, які можна обміняти (ми їх називатимемо Token0 та Token1). Натомість вони отримують третій токен, що представляє часткове володіння пулом і називається токеном ліквідності.

Трейдери надсилають один тип токена в пул і отримують інший (наприклад, надсилають Token0 і отримують Token1) з пулу, наданого постачальниками ліквідності. Обмінний курс визначається відносною кількістю токенів Token0 і Token1, які є в пулі. Крім того, пул бере невеликий відсоток як винагороду для пулу ліквідності.

Коли постачальники ліквідності хочуть повернути свої активи, вони можуть спалити токени пулу та отримати назад свої токени, включно з їхньою часткою винагороди.

Натисніть тут, щоб переглянути детальніший описopens in a new tab.

Чому v2? Чому не v3?

Uniswap v3opens in a new tab — це оновлення, яке є набагато складнішим, ніж v2. Легше спочатку вивчити v2, а вже потім перейти до v3.

Основні та периферійні контракти

Uniswap v2 складається з двох компонентів: ядра та периферії. Такий поділ дає змогу зробити основні контракти, які зберігають активи й тому мають бути безпечними, простішими та легшими для аудиту. Весь додатковий функціонал, необхідний трейдерам, може бути наданий периферійними контрактами.

Потоки даних і керування

Це потік даних і керування, що відбувається під час виконання трьох основних дій Uniswap:

  1. Обмін між різними токенами
  2. Додавання ліквідності на ринок і отримання токенів ліквідності ERC-20 для парного обміну як винагороди
  3. Спалювання токенів ліквідності ERC-20 та отримання назад токенів ERC-20, які парна біржа дозволяє трейдерам обмінювати

Обмін

Це найпоширеніший потік, який використовують трейдери:

Викликач

  1. Надайте периферійному рахунку дозвіл на суму, що підлягає обміну.
  2. Викличте одну з багатьох функцій обміну периферійного контракту (яку саме — залежить від того, чи залучено ETH, чи вказує трейдер суму токенів для депозиту чи суму токенів для повернення тощо). Кожна функція обміну приймає path — масив бірж для проходження.

У периферійному контракті (UniswapV2Router02.sol)

  1. Визначте суми, якими потрібно торгувати на кожній біржі вздовж шляху.
  2. Ітерація шляхом. Для кожного обміну на шляху він надсилає вхідний токен, а потім викликає функцію swap біржі. У більшості випадків адресою призначення для токенів є наступна парна біржа на шляху. На останній біржі це адреса, надана трейдером.

В основному контракті (UniswapV2Pair.sol) {#in-the-core-contract-uniswapv2pairsol-2}5. Перевірте, що основний контракт не обманюють і що він може підтримувати достатню ліквідність після обміну.

  1. Перевірте, скільки додаткових токенів ми маємо крім відомих резервів. Ця сума — це кількість вхідних токенів, які ми отримали для обміну.
  2. Надішліть вихідні токени до місця призначення.
  3. Викличте _update, щоб оновити суми резервів

Назад у периферійний контракт (UniswapV2Router02.sol)

  1. Виконайте будь-які необхідні очищення (наприклад, спаліть токени WETH, щоб повернути ETH для відправки трейдеру)

Додати ліквідність

Викликач

  1. Надайте периферійному рахунку дозвіл на суми, які потрібно додати до пулу ліквідності.
  2. Викличте одну з функцій addLiquidity периферійного контракту.

У периферійному контракті (UniswapV2Router02.sol)

  1. Створіть новий парний обмін за потреби
  2. Якщо існує парний обмін, розрахуйте кількість токенів, які потрібно додати. Це має бути ідентичне значення для обох токенів, тобто однакове співвідношення нових токенів до наявних.
  3. Перевірте, чи прийнятні суми (викликачі можуть вказати мінімальну суму, нижче якої вони не бажають додавати ліквідність)
  4. Викличте основний контракт.

В основному контракті (UniswapV2Pair.sol)

  1. Карбуйте токени ліквідності та надсилайте їх викликачу
  2. Викличте _update, щоб оновити суми резервів

Вилучення ліквідності

Викликач

  1. Надайте периферійному рахунку дозвіл на спалювання токенів ліквідності в обмін на базові токени.
  2. Викличте одну з функцій removeLiquidity периферійного контракту.

У периферійному контракті (UniswapV2Router02.sol)

  1. Надішліть токени ліквідності на парний обмін

В основному контракті (UniswapV2Pair.sol)

  1. Надішліть на адресу призначення базові токени пропорційно до спалених токенів. Наприклад, якщо в пулі є 1000 токенів A, 500 токенів B і 90 токенів ліквідності, і ми отримуємо 9 токенів для спалювання, ми спалюємо 10 % токенів ліквідності, тому повертаємо користувачеві 100 токенів A і 50 токенів B.
  2. Спаліть токени ліквідності
  3. Викличте _update, щоб оновити суми резервів

Основні контракти

Це безпечні контракти, які зберігають ліквідність.

UniswapV2Pair.sol

Цей контрактopens in a new tab реалізує фактичний пул, що обмінює токени. Це основний функціонал Uniswap.

1pragma solidity =0.5.16;
2
3import './interfaces/IUniswapV2Pair.sol';
4import './UniswapV2ERC20.sol';
5import './libraries/Math.sol';
6import './libraries/UQ112x112.sol';
7import './interfaces/IERC20.sol';
8import './interfaces/IUniswapV2Factory.sol';
9import './interfaces/IUniswapV2Callee.sol';
Показати все

Це всі інтерфейси, про які контракт має знати, або тому, що контракт їх реалізує (IUniswapV2Pair і UniswapV2ERC20), або тому, що він викликає контракти, які їх реалізують.

1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {

Цей контракт успадковує UniswapV2ERC20, який надає функції ERC-20 для токенів ліквідності.

1 using SafeMath for uint;

Бібліотека SafeMathopens in a new tab використовується для запобігання переповненням (overflows) та антипереповненням (underflows). Це важливо, оскільки інакше ми можемо опинитися в ситуації, коли значення має бути -1, а натомість дорівнює 2^256-1.

1 using UQ112x112 for uint224;

Багато обчислень у контракті пулу вимагають дробів. Однак дроби не підтримуються EVM. Рішення, яке знайшов Uniswap, полягає у використанні 224-бітних значень, зі 112 бітами для цілої частини та 112 бітами для дробу. Отже, 1.0 представлено як 2^112, 1.5 представлено як 2^112 + 2^111 тощо.

Більш детальна інформація про цю бібліотеку доступна пізніше в цьому документі.

Змінні

1 uint public constant MINIMUM_LIQUIDITY = 10**3;

Щоб уникнути випадків ділення на нуль, існує мінімальна кількість токенів ліквідності, яка завжди є (але належить нульовому рахунку). Це число — MINIMUM_LIQUIDITY, тисяча.

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

Це селектор ABI для функції передачі ERC-20. Він використовується для передачі токенів ERC-20 на двох рахунках токенів.

1 address public factory;

Це фабричний контракт, який створив цей пул. Кожен пул — це біржа між двома токенами ERC-20, а фабрика — це центральна точка, яка з’єднує всі ці пули.

1 address public token0;
2 address public token1;

Це адреси контрактів для двох типів токенів ERC-20, які можуть бути обміняні цим пулом.

1 uint112 private reserve0; // використовує один слот сховища, доступний через getReserves
2 uint112 private reserve1; // використовує один слот сховища, доступний через getReserves

Резерви, які пул має для кожного типу токена. Ми припускаємо, що обидва представляють однакову вартість, і тому кожен token0 вартий reserve1/reserve0 токенів token1.

1 uint32 private blockTimestampLast; // використовує один слот сховища, доступний через getReserves

Часова мітка для останнього блоку, в якому відбувся обмін, використовується для відстеження курсів обміну в часі.

Однією з найбільших витрат на газ у контрактах Ethereum є сховище, яке зберігається від одного виклику контракту до наступного. Кожна клітинка сховища має довжину 256 біт. Отже, три змінні, reserve0, reserve1 і blockTimestampLast, виділяються таким чином, що одне значення сховища може містити всі три (112+112+32=256).

1 uint public price0CumulativeLast;
2 uint public price1CumulativeLast;

Ці змінні містять сукупні вартості для кожного токена (кожен у перерахунку на інший). Їх можна використовувати для обчислення середнього обмінного курсу за певний проміжок часу.

1 uint public kLast; // reserve0 * reserve1, станом на момент одразу після останньої події, пов'язаної з ліквідністю

Спосіб, у який парний обмін визначає обмінний курс між token0 і token1, полягає в тому, щоб підтримувати добуток двох резервів постійним під час торгів. kLast — це значення. Він змінюється, коли постачальник ліквідності вносить або знімає токени, і трохи збільшується через ринкову комісію в розмірі 0,3%.

Ось простий приклад. Зауважте, що для простоти таблиця містить лише три цифри після десяткової коми, і ми ігноруємо торгову комісію в розмірі 0,3%, тому цифри неточні.

Подіяreserve0reserve1reserve0 * reserve1Середній обмінний курс (token1 / token0)
Початкове налаштування1 000,0001 000,0001 000 000
Трейдер A обмінює 50 token0 на 47,619 token11 050,000952,3811 000 0000,952
Трейдер B обмінює 10 token0 на 8,984 token11 060,000943,3961 000 0000,898
Трейдер C обмінює 40 token0 на 34,305 token11 100,000909,0901 000 0000,858
Трейдер D обмінює 100 token1 на 109,01 token0990,9901 009,0901 000 0000,917
Трейдер E обмінює 10 token0 на 10,079 token11 000,990999,0101 000 0001,008

Оскільки трейдери надають більше token0, відносна вартість token1 зростає, і навпаки, залежно від попиту та пропозиції.

Блокування

1 uint private unlocked = 1;

Існує клас вразливостей безпеки, що ґрунтуються на зловживанні повторним входом (reentrancy abuse)opens in a new tab. Uniswap має передавати довільні токени ERC-20, що означає виклик контрактів ERC-20, які можуть намагатися зловживати ринком Uniswap, що їх викликає. Маючи змінну unlocked як частину контракту, ми можемо запобігти виклику функцій під час їх виконання (в межах однієї транзакції).

1 modifier lock() {

Ця функція є модифікаторомopens in a new tab — функцією, яка обгортає звичайну функцію, щоб певним чином змінити її поведінку.

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

Якщо unlocked дорівнює одиниці, встановіть його на нуль. Якщо він уже дорівнює нулю, скасуйте виклик, зробіть його невдалим.

1 _;

У модифікаторі _; — це початковий виклик функції (з усіма параметрами). Тут це означає, що виклик функції відбувається, тільки якщо unlocked дорівнював одиниці під час виклику, і поки він виконується, значення unlocked дорівнює нулю.

1 unlocked = 1;
2 }

Після повернення основної функції зніміть блокування.

Різне функції

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

Ця функція надає тим, хто її викликає, поточний стан біржі. Зверніть увагу, що функції Solidity можуть повертати кілька значеньopens in a new tab.

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

Ця внутрішня функція переказує певну кількість токенів ERC20 з біржі комусь іншому. SELECTOR вказує, що функція, яку ми викликаємо, — це transfer(address,uint) (див. визначення вище).

Щоб не імпортувати інтерфейс для функції токена, ми створюємо виклик «вручну», використовуючи одну з функцій ABIopens in a new tab.

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

Існує два способи, якими виклик переказу ERC-20 може повідомити про збій:

  1. Скасування. Якщо виклик зовнішнього контракту скасовується, тоді логічне значення, що повертається, — false.
  2. Завершитися нормально, але повідомити про збій. У цьому випадку буфер значення, що повертається, має ненульову довжину, і під час декодування як логічне значення він дорівнює false.

Якщо трапляється будь-яка з цих умов, відбувається скасування.

Події

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

Ці дві події генеруються, коли постачальник ліквідності або вносить ліквідність (Mint), або виводить її (Burn). У будь-якому випадку кількість токенів token0 і token1, які вносяться або виводяться, є частиною події, так само як і ідентифікатор облікового запису, який нас викликав (sender). У разі виведення коштів подія також включає ціль, яка отримала токени (to), і вона може не збігатися з відправником.

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

Ця подія генерується, коли трейдер обмінює один токен на інший. Знову ж таки, відправник і одержувач можуть не збігатися. Кожен токен може бути або надісланий на біржу, або отриманий з неї.

1 event Sync(uint112 reserve0, uint112 reserve1);

Нарешті, подія Sync генерується кожного разу, коли токени додаються або виводяться, незалежно від причини, щоб надати останню інформацію про резерви (і, отже, обмінний курс).

Функції налаштування

Передбачається, що ці функції викликаються один раз під час налаштування нової біржової пари.

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

Конструктор гарантує, що ми будемо відстежувати адресу фабрики, яка створила пару. Ця інформація потрібна для initialize та для комісії фабрики (якщо така існує).

1 // викликається один раз фабрикою під час розгортання
2 function initialize(address _token0, address _token1) external {
3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // достатня перевірка
4 token0 = _token0;
5 token1 = _token1;
6 }

Ця функція дозволяє фабриці (і тільки фабриці) вказати два токени ERC-20, якими обмінюватиметься ця пара.

Внутрішні функції оновлення

_update
1 // оновлює резерви та, під час першого виклику за блок, накопичувачі ціни
2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {

Ця функція викликається кожного разу, коли токени вносяться або виводяться.

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

Якщо balance0 або balance1 (uint256) більше за uint112(-1) (=2^112-1) (тобто відбувається переповнення та повернення до 0 під час конвертації в uint112), виконання _update припиняється, щоб запобігти переповненню. Для звичайного токена, який можна розділити на 10^18 одиниць, це означає, що кожна біржа обмежена приблизно 5.1*10^15 кожного з токенів. Досі це не було проблемою.

1 uint32 blockTimestamp = uint32(block.timestamp % 2**32);
2 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // переповнення є бажаним
3 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {

Якщо час, що минув, не дорівнює нулю, це означає, що це перша транзакція обміну в цьому блоці. У такому випадку нам потрібно оновити накопичувачі вартості.

1 // * ніколи не переповнюється, а для + переповнення є бажаним
2 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
3 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
4 }

Кожен акумулятор вартості оновлюється з останньою вартістю (резерв іншого токена/резерв цього токена), помноженою на час, що минув, у секундах. Щоб отримати середню ціну, ви зчитуєте сукупну ціну в двох точках часу і ділите на різницю в часі між ними. Наприклад, припустимо таку послідовність подій:

Подіяreserve0reserve1часова міткаГраничний обмінний курс (reserve1 / reserve0)price0CumulativeLast
Початкове налаштування1 000,0001 000,0005 0001,0000
Трейдер А вносить 50 token0 і отримує назад 47,619 token11 050,000952,3815 0200,90720
Трейдер B вносить 10 token0 і отримує назад 8,984 token11 060,000943,3965 0300,89020+10*0,907 = 29,07
Трейдер C вносить 40 token0 і отримує назад 34,305 token11 100,000909,0905 1000,82629,07+70*0,890 = 91,37
Трейдер D вносить 100 token1 і отримує назад 109,01 token0990,9901 009,0905 1101,01891,37+10*0,826 = 99,63
Трейдер E вносить 10 token0 і отримує назад 10,079 token11 000,990999,0105 1500,99899,63+40*1,1018 = 143,702

Припустимо, ми хочемо обчислити середню ціну Token0 між часовими мітками 5030 і 5150. Різниця у значенні price0Cumulative становить 143,702-29,07=114,632. Це середнє значення за дві хвилини (120 секунд). Отже, середня ціна становить 114,632/120 = 0,955.

Цей розрахунок ціни є причиною того, що нам потрібно знати старі розміри резервів.

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

Нарешті, оновіть глобальні змінні та випустіть подію Sync.

_mintFee
1 // якщо комісія увімкнена, карбуємо ліквідність, еквівалентну 1/6 росту в sqrt(k)
2 function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {

У Uniswap 2.0 трейдери сплачують комісію 0,30% за використання ринку. Більшість цієї комісії (0,25% від торгівлі) завжди йде до постачальників ліквідності. Решта 0,05 % може надходити або постачальникам ліквідності, або на адресу, вказану фабрикою, як комісія протоколу, що є платою для Uniswap за їхні зусилля з розробки.

Щоб зменшити обчислення (а отже, і витрати на газ), ця комісія розраховується лише тоді, коли ліквідність додається або видаляється з пулу, а не при кожній транзакції.

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

Прочитайте адресу призначення комісії фабрики. Якщо вона дорівнює нулю, то комісії протоколу немає і не потрібно її розраховувати.

1 uint _kLast = kLast; // економія газу

Змінна стану kLast знаходиться в сховищі, тому вона матиме значення між різними викликами контракту. Доступ до сховища набагато дорожчий, ніж доступ до енергозалежної пам’яті, яка звільняється після завершення виклику функції контракту, тому ми використовуємо внутрішню змінну для економії газу.

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

Постачальники ліквідності отримують свою частку просто за рахунок зростання вартості їхніх токенів ліквідності. Але комісія протоколу вимагає, щоб нові токени ліквідності були викарбувані та надані на адресу feeTo.

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

Якщо є нова ліквідність, на яку можна збирати комісію протоколу. Ви можете побачити функцію квадратного кореня пізніше в цій статті.

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

Це складне обчислення комісій пояснюється в білій книзіopens in a new tab на сторінці 5. Ми знаємо, що між часом, коли було розраховано kLast, і теперішнім моментом, ліквідність не додавалася і не видалялася (оскільки ми виконуємо цей розрахунок щоразу, коли ліквідність додається або видаляється, до її фактичної зміни), тому будь-яка зміна в reserve0 * reserve1 має походити від комісій за транзакції (без них ми б тримали reserve0 * reserve1 постійним).

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

Використовуйте функцію UniswapV2ERC20._mint, щоб фактично створити додаткові токени ліквідності та призначити їх feeTo.

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

Якщо комісія не встановлена, встановіть kLast на нуль (якщо він ще не встановлений). Коли цей контракт був написаний, існувала функція повернення газуopens in a new tab, яка заохочувала контракти зменшувати загальний розмір стану Ethereum шляхом обнулення сховища, яке їм не було потрібне. Цей код отримує це відшкодування, коли це можливо.

Зовнішньо доступні функції

Зверніть увагу, що хоча будь-яка транзакція або контракт може викликати ці функції, вони призначені для виклику з периферійного контракту. Якщо ви викликаєте їх безпосередньо, ви не зможете обдурити парний обмін, але можете втратити вартість через помилку.

карбування
1 // цю низькорівневу функцію слід викликати з контракту, який виконує важливі перевірки безпеки
2 function mint(address to) external lock returns (uint liquidity) {

Ця функція викликається, коли постачальник ліквідності додає ліквідність до пулу. Вона карбує додаткові токени ліквідності як винагороду. Її слід викликати з периферійного контракту, який викликає її після додавання ліквідності в тій самій транзакції (щоб ніхто інший не міг подати транзакцію, яка вимагає нової ліквідності, раніше за законного власника).

1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // економія газу

Це спосіб читання результатів функції Solidity, яка повертає кілька значень. Ми відкидаємо останні повернуті значення, часову мітку блоку, тому що вони нам не потрібні.

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

Отримайте поточні баланси та подивіться, скільки було додано кожного типу токена.

1 bool feeOn = _mintFee(_reserve0, _reserve1);

Розрахуйте комісії протоколу для збору, якщо такі є, і викарбуйте відповідні токени ліквідності. Оскільки параметри для _mintFee — це старі значення резерву, комісія розраховується точно на основі змін пулу, що відбулися через комісії.

1 uint _totalSupply = totalSupply; // економія газу, має бути визначено тут, оскільки totalSupply може оновлюватися в _mintFee
2 if (_totalSupply == 0) {
3 liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
4 _mint(address(0), MINIMUM_LIQUIDITY); // назавжди заблокувати перші MINIMUM_LIQUIDITY токени

Якщо це перший депозит, створіть токени MINIMUM_LIQUIDITY і надішліть їх на нульову адресу, щоб заблокувати їх. Вони ніколи не можуть бути викуплені, що означає, що пул ніколи не буде повністю спорожнений (це рятує нас від ділення на нуль у деяких місцях). Значення MINIMUM_LIQUIDITY становить тисячу, що, враховуючи, що більшість ERC-20 поділяються на одиниці 10^-18-ї частини токена, як ETH ділиться на wei, становить 10^-15 від вартості одного токена. Невисока вартість.

Під час першого депозиту ми не знаємо відносної вартості двох токенів, тому ми просто множимо суми і беремо квадратний корінь, припускаючи, що депозит надає нам однакову вартість в обох токенах.

Ми можемо довіряти цьому, оскільки в інтересах вкладника надавати рівну вартість, щоб уникнути втрати вартості через арбітраж. Припустимо, що вартість двох токенів однакова, але наш вкладник вніс у чотири рази більше Token1, ніж Token0. Трейдер може скористатися тим фактом, що парна біржа вважає Token0 ціннішим, щоб витягти з нього вартість.

Подіяreserve0reserve1reserve0 * reserve1Вартість пулу (reserve0 + reserve1)
Початкове налаштування83225640
Трейдер вносить 8 токенів Token0, отримує назад 16 Token1161625632

Як бачите, трейдер заробив додаткові 8 токенів, які походять від зменшення вартості пулу, що шкодить вкладнику, який ним володіє.

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

З кожним наступним депозитом ми вже знаємо обмінний курс між двома активами, і ми очікуємо, що постачальники ліквідності нададуть однакову вартість в обох. Якщо вони цього не зроблять, ми даємо їм токени ліквідності на основі меншої вартості, яку вони надали, як покарання.

Незалежно від того, чи це початковий депозит, чи наступний, кількість токенів ліквідності, які ми надаємо, дорівнює квадратному кореню зміни в reserve0*reserve1, і вартість токена ліквідності не змінюється (якщо ми не отримаємо депозит, який не має однакових значень обох типів, у цьому випадку «штраф» розподіляється). Ось ще один приклад з двома токенами, які мають однакове значення, з трьома вдалими депозитами та одним невдалим (внесено тільки один тип токена, тому він не надає ніяких токенів ліквідності).

Подіяreserve0reserve1reserve0 * reserve1Вартість пулу (reserve0 + reserve1)Токени ліквідності, викарбувані для цього депозитуЗагальна кількість токенів ліквідностівартість кожного токена ліквідності
Початкове налаштування8,0008,0006416,000882,000
Депозит по чотири кожного типу12,00012,00014424,0004122,000
Депозит по два кожного типу14,00014,00019628,0002142,000
Депозит з неоднаковою вартістю18,00014,00025232,000014~2,286
Після арбітражу~15,874~15,874252~31,748014~2,267
1 }
2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
3 _mint(to, liquidity);

Використовуйте функцію UniswapV2ERC20._mint, щоб фактично створити додаткові токени ліквідності та передати їх на правильний рахунок.

1
2 _update(balance0, balance1, _reserve0, _reserve1);
3 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 і reserve1 оновлені
4 emit Mint(msg.sender, amount0, amount1);
5 }

Оновіть змінні стану (reserve0, reserve1 і, за потреби, kLast) і випустіть відповідну подію.

спалювання
1 // цю низькорівневу функцію слід викликати з контракту, який виконує важливі перевірки безпеки
2 function burn(address to) external lock returns (uint amount0, uint amount1) {

Ця функція викликається, коли ліквідність виводиться, і відповідні токени ліквідності потрібно спалити. Її також слід викликати з периферійного рахунку.

1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // економія газу
2 address _token0 = token0; // економія газу
3 address _token1 = token1; // економія газу
4 uint balance0 = IERC20(_token0).balanceOf(address(this));
5 uint balance1 = IERC20(_token1).balanceOf(address(this));
6 uint liquidity = balanceOf[address(this)];

Периферійний контракт передав ліквідність для спалювання в цей контракт перед викликом. Таким чином ми знаємо, скільки ліквідності спалити, і можемо переконатися, що вона буде спалена.

1 bool feeOn = _mintFee(_reserve0, _reserve1);
2 uint _totalSupply = totalSupply; // економія газу, має бути визначено тут, оскільки totalSupply може оновлюватися в _mintFee
3 amount0 = liquidity.mul(balance0) / _totalSupply; // використання балансів забезпечує пропорційний розподіл
4 amount1 = liquidity.mul(balance1) / _totalSupply; // використання балансів забезпечує пропорційний розподіл
5 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');

Постачальник ліквідності отримує однакову вартість обох токенів. Таким чином ми не змінюємо обмінний курс.

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 і reserve1 оновлені
9 emit Burn(msg.sender, amount0, amount1, to);
10 }
11
Показати все

Решта функції burn є дзеркальним відображенням функції mint вище.

обмін
1 // цю низькорівневу функцію слід викликати з контракту, який виконує важливі перевірки безпеки
2 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {

Цю функцію також слід викликати з периферійного контракту.

1 require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
2 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // економія газу
3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
4
5 uint balance0;
6 uint balance1;
7 { // область видимості для _token{0,1}, уникає помилок «стек занадто глибокий»

Локальні змінні можна зберігати або в пам'яті, або, якщо їх не надто багато, безпосередньо в стеку. Якщо ми можемо обмежити їх кількість, щоб використовувати стек, ми використовуємо менше газу. Для отримання додаткової інформації див. жовту книгу, офіційні специфікації Ethereumopens in a new tab, с. 26, рівняння 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); // оптимістично передаємо токени
5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // оптимістично передаємо токени

Цей переказ є оптимістичним, оскільки ми передаємо кошти до того, як переконаємося, що всі умови виконані. Це нормально в Ethereum, тому що якщо умови не будуть виконані пізніше під час виклику, ми скасуємо його та будь-які зміни, які він створив.

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

Повідомте одержувача про обмін, якщо це необхідно.

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

Отримати поточні баланси. Периферійний контракт надсилає нам токени перед тим, як викликати нас для обміну. Це полегшує контракту перевірку того, що його не обманюють — перевірка, яка має відбуватися в основному контракті (оскільки нас можуть викликати інші суб’єкти, крім нашого периферійного контракту).

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 { // область видимості для reserve{0,1}Adjusted, уникає помилок «стек занадто глибокий»
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');

Це перевірка на адекватність, щоб переконатися, що ми не втрачаємо від обміну. За жодних обставин обмін не повинен зменшувати reserve0*reserve1. Тут також ми забезпечуємо, щоб під час обміну стягувалася комісія в розмірі 0,3%; перш ніж перевіряти значення K на адекватність, ми множимо обидва баланси на 1000, віднімаючи суми, помножені на 3, це означає, що 0,3% (3/1000 = 0,003 = 0,3%) віднімається з балансу перед порівнянням його значення K зі значенням K поточних резервів.

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

Оновіть reserve0 і reserve1, а за необхідності — акумулятори цін і часову мітку, та випустіть подію.

Синхронізація або зняття

Можливо, що реальні баланси розсинхронізуються з резервами, які, на думку парної біржі, вона має. Немає способу вивести токени без згоди контракту, але депозити — це інша справа. Рахунок може переказувати токени на біржу, не викликаючи ні mint, ні swap.

У цьому випадку є два рішення:

  • sync — оновити резерви до поточних балансів
  • skim — вивести зайву суму. Зверніть увагу, що будь-який рахунок може викликати skim, оскільки ми не знаємо, хто вніс токени. Ця інформація випромінюється в події, але події не доступні з блокчейну.
1 // примусово змусити баланси відповідати резервам
2 function skim(address to) external lock {
3 address _token0 = token0; // економія газу
4 address _token1 = token1; // економія газу
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 // примусово змусити резерви відповідати балансам
12 function sync() external lock {
13 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
14 }
15}
Показати все

UniswapV2Factory.sol

Цей контрактopens in a new tab створює парні обміни.

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;

Ці змінні стану необхідні для реалізації комісії протоколу (див. білу книгуopens in a new tab, с. 5). Адреса feeTo накопичує токени ліквідності для комісії протоколу, а feeToSetter — це адреса, якій дозволено змінювати feeTo на іншу адресу.

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

Ці змінні відстежують пари, тобто обміни між двома типами токенів.

Перша, getPair, — це відображення, яке ідентифікує контракт парного обміну на основі двох токенів ERC-20, які він обмінює. Токени ERC-20 ідентифікуються за адресами контрактів, які їх реалізують, тому ключі та значення є адресами. Щоб отримати адресу парного обміну, яка дозволяє конвертувати tokenA в tokenB, ви використовуєте getPair[<адреса tokenA>][<адреса tokenB>] (або навпаки).

Друга змінна, allPairs, — це масив, що містить усі адреси парних бірж, створених цією фабрикою. В Ethereum ви не можете ітерувати по вмісту відображення або отримати список усіх ключів, тому ця змінна є єдиним способом дізнатися, якими біржами керує ця фабрика.

Примітка: причина, чому ви не можете ітерувати по всіх ключах відображення, полягає в тому, що зберігання даних контракту дороге, тому чим менше ми його використовуємо і чим рідше змінюємо, тим краще. Ви можете створювати відображення, які підтримують ітераціюopens in a new tab, але вони вимагають додаткового сховища для списку ключів. У більшості застосунків це не потрібно.

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

Ця подія випромінюється, коли створюється новий парний обмін. Вона включає адреси токенів, адресу парної біржі та загальну кількість бірж, керованих фабрикою.

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

Єдине, що робить конструктор, — це вказує feeToSetter. Фабрики починають без комісії, і тільки feeSetter може це змінити.

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

Ця функція повертає кількість пар обміну.

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

Це основна функція фабрики — створення парного обміну між двома токенами ERC-20. Зверніть увагу, що будь-хто може викликати цю функцію. Вам не потрібен дозвіл від Uniswap, щоб створити новий парний обмін.

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

Ми хочемо, щоб адреса нової біржі була детермінованою, щоб її можна було обчислити заздалегідь поза ланцюгом (це може бути корисно для транзакцій рівня 2). Для цього нам потрібен послідовний порядок адрес токенів, незалежно від порядку, в якому ми їх отримали, тому ми сортуємо їх тут.

1 require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
2 require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // достатньо однієї перевірки

Великі пули ліквідності кращі за малі, оскільки вони мають більш стабільні ціни. Ми не хочемо мати більше одного пулу ліквідності на пару токенів. Якщо біржа вже існує, немає потреби створювати ще одну для тієї ж пари.

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

Щоб створити новий контракт, нам потрібен код, який його створює (і функція конструктора, і код, який записує в пам’ять байт-код EVM фактичного контракту). Зазвичай у Solidity ми просто використовуємо addr = new <name of contract>(<constructor parameters>), і компілятор дбає про все за нас, але щоб мати детерміновану адресу контракту, нам потрібно використовувати опкод CREATE2opens in a new tab. Коли цей код був написаний, цей опкод ще не підтримувався Solidity, тому було необхідно вручну отримувати код. Це більше не є проблемою, оскільки Solidity тепер підтримує 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 }

Коли опкод ще не підтримується Solidity, ми можемо викликати його за допомогою вбудованого асемблераopens in a new tab.

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

Викличте функцію initialize, щоб повідомити новій біржі, які два токени вона обмінює.

1 getPair[token0][token1] = pair;
2 getPair[token1][token0] = pair; // заповнити відображення у зворотному напрямку
3 allPairs.push(pair);
4 emit PairCreated(token0, token1, pair, allPairs.length);
5 }

Збережіть інформацію про нову пару у змінних стану та випустіть подію, щоб повідомити світу про нову парну біржу.

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}
Показати все

Ці дві функції дозволяють feeSetter контролювати одержувача комісії (якщо такий є) та змінювати feeSetter на нову адресу.

UniswapV2ERC20.sol

Цей контрактopens in a new tab реалізує токен ліквідності ERC-20. Він схожий на контракт ERC-20 від OpenZeppelin, тому я поясню лише ту частину, яка відрізняється — функціонал permit.

Транзакції в мережі Ethereum коштують ефір (ETH), що є еквівалентом реальних грошей. Якщо у вас є токени ERC-20, але немає ETH, ви не можете надсилати транзакції, тому ви нічого не можете з ними зробити. Одним із рішень цієї проблеми є метатранзакціїopens in a new tab. Власник токенів підписує транзакцію, яка дозволяє комусь іншому вивести токени поза ланцюгом, і надсилає її через Інтернет одержувачу. Одержувач, у якого є ETH, потім подає дозвіл від імені власника.

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;

Цей хеш є ідентифікатором для типу транзакціїopens in a new tab. Єдиний, який ми тут підтримуємо, — це Permit з цими параметрами.

1 mapping(address => uint) public nonces;

Для одержувача неможливо підробити цифровий підпис. Однак тривіально надіслати ту саму транзакцію двічі (це форма атаки повторного відтворенняopens in a new tab). Щоб запобігти цьому, ми використовуємо nonceopens in a new tab. Якщо nonce нового Permit не на одиницю більший за останній використаний, ми вважаємо його недійсним.

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

Це код для отримання ідентифікатора ланцюгаopens in a new tab. Він використовує асемблерний діалект EVM під назвою Yulopens in a new tab. Зверніть увагу, що в поточній версії Yul ви повинні використовувати chainid(), а не 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 }
Показати все

Обчисліть роздільник доменуopens in a new tab для EIP-712.

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

Це функція, яка реалізує дозволи. Вона отримує як параметри відповідні поля та три скалярні значення для підписуopens in a new tab (v, r та s).

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

Не приймати транзакції після закінчення терміну дії.

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(...) — це повідомлення, яке ми очікуємо отримати. Ми знаємо, яким має бути nonce, тому нам не потрібно отримувати його як параметр.

Алгоритм підпису Ethereum очікує отримати 256 біт для підпису, тому ми використовуємо хеш-функцію keccak256.

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

З дайджесту та підпису ми можемо отримати адресу, яка його підписала, використовуючи ecrecoveropens in a new tab.

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

Якщо все гаразд, розглядати це як схвалення ERC-20opens in a new tab.

Периферійні контракти

Периферійні контракти — це API (інтерфейс прикладного програмування) для Uniswap. Вони доступні для зовнішніх викликів, або з інших контрактів, або з децентралізованих застосунків. Ви могли б викликати основні контракти безпосередньо, але це складніше, і ви можете втратити кошти, якщо припуститеся помилки. Основні контракти містять лише тести, щоб переконатися, що їх не обманюють, а не перевірки на адекватність для будь-кого іншого. Вони знаходяться в периферії, щоб їх можна було оновлювати за потреби.

UniswapV2Router01.sol

Цей контрактopens in a new tab має проблеми, і його більше не слід використовуватиopens in a new tab. На щастя, периферійні контракти не мають стану і не зберігають жодних активів, тому його легко зняти з експлуатації та запропонувати людям використовувати натомість заміну, UniswapV2Router02.

UniswapV2Router02.sol

У більшості випадків ви будете використовувати Uniswap через цей контрактopens in a new tab. Ви можете побачити, як його використовувати, тутopens in a new tab.

1pragma solidity =0.6.6;
2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
4import '@uniswap/lib/contracts/libraries/TransferHelper.sol';
5
6import './interfaces/IUniswapV2Router02.sol';
7import './libraries/UniswapV2Library.sol';
8import './libraries/SafeMath.sol';
9import './interfaces/IERC20.sol';
10import './interfaces/IWETH.sol';
Показати все

Більшість із них ми або зустрічали раніше, або вони досить очевидні. Єдиний виняток — IWETH.sol. Uniswap v2 дозволяє обмін будь-якої пари токенів ERC-20, але сам ефір (ETH) не є токеном ERC-20. Він існував до стандарту і передається за допомогою унікальних механізмів. Щоб уможливити використання ETH у контрактах, які застосовуються до токенів ERC-20, люди придумали контракт «загорнутого» ефіру (WETH)opens in a new tab. Ви надсилаєте цьому контракту ETH, і він карбує вам еквівалентну кількість WETH. Або ви можете спалити WETH і отримати назад ETH.

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

Роутеру потрібно знати, яку фабрику використовувати, а для транзакцій, що вимагають WETH, — який контракт WETH використовувати. Ці значення є незміннимиopens in a new tab, що означає, що їх можна встановити лише в конструкторі. Це дає користувачам впевненість, що ніхто не зможе змінити їх так, щоб вони вказували на менш чесні контракти.

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

Цей модифікатор гарантує, що транзакції з обмеженням часу («зробити X до часу Y, якщо можливо») не відбуваються після їхнього часового ліміту.

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

Конструктор лише встановлює незмінні змінні стану.

1 receive() external payable {
2 assert(msg.sender == WETH); // приймати ETH лише через fallback з контракту WETH
3 }

Ця функція викликається, коли ми викуповуємо токени з контракту WETH назад в ETH. Лише контракт WETH, який ми використовуємо, має право це робити.

Додавання ліквідності

Ці функції додають токени до парної біржі, що збільшує пул ліквідності.

1
2 // **** ДОДАВАННЯ ЛІКВІДНОСТІ ****
3 function _addLiquidity(

Ця функція використовується для обчислення кількості токенів A і B, які слід внести на парну біржу.

1 address tokenA,
2 address tokenB,

Це адреси контрактів токенів ERC-20.

1 uint amountADesired,
2 uint amountBDesired,

Це суми, які постачальник ліквідності хоче внести. Вони також є максимальними сумами A і B, які будуть внесені.

1 uint amountAMin,
2 uint amountBMin

Це мінімальні прийнятні суми для депозиту. Якщо транзакція не може відбутися з цими сумами або більшими, скасуйте її. Якщо вам не потрібна ця функція, просто вкажіть нуль.

Постачальники ліквідності зазвичай вказують мінімум, оскільки вони хочуть обмежити транзакцію обмінним курсом, близьким до поточного. Якщо обмінний курс коливається занадто сильно, це може означати новини, які змінюють базові вартості, і вони хочуть вручну вирішити, що робити.

Наприклад, уявіть випадок, коли обмінний курс становить один до одного, і постачальник ліквідності вказує такі значення:

ПараметрЗначення
amountADesired1000
amountBDesired1000
amountAMin900
amountBMin800

Поки обмінний курс залишається між 0,9 і 1,25, транзакція відбувається. Якщо обмінний курс виходить за межі цього діапазону, транзакція скасовується.

Причина цього запобіжного заходу полягає в тому, що транзакції не є миттєвими: ви їх надсилаєте, і згодом валідатор включить їх у блок (якщо тільки ціна на газ не дуже низька, і в цьому випадку вам доведеться надіслати іншу транзакцію з тим самим nonce і вищою ціною на газ, щоб її перезаписати). Ви не можете контролювати те, що відбувається в проміжку між поданням і включенням.

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

Функція повертає суми, які постачальник ліквідності повинен внести, щоб мати співвідношення, рівне поточному співвідношенню між резервами.

1 // створити пару, якщо її ще не існує
2 if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
3 IUniswapV2Factory(factory).createPair(tokenA, tokenB);
4 }

Якщо для цієї пари токенів ще немає біржі, створіть її.

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

Отримати поточні резерви в парі.

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

Якщо поточні резерви порожні, то це новий парний обмін. Суми, що вносяться, повинні точно відповідати тим, які хоче надати постачальник ліквідності.

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

Якщо нам потрібно побачити, якими будуть суми, ми отримуємо оптимальну суму за допомогою цієї функціїopens in a new tab. Ми хочемо, щоб співвідношення було таким же, як і в поточних резервах.

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

Якщо amountBOptimal менше суми, яку постачальник ліквідності хоче внести, це означає, що токен B наразі цінніший, ніж думає постачальник ліквідності, тому потрібна менша сума.

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

Якщо оптимальна сума B більша за бажану суму B, це означає, що токени B наразі менш цінні, ніж думає постачальник ліквідності, тому потрібна більша сума. Однак бажана сума є максимальною, тому ми не можемо цього зробити. Натомість ми обчислюємо оптимальну кількість токенів A для бажаної кількості токенів B.

Зібравши все разом, ми отримуємо цей графік. Припустимо, ви намагаєтеся внести тисячу токенів A (синя лінія) і тисячу токенів B (червона лінія). Вісь X — це обмінний курс, A/B. Якщо x=1, вони рівні за вартістю, і ви вносите по тисячі кожного. Якщо x=2, A вдвічі цінніший за B (ви отримуєте два токени B за кожен токен A), тому ви вносите тисячу токенів B, але лише 500 токенів A. Якщо x=0.5, ситуація зворотна: тисяча токенів A і п'ятсот токенів B.

Графік

Ви могли б внести ліквідність безпосередньо в основний контракт (використовуючи UniswapV2Pair::mintopens in a new tab), але основний контракт лише перевіряє, що його не обманюють, тому ви ризикуєте втратити вартість, якщо обмінний курс зміниться між часом, коли ви подасте транзакцію, і часом її виконання. Якщо ви використовуєте периферійний контракт, він визначає суму, яку ви повинні внести, і вносить її негайно, тому обмінний курс не змінюється, і ви нічого не втрачаєте.

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
Показати все

Ця функція може бути викликана транзакцією для внесення ліквідності. Більшість параметрів такі ж, як і в _addLiquidity вище, за двома винятками:

. to — це адреса, яка отримує нові викарбувані токени ліквідності, щоб показати частку постачальника ліквідності в пулі . deadline — це часовий ліміт для транзакції

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

Ми обчислюємо суми для фактичного депозиту, а потім знаходимо адресу пулу ліквідності. Щоб заощадити газ, ми не робимо це, запитуючи фабрику, а використовуємо функцію бібліотеки pairFor (див. нижче в бібліотеках).

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

Перекажіть правильні суми токенів від користувача на парну біржу.

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

Натомість надайте на адресу to токени ліквідності для часткового володіння пулом. Функція mint основного контракту бачить, скільки додаткових токенів вона має (порівняно з тим, що було востаннє, коли змінювалася ліквідність), і відповідно карбує ліквідність.

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

Коли постачальник ліквідності хоче надати ліквідність для парного обміну Token/ETH, є кілька відмінностей. Контракт обробляє обгортання ETH для постачальника ліквідності. Немає потреби вказувати, скільки ETH користувач хоче внести, оскільки користувач просто надсилає їх разом із транзакцією (сума доступна в 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));
Показати все

Щоб внести ETH, контракт спочатку обгортає його в WETH, а потім переказує WETH у пару. Зверніть увагу, що переказ обгорнутий в assert. Це означає, що якщо переказ не вдасться, цей виклик контракту також не вдасться, і тому обгортання насправді не відбувається.

1 liquidity = IUniswapV2Pair(pair).mint(to);
2 // повернути залишок eth, якщо є
3 if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
4 }

Користувач уже надіслав нам ETH, тому якщо залишився якийсь залишок (оскільки інший токен менш цінний, ніж думав користувач), нам потрібно повернути гроші.

Вилучення ліквідності

Ці функції видалять ліквідність і повернуть кошти постачальнику ліквідності.

1 // **** ВИДАЛЕННЯ ЛІКВІДНОСТІ ****
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) {
Показати все

Найпростіший випадок видалення ліквідності. Існує мінімальна кількість кожного токена, яку постачальник ліквідності погоджується прийняти, і це має відбутися до закінчення терміну дії.

1 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
2 IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // надіслати ліквідність парі
3 (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);

Функція burn основного контракту обробляє повернення токенів користувачеві.

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

Коли функція повертає кілька значень, але нас цікавлять лише деякі з них, ось як ми отримуємо лише ці значення. Це дещо дешевше з точки зору газу, ніж читання значення і ніколи його не використання.

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

Перетворіть суми з того, як їх повертає основний контракт (спочатку токен із меншою адресою), на те, як їх очікує користувач (відповідно до tokenA та tokenB).

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

Нормально спочатку виконати переказ, а потім перевірити його легітимність, тому що якщо він нелегітимний, ми скасуємо всі зміни стану.

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 }
Показати все

Видалення ліквідності для ETH майже таке ж, за винятком того, що ми отримуємо токени WETH, а потім викуповуємо їх за ETH, щоб повернути постачальнику ліквідності.

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 }
Показати все

Ці функції передають метатранзакції, щоб дозволити користувачам без ефіру виводити кошти з пулу, використовуючи механізм дозволу.

1
2 // **** ВИДАЛЕННЯ ЛІКВІДНОСТІ (підтримка токенів з комісією за переказ) ****
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
Показати все

Цю функцію можна використовувати для токенів, які мають комісію за переказ або зберігання. Коли токен має такі комісії, ми не можемо покладатися на функцію removeLiquidity, щоб дізнатися, скільки токена ми отримаємо назад, тому нам потрібно спочатку вивести кошти, а потім отримати баланс.

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 }
Показати все

Остання функція поєднує плату за сховище з метатранзакціями.

Торгівля

1 // **** ОБМІН ****
2 // вимагає, щоб початкова сума вже була відправлена першій парі
3 function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {

Ця функція виконує внутрішню обробку, яка потрібна для функцій, доступних для трейдерів.

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

На момент написання цієї статті існує 388 160 токенів ERC-20opens in a new tab. Якби існував парний обмін для кожної пари токенів, то було б понад 150 мільярдів парних обмінів. Увесь ланцюжок наразі має лише 0,1 % від цієї кількості акаунтівopens in a new tab. Натомість функції обміну підтримують концепцію шляху. Трейдер може обміняти A на B, B на C і C на D, тому немає потреби в прямому обміні пари A-D.

Ціни на цих ринках, як правило, синхронізовані, оскільки коли вони несинхронізовані, це створює можливість для арбітражу. Уявіть, наприклад, три токени: A, B і C. Існує три парні обміни, по одному на кожну пару.

  1. Початкова ситуація
  2. Трейдер продає 24,695 токена A й отримує 25,305 токена B.
  3. Трейдер продає 24,695 токена B за 25,305 токена C, залишаючи приблизно 0,61 токена B як прибуток.
  4. Потім трейдер продає 24,695 токена C за 25,305 токена A, залишаючи приблизно 0,61 токена C як прибуток. У трейдера також є 0,61 додаткового токена A (25,305, які трейдер отримує в кінці, мінус початкові інвестиції в розмірі 24,695).
КрокОбмін A-BОбмін B-CОбмін 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];

Отримайте пару, яку ми зараз обробляємо, відсортуйте її (для використання з парою) та отримайте очікувану вихідну суму.

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

Отримайте очікувані вихідні суми, відсортовані так, як їх очікує парний обмін.

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

Це останній обмін? Якщо так, надішліть токени, отримані для торгівлі, до місця призначення. Якщо ні, надішліть їх на наступний парний обмін.

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

Фактично викликайте парний обмін, щоб обміняти токени. Нам не потрібен зворотний виклик для сповіщення про обмін, тому ми не надсилаємо жодних байтів у це поле.

1 function swapExactTokensForTokens(

Ця функція використовується трейдерами безпосередньо для обміну одного токена на інший.

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

Цей параметр містить адреси контрактів ERC-20. Як пояснювалося вище, це масив, тому що вам, можливо, доведеться пройти через кілька парних обмінів, щоб дістатися від активу, який у вас є, до активу, який ви хочете отримати.

Параметр функції в Solidity може зберігатися або в memory, або в calldata. Якщо функція є точкою входу до контракту, що викликається безпосередньо користувачем (за допомогою транзакції) або з іншого контракту, то значення параметра можна взяти безпосередньо з даних виклику. Якщо функція викликається внутрішньо, як _swap вище, то параметри мають зберігатися в memory. З точки зору викликаного контракту calldata є лише для читання.

Зі скалярними типами, такими як uint або address, компілятор сам обирає для нас сховище, але для масивів, які є довшими та дорожчими, ми вказуємо тип сховища, який потрібно використовувати.

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

Значення, що повертаються, завжди повертаються в пам'яті (memory).

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

Обчисліть суму, яка буде придбана під час кожного обміну. Якщо результат менший за мінімум, який готовий прийняти трейдер, скасуйте транзакцію.

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

Нарешті, перекажіть початковий токен ERC-20 на акаунт для першого парного обміну та викличте _swap. Це все відбувається в одній транзакції, тому парний обмін знає, що будь-які неочікувані токени є частиною цього переказу.

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 }
Показати все

Попередня функція, swapTokensForTokens, дозволяє трейдеру вказати точну кількість вхідних токенів, які він готовий віддати, і мінімальну кількість вихідних токенів, які він готовий отримати натомість. Ця функція виконує зворотний обмін: вона дозволяє трейдеру вказати кількість вихідних токенів, які він хоче отримати, і максимальну кількість вхідних токенів, які він готовий за них заплатити.

В обох випадках трейдер повинен спочатку надати цьому периферійному контракту дозвіл на переказ коштів.

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 // повернути залишки ETH, якщо вони є
72 if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
73 }
Показати все

Усі ці чотири варіанти передбачають торгівлю між ETH і токенами. Єдина відмінність полягає в тому, що ми або отримуємо ETH від трейдера та використовуємо його для карбування WETH, або отримуємо WETH від останнього обміну на шляху та спалюємо його, повертаючи трейдеру отриманий ETH.

1 // **** ОБМІН (з підтримкою токенів з комісією за переказ) ****
2 // вимагає, щоб початкова сума вже була відправлена першій парі
3 function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {

Це внутрішня функція для обміну токенів, що мають комісію за переказ або зберігання, щоб вирішити (цю проблемуopens in a new tab).

1 for (uint i; i < path.length - 1; i++) {
2 (address input, address output) = (path[i], path[i + 1]);
3 (address token0,) = UniswapV2Library.sortTokens(input, output);
4 IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
5 uint amountInput;
6 uint amountOutput;
7 { // область видимості для уникнення помилок 'stack too deep'
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);
Показати все

Через комісії за переказ ми не можемо покладатися на функцію getAmountsOut, щоб дізнатися, скільки ми отримуємо з кожного переказу (так, як ми це робили перед викликом початкової функції _swap). Натомість ми повинні спочатку здійснити переказ, а потім подивитися, скільки токенів ми отримали назад.

Примітка. Теоретично, ми могли б просто використовувати цю функцію замість _swap, але в певних випадках (наприклад, якщо переказ у кінцевому підсумку буде скасовано, оскільки наприкінці не вистачить коштів для досягнення необхідного мінімуму), це коштуватиме більше газу. Токени з комісією за переказ досить рідкісні, тому, хоча ми повинні їх підтримувати, немає необхідності, щоб усі обміни припускали, що вони проходять хоча б через один із них.

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 }
Показати все

Це ті самі варіанти, що використовуються для звичайних токенів, але замість цього вони викликають _swapSupportingFeeOnTransferTokens.

1 // **** ФУНКЦІЇ БІБЛІОТЕКИ ****
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}
Показати все

Ці функції є просто проксі, які викликають функції UniswapV2Library.

UniswapV2Migrator.sol

Цей контракт використовувався для міграції обмінів зі старої версії v1 на v2. Тепер, коли вони перенесені, він більше не актуальний.

Бібліотеки

Бібліотека SafeMathopens in a new tab добре задокументована, тому немає потреби документувати її тут.

Math

Ця бібліотека містить деякі математичні функції, які зазвичай не потрібні в коді на Solidity, тому вони не є частиною мови.

1pragma solidity =0.5.16;
2
3// бібліотека для виконання різних математичних операцій
4
5library Math {
6 function min(uint x, uint y) internal pure returns (uint z) {
7 z = x < y ? x : y;
8 }
9
10 // вавилонський метод (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;
Показати все

Почніть з x як оцінки, що є вищою за квадратний корінь (ось чому нам потрібно розглядати 1-3 як особливі випадки).

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

Отримайте ближчу оцінку: середнє значення попередньої оцінки та числа, квадратний корінь якого ми намагаємося знайти, поділене на попередню оцінку. Повторюйте, поки нова оцінка не стане нижчою за наявну. Для отримання додаткової інформації дивіться тутopens in a new tab.

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

Нам ніколи не знадобиться квадратний корінь з нуля. Квадратні корені з одиниці, двох і трьох приблизно дорівнюють одиниці (ми використовуємо цілі числа, тому ігноруємо дробову частину).

1 }
2 }
3}

Дроби з фіксованою комою (UQ112x112)

Ця бібліотека обробляє дроби, які зазвичай не є частиною арифметики Ethereum. Вона робить це шляхом кодування числа x як x*2^112. Це дозволяє нам використовувати вихідні коди операцій додавання та віднімання без змін.

1pragma solidity =0.5.16;
2
3// бібліотека для роботи з двійковими числами з фіксованою комою (https://wikipedia.org/wiki/Q_(number_format))
4
5// діапазон: [0, 2**112 - 1]
6// роздільна здатність: 1 / 2**112
7
8library UQ112x112 {
9 uint224 constant Q112 = 2**112;
Показати все

Q112 — це кодування одиниці.

1 // кодувати uint112 як UQ112x112
2 function encode(uint112 y) internal pure returns (uint224 z) {
3 z = uint224(y) * Q112; // ніколи не переповнюється
4 }

Оскільки y є uint112, максимальне значення, яке він може приймати, — це 2^112-1. Це число все ще можна закодувати як UQ112x112.

1 // ділить UQ112x112 на uint112, повертаючи UQ112x112
2 function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
3 z = x / uint224(y);
4 }
5}

Якщо ми ділимо два значення UQ112x112, результат більше не множиться на 2^112. Тому замість цього ми беремо ціле число для знаменника. Нам би знадобився подібний трюк для множення, але нам не потрібно множити значення UQ112x112.

UniswapV2Library

Ця бібліотека використовується лише периферійними контрактами

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 // повертає відсортовані адреси токенів, що використовуються для обробки значень, що повертаються з пар, відсортованих у цьому порядку
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 }
Показати все

Відсортуйте два токени за адресою, щоб ми могли отримати адресу парного обміну для них. Це необхідно, тому що інакше ми мали б дві можливості: одну для параметрів A, B і іншу для параметрів B, A, що призвело б до двох обмінів замість одного.

1 // обчислює адресу CREATE2 для пари без зовнішніх викликів
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' // хеш коду ініціалізації
9 ))));
10 }
Показати все

Ця функція обчислює адресу парного обміну для двох токенів. Цей контракт створюється за допомогою коду операції CREATE2opens in a new tab, тому ми можемо обчислити адресу, використовуючи той самий алгоритм, якщо ми знаємо параметри, які він використовує. Це набагато дешевше, ніж запитувати фабрику, і

1 // отримує та сортує резерви для пари
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 }

Ця функція повертає резерви двох токенів, які є у парного обміну. Зверніть увагу, що вона може отримувати токени в будь-якому порядку і сортує їх для внутрішнього використання.

1 // маючи певну кількість активу та резерви пари, повертає еквівалентну кількість іншого активу
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 }

Ця функція надає вам кількість токена B, яку ви отримаєте в обмін на токен A, якщо немає комісії. Цей розрахунок враховує, що переказ змінює обмінний курс.

1 // маючи вхідну кількість активу та резерви пари, повертає максимальну вихідну кількість іншого активу
2 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {

Наведена вище функція quote чудово працює, якщо немає плати за використання парного обміну. Однак, якщо є комісія за обмін у розмірі 0,3 %, сума, яку ви фактично отримаєте, буде меншою. Ця функція розраховує суму після сплати комісії за обмін.

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 не підтримує дроби нативно, тому ми не можемо просто помножити вихідну суму на 0,997. Замість цього ми множимо чисельник на 997, а знаменник на 1000, досягаючи того ж ефекту.

1 // маючи вихідну кількість активу та резерви пари, повертає необхідну вхідну кількість іншого активу
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 }

Ця функція робить приблизно те ж саме, але вона отримує вихідну суму і надає вхідну.

1
2 // виконує ланцюгові розрахунки getAmountOut для будь-якої кількості пар
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 // виконує ланцюгові розрахунки getAmountIn для будь-якої кількості пар
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}
Показати все

Ці дві функції обробляють ідентифікацію значень, коли необхідно пройти через кілька парних обмінів.

Помічник з переказів

Ця бібліотекаopens in a new tab додає перевірки успішності для переказів ERC-20 та Ethereum, щоб однаково обробляти скасування (revert) і повернення значення false.

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3pragma solidity >=0.6.0;
4
5// допоміжні методи для взаємодії з токенами ERC20 та надсилання ETH, які не завжди повертають 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
Показати все

Ми можемо викликати інший контракт одним із двох способів:

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

Для зворотної сумісності з токенами, створеними до стандарту ERC-20, виклик ERC-20 може завершитися невдачею або шляхом скасування (revert) (у цьому випадку success дорівнює false), або шляхом успішного виконання з поверненням значення false (у цьому випадку є вихідні дані, і якщо ви декодуєте їх як логічне значення, ви отримаєте 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 }
Показати все

Ця функція реалізує функціональність transfer стандарту ERC-20opens in a new tab, яка дозволяє акаунту витрачати кошти в межах дозволу, наданого іншим акаунтом.

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 }
Показати все

Ця функція реалізує функціональність transferFrom стандарту ERC-20opens in a new tab, яка дозволяє акаунту витрачати кошти в межах дозволу, наданого іншим акаунтом.

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}

Ця функція переказує ефір на акаунт. Будь-який виклик до іншого контракту може спробувати надіслати ефір. Оскільки нам не потрібно викликати жодної функції, ми не надсилаємо жодних даних із викликом.

Висновок

Це довга стаття обсягом близько 50 сторінок. Якщо ви дійшли до цього місця, вітаємо! Сподіваємося, що тепер ви зрозуміли, що слід враховувати під час написання реального застосунку (на відміну від коротких прикладів програм) і зможете краще писати контракти для власних сценаріїв використання.

А тепер ідіть, напишіть щось корисне і здивуйте нас.

Більше моїх робіт дивіться тутopens in a new tab.

Останні оновлення сторінки: 25 лютого 2026 р.

Чи була ця інструкція корисною?