Lompat ke konten utama

Penelusuran Kontrak Uniswap-v2

Solidity
dapps
Menengah
Ori Pomerantz
1 Mei 2021
58 menit baca

Pengantar

Uniswap v2 (opens in a new tab) dapat membuat pasar pertukaran antara dua token ERC-20 apa pun. Dalam artikel ini kita akan membahas kode sumber untuk kontrak yang mengimplementasikan protokol ini dan melihat mengapa mereka ditulis dengan cara ini.

Apa yang Dilakukan Uniswap?

Pada dasarnya, ada dua jenis pengguna: penyedia likuiditas dan pedagang.

Penyedia likuiditas menyediakan kolam dengan dua token yang dapat ditukar (kita akan menyebutnya Token0 dan Token1). Sebagai imbalannya, mereka menerima token ketiga yang mewakili kepemilikan sebagian dari kolam yang disebut token likuiditas.

Pedagang mengirimkan satu jenis token ke kolam dan menerima yang lain (misalnya, mengirim Token0 dan menerima Token1) dari kolam yang disediakan oleh penyedia likuiditas. Nilai tukar ditentukan oleh jumlah relatif Token0 dan Token1 yang dimiliki kolam. Selain itu, kolam mengambil persentase kecil sebagai hadiah untuk kolam likuiditas.

Ketika penyedia likuiditas menginginkan aset mereka kembali, mereka dapat membakar token kolam dan menerima kembali token mereka, termasuk bagian hadiah mereka.

Klik di sini untuk deskripsi yang lebih lengkap (opens in a new tab).

Mengapa v2? Mengapa bukan v3?

Uniswap v3 (opens in a new tab) adalah peningkatan yang jauh lebih rumit daripada v2. Lebih mudah untuk mempelajari v2 terlebih dahulu dan kemudian beralih ke v3.

Kontrak Inti vs Kontrak Periferal

Uniswap v2 dibagi menjadi dua komponen, inti (core) dan periferal (periphery). Pembagian ini memungkinkan kontrak inti, yang menyimpan aset dan oleh karena itu harus aman, menjadi lebih sederhana dan lebih mudah diaudit. Semua fungsionalitas tambahan yang diperlukan oleh pedagang kemudian dapat disediakan oleh kontrak periferal.

Aliran Data dan Kontrol

Ini adalah aliran data dan kontrol yang terjadi saat Anda melakukan tiga tindakan utama Uniswap:

  1. Tukar antar token yang berbeda
  2. Tambahkan likuiditas ke pasar dan dapatkan hadiah berupa token likuiditas ERC-20 pertukaran pasangan
  3. Bakar token likuiditas ERC-20 dan dapatkan kembali token ERC-20 yang diizinkan oleh pertukaran pasangan untuk ditukar oleh pedagang

Tukar

Ini adalah aliran yang paling umum, digunakan oleh pedagang:

Pemanggil

  1. Berikan akun periphery dengan jatah (allowance) sejumlah yang akan ditukar.
  2. Panggil salah satu dari banyak fungsi tukar kontrak periphery (yang mana tergantung pada apakah ETH terlibat atau tidak, apakah pedagang menentukan jumlah token untuk disetor atau jumlah token untuk didapatkan kembali, dll). Setiap fungsi tukar menerima path, sebuah array pertukaran yang harus dilalui.

Di dalam kontrak periphery (UniswapV2Router02.sol)

  1. Identifikasi jumlah yang perlu diperdagangkan di setiap pertukaran di sepanjang jalur (path).
  2. Mengulangi (iterasi) di sepanjang jalur. Untuk setiap pertukaran di sepanjang jalan, ia mengirimkan token input dan kemudian memanggil fungsi swap pertukaran tersebut. Dalam kebanyakan kasus, alamat tujuan untuk token adalah pertukaran pasangan berikutnya di jalur tersebut. Pada pertukaran terakhir, itu adalah alamat yang diberikan oleh pedagang.

Di dalam kontrak inti (UniswapV2Pair.sol)

  1. Verifikasi bahwa kontrak inti tidak dicurangi dan dapat mempertahankan likuiditas yang cukup setelah tukar.
  2. Lihat berapa banyak token ekstra yang kita miliki selain cadangan yang diketahui. Jumlah tersebut adalah jumlah token input yang kita terima untuk ditukar.
  3. Kirim token output ke tujuan.
  4. Panggil _update untuk memperbarui jumlah cadangan

Kembali ke kontrak periphery (UniswapV2Router02.sol)

  1. Lakukan pembersihan yang diperlukan (misalnya, bakar token WETH untuk mendapatkan kembali ETH untuk dikirim ke pedagang)

Tambah Likuiditas

Pemanggil

  1. Berikan akun periphery dengan jatah dalam jumlah yang akan ditambahkan ke kolam likuiditas.
  2. Panggil salah satu fungsi addLiquidity kontrak periphery.

Di dalam kontrak periphery (UniswapV2Router02.sol)

  1. Buat pertukaran pasangan baru jika perlu
  2. Jika ada pertukaran pasangan yang sudah ada, hitung jumlah token yang akan ditambahkan. Ini seharusnya bernilai identik untuk kedua token, jadi rasio token baru terhadap token yang ada sama.
  3. Periksa apakah jumlahnya dapat diterima (pemanggil dapat menentukan jumlah minimum di mana mereka lebih suka tidak menambahkan likuiditas)
  4. Panggil kontrak inti.

Di dalam kontrak inti (UniswapV2Pair.sol)

  1. Mint token likuiditas dan kirimkan ke pemanggil
  2. Panggil _update untuk memperbarui jumlah cadangan

Hapus Likuiditas

Pemanggil

  1. Berikan akun periphery dengan jatah token likuiditas untuk dibakar sebagai ganti token yang mendasarinya.
  2. Panggil salah satu fungsi removeLiquidity kontrak periphery.

Di dalam kontrak periphery (UniswapV2Router02.sol)

  1. Kirim token likuiditas ke pertukaran pasangan

Di dalam kontrak inti (UniswapV2Pair.sol)

  1. Kirimkan alamat tujuan token yang mendasarinya secara proporsional dengan token yang dibakar. Misalnya jika ada 1000 token A di kolam, 500 token B, dan 90 token likuiditas, dan kita menerima 9 token untuk dibakar, kita membakar 10% dari token likuiditas sehingga kita mengirim kembali pengguna 100 token A dan 50 token B.
  2. Bakar token likuiditas
  3. Panggil _update untuk memperbarui jumlah cadangan

Kontrak Inti

Ini adalah kontrak aman yang menyimpan likuiditas.

UniswapV2Pair.sol

Kontrak ini (opens in a new tab) mengimplementasikan kolam aktual yang menukar token. Ini adalah fungsionalitas inti 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';
Tampilkan semua

Ini adalah semua antarmuka yang perlu diketahui oleh kontrak, baik karena kontrak mengimplementasikannya (IUniswapV2Pair dan UniswapV2ERC20) atau karena ia memanggil kontrak yang mengimplementasikannya.

1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {

Kontrak ini mewarisi dari UniswapV2ERC20, yang menyediakan fungsi ERC-20 untuk token likuiditas.

1 using SafeMath for uint;

Pustaka SafeMath (opens in a new tab) digunakan untuk menghindari overflow dan underflow. Ini penting karena jika tidak, kita mungkin akan berakhir pada situasi di mana sebuah nilai seharusnya -1, tetapi malah menjadi 2^256-1.

1 using UQ112x112 for uint224;

Banyak perhitungan dalam kontrak kolam memerlukan pecahan. Namun, pecahan tidak didukung oleh EVM. Solusi yang ditemukan Uniswap adalah menggunakan nilai 224 bit, dengan 112 bit untuk bagian bilangan bulat, dan 112 bit untuk pecahan. Jadi 1.0 direpresentasikan sebagai 2^112, 1.5 direpresentasikan sebagai 2^112 + 2^111, dll.

Detail lebih lanjut tentang pustaka ini tersedia nanti di dokumen ini.

Variabel

1 uint public constant MINIMUM_LIQUIDITY = 10**3;

Untuk menghindari kasus pembagian dengan nol, ada jumlah minimum token likuiditas yang selalu ada (tetapi dimiliki oleh akun nol). Jumlah tersebut adalah MINIMUM_LIQUIDITY, seribu.

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

Ini adalah pemilih ABI untuk fungsi transfer ERC-20. Ini digunakan untuk mentransfer token ERC-20 di kedua akun token.

1 address public factory;

Ini adalah kontrak pabrik yang membuat kolam ini. Setiap kolam adalah pertukaran antara dua token ERC-20, pabrik adalah titik pusat yang menghubungkan semua kolam ini.

1 address public token0;
2 address public token1;

Terdapat alamat kontrak untuk dua jenis token ERC-20 yang dapat ditukar oleh kolam ini.

1 uint112 private reserve0; // uses single storage slot, accessible via getReserves // menggunakan slot penyimpanan tunggal, dapat diakses melalui getReserves
2 uint112 private reserve1; // uses single storage slot, accessible via getReserves // menggunakan slot penyimpanan tunggal, dapat diakses melalui getReserves

Cadangan yang dimiliki kolam untuk setiap jenis token. Kita berasumsi bahwa keduanya mewakili jumlah nilai yang sama, dan oleh karena itu setiap token0 bernilai reserve1/reserve0 dari token1.

1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves // menggunakan slot penyimpanan tunggal, dapat diakses melalui getReserves

Stempel waktu untuk blok terakhir di mana pertukaran terjadi, digunakan untuk melacak nilai tukar dari waktu ke waktu.

Salah satu pengeluaran gas terbesar dari kontrak Ethereum adalah penyimpanan, yang bertahan dari satu panggilan kontrak ke panggilan berikutnya. Setiap sel penyimpanan memiliki panjang 256 bit. Jadi tiga variabel, reserve0, reserve1, dan blockTimestampLast, dialokasikan sedemikian rupa sehingga satu nilai penyimpanan dapat mencakup ketiganya (112+112+32=256).

1 uint public price0CumulativeLast;
2 uint public price1CumulativeLast;

Variabel-variabel ini menyimpan biaya kumulatif untuk setiap token (masing-masing dalam kaitannya dengan yang lain). Mereka dapat digunakan untuk menghitung nilai tukar rata-rata selama periode waktu tertentu.

1 uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event // reserve0 * reserve1, segera setelah peristiwa likuiditas terbaru

Cara pertukaran pasangan memutuskan nilai tukar antara token0 dan token1 adalah dengan menjaga kelipatan dari kedua cadangan tetap konstan selama perdagangan. kLast adalah nilai ini. Ini berubah ketika penyedia likuiditas menyetor atau menarik token, dan sedikit meningkat karena biaya pasar 0,3%.

Berikut adalah contoh sederhana. Perhatikan bahwa demi kesederhanaan, tabel hanya memiliki tiga digit di belakang koma, dan kita mengabaikan biaya perdagangan 0,3% sehingga angkanya tidak akurat.

Peristiwareserve0reserve1reserve0 * reserve1Nilai tukar rata-rata (token1 / token0)
Pengaturan awal1,000.0001,000.0001,000,000
Pedagang A menukar 50 token0 dengan 47.619 token11,050.000952.3811,000,0000.952
Pedagang B menukar 10 token0 dengan 8.984 token11,060.000943.3961,000,0000.898
Pedagang C menukar 40 token0 dengan 34.305 token11,100.000909.0901,000,0000.858
Pedagang D menukar 100 token1 dengan 109.01 token0990.9901,009.0901,000,0000.917
Pedagang E menukar 10 token0 dengan 10.079 token11,000.990999.0101,000,0001.008

Seiring pedagang menyediakan lebih banyak token0, nilai relatif token1 meningkat, dan sebaliknya, berdasarkan penawaran dan permintaan.

Kunci

1 uint private unlocked = 1;

Ada kelas kerentanan keamanan yang didasarkan pada penyalahgunaan reentrancy (opens in a new tab). Uniswap perlu mentransfer token ERC-20 arbitrer, yang berarti memanggil kontrak ERC-20 yang mungkin mencoba menyalahgunakan pasar Uniswap yang memanggil mereka. Dengan memiliki variabel unlocked sebagai bagian dari kontrak, kita dapat mencegah fungsi dipanggil saat mereka sedang berjalan (dalam transaksi yang sama).

1 modifier lock() {

Fungsi ini adalah pengubah (modifier) (opens in a new tab), sebuah fungsi yang membungkus fungsi normal untuk mengubah perilakunya dengan cara tertentu.

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

Jika unlocked sama dengan satu, atur menjadi nol. Jika sudah nol, kembalikan (revert) panggilan, buat itu gagal.

1 _;

Dalam sebuah pengubah, _; adalah panggilan fungsi asli (dengan semua parameter). Di sini itu berarti bahwa panggilan fungsi hanya terjadi jika unlocked bernilai satu saat dipanggil, dan saat sedang berjalan nilai unlocked adalah nol.

1 unlocked = 1;
2 }

Setelah fungsi utama kembali, lepaskan kunci.

Fungsi Lain-lain

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

Fungsi ini memberi pemanggil status pertukaran saat ini. Perhatikan bahwa fungsi Solidity dapat mengembalikan beberapa nilai (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));

Fungsi internal ini mentransfer sejumlah token ERC20 dari pertukaran ke orang lain. SELECTOR menentukan bahwa fungsi yang kita panggil adalah transfer(address,uint) (lihat definisi di atas).

Untuk menghindari keharusan mengimpor antarmuka untuk fungsi token, kita "secara manual" membuat panggilan menggunakan salah satu fungsi ABI (opens in a new tab).

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

Ada dua cara di mana panggilan transfer ERC-20 dapat melaporkan kegagalan:

  1. Revert. Jika panggilan ke kontrak eksternal dikembalikan (revert), maka nilai kembalian boolean adalah false
  2. Berakhir secara normal tetapi melaporkan kegagalan. Dalam hal ini buffer nilai kembalian memiliki panjang bukan nol, dan ketika didekodekan sebagai nilai boolean, itu adalah false

Jika salah satu dari kondisi ini terjadi, kembalikan (revert).

Peristiwa

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

Kedua peristiwa ini dipancarkan ketika penyedia likuiditas menyetor likuiditas (Mint) atau menariknya (Burn). Dalam kedua kasus tersebut, jumlah token0 dan token1 yang disetor atau ditarik adalah bagian dari peristiwa, serta identitas akun yang memanggil kita (sender). Dalam kasus penarikan, peristiwa tersebut juga mencakup target yang menerima token (to), yang mungkin tidak sama dengan pengirim.

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

Peristiwa ini dipancarkan ketika seorang pedagang menukar satu token dengan token lainnya. Sekali lagi, pengirim dan tujuan mungkin tidak sama. Setiap token dapat dikirim ke pertukaran, atau diterima darinya.

1 event Sync(uint112 reserve0, uint112 reserve1);

Terakhir, Sync dipancarkan setiap kali token ditambahkan atau ditarik, terlepas dari alasannya, untuk memberikan informasi cadangan terbaru (dan karenanya nilai tukar).

Fungsi Pengaturan

Fungsi-fungsi ini seharusnya dipanggil sekali ketika pertukaran pasangan baru diatur.

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

Konstruktor memastikan kita akan melacak alamat pabrik yang membuat pasangan tersebut. Informasi ini diperlukan untuk initialize dan untuk biaya pabrik (jika ada)

1 // called once by the factory at time of deployment // dipanggil sekali oleh factory pada saat penerapan
2 function initialize(address _token0, address _token1) external {
3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check // pemeriksaan yang cukup
4 token0 = _token0;
5 token1 = _token1;
6 }

Fungsi ini memungkinkan pabrik (dan hanya pabrik) untuk menentukan dua token ERC-20 yang akan ditukar oleh pasangan ini.

Fungsi Pembaruan Internal

_update
1 // update reserves and, on the first call per block, price accumulators // memperbarui cadangan dan, pada panggilan pertama per blok, akumulator harga
2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {

Fungsi ini dipanggil setiap kali token disetor atau ditarik.

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

Jika balance0 atau balance1 (uint256) lebih tinggi dari uint112(-1) (=2^112-1) (sehingga meluap & kembali ke 0 saat dikonversi ke uint112) tolak untuk melanjutkan _update guna mencegah overflow. Dengan token normal yang dapat dibagi lagi menjadi 10^18 unit, ini berarti setiap pertukaran dibatasi hingga sekitar 5.1*10^15 dari setiap token. Sejauh ini hal tersebut belum menjadi masalah.

1 uint32 blockTimestamp = uint32(block.timestamp % 2**32);
2 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired // overflow diinginkan
3 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {

Jika waktu yang berlalu bukan nol, itu berarti kita adalah transaksi pertukaran pertama pada blok ini. Dalam hal ini, kita perlu memperbarui akumulator biaya.

1 // * never overflows, and + overflow is desired // * tidak pernah overflow, dan + overflow diinginkan
2 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
3 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
4 }

Setiap akumulator biaya diperbarui dengan biaya terbaru (cadangan token lain/cadangan token ini) dikalikan waktu yang berlalu dalam detik. Untuk mendapatkan harga rata-rata, Anda membaca harga kumulatif di dua titik waktu dan membaginya dengan perbedaan waktu di antara keduanya. Misalnya, asumsikan urutan peristiwa ini:

Peristiwareserve0reserve1stempel waktuNilai tukar marjinal (reserve1 / reserve0)price0CumulativeLast
Pengaturan awal1,000.0001,000.0005,0001.0000
Pedagang A menyetor 50 token0 dan mendapatkan kembali 47.619 token11,050.000952.3815,0200.90720
Pedagang B menyetor 10 token0 dan mendapatkan kembali 8.984 token11,060.000943.3965,0300.89020+10*0.907 = 29.07
Pedagang C menyetor 40 token0 dan mendapatkan kembali 34.305 token11,100.000909.0905,1000.82629.07+70*0.890 = 91.37
Pedagang D menyetor 100 token1 dan mendapatkan kembali 109.01 token0990.9901,009.0905,1101.01891.37+10*0.826 = 99.63
Pedagang E menyetor 10 token0 dan mendapatkan kembali 10.079 token11,000.990999.0105,1500.99899.63+40*1.1018 = 143.702

Katakanlah kita ingin menghitung harga rata-rata Token0 antara stempel waktu 5.030 dan 5.150. Perbedaan nilai price0Cumulative adalah 143.702-29.07=114.632. Ini adalah rata-rata selama dua menit (120 detik). Jadi harga rata-ratanya adalah 114.632/120 = 0.955.

Perhitungan harga ini adalah alasan kita perlu mengetahui ukuran cadangan yang lama.

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

Terakhir, perbarui variabel global dan pancarkan peristiwa Sync.

_mintFee
1 // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k) // jika biaya aktif, mint likuiditas yang setara dengan 1/6 dari pertumbuhan sqrt(k)
2 function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {

Di Uniswap 2.0 pedagang membayar biaya 0,30% untuk menggunakan pasar. Sebagian besar biaya tersebut (0,25% dari perdagangan) selalu diberikan kepada penyedia likuiditas. Sisa 0,05% dapat diberikan kepada penyedia likuiditas atau ke alamat yang ditentukan oleh pabrik sebagai biaya protokol, yang membayar Uniswap untuk upaya pengembangan mereka.

Untuk mengurangi perhitungan (dan karenanya biaya gas), biaya ini hanya dihitung ketika likuiditas ditambahkan atau dihapus dari kolam, bukan pada setiap transaksi.

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

Baca tujuan biaya dari pabrik. Jika nol maka tidak ada biaya protokol dan tidak perlu menghitung biaya tersebut.

1 uint _kLast = kLast; // gas savings // penghematan gas

Variabel status kLast terletak di penyimpanan, sehingga akan memiliki nilai di antara panggilan yang berbeda ke kontrak. Akses ke penyimpanan jauh lebih mahal daripada akses ke memori volatil yang dilepaskan ketika panggilan fungsi ke kontrak berakhir, jadi kita menggunakan variabel internal untuk menghemat gas.

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

Penyedia likuiditas mendapatkan bagian mereka hanya dengan apresiasi token likuiditas mereka. Tetapi biaya protokol mengharuskan token likuiditas baru di-mint dan diberikan ke alamat feeTo.

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

Jika ada likuiditas baru yang dapat dipungut biaya protokol. Anda dapat melihat fungsi akar kuadrat nanti di artikel ini

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

Perhitungan biaya yang rumit ini dijelaskan dalam buku putih (opens in a new tab) di halaman 5. Kita tahu bahwa antara waktu kLast dihitung dan saat ini tidak ada likuiditas yang ditambahkan atau dihapus (karena kita menjalankan perhitungan ini setiap kali likuiditas ditambahkan atau dihapus, sebelum benar-benar berubah), jadi setiap perubahan dalam reserve0 * reserve1 harus berasal dari biaya transaksi (tanpanya kita akan menjaga reserve0 * reserve1 tetap konstan).

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

Gunakan fungsi UniswapV2ERC20._mint untuk benar-benar membuat token likuiditas tambahan dan menetapkannya ke feeTo.

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

Jika tidak ada biaya, atur kLast menjadi nol (jika belum). Ketika kontrak ini ditulis, ada fitur pengembalian dana gas (opens in a new tab) yang mendorong kontrak untuk mengurangi ukuran keseluruhan status Ethereum dengan mengosongkan penyimpanan yang tidak mereka butuhkan. Kode ini mendapatkan pengembalian dana tersebut jika memungkinkan.

Fungsi yang Dapat Diakses Secara Eksternal

Perhatikan bahwa meskipun transaksi atau kontrak apa pun dapat memanggil fungsi-fungsi ini, mereka dirancang untuk dipanggil dari kontrak periferal. Jika Anda memanggilnya secara langsung, Anda tidak akan dapat menipu pertukaran pasangan, tetapi Anda mungkin kehilangan nilai karena kesalahan.

mint
1 // this low-level function should be called from a contract which performs important safety checks // fungsi tingkat rendah ini harus dipanggil dari kontrak yang melakukan pemeriksaan keamanan penting
2 function mint(address to) external lock returns (uint liquidity) {

Fungsi ini dipanggil ketika penyedia likuiditas menambahkan likuiditas ke kolam. Ini me-mint token likuiditas tambahan sebagai hadiah. Ini harus dipanggil dari kontrak periferal yang memanggilnya setelah menambahkan likuiditas dalam transaksi yang sama (sehingga tidak ada orang lain yang dapat mengirimkan transaksi yang mengklaim likuiditas baru sebelum pemilik yang sah).

1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings // penghematan gas

Ini adalah cara membaca hasil dari fungsi Solidity yang mengembalikan beberapa nilai. Kita membuang nilai yang dikembalikan terakhir, stempel waktu blok, karena kita tidak membutuhkannya.

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

Dapatkan saldo saat ini dan lihat berapa banyak yang ditambahkan dari setiap jenis token.

1 bool feeOn = _mintFee(_reserve0, _reserve1);

Hitung biaya protokol yang akan dikumpulkan, jika ada, dan mint token likuiditas yang sesuai. Karena parameter untuk _mintFee adalah nilai cadangan lama, biaya dihitung secara akurat hanya berdasarkan perubahan kolam akibat biaya.

1 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee // penghematan gas, harus didefinisikan di sini karena totalSupply dapat diperbarui di _mintFee
2 if (_totalSupply == 0) {
3 liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
4 _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens // mengunci secara permanen token MINIMUM_LIQUIDITY pertama

Jika ini adalah setoran pertama, buat token MINIMUM_LIQUIDITY dan kirimkan ke alamat nol untuk menguncinya. Mereka tidak akan pernah dapat ditebus, yang berarti kolam tidak akan pernah dikosongkan sepenuhnya (ini menyelamatkan kita dari pembagian dengan nol di beberapa tempat). Nilai MINIMUM_LIQUIDITY adalah seribu, yang mengingat sebagian besar ERC-20 dibagi lagi menjadi unit 10^-18 dari sebuah token, seperti ETH dibagi menjadi wei, adalah 10^-15 dari nilai satu token. Bukan biaya yang tinggi.

Pada saat setoran pertama kita tidak mengetahui nilai relatif dari kedua token, jadi kita hanya mengalikan jumlahnya dan mengambil akar kuadrat, dengan asumsi bahwa setoran tersebut memberi kita nilai yang sama di kedua token.

Kita dapat mempercayai ini karena merupakan kepentingan penyetor untuk memberikan nilai yang sama, untuk menghindari kehilangan nilai akibat arbitrase. Katakanlah nilai kedua token identik, tetapi penyetor kita menyetor Token1 empat kali lebih banyak daripada Token0. Seorang pedagang dapat menggunakan fakta bahwa pertukaran pasangan berpikir bahwa Token0 lebih berharga untuk mengekstrak nilai darinya.

Peristiwareserve0reserve1reserve0 * reserve1Nilai kolam (reserve0 + reserve1)
Pengaturan awal83225640
Pedagang menyetor 8 token Token0, mendapatkan kembali 16 Token1161625632

Seperti yang Anda lihat, pedagang mendapatkan tambahan 8 token, yang berasal dari pengurangan nilai kolam, merugikan penyetor yang memilikinya.

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

Dengan setiap setoran berikutnya kita sudah mengetahui nilai tukar antara kedua aset, dan kita mengharapkan penyedia likuiditas untuk memberikan nilai yang sama di keduanya. Jika tidak, kita memberi mereka token likuiditas berdasarkan nilai yang lebih kecil yang mereka berikan sebagai hukuman.

Baik itu setoran awal atau setoran berikutnya, jumlah token likuiditas yang kita berikan sama dengan akar kuadrat dari perubahan dalam reserve0*reserve1 dan nilai token likuiditas tidak berubah (kecuali kita mendapatkan setoran yang tidak memiliki nilai yang sama dari kedua jenis, dalam hal ini "denda" didistribusikan). Berikut adalah contoh lain dengan dua token yang memiliki nilai yang sama, dengan tiga setoran yang baik dan satu yang buruk (setoran hanya satu jenis token, sehingga tidak menghasilkan token likuiditas apa pun).

Peristiwareserve0reserve1reserve0 * reserve1Nilai kolam (reserve0 + reserve1)Token likuiditas yang di-mint untuk setoran iniTotal token likuiditasnilai setiap token likuiditas
Pengaturan awal8.0008.0006416.000882.000
Setor empat dari setiap jenis12.00012.00014424.0004122.000
Setor dua dari setiap jenis14.00014.00019628.0002142.000
Setoran nilai tidak sama18.00014.00025232.000014~2.286
Setelah arbitrase~15.874~15.874252~31.748014~2.267
1 }
2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
3 _mint(to, liquidity);

Gunakan fungsi UniswapV2ERC20._mint untuk benar-benar membuat token likuiditas tambahan dan memberikannya ke akun yang benar.

1
2 _update(balance0, balance1, _reserve0, _reserve1);
3 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date // reserve0 dan reserve1 sudah diperbarui
4 emit Mint(msg.sender, amount0, amount1);
5 }

Perbarui variabel status (reserve0, reserve1, dan jika perlu kLast) dan pancarkan peristiwa yang sesuai.

burn
1 // this low-level function should be called from a contract which performs important safety checks // fungsi tingkat rendah ini harus dipanggil dari kontrak yang melakukan pemeriksaan keamanan penting
2 function burn(address to) external lock returns (uint amount0, uint amount1) {

Fungsi ini dipanggil ketika likuiditas ditarik dan token likuiditas yang sesuai perlu dibakar (burn). Ini juga harus dipanggil dari akun periferal.

1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings // penghematan gas
2 address _token0 = token0; // gas savings // penghematan gas
3 address _token1 = token1; // gas savings // penghematan gas
4 uint balance0 = IERC20(_token0).balanceOf(address(this));
5 uint balance1 = IERC20(_token1).balanceOf(address(this));
6 uint liquidity = balanceOf[address(this)];

Kontrak periferal mentransfer likuiditas yang akan dibakar ke kontrak ini sebelum panggilan. Dengan begitu kita tahu berapa banyak likuiditas yang harus dibakar, dan kita dapat memastikan bahwa itu dibakar.

1 bool feeOn = _mintFee(_reserve0, _reserve1);
2 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee // penghematan gas, harus didefinisikan di sini karena totalSupply dapat diperbarui di _mintFee
3 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution // menggunakan saldo memastikan distribusi pro-rata
4 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution // menggunakan saldo memastikan distribusi pro-rata
5 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');

Penyedia likuiditas menerima nilai yang sama dari kedua token. Dengan cara ini kita tidak mengubah nilai tukar.

1 _burn(address(this), liquidity);
2 _safeTransfer(_token0, to, amount0);
3 _safeTransfer(_token1, to, amount1);
4 balance0 = IERC20(_token0).balanceOf(address(this));
5 balance1 = IERC20(_token1).balanceOf(address(this));
6
7 _update(balance0, balance1, _reserve0, _reserve1);
8 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date // reserve0 dan reserve1 sudah diperbarui
9 emit Burn(msg.sender, amount0, amount1, to);
10 }
11
Tampilkan semua

Sisa dari fungsi burn adalah cerminan dari fungsi mint di atas.

swap
1 // this low-level function should be called from a contract which performs important safety checks // fungsi tingkat rendah ini harus dipanggil dari kontrak yang melakukan pemeriksaan keamanan penting
2 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {

Fungsi ini juga seharusnya dipanggil dari kontrak periferal.

1 require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
2 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings // penghematan gas
3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
4
5 uint balance0;
6 uint balance1;
7 { // scope for _token{0,1}, avoids stack too deep errors // cakupan untuk _token{0,1}, menghindari kesalahan stack too deep

Variabel lokal dapat disimpan baik di memori atau, jika jumlahnya tidak terlalu banyak, langsung di tumpukan (stack). Jika kita dapat membatasi jumlahnya sehingga kita akan menggunakan tumpukan, kita menggunakan lebih sedikit gas. Untuk detail lebih lanjut lihat kertas kuning, spesifikasi formal Ethereum (opens in a new tab), hal. 26, persamaan 298.

1 address _token0 = token0;
2 address _token1 = token1;
3 require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
4 if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens // mentransfer token secara optimis
5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens // mentransfer token secara optimis

Transfer ini bersifat optimis, karena kita mentransfer sebelum kita yakin semua kondisi terpenuhi. Ini tidak masalah di Ethereum karena jika kondisi tidak terpenuhi di kemudian hari dalam panggilan, kita mengembalikannya (revert) dan membatalkan setiap perubahan yang dibuatnya.

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

Beri tahu penerima tentang tukar jika diminta.

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

Dapatkan saldo saat ini. Kontrak periferal mengirimi kita token sebelum memanggil kita untuk tukar. Ini memudahkan kontrak untuk memeriksa bahwa ia tidak ditipu, sebuah pemeriksaan yang harus terjadi di kontrak inti (karena kita dapat dipanggil oleh entitas lain selain kontrak periferal kita).

1 uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
2 uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
3 require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
4 { // scope for reserve{0,1}Adjusted, avoids stack too deep errors // cakupan untuk reserve{0,1}Adjusted, menghindari kesalahan 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');

Ini adalah pemeriksaan kewarasan (sanity check) untuk memastikan kita tidak rugi dari tukar. Tidak ada keadaan di mana tukar harus mengurangi reserve0*reserve1. Ini juga di mana kita memastikan biaya 0,3% dikirim pada tukar; sebelum memeriksa kewarasan nilai K, kita mengalikan kedua saldo dengan 1000 dikurangi jumlah yang dikalikan dengan 3, ini berarti 0,3% (3/1000 = 0,003 = 0,3%) dipotong dari saldo sebelum membandingkan nilai K-nya dengan nilai K cadangan saat ini.

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

Perbarui reserve0 dan reserve1, dan jika perlu akumulator harga dan stempel waktu serta pancarkan peristiwa.

Sync atau Skim

Ada kemungkinan saldo riil menjadi tidak sinkron dengan cadangan yang menurut pertukaran pasangan dimilikinya. Tidak ada cara untuk menarik token tanpa persetujuan kontrak, tetapi setoran adalah masalah yang berbeda. Sebuah akun dapat mentransfer token ke pertukaran tanpa memanggil mint atau swap.

Dalam hal ini ada dua solusi:

  • sync, perbarui cadangan ke saldo saat ini
  • skim, tarik jumlah ekstra. Perhatikan bahwa akun mana pun diizinkan untuk memanggil skim karena kita tidak tahu siapa yang menyetor token. Informasi ini dipancarkan dalam sebuah peristiwa, tetapi peristiwa tidak dapat diakses dari blockchain.
1 // force balances to match reserves // memaksa saldo agar sesuai dengan cadangan
2 function skim(address to) external lock {
3 address _token0 = token0; // gas savings // penghematan gas
4 address _token1 = token1; // gas savings // penghematan gas
5 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
6 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
7 }
8
9
10
11 // force reserves to match balances // memaksa cadangan agar sesuai dengan saldo
12 function sync() external lock {
13 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
14 }
15}
Tampilkan semua

UniswapV2Factory.sol

Kontrak ini (opens in a new tab) membuat pertukaran pasangan.

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;

Variabel status ini diperlukan untuk mengimplementasikan biaya protokol (lihat buku putih (opens in a new tab), hal. 5). Alamat feeTo mengakumulasi token likuiditas untuk biaya protokol, dan feeToSetter adalah alamat yang diizinkan untuk mengubah feeTo ke alamat yang berbeda.

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

Variabel-variabel ini melacak pasangan, pertukaran antara dua jenis token.

Yang pertama, getPair, adalah pemetaan yang mengidentifikasi kontrak pertukaran pasangan berdasarkan dua token ERC-20 yang ditukarnya. Token ERC-20 diidentifikasi oleh alamat kontrak yang mengimplementasikannya, sehingga kunci dan nilainya semuanya adalah alamat. Untuk mendapatkan alamat pertukaran pasangan yang memungkinkan Anda mengonversi dari tokenA ke tokenB, Anda menggunakan getPair[<alamat tokenA>][<alamat tokenB>] (atau sebaliknya).

Variabel kedua, allPairs, adalah array yang mencakup semua alamat pertukaran pasangan yang dibuat oleh pabrik ini. Di Ethereum Anda tidak dapat melakukan iterasi pada konten pemetaan, atau mendapatkan daftar semua kunci, jadi variabel ini adalah satu-satunya cara untuk mengetahui pertukaran mana yang dikelola pabrik ini.

Catatan: Alasan Anda tidak dapat melakukan iterasi pada semua kunci pemetaan adalah karena penyimpanan data kontrak itu mahal, jadi semakin sedikit kita menggunakannya semakin baik, dan semakin jarang kita mengubahnya semakin baik. Anda dapat membuat pemetaan yang mendukung iterasi (opens in a new tab), tetapi mereka memerlukan penyimpanan ekstra untuk daftar kunci. Di sebagian besar aplikasi Anda tidak membutuhkannya.

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

Peristiwa ini dipancarkan ketika pertukaran pasangan baru dibuat. Ini mencakup alamat token, alamat pertukaran pasangan, dan jumlah total pertukaran yang dikelola oleh pabrik.

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

Satu-satunya hal yang dilakukan konstruktor adalah menentukan feeToSetter. Pabrik mulai tanpa biaya, dan hanya feeSetter yang dapat mengubahnya.

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

Fungsi ini mengembalikan jumlah pasangan pertukaran.

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

Ini adalah fungsi utama pabrik, untuk membuat pertukaran pasangan antara dua token ERC-20. Perhatikan bahwa siapa pun dapat memanggil fungsi ini. Anda tidak memerlukan izin dari Uniswap untuk membuat pertukaran pasangan baru.

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

Kita ingin alamat pertukaran baru menjadi deterministik, sehingga dapat dihitung sebelumnya secara offchain (ini dapat berguna untuk transaksi layer 2). Untuk melakukan ini kita perlu memiliki urutan alamat token yang konsisten, terlepas dari urutan kita menerimanya, jadi kita mengurutkannya di sini.

1 require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
2 require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient // pemeriksaan tunggal sudah cukup

Kolam likuiditas yang besar lebih baik daripada yang kecil, karena mereka memiliki harga yang lebih stabil. Kita tidak ingin memiliki lebih dari satu kolam likuiditas per pasangan token. Jika sudah ada pertukaran, tidak perlu membuat yang lain untuk pasangan yang sama.

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

Untuk membuat kontrak baru kita memerlukan kode yang membuatnya (baik fungsi konstruktor maupun kode yang menulis ke memori bytecode EVM dari kontrak aktual). Biasanya di Solidity kita hanya menggunakan addr = new <nama kontrak>(<parameter konstruktor>) dan kompiler mengurus semuanya untuk kita, tetapi untuk memiliki alamat kontrak yang deterministik kita perlu menggunakan opcode CREATE2 (opens in a new tab). Ketika kode ini ditulis, opcode tersebut belum didukung oleh Solidity, sehingga perlu untuk mendapatkan kode secara manual. Ini tidak lagi menjadi masalah, karena Solidity sekarang mendukung CREATE2 (opens in a new tab).

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

Ketika sebuah opcode belum didukung oleh Solidity, kita dapat memanggilnya menggunakan assembly sebaris (inline assembly) (opens in a new tab).

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

Panggil fungsi initialize untuk memberi tahu pertukaran baru dua token apa yang ditukarnya.

1 getPair[token0][token1] = pair;
2 getPair[token1][token0] = pair; // populate mapping in the reverse direction // mengisi pemetaan ke arah sebaliknya
3 allPairs.push(pair);
4 emit PairCreated(token0, token1, pair, allPairs.length);
5 }

Simpan informasi pasangan baru dalam variabel status dan pancarkan peristiwa untuk memberi tahu dunia tentang pertukaran pasangan baru.

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}
Tampilkan semua

Kedua fungsi ini memungkinkan feeSetter untuk mengontrol penerima biaya (jika ada), dan untuk mengubah feeSetter ke alamat baru.

UniswapV2ERC20.sol

Kontrak ini (opens in a new tab) mengimplementasikan token likuiditas ERC-20. Ini mirip dengan kontrak ERC-20 OpenZeppelin, jadi saya hanya akan menjelaskan bagian yang berbeda, yaitu fungsionalitas permit.

Transaksi di Ethereum membutuhkan biaya ether (ETH), yang setara dengan uang sungguhan. Jika Anda memiliki token ERC-20 tetapi tidak memiliki ETH, Anda tidak dapat mengirim transaksi, sehingga Anda tidak dapat melakukan apa pun dengannya. Salah satu solusi untuk menghindari masalah ini adalah transaksi meta (opens in a new tab). Pemilik token menandatangani transaksi yang memungkinkan orang lain untuk menarik token secara offchain dan mengirimkannya menggunakan Internet ke penerima. Penerima, yang memiliki ETH, kemudian mengirimkan izin (permit) atas nama pemilik.

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

Hash ini adalah pengidentifikasi untuk jenis transaksi (opens in a new tab). Satu-satunya yang kita dukung di sini adalah Permit dengan parameter-parameter ini.

1 mapping(address => uint) public nonces;

Tidak mungkin bagi penerima untuk memalsukan tanda tangan digital. Namun, sangat mudah untuk mengirim transaksi yang sama dua kali (ini adalah bentuk serangan replay (opens in a new tab)). Untuk mencegah hal ini, kita menggunakan nonce (opens in a new tab). Jika nonce dari Permit baru tidak lebih satu dari yang terakhir digunakan, kita menganggapnya tidak valid.

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

Ini adalah kode untuk mengambil pengidentifikasi rantai (opens in a new tab). Ini menggunakan dialek assembly EVM yang disebut Yul (opens in a new tab). Perhatikan bahwa dalam versi Yul saat ini Anda harus menggunakan chainid(), bukan 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 }
Tampilkan semua

Hitung pemisah domain (opens in a new tab) untuk EIP-712.

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

Ini adalah fungsi yang mengimplementasikan izin. Ini menerima sebagai parameter bidang yang relevan, dan tiga nilai skalar untuk tanda tangan (opens in a new tab) (v, r, dan s).

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

Jangan terima transaksi setelah tenggat waktu.

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(...) adalah pesan yang kita harapkan untuk didapatkan. Kita tahu apa seharusnya nonce tersebut, jadi tidak perlu bagi kita untuk mendapatkannya sebagai parameter.

Algoritma tanda tangan Ethereum mengharapkan untuk mendapatkan 256 bit untuk ditandatangani, jadi kita menggunakan fungsi hash keccak256.

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

Dari intisari (digest) dan tanda tangan kita bisa mendapatkan alamat yang menandatanganinya menggunakan ecrecover (opens in a new tab).

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

Jika semuanya baik-baik saja, perlakukan ini sebagai persetujuan (approve) ERC-20 (opens in a new tab).

Kontrak Periphery

Kontrak periphery adalah API (antarmuka program aplikasi) untuk Uniswap. Kontrak ini tersedia untuk panggilan eksternal, baik dari kontrak lain maupun aplikasi terdesentralisasi. Anda dapat memanggil kontrak inti secara langsung, tetapi itu lebih rumit dan Anda mungkin kehilangan nilai jika melakukan kesalahan. Kontrak inti hanya berisi pengujian untuk memastikan mereka tidak dicurangi, bukan pemeriksaan kewarasan (sanity checks) untuk orang lain. Pemeriksaan tersebut ada di periphery sehingga dapat diperbarui sesuai kebutuhan.

UniswapV2Router01.sol

Kontrak ini (opens in a new tab) memiliki masalah, dan tidak boleh lagi digunakan (opens in a new tab). Untungnya, kontrak periphery bersifat tanpa status (stateless) dan tidak menyimpan aset apa pun, sehingga mudah untuk menghentikan penggunaannya dan menyarankan orang-orang untuk menggunakan penggantinya, UniswapV2Router02.

UniswapV2Router02.sol

Dalam kebanyakan kasus, Anda akan menggunakan Uniswap melalui kontrak ini (opens in a new tab). Anda dapat melihat cara menggunakannya di sini (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';
Tampilkan semua

Sebagian besar dari ini pernah kita temui sebelumnya, atau cukup jelas. Satu-satunya pengecualian adalah IWETH.sol. Uniswap v2 memungkinkan pertukaran untuk pasangan token ERC-20 apa pun, tetapi ether (ETH) itu sendiri bukanlah token ERC-20. ETH mendahului standar tersebut dan ditransfer dengan mekanisme yang unik. Untuk memungkinkan penggunaan ETH dalam kontrak yang berlaku untuk token ERC-20, orang-orang menciptakan kontrak wrapped ether (WETH) (opens in a new tab). Anda mengirimkan ETH ke kontrak ini, dan kontrak tersebut akan melakukan mint sejumlah WETH yang setara untuk Anda. Atau Anda dapat membakar WETH, dan mendapatkan ETH kembali.

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

Router perlu mengetahui factory mana yang akan digunakan, dan untuk transaksi yang memerlukan WETH, kontrak WETH mana yang akan digunakan. Nilai-nilai ini bersifat tetap (opens in a new tab), yang berarti mereka hanya dapat diatur di dalam konstruktor. Hal ini memberikan keyakinan kepada pengguna bahwa tidak ada yang dapat mengubahnya untuk menunjuk ke kontrak yang kurang jujur.

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

Modifier ini memastikan bahwa transaksi dengan batas waktu ("lakukan X sebelum waktu Y jika Anda bisa") tidak terjadi setelah batas waktunya.

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

Konstruktor hanya mengatur variabel status yang tetap.

1 receive() external payable {
2 assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract // hanya menerima ETH melalui fallback dari kontrak WETH
3 }

Fungsi ini dipanggil ketika kita menebus token dari kontrak WETH kembali menjadi ETH. Hanya kontrak WETH yang kita gunakan yang berwenang untuk melakukan itu.

Tambah Likuiditas

Fungsi-fungsi ini menambahkan token ke pertukaran pasangan, yang meningkatkan kolam likuiditas.

1
2 // **** ADD LIQUIDITY **** // **** TAMBAH LIKUIDITAS ****
3 function _addLiquidity(

Fungsi ini digunakan untuk menghitung jumlah token A dan B yang harus disetorkan ke dalam pertukaran pasangan.

1 address tokenA,
2 address tokenB,

Ini adalah alamat dari kontrak token ERC-20.

1 uint amountADesired,
2 uint amountBDesired,

Ini adalah jumlah yang ingin disetorkan oleh penyedia likuiditas. Ini juga merupakan jumlah maksimum A dan B yang akan disetorkan.

1 uint amountAMin,
2 uint amountBMin

Ini adalah jumlah minimum yang dapat diterima untuk disetorkan. Jika transaksi tidak dapat terjadi dengan jumlah ini atau lebih, batalkan (revert) transaksi tersebut. Jika Anda tidak menginginkan fitur ini, cukup tentukan nol.

Penyedia likuiditas biasanya menentukan nilai minimum, karena mereka ingin membatasi transaksi pada nilai tukar yang mendekati nilai saat ini. Jika nilai tukar berfluktuasi terlalu banyak, itu mungkin berarti ada berita yang mengubah nilai yang mendasarinya, dan mereka ingin memutuskan secara manual apa yang harus dilakukan.

Sebagai contoh, bayangkan sebuah kasus di mana nilai tukarnya adalah satu banding satu dan penyedia likuiditas menentukan nilai-nilai ini:

ParameterNilai
amountADesired1000
amountBDesired1000
amountAMin900
amountBMin800

Selama nilai tukar tetap berada di antara 0.9 dan 1.25, transaksi akan terjadi. Jika nilai tukar keluar dari kisaran tersebut, transaksi akan dibatalkan.

Alasan dari tindakan pencegahan ini adalah karena transaksi tidak terjadi seketika, Anda mengirimkannya dan pada akhirnya seorang validator akan memasukkannya ke dalam sebuah blok (kecuali jika harga gas Anda sangat rendah, dalam hal ini Anda harus mengirimkan transaksi lain dengan nonce yang sama dan harga gas yang lebih tinggi untuk menimpanya). Anda tidak dapat mengontrol apa yang terjadi selama interval antara pengiriman dan penyertaan.

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

Fungsi ini mengembalikan jumlah yang harus disetorkan oleh penyedia likuiditas untuk memiliki rasio yang sama dengan rasio saat ini di antara cadangan.

1 // create the pair if it doesn't exist yet // buat pasangan jika belum ada
2 if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
3 IUniswapV2Factory(factory).createPair(tokenA, tokenB);
4 }

Jika belum ada pertukaran untuk pasangan token ini, buatlah.

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

Dapatkan cadangan saat ini dalam pasangan tersebut.

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

Jika cadangan saat ini kosong, maka ini adalah pertukaran pasangan baru. Jumlah yang akan disetorkan harus sama persis dengan yang ingin diberikan oleh penyedia likuiditas.

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

Jika kita perlu melihat berapa jumlahnya, kita mendapatkan jumlah optimal menggunakan fungsi ini (opens in a new tab). Kita menginginkan rasio yang sama dengan cadangan saat ini.

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

Jika amountBOptimal lebih kecil dari jumlah yang ingin disetorkan oleh penyedia likuiditas, itu berarti token B saat ini lebih berharga daripada yang diperkirakan oleh penyetor likuiditas, sehingga jumlah yang lebih kecil diperlukan.

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

Jika jumlah B yang optimal lebih dari jumlah B yang diinginkan, itu berarti token B saat ini kurang berharga daripada yang diperkirakan oleh penyetor likuiditas, sehingga jumlah yang lebih tinggi diperlukan. Namun, jumlah yang diinginkan adalah maksimum, jadi kita tidak dapat melakukannya. Sebagai gantinya, kita menghitung jumlah token A yang optimal untuk jumlah token B yang diinginkan.

Menggabungkan semuanya, kita mendapatkan grafik ini. Asumsikan Anda mencoba menyetorkan seribu token A (garis biru) dan seribu token B (garis merah). Sumbu x adalah nilai tukar, A/B. Jika x=1, nilainya sama dan Anda menyetorkan seribu untuk masing-masing token. Jika x=2, nilai A dua kali lipat dari B (Anda mendapatkan dua token B untuk setiap token A) sehingga Anda menyetorkan seribu token B, tetapi hanya 500 token A. Jika x=0.5, situasinya terbalik, seribu token A dan lima ratus token B.

Graph

Anda dapat menyetorkan likuiditas secara langsung ke dalam kontrak inti (menggunakan UniswapV2Pair::mint (opens in a new tab)), tetapi kontrak inti hanya memeriksa bahwa ia tidak dicurangi, sehingga Anda berisiko kehilangan nilai jika nilai tukar berubah antara waktu Anda mengirimkan transaksi dan waktu pelaksanaannya. Jika Anda menggunakan kontrak periphery, kontrak tersebut akan menghitung jumlah yang harus Anda setorkan dan segera menyetorkannya, sehingga nilai tukar tidak berubah dan Anda tidak kehilangan apa pun.

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
Tampilkan semua

Fungsi ini dapat dipanggil oleh sebuah transaksi untuk menyetorkan likuiditas. Sebagian besar parameter sama dengan _addLiquidity di atas, dengan dua pengecualian:

. to adalah alamat yang mendapatkan token likuiditas baru yang di-mint untuk menunjukkan porsi penyedia likuiditas di dalam kolam . deadline adalah batas waktu pada transaksi

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

Kita menghitung jumlah yang benar-benar akan disetorkan dan kemudian menemukan alamat kolam likuiditas. Untuk menghemat gas, kita tidak melakukan ini dengan bertanya kepada factory, melainkan menggunakan fungsi pustaka pairFor (lihat di bawah pada bagian pustaka)

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

Transfer jumlah token yang benar dari pengguna ke dalam pertukaran pasangan.

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

Sebagai imbalannya, berikan alamat to token likuiditas untuk kepemilikan sebagian dari kolam tersebut. Fungsi mint dari kontrak inti melihat berapa banyak token ekstra yang dimilikinya (dibandingkan dengan yang dimilikinya saat terakhir kali likuiditas berubah) dan melakukan mint likuiditas yang sesuai.

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

Ketika penyedia likuiditas ingin menyediakan likuiditas ke pertukaran pasangan Token/ETH, ada beberapa perbedaan. Kontrak menangani pembungkusan (wrapping) ETH untuk penyedia likuiditas. Tidak perlu menentukan berapa banyak ETH yang ingin disetorkan pengguna, karena pengguna hanya mengirimkannya bersama transaksi (jumlahnya tersedia di 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));
Tampilkan semua

Untuk menyetorkan ETH, kontrak pertama-tama membungkusnya menjadi WETH dan kemudian mentransfer WETH ke dalam pasangan tersebut. Perhatikan bahwa transfer dibungkus dalam sebuah assert. Ini berarti bahwa jika transfer gagal, panggilan kontrak ini juga gagal, dan oleh karena itu pembungkusan tidak benar-benar terjadi.

1 liquidity = IUniswapV2Pair(pair).mint(to);
2 // refund dust eth, if any // kembalikan sisa eth (dust), jika ada
3 if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
4 }

Pengguna telah mengirimi kita ETH, jadi jika ada sisa (karena token lainnya kurang berharga dari yang diperkirakan pengguna), kita perlu mengeluarkan pengembalian dana (refund).

Hapus Likuiditas

Fungsi-fungsi ini akan menghapus likuiditas dan membayar kembali penyedia likuiditas.

1 // **** REMOVE LIQUIDITY **** // **** HAPUS LIKUIDITAS ****
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) {
Tampilkan semua

Kasus paling sederhana dalam menghapus likuiditas. Ada jumlah minimum dari setiap token yang disetujui untuk diterima oleh penyedia likuiditas, dan itu harus terjadi sebelum tenggat waktu.

1 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
2 IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair // kirim likuiditas ke pasangan
3 (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);

Fungsi burn dari kontrak inti menangani pembayaran kembali token kepada pengguna.

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

Ketika sebuah fungsi mengembalikan beberapa nilai, tetapi kita hanya tertarik pada beberapa di antaranya, beginilah cara kita hanya mendapatkan nilai-nilai tersebut. Ini agak lebih murah dalam hal gas daripada membaca sebuah nilai dan tidak pernah menggunakannya.

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

Terjemahkan jumlah dari cara kontrak inti mengembalikannya (token dengan alamat lebih rendah terlebih dahulu) ke cara yang diharapkan pengguna (sesuai dengan tokenA dan tokenB).

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

Tidak masalah untuk melakukan transfer terlebih dahulu dan kemudian memverifikasi bahwa itu sah, karena jika tidak, kita akan membatalkan (revert) semua perubahan status.

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 }
Tampilkan semua

Menghapus likuiditas untuk ETH hampir sama, kecuali bahwa kita menerima token WETH dan kemudian menebusnya dengan ETH untuk dikembalikan kepada penyedia likuiditas.

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 }
Tampilkan semua

Fungsi-fungsi ini meneruskan transaksi meta (meta-transactions) untuk memungkinkan pengguna tanpa ether menarik dana dari kolam, menggunakan mekanisme permit.

1
2 // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) **** // **** HAPUS LIKUIDITAS (mendukung token dengan biaya transfer) ****
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
Tampilkan semua

Fungsi ini dapat digunakan untuk token yang memiliki biaya transfer atau penyimpanan. Ketika sebuah token memiliki biaya seperti itu, kita tidak dapat mengandalkan fungsi removeLiquidity untuk memberi tahu kita berapa banyak token yang kita dapatkan kembali, jadi kita perlu menariknya terlebih dahulu dan kemudian mendapatkan saldonya.

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 }
Tampilkan semua

Fungsi terakhir menggabungkan biaya penyimpanan dengan transaksi meta.

Perdagangan

1 // **** SWAP **** // **** TUKAR ****
2 // requires the initial amount to have already been sent to the first pair // mengharuskan jumlah awal sudah dikirim ke pasangan pertama
3 function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {

Fungsi ini melakukan pemrosesan internal yang diperlukan untuk fungsi-fungsi yang diekspos kepada pedagang.

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

Saat saya menulis ini, ada 388.160 token ERC-20 (opens in a new tab). Jika ada pertukaran pasangan untuk setiap pasangan token, akan ada lebih dari 150 miliar pertukaran pasangan. Seluruh chain, pada saat ini, hanya memiliki 0,1% dari jumlah akun tersebut (opens in a new tab). Sebagai gantinya, fungsi tukar mendukung konsep jalur (path). Seorang pedagang dapat menukar A dengan B, B dengan C, dan C dengan D, sehingga tidak perlu ada pertukaran pasangan A-D secara langsung.

Harga di pasar-pasar ini cenderung tersinkronisasi, karena ketika tidak sinkron, hal itu menciptakan peluang untuk arbitrase. Bayangkan, misalnya, tiga token, A, B, dan C. Ada tiga pertukaran pasangan, satu untuk setiap pasangan.

  1. Situasi awal
  2. Seorang pedagang menjual 24.695 token A dan mendapatkan 25.305 token B.
  3. Pedagang menjual 24.695 token B untuk 25.305 token C, menyimpan sekitar 0.61 token B sebagai keuntungan.
  4. Kemudian pedagang menjual 24.695 token C untuk 25.305 token A, menyimpan sekitar 0.61 token C sebagai keuntungan. Pedagang juga memiliki 0.61 token A ekstra (25.305 yang akhirnya dimiliki pedagang, dikurangi investasi awal sebesar 24.695).
LangkahPertukaran A-BPertukaran B-CPertukaran 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];

Dapatkan pasangan yang sedang kita tangani, urutkan (untuk digunakan dengan pasangan tersebut) dan dapatkan jumlah output yang diharapkan.

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

Dapatkan jumlah keluar yang diharapkan, diurutkan sesuai dengan yang diharapkan oleh pertukaran pasangan.

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

Apakah ini pertukaran terakhir? Jika ya, kirim token yang diterima untuk perdagangan ke tujuan. Jika tidak, kirimkan ke pertukaran pasangan berikutnya.

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

Benar-benar memanggil pertukaran pasangan untuk menukar token. Kita tidak memerlukan callback untuk diberi tahu tentang pertukaran tersebut, jadi kita tidak mengirim byte apa pun di bidang itu.

1 function swapExactTokensForTokens(

Fungsi ini digunakan secara langsung oleh pedagang untuk menukar satu token dengan token lainnya.

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

Parameter ini berisi alamat dari kontrak ERC-20. Seperti yang dijelaskan di atas, ini adalah sebuah array karena Anda mungkin perlu melalui beberapa pertukaran pasangan untuk mendapatkan aset yang Anda inginkan dari aset yang Anda miliki.

Parameter fungsi dalam solidity dapat disimpan baik di memory maupun calldata. Jika fungsi tersebut adalah titik masuk ke kontrak, dipanggil langsung dari pengguna (menggunakan transaksi) atau dari kontrak yang berbeda, maka nilai parameter dapat diambil langsung dari data panggilan (call data). Jika fungsi dipanggil secara internal, seperti _swap di atas, maka parameter harus disimpan di memory. Dari perspektif kontrak yang dipanggil, calldata bersifat hanya-baca (read only).

Dengan tipe skalar seperti uint atau address, kompiler menangani pilihan penyimpanan untuk kita, tetapi dengan array, yang lebih panjang dan lebih mahal, kita menentukan tipe penyimpanan yang akan digunakan.

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

Nilai kembalian selalu dikembalikan di dalam memori.

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

Hitung jumlah yang akan dibeli di setiap pertukaran. Jika hasilnya kurang dari minimum yang bersedia diterima oleh pedagang, batalkan (revert) transaksi tersebut.

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

Terakhir, transfer token ERC-20 awal ke akun untuk pertukaran pasangan pertama dan panggil _swap. Ini semua terjadi dalam transaksi yang sama, sehingga pertukaran pasangan tahu bahwa setiap token yang tidak terduga adalah bagian dari transfer ini.

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 }
Tampilkan semua

Fungsi sebelumnya, swapTokensForTokens, memungkinkan seorang pedagang untuk menentukan jumlah pasti token input yang bersedia ia berikan dan jumlah minimum token output yang bersedia ia terima sebagai imbalannya. Fungsi ini melakukan pertukaran sebaliknya, ini memungkinkan seorang pedagang menentukan jumlah token output yang ia inginkan, dan jumlah maksimum token input yang bersedia ia bayarkan untuk itu.

Dalam kedua kasus tersebut, pedagang harus terlebih dahulu memberikan allowance (izin) kepada kontrak periphery ini agar dapat mentransfernya.

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 // refund dust eth, if any // kembalikan sisa eth (dust), jika ada
72 if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
73 }
Tampilkan semua

Keempat varian ini semuanya melibatkan perdagangan antara ETH dan token. Satu-satunya perbedaan adalah bahwa kita menerima ETH dari pedagang dan menggunakannya untuk melakukan mint WETH, atau kita menerima WETH dari pertukaran terakhir di jalur tersebut dan membakarnya, mengirimkan kembali ETH yang dihasilkan kepada pedagang.

1 // **** SWAP (supporting fee-on-transfer tokens) **** // **** TUKAR (mendukung token dengan biaya transfer) ****
2 // requires the initial amount to have already been sent to the first pair // mengharuskan jumlah awal sudah dikirim ke pasangan pertama
3 function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {

Ini adalah fungsi internal untuk menukar token yang memiliki biaya transfer atau penyimpanan untuk menyelesaikan (masalah ini (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 { // scope to avoid stack too deep errors // cakupan untuk menghindari kesalahan 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);
Tampilkan semua

Karena adanya biaya transfer, kita tidak dapat mengandalkan fungsi getAmountsOut untuk memberi tahu kita berapa banyak yang kita dapatkan dari setiap transfer (seperti yang kita lakukan sebelum memanggil _swap yang asli). Sebagai gantinya, kita harus mentransfer terlebih dahulu dan kemudian melihat berapa banyak token yang kita dapatkan kembali.

Catatan: Secara teori kita bisa saja menggunakan fungsi ini alih-alih _swap, tetapi dalam kasus tertentu (misalnya, jika transfer akhirnya dibatalkan karena tidak ada cukup dana pada akhirnya untuk memenuhi minimum yang disyaratkan) itu akan berakhir dengan menghabiskan lebih banyak gas. Token dengan biaya transfer cukup langka, jadi meskipun kita perlu mengakomodasinya, tidak perlu semua pertukaran berasumsi bahwa mereka melalui setidaknya satu dari token tersebut.

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 }
Tampilkan semua

Ini adalah varian yang sama yang digunakan untuk token normal, tetapi mereka memanggil _swapSupportingFeeOnTransferTokens sebagai gantinya.

1 // **** LIBRARY FUNCTIONS **** // **** FUNGSI PUSTAKA ****
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}
Tampilkan semua

Fungsi-fungsi ini hanyalah proxy yang memanggil fungsi UniswapV2Library.

UniswapV2Migrator.sol

Kontrak ini digunakan untuk memigrasikan pertukaran dari v1 lama ke v2. Sekarang setelah mereka dimigrasikan, kontrak ini tidak lagi relevan.

Pustaka

Pustaka SafeMath (opens in a new tab) didokumentasikan dengan baik, jadi tidak perlu mendokumentasikannya di sini.

Math

Pustaka ini berisi beberapa fungsi matematika yang biasanya tidak diperlukan dalam kode Solidity, sehingga tidak menjadi bagian dari bahasa tersebut.

1pragma solidity =0.5.16;
2
3// a library for performing various math operations // pustaka untuk melakukan berbagai operasi matematika
4
5library Math {
6 function min(uint x, uint y) internal pure returns (uint z) {
7 z = x < y ? x : y;
8 }
9
10 // babylonian method (https://wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) // metode babilonia (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;
Tampilkan semua

Mulai dengan x sebagai perkiraan yang lebih tinggi dari akar kuadrat (itulah alasan kita perlu memperlakukan 1-3 sebagai kasus khusus).

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

Dapatkan perkiraan yang lebih dekat, rata-rata dari perkiraan sebelumnya dan angka yang akar kuadratnya sedang kita coba temukan dibagi dengan perkiraan sebelumnya. Ulangi hingga perkiraan baru tidak lebih rendah dari yang sudah ada. Untuk detail lebih lanjut, lihat di sini (opens in a new tab).

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

Kita seharusnya tidak pernah membutuhkan akar kuadrat dari nol. Akar kuadrat dari satu, dua, dan tiga kira-kira satu (kita menggunakan bilangan bulat, jadi kita mengabaikan pecahannya).

1 }
2 }
3}

Pecahan Titik Tetap (UQ112x112)

Pustaka ini menangani pecahan, yang biasanya bukan bagian dari aritmatika Ethereum. Ini dilakukan dengan menyandikan angka x sebagai x*2^112. Ini memungkinkan kita menggunakan opcode penambahan dan pengurangan asli tanpa perubahan.

1pragma solidity =0.5.16;
2
3// a library for handling binary fixed point numbers (https://wikipedia.org/wiki/Q_(number_format)) // pustaka untuk menangani angka titik tetap biner (https://wikipedia.org/wiki/Q_(number_format))
4
5// range: [0, 2**112 - 1] // rentang: [0, 2**112 - 1]
6// resolution: 1 / 2**112 // resolusi: 1 / 2**112
7
8library UQ112x112 {
9 uint224 constant Q112 = 2**112;
Tampilkan semua

Q112 adalah penyandian untuk satu.

1 // encode a uint112 as a UQ112x112 // menyandikan uint112 sebagai UQ112x112
2 function encode(uint112 y) internal pure returns (uint224 z) {
3 z = uint224(y) * Q112; // never overflows // tidak pernah overflow
4 }

Karena y adalah uint112, nilai maksimalnya adalah 2^112-1. Angka tersebut masih dapat disandikan sebagai UQ112x112.

1 // divide a UQ112x112 by a uint112, returning a UQ112x112 // membagi UQ112x112 dengan uint112, mengembalikan UQ112x112
2 function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
3 z = x / uint224(y);
4 }
5}

Jika kita membagi dua nilai UQ112x112, hasilnya tidak lagi dikalikan dengan 2^112. Jadi sebagai gantinya kita mengambil bilangan bulat untuk penyebutnya. Kita akan membutuhkan trik serupa untuk melakukan perkalian, tetapi kita tidak perlu melakukan perkalian nilai UQ112x112.

UniswapV2Library

Pustaka ini hanya digunakan oleh kontrak pinggiran (periphery)

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 // returns sorted token addresses, used to handle return values from pairs sorted in this order // mengembalikan alamat token yang diurutkan, digunakan untuk menangani nilai kembalian dari pasangan yang diurutkan dalam urutan ini
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 }
Tampilkan semua

Urutkan kedua token berdasarkan alamat, sehingga kita akan dapat memperoleh alamat pertukaran pasangan untuk keduanya. Ini diperlukan karena jika tidak, kita akan memiliki dua kemungkinan, satu untuk parameter A,B dan satu lagi untuk parameter B,A, yang mengarah ke dua pertukaran alih-alih satu.

1 // calculates the CREATE2 address for a pair without making any external calls // menghitung alamat CREATE2 untuk pasangan tanpa melakukan panggilan eksternal apa pun
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' // init code hash // hash kode inisialisasi
9 ))));
10 }
Tampilkan semua

Fungsi ini menghitung alamat pertukaran pasangan untuk kedua token. Kontrak ini dibuat menggunakan opcode CREATE2 (opens in a new tab), sehingga kita dapat menghitung alamat menggunakan algoritma yang sama jika kita mengetahui parameter yang digunakannya. Ini jauh lebih murah daripada bertanya kepada pabrik (factory), dan

1 // fetches and sorts the reserves for a pair // mengambil dan mengurutkan cadangan untuk pasangan
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 }

Fungsi ini mengembalikan cadangan dari kedua token yang dimiliki oleh pertukaran pasangan. Perhatikan bahwa fungsi ini dapat menerima token dalam urutan apa pun, dan mengurutkannya untuk penggunaan internal.

1 // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset // diberikan sejumlah aset dan cadangan pasangan, mengembalikan jumlah yang setara dari aset lainnya
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 }

Fungsi ini memberi Anda jumlah token B yang akan Anda dapatkan sebagai imbalan untuk token A jika tidak ada biaya yang terlibat. Perhitungan ini memperhitungkan bahwa transfer mengubah nilai tukar.

1 // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset // diberikan jumlah input aset dan cadangan pasangan, mengembalikan jumlah output maksimum dari aset lainnya
2 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {

Fungsi quote di atas berfungsi dengan baik jika tidak ada biaya untuk menggunakan pertukaran pasangan. Namun, jika ada biaya pertukaran 0,3%, jumlah yang sebenarnya Anda dapatkan lebih rendah. Fungsi ini menghitung jumlah setelah biaya pertukaran.

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 tidak menangani pecahan secara bawaan, jadi kita tidak bisa begitu saja mengalikan jumlahnya dengan 0,997. Sebagai gantinya, kita mengalikan pembilang dengan 997 dan penyebut dengan 1000, mencapai efek yang sama.

1 // given an output amount of an asset and pair reserves, returns a required input amount of the other asset // diberikan jumlah output aset dan cadangan pasangan, mengembalikan jumlah input yang diperlukan dari aset lainnya
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 }

Fungsi ini melakukan hal yang kurang lebih sama, tetapi mendapatkan jumlah keluaran dan menyediakan masukannya.

1
2 // performs chained getAmountOut calculations on any number of pairs // melakukan perhitungan getAmountOut berantai pada sejumlah pasangan
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 // performs chained getAmountIn calculations on any number of pairs // melakukan perhitungan getAmountIn berantai pada sejumlah pasangan
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}
Tampilkan semua

Kedua fungsi ini menangani identifikasi nilai ketika perlu melalui beberapa pertukaran pasangan.

Transfer Helper

Pustaka ini (opens in a new tab) menambahkan pemeriksaan keberhasilan di sekitar transfer ERC-20 dan Ethereum untuk memperlakukan pengembalian (revert) dan pengembalian nilai false dengan cara yang sama.

1// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
2
3pragma solidity >=0.6.0;
4
5// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false // metode pembantu untuk berinteraksi dengan token ERC20 dan mengirim ETH yang tidak secara konsisten mengembalikan 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)'))); // bytes4(keccak256(bytes('approve(address,uint256)')));
13 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));
14
Tampilkan semua

Kita dapat memanggil kontrak yang berbeda dengan salah satu dari dua cara:

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

Demi kompatibilitas mundur dengan token yang dibuat sebelum standar ERC-20, panggilan ERC-20 dapat gagal baik dengan mengembalikan (dalam hal ini success adalah false) atau dengan berhasil dan mengembalikan nilai false (dalam hal ini ada data keluaran, dan jika Anda mendekodenya sebagai boolean, Anda mendapatkan false).

1
2
3 function safeTransfer(
4 address token,
5 address to,
6 uint256 value
7 ) internal {
8 // bytes4(keccak256(bytes('transfer(address,uint256)'))); // 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 }
Tampilkan semua

Fungsi ini mengimplementasikan fungsionalitas transfer ERC-20 (opens in a new tab), yang memungkinkan sebuah akun untuk menghabiskan jatah yang disediakan oleh akun yang berbeda.

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)'))); // 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 }
Tampilkan semua

Fungsi ini mengimplementasikan fungsionalitas transferFrom ERC-20 (opens in a new tab), yang memungkinkan sebuah akun untuk menghabiskan jatah yang disediakan oleh akun yang berbeda.

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}

Fungsi ini mentransfer ether ke sebuah akun. Panggilan apa pun ke kontrak yang berbeda dapat mencoba mengirim ether. Karena kita tidak perlu benar-benar memanggil fungsi apa pun, kita tidak mengirim data apa pun dengan panggilan tersebut.

Kesimpulan

Ini adalah artikel panjang sekitar 50 halaman. Jika Anda berhasil sampai di sini, selamat! Semoga sekarang Anda telah memahami pertimbangan dalam menulis aplikasi di kehidupan nyata (berbeda dengan program sampel pendek) dan lebih mampu menulis kontrak untuk kasus penggunaan Anda sendiri.

Sekarang pergilah dan tulis sesuatu yang berguna dan buat kami takjub.

Lihat di sini untuk karya saya yang lain (opens in a new tab).

Pembaruan terakhir halaman: 25 Februari 2026

Apakah tutorial ini membantu?