Zum Hauptinhalt springen

Uniswap-v2 Contract Walk-Through

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

Einführung

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

Was macht Uniswap?

Grundsätzlich gibt es zwei Arten von Nutzern: 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 den teilweisen Besitz des Pools darstellt und als Liquiditäts-Token bezeichnet 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 hält. Darüber hinaus behält der Pool einen kleinen Prozentsatz als Belohnung für den Liquiditäts-Pool ein.

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.

Core-Contracts vs. Periphery-Contracts

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

Daten- und Kontrollflüsse

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

  1. Tauschen zwischen verschiedenen Token
  2. Liquidität zum Markt hinzufügen und mit ERC-20-Liquiditäts-Token der Tauschbörse belohnt werden
  3. ERC-20-Liquiditäts-Token verbrennen und die ERC-20-Token zurückerhalten, die die Tauschbörse Händlern zum Tauschen anbietet

Tauschen

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

Aufrufer

  1. Dem Peripherie-Konto eine Freigabe (Allowance) in Höhe des zu tauschenden Betrags erteilen.
  2. Eine der vielen Tausch-Funktionen des Peripherie-Vertrags aufrufen (welche, 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 Peripherie-Vertrag (UniswapV2Router02.sol)

  1. Die Beträge identifizieren, 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 wird der Eingabe-Token gesendet und dann die swap-Funktion der Tauschbörse aufgerufen. In den meisten Fällen ist die Zieladresse für die Token die nächste Tauschbörse im Pfad. Bei der letzten Tauschbörse ist es die vom Händler angegebene Adresse.

Im Kernvertrag (UniswapV2Pair.sol)

  1. Überprüfen, ob der Kernvertrag nicht betrogen wird und nach dem Tauschen 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 Tauschen erhalten haben.
  3. Die Ausgabe-Token an das Ziel senden.
  4. _update aufrufen, um die Reservebeträge zu aktualisieren

Zurück im Peripherie-Vertrag (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 eine Freigabe (Allowance) in den Beträgen erteilen, die dem Liquiditätspool hinzugefügt werden sollen.
  2. Eine der addLiquidity-Funktionen des Peripherie-Vertrags aufrufen.

Im Peripherie-Vertrag (UniswapV2Router02.sol)

  1. Bei Bedarf eine neue Tauschbörse für das Paar erstellen
  2. Wenn es eine bestehende Tauschbörse für das Paar gibt, die Menge der hinzuzufügenden Token berechnen. Dies soll für beide Token ein identischer Wert sein, also das gleiche Verhältnis von neuen Token zu bestehenden Token.
  3. Überprüfen, ob die Beträge akzeptabel sind (Aufrufer können einen Mindestbetrag angeben, unter dem 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 eine Freigabe (Allowance) für Liquiditäts-Token erteilen, die im Austausch gegen die zugrunde liegenden Token verbrannt werden sollen.
  2. Eine der removeLiquidity-Funktionen des Peripherie-Vertrags aufrufen.

Im Peripherie-Vertrag (UniswapV2Router02.sol)

  1. Die Liquiditäts-Token an die Tauschbörse des Paares 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 Smart Contracts, die die Liquidität halten.

UniswapV2Pair.sol

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

1pragma solidity =0.5.16;
2
3import './interfaces/IUniswapV2Pair.sol';
4import './UniswapV2ERC20.sol';
5import './libraries/Math.sol';
6import './libraries/UQ112x112.sol';
7import './interfaces/IERC20.sol';
8import './interfaces/IUniswapV2Factory.sol';
9import './interfaces/IUniswapV2Callee.sol';
Alle anzeigen

Dies sind alle Schnittstellen, die der Smart Contract kennen muss, entweder weil der Smart Contract sie implementiert (IUniswapV2Pair und UniswapV2ERC20) oder weil er Smart Contracts aufruft, die sie implementieren.

1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {

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

1 using SafeMath for uint;

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

1 using UQ112x112 for uint224;

Viele Berechnungen im Pool-Vertrag erfordern Brüche. Brüche werden jedoch von der Ethereum Virtual Machine 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 stehen. 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

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

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

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

1 address public factory;

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

1 address public token0;
2 address public token1;

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

1 uint112 private reserve0; // nutzt einen einzigen Speicherplatz, zugänglich über getReserves
2 uint112 private reserve1; // nutzt einen einzigen 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 den Wert von reserve1/reserve0 token1 wert.

1 uint32 private blockTimestampLast; // nutzt einen einzigen Speicherplatz, zugänglich über getReserves

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

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

1 uint public price0CumulativeLast;
2 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.

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

Die Art und Weise, wie die Paar-Börse ü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)

1 uint private unlocked = 1;

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

1 modifier lock() {

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

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

Wenn unlocked gleich eins ist, setzen Sie es auf null. Wenn es bereits null ist, machen Sie den Aufruf rückgängig (revert) und lassen Sie ihn fehlschlagen.

1 _;

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.

1 unlocked = 1;
2 }

Nachdem die Hauptfunktion zurückkehrt, geben Sie die Sperre frei.

Sonstige Funktionen

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

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

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

Diese interne Funktion überträgt eine Menge an ERC-20-Token von der Börse 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).

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

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

  1. Revert (Rückgängig machen). Wenn ein Aufruf an einen externen Smart Contract 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, machen Sie den Vorgang rückgängig (revert).

Ereignisse (Events)

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

Diese beiden Ereignisse werden ausgelöst, wenn ein Liquiditätsanbieter entweder Liquidität einzahlt (Mint) oder abhebt (Burn). In beiden Fällen sind die Beträge 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.

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

Dieses Ereignis wird ausgelöst, 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 die Börse gesendet oder von ihr empfangen werden.

1 event Sync(uint112 reserve0, uint112 reserve1);

Schließlich wird Sync jedes Mal ausgelöst, 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 die neue Paar-Börse eingerichtet wird.

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

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.

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

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
1 // aktualisiert Reserven und, beim ersten Aufruf pro Block, Preisakkumulatoren
2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {

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

1 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 zurückspringt), weigern Sie sich, 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.

1 uint32 blockTimestamp = uint32(block.timestamp % 2**32);
2 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // Überlauf ist erwünscht
3 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.

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

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:

Ereignisreserve0reserve1ZeitstempelMarginaler 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.

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

Aktualisieren Sie schließlich die globalen Variablen und lösen Sie ein Sync-Ereignis aus.

_mintFee
1 // wenn die Gebühr aktiviert ist, Prägen von Liquidität entsprechend 1/6 des Wachstums von sqrt(k)
2 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.

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

Lesen Sie 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.

1 uint _kLast = kLast; // Gaseinsparungen

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

1 if (feeOn) {
2 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 an die Adresse feeTo bereitgestellt werden.

1 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
2 uint rootKLast = Math.sqrt(_kLast);
3 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.

1 uint numerator = totalSupply.mul(rootK.sub(rootKLast));
2 uint denominator = rootK.mul(5).add(rootKLast);
3 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).

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

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

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

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

Von außen zugängliche Funktionen

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

mint
1 // diese Low-Level-Funktion sollte von einem Vertrag aufgerufen werden, der wichtige Sicherheitsprüfungen durchführt
2 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 (sodass niemand sonst eine Transaktion einreichen könnte, die die neue Liquidität vor dem rechtmäßigen Eigentümer beansprucht).

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

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

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

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

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

Wenn dies die erste Einzahlung ist, erstellen Sie MINIMUM_LIQUIDITY-Token und senden Sie 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 viele Token1 wie Token0 eingezahlt. Ein Händler kann die Tatsache nutzen, dass die Paar-Börse denkt, dass Token0 wertvoller ist, um Wert daraus zu extrahieren.

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.

1 } else {
2 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
1 }
2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
3 _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.

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

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

burn
1 // diese Low-Level-Funktion sollte von einem Vertrag aufgerufen werden, der wichtige Sicherheitsprüfungen durchführt
2 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.

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

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

1 bool feeOn = _mintFee(_reserve0, _reserve1);
2 uint _totalSupply = totalSupply; // Gaseinsparungen, muss hier definiert werden, da totalSupply in _mintFee aktualisiert werden kann
3 amount0 = liquidity.mul(balance0) / _totalSupply; // die Verwendung von Salden stellt eine anteilige Verteilung sicher
4 amount1 = liquidity.mul(balance1) / _totalSupply; // die Verwendung von Salden stellt eine anteilige Verteilung sicher
5 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.

1 _burn(address(this), liquidity);
2 _safeTransfer(_token0, to, amount0);
3 _safeTransfer(_token1, to, amount1);
4 balance0 = IERC20(_token0).balanceOf(address(this));
5 balance1 = IERC20(_token1).balanceOf(address(this));
6
7 _update(balance0, balance1, _reserve0, _reserve1);
8 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 und reserve1 sind aktuell
9 emit Burn(msg.sender, amount0, amount1, to);
10 }
11
Alle anzeigen

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

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

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

1 require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
2 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // Gaseinsparungen
3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
4
5 uint balance0;
6 uint balance1;
7 { // 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 davon gibt, 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.

1 address _token0 = token0;
2 address _token1 = token1;
3 require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
4 if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // überträgt Token optimistisch
5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // überträgt Token optimistisch

Dieser Transfer ist optimistisch, da wir übertragen, 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.

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

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

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

Holen Sie sich die aktuellen Salden. Der Peripherie-Vertrag sendet uns die Token, bevor er uns für den Tausch aufruft. Dies macht es für den Smart Contract einfach 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).

1 uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
2 uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
3 require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
4 { // Gültigkeitsbereich für reserve{0,1}Adjusted, vermeidet 'Stack too deep'-Fehler
5 uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
6 uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
7 require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');

Dies ist eine Plausibilitätsprüfung (Sanity Check), 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 % auf den Tausch erhoben 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.

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

Aktualisieren Sie reserve0 und reserve1 und bei Bedarf die Preisakkumulatoren und den Zeitstempel und lösen 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 die Paar-Börse glaubt, dass sie sie hat. Es gibt keine Möglichkeit, Token ohne die Zustimmung des Smart Contracts abzuheben, aber Einzahlungen sind eine andere Sache. Ein Konto kann Token an die Börse übertragen, ohne entweder mint oder swap aufzurufen.

In diesem Fall gibt es zwei Lösungen:

  • sync, aktualisieren Sie die Reserven auf die aktuellen Salden
  • skim, heben Sie 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.
1 // erzwingt, dass Salden den Reserven entsprechen
2 function skim(address to) external lock {
3 address _token0 = token0; // Gaseinsparungen
4 address _token1 = token1; // Gaseinsparungen
5 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
6 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
7 }
8
9
10
11 // erzwingt, dass Reserven den Salden entsprechen
12 function sync() external lock {
13 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
14 }
15}
Alle anzeigen

UniswapV2Factory.sol

Dieser Smart Contract (opens in a new tab) erstellt die Paar-Börsen.

1pragma solidity =0.5.16;
2
3import './interfaces/IUniswapV2Factory.sol';
4import './UniswapV2Pair.sol';
5
6contract UniswapV2Factory is IUniswapV2Factory {
7 address public feeTo;
8 address public feeToSetter;

Diese Zustandsvariablen sind notwendig, um die Protokollgebühr zu implementieren (siehe das 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.

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

Diese Variablen verfolgen die Paare, die Tauschbörsen zwischen zwei Token-Typen.

Die erste, getPair, ist ein Mapping, das einen Paar-Börsen-Vertrag basierend auf den beiden ERC-20-Token identifiziert, die er tauscht. ERC-20-Token werden durch die Adressen der Smart Contracts identifiziert, die sie implementieren, sodass die Schlüssel und der Wert alle Adressen sind. Um die Adresse der Paar-Börse zu erhalten, mit der 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-Börsen 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 Börsen diese Factory verwaltet.

Hinweis: Der Grund, warum Sie nicht über alle Schlüssel eines Mappings iterieren können, ist, dass die Datenspeicherung von Smart Contracts 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 Speicherplatz für eine Liste von Schlüsseln. In den meisten Anwendungen benötigen Sie das nicht.

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

Dieses Ereignis wird ausgelöst, wenn eine neue Paar-Börse erstellt wird. Es enthält die Adressen der Token, die Adresse der Paar-Börse und die Gesamtzahl der von der Factory verwalteten Börsen.

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

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

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

Diese Funktion gibt die Anzahl der Tauschpaare zurück.

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

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

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

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

1 require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
2 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 eine Börse gibt, besteht keine Notwendigkeit, eine weitere für dasselbe Paar zu erstellen.

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

Um einen neuen Smart Contract zu erstellen, benötigen wir den Code, der ihn erstellt (sowohl die Konstruktorfunktion als auch den Code, der den EVM-Bytecode des eigentlichen Smart Contracts 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).

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

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

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

Rufen Sie die Funktion initialize auf, um der neuen Börse mitzuteilen, welche beiden Token sie tauscht.

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

Speichern Sie die neuen Paar-Informationen in den Zustandsvariablen und lösen Sie ein Ereignis aus, um die Welt über die neue Paar-Börse zu informieren.

1 function setFeeTo(address _feeTo) external {
2 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
3 feeTo = _feeTo;
4 }
5
6 function setFeeToSetter(address _feeToSetter) external {
7 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
8 feeToSetter = _feeToSetter;
9 }
10}
Alle anzeigen

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

UniswapV2ERC20.sol

Dieser Smart Contract (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 kein 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 Off-Chain abzuheben, und sendet sie über das Internet an den Empfänger. Der Empfänger, der über ETH verfügt, reicht dann die Erlaubnis (Permit) im Namen des Eigentümers ein.

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

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.

1 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 des Replay-Angriffs (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 höher ist als die zuletzt verwendete, gehen wir davon aus, dass sie ungültig ist.

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

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.

1 DOMAIN_SEPARATOR = keccak256(
2 abi.encode(
3 keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
4 keccak256(bytes(name)),
5 keccak256(bytes('1')),
6 chainId,
7 address(this)
8 )
9 );
10 }
Alle anzeigen

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

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

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

Akzeptieren Sie keine Transaktionen nach Ablauf der Frist.

1 bytes32 digest = keccak256(
2 abi.encodePacked(
3 '\x19\x01',
4 DOMAIN_SEPARATOR,
5 keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
6 )
7 );

abi.encodePacked(...) ist die Nachricht, die wir erwarten. Wir wissen, wie die Nonce lauten sollte, daher ist es nicht nötig, sie als Parameter zu erhalten.

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

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

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

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

Wenn alles in Ordnung ist, behandeln Sie dies als ein ERC-20-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 dezentralisierten Anwendungen. 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, 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.

1pragma solidity =0.6.6;
2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
4import '@uniswap/lib/contracts/libraries/TransferHelper.sol';
5
6import './interfaces/IUniswapV2Router02.sol';
7import './libraries/UniswapV2Library.sol';
8import './libraries/SafeMath.sol';
9import './interfaces/IERC20.sol';
10import './interfaces/IWETH.sol';
Alle anzeigen

Den meisten davon sind wir entweder schon begegnet oder sie sind ziemlich offensichtlich. Die einzige Ausnahme ist IWETH.sol. Uniswap v2 ermöglicht den Austausch für jedes Paar von ERC-20-Token, aber Ether (ETH) selbst ist kein ERC-20-Token. Es ist älter als der Standard und wird durch einzigartige Mechanismen übertragen. Um die Verwendung von ETH in Verträgen zu ermöglichen, die für ERC-20-Token gelten, haben sich die Leute den Wrapped Ether (WETH) (opens in a new tab)-Vertrag ausgedacht. 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.

1contract UniswapV2Router02 is IUniswapV2Router02 {
2 using SafeMath for uint;
3
4 address public immutable override factory;
5 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 in der Lage wäre, sie so zu ändern, dass sie auf weniger ehrliche Verträge verweisen.

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

Dieser Modifikator stellt sicher, dass zeitlich begrenzte Transaktionen („mache X vor der Zeit Y, wenn du kannst“) nicht nach ihrem Zeitlimit stattfinden.

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

Der Konstruktor setzt lediglich die unveränderlichen Zustandsvariablen.

1 receive() external payable {
2 assert(msg.sender == WETH); // akzeptiert ETH nur über Fallback vom WETH-Vertrag
3 }

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 berechtigt.

Liquidität hinzufügen

Diese Funktionen fügen der Paar-Börse Token hinzu, was den Liquiditätspool vergrößert.

1
2 // **** LIQUIDITÄT HINZUFÜGEN ****
3 function _addLiquidity(

Diese Funktion wird verwendet, um die Menge der A- und B-Token zu berechnen, die in die Paar-Börse eingezahlt werden sollen.

1 address tokenA,
2 address tokenB,

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

1 uint amountADesired,
2 uint amountBDesired,

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

1 uint amountAMin,
2 uint amountBMin

Dies sind die akzeptablen Mindestbeträge für die Einzahlung. Wenn die Transaktion nicht mit diesen oder höheren Beträgen stattfinden kann, wird sie rückgängig gemacht. Wenn Sie diese Funktion nicht möchten, 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 bedeuten, dass Nachrichten 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 diesem Fall müssen Sie eine weitere Transaktion mit derselben Nonce und einem höheren Gaspreis einreichen, um sie zu überschreiben). Sie können nicht kontrollieren, was in der Zeit zwischen der Einreichung und der Aufnahme passiert.

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

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

Wenn es noch keine Börse für dieses Token-Paar gibt, erstellen Sie sie.

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

Rufen Sie die aktuellen Reserven im Paar ab.

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

Wenn die aktuellen Reserven leer sind, handelt es sich um eine neue Paar-Börse. Die einzuzahlenden Beträge sollten genau denjenigen entsprechen, die der Liquiditätsanbieter bereitstellen möchte.

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

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

1 if (amountBOptimal <= amountBDesired) {
2 require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
3 (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.

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

Wenn der optimale B-Betrag größer 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.

Wenn wir alles zusammenfassen, erhalten wir 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.

Diagramm

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.

1 function addLiquidity(
2 address tokenA,
3 address tokenB,
4 uint amountADesired,
5 uint amountBDesired,
6 uint amountAMin,
7 uint amountBMin,
8 address to,
9 uint deadline
Alle anzeigen

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, an 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

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

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 bei den Bibliotheken).

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

Übertragen Sie die korrekten Token-Beträge vom Benutzer in die Paar-Börse.

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

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

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

Wenn ein Liquiditätsanbieter Liquidität für eine Token/ETH-Paar-Börse bereitstellen möchte, gibt es einige Unterschiede. Der Vertrag übernimmt das Wrapping der ETH für den Liquiditätsanbieter. Es ist nicht erforderlich 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).

1 uint amountTokenMin,
2 uint amountETHMin,
3 address to,
4 uint deadline
5 ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
6 (amountToken, amountETH) = _addLiquidity(
7 token,
8 WETH,
9 amountTokenDesired,
10 msg.value,
11 amountTokenMin,
12 amountETHMin
13 );
14 address pair = UniswapV2Library.pairFor(factory, token, WETH);
15 TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
16 IWETH(WETH).deposit{value: amountETH}();
17 assert(IWETH(WETH).transfer(pair, amountETH));
Alle anzeigen

Um die ETH einzuzahlen, verpackt der Vertrag sie zunächst in WETH und überträgt die WETH dann in das Paar. Beachten Sie, dass die Übertragung in ein assert verpackt ist. Das bedeutet, dass bei einem Fehlschlagen der Übertragung auch dieser Vertragsaufruf fehlschlägt und das Wrapping daher nicht wirklich stattfindet.

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

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.

1 // **** LIQUIDITÄT ENTFERNEN ****
2 function removeLiquidity(
3 address tokenA,
4 address tokenB,
5 uint liquidity,
6 uint amountAMin,
7 uint amountBMin,
8 address to,
9 uint deadline
10 ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
Alle anzeigen

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.

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

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

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

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

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

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

1 function removeLiquidityETH(
2 address token,
3 uint liquidity,
4 uint amountTokenMin,
5 uint amountETHMin,
6 address to,
7 uint deadline
8 ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
9 (amountToken, amountETH) = removeLiquidity(
10 token,
11 WETH,
12 liquidity,
13 amountTokenMin,
14 amountETHMin,
15 address(this),
16 deadline
17 );
18 TransferHelper.safeTransfer(token, to, amountToken);
19 IWETH(WETH).withdraw(amountETH);
20 TransferHelper.safeTransferETH(to, amountETH);
21 }
Alle anzeigen

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.

1 function removeLiquidityWithPermit(
2 address tokenA,
3 address tokenB,
4 uint liquidity,
5 uint amountAMin,
6 uint amountBMin,
7 address to,
8 uint deadline,
9 bool approveMax, uint8 v, bytes32 r, bytes32 s
10 ) external virtual override returns (uint amountA, uint amountB) {
11 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
12 uint value = approveMax ? uint(-1) : liquidity;
13 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
14 (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
15 }
16
17
18 function removeLiquidityETHWithPermit(
19 address token,
20 uint liquidity,
21 uint amountTokenMin,
22 uint amountETHMin,
23 address to,
24 uint deadline,
25 bool approveMax, uint8 v, bytes32 r, bytes32 s
26 ) external virtual override returns (uint amountToken, uint amountETH) {
27 address pair = UniswapV2Library.pairFor(factory, token, WETH);
28 uint value = approveMax ? uint(-1) : liquidity;
29 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
30 (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);
31 }
Alle anzeigen

Diese Funktionen leiten Meta-Transaktionen weiter, um Benutzern ohne Ether zu ermöglichen, sich aus dem Pool zurückzuziehen, indem sie den Permit-Mechanismus verwenden.

1
2 // **** LIQUIDITÄT ENTFERNEN (unterstützt Fee-on-Transfer-Token) ****
3 function removeLiquidityETHSupportingFeeOnTransferTokens(
4 address token,
5 uint liquidity,
6 uint amountTokenMin,
7 uint amountETHMin,
8 address to,
9 uint deadline
10 ) public virtual override ensure(deadline) returns (uint amountETH) {
11 (, amountETH) = removeLiquidity(
12 token,
13 WETH,
14 liquidity,
15 amountTokenMin,
16 amountETHMin,
17 address(this),
18 deadline
19 );
20 TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
21 IWETH(WETH).withdraw(amountETH);
22 TransferHelper.safeTransferETH(to, amountETH);
23 }
24
Alle anzeigen

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

1
2
3 function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
4 address token,
5 uint liquidity,
6 uint amountTokenMin,
7 uint amountETHMin,
8 address to,
9 uint deadline,
10 bool approveMax, uint8 v, bytes32 r, bytes32 s
11 ) external virtual override returns (uint amountETH) {
12 address pair = UniswapV2Library.pairFor(factory, token, WETH);
13 uint value = approveMax ? uint(-1) : liquidity;
14 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
15 amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(
16 token, liquidity, amountTokenMin, amountETHMin, to, deadline
17 );
18 }
Alle anzeigen

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

Handeln

1 // **** SWAP ****
2 // setzt voraus, dass der anfängliche Betrag bereits an das erste Paar gesendet wurde
3 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.

1 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 eine Paar-Börse gäbe, wären das über 150 Milliarden Paar-Börsen. Die gesamte Chain hat im Moment nur 0,1 % dieser Anzahl an Konten (opens in a new tab). Stattdessen unterstützen die Swap-Funktionen das Konzept eines Pfades. Ein Händler kann A gegen B, B gegen C und C gegen D tauschen, sodass keine direkte A-D-Paar-Börse erforderlich ist.

Die Preise auf diesen Märkten neigen dazu, synchronisiert zu sein, denn wenn sie nicht synchron sind, entsteht eine Gelegenheit für Arbitrage. Stellen Sie sich zum Beispiel drei Token vor: A, B und C. Es gibt drei Paar-Börsen, eine 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-BörseB-C-BörseA-C-Börse
1A:1000 B:1050 A/B=1.05B:1000 C:1050 B/C=1.05A:1050 C:1000 C/A=1.05
2A:1024.695 B:1024.695 A/B=1B:1000 C:1050 B/C=1.05A:1050 C:1000 C/A=1.05
3A:1024.695 B:1024.695 A/B=1B:1024.695 C:1024.695 B/C=1A:1050 C:1000 C/A=1.05
4A:1024.695 B:1024.695 A/B=1B:1024.695 C:1024.695 B/C=1A:1024.695 C:1024.695 C/A=1
1 (address input, address output) = (path[i], path[i + 1]);
2 (address token0,) = UniswapV2Library.sortTokens(input, output);
3 uint amountOut = amounts[i + 1];

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

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

Rufen Sie die erwarteten Ausgabebeträge ab, sortiert so, wie die Paar-Börse sie erwartet.

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

Ist dies die letzte Börse? Wenn ja, senden Sie die für den Handel erhaltenen Token an das Ziel. Wenn nicht, senden Sie sie an die nächste Paar-Börse.

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

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

1 function swapExactTokensForTokens(

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

1 uint amountIn,
2 uint amountOutMin,
3 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 Paar-Börsen 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 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.

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

Rückgabewerte werden immer im Memory zurückgegeben.

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

Berechnen Sie den Betrag, der bei jedem Swap 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.

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

Übertragen Sie schließlich den anfänglichen ERC-20-Token auf das Konto für die erste Paar-Börse und rufen Sie _swap auf. Dies geschieht alles in derselben Transaktion, sodass die Paar-Börse weiß, dass alle unerwarteten Token Teil dieser Übertragung sind.

1 function swapTokensForExactTokens(
2 uint amountOut,
3 uint amountInMax,
4 address[] calldata path,
5 address to,
6 uint deadline
7 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
8 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
9 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
10 TransferHelper.safeTransferFrom(
11 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
12 );
13 _swap(amounts, path, to);
14 }
Alle anzeigen

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 Swap 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 eine Freigabe (Allowance) erteilen, damit dieser sie übertragen darf.

1 function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
2 external
3 virtual
4 override
5 payable
6 ensure(deadline)
7 returns (uint[] memory amounts)
8 {
9 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
10 amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
11 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
12 IWETH(WETH).deposit{value: amounts[0]}();
13 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
14 _swap(amounts, path, to);
15 }
16
17
18 function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
19 external
20 virtual
21 override
22 ensure(deadline)
23 returns (uint[] memory amounts)
24 {
25 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
26 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
27 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
28 TransferHelper.safeTransferFrom(
29 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
30 );
31 _swap(amounts, path, address(this));
32 IWETH(WETH).withdraw(amounts[amounts.length - 1]);
33 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
34 }
35
36
37
38 function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
39 external
40 virtual
41 override
42 ensure(deadline)
43 returns (uint[] memory amounts)
44 {
45 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
46 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
47 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
48 TransferHelper.safeTransferFrom(
49 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
50 );
51 _swap(amounts, path, address(this));
52 IWETH(WETH).withdraw(amounts[amounts.length - 1]);
53 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
54 }
55
56
57 function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
58 external
59 virtual
60 override
61 payable
62 ensure(deadline)
63 returns (uint[] memory amounts)
64 {
65 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
66 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
67 require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
68 IWETH(WETH).deposit{value: amounts[0]}();
69 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
70 _swap(amounts, path, to);
71 // erstattet Dust-ETH, falls vorhanden
72 if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
73 }
Alle anzeigen

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 von der letzten Börse im Pfad und verbrennen sie, wobei wir dem Händler die resultierenden ETH zurücksenden.

1 // **** SWAP (unterstützt Fee-on-Transfer-Token) ****
2 // setzt voraus, dass der anfängliche Betrag bereits an das erste Paar gesendet wurde
3 function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {

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

1 for (uint i; i < path.length - 1; i++) {
2 (address input, address output) = (path[i], path[i + 1]);
3 (address token0,) = UniswapV2Library.sortTokens(input, output);
4 IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
5 uint amountInput;
6 uint amountOutput;
7 { // Gültigkeitsbereich, um 'Stack too deep'-Fehler zu vermeiden
8 (uint reserve0, uint reserve1,) = pair.getReserves();
9 (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
10 amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
11 amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
Alle anzeigen

Aufgrund der Übertragungsgebühren können wir uns nicht auf die Funktion getAmountsOut verlassen, um uns mitzuteilen, wie viel wir aus jeder Übertragung herausbekommen (so wie wir es vor dem Aufruf des ursprünglichen _swap tun). Stattdessen müssen wir zuerst übertragen 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 die Übertragung am Ende rückgängig gemacht wird, weil am Ende nicht genug vorhanden ist, um das erforderliche Minimum zu erreichen) würde das am Ende mehr Gas kosten. Token mit Übertragungsgebühren sind ziemlich selten. Obwohl wir sie also berücksichtigen müssen, besteht keine Notwendigkeit, bei allen Swaps davon auszugehen, dass sie mindestens einen davon durchlaufen.

1 }
2 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));
3 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
4 pair.swap(amount0Out, amount1Out, to, new bytes(0));
5 }
6 }
7
8
9 function swapExactTokensForTokensSupportingFeeOnTransferTokens(
10 uint amountIn,
11 uint amountOutMin,
12 address[] calldata path,
13 address to,
14 uint deadline
15 ) external virtual override ensure(deadline) {
16 TransferHelper.safeTransferFrom(
17 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
18 );
19 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
20 _swapSupportingFeeOnTransferTokens(path, to);
21 require(
22 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
23 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
24 );
25 }
26
27
28 function swapExactETHForTokensSupportingFeeOnTransferTokens(
29 uint amountOutMin,
30 address[] calldata path,
31 address to,
32 uint deadline
33 )
34 external
35 virtual
36 override
37 payable
38 ensure(deadline)
39 {
40 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
41 uint amountIn = msg.value;
42 IWETH(WETH).deposit{value: amountIn}();
43 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));
44 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
45 _swapSupportingFeeOnTransferTokens(path, to);
46 require(
47 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
48 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
49 );
50 }
51
52
53 function swapExactTokensForETHSupportingFeeOnTransferTokens(
54 uint amountIn,
55 uint amountOutMin,
56 address[] calldata path,
57 address to,
58 uint deadline
59 )
60 external
61 virtual
62 override
63 ensure(deadline)
64 {
65 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
66 TransferHelper.safeTransferFrom(
67 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
68 );
69 _swapSupportingFeeOnTransferTokens(path, address(this));
70 uint amountOut = IERC20(WETH).balanceOf(address(this));
71 require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
72 IWETH(WETH).withdraw(amountOut);
73 TransferHelper.safeTransferETH(to, amountOut);
74 }
Alle anzeigen

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

1 // **** BIBLIOTHEKSFUNKTIONEN ****
2 function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {
3 return UniswapV2Library.quote(amountA, reserveA, reserveB);
4 }
5
6 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)
7 public
8 pure
9 virtual
10 override
11 returns (uint amountOut)
12 {
13 return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
14 }
15
16 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)
17 public
18 pure
19 virtual
20 override
21 returns (uint amountIn)
22 {
23 return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);
24 }
25
26 function getAmountsOut(uint amountIn, address[] memory path)
27 public
28 view
29 virtual
30 override
31 returns (uint[] memory amounts)
32 {
33 return UniswapV2Library.getAmountsOut(factory, amountIn, path);
34 }
35
36 function getAmountsIn(uint amountOut, address[] memory path)
37 public
38 view
39 virtual
40 override
41 returns (uint[] memory amounts)
42 {
43 return UniswapV2Library.getAmountsIn(factory, amountOut, path);
44 }
45}
Alle anzeigen

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

UniswapV2Migrator.sol

Dieser Vertrag wurde verwendet, um Börsen 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 weiter erläutert 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.

1pragma solidity =0.5.16;
2
3// eine Bibliothek zur Durchführung verschiedener mathematischer Operationen
4
5library Math {
6 function min(uint x, uint y) internal pure returns (uint z) {
7 z = x < y ? x : y;
8 }
9
10 // babylonisches Verfahren (https://wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
11 function sqrt(uint y) internal pure returns (uint z) {
12 if (y > 3) {
13 z = y;
14 uint x = y / 2 + 1;
Alle anzeigen

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

1 while (x < z) {
2 z = x;
3 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).

1 }
2 } else if (y != 0) {
3 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).

1 }
2 }
3}

Festkommabrüche (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.

1pragma solidity =0.5.16;
2
3// eine Bibliothek zur Handhabung binärer Festkommazahlen (https://wikipedia.org/wiki/Q_(number_format))
4
5// Bereich: [0, 2**112 - 1]
6// Auflösung: 1 / 2**112
7
8library UQ112x112 {
9 uint224 constant Q112 = 2**112;
Alle anzeigen

Q112 ist die Kodierung für eins.

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

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

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

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

1pragma solidity >=0.5.0;
2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
4
5import "./SafeMath.sol";
6
7library UniswapV2Library {
8 using SafeMath for uint;
9
10 // gibt sortierte Token-Adressen zurück, wird verwendet, um Rückgabewerte von in dieser Reihenfolge sortierten Paaren zu verarbeiten
11 function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
12 require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
13 (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
14 require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
15 }
Alle anzeigen

Sortieren Sie die beiden Token nach Adresse, damit wir die Adresse der Paar-Börse 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 Börsen statt einer führen würde.

1 // berechnet die CREATE2-Adresse für ein Paar, ohne externe Aufrufe zu tätigen
2 function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
3 (address token0, address token1) = sortTokens(tokenA, tokenB);
4 pair = address(uint(keccak256(abi.encodePacked(
5 hex'ff',
6 factory,
7 keccak256(abi.encodePacked(token0, token1)),
8 hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // Init-Code-Hash
9 ))));
10 }
Alle anzeigen

Diese Funktion berechnet die Adresse der Paar-Börse 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

1 // ruft die Reserven für ein Paar ab und sortiert sie
2 function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
3 (address token0,) = sortTokens(tokenA, tokenB);
4 (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
5 (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
6 }

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

1 // gibt bei einer bestimmten Menge eines Vermögenswerts und Paar-Reserven eine äquivalente Menge des anderen Vermögenswerts zurück
2 function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
3 require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
4 require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
5 amountB = amountA.mul(reserveB) / reserveA;
6 }

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

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

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

1
2 require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
3 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
4 uint amountInWithFee = amountIn.mul(997);
5 uint numerator = amountInWithFee.mul(reserveOut);
6 uint denominator = reserveIn.mul(1000).add(amountInWithFee);
7 amountOut = numerator / denominator;
8 }

Solidity 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.

1 // gibt bei einer Ausgabemenge eines Vermögenswerts und Paar-Reserven eine erforderliche Eingabemenge des anderen Vermögenswerts zurück
2 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
3 require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
4 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
5 uint numerator = reserveIn.mul(amountOut).mul(1000);
6 uint denominator = reserveOut.sub(amountOut).mul(997);
7 amountIn = (numerator / denominator).add(1);
8 }

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

1
2 // führt verkettete getAmountOut-Berechnungen für eine beliebige Anzahl von Paaren durch
3 function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
4 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
5 amounts = new uint[](path.length);
6 amounts[0] = amountIn;
7 for (uint i; i < path.length - 1; i++) {
8 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
9 amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
10 }
11 }
12
13 // führt verkettete getAmountIn-Berechnungen für eine beliebige Anzahl von Paaren durch
14 function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
15 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
16 amounts = new uint[](path.length);
17 amounts[amounts.length - 1] = amountOut;
18 for (uint i = path.length - 1; i > 0; i--) {
19 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
20 amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
21 }
22 }
23}
Alle anzeigen

Diese beiden Funktionen übernehmen die Identifizierung der Werte, wenn es notwendig ist, mehrere Paar-Börsen zu durchlaufen.

Transfer Helper

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

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3pragma solidity >=0.6.0;
4
5// Hilfsmethoden für die Interaktion mit ERC20-Token und das Senden von ETH, die nicht konsistent true/false zurückgeben
6library TransferHelper {
7 function safeApprove(
8 address token,
9 address to,
10 uint256 value
11 ) internal {
12 // bytes4(keccak256(bytes('approve(address,uint256)')));
13 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));
14
Alle anzeigen

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

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

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 revertiert (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).

1
2
3 function safeTransfer(
4 address token,
5 address to,
6 uint256 value
7 ) internal {
8 // bytes4(keccak256(bytes('transfer(address,uint256)')));
9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
10 require(
11 success && (data.length == 0 || abi.decode(data, (bool))),
12 'TransferHelper::safeTransfer: transfer failed'
13 );
14 }
Alle anzeigen

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

1
2 function safeTransferFrom(
3 address token,
4 address from,
5 address to,
6 uint256 value
7 ) internal {
8 // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
10 require(
11 success && (data.length == 0 || abi.decode(data, (bool))),
12 'TransferHelper::transferFrom: transferFrom failed'
13 );
14 }
Alle anzeigen

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

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

Diese Funktion überträgt Ether auf 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 von etwa 50 Seiten. Wenn du es bis hierher geschafft hast, herzlichen Glückwunsch! Hoffentlich hast du nun die Besonderheiten bei der Entwicklung einer realen Anwendung (im Gegensatz zu kurzen Beispielprogrammen) verstanden und bist besser darauf vorbereitet, Smart Contracts für deine eigenen Anwendungsfälle zu schreiben.

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

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

Letzte Aktualisierung der Seite: 25. Februar 2026

War dieses Tutorial hilfreich?