Ana içeriğe atla

Uniswap-v2 Sözleşmesi İncelemesi

Solidity
dapp'ler
Orta
Ori Pomerantz
1 Mayıs 2021
51 dakikalık okuma

Giriş

Uniswap v2 (opens in a new tab), herhangi iki ERC-20 token'ı arasında bir takas piyasası oluşturabilir. Bu makalede, bu protokolü uygulayan sözleşmelerin kaynak kodunu inceleyecek ve neden bu şekilde yazıldıklarını göreceğiz.

Uniswap Ne Yapar?

Temel olarak iki tür kullanıcı vardır: likidite sağlayıcılar ve alım satım yapanlar.

Likidite sağlayıcılar, havuza takas edilebilecek iki token'ı sağlarlar (bunları Token0 ve Token1 olarak adlandıracağız). Karşılığında, havuzun kısmi sahipliğini temsil eden ve likidite token'ı olarak adlandırılan üçüncü bir token alırlar.

Alım satım yapanlar, havuza bir tür token gönderir ve likidite sağlayıcılar tarafından sağlanan havuzdan diğerini alırlar (örneğin, Token0 gönderip Token1 alırlar). Takas kuru, havuzun sahip olduğu Token0 ve Token1'lerin göreceli sayısına göre belirlenir. Buna ek olarak havuz, likidite havuzu için bir ödül olarak küçük bir yüzde alır.

Likidite sağlayıcılar varlıklarını geri istediklerinde, havuz token'larının yakımını gerçekleştirebilir ve ödüllerdeki payları da dahil olmak üzere token'larını geri alabilirler.

Daha kapsamlı bir açıklama için buraya tıklayın (opens in a new tab).

Neden v2? Neden v3 değil?

Uniswap v3 (opens in a new tab), v2'den çok daha karmaşık olan bir yükseltmedir. Önce v2'yi öğrenip ardından v3'e geçmek daha kolaydır.

Çekirdek Sözleşmeler ve Çevre Sözleşmeleri

Uniswap v2, çekirdek ve çevre olmak üzere iki bileşene ayrılmıştır. Bu ayrım, varlıkları tutan ve bu nedenle güvenli olması gereken çekirdek sözleşmelerin daha basit ve denetlenmesinin daha kolay olmasını sağlar. Alım satım yapanların ihtiyaç duyduğu tüm ekstra işlevsellik daha sonra çevre sözleşmeleri tarafından sağlanabilir.

Veri ve Kontrol Akışları

Uniswap'ın üç ana eylemini gerçekleştirdiğinizde meydana gelen veri ve kontrol akışı şöyledir:

  1. Farklı token'lar arasında takas yapmak
  2. Piyasaya likidite eklemek ve ödül olarak çift borsası ERC-20 likidite token'ları almak
  3. ERC-20 likidite token'larını yakmak ve çift borsasının yatırımcıların takas etmesine izin verdiği ERC-20 token'larını geri almak

Takas

Bu, yatırımcılar tarafından kullanılan en yaygın akıştır:

Çağırıcı

  1. Periphery hesabına takas edilecek miktar kadar harcama izni verin.
  2. Periphery sözleşmesinin birçok takas fonksiyonundan birini çağırın (hangisinin çağrılacağı, ETH'nin dahil olup olmadığına, yatırımcının yatırılacak token miktarını mı yoksa geri alınacak token miktarını mı belirttiğine vb. bağlıdır). Her takas fonksiyonu, üzerinden geçilecek borsaların bir dizisi olan bir path kabul eder.

Periphery sözleşmesinde (UniswapV2Router02.sol)

  1. Yol boyunca her borsada alınıp satılması gereken miktarları belirleyin.
  2. Yol üzerinde yineler. Yol boyunca her borsa için girdi token'ını gönderir ve ardından borsanın swap fonksiyonunu çağırır. Çoğu durumda token'lar için hedef adres, yoldaki bir sonraki çift borsasıdır. Son borsada ise yatırımcı tarafından sağlanan adrestir.

Çekirdek sözleşmede (UniswapV2Pair.sol)

  1. Çekirdek sözleşmenin kandırılmadığını ve takastan sonra yeterli likiditeyi koruyabildiğini doğrulayın.
  2. Bilinen rezervlere ek olarak ne kadar fazladan token'ımız olduğuna bakın. Bu miktar, takas etmek için aldığımız girdi token'larının sayısıdır.
  3. Çıktı token'larını hedefe gönderin.
  4. Rezerv miktarlarını güncellemek için _update çağrısı yapın

Periphery sözleşmesine dönüş (UniswapV2Router02.sol)

  1. Gerekli temizlik işlemlerini gerçekleştirin (örneğin, yatırımcıya gönderilecek ETH'yi geri almak için WETH token'larını yakmak)

Likidite Ekleme

Çağırıcı

  1. Periphery hesabına, likidite havuzuna eklenecek miktarlarda harcama izni verin.
  2. Periphery sözleşmesinin addLiquidity fonksiyonlarından birini çağırın.

Periphery sözleşmesinde (UniswapV2Router02.sol)

  1. Gerekirse yeni bir çift borsası oluşturun
  2. Mevcut bir çift borsası varsa, eklenecek token miktarını hesaplayın. Bunun her iki token için de aynı değerde olması, yani yeni token'ların mevcut token'lara oranının aynı olması gerekir.
  3. Miktarların kabul edilebilir olup olmadığını kontrol edin (çağırıcılar, altına düştüğünde likidite eklememeyi tercih edecekleri minimum bir miktar belirtebilirler)
  4. Çekirdek sözleşmeyi çağırın.

Çekirdek sözleşmede (UniswapV2Pair.sol)

  1. Likidite token'ları basın ve bunları çağırıcıya gönderin
  2. Rezerv miktarlarını güncellemek için _update çağrısı yapın

Likidite Çıkarma

Çağırıcı

  1. Periphery hesabına, dayanak token'lar karşılığında yakılacak likidite token'ları için harcama izni verin.
  2. Periphery sözleşmesinin removeLiquidity fonksiyonlarından birini çağırın.

Periphery sözleşmesinde (UniswapV2Router02.sol)

  1. Likidite token'larını çift borsasına gönderin

Çekirdek sözleşmede (UniswapV2Pair.sol)

  1. Hedef adrese, yakılan token'larla orantılı olarak dayanak token'ları gönderin. Örneğin havuzda 1000 A token'ı, 500 B token'ı ve 90 likidite token'ı varsa ve yakmak için 9 token alırsak, likidite token'larının %10'unu yakıyoruz demektir, bu nedenle kullanıcıya 100 A token'ı ve 50 B token'ı geri göndeririz.
  2. Likidite token'larını yakın
  3. Rezerv miktarlarını güncellemek için _update çağrısı yapın

Çekirdek Sözleşmeler

Bunlar, likiditeyi tutan güvenli sözleşmelerdir.

UniswapV2Pair.sol

Bu sözleşme (opens in a new tab), token'ları takas eden asıl havuzu uygular. Bu, temel Uniswap işlevselliğidir.

Bunlar, sözleşmenin ya kendisi uyguladığı için (IUniswapV2Pair ve UniswapV2ERC20) ya da bunları uygulayan sözleşmeleri çağırdığı için bilmesi gereken tüm arayüzlerdir.

contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {

Bu sözleşme, likidite token'ları için ERC-20 fonksiyonlarını sağlayan UniswapV2ERC20 sözleşmesinden miras alır.

    using SafeMath  for uint;

SafeMath kütüphanesi (opens in a new tab), taşmaları (overflow) ve alt taşmaları (underflow) önlemek için kullanılır. Bu önemlidir çünkü aksi takdirde bir değerin -1 olması gerekirken 2^256-1 olduğu bir durumla karşılaşabiliriz.

    using UQ112x112 for uint224;

Havuz sözleşmesindeki birçok hesaplama kesirler gerektirir. Ancak kesirler EVM tarafından desteklenmez. Uniswap'ın bulduğu çözüm, tam sayı kısmı için 112 bit ve kesir kısmı için 112 bit olmak üzere 224 bitlik değerler kullanmaktır. Yani 1.0, 2^112 olarak temsil edilir, 1.5, 2^112 + 2^111 olarak temsil edilir vb.

Bu kütüphane hakkında daha fazla ayrıntı belgenin ilerleyen kısımlarında mevcuttur.

Değişkenler

    uint public constant MINIMUM_LIQUIDITY = 10**3;

Sıfıra bölme durumlarından kaçınmak için, her zaman var olan (ancak sıfırıncı hesaba ait olan) minimum sayıda likidite token'ı vardır. Bu sayı MINIMUM_LIQUIDITY'dir, yani bindir.

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

Bu, ERC-20 transfer fonksiyonu için ABI seçicisidir. İki token hesabındaki ERC-20 token'larını transfer etmek için kullanılır.

    address public factory;

Bu, bu havuzu oluşturan fabrika sözleşmesidir. Her havuz iki ERC-20 token'ı arasında bir takastır, fabrika tüm bu havuzları birbirine bağlayan merkezi bir noktadır.

    address public token0;
    address public token1;

Bu havuz tarafından takas edilebilen iki tür ERC-20 token'ı için sözleşmelerin adresleri vardır.

    uint112 private reserve0;           // tek bir depolama yuvası kullanır, getReserves aracılığıyla erişilebilir
    uint112 private reserve1;           // tek bir depolama yuvası kullanır, getReserves aracılığıyla erişilebilir

Havuzun her bir token türü için sahip olduğu rezervler. İkisinin de aynı miktarda değeri temsil ettiğini ve bu nedenle her bir token0'ın reserve1/reserve0 token1 değerinde olduğunu varsayıyoruz.

    uint32  private blockTimestampLast; // tek bir depolama yuvası kullanır, getReserves aracılığıyla erişilebilir

Zaman içindeki döviz kurlarını izlemek için kullanılan, bir takasın gerçekleştiği son bloğun zaman damgası.

Ethereum sözleşmelerinin en büyük gaz giderlerinden biri, sözleşmenin bir çağrısından diğerine kalıcı olan depolamadır. Her depolama hücresi 256 bit uzunluğundadır. Bu nedenle reserve0, reserve1 ve blockTimestampLast olmak üzere üç değişken, tek bir depolama değerinin üçünü de içerebileceği şekilde tahsis edilir (112+112+32=256).

    uint public price0CumulativeLast;
    uint public price1CumulativeLast;

Bu değişkenler, her bir token için (her biri diğeri cinsinden) kümülatif maliyetleri tutar. Belirli bir süre boyunca ortalama döviz kurunu hesaplamak için kullanılabilirler.

    uint public kLast; // reserve0 * reserve1, en son Likidite olayından hemen sonraki haliyle

Çift takasının token0 ve token1 arasındaki döviz kuruna karar verme yolu, işlemler sırasında iki rezervin çarpımını sabit tutmaktır. kLast bu değerdir. Bir likidite sağlayıcı token yatırdığında veya çektiğinde değişir ve %0,3'lük piyasa ücreti nedeniyle biraz artar.

İşte basit bir örnek. Basitlik adına tablonun ondalık noktadan sonra sadece üç basamağı olduğuna ve %0,3'lük işlem ücretini göz ardı ettiğimize dikkat edin, bu nedenle sayılar tam olarak doğru değildir.

Olayreserve0reserve1reserve0 * reserve1Ortalama döviz kuru (token1 / token0)
İlk kurulum1,000.0001,000.0001,000,000
Yatırımcı A, 47.619 token1 karşılığında 50 token0 takas eder1,050.000952.3811,000,0000.952
Yatırımcı B, 8.984 token1 karşılığında 10 token0 takas eder1,060.000943.3961,000,0000.898
Yatırımcı C, 34.305 token1 karşılığında 40 token0 takas eder1,100.000909.0901,000,0000.858
Yatırımcı D, 109.01 token0 karşılığında 100 token1 takas eder990.9901,009.0901,000,0000.917
Yatırımcı E, 10.079 token1 karşılığında 10 token0 takas eder1,000.990999.0101,000,0001.008

Yatırımcılar daha fazla token0 sağladıkça, arz ve talebe bağlı olarak token1'in göreceli değeri artar ve bunun tersi de geçerlidir.

Kilit

    uint private unlocked = 1;

Yeniden giriş (reentrancy) istismarına (opens in a new tab) dayanan bir güvenlik açığı sınıfı vardır. Uniswap'ın rastgele ERC-20 token'larını transfer etmesi gerekir, bu da kendilerini çağıran Uniswap piyasasını istismar etmeye çalışabilecek ERC-20 sözleşmelerini çağırmak anlamına gelir. Sözleşmenin bir parçası olarak bir unlocked değişkenine sahip olarak, fonksiyonların çalışırken (aynı işlem içinde) çağrılmasını önleyebiliriz.

    modifier lock() {

Bu fonksiyon bir değiştiricidir (modifier) (opens in a new tab), davranışını bir şekilde değiştirmek için normal bir fonksiyonu saran bir fonksiyondur.

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

Eğer unlocked bire eşitse, onu sıfıra ayarlayın. Zaten sıfırsa çağrıyı geri alın (revert), başarısız olmasını sağlayın.

        _;

Bir değiştiricide _;, orijinal fonksiyon çağrısıdır (tüm parametrelerle birlikte). Burada, fonksiyon çağrısının yalnızca çağrıldığında unlocked bir ise gerçekleştiği ve çalışırken unlocked değerinin sıfır olduğu anlamına gelir.

        unlocked = 1;
    }

Ana fonksiyon döndükten sonra kilidi serbest bırakın.

Çeşitli fonksiyonlar

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

Bu fonksiyon, arayanlara takasın mevcut durumunu sağlar. Solidity fonksiyonlarının birden fazla değer döndürebileceğine (opens in a new tab) dikkat edin.

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

Bu dahili fonksiyon, takastan başka birine bir miktar ERC-20 token'ı transfer eder. SELECTOR, çağırdığımız fonksiyonun transfer(address,uint) olduğunu belirtir (yukarıdaki tanıma bakın).

Token fonksiyonu için bir arayüz içe aktarmak zorunda kalmamak için, ABI fonksiyonlarından (opens in a new tab) birini kullanarak çağrıyı "manuel olarak" oluşturuyoruz.

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

Bir ERC-20 transfer çağrısının başarısızlığı bildirmesinin iki yolu vardır:

  1. Geri al (Revert). Harici bir sözleşmeye yapılan çağrı geri alınırsa, boolean dönüş değeri false olur.
  2. Normal şekilde sonlanır ancak bir başarısızlık bildirir. Bu durumda dönüş değeri arabelleği sıfır olmayan bir uzunluğa sahiptir ve bir boolean değeri olarak çözüldüğünde false olur.

Bu koşullardan herhangi biri gerçekleşirse, geri alın.

Olaylar

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

Bu iki olay, bir likidite sağlayıcı likidite yatırdığında (Mint) veya çektiğinde (Burn) yayınlanır. Her iki durumda da, yatırılan veya çekilen token0 ve token1 miktarları, bizi çağıran hesabın kimliği (sender) ile birlikte olayın bir parçasıdır. Bir çekim durumunda olay, gönderenle aynı olmayabilen, token'ları alan hedefi (to) de içerir.

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

Bu olay, bir yatırımcı bir token'ı diğeriyle takas ettiğinde yayınlanır. Yine, gönderen ve hedef aynı olmayabilir. Her bir token takasa gönderilebilir veya takastan alınabilir.

    event Sync(uint112 reserve0, uint112 reserve1);

Son olarak, en son rezerv bilgisini (ve dolayısıyla döviz kurunu) sağlamak için, nedenden bağımsız olarak token'lar her eklendiğinde veya çekildiğinde Sync yayınlanır.

Kurulum Fonksiyonları

Bu fonksiyonların, yeni çift takası kurulduğunda bir kez çağrılması beklenir.

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

Kurucu (constructor), çifti oluşturan fabrikanın adresini takip edeceğimizden emin olur. Bu bilgi initialize ve (eğer varsa) fabrika ücreti için gereklidir.

    // dağıtım sırasında fabrika tarafından bir kez çağrılır
    function initialize(address _token0, address _token1) external {
        require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // yeterlilik kontrolü
        token0 = _token0;
        token1 = _token1;
    }

Bu fonksiyon, fabrikanın (ve yalnızca fabrikanın) bu çiftin takas edeceği iki ERC-20 token'ını belirlemesine olanak tanır.

Dahili Güncelleme Fonksiyonları

_update
    // rezervleri ve her Blok başına ilk çağrıda fiyat biriktiricilerini günceller
    function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {

Bu fonksiyon, token'lar her yatırıldığında veya çekildiğinde çağrılır.

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

Eğer balance0 veya balance1 (uint256), uint112(-1) (=2^112-1) değerinden yüksekse (böylece uint112'ye dönüştürüldüğünde taşar ve 0'a geri döner), taşmaları önlemek için _update işlemine devam etmeyi reddedin. 10^18 birime bölünebilen normal bir token ile bu, her takasın her bir token'dan yaklaşık 5.1*10^15 ile sınırlı olduğu anlamına gelir. Şimdiye kadar bu bir sorun olmadı.

        uint32 blockTimestamp = uint32(block.timestamp % 2**32);
        uint32 timeElapsed = blockTimestamp - blockTimestampLast; // taşma istenmektedir
        if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {

Geçen süre sıfır değilse, bu bloktaki ilk takas işlemi olduğumuz anlamına gelir. Bu durumda, maliyet biriktiricilerini güncellememiz gerekir.

            // * asla taşmaz ve + taşması istenir
            price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
            price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
        }

Her maliyet biriktiricisi, en son maliyet (diğer token'ın rezervi/bu token'ın rezervi) çarpı saniye cinsinden geçen süre ile güncellenir. Ortalama bir fiyat elde etmek için, iki zaman noktasındaki kümülatif fiyatı okur ve aralarındaki zaman farkına bölersiniz. Örneğin, şu olay dizisini varsayalım:

Olayreserve0reserve1zaman damgasıMarjinal döviz kuru (reserve1 / reserve0)price0CumulativeLast
İlk kurulum1,000.0001,000.0005,0001.0000
Yatırımcı A 50 token0 yatırır ve 47.619 token1 geri alır1,050.000952.3815,0200.90720
Yatırımcı B 10 token0 yatırır ve 8.984 token1 geri alır1,060.000943.3965,0300.89020+10*0.907 = 29.07
Yatırımcı C 40 token0 yatırır ve 34.305 token1 geri alır1,100.000909.0905,1000.82629.07+70*0.890 = 91.37
Yatırımcı D 100 token1 yatırır ve 109.01 token0 geri alır990.9901,009.0905,1101.01891.37+10*0.826 = 99.63
Yatırımcı E 10 token0 yatırır ve 10.079 token1 geri alır1,000.990999.0105,1500.99899.63+40*1.1018 = 143.702

Diyelim ki 5.030 ve 5.150 zaman damgaları arasında Token0'ın ortalama fiyatını hesaplamak istiyoruz. price0Cumulative değerindeki fark 143.702-29.07=114.632'dir. Bu, iki dakika (120 saniye) boyunca ortalamadır. Yani ortalama fiyat 114.632/120 = 0.955'tir.

Bu fiyat hesaplaması, eski rezerv boyutlarını bilmemiz gerekmesinin nedenidir.

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

Son olarak, global değişkenleri güncelleyin ve bir Sync olayı yayınlayın.

_mintFee
    // eğer ücret açıksa, sqrt(k)'daki büyümenin 1/6'sına eşdeğer Likidite bas
    function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {

Uniswap 2.0'da yatırımcılar piyasayı kullanmak için %0,30 ücret öderler. Bu ücretin çoğu (işlemin %0,25'i) her zaman likidite sağlayıcılara gider. Kalan %0,05'lik kısım ya likidite sağlayıcılara ya da Uniswap'a geliştirme çabaları için ödeme yapan bir protokol ücreti olarak fabrika tarafından belirlenen bir adrese gidebilir.

Hesaplamaları (ve dolayısıyla gaz maliyetlerini) azaltmak için bu ücret, her işlemde değil, yalnızca havuza likidite eklendiğinde veya havuzdan çıkarıldığında hesaplanır.

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

Fabrikanın ücret hedefini okuyun. Eğer sıfırsa, protokol ücreti yoktur ve bu ücreti hesaplamaya gerek yoktur.

        uint _kLast = kLast; // Gaz tasarrufu

kLast durum değişkeni depolamada bulunur, bu nedenle sözleşmeye yapılan farklı çağrılar arasında bir değere sahip olacaktır. Depolamaya erişim, sözleşmeye yapılan fonksiyon çağrısı sona erdiğinde serbest bırakılan geçici belleğe erişimden çok daha pahalıdır, bu nedenle gazdan tasarruf etmek için dahili bir değişken kullanırız.

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

Likidite sağlayıcılar paylarını sadece likidite token'larının değer kazanmasıyla alırlar. Ancak protokol ücreti, yeni likidite token'larının basılmasını ve feeTo adresine sağlanmasını gerektirir.

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

Protokol ücreti tahsil edilecek yeni bir likidite varsa. Karekök fonksiyonunu bu makalenin ilerleyen kısımlarında görebilirsiniz.

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

Ücretlerin bu karmaşık hesaplaması tanıtım belgesinin (opens in a new tab) 5. sayfasında açıklanmıştır. kLast değerinin hesaplandığı zaman ile şu an arasında hiçbir likiditenin eklenmediğini veya çıkarılmadığını biliyoruz (çünkü bu hesaplamayı her likidite eklendiğinde veya çıkarıldığında, gerçekten değişmeden önce çalıştırıyoruz), bu nedenle reserve0 * reserve1 değerindeki herhangi bir değişiklik işlem ücretlerinden kaynaklanmalıdır (onlar olmasaydı reserve0 * reserve1 değerini sabit tutardık).

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

Ek likidite token'larını gerçekten oluşturmak ve bunları feeTo adresine atamak için UniswapV2ERC20._mint fonksiyonunu kullanın.

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

Eğer bir ücret yoksa kLast değerini sıfıra ayarlayın (zaten öyle değilse). Bu sözleşme yazıldığında, sözleşmeleri ihtiyaç duymadıkları depolamayı sıfırlayarak Ethereum durumunun genel boyutunu küçültmeye teşvik eden bir gaz iadesi özelliği (opens in a new tab) vardı. Bu kod, mümkün olduğunda bu iadeyi alır.

Dışarıdan Erişilebilir Fonksiyonlar

Herhangi bir işlem veya sözleşme bu fonksiyonları çağırabilse de, bunların çevre (periphery) sözleşmesinden çağrılmak üzere tasarlandığını unutmayın. Bunları doğrudan çağırırsanız çift takasını kandıramazsınız, ancak bir hata nedeniyle değer kaybedebilirsiniz.

mint
    // bu düşük seviyeli fonksiyon, önemli güvenlik kontrollerini gerçekleştiren bir Sözleşme tarafından çağrılmalıdır
    function mint(address to) external lock returns (uint liquidity) {

Bu fonksiyon, bir likidite sağlayıcı havuza likidite eklediğinde çağrılır. Ödül olarak ek likidite token'ları basar. Aynı işlemde likiditeyi ekledikten sonra onu çağıran bir çevre sözleşmesinden çağrılmalıdır (böylece başka hiç kimse meşru sahibinden önce yeni likiditeyi talep eden bir işlem gönderemez).

        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // Gaz tasarrufu

Bu, birden fazla değer döndüren bir Solidity fonksiyonunun sonuçlarını okumanın yoludur. İhtiyacımız olmadığı için döndürülen son değerleri, yani blok zaman damgasını atıyoruz.

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

Mevcut bakiyeleri alın ve her bir token türünden ne kadar eklendiğini görün.

        bool feeOn = _mintFee(_reserve0, _reserve1);

Varsa tahsil edilecek protokol ücretlerini hesaplayın ve buna göre likidite token'ları basın. _mintFee parametreleri eski rezerv değerleri olduğundan, ücret yalnızca ücretlerden kaynaklanan havuz değişikliklerine göre doğru bir şekilde hesaplanır.

        uint _totalSupply = totalSupply; // Gaz tasarrufu, totalSupply _mintFee içinde güncellenebileceğinden burada tanımlanmalıdır
        if (_totalSupply == 0) {
            liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
           _mint(address(0), MINIMUM_LIQUIDITY); // ilk MINIMUM_LIQUIDITY Token'larını kalıcı olarak kilitle

Eğer bu ilk yatırma işlemiyse, MINIMUM_LIQUIDITY token oluşturun ve bunları kilitlemek için sıfır adresine gönderin. Bunlar asla kullanılamaz, bu da havuzun asla tamamen boşaltılmayacağı anlamına gelir (bu bizi bazı yerlerde sıfıra bölünmekten kurtarır). MINIMUM_LIQUIDITY değeri bindir, bu da çoğu ERC-20'nin ETH'nin Wei'ye bölünmesi gibi bir token'ın 10^-18'i birimlerine bölündüğü düşünüldüğünde, tek bir token'ın değerinin 10^-15'idir. Yüksek bir maliyet değil.

İlk yatırma işlemi sırasında iki token'ın göreceli değerini bilmiyoruz, bu nedenle yatırma işleminin bize her iki token'da da eşit değer sağladığını varsayarak miktarları çarpıyor ve karekökünü alıyoruz.

Buna güvenebiliriz çünkü arbitraj nedeniyle değer kaybetmekten kaçınmak için eşit değer sağlamak yatırıcının çıkarınadır. Diyelim ki iki token'ın değeri aynı, ancak yatırıcımız Token0'ın dört katı kadar Token1 yatırdı. Bir yatırımcı, çift takasının Token0'ın daha değerli olduğunu düşünmesi gerçeğini kullanarak ondan değer elde edebilir.

Olayreserve0reserve1reserve0 * reserve1Havuzun değeri (reserve0 + reserve1)
İlk kurulum83225640
Yatırımcı 8 Token0 token'ı yatırır, 16 Token1 geri alır161625632

Gördüğünüz gibi, yatırımcı havuzun değerindeki bir düşüşten kaynaklanan fazladan 8 token kazandı ve bu da ona sahip olan yatırıcıya zarar verdi.

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

Sonraki her yatırma işleminde iki varlık arasındaki döviz kurunu zaten biliyoruz ve likidite sağlayıcıların her ikisinde de eşit değer sağlamasını bekliyoruz. Eğer yapmazlarsa, onlara bir ceza olarak sağladıkları daha düşük değere göre likidite token'ları veririz.

İster ilk yatırma işlemi ister sonraki bir işlem olsun, sağladığımız likidite token'larının sayısı reserve0*reserve1 değerindeki değişimin kareköküne eşittir ve likidite token'ının değeri değişmez (her iki türün de eşit değerlerine sahip olmayan bir yatırma işlemi almadığımız sürece, bu durumda "ceza" dağıtılır). İşte aynı değere sahip iki token ile üç iyi yatırma işlemi ve bir kötü yatırma işlemi (yalnızca bir token türünün yatırılması, bu nedenle herhangi bir likidite token'ı üretmez) içeren başka bir örnek.

Olayreserve0reserve1reserve0 * reserve1Havuz değeri (reserve0 + reserve1)Bu yatırma işlemi için basılan likidite token'larıToplam likidite token'larıher bir likidite token'ının değeri
İlk kurulum8.0008.0006416.000882.000
Her türden dört tane yatırın12.00012.00014424.0004122.000
Her türden iki tane yatırın14.00014.00019628.0002142.000
Eşit olmayan değerde yatırma18.00014.00025232.000014~2.286
Arbitraj sonrası~15.874~15.874252~31.748014~2.267
        }
        require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
        _mint(to, liquidity);

Ek likidite token'larını gerçekten oluşturmak ve bunları doğru hesaba vermek için UniswapV2ERC20._mint fonksiyonunu kullanın.


        _update(balance0, balance1, _reserve0, _reserve1);
        if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 ve reserve1 günceldir
        emit Mint(msg.sender, amount0, amount1);
    }

Durum değişkenlerini (reserve0, reserve1 ve gerekirse kLast) güncelleyin ve uygun olayı yayınlayın.

burn
    // bu düşük seviyeli fonksiyon, önemli güvenlik kontrollerini gerçekleştiren bir Sözleşme tarafından çağrılmalıdır
    function burn(address to) external lock returns (uint amount0, uint amount1) {

Bu fonksiyon, likidite çekildiğinde ve uygun likidite token'larının yakılması gerektiğinde çağrılır. Ayrıca bir çevre hesabından çağrılmalıdır.

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

Çevre sözleşmesi, çağrıdan önce yakılacak likiditeyi bu sözleşmeye transfer etti. Bu şekilde ne kadar likidite yakacağımızı biliyoruz ve yakıldığından emin olabiliyoruz.

        bool feeOn = _mintFee(_reserve0, _reserve1);
        uint _totalSupply = totalSupply; // Gaz tasarrufu, totalSupply _mintFee içinde güncellenebileceğinden burada tanımlanmalıdır
        amount0 = liquidity.mul(balance0) / _totalSupply; // bakiyelerin kullanılması oransal dağılımı sağlar
        amount1 = liquidity.mul(balance1) / _totalSupply; // bakiyelerin kullanılması oransal dağılımı sağlar
        require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');

Likidite sağlayıcı her iki token'dan da eşit değer alır. Bu şekilde döviz kurunu değiştirmemiş oluruz.

burn fonksiyonunun geri kalanı, yukarıdaki mint fonksiyonunun ayna görüntüsüdür.

swap
    // bu düşük seviyeli fonksiyon, önemli güvenlik kontrollerini gerçekleştiren bir Sözleşme tarafından çağrılmalıdır
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {

Bu fonksiyonun da bir çevre sözleşmesinden çağrılması beklenir.

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

        uint balance0;
        uint balance1;
        { // _token{0,1} için kapsam, yığın çok derin hatalarını önler

Yerel değişkenler bellekte veya çok fazla yoksa doğrudan yığında (stack) saklanabilir. Sayıyı sınırlandırabilirsek, yığını kullanacağımız için daha az gaz kullanırız. Daha fazla ayrıntı için bkz. Sarı Bülten, resmi Ethereum spesifikasyonları (opens in a new tab), s. 26, denklem 298.

            address _token0 = token0;
            address _token1 = token1;
            require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
            if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // Token'ları iyimser bir şekilde transfer et
            if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // Token'ları iyimser bir şekilde transfer et

Bu transfer iyimserdir, çünkü tüm koşulların karşılandığından emin olmadan önce transfer ederiz. Bu Ethereum'da sorun değildir çünkü çağrının ilerleyen kısımlarında koşullar karşılanmazsa, işlemi ve yarattığı tüm değişiklikleri geri alırız (revert).

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

İstenirse alıcıyı takas hakkında bilgilendirin.

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

Mevcut bakiyeleri alın. Çevre sözleşmesi, takas için bizi çağırmadan önce bize token'ları gönderir. Bu, sözleşmenin kandırılmadığını kontrol etmesini kolaylaştırır; bu kontrol çekirdek sözleşmede gerçekleşmek zorundadır (çünkü çevre sözleşmemiz dışındaki diğer varlıklar tarafından da çağrılabiliriz).

        uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
        uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
        require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
        { // reserve{0,1}Adjusted için kapsam, yığın çok derin hatalarını önler
            uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
            uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
            require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');

Bu, takastan kaybetmediğimizden emin olmak için bir mantık kontrolüdür. Bir takasın reserve0*reserve1 değerini düşürmesi gereken hiçbir durum yoktur. Burası aynı zamanda takasta %0,3'lük bir ücretin gönderildiğinden emin olduğumuz yerdir; K değerini mantık kontrolünden geçirmeden önce, her iki bakiyeyi 1000 ile çarpıp miktarların 3 ile çarpımını çıkarıyoruz, bu da K değerini mevcut rezervlerin K değeriyle karşılaştırmadan önce bakiyeden %0,3'ün (3/1000 = 0,003 = %0,3) düşüldüğü anlamına gelir.

        }

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

reserve0 ve reserve1 değerlerini ve gerekirse fiyat biriktiricilerini ve zaman damgasını güncelleyin ve bir olay yayınlayın.

Sync veya Skim

Gerçek bakiyelerin, çift takasının sahip olduğunu düşündüğü rezervlerle eşzamanlamasının bozulması mümkündür. Sözleşmenin izni olmadan token çekmenin bir yolu yoktur, ancak yatırma işlemleri farklı bir konudur. Bir hesap, mint veya swap fonksiyonlarını çağırmadan takasa token transfer edebilir.

Bu durumda iki çözüm vardır:

  • sync, rezervleri mevcut bakiyelere güncelleyin
  • skim, ekstra miktarı çekin. Token'ları kimin yatırdığını bilmediğimiz için herhangi bir hesabın skim çağırmasına izin verildiğini unutmayın. Bu bilgi bir olayda yayınlanır, ancak olaylara blokzincirden erişilemez.

UniswapV2Factory.sol

Bu sözleşme (opens in a new tab) çift takaslarını oluşturur.

pragma solidity =0.5.16;

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

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

Bu durum değişkenleri protokol ücretini uygulamak için gereklidir (bkz. tanıtım belgesi (opens in a new tab), s. 5). feeTo adresi protokol ücreti için likidite token'larını biriktirir ve feeToSetter, feeTo adresini farklı bir adresle değiştirmesine izin verilen adrestir.

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

Bu değişkenler çiftleri, yani iki token türü arasındaki takasları takip eder.

İlki olan getPair, takas ettiği iki ERC-20 token'ına dayalı olarak bir çift takas sözleşmesini tanımlayan bir eşlemedir (mapping). ERC-20 token'ları, onları uygulayan sözleşmelerin adresleriyle tanımlanır, bu nedenle anahtarlar ve değerlerin tümü adreslerdir. tokenA'den tokenB'ya dönüştürmenizi sağlayan çift takasının adresini almak için getPair[<tokenA address>][<tokenB address>] kullanırsınız (veya tam tersi).

İkinci değişken olan allPairs, bu fabrika tarafından oluşturulan çift takaslarının tüm adreslerini içeren bir dizidir. Ethereum'da bir eşlemenin içeriği üzerinde yineleme yapamazsınız veya tüm anahtarların bir listesini alamazsınız, bu nedenle bu değişken bu fabrikanın hangi takasları yönettiğini bilmenin tek yoludur.

Not: Bir eşlemenin tüm anahtarları üzerinde yineleme yapamamanızın nedeni, sözleşme veri depolamasının pahalı olmasıdır, bu nedenle ne kadar az kullanırsak o kadar iyidir ve ne kadar az değiştirirsek o kadar iyidir. Yinelemeyi destekleyen eşlemeler (opens in a new tab) oluşturabilirsiniz, ancak bunlar bir anahtar listesi için ekstra depolama alanı gerektirir. Çoğu uygulamada buna ihtiyacınız yoktur.

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

Bu olay, yeni bir çift takası oluşturulduğunda yayınlanır. Token'ların adreslerini, çift takasının adresini ve fabrika tarafından yönetilen toplam takas sayısını içerir.

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

Kurucunun yaptığı tek şey feeToSetter adresini belirlemektir. Fabrikalar ücretsiz başlar ve bunu yalnızca feeSetter değiştirebilir.

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

Bu fonksiyon takas çiftlerinin sayısını döndürür.

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

Bu, fabrikanın iki ERC-20 token'ı arasında bir çift takası oluşturmak için ana fonksiyonudur. Herkesin bu fonksiyonu çağırabileceğini unutmayın. Yeni bir çift takası oluşturmak için Uniswap'tan izin almanıza gerek yoktur.

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

Yeni takasın adresinin deterministik olmasını istiyoruz, böylece zincir dışı olarak önceden hesaplanabilir (bu, katman 2 (l2) işlemleri için yararlı olabilir). Bunu yapmak için, onları hangi sırayla aldığımıza bakılmaksızın token adreslerinin tutarlı bir sırasına sahip olmamız gerekir, bu yüzden onları burada sıralıyoruz.

        require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
        require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // tek bir kontrol yeterlidir

Büyük likidite havuzları küçük olanlardan daha iyidir, çünkü daha istikrarlı fiyatlara sahiptirler. Token çifti başına birden fazla likidite havuzuna sahip olmak istemiyoruz. Zaten bir takas varsa, aynı çift için başka bir tane oluşturmaya gerek yoktur.

        bytes memory bytecode = type(UniswapV2Pair).creationCode;

Yeni bir sözleşme oluşturmak için onu oluşturan koda ihtiyacımız var (hem kurucu fonksiyon hem de asıl sözleşmenin EVM baytkodunu belleğe yazan kod). Normalde Solidity'de sadece addr = new <name of contract>(<constructor parameters>) kullanırız ve derleyici bizim için her şeyi halleder, ancak deterministik bir sözleşme adresine sahip olmak için CREATE2 işlem kodunu (opens in a new tab) kullanmamız gerekir. Bu kod yazıldığında bu işlem kodu henüz Solidity tarafından desteklenmiyordu, bu nedenle kodu manuel olarak almak gerekiyordu. Solidity artık CREATE2'yi desteklediği (opens in a new tab) için bu artık bir sorun değil.

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

Bir işlem kodu henüz Solidity tarafından desteklenmediğinde, onu satır içi çevirici (inline assembly) (opens in a new tab) kullanarak çağırabiliriz.

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

Yeni takasa hangi iki token'ı takas ettiğini söylemek için initialize fonksiyonunu çağırın.

        getPair[token0][token1] = pair;
        getPair[token1][token0] = pair; // eşlemeyi ters yönde doldur
        allPairs.push(pair);
        emit PairCreated(token0, token1, pair, allPairs.length);
    }

Yeni çift bilgisini durum değişkenlerine kaydedin ve dünyayı yeni çift takası hakkında bilgilendirmek için bir olay yayınlayın.

Bu iki fonksiyon, feeSetter adresinin ücret alıcısını (varsa) kontrol etmesine ve feeSetter adresini yeni bir adresle değiştirmesine olanak tanır.

UniswapV2ERC20.sol

Bu sözleşme (opens in a new tab) ERC-20 likidite token'ını uygular. OpenZeppelin ERC-20 sözleşmesine benzer, bu yüzden sadece farklı olan kısmı, yani permit işlevselliğini açıklayacağım.

Ethereum'daki işlemler, gerçek paraya eşdeğer olan Ether (ETH) maliyetindedir. ERC-20 token'larınız var ancak ETH'niz yoksa, işlem gönderemezsiniz, bu nedenle onlarla hiçbir şey yapamazsınız. Bu sorunu önlemenin bir çözümü meta işlemlerdir (opens in a new tab). Token'ların sahibi, başka birinin zincir dışı olarak token çekmesine izin veren bir işlemi imzalar ve bunu İnternet'i kullanarak alıcıya gönderir. ETH'si olan alıcı, daha sonra izni sahibi adına sunar.

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

Bu hash, işlem türü için tanımlayıcıdır (opens in a new tab). Burada desteklediğimiz tek şey bu parametrelere sahip Permit işlemidir.

    mapping(address => uint) public nonces;

Bir alıcının dijital imzayı taklit etmesi mümkün değildir. Ancak, aynı işlemi iki kez göndermek önemsizdir (bu bir tür tekrarlama saldırısıdır (replay attack) (opens in a new tab)). Bunu önlemek için bir nonce (opens in a new tab) kullanırız. Yeni bir Permit işleminin nonce değeri kullanılan son değerden bir fazla değilse, geçersiz olduğunu varsayarız.

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

Bu, zincir tanımlayıcısını (opens in a new tab) almak için kullanılan koddur. Yul (opens in a new tab) adı verilen bir EVM çevirici lehçesi kullanır. Yul'un mevcut sürümünde chainid değil, chainid() kullanmanız gerektiğine dikkat edin.

EIP-712 için alan ayırıcısını (domain separator) (opens in a new tab) hesaplayın.

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

Bu, izinleri uygulayan fonksiyondur. İlgili alanları ve imza (opens in a new tab) için üç skaler değeri (v, r ve s) parametre olarak alır.

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

Son teslim tarihinden sonraki işlemleri kabul etmeyin.

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

abi.encodePacked(...) almayı beklediğimiz mesajdır. Nonce değerinin ne olması gerektiğini biliyoruz, bu yüzden onu bir parametre olarak almamıza gerek yok.

Ethereum imza algoritması imzalamak için 256 bit almayı bekler, bu nedenle keccak256 hash fonksiyonunu kullanırız.

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

Özet (digest) ve imzadan, ecrecover (opens in a new tab) kullanarak onu imzalayan adresi alabiliriz.

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

Her şey yolundaysa, bunu bir ERC-20 onayı (approve) (opens in a new tab) olarak değerlendirin.

Çevre Sözleşmeleri

Çevre sözleşmeleri, Uniswap için API'dir (uygulama programlama arayüzü). Diğer sözleşmelerden veya merkeziyetsiz uygulamalardan (dapp) gelen harici çağrılar için kullanılabilirler. Çekirdek sözleşmeleri doğrudan çağırabilirsiniz, ancak bu daha karmaşıktır ve bir hata yaparsanız değer kaybedebilirsiniz. Çekirdek sözleşmeler, yalnızca kandırılmadıklarından emin olmak için testler içerir, başkaları için mantık kontrolleri (sanity checks) içermez. Bunlar, gerektiğinde güncellenebilmeleri için çevre sözleşmelerinde yer alır.

UniswapV2Router01.sol

Bu sözleşmenin (opens in a new tab) sorunları vardır ve artık kullanılmamalıdır (opens in a new tab). Neyse ki, çevre sözleşmeleri durumsuzdur (stateless) ve herhangi bir varlık tutmazlar, bu nedenle onu kullanımdan kaldırmak ve insanlara bunun yerine UniswapV2Router02 kullanmalarını önermek kolaydır.

UniswapV2Router02.sol

Çoğu durumda Uniswap'ı bu sözleşme (opens in a new tab) aracılığıyla kullanırsınız. Nasıl kullanılacağını buradan (opens in a new tab) görebilirsiniz.

Bunların çoğuyla daha önce karşılaştık veya oldukça açıklar. Tek istisna IWETH.sol'dir. Uniswap v2, herhangi bir ERC-20 token çifti için takaslara izin verir, ancak Ether (ETH) kendisi bir ERC-20 token'ı değildir. Standarttan daha eskidir ve benzersiz mekanizmalarla transfer edilir. ETH'nin ERC-20 token'larına uygulanan sözleşmelerde kullanılmasını sağlamak için insanlar sarılmış ether (WETH) (opens in a new tab) sözleşmesini buldular. Bu sözleşmeye ETH gönderirsiniz ve size eşdeğer miktarda WETH basar. Veya WETH yakıp ETH'nizi geri alabilirsiniz.

contract UniswapV2Router02 is IUniswapV2Router02 {
    using SafeMath for uint;

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

Yönlendiricinin (router) hangi fabrikayı kullanacağını ve WETH gerektiren işlemler için hangi WETH sözleşmesini kullanacağını bilmesi gerekir. Bu değerler değişmez (opens in a new tab)dir, yani yalnızca kurucu (constructor) içinde ayarlanabilirler. Bu, kullanıcılara hiç kimsenin onları daha az dürüst sözleşmeleri işaret edecek şekilde değiştiremeyeceği güvenini verir.

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

Bu değiştirici (modifier), zaman sınırlı işlemlerin ("yapabiliyorsan Y zamanından önce X'i yap") zaman sınırından sonra gerçekleşmemesini sağlar.

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

Kurucu sadece değişmez durum değişkenlerini ayarlar.

    receive() external payable {
        assert(msg.sender == WETH); // ETH'yi yalnızca WETH Sözleşmesi'nden fallback yoluyla kabul et
    }

Bu fonksiyon, WETH sözleşmesinden token'ları tekrar ETH'ye çevirdiğimizde çağrılır. Bunu yapmaya yalnızca kullandığımız WETH sözleşmesi yetkilidir.

Likidite Ekleme

Bu fonksiyonlar, çift takasına token ekler, bu da likidite havuzunu artırır.


    // **** LİKİDİTE EKLE ****
    function _addLiquidity(

Bu fonksiyon, çift takasına yatırılması gereken A ve B token'larının miktarını hesaplamak için kullanılır.

        address tokenA,
        address tokenB,

Bunlar ERC-20 token sözleşmelerinin adresleridir.

        uint amountADesired,
        uint amountBDesired,

Bunlar, likidite sağlayıcının yatırmak istediği miktarlardır. Ayrıca yatırılacak maksimum A ve B miktarlarıdır.

        uint amountAMin,
        uint amountBMin

Bunlar yatırılacak kabul edilebilir minimum miktarlardır. İşlem bu miktarlarla veya daha fazlasıyla gerçekleşemezse, işlemi geri al (revert). Bu özelliği istemiyorsanız, sadece sıfır belirtin.

Likidite sağlayıcılar genellikle bir minimum belirlerler, çünkü işlemi mevcut olana yakın bir döviz kuruyla sınırlamak isterler. Döviz kuru çok fazla dalgalanırsa, bu temel değerleri değiştiren haberler anlamına gelebilir ve ne yapacaklarına manuel olarak karar vermek isterler.

Örneğin, döviz kurunun bire bir olduğu ve likidite sağlayıcının şu değerleri belirttiği bir durumu hayal edin:

ParametreDeğer
amountADesired1000
amountBDesired1000
amountAMin900
amountBMin800

Döviz kuru 0.9 ile 1.25 arasında kaldığı sürece işlem gerçekleşir. Döviz kuru bu aralığın dışına çıkarsa işlem iptal edilir.

Bu önlemin nedeni, işlemlerin anında gerçekleşmemesidir; onları gönderirsiniz ve sonunda bir doğrulayıcı onları bir bloğa dahil eder (gas fiyatınız çok düşük olmadığı sürece, bu durumda üzerine yazmak için aynı nonce ve daha yüksek bir gas fiyatı ile başka bir işlem göndermeniz gerekecektir). Gönderim ile dahil edilme arasındaki sürede ne olacağını kontrol edemezsiniz.

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

Fonksiyon, rezervler arasındaki mevcut orana eşit bir orana sahip olmak için likidite sağlayıcının yatırması gereken miktarları döndürür.

        // henüz mevcut değilse çifti oluştur
        if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
            IUniswapV2Factory(factory).createPair(tokenA, tokenB);
        }

Bu token çifti için henüz bir takas yoksa, onu oluşturun.

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

Çiftteki mevcut rezervleri alın.

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

Mevcut rezervler boşsa, bu yeni bir çift takasıdır. Yatırılacak miktarlar, likidite sağlayıcının sağlamak istediği miktarlarla tamamen aynı olmalıdır.

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

Miktarların ne olacağını görmemiz gerekirse, bu fonksiyonu (opens in a new tab) kullanarak optimal miktarı alırız. Mevcut rezervlerle aynı oranı istiyoruz.

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

Eğer amountBOptimal, likidite sağlayıcının yatırmak istediği miktardan daha küçükse, bu, B token'ının şu anda likidite yatıranın düşündüğünden daha değerli olduğu anlamına gelir, bu nedenle daha küçük bir miktar gereklidir.

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

Optimal B miktarı istenen B miktarından fazlaysa, bu, B token'larının şu anda likidite yatıranın düşündüğünden daha az değerli olduğu anlamına gelir, bu nedenle daha yüksek bir miktar gereklidir. Ancak, istenen miktar bir maksimumdur, bu yüzden bunu yapamayız. Bunun yerine, istenen B token'ı miktarı için optimal A token'ı sayısını hesaplarız.

Hepsini bir araya getirdiğimizde bu grafiği elde ederiz. Bin A token'ı (mavi çizgi) ve bin B token'ı (kırmızı çizgi) yatırmaya çalıştığınızı varsayalım. X ekseni döviz kurudur, A/B. Eğer x=1 ise, değerleri eşittir ve her birinden bin tane yatırırsınız. Eğer x=2 ise, A, B'nin iki katı değerindedir (her A token'ı için iki B token'ı alırsınız), bu yüzden bin B token'ı yatırırsınız, ancak sadece 500 A token'ı yatırırsınız. Eğer x=0.5 ise, durum tersine döner, bin A token'ı ve beş yüz B token'ı.

Graph

Likiditeyi doğrudan çekirdek sözleşmeye yatırabilirsiniz (UniswapV2Pair::mint (opens in a new tab) kullanarak), ancak çekirdek sözleşme yalnızca kendisinin kandırılmadığını kontrol eder, bu nedenle işleminizi gönderdiğiniz zaman ile yürütüldüğü zaman arasında döviz kuru değişirse değer kaybetme riskiyle karşı karşıya kalırsınız. Çevre sözleşmesini kullanırsanız, yatırmanız gereken miktarı hesaplar ve hemen yatırır, böylece döviz kuru değişmez ve hiçbir şey kaybetmezsiniz.

Bu fonksiyon, likidite yatırmak için bir işlem tarafından çağrılabilir. Çoğu parametre yukarıdaki _addLiquidity ile aynıdır, iki istisna dışında:

. to, likidite sağlayıcının havuzdaki payını göstermek için basılan yeni likidite token'larını alan adrestir . deadline, işlem üzerindeki bir zaman sınırıdır

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

Gerçekte yatırılacak miktarları hesaplıyoruz ve ardından likidite havuzunun adresini buluyoruz. Gaz tasarrufu yapmak için bunu fabrikaya sorarak değil, pairFor kütüphane fonksiyonunu kullanarak yapıyoruz (aşağıdaki kütüphanelere bakın)

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

Kullanıcıdan çift takasına doğru miktarda token transfer edin.

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

Buna karşılık, havuzun kısmi sahipliği için to adresine likidite token'ları verin. Çekirdek sözleşmenin mint fonksiyonu, (likiditenin en son değiştiği zamana kıyasla) ne kadar ekstra token'a sahip olduğunu görür ve buna göre likidite basar.

    function addLiquidityETH(
        address token,
        uint amountTokenDesired,

Bir likidite sağlayıcı, bir Token/ETH çifti takasına likidite sağlamak istediğinde, birkaç fark vardır. Sözleşme, likidite sağlayıcı için ETH'yi sarmayı (wrapping) halleder. Kullanıcının ne kadar ETH yatırmak istediğini belirtmesine gerek yoktur, çünkü kullanıcı bunları işlemle birlikte gönderir (miktar msg.value içinde mevcuttur).

ETH'yi yatırmak için sözleşme önce onu WETH'ye sarar ve ardından WETH'yi çifte transfer eder. Transferin bir assert içine sarıldığına dikkat edin. Bu, transfer başarısız olursa bu sözleşme çağrısının da başarısız olacağı ve bu nedenle sarma işleminin gerçekten gerçekleşmeyeceği anlamına gelir.

        liquidity = IUniswapV2Pair(pair).mint(to);
        // varsa toz ETH'yi iade et
        if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
    }

Kullanıcı bize zaten ETH gönderdi, bu yüzden geriye fazladan bir şey kalırsa (çünkü diğer token kullanıcının düşündüğünden daha az değerlidir), bir geri ödeme yapmamız gerekir.

Likiditeyi Kaldırma

Bu fonksiyonlar likiditeyi kaldıracak ve likidite sağlayıcıya geri ödeme yapacaktır.

Likiditeyi kaldırmanın en basit durumu. Likidite sağlayıcının kabul etmeyi kabul ettiği her bir token'ın minimum bir miktarı vardır ve bu, son tarihten önce gerçekleşmelidir.

        address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
        IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // çifte Likidite gönder
        (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);

Çekirdek sözleşmenin burn fonksiyonu, kullanıcıya token'ları geri ödemeyi halleder.

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

Bir fonksiyon birden fazla değer döndürdüğünde, ancak biz sadece bazılarıyla ilgilendiğimizde, sadece o değerleri bu şekilde alırız. Bir değeri okuyup hiç kullanmamaktan gaz açısından biraz daha ucuzdur.

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

Miktarları, çekirdek sözleşmenin döndürdüğü şekilden (önce düşük adresli token) kullanıcının beklediği şekle (tokenA ve tokenB'ya karşılık gelen) çevirin.

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

Önce transferi yapmak ve ardından meşru olduğunu doğrulamak sorun değildir, çünkü değilse tüm durum değişikliklerini geri alacağız (revert).

ETH için likiditeyi kaldırmak, WETH token'larını almamız ve ardından bunları likidite sağlayıcıya geri vermek üzere ETH'ye çevirmemiz dışında neredeyse aynıdır.

Bu fonksiyonlar, ether'i olmayan kullanıcıların izin mekanizmasını (permit mechanism) kullanarak havuzdan çekim yapmasına olanak tanımak için meta-işlemleri iletir.

Bu fonksiyon, transfer veya depolama ücretleri olan token'lar için kullanılabilir. Bir token'ın bu tür ücretleri olduğunda, ne kadar token geri alacağımızı söylemesi için removeLiquidity fonksiyonuna güvenemeyiz, bu yüzden önce çekim yapmamız ve ardından bakiyeyi almamız gerekir.

Son fonksiyon, depolama ücretlerini meta-işlemlerle birleştirir.

Ticaret

    // **** TAKAS ****
    // başlangıç miktarının ilk çifte zaten gönderilmiş olmasını gerektirir
    function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {

Bu fonksiyon, yatırımcılara (traders) sunulan fonksiyonlar için gerekli olan dahili işlemeyi gerçekleştirir.

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

Bunu yazarken 388.160 ERC-20 token'ı (opens in a new tab) var. Her token çifti için bir çift takası olsaydı, 150 milyardan fazla çift takası olurdu. Tüm zincir, şu anda, bu hesap sayısının sadece %0,1'ine sahip (opens in a new tab). Bunun yerine, takas fonksiyonları bir yol (path) kavramını destekler. Bir yatırımcı A'yı B ile, B'yi C ile ve C'yi D ile takas edebilir, bu nedenle doğrudan bir A-D çifti takasına gerek yoktur.

Bu piyasalardaki fiyatlar senkronize olma eğilimindedir, çünkü senkronizasyon bozulduğunda arbitraj için bir fırsat yaratır. Örneğin, A, B ve C olmak üzere üç token hayal edin. Her çift için bir tane olmak üzere üç çift takası vardır.

  1. Başlangıç durumu
  2. Bir yatırımcı 24.695 A token'ı satar ve 25.305 B token'ı alır.
  3. Yatırımcı 25.305 C token'ı için 24.695 B token'ı satar ve yaklaşık 0.61 B token'ını kâr olarak tutar.
  4. Ardından yatırımcı 25.305 A token'ı için 24.695 C token'ı satar ve yaklaşık 0.61 C token'ını kâr olarak tutar. Yatırımcının ayrıca 0.61 ekstra A token'ı vardır (yatırımcının elinde kalan 25.305 eksi 24.695'lik orijinal yatırım).
AdımA-B TakasıB-C TakasıA-C Takası
1A:1000 B:1050 A/B=1.05B:1000 C:1050 B/C=1.05A:1050 C:1000 C/A=1.05
2A:1024.695 B:1024.695 A/B=1B:1000 C:1050 B/C=1.05A:1050 C:1000 C/A=1.05
3A:1024.695 B:1024.695 A/B=1B:1024.695 C:1024.695 B/C=1A:1050 C:1000 C/A=1.05
4A:1024.695 B:1024.695 A/B=1B:1024.695 C:1024.695 B/C=1A:1024.695 C:1024.695 C/A=1
            (address input, address output) = (path[i], path[i + 1]);
            (address token0,) = UniswapV2Library.sortTokens(input, output);
            uint amountOut = amounts[i + 1];

Şu anda işlediğimiz çifti alın, sıralayın (çiftle kullanmak için) ve beklenen çıktı miktarını alın.

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

Beklenen çıktı miktarlarını, çift takasının beklediği şekilde sıralanmış olarak alın.

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

Bu son takas mı? Öyleyse, ticaret için alınan token'ları hedefe gönderin. Değilse, bir sonraki çift takasına gönderin.


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

Token'ları takas etmek için çift takasını gerçekten çağırın. Takas hakkında bilgilendirilmek için bir geri aramaya (callback) ihtiyacımız yok, bu yüzden o alana herhangi bir bayt göndermiyoruz.

    function swapExactTokensForTokens(

Bu fonksiyon, yatırımcılar tarafından bir token'ı diğeriyle takas etmek için doğrudan kullanılır.

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

Bu parametre, ERC-20 sözleşmelerinin adreslerini içerir. Yukarıda açıklandığı gibi, bu bir dizidir çünkü sahip olduğunuz varlıktan istediğiniz varlığa geçmek için birkaç çift takasından geçmeniz gerekebilir.

Solidity'de bir fonksiyon parametresi memory veya calldata içinde saklanabilir. Eğer fonksiyon sözleşmeye bir giriş noktasıysa, doğrudan bir kullanıcıdan (bir işlem kullanarak) veya farklı bir sözleşmeden çağrılıyorsa, parametrenin değeri doğrudan çağrı verisinden (call data) alınabilir. Eğer fonksiyon yukarıdaki _swap gibi dahili olarak çağrılırsa, parametrelerin memory içinde saklanması gerekir. Çağrılan sözleşmenin perspektifinden calldata salt okunurdur.

uint veya address gibi skaler türlerde derleyici depolama seçimini bizim için halleder, ancak daha uzun ve daha pahalı olan dizilerde kullanılacak depolama türünü biz belirtiriz.

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

Dönüş değerleri her zaman bellekte (memory) döndürülür.

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

Her takasta satın alınacak miktarı hesaplayın. Sonuç, yatırımcının kabul etmeye istekli olduğu minimum değerden azsa, işlemi geri alın.

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

Son olarak, ilk ERC-20 token'ını ilk çift takası için hesaba transfer edin ve _swap çağrısı yapın. Bunların hepsi aynı işlemde gerçekleşiyor, bu nedenle çift takası beklenmeyen token'ların bu transferin bir parçası olduğunu bilir.

Önceki fonksiyon olan swapTokensForTokens, bir yatırımcının vermeye istekli olduğu kesin girdi token'ı sayısını ve karşılığında almaya istekli olduğu minimum çıktı token'ı sayısını belirtmesine olanak tanır. Bu fonksiyon ters takası yapar, bir yatırımcının istediği çıktı token'ı sayısını ve bunlar için ödemeye istekli olduğu maksimum girdi token'ı sayısını belirtmesine izin verir.

Her iki durumda da, yatırımcının bu çevre sözleşmesine önce onları transfer etmesine izin vermek için bir harcama izni (allowance) vermesi gerekir.

Bu dört varyantın tümü ETH ve token'lar arasında ticareti içerir. Tek fark, ya yatırımcıdan ETH alıp WETH basmak için kullanmamız ya da yoldaki son takastan WETH alıp yakmamız ve ortaya çıkan ETH'yi yatırımcıya geri göndermemizdir.

    // **** TAKAS (transferde ücret kesen Token'ları destekler) ****
    // başlangıç miktarının ilk çifte zaten gönderilmiş olmasını gerektirir
    function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {

Bu, transfer veya depolama ücretleri olan token'ları takas etmek ve (bu sorunu (opens in a new tab)) çözmek için kullanılan dahili fonksiyondur.

Transfer ücretleri nedeniyle, her transferden ne kadar elde ettiğimizi söylemesi için getAmountsOut fonksiyonuna güvenemeyiz (orijinal _swap çağrısından önce yaptığımız gibi). Bunun yerine önce transfer etmeli ve ardından ne kadar token geri aldığımızı görmeliyiz.

Not: Teoride _swap yerine sadece bu fonksiyonu kullanabilirdik, ancak belirli durumlarda (örneğin, sonunda gerekli minimumu karşılayacak kadar olmadığı için transfer geri alınırsa) bu daha fazla gaza mal olurdu. Transfer ücreti olan token'lar oldukça nadirdir, bu nedenle onlara uyum sağlamamız gerekse de, tüm takasların en az birinden geçtiğini varsaymaya gerek yoktur.

Bunlar normal token'lar için kullanılan aynı varyantlardır, ancak bunun yerine _swapSupportingFeeOnTransferTokens çağırırlar.

Bu fonksiyonlar sadece UniswapV2Library fonksiyonlarını çağıran vekillerdir (proxy).

UniswapV2Migrator.sol

Bu sözleşme, takasları eski v1'den v2'ye taşımak için kullanıldı. Artık taşındıklarına göre, artık geçerli değildir.

Kütüphaneler

SafeMath kütüphanesi (opens in a new tab) iyi bir şekilde belgelenmiştir, bu yüzden burada belgelemeye gerek yoktur.

Math

Bu kütüphane, Solidity kodunda normalde ihtiyaç duyulmayan bazı matematik fonksiyonlarını içerir, bu yüzden dilin bir parçası değillerdir.

Karekökten daha yüksek bir tahmin olarak x ile başlayın (1-3'ü özel durumlar olarak ele almamızın nedeni budur).

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

Daha yakın bir tahmin elde edin; önceki tahmin ile karekökünü bulmaya çalıştığımız sayının önceki tahmine bölümünün ortalaması. Yeni tahmin mevcut olandan daha düşük olmayana kadar tekrarlayın. Daha fazla detay için buraya bakın (opens in a new tab).

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

Sıfırın kareköküne asla ihtiyacımız olmamalıdır. Bir, iki ve üçün karekökleri kabaca birdir (tam sayılar kullanıyoruz, bu yüzden kesri görmezden geliyoruz).

        }
    }
}

Sabit Noktalı Kesirler (UQ112x112)

Bu kütüphane, normalde Ethereum aritmetiğinin bir parçası olmayan kesirleri işler. Bunu, x sayısını x*2^112 olarak kodlayarak yapar. Bu, orijinal toplama ve çıkarma işlem kodlarını değiştirmeden kullanmamızı sağlar.

Q112 birin kodlamasıdır.

    // bir uint112'yi UQ112x112 olarak kodla
    function encode(uint112 y) internal pure returns (uint224 z) {
        z = uint224(y) * Q112; // asla taşmaz
    }

y uint112 olduğu için, olabileceği en yüksek değer 2^112-1'dir. Bu sayı hala bir UQ112x112 olarak kodlanabilir.

    // bir UQ112x112'yi uint112'ye bölerek bir UQ112x112 döndürür
    function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
        z = x / uint224(y);
    }
}

İki UQ112x112 değerini bölersek, sonuç artık 2^112 ile çarpılmaz. Bu yüzden bunun yerine payda için bir tam sayı alırız. Çarpma işlemi yapmak için benzer bir numara kullanmamız gerekirdi, ancak UQ112x112 değerlerinin çarpımını yapmamıza gerek yoktur.

UniswapV2Library

Bu kütüphane yalnızca çevre (periphery) sözleşmeleri tarafından kullanılır

İki Token'ı adrese göre sıralayın, böylece onlar için çift takasının adresini alabileceğiz. Bu gereklidir çünkü aksi takdirde biri A,B parametreleri ve diğeri B,A parametreleri için olmak üzere iki olasılığımız olurdu, bu da bir yerine iki takasa yol açardı.

Bu fonksiyon, iki Token için çift takasının adresini hesaplar. Bu sözleşme CREATE2 işlem kodu (opens in a new tab) kullanılarak oluşturulmuştur, bu nedenle kullandığı parametreleri biliyorsak aynı algoritmayı kullanarak adresi hesaplayabiliriz. Bu, fabrikaya sormaktan çok daha ucuzdur ve

    // bir çift için rezervleri getirir ve sıralar
    function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
        (address token0,) = sortTokens(tokenA, tokenB);
        (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
        (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
    }

Bu fonksiyon, çift takasının sahip olduğu iki Token'ın rezervlerini döndürür. Token'ları her iki sırayla da alabileceğini ve dahili kullanım için sıraladığını unutmayın.

    // belirli bir miktar varlık ve çift rezervleri verildiğinde, diğer varlığın eşdeğer miktarını döndürür
    function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
        require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
        require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
        amountB = amountA.mul(reserveB) / reserveA;
    }

Bu fonksiyon, herhangi bir ücret söz konusu değilse Token A karşılığında alacağınız Token B miktarını verir. Bu hesaplama, transferin döviz kurunu değiştirdiğini dikkate alır.

    // bir varlığın girdi miktarı ve çift rezervleri verildiğinde, diğer varlığın maksimum çıktı miktarını döndürür
    function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {

Yukarıdaki quote fonksiyonu, çift takasını kullanmak için herhangi bir ücret yoksa harika çalışır. Ancak, %0,3'lük bir takas ücreti varsa, gerçekte alacağınız miktar daha düşüktür. Bu fonksiyon, takas ücretinden sonraki miktarı hesaplar.


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

Solidity kesirleri yerel olarak işlemez, bu yüzden miktarı doğrudan 0,997 ile çarpamayız. Bunun yerine, payı 997 ve paydayı 1000 ile çarparak aynı etkiyi elde ederiz.

    // bir varlığın çıktı miktarı ve çift rezervleri verildiğinde, diğer varlığın gerekli girdi miktarını döndürür
    function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
        require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
        require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
        uint numerator = reserveIn.mul(amountOut).mul(1000);
        uint denominator = reserveOut.sub(amountOut).mul(997);
        amountIn = (numerator / denominator).add(1);
    }

Bu fonksiyon kabaca aynı şeyi yapar, ancak çıktı miktarını alır ve girdiyi sağlar.

Bu iki fonksiyon, birkaç çift takasından geçmek gerektiğinde değerleri tanımlamayı işler.

Transfer Yardımcısı

Bu kütüphane (opens in a new tab), bir geri al (revert) ve bir false değer dönüşünü aynı şekilde ele almak için ERC-20 ve Ethereum transferlerinin etrafına başarı kontrolleri ekler.

Farklı bir sözleşmeyi iki yoldan biriyle çağırabiliriz:

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

ERC-20 standardından önce oluşturulan Token'larla geriye dönük uyumluluk adına, bir ERC-20 çağrısı ya geri alınarak (bu durumda success false olur) ya da başarılı olup bir false değeri döndürerek (bu durumda çıktı verisi vardır ve bunu bir boolean olarak çözerseniz false elde edersiniz) başarısız olabilir.

Bu fonksiyon, bir hesabın farklı bir hesap tarafından sağlanan harcama iznini harcamasına olanak tanıyan ERC-20'nin transfer işlevselliğini (opens in a new tab) uygular.

Bu fonksiyon, bir hesabın farklı bir hesap tarafından sağlanan harcama iznini harcamasına olanak tanıyan ERC-20'nin transferFrom işlevselliğini (opens in a new tab) uygular.


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

Bu fonksiyon bir hesaba Ether transfer eder. Farklı bir sözleşmeye yapılan herhangi bir çağrı Ether göndermeyi deneyebilir. Aslında herhangi bir fonksiyonu çağırmamız gerekmediği için, çağrıyla birlikte herhangi bir veri göndermeyiz.

Sonuç

Bu, yaklaşık 50 sayfalık uzun bir makale. Buraya kadar geldiyseniz, tebrikler! Umarım artık (kısa örnek programların aksine) gerçek hayatta kullanılacak bir uygulama yazarken dikkat edilmesi gerekenleri anlamışsınızdır ve kendi kullanım durumlarınız için sözleşmeler yazma konusunda daha yetkinsinizdir.

Şimdi gidin, faydalı bir şeyler yazın ve bizi şaşırtın.

Çalışmalarımın daha fazlası için buraya göz atın (opens in a new tab).

Sayfanın son güncellenme tarihi: 3 Nisan 2026