Zum Hauptinhalt springen

Uniswap-v2-Vertrag im Detail

Solidity
dapps
Fortgeschritten
Ori Pomerantz
1. Mai 2021
59 Minuten Lesezeit

Einführung

Uniswap v2 (opens in a new tab) kann einen Tauschmarkt zwischen zwei beliebigen ERC-20-Token erstellen. In diesem Artikel werden wir den Quellcode für die Verträge durchgehen, die dieses Protokoll implementieren, und sehen, warum sie auf diese Weise geschrieben sind.

Was macht Uniswap?

Grundsätzlich gibt es zwei Arten von Benutzern: Liquiditätsanbieter und Händler.

Die Liquiditätsanbieter stellen dem Pool die beiden Token zur Verfügung, die getauscht werden können (wir nennen sie Token0 und Token1). Im Gegenzug erhalten sie einen dritten Token, der einen teilweisen Besitz am Pool darstellt und Liquiditäts-Token genannt wird.

Händler senden eine Art von Token an den Pool und erhalten die andere (zum Beispiel senden sie Token0 und erhalten Token1) aus dem Pool, der von den Liquiditätsanbietern bereitgestellt wird. Der Wechselkurs wird durch die relative Anzahl von Token0 und Token1 bestimmt, die der Pool hat. Darüber hinaus nimmt der Pool einen kleinen Prozentsatz als Belohnung für den Liquiditätspool.

Wenn Liquiditätsanbieter ihre Vermögenswerte zurückhaben möchten, können sie die Pool-Token verbrennen und ihre Token zurückerhalten, einschließlich ihres Anteils an den Belohnungen.

Klicken Sie hier für eine ausführlichere Beschreibung (opens in a new tab).

Warum v2? Warum nicht v3?

Uniswap v3 (opens in a new tab) ist ein Upgrade, das viel komplizierter ist als v2. Es ist einfacher, zuerst v2 zu lernen und dann zu v3 überzugehen.

Kernverträge vs. Peripherieverträge

Uniswap v2 ist in zwei Komponenten unterteilt: einen Kern (Core) und eine Peripherie. Diese Aufteilung ermöglicht es, dass die Kernverträge, die die Vermögenswerte halten und daher sicher sein müssen, einfacher und leichter zu prüfen sind. Alle zusätzlichen Funktionen, die von Händlern benötigt werden, können dann durch Peripherieverträge bereitgestellt werden.

Daten- und Kontrollflüsse

Dies ist der Daten- und Kontrollfluss, der stattfindet, wenn Sie die drei Hauptaktionen von Uniswap ausführen:

  1. Tausch zwischen verschiedenen Token
  2. Liquidität zum Markt hinzufügen und als Belohnung ERC-20-Liquiditäts-Token des Tauschpaares erhalten
  3. ERC-20-Liquiditäts-Token verbrennen und die ERC-20-Token zurückerhalten, die das Tauschpaar Händlern zum Tausch anbietet

Tausch

Dies ist der häufigste Ablauf, der von Händlern verwendet wird:

Aufrufer

  1. Dem Peripherie-Konto einen Freigabebetrag in Höhe des zu tauschenden Betrags zur Verfügung stellen.
  2. Eine der vielen Tausch-Funktionen des Peripherievertrags aufrufen (welche genau, hängt davon ab, ob ETH involviert ist oder nicht, ob der Händler die Menge der einzuzahlenden Token oder die Menge der zurückzuerhaltenden Token angibt usw.). Jede Tausch-Funktion akzeptiert einen path, ein Array von Tauschbörsen, die durchlaufen werden sollen.

Im Peripherievertrag (UniswapV2Router02.sol)

  1. Die Beträge ermitteln, die an jeder Tauschbörse entlang des Pfades gehandelt werden müssen.
  2. Über den Pfad iterieren. Für jede Tauschbörse auf dem Weg sendet er den Eingabe-Token und ruft dann die Funktion swap der Tauschbörse auf. In den meisten Fällen ist die Zieladresse für die Token das nächste Tauschpaar auf dem Pfad. Bei der letzten Tauschbörse ist es die vom Händler angegebene Adresse.

Im Kernvertrag (UniswapV2Pair.sol)

  1. Überprüfen, dass der Kernvertrag nicht betrogen wird und nach dem Tausch ausreichend Liquidität aufrechterhalten kann.
  2. Prüfen, wie viele zusätzliche Token wir zusätzlich zu den bekannten Reserven haben. Dieser Betrag ist die Anzahl der Eingabe-Token, die wir zum Tausch erhalten haben.
  3. Die Ausgabe-Token an das Ziel senden.
  4. _update aufrufen, um die Reservebeträge zu aktualisieren

Zurück im Peripherievertrag (UniswapV2Router02.sol)

  1. Notwendige Bereinigungen durchführen (zum Beispiel WETH-Token verbrennen, um ETH zurückzuerhalten und an den Händler zu senden)

Liquidität hinzufügen

Aufrufer

  1. Dem Peripherie-Konto einen Freigabebetrag in Höhe der Beträge zur Verfügung stellen, die dem Liquiditätspool hinzugefügt werden sollen.
  2. Eine der addLiquidity-Funktionen des Peripherievertrags aufrufen.

Im Peripherievertrag (UniswapV2Router02.sol)

  1. Bei Bedarf ein neues Tauschpaar erstellen
  2. Wenn bereits ein Tauschpaar existiert, die Menge der hinzuzufügenden Token berechnen. Dies soll für beide Token der gleiche Wert sein, also das gleiche Verhältnis von neuen Token zu bestehenden Token.
  3. Prüfen, ob die Beträge akzeptabel sind (Aufrufer können einen Mindestbetrag angeben, unterhalb dessen sie lieber keine Liquidität hinzufügen möchten)
  4. Den Kernvertrag aufrufen.

Im Kernvertrag (UniswapV2Pair.sol)

  1. Liquiditäts-Token prägen und an den Aufrufer senden
  2. _update aufrufen, um die Reservebeträge zu aktualisieren

Liquidität entfernen

Aufrufer

  1. Dem Peripherie-Konto einen Freigabebetrag an Liquiditäts-Token zur Verfügung stellen, die im Austausch gegen die zugrunde liegenden Token verbrannt werden sollen.
  2. Eine der removeLiquidity-Funktionen des Peripherievertrags aufrufen.

Im Peripherievertrag (UniswapV2Router02.sol)

  1. Die Liquiditäts-Token an das Tauschpaar senden

Im Kernvertrag (UniswapV2Pair.sol)

  1. Die zugrunde liegenden Token im Verhältnis zu den verbrannten Token an die Zieladresse senden. Wenn sich beispielsweise 1000 A-Token, 500 B-Token und 90 Liquiditäts-Token im Pool befinden und wir 9 Token zum Verbrennen erhalten, verbrennen wir 10 % der Liquiditäts-Token, sodass wir dem Benutzer 100 A-Token und 50 B-Token zurücksenden.
  2. Die Liquiditäts-Token verbrennen
  3. _update aufrufen, um die Reservebeträge zu aktualisieren

Die Kernverträge

Dies sind die sicheren Verträge, die die Liquidität halten.

UniswapV2Pair.sol

Dieser Vertrag (opens in a new tab) implementiert den eigentlichen Pool, der Token tauscht. Er ist die Kernfunktionalität von Uniswap.

Dies sind alle Schnittstellen, die der Vertrag kennen muss, entweder weil der Vertrag sie implementiert (IUniswapV2Pair und UniswapV2ERC20) oder weil er Verträge aufruft, die sie implementieren.

contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {

Dieser Vertrag erbt von UniswapV2ERC20, was die ERC-20-Funktionen für die Liquiditäts-Token bereitstellt.

    using SafeMath  for uint;

Die SafeMath-Bibliothek (opens in a new tab) wird verwendet, um Über- und Unterläufe zu vermeiden. Dies ist wichtig, da wir sonst in eine Situation geraten könnten, in der ein Wert -1 sein sollte, aber stattdessen 2^256-1 ist.

    using UQ112x112 for uint224;

Viele Berechnungen im Pool-Vertrag erfordern Brüche. Brüche werden jedoch von der EVM nicht unterstützt. Die Lösung, die Uniswap gefunden hat, besteht darin, 224-Bit-Werte zu verwenden, wobei 112 Bit für den ganzzahligen Teil und 112 Bit für den Bruchteil vorgesehen sind. So wird 1.0 als 2^112 dargestellt, 1.5 als 2^112 + 2^111 usw.

Weitere Details zu dieser Bibliothek finden Sie später in diesem Dokument.

Variablen

    uint public constant MINIMUM_LIQUIDITY = 10**3;

Um Fälle von Division durch Null zu vermeiden, gibt es eine Mindestanzahl von Liquiditäts-Token, die immer existieren (aber dem Konto Null gehören). Diese Zahl ist MINIMUM_LIQUIDITY, eintausend.

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

Dies ist der ABI-Selektor für die ERC-20-Transfer-Funktion. Er wird verwendet, um ERC-20-Token in den beiden Token-Konten zu transferieren.

    address public factory;

Dies ist der Factory-Vertrag, der diesen Pool erstellt hat. Jeder Pool ist ein Tausch zwischen zwei ERC-20-Token, die Factory ist ein zentraler Punkt, der all diese Pools verbindet.

    address public token0;
    address public token1;

Dies sind die Adressen der Verträge für die beiden Arten von ERC-20-Token, die durch diesen Pool getauscht werden können.

    uint112 private reserve0;           // verwendet einen einzelnen Speicherplatz, zugänglich über getReserves
    uint112 private reserve1;           // verwendet einen einzelnen Speicherplatz, zugänglich über getReserves

Die Reserven, die der Pool für jeden Token-Typ hat. Wir gehen davon aus, dass beide den gleichen Wert repräsentieren, und daher ist jeder token0 reserve1/reserve0 token1 wert.

    uint32  private blockTimestampLast; // verwendet einen einzelnen Speicherplatz, zugänglich über getReserves

Der Zeitstempel für den letzten Block, in dem ein Tausch stattfand, wird verwendet, um Wechselkurse im Laufe der Zeit zu verfolgen.

Eine der größten Gas-Ausgaben von Ethereum-Verträgen ist der Speicher (Storage), der von einem Aufruf des Vertrags zum nächsten bestehen bleibt. Jede Speicherzelle ist 256 Bit lang. Daher werden drei Variablen, reserve0, reserve1 und blockTimestampLast, so zugewiesen, dass ein einzelner Speicherwert alle drei umfassen kann (112+112+32=256).

    uint public price0CumulativeLast;
    uint public price1CumulativeLast;

Diese Variablen enthalten die kumulierten Kosten für jeden Token (jeweils in Bezug auf den anderen). Sie können verwendet werden, um den durchschnittlichen Wechselkurs über einen bestimmten Zeitraum zu berechnen.

    uint public kLast; // reserve0 * reserve1, Stand unmittelbar nach dem jüngsten Liquiditätsereignis

Die Art und Weise, wie der Paar-Tausch über den Wechselkurs zwischen token0 und token1 entscheidet, besteht darin, das Vielfache der beiden Reserven während der Trades konstant zu halten. kLast ist dieser Wert. Er ändert sich, wenn ein Liquiditätsanbieter Token einzahlt oder abhebt, und er steigt aufgrund der Marktgebühr von 0,3 % leicht an.

Hier ist ein einfaches Beispiel. Beachten Sie, dass die Tabelle der Einfachheit halber nur drei Nachkommastellen hat und wir die Handelsgebühr von 0,3 % ignorieren, sodass die Zahlen nicht exakt sind.

Ereignisreserve0reserve1reserve0 * reserve1Durchschnittlicher Wechselkurs (token1 / token0)
Initiale Einrichtung1.000,0001.000,0001.000.000
Händler A tauscht 50 token0 gegen 47,619 token11.050,000952,3811.000.0000,952
Händler B tauscht 10 token0 gegen 8,984 token11.060,000943,3961.000.0000,898
Händler C tauscht 40 token0 gegen 34,305 token11.100,000909,0901.000.0000,858
Händler D tauscht 100 token1 gegen 109,01 token0990,9901.009,0901.000.0000,917
Händler E tauscht 10 token0 gegen 10,079 token11.000,990999,0101.000.0001,008

Wenn Händler mehr von token0 bereitstellen, steigt der relative Wert von token1 und umgekehrt, basierend auf Angebot und Nachfrage.

Sperre (Lock)

    uint private unlocked = 1;

Es gibt eine Klasse von Sicherheitslücken, die auf dem Missbrauch durch Wiedereintritt (opens in a new tab) basieren. Uniswap muss beliebige ERC-20-Token transferieren, was bedeutet, dass ERC-20-Verträge aufgerufen werden, die versuchen könnten, den Uniswap-Markt, der sie aufruft, zu missbrauchen. Indem wir eine unlocked-Variable als Teil des Vertrags haben, können wir verhindern, dass Funktionen aufgerufen werden, während sie ausgeführt werden (innerhalb derselben Transaktion).

    modifier lock() {

Diese Funktion ist ein Modifikator (opens in a new tab), eine Funktion, die eine normale Funktion umschließt, um ihr Verhalten in irgendeiner Weise zu ändern.

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

Wenn unlocked gleich eins ist, setze es auf null. Wenn es bereits null ist, mache den Aufruf rückgängig (revert), lass ihn fehlschlagen.

        _;

In einem Modifikator ist _; der ursprüngliche Funktionsaufruf (mit allen Parametern). Hier bedeutet dies, dass der Funktionsaufruf nur stattfindet, wenn unlocked beim Aufruf eins war, und während er ausgeführt wird, ist der Wert von unlocked null.

        unlocked = 1;
    }

Nachdem die Hauptfunktion zurückkehrt, gib die Sperre frei.

Verschiedene Funktionen

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

Diese Funktion liefert Aufrufern den aktuellen Zustand des Tauschs. Beachten Sie, dass Solidity-Funktionen mehrere Werte zurückgeben können (opens in a new tab).

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

Diese interne Funktion transferiert eine Menge an ERC-20-Token vom Tausch an jemand anderen. SELECTOR gibt an, dass die Funktion, die wir aufrufen, transfer(address,uint) ist (siehe Definition oben).

Um zu vermeiden, dass wir eine Schnittstelle für die Token-Funktion importieren müssen, erstellen wir den Aufruf "manuell" unter Verwendung einer der ABI-Funktionen (opens in a new tab).

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

Es gibt zwei Möglichkeiten, wie ein ERC-20-Transfer-Aufruf einen Fehler melden kann:

  1. Rückgängig machen (Revert). Wenn ein Aufruf an einen externen Vertrag rückgängig gemacht wird, ist der boolesche Rückgabewert false
  2. Normal beenden, aber einen Fehler melden. In diesem Fall hat der Rückgabewert-Puffer eine Länge ungleich null, und wenn er als boolescher Wert decodiert wird, ist er false

Wenn eine dieser Bedingungen eintritt, mache es rückgängig.

Ereignisse

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

Diese beiden Ereignisse werden ausgegeben, wenn ein Liquiditätsanbieter entweder Liquidität einzahlt (Mint) oder sie abhebt (Burn). In beiden Fällen sind die Mengen von token0 und token1, die eingezahlt oder abgehoben werden, Teil des Ereignisses, ebenso wie die Identität des Kontos, das uns aufgerufen hat (sender). Im Falle einer Abhebung enthält das Ereignis auch das Ziel, das die Token erhalten hat (to), was möglicherweise nicht mit dem Absender identisch ist.

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

Dieses Ereignis wird ausgegeben, wenn ein Händler einen Token gegen den anderen tauscht. Auch hier sind Absender und Ziel möglicherweise nicht identisch. Jeder Token kann entweder an den Tausch gesendet oder von ihm empfangen werden.

    event Sync(uint112 reserve0, uint112 reserve1);

Schließlich wird Sync jedes Mal ausgegeben, wenn Token hinzugefügt oder abgehoben werden, unabhängig vom Grund, um die neuesten Reserveinformationen (und damit den Wechselkurs) bereitzustellen.

Setup-Funktionen

Diese Funktionen sollen einmal aufgerufen werden, wenn der neue Paar-Tausch eingerichtet wird.

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

Der Konstruktor stellt sicher, dass wir die Adresse der Factory, die das Paar erstellt hat, im Auge behalten. Diese Information wird für initialize und für die Factory-Gebühr (falls vorhanden) benötigt.

    // wird einmalig von der Factory zum Zeitpunkt des Deployments aufgerufen
    function initialize(address _token0, address _token1) external {
        require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // ausreichende Prüfung
        token0 = _token0;
        token1 = _token1;
    }

Diese Funktion ermöglicht es der Factory (und nur der Factory), die beiden ERC-20-Token anzugeben, die dieses Paar tauschen wird.

Interne Update-Funktionen

_update
    // aktualisiert Reserven und, beim ersten Aufruf pro Block, Preisakkumulatoren
    function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {

Diese Funktion wird jedes Mal aufgerufen, wenn Token eingezahlt oder abgehoben werden.

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

Wenn entweder balance0 oder balance1 (uint256) höher als uint112(-1) (=2^112-1) ist (sodass es überläuft und bei der Konvertierung in uint112 wieder auf 0 springt), weigere dich, das _update fortzusetzen, um Überläufe zu verhindern. Bei einem normalen Token, der in 10^18 Einheiten unterteilt werden kann, bedeutet dies, dass jeder Tausch auf etwa 5,1*10^15 jedes Tokens begrenzt ist. Bisher war das kein Problem.

        uint32 blockTimestamp = uint32(block.timestamp % 2**32);
        uint32 timeElapsed = blockTimestamp - blockTimestampLast; // Überlauf ist erwünscht
        if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {

Wenn die verstrichene Zeit nicht null ist, bedeutet dies, dass wir die erste Tausch-Transaktion in diesem Block sind. In diesem Fall müssen wir die Kostenakkumulatoren aktualisieren.

            // * läuft nie über, und + Überlauf ist erwünscht
            price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
            price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
        }

Jeder Kostenakkumulator wird mit den neuesten Kosten (Reserve des anderen Tokens/Reserve dieses Tokens) multipliziert mit der verstrichenen Zeit in Sekunden aktualisiert. Um einen Durchschnittspreis zu erhalten, lesen Sie den kumulierten Preis zu zwei Zeitpunkten ab und dividieren ihn durch die Zeitdifferenz zwischen ihnen. Nehmen wir zum Beispiel diese Abfolge von Ereignissen an:

Ereignisreserve0reserve1ZeitstempelGrenz-Wechselkurs (reserve1 / reserve0)price0CumulativeLast
Initiale Einrichtung1.000,0001.000,0005.0001,0000
Händler A zahlt 50 token0 ein und erhält 47,619 token1 zurück1.050,000952,3815.0200,90720
Händler B zahlt 10 token0 ein und erhält 8,984 token1 zurück1.060,000943,3965.0300,89020+10*0,907 = 29,07
Händler C zahlt 40 token0 ein und erhält 34,305 token1 zurück1.100,000909,0905.1000,82629,07+70*0,890 = 91,37
Händler D zahlt 100 token1 ein und erhält 109,01 token0 zurück990,9901.009,0905.1101,01891,37+10*0,826 = 99,63
Händler E zahlt 10 token0 ein und erhält 10,079 token1 zurück1.000,990999,0105.1500,99899,63+40*1,1018 = 143,702

Nehmen wir an, wir möchten den Durchschnittspreis von Token0 zwischen den Zeitstempeln 5.030 und 5.150 berechnen. Die Differenz im Wert von price0Cumulative beträgt 143,702-29,07=114,632. Dies ist der Durchschnitt über zwei Minuten (120 Sekunden). Der Durchschnittspreis beträgt also 114,632/120 = 0,955.

Diese Preisberechnung ist der Grund, warum wir die alten Reservegrößen kennen müssen.

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

Aktualisieren Sie schließlich die globalen Variablen und geben Sie ein Sync-Ereignis aus.

_mintFee
    // wenn Gebühr aktiviert ist, präge Liquidität entsprechend 1/6 des Wachstums von sqrt(k)
    function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {

In Uniswap 2.0 zahlen Händler eine Gebühr von 0,30 %, um den Markt zu nutzen. Der Großteil dieser Gebühr (0,25 % des Trades) geht immer an die Liquiditätsanbieter. Die restlichen 0,05 % können entweder an die Liquiditätsanbieter oder an eine von der Factory als Protokollgebühr angegebene Adresse gehen, die Uniswap für ihren Entwicklungsaufwand bezahlt.

Um Berechnungen (und damit Gaskosten) zu reduzieren, wird diese Gebühr nur berechnet, wenn Liquidität zum Pool hinzugefügt oder daraus entfernt wird, und nicht bei jeder Transaktion.

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

Lies das Gebührenziel der Factory. Wenn es null ist, gibt es keine Protokollgebühr und es ist nicht nötig, diese Gebühr zu berechnen.

        uint _kLast = kLast; // Gaseinsparungen

Die Zustandsvariable kLast befindet sich im Speicher (Storage), sodass sie zwischen verschiedenen Aufrufen des Vertrags einen Wert hat. Der Zugriff auf den Speicher ist viel teurer als der Zugriff auf den flüchtigen Speicher (Memory), der freigegeben wird, wenn der Funktionsaufruf an den Vertrag endet, daher verwenden wir eine interne Variable, um Gas zu sparen.

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

Die Liquiditätsanbieter erhalten ihren Anteil einfach durch die Wertsteigerung ihrer Liquiditäts-Token. Die Protokollgebühr erfordert jedoch, dass neue Liquiditäts-Token geprägt und der Adresse feeTo zur Verfügung gestellt werden.

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

Wenn es neue Liquidität gibt, auf die eine Protokollgebühr erhoben werden kann. Sie können die Quadratwurzelfunktion später in diesem Artikel sehen.

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

Diese komplizierte Berechnung der Gebühren wird im Whitepaper (opens in a new tab) auf Seite 5 erklärt. Wir wissen, dass zwischen dem Zeitpunkt, an dem kLast berechnet wurde, und der Gegenwart keine Liquidität hinzugefügt oder entfernt wurde (da wir diese Berechnung jedes Mal durchführen, wenn Liquidität hinzugefügt oder entfernt wird, bevor sie sich tatsächlich ändert), sodass jede Änderung in reserve0 * reserve1 aus Transaktionsgebühren stammen muss (ohne sie würden wir reserve0 * reserve1 konstant halten).

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

Verwenden Sie die Funktion UniswapV2ERC20._mint, um die zusätzlichen Liquiditäts-Token tatsächlich zu erstellen und sie feeTo zuzuweisen.

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

Wenn keine Gebühr festgelegt ist, setze kLast auf null (falls es das nicht schon ist). Als dieser Vertrag geschrieben wurde, gab es eine Gas-Rückerstattungsfunktion (opens in a new tab), die Verträge dazu ermutigte, die Gesamtgröße des Ethereum-Zustands zu reduzieren, indem sie Speicher, den sie nicht benötigten, auf null setzten. Dieser Code holt sich diese Rückerstattung, wenn möglich.

Von außen zugängliche Funktionen

Beachten Sie, dass zwar jede Transaktion oder jeder Vertrag diese Funktionen aufrufen kann, sie jedoch so konzipiert sind, dass sie vom Peripherie-Vertrag aufgerufen werden. Wenn Sie sie direkt aufrufen, können Sie den Paar-Tausch nicht betrügen, aber Sie könnten durch einen Fehler Wert verlieren.

mint
    // diese Low-Level-Funktion sollte von einem Vertrag aufgerufen werden, der wichtige Sicherheitsprüfungen durchführt
    function mint(address to) external lock returns (uint liquidity) {

Diese Funktion wird aufgerufen, wenn ein Liquiditätsanbieter dem Pool Liquidität hinzufügt. Sie prägt zusätzliche Liquiditäts-Token als Belohnung. Sie sollte von einem Peripherie-Vertrag aufgerufen werden, der sie nach dem Hinzufügen der Liquidität in derselben Transaktion aufruft (damit niemand sonst eine Transaktion einreichen kann, die die neue Liquidität vor dem rechtmäßigen Eigentümer beansprucht).

        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // Gaseinsparungen

Dies ist die Art und Weise, die Ergebnisse einer Solidity-Funktion zu lesen, die mehrere Werte zurückgibt. Wir verwerfen den letzten zurückgegebenen Wert, den Block-Zeitstempel, da wir ihn nicht benötigen.

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

Holen Sie sich die aktuellen Salden und sehen Sie, wie viel von jedem Token-Typ hinzugefügt wurde.

        bool feeOn = _mintFee(_reserve0, _reserve1);

Berechnen Sie die zu erhebenden Protokollgebühren, falls vorhanden, und prägen Sie entsprechend Liquiditäts-Token. Da die Parameter für _mintFee die alten Reservewerte sind, wird die Gebühr genau berechnet, basierend nur auf Pool-Änderungen aufgrund von Gebühren.

        uint _totalSupply = totalSupply; // Gaseinsparungen, muss hier definiert werden, da sich totalSupply in _mintFee aktualisieren kann
        if (_totalSupply == 0) {
            liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
           _mint(address(0), MINIMUM_LIQUIDITY); // sperrt die ersten MINIMUM_LIQUIDITY Token dauerhaft

Wenn dies die erste Einzahlung ist, erstelle MINIMUM_LIQUIDITY Token und sende sie an die Adresse null, um sie zu sperren. Sie können niemals eingelöst werden, was bedeutet, dass der Pool niemals vollständig geleert wird (dies bewahrt uns an einigen Stellen vor einer Division durch null). Der Wert von MINIMUM_LIQUIDITY ist eintausend, was in Anbetracht der Tatsache, dass die meisten ERC-20 in Einheiten von 10^-18 eines Tokens unterteilt sind, so wie ETH in Wei unterteilt ist, 10^-15 des Wertes eines einzelnen Tokens entspricht. Keine hohen Kosten.

Zum Zeitpunkt der ersten Einzahlung kennen wir den relativen Wert der beiden Token nicht, also multiplizieren wir einfach die Beträge und ziehen eine Quadratwurzel, in der Annahme, dass die Einzahlung uns in beiden Token den gleichen Wert liefert.

Wir können darauf vertrauen, da es im Interesse des Einzahlers liegt, den gleichen Wert bereitzustellen, um keinen Wert durch Arbitrage zu verlieren. Nehmen wir an, der Wert der beiden Token ist identisch, aber unser Einzahler hat viermal so viel von Token1 wie von Token0 eingezahlt. Ein Händler kann die Tatsache nutzen, dass der Paar-Tausch denkt, dass Token0 wertvoller ist, um Wert daraus zu ziehen.

Ereignisreserve0reserve1reserve0 * reserve1Wert des Pools (reserve0 + reserve1)
Initiale Einrichtung83225640
Händler zahlt 8 Token0-Token ein, erhält 16 Token1 zurück161625632

Wie Sie sehen können, hat der Händler zusätzliche 8 Token verdient, die aus einer Verringerung des Wertes des Pools stammen, was dem Einzahler schadet, dem er gehört.

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

Bei jeder nachfolgenden Einzahlung kennen wir bereits den Wechselkurs zwischen den beiden Vermögenswerten und erwarten, dass Liquiditätsanbieter in beiden den gleichen Wert bereitstellen. Wenn sie dies nicht tun, geben wir ihnen als Strafe Liquiditäts-Token basierend auf dem geringeren Wert, den sie bereitgestellt haben.

Unabhängig davon, ob es sich um die erste Einzahlung oder eine nachfolgende handelt, entspricht die Anzahl der von uns bereitgestellten Liquiditäts-Token der Quadratwurzel der Änderung in reserve0*reserve1 und der Wert des Liquiditäts-Tokens ändert sich nicht (es sei denn, wir erhalten eine Einzahlung, die nicht gleiche Werte beider Typen aufweist, in welchem Fall die "Strafe" verteilt wird). Hier ist ein weiteres Beispiel mit zwei Token, die den gleichen Wert haben, mit drei guten Einzahlungen und einer schlechten (Einzahlung von nur einem Token-Typ, sodass keine Liquiditäts-Token produziert werden).

Ereignisreserve0reserve1reserve0 * reserve1Pool-Wert (reserve0 + reserve1)Für diese Einzahlung geprägte Liquiditäts-TokenGesamte Liquiditäts-TokenWert jedes Liquiditäts-Tokens
Initiale Einrichtung8,0008,0006416,000882,000
Einzahlung von vier jedes Typs12,00012,00014424,0004122,000
Einzahlung von zwei jedes Typs14,00014,00019628,0002142,000
Einzahlung mit ungleichem Wert18,00014,00025232,000014~2,286
Nach Arbitrage~15,874~15,874252~31,748014~2,267
        }
        require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
        _mint(to, liquidity);

Verwenden Sie die Funktion UniswapV2ERC20._mint, um die zusätzlichen Liquiditäts-Token tatsächlich zu erstellen und sie dem richtigen Konto zu geben.


        _update(balance0, balance1, _reserve0, _reserve1);
        if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 und reserve1 sind aktuell
        emit Mint(msg.sender, amount0, amount1);
    }

Aktualisieren Sie die Zustandsvariablen (reserve0, reserve1 und bei Bedarf kLast) und geben Sie das entsprechende Ereignis aus.

burn
    // diese Low-Level-Funktion sollte von einem Vertrag aufgerufen werden, der wichtige Sicherheitsprüfungen durchführt
    function burn(address to) external lock returns (uint amount0, uint amount1) {

Diese Funktion wird aufgerufen, wenn Liquidität abgehoben wird und die entsprechenden Liquiditäts-Token verbrannt werden müssen. Sie sollte ebenfalls von einem Peripherie-Konto aufgerufen werden.

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

Der Peripherie-Vertrag hat die zu verbrennende Liquidität vor dem Aufruf an diesen Vertrag transferiert. Auf diese Weise wissen wir, wie viel Liquidität verbrannt werden muss, und wir können sicherstellen, dass sie verbrannt wird.

        bool feeOn = _mintFee(_reserve0, _reserve1);
        uint _totalSupply = totalSupply; // Gaseinsparungen, muss hier definiert werden, da sich totalSupply in _mintFee aktualisieren kann
        amount0 = liquidity.mul(balance0) / _totalSupply; // die Verwendung von Salden stellt eine anteilige Verteilung sicher
        amount1 = liquidity.mul(balance1) / _totalSupply; // die Verwendung von Salden stellt eine anteilige Verteilung sicher
        require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');

Der Liquiditätsanbieter erhält den gleichen Wert beider Token. Auf diese Weise ändern wir den Wechselkurs nicht.

Der Rest der Funktion burn ist das Spiegelbild der obigen Funktion mint.

swap
    // diese Low-Level-Funktion sollte von einem Vertrag aufgerufen werden, der wichtige Sicherheitsprüfungen durchführt
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {

Diese Funktion soll ebenfalls von einem Peripherie-Vertrag aufgerufen werden.

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

        uint balance0;
        uint balance1;
        { // Gültigkeitsbereich für _token{0,1}, vermeidet 'stack too deep'-Fehler

Lokale Variablen können entweder im Speicher (Memory) oder, wenn es nicht zu viele sind, direkt auf dem Stack gespeichert werden. Wenn wir die Anzahl begrenzen können, sodass wir den Stack verwenden, verbrauchen wir weniger Gas. Weitere Details finden Sie im Yellow Paper, den formalen Ethereum-Spezifikationen (opens in a new tab), S. 26, Gleichung 298.

            address _token0 = token0;
            address _token1 = token1;
            require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
            if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // Token optimistisch transferieren
            if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // Token optimistisch transferieren

Dieser Transfer ist optimistisch, da wir transferieren, bevor wir sicher sind, dass alle Bedingungen erfüllt sind. Dies ist in Ethereum in Ordnung, denn wenn die Bedingungen später im Aufruf nicht erfüllt sind, machen wir ihn rückgängig (revert) und alle dadurch verursachten Änderungen werden verworfen.

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

Informieren Sie den Empfänger auf Anfrage über den Tausch.

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

Holen Sie sich die aktuellen Salden. Der Peripherie-Vertrag sendet uns die Token, bevor er uns für den Tausch aufruft. Dies macht es dem Vertrag leicht zu überprüfen, dass er nicht betrogen wird, eine Überprüfung, die im Kernvertrag stattfinden muss (da wir von anderen Entitäten als unserem Peripherie-Vertrag aufgerufen werden können).

        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');
        { // Gültigkeitsbereich für reserve{0,1}Adjusted, vermeidet 'stack too deep'-Fehler
            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');

Dies ist eine Plausibilitätsprüfung, um sicherzustellen, dass wir durch den Tausch nicht verlieren. Es gibt keinen Umstand, unter dem ein Tausch reserve0*reserve1 reduzieren sollte. Hier stellen wir auch sicher, dass eine Gebühr von 0,3 % beim Tausch gesendet wird; bevor wir den Wert von K auf Plausibilität prüfen, multiplizieren wir beide Salden mit 1000 abzüglich der mit 3 multiplizierten Beträge, was bedeutet, dass 0,3 % (3/1000 = 0,003 = 0,3 %) vom Saldo abgezogen werden, bevor sein K-Wert mit dem K-Wert der aktuellen Reserven verglichen wird.

        }

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

Aktualisieren Sie reserve0 und reserve1 und bei Bedarf die Preisakkumulatoren und den Zeitstempel und geben Sie ein Ereignis aus.

Sync oder Skim

Es ist möglich, dass die tatsächlichen Salden nicht mehr mit den Reserven synchron sind, von denen der Paar-Tausch glaubt, dass er sie hat. Es gibt keine Möglichkeit, Token ohne die Zustimmung des Vertrags abzuheben, aber Einzahlungen sind eine andere Sache. Ein Konto kann Token an den Tausch transferieren, ohne entweder mint oder swap aufzurufen.

In diesem Fall gibt es zwei Lösungen:

  • sync, aktualisiere die Reserven auf die aktuellen Salden
  • skim, hebe den zusätzlichen Betrag ab. Beachten Sie, dass jedes Konto skim aufrufen darf, da wir nicht wissen, wer die Token eingezahlt hat. Diese Information wird in einem Ereignis ausgegeben, aber Ereignisse sind von der Blockchain aus nicht zugänglich.

UniswapV2Factory.sol

Dieser Vertrag (opens in a new tab) erstellt die Paar-Tausche.

pragma solidity =0.5.16;

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

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

Diese Zustandsvariablen sind notwendig, um die Protokollgebühr zu implementieren (siehe Whitepaper (opens in a new tab), S. 5). Die Adresse feeTo sammelt die Liquiditäts-Token für die Protokollgebühr an, und feeToSetter ist die Adresse, die feeTo in eine andere Adresse ändern darf.

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

Diese Variablen verfolgen die Paare, die Tausche zwischen zwei Token-Typen.

Die erste, getPair, ist ein Mapping, das einen Paar-Tausch-Vertrag basierend auf den beiden ERC-20-Token identifiziert, die er tauscht. ERC-20-Token werden durch die Adressen der Verträge identifiziert, die sie implementieren, sodass die Schlüssel und der Wert alle Adressen sind. Um die Adresse des Paar-Tauschs zu erhalten, mit dem Sie von tokenA in tokenB konvertieren können, verwenden Sie getPair[<tokenA address>][<tokenB address>] (oder umgekehrt).

Die zweite Variable, allPairs, ist ein Array, das alle Adressen von Paar-Tauschen enthält, die von dieser Factory erstellt wurden. In Ethereum können Sie nicht über den Inhalt eines Mappings iterieren oder eine Liste aller Schlüssel erhalten, daher ist diese Variable die einzige Möglichkeit zu wissen, welche Tausche diese Factory verwaltet.

Hinweis: Der Grund, warum Sie nicht über alle Schlüssel eines Mappings iterieren können, ist, dass die Datenspeicherung von Verträgen teuer ist. Je weniger wir davon verwenden, desto besser, und je seltener wir sie ändern, desto besser. Sie können Mappings erstellen, die Iteration unterstützen (opens in a new tab), aber sie erfordern zusätzlichen Speicher für eine Liste von Schlüsseln. In den meisten Anwendungen benötigen Sie das nicht.

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

Dieses Ereignis wird ausgegeben, wenn ein neuer Paar-Tausch erstellt wird. Es enthält die Adressen der Token, die Adresse des Paar-Tauschs und die Gesamtzahl der von der Factory verwalteten Tausche.

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

Das Einzige, was der Konstruktor tut, ist die Angabe von feeToSetter. Factorys starten ohne Gebühr, und nur feeSetter kann das ändern.

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

Diese Funktion gibt die Anzahl der Tauschpaare zurück.

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

Dies ist die Hauptfunktion der Factory, um einen Paar-Tausch zwischen zwei ERC-20-Token zu erstellen. Beachten Sie, dass jeder diese Funktion aufrufen kann. Sie benötigen keine Erlaubnis von Uniswap, um einen neuen Paar-Tausch zu erstellen.

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

Wir möchten, dass die Adresse des neuen Tauschs deterministisch ist, damit sie im Voraus offchain berechnet werden kann (dies kann für Layer-2-Transaktionen nützlich sein). Dazu benötigen wir eine konsistente Reihenfolge der Token-Adressen, unabhängig von der Reihenfolge, in der wir sie erhalten haben, also sortieren wir sie hier.

        require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
        require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // eine einzige Prüfung ist ausreichend

Große Liquiditätspools sind besser als kleine, da sie stabilere Preise haben. Wir möchten nicht mehr als einen einzigen Liquiditätspool pro Token-Paar haben. Wenn es bereits einen Tausch gibt, besteht keine Notwendigkeit, einen weiteren für dasselbe Paar zu erstellen.

        bytes memory bytecode = type(UniswapV2Pair).creationCode;

Um einen neuen Vertrag zu erstellen, benötigen wir den Code, der ihn erstellt (sowohl die Konstruktor-Funktion als auch den Code, der den EVM-Bytecode des eigentlichen Vertrags in den Speicher schreibt). Normalerweise verwenden wir in Solidity einfach addr = new <name of contract>(<constructor parameters>) und der Compiler kümmert sich um alles für uns, aber um eine deterministische Vertragsadresse zu haben, müssen wir den CREATE2-Opcode (opens in a new tab) verwenden. Als dieser Code geschrieben wurde, wurde dieser Opcode noch nicht von Solidity unterstützt, daher war es notwendig, den Code manuell abzurufen. Dies ist kein Problem mehr, da Solidity jetzt CREATE2 unterstützt (opens in a new tab).

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

Wenn ein Opcode noch nicht von Solidity unterstützt wird, können wir ihn mithilfe von Inline-Assembly (opens in a new tab) aufrufen.

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

Rufen Sie die Funktion initialize auf, um dem neuen Tausch mitzuteilen, welche zwei Token er tauscht.

        getPair[token0][token1] = pair;
        getPair[token1][token0] = pair; // füllt das Mapping in umgekehrter Richtung
        allPairs.push(pair);
        emit PairCreated(token0, token1, pair, allPairs.length);
    }

Speichern Sie die neuen Paar-Informationen in den Zustandsvariablen und geben Sie ein Ereignis aus, um die Welt über den neuen Paar-Tausch zu informieren.

Diese beiden Funktionen ermöglichen es feeSetter, den Gebührenempfänger (falls vorhanden) zu steuern und feeSetter in eine neue Adresse zu ändern.

UniswapV2ERC20.sol

Dieser Vertrag (opens in a new tab) implementiert den ERC-20-Liquiditäts-Token. Er ähnelt dem OpenZeppelin-ERC-20-Vertrag, daher werde ich nur den Teil erklären, der anders ist, die permit-Funktionalität.

Transaktionen auf Ethereum kosten Ether (ETH), was echtem Geld entspricht. Wenn Sie ERC-20-Token, aber keine ETH haben, können Sie keine Transaktionen senden, also können Sie nichts damit anfangen. Eine Lösung zur Vermeidung dieses Problems sind Meta-Transaktionen (opens in a new tab). Der Eigentümer der Token signiert eine Transaktion, die es jemand anderem ermöglicht, Token offchain abzuheben, und sendet sie über das Internet an den Empfänger. Der Empfänger, der über ETH verfügt, reicht dann die Genehmigung im Namen des Eigentümers ein.

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

Dieser Hash ist der Identifikator für den Transaktionstyp (opens in a new tab). Der einzige, den wir hier unterstützen, ist Permit mit diesen Parametern.

    mapping(address => uint) public nonces;

Es ist für einen Empfänger nicht machbar, eine digitale Signatur zu fälschen. Es ist jedoch trivial, dieselbe Transaktion zweimal zu senden (dies ist eine Form der Replay-Attacke (opens in a new tab)). Um dies zu verhindern, verwenden wir eine Nonce (opens in a new tab). Wenn die Nonce eines neuen Permit nicht um eins größer ist als die zuletzt verwendete, gehen wir davon aus, dass sie ungültig ist.

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

Dies ist der Code zum Abrufen des Chain-Identifikators (opens in a new tab). Er verwendet einen EVM-Assembly-Dialekt namens Yul (opens in a new tab). Beachten Sie, dass Sie in der aktuellen Version von Yul chainid() verwenden müssen, nicht chainid.

Berechnen Sie den Domain-Separator (opens in a new tab) für EIP-712.

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

Dies ist die Funktion, die die Berechtigungen implementiert. Sie erhält als Parameter die relevanten Felder und die drei skalaren Werte für die Signatur (opens in a new tab) (v, r und s).

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

Akzeptieren Sie keine Transaktionen nach Ablauf der Frist.

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

abi.encodePacked(...) ist die Nachricht, die wir erwarten. Wir wissen, wie die Nonce lauten sollte, daher müssen wir sie nicht als Parameter erhalten.

Der Ethereum-Signaturalgorithmus erwartet 256 Bit zum Signieren, daher verwenden wir die Hashfunktion keccak256.

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

Aus dem Digest und der Signatur können wir die Adresse, die sie signiert hat, mithilfe von ecrecover (opens in a new tab) ermitteln.

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

Wenn alles in Ordnung ist, behandeln Sie dies als eine ERC-20-Genehmigung (approve) (opens in a new tab).

Die Peripherie-Verträge

Die Peripherie-Verträge sind die API (Application Programming Interface) für Uniswap. Sie stehen für externe Aufrufe zur Verfügung, entweder von anderen Verträgen oder dezentralen Anwendungen (dapps). Sie könnten die Kernverträge direkt aufrufen, aber das ist komplizierter und Sie könnten Werte verlieren, wenn Sie einen Fehler machen. Die Kernverträge enthalten nur Tests, um sicherzustellen, dass sie nicht betrogen werden, aber keine Plausibilitätsprüfungen für andere. Diese befinden sich in der Peripherie, damit sie bei Bedarf aktualisiert werden können.

UniswapV2Router01.sol

Dieser Vertrag (opens in a new tab) hat Probleme und sollte nicht mehr verwendet werden (opens in a new tab). Glücklicherweise sind die Peripherie-Verträge zustandslos und halten keine Vermögenswerte, sodass es einfach ist, ihn als veraltet zu markieren und den Leuten vorzuschlagen, stattdessen den Ersatz, UniswapV2Router02, zu verwenden.

UniswapV2Router02.sol

In den meisten Fällen würden Sie Uniswap über diesen Vertrag (opens in a new tab) nutzen. Wie man ihn verwendet, können Sie hier (opens in a new tab) sehen.

Die meisten davon sind uns entweder schon begegnet oder ziemlich offensichtlich. Die einzige Ausnahme ist IWETH.sol. Uniswap v2 ermöglicht den Tausch für jedes Paar von ERC-20-Token, aber Ether (ETH) selbst ist kein ERC-20-Token. Er ist älter als der Standard und wird durch einzigartige Mechanismen transferiert. Um die Verwendung von ETH in Verträgen zu ermöglichen, die für ERC-20-Token gelten, wurde der Vertrag für Wrapped Ether (WETH) (opens in a new tab) entwickelt. Sie senden diesem Vertrag ETH, und er prägt Ihnen eine entsprechende Menge an WETH. Oder Sie können WETH verbrennen und erhalten ETH zurück.

contract UniswapV2Router02 is IUniswapV2Router02 {
    using SafeMath for uint;

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

Der Router muss wissen, welche Factory verwendet werden soll, und für Transaktionen, die WETH erfordern, welcher WETH-Vertrag verwendet werden soll. Diese Werte sind unveränderlich (opens in a new tab), was bedeutet, dass sie nur im Konstruktor festgelegt werden können. Dies gibt den Benutzern die Gewissheit, dass niemand sie ändern kann, um auf weniger ehrliche Verträge zu verweisen.

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

Dieser Modifikator stellt sicher, dass zeitlich begrenzte Transaktionen („mache X vor Zeitpunkt Y, wenn du kannst“) nicht nach Ablauf ihres Zeitlimits stattfinden.

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

Der Konstruktor setzt lediglich die unveränderlichen Zustandsvariablen.

    receive() external payable {
        assert(msg.sender == WETH); // akzeptiert ETH nur über Fallback aus dem WETH-Vertrag
    }

Diese Funktion wird aufgerufen, wenn wir Token aus dem WETH-Vertrag wieder in ETH einlösen. Nur der von uns verwendete WETH-Vertrag ist dazu autorisiert.

Liquidität hinzufügen

Diese Funktionen fügen dem Tauschpaar Token hinzu, was den Liquiditätspool vergrößert.


    // **** LIQUIDITÄT HINZUFÜGEN ****
    function _addLiquidity(

Diese Funktion wird verwendet, um die Menge an A- und B-Token zu berechnen, die in das Tauschpaar eingezahlt werden sollen.

        address tokenA,
        address tokenB,

Dies sind die Adressen der ERC-20-Token-Verträge.

        uint amountADesired,
        uint amountBDesired,

Dies sind die Beträge, die der Liquiditätsanbieter einzahlen möchte. Sie sind auch die maximalen Beträge von A und B, die eingezahlt werden sollen.

        uint amountAMin,
        uint amountBMin

Dies sind die minimal akzeptablen Einzahlungsbeträge. Wenn die Transaktion nicht mit diesen oder höheren Beträgen stattfinden kann, wird sie rückgängig gemacht. Wenn Sie diese Funktion nicht wünschen, geben Sie einfach null an.

Liquiditätsanbieter geben in der Regel ein Minimum an, weil sie die Transaktion auf einen Wechselkurs beschränken wollen, der nahe am aktuellen liegt. Wenn der Wechselkurs zu stark schwankt, könnte das auf Neuigkeiten hindeuten, die die zugrunde liegenden Werte verändern, und sie möchten manuell entscheiden, was zu tun ist.

Stellen Sie sich zum Beispiel einen Fall vor, in dem der Wechselkurs eins zu eins ist und der Liquiditätsanbieter diese Werte angibt:

ParameterWert
amountADesired1000
amountBDesired1000
amountAMin900
amountBMin800

Solange der Wechselkurs zwischen 0,9 und 1,25 bleibt, findet die Transaktion statt. Wenn der Wechselkurs diesen Bereich verlässt, wird die Transaktion abgebrochen.

Der Grund für diese Vorsichtsmaßnahme ist, dass Transaktionen nicht sofort erfolgen; Sie reichen sie ein und schließlich wird ein Validator sie in einen Block aufnehmen (es sei denn, Ihr Gaspreis ist sehr niedrig, in welchem Fall Sie eine weitere Transaktion mit derselben Nonce und einem höheren Gaspreis einreichen müssen, um sie zu überschreiben). Sie können nicht kontrollieren, was in der Zeit zwischen Einreichung und Aufnahme passiert.

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

Die Funktion gibt die Beträge zurück, die der Liquiditätsanbieter einzahlen sollte, um ein Verhältnis zu haben, das dem aktuellen Verhältnis zwischen den Reserven entspricht.

        // erstellt das Paar, falls es noch nicht existiert
        if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
            IUniswapV2Factory(factory).createPair(tokenA, tokenB);
        }

Wenn es noch keinen Tausch für dieses Token-Paar gibt, erstellen Sie ihn.

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

Rufen Sie die aktuellen Reserven im Paar ab.

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

Wenn die aktuellen Reserven leer sind, handelt es sich um ein neues Tauschpaar. Die einzuzahlenden Beträge sollten genau den Beträgen entsprechen, die der Liquiditätsanbieter bereitstellen möchte.

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

Wenn wir sehen müssen, wie hoch die Beträge sein werden, ermitteln wir den optimalen Betrag mit dieser Funktion (opens in a new tab). Wir wollen das gleiche Verhältnis wie bei den aktuellen Reserven.

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

Wenn amountBOptimal kleiner ist als der Betrag, den der Liquiditätsanbieter einzahlen möchte, bedeutet dies, dass Token B derzeit wertvoller ist, als der Liquiditätseinleger denkt, sodass ein kleinerer Betrag erforderlich ist.

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

Wenn der optimale B-Betrag höher ist als der gewünschte B-Betrag, bedeutet dies, dass B-Token derzeit weniger wertvoll sind, als der Liquiditätseinleger denkt, sodass ein höherer Betrag erforderlich ist. Der gewünschte Betrag ist jedoch ein Maximum, daher können wir das nicht tun. Stattdessen berechnen wir die optimale Anzahl von A-Token für die gewünschte Menge an B-Token.

Zusammengenommen ergibt sich dieses Diagramm. Nehmen wir an, Sie versuchen, tausend A-Token (blaue Linie) und tausend B-Token (rote Linie) einzuzahlen. Die x-Achse ist der Wechselkurs, A/B. Wenn x=1, sind sie gleich viel wert und Sie zahlen von jedem tausend ein. Wenn x=2, ist A doppelt so viel wert wie B (Sie erhalten zwei B-Token für jeden A-Token), also zahlen Sie tausend B-Token ein, aber nur 500 A-Token. Wenn x=0,5, ist die Situation umgekehrt: tausend A-Token und fünfhundert B-Token.

Graph

Sie könnten Liquidität direkt in den Kernvertrag einzahlen (mit UniswapV2Pair::mint (opens in a new tab)), aber der Kernvertrag prüft nur, ob er selbst nicht betrogen wird. Sie gehen also das Risiko ein, an Wert zu verlieren, wenn sich der Wechselkurs zwischen dem Zeitpunkt der Einreichung Ihrer Transaktion und dem Zeitpunkt ihrer Ausführung ändert. Wenn Sie den Peripherie-Vertrag verwenden, berechnet er den Betrag, den Sie einzahlen sollten, und zahlt ihn sofort ein, sodass sich der Wechselkurs nicht ändert und Sie nichts verlieren.

Diese Funktion kann durch eine Transaktion aufgerufen werden, um Liquidität einzuzahlen. Die meisten Parameter sind die gleichen wie in _addLiquidity oben, mit zwei Ausnahmen:

. to ist die Adresse, für die die neuen Liquiditäts-Token geprägt werden, um den Anteil des Liquiditätsanbieters am Pool anzuzeigen . deadline ist ein Zeitlimit für die Transaktion

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

Wir berechnen die tatsächlich einzuzahlenden Beträge und ermitteln dann die Adresse des Liquiditätspools. Um Gas zu sparen, tun wir dies nicht, indem wir die Factory abfragen, sondern indem wir die Bibliotheksfunktion pairFor verwenden (siehe unten unter Bibliotheken).

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

Transferieren Sie die korrekten Token-Beträge vom Benutzer in das Tauschpaar.

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

Im Gegenzug erhält die Adresse to Liquiditäts-Token für den teilweisen Besitz des Pools. Die Funktion mint des Kernvertrags prüft, wie viele zusätzliche Token er hat (im Vergleich zum letzten Mal, als sich die Liquidität geändert hat), und prägt entsprechend Liquidität.

    function addLiquidityETH(
        address token,
        uint amountTokenDesired,

Wenn ein Liquiditätsanbieter Liquidität für einen Token/ETH-Paar-Tausch bereitstellen möchte, gibt es einige Unterschiede. Der Vertrag übernimmt das Wrapping der ETH für den Liquiditätsanbieter. Es ist nicht nötig anzugeben, wie viele ETH der Benutzer einzahlen möchte, da der Benutzer sie einfach mit der Transaktion sendet (der Betrag ist in msg.value verfügbar).

Um die ETH einzuzahlen, verpackt der Vertrag sie zunächst in WETH und transferiert die WETH dann in das Paar. Beachten Sie, dass der Transfer in ein assert eingebettet ist. Das bedeutet, dass bei einem Fehlschlagen des Transfers auch dieser Vertragsaufruf fehlschlägt und das Wrapping somit nicht wirklich stattfindet.

        liquidity = IUniswapV2Pair(pair).mint(to);
        // erstattet Dust-Ether, falls vorhanden
        if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
    }

Der Benutzer hat uns die ETH bereits gesendet. Wenn also etwas übrig bleibt (weil der andere Token weniger wertvoll ist, als der Benutzer dachte), müssen wir eine Rückerstattung veranlassen.

Liquidität entfernen

Diese Funktionen entfernen Liquidität und zahlen den Liquiditätsanbieter aus.

Der einfachste Fall der Liquiditätsentfernung. Es gibt einen Mindestbetrag für jeden Token, den der Liquiditätsanbieter zu akzeptieren bereit ist, und dies muss vor Ablauf der Frist geschehen.

        address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
        IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // sendet Liquidität an das Paar
        (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);

Die Funktion burn des Kernvertrags übernimmt die Rückzahlung der Token an den Benutzer.

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

Wenn eine Funktion mehrere Werte zurückgibt, wir aber nur an einigen davon interessiert sind, erhalten wir auf diese Weise nur diese Werte. Es ist in Bezug auf Gas etwas günstiger, als einen Wert zu lesen und ihn nie zu verwenden.

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

Übersetzen Sie die Beträge von der Art und Weise, wie der Kernvertrag sie zurückgibt (Token mit der niedrigeren Adresse zuerst), in die Art und Weise, wie der Benutzer sie erwartet (entsprechend tokenA und tokenB).

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

Es ist in Ordnung, zuerst den Transfer durchzuführen und dann zu überprüfen, ob er legitim ist, denn wenn dies nicht der Fall ist, machen wir alle Zustandsänderungen rückgängig.

Das Entfernen von Liquidität für ETH ist fast dasselbe, außer dass wir die WETH-Token erhalten und sie dann gegen ETH einlösen, um sie dem Liquiditätsanbieter zurückzugeben.

Diese Funktionen leiten Meta-Transaktionen weiter, um Benutzern ohne Ether die Abhebung aus dem Pool zu ermöglichen, wobei der Permit-Mechanismus verwendet wird.

Diese Funktion kann für Token verwendet werden, die Transfer- oder Speichergebühren haben. Wenn ein Token solche Gebühren hat, können wir uns nicht auf die Funktion removeLiquidity verlassen, um uns mitzuteilen, wie viel von dem Token wir zurückbekommen. Daher müssen wir zuerst abheben und dann den Kontostand abrufen.

Die letzte Funktion kombiniert Speichergebühren mit Meta-Transaktionen.

Handel

    // **** TAUSCH ****
    // setzt voraus, dass der anfängliche Betrag bereits an das erste Paar gesendet wurde
    function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {

Diese Funktion führt interne Verarbeitungen durch, die für die Funktionen erforderlich sind, die Händlern zur Verfügung stehen.

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

Während ich dies schreibe, gibt es 388.160 ERC-20-Token (opens in a new tab). Wenn es für jedes Token-Paar ein Tauschpaar gäbe, wären das über 150 Milliarden Tauschpaare. Die gesamte Chain hat im Moment nur 0,1 % dieser Anzahl an Konten (opens in a new tab). Stattdessen unterstützen die Tausch-Funktionen das Konzept eines Pfades. Ein Händler kann A gegen B, B gegen C und C gegen D tauschen, sodass kein direktes A-D-Tauschpaar erforderlich ist.

Die Preise auf diesen Märkten sind in der Regel synchronisiert, denn wenn sie nicht synchron sind, entsteht eine Möglichkeit für Arbitrage. Stellen Sie sich zum Beispiel drei Token vor: A, B und C. Es gibt drei Tauschpaare, eines für jedes Paar.

  1. Die Ausgangssituation
  2. Ein Händler verkauft 24,695 A-Token und erhält 25,305 B-Token.
  3. Der Händler verkauft 24,695 B-Token für 25,305 C-Token und behält etwa 0,61 B-Token als Gewinn.
  4. Dann verkauft der Händler 24,695 C-Token für 25,305 A-Token und behält etwa 0,61 C-Token als Gewinn. Der Händler hat außerdem 0,61 zusätzliche A-Token (die 25,305, die der Händler am Ende hat, abzüglich der ursprünglichen Investition von 24,695).
SchrittA-B TauschB-C TauschA-C Tausch
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];

Rufen Sie das Paar ab, das wir gerade bearbeiten, sortieren Sie es (zur Verwendung mit dem Paar) und rufen Sie den erwarteten Ausgabebetrag ab.

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

Rufen Sie die erwarteten Ausgabebeträge ab, sortiert so, wie das Tauschpaar sie erwartet.

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

Ist dies der letzte Tausch? Wenn ja, senden Sie die für den Handel erhaltenen Token an das Ziel. Wenn nicht, senden Sie sie an das nächste Tauschpaar.


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

Rufen Sie tatsächlich das Tauschpaar auf, um die Token zu tauschen. Wir benötigen keinen Callback, um über den Tausch informiert zu werden, daher senden wir keine Bytes in diesem Feld.

    function swapExactTokensForTokens(

Diese Funktion wird direkt von Händlern verwendet, um einen Token gegen einen anderen zu tauschen.

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

Dieser Parameter enthält die Adressen der ERC-20-Verträge. Wie oben erklärt, handelt es sich um ein Array, da Sie möglicherweise mehrere Tauschpaare durchlaufen müssen, um von dem Vermögenswert, den Sie haben, zu dem Vermögenswert zu gelangen, den Sie möchten.

Ein Funktionsparameter in Solidity kann entweder im memory oder in den calldata gespeichert werden. Wenn die Funktion ein Einstiegspunkt in den Vertrag ist, der direkt von einem Benutzer (über eine Transaktion) oder von einem anderen Vertrag aufgerufen wird, kann der Wert des Parameters direkt aus den Aufrufdaten entnommen werden. Wenn die Funktion intern aufgerufen wird, wie _swap oben, müssen die Parameter im memory gespeichert werden. Aus der Perspektive des aufgerufenen Vertrags sind die calldata schreibgeschützt.

Bei skalaren Typen wie uint oder address übernimmt der Compiler die Wahl des Speichers für uns, aber bei Arrays, die länger und teurer sind, geben wir die Art des zu verwendenden Speichers an.

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

Rückgabewerte werden immer im Memory zurückgegeben.

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

Berechnen Sie den Betrag, der bei jedem Tausch gekauft werden soll. Wenn das Ergebnis geringer ist als das Minimum, das der Händler zu akzeptieren bereit ist, machen Sie die Transaktion rückgängig.

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

Transferieren Sie schließlich den anfänglichen ERC-20-Token auf das Konto für das erste Tauschpaar und rufen Sie _swap auf. Dies geschieht alles in derselben Transaktion, sodass das Tauschpaar weiß, dass alle unerwarteten Token Teil dieses Transfers sind.

Die vorherige Funktion, swapTokensForTokens, ermöglicht es einem Händler, eine genaue Anzahl von Eingabe-Token anzugeben, die er zu geben bereit ist, und die Mindestanzahl von Ausgabe-Token, die er im Gegenzug zu erhalten bereit ist. Diese Funktion führt den umgekehrten Tausch durch: Sie lässt einen Händler die Anzahl der gewünschten Ausgabe-Token und die maximale Anzahl der Eingabe-Token angeben, die er dafür zu zahlen bereit ist.

In beiden Fällen muss der Händler diesem Peripherie-Vertrag zunächst einen Freigabebetrag gewähren, damit dieser sie transferieren kann.

Diese vier Varianten beinhalten alle den Handel zwischen ETH und Token. Der einzige Unterschied besteht darin, dass wir entweder ETH vom Händler erhalten und diese verwenden, um WETH zu prägen, oder wir erhalten WETH vom letzten Tausch im Pfad und verbrennen sie, um dem Händler die resultierenden ETH zurückzusenden.

    // **** TAUSCH (unterstützt Token mit Gebühr bei Transfer) ****
    // setzt voraus, dass der anfängliche Betrag bereits an das erste Paar gesendet wurde
    function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {

Dies ist die interne Funktion zum Tauschen von Token, die Transfer- oder Speichergebühren haben, um (dieses Problem (opens in a new tab)) zu lösen.

Aufgrund der Transfergebühren können wir uns nicht auf die Funktion getAmountsOut verlassen, um uns mitzuteilen, wie viel wir aus jedem Transfer herausbekommen (so wie wir es vor dem Aufruf der ursprünglichen _swap tun). Stattdessen müssen wir zuerst transferieren und dann sehen, wie viele Token wir zurückbekommen haben.

Hinweis: Theoretisch könnten wir einfach diese Funktion anstelle von _swap verwenden, aber in bestimmten Fällen (zum Beispiel, wenn der Transfer am Ende rückgängig gemacht wird, weil am Ende nicht genug vorhanden ist, um das erforderliche Minimum zu erreichen) würde das letztendlich mehr Gas kosten. Token mit Transfergebühren sind ziemlich selten. Obwohl wir sie also berücksichtigen müssen, besteht keine Notwendigkeit, bei allen Tauschen davon auszugehen, dass sie mindestens einen davon durchlaufen.

Dies sind dieselben Varianten, die für normale Token verwendet werden, aber sie rufen stattdessen _swapSupportingFeeOnTransferTokens auf.

Diese Funktionen sind nur Proxys, die die UniswapV2Library-Funktionen aufrufen.

UniswapV2Migrator.sol

Dieser Vertrag wurde verwendet, um Tausche von der alten v1 auf v2 zu migrieren. Da sie nun migriert wurden, ist er nicht mehr relevant.

Die Bibliotheken

Die SafeMath-Bibliothek (opens in a new tab) ist gut dokumentiert, daher muss sie hier nicht dokumentiert werden.

Math

Diese Bibliothek enthält einige mathematische Funktionen, die normalerweise in Solidity-Code nicht benötigt werden, weshalb sie nicht Teil der Sprache sind.

Beginnen Sie mit x als Schätzwert, der höher als die Quadratwurzel ist (das ist der Grund, warum wir 1-3 als Sonderfälle behandeln müssen).

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

Ermitteln Sie einen genaueren Schätzwert: den Durchschnitt aus dem vorherigen Schätzwert und der Zahl, deren Quadratwurzel wir suchen, geteilt durch den vorherigen Schätzwert. Wiederholen Sie dies, bis der neue Schätzwert nicht mehr niedriger als der bestehende ist. Weitere Details finden Sie hier (opens in a new tab).

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

Wir sollten niemals die Quadratwurzel von null benötigen. Die Quadratwurzeln von eins, zwei und drei sind ungefähr eins (wir verwenden Ganzzahlen, ignorieren also den Bruchteil).

        }
    }
}

Festkommabruchteile (UQ112x112)

Diese Bibliothek verarbeitet Brüche, die normalerweise nicht Teil der Ethereum-Arithmetik sind. Sie tut dies, indem sie die Zahl x als x*2^112 kodiert. Dadurch können wir die ursprünglichen Additions- und Subtraktions-Opcodes ohne Änderung verwenden.

Q112 ist die Kodierung für eins.

    // kodiert einen uint112 als UQ112x112
    function encode(uint112 y) internal pure returns (uint224 z) {
        z = uint224(y) * Q112; // läuft nie über
    }

Da y uint112 ist, kann es höchstens 2^112-1 sein. Diese Zahl kann immer noch als UQ112x112 kodiert werden.

    // dividiert einen UQ112x112 durch einen uint112 und gibt einen UQ112x112 zurück
    function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
        z = x / uint224(y);
    }
}

Wenn wir zwei UQ112x112-Werte dividieren, wird das Ergebnis nicht mehr mit 2^112 multipliziert. Stattdessen nehmen wir also eine Ganzzahl für den Nenner. Wir hätten einen ähnlichen Trick für die Multiplikation anwenden müssen, aber wir müssen keine Multiplikation von UQ112x112-Werten durchführen.

UniswapV2Library

Diese Bibliothek wird nur von den Peripherie-Verträgen verwendet

Sortieren Sie die beiden Token nach Adresse, damit wir die Adresse des Paar-Tauschs für sie erhalten können. Dies ist notwendig, da wir sonst zwei Möglichkeiten hätten, eine für die Parameter A,B und eine weitere für die Parameter B,A, was zu zwei Tauschvorgängen statt einem führen würde.

Diese Funktion berechnet die Adresse des Paar-Tauschs für die beiden Token. Dieser Vertrag wird mit dem CREATE2-Opcode (opens in a new tab) erstellt, sodass wir die Adresse mit demselben Algorithmus berechnen können, wenn wir die verwendeten Parameter kennen. Dies ist viel günstiger, als die Factory abzufragen, und

    // ruft die Reserven für ein Paar ab und sortiert sie
    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);
    }

Diese Funktion gibt die Reserven der beiden Token zurück, über die der Paar-Tausch verfügt. Beachten Sie, dass sie die Token in beliebiger Reihenfolge empfangen kann und sie für die interne Verwendung sortiert.

    // gibt bei einer bestimmten Menge eines Vermögenswerts und Paar-Reserven eine äquivalente Menge des anderen Vermögenswerts zurück
    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;
    }

Diese Funktion gibt Ihnen die Menge an Token B an, die Sie im Gegenzug für Token A erhalten, wenn keine Gebühr anfällt. Diese Berechnung berücksichtigt, dass der Transfer den Wechselkurs ändert.

    // gibt bei einer Eingabemenge eines Vermögenswerts und Paar-Reserven die maximale Ausgabemenge des anderen Vermögenswerts zurück
    function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {

Die obige Funktion quote funktioniert hervorragend, wenn für die Nutzung des Paar-Tauschs keine Gebühr anfällt. Wenn jedoch eine Tauschgebühr von 0,3 % anfällt, ist der Betrag, den Sie tatsächlich erhalten, geringer. Diese Funktion berechnet den Betrag nach Abzug der Tauschgebühr.


        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 verarbeitet Brüche nicht nativ, daher können wir den Betrag nicht einfach mit 0,997 multiplizieren. Stattdessen multiplizieren wir den Zähler mit 997 und den Nenner mit 1000, was denselben Effekt erzielt.

    // gibt bei einer Ausgabemenge eines Vermögenswerts und Paar-Reserven eine erforderliche Eingabemenge des anderen Vermögenswerts zurück
    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);
    }

Diese Funktion macht in etwa dasselbe, aber sie erhält den Ausgabebetrag und liefert die Eingabe.

Diese beiden Funktionen übernehmen die Identifizierung der Werte, wenn es notwendig ist, mehrere Paar-Tauschvorgänge zu durchlaufen.

Transfer Helper

Diese Bibliothek (opens in a new tab) fügt Erfolgsprüfungen rund um ERC-20- und Ethereum-Transfers hinzu, um einen Revert und die Rückgabe eines false-Wertes auf die gleiche Weise zu behandeln.

Wir können einen anderen Vertrag auf eine von zwei Arten aufrufen:

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

Aus Gründen der Abwärtskompatibilität mit Token, die vor dem ERC-20-Standard erstellt wurden, kann ein ERC-20-Aufruf entweder fehlschlagen, indem er rückgängig gemacht wird (in diesem Fall ist success gleich false), oder indem er erfolgreich ist und einen false-Wert zurückgibt (in diesem Fall gibt es Ausgabedaten, und wenn Sie diese als Boolean dekodieren, erhalten Sie false).

Diese Funktion implementiert die Transfer-Funktionalität von ERC-20 (opens in a new tab), die es einem Konto ermöglicht, den von einem anderen Konto bereitgestellten Freigabebetrag auszugeben.

Diese Funktion implementiert die transferFrom-Funktionalität von ERC-20 (opens in a new tab), die es einem Konto ermöglicht, den von einem anderen Konto bereitgestellten Freigabebetrag auszugeben.


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

Diese Funktion transferiert Ether an ein Konto. Jeder Aufruf an einen anderen Vertrag kann versuchen, Ether zu senden. Da wir eigentlich keine Funktion aufrufen müssen, senden wir keine Daten mit dem Aufruf.

Fazit

Dies ist ein langer Artikel mit etwa 50 Seiten. Wenn du es bis hierher geschafft hast: Herzlichen Glückwunsch! Hoffentlich hast du inzwischen die Überlegungen verstanden, die beim Schreiben einer echten Anwendung (im Gegensatz zu kurzen Beispielprogrammen) eine Rolle spielen, und bist nun besser in der Lage, Verträge für deine eigenen Anwendungsfälle zu schreiben.

Geh nun, schreibe etwas Nützliches und beeindrucke uns.

Weitere Arbeiten von mir findest du hier (opens in a new tab).

Letzte Aktualisierung der Seite: 3. April 2026