Aufbau eines Arbitrage-Bots: Automated Market Maker und Uniswap V2
September 28th, 2023

Künstlerische Darstellung der konstanten Produktformel x*y = k

Dieser Artikel ist Teil einer Serie zur MEV-Bot-Entwicklung. Ziel dieser Serie ist es, eine Schritt-für-Schritt-Anleitung für die Entwicklung eines Arbitrage-Bots bereitzustellen. In diesem Artikel werden die Grundlagen von Automated Market Makers (AMMs), insbesondere Uniswap V2, erläutert und wie man mit ihnen mithilfe von Python und Web3py interagiert.

Es wird auch Soliditätscode bereitgestellt, um Swaps in der Kette durchzuführen. In einem letzten Abschnitt wird erläutert, wie Flash-Swaps mit Uniswap V2 durchgeführt werden

Allgemeine Einführung in AMMs

Was ist ein AMM?

In DeFi ist ein Automated Market Maker (AMM) ein intelligenter Vertrag, der Liquidität in einem Pool hält und es Benutzern ermöglicht, zwischen zwei Vermögenswerten zu tauschen.

Jeder Benutzer kann Liquidität finanzieren und dafür einen Anteil an den Gebühren erhalten, die bei jedem Swap erhoben werden. Die Preisbewegung, die den Nutzern, die einen Tausch durchführen möchten, angeboten wird, wird durch eine mathematische Formel bestimmt, die eine Vorhersagbarkeit ermöglicht. Diese Formel wird manchmal als Bindungskurve des AMM bezeichnet.

Anwendungsfälle

AMMs sind eine Schlüsselkomponente des DeFi-Ökosystems. Sie sind in der Lage, Liquidität für jedes Vermögenswertpaar bereitzustellen, ohne dass eine zentralisierte Einheit einen Preis-Feed bereitstellen muss. Benutzer können ohne Erlaubnis mit dem Protokoll interagieren und zwischen zwei beliebigen Assets wechseln.

Ersteller neuer Token können AMMs auch nutzen, um die Liquidität für ihre Token zu steigern, indem sie selbst Liquidität bereitstellen, auch auf erlaubnislose Weise.

Dies ist eine der wenigen Möglichkeiten, in DeFi nachhaltige Erträge zu erzielen, da Gebühren erhoben werden, solange Benutzer mit dem Protokoll interagieren.

Das erste Protokoll zur Implementierung von AMMs war Bancor im Jahr 2017. Seitdem haben viele weitere Protokolle ähnliche AMMs implementiert, wie Uniswap, Curve, Balancer, Sushiswap usw. Uniswap ist jedoch das beliebteste und das Protokoll, auf das wir uns konzentrieren werden In diesem Artikel.

Uniswap

Bis heute hat Uniswap drei Versionen seines Protokolls veröffentlicht. V3 ist die neueste Version, mit einem etwas komplexeren System als V2, aber mehr Volumen.

Trotz seines veralteten Status verfügt Uniswap V2 immer noch über ein beträchtliches Volumen, da es immer noch über viel Liquidität verfügt. Die Interaktion ist sehr einfach, und ihr Code wurde von Hunderten anderen Protokollen gespalten, die fast immer dieselbe Schnittstelle verwenden. Dies macht es zu einem guten Ausgangspunkt für unseren Arbitrage-Bot.

Uniswap V2 vereinfachte die Logik der Paarverträge erheblich, indem es den Handel mit zwei beliebigen Paaren von ERC20-Tokens statt nur mit ETH- und ERC20-Tokens ermöglichte.

Wenn jemand Liquidität zwischen einem $RANDOM-Token und Eth finanzieren möchte, kann er dies einfach tun, indem er das Paar zwischen $RANDOM und $WETH erstellt.

WETH (Wrapped ETH) ist ein ERC20-Token, der 1:1 an ETH gekoppelt ist. Es handelt sich um einen intelligenten Vertrag, der native Ether sperrt und eine entsprechende Menge WETH ausgibt. WETH kann jederzeit wieder in ETH umgewandelt werden, indem es an den Smart Contract zurückgesendet wird. Die Vorgänge des Ein- und Auspackens von Ether werden oft als Einzahlen bzw. Abheben bezeichnet.

Den WETH-Vertrag finden Sie hier .

Auf der Registerkarte „Vertrag / Vertrag schreiben“ können Sie die Einzahlungs- und Auszahlungsfunktionen sehen , mit denen Sie Ether ein- bzw. auspacken können (Beachten Sie, dass deposit()als Argument eine Dezimalzahl von Ether verwendet wird. „draw()“ benötigt jedoch eine ganze Zahl von Wei. 1 Ether ist 10^18 Wei).

Ein- und Auspacken von WETH auf etherscan.io

Uniswap V2 AMM

Uniswap V2 ist in einem Fabrikvertrag enthalten , der es jedem ermöglicht, ein neues Handelspaar für zwei beliebige ERC20-Token zu erstellen.

Dadurch wird ein neuer Smart Contract eingesetzt, der die Liquidität für dieses Paar hält. Dieser Vertrag wird Paarvertrag genannt. (Sehen Sie sich die Transaktion zur Erstellung des WETH/USDC-Paares an ).

Liquiditätsanbieter (LPs) können den Paarvertrag dann mit einem beliebigen Betrag beider Token finanzieren und erhalten einen Anteil der bei jedem Swap erhobenen Gebühren.

Bei der Bereitstellung von Liquidität muss der LP einen gleichen Wert beider Token entsprechend dem aktuellen Preis des Paares bereitstellen. Diese wiederum erhalten einen Anteil am Liquiditätspool, der durch einen Token namens LP-Token repräsentiert wird. Dieser Token kann jederzeit gegen die zugrunde liegenden Vermögenswerte eingetauscht werden, und die Höhe der erhaltenen Vermögenswerte ist proportional zum Anteil des Pools, der dem LP gehört.

Benutzer können zwischen den beiden Token wechseln, indem sie einen der beiden Token an den Paarvertrag senden und im Gegenzug den anderen erhalten. Das Protokoll erhebt eine Gebühr als Prozentsatz der Menge des gesendeten Eingabe-Tokens und fügt sie dem Liquiditätspool hinzu. Bei Uniswap V2 beträgt diese Gebühr 0,3 %. Bei Uniswap V3 gibt es mehrere Gebührenstufen, die zwischen 0,05 % und 1 % liegen. Das Uniswap-Team hat sich die Möglichkeit gelassen, den „Protokollgebührenschalter“ umzuschalten. Es handelt sich um eine zusätzliche Gebühr, die bei jedem Tausch erhoben wird. Der Betrag ist im Smart Contract auf einen angemessenen Wert begrenzt.

Normale Benutzer interagieren oft nicht direkt mit dem Pair-Vertrag, sondern mit einem Router-Vertrag, der den Austausch in ihrem Namen durchführt. Der Router versucht, den besten Preis für den Benutzer zu erzielen, indem er den Swap bei Bedarf auf mehrere Paare aufteilt oder sogar austauscht.

Um den unnötigen Aufwand durch den Router zu vermeiden, interagieren Bots fast immer direkt mit dem Paarvertrag, da sie genau wissen, auf welches Paar sie tauschen möchten. Komplexere Arbitrage-Strategien führen häufig dazu, dass eine Art Routing implementiert wird, wie wir in einem späteren Artikel sehen werden.

Geldproduktformel

Beschreibung

Um zu bestimmen, wie viel vom Ausgabe-Token eine bestimmte Menge an Eingabe-Token während eines Tauschs zurückgibt, implementieren AMMs häufig eine konstante Produktformel oder Variationen davon. Diese Formel ist sehr einfach und ermöglicht eine vorhersehbare und vollständig dezentralisierte Preisentwicklung. Die Formel muss während eines Austauschs gültig bleiben, wenn sich einige der Variablen ändern.

Die konstante Produktformel lautet:

x * y = k ( 0 )

Dabei sind x und y die Menge der Reserven jedes Tokens im Pool und k eine Konstante. Diese Konstante ändert sich, wenn LPs dem Pool Liquidität hinzufügen oder daraus entfernen. Während einer Swap-Operation darf sich dieses k nie ändern. Wenn wir also wissen, um wie viel sich eine der Reserven ändert, können wir bestimmen, um wie viel sich die andere ändert, um k konstant zu halten.

Ergebnisse der Formel

Wenn wir beim Austausch eines x-Tokens gegen ein y-Token dx als Addition zur Reserve x und dy als resultierende Änderung der Reserve y bezeichnen, können wir Folgendes schreiben:

(x + dx) * (y + dy) = k

Wir können dann nach dy auflösen:

y + dy = k / (x + dx) 
dy = k/(x + dx) - y 
dy = y * (x/(x + dx) - 1) (1)

Hätte der Benutzer stattdessen ein y-Token gegen ein x-Token ausgetauscht, hätten wir den folgenden Ausdruck für dx erhalten:

dx = x * (y/(y + dy) - 1) (2)

Beachten Sie, dass dx und dy die Änderungen der Reserven sind, nicht unbedingt die Swap-Ausgabe. Bei einem Tausch sind diese Werte negativ, da die Produktionsreserven sinken.

Wir können auch grafisch anzeigen, was passiert, wenn ein Tausch stattfindet, indem wir die Funktion grafisch darstellen y(x) = k/x. Die Steigung der Funktion an jedem Punkt ist gleich dem Preis des Paares an diesem Punkt.

Darstellung der konstanten Produktformel. Am ausgewählten Punkt zeigt der Pool mit k=1 einen Preis von 4/0,25 = 16. Swaps lassen den Punkt entlang der Kurve gleiten

Um die Mathematik besser zu verstehen, werden wir einige Tauschvorgänge simulieren. Denken Sie daran, dass wir in diesem Artikel die vom Protokoll erhobene Gebühr größtenteils nicht berücksichtigen.

Wir werden eine Formel (1)mit x = 2 WETH, y = 2000 USDC und k = 2*2000 WETHUSDC verwenden. Wir simulieren einen Swap von 0,1 WETH gegen USDC. (Beachten Sie, dass wir die Einheitenumrechnungen ignorieren. 1 WETH = 10^18; 1 USDC = 10^6)

dy = 2000 * (2/(2 + 0,1) - 1) 
dy = 2000 * (2/2,1 - 1) 
dy = 2000 * (-1/21) 
dy = -95,24

Der Tausch führt dazu, dass der Benutzer 95,24 USDC im Austausch für 0,1 WETH erhält. Dem Pool verbleiben Reserven von 2,1 WETH und 1904,76 USDC.

Beachten Sie, dass der Preis vor der Ausführung 2000/2 = 1000 USDC/WETH betrug. Der Preis nach der Ausführung beträgt 1904,76/2,1 = 907,98 USDC/WETH. Der Preis wurde durch den Handel verschoben und der Benutzer hat einen schlechteren Preis erhalten als den, der auf der Benutzeroberfläche angezeigt wird. Dies wird als Preiseffekt bezeichnet.

Der durchschnittliche Ausführungspreis des Swaps beträgt p = Output/Input = 95,24/0,1 = 952,4 USDC/WETH.

Wenn wir gleich danach erneut 0,1 WETH gegen USDC tauschen, erhalten wir Folgendes:

dy = 1904,76 * (2,1/(2,1 + 0,1) - 1) 
dy = -86,58

Diesmal haben wir 86,58 USDC statt 95,24 USDC erhalten.

Der durchschnittliche Ausführungspreis betrug p = 86,58/0,1 = 865,8 USDC/WETH.

Wir können beobachten, dass sich der Preis des Paares während eines Swaps nicht linear ändert.

Der Preis bewegt sich immer in die Richtung, die den Tausch in die gleiche Richtung teurer macht. Die Eingabe einer großen Menge an Token führt zu einem schlechteren Preis als die Eingabe einer kleinen Menge an Token.

Die Aufteilung des Swaps in mehrere kleinere aufeinanderfolgende Swaps führt nicht zu einem besseren Gesamtpreis. Wenn wir 0,2 WETH gegen USDC getauscht hätten, hätten wir Folgendes erhalten:

dy = 2000 * ( 2 /( 2 + 0,2 ) - 1 ) 
dy = - 181,82

Wir haben 181,82 USDC erhalten. Zuvor haben wir 95,24 + 86,58 = 181,82 USDC erhalten, indem wir den Swap in zwei kleinere Swaps aufgeteilt haben, das gleiche Ergebnis.

Konstantprodukt-AMMs sollen auf diese Weise Pfadunabhängigkeit aufweisen.

Die Aufteilung eines Swaps in mehrere kleinere Swaps, die durch andere Swaps getrennt sind, führt definitiv zu einem besseren Preis, da wir davon ausgehen können, dass Arbitrageure den Preis auf einen Wert nahe dem Startpreis zurücksetzen. Der zu tauschende Betrag muss groß genug sein, damit sich die zusätzlichen Benzinkosten lohnen.

Eine letzte Bemerkung ist, dass immer dann, wenn sich der Preis eines der Token in einem Pool im Vergleich zum anderen stark ändert (was bei Kryptowährungen sehr häufig vorkommt), der Pool nach und nach von den wertvollsten Token erschöpft wird, was auch für die Benutzer der Fall ist Sie möchten es durch Tausch mit dem Token mit niedrigerem Wert erwerben. Wenn LPs ihre Liquidität abziehen (ohne Berücksichtigung aufgelaufener Gebühren), besteht der Wert der Token, die sie erhalten, tendenziell eher aus dem Token mit geringerem Wert, was zu einem Kapitalverlust führt. Dies wird als vergänglicher Verlust bezeichnet .

Wert der Position abhängig von der relativen Preisdivergenz der Token

Eine kurze Ableitung der Formel für vergänglichen Verlust finden Sie in diesem Medium-Artikel von Peteris Erins.

Aus diesem Grund haben Liquiditätsanbieter einen Anreiz, Liquidität für Paare bereitzustellen, von denen sie glauben, dass sie keine zu große Preisdivergenz aufweisen. Bitte seien Sie davon überzeugt, dass „vorübergehender Verlust“ eine Fehlbezeichnung ist und tatsächlich genauso dauerhaft ist wie jeder andere finanzielle Verlust.

Den Preis eines Pools lesen

Da wir nun wissen, dass der Preis eines Paares (oder austauschbar eines Pools) dem Wert der Reserven eines Tokens geteilt durch den anderen entspricht, müssen wir diese Werte nur noch aus dem Paarvertrag abrufen.

V2-Verträge stellen die Funktion bereit getReserves(), die die Reserven der beiden Token im Paar zurückgibt (zusammen mit dem Zeitstempel der letzten Änderung der Reserven, den wir jedoch nicht verwenden).

Wir werden Python und web3.py verwenden, um die Reserven eines Paares im Ethereum-Mainnet zu lesen. Beachten Sie, dass Sie zum Ausführen dieses Codes einen Knoten-RPC-Endpunkt benötigen. Infura, Alchemy und QuickNode bieten alle kostenlose Endpunkte.

Um einfach mit bereitgestelltem Code interagieren zu können, nutzen intelligente Vertragstools/-bibliotheken ABIs (Application Binary Interfaces). Dabei handelt es sich um JSON-Dateien, die die Funktionen und Ereignisse eines Smart Contracts beschreiben. Diese ABI werden vom Solidity-Compiler generiert, um die Interoperabilität zu erleichtern. Fortgeschrittene Benutzer können ohne ABIs interagieren, aber das macht den Prozess etwas mühsamer.

Eine einfache Möglichkeit, den ABI eines verifizierten Vertrags zu erhalten, ist die Verwendung von Etherscan:

ABI des UniswapV2Pair-Vertrags

Den ABI des UniswapV2Pair-Vertrags für das WETH-USDT-Paar finden Sie im Abschnitt Vertrags-ABI .

Der folgende Codeteil gibt die beiden Reserven aus:

from web3 import Web3 
import json 
# Mit einem Knoten verbinden. Wenn Sie Infura verwenden, sollte es so aussehen:
 w3 = Web3(Web3.HTTPProvider( 'https://mainnet.infura.io/v3/<YOUR_INFURA_PROJECT_ID>' )) 
# Laden Sie die ABI des UniswapV2Pair-Vertrags 
mit  open ( 'UniswapV2Pair .json' , 'r' ) as f: 
    pairABI = json.load(f) 
# Erstellen Sie ein Vertragsobjekt mit der Paaradresse. 
pair = w3.eth.contract(address= '0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852' , abi=pairABI) 
# Rufen Sie die Funktion getReserves() aufreserves
 =pair.functions.getReserves().call()
# Drucken Sie den Reservedruck 
aus (Reserven)

Wir erhalten die folgende Ausgabe:

[16213967662142758453773, 30340153518332, 1685519663]

Die ersten beiden Werte sind die Reserven von WETH bzw. USDT, und der letzte Wert ist der Zeitstempel der letzten Änderung der Reserven. Denken Sie daran, dass Smart Contracts bei der Token-Buchhaltung immer in Wei und nicht in den üblichen Einheiten argumentieren .

Um diesen Wert für jeden Token in die übliche Einheit umzuwandeln, müssen wir ihn durch 10^Dezimalstellen dividieren. Für WETH sind Dezimalstellen = 18, also müssen wir durch 1⁰¹⁸ dividieren. Für USDT sind Dezimalstellen = 6, also müssen wir durch 1⁰⁶ dividieren. In üblichen Einheiten betragen die Reserven:

WETH: 16213967662142758453773 / 10^18 = 16.213,97 $WETH
 USDT: 30340153518332 / 10^6 = 30.340.153,52 $USDT

Beachten Sie, dass, da der Smart Contract nur diese großen Ganzzahlen verwendet, keine Notwendigkeit besteht, Gleitkommazahlen zu verwenden, außer zu Anzeigezwecken.

Sie können das Dezimalstellenattribut jedes ERC20-Tokens abrufen, indem Sie die decimals()Funktion des Token-Vertrags aufrufen:

# [...] 
# Laden Sie die ABI des ERC20-Vertrags 
mit  open ( 'ERC20ABI.json' , 'r' ) as f: 
    ercABI = json.load(f) 
# Erstellen Sie ein Vertragsobjekt mit der Token-Adresse. Hier haben wir USDT verwendet. 
token = w3.eth.contract(address= '0xdAC17F958D2ee523a2206206994597C13D831ec7' , abi=ercABI) 
# Rufen Sie die Funktion decimals() auf decimals
 = token.functions.decimals().call() 
# Drucken Sie die Dezimalstellen 
aus print (decimals)

Wir erhalten die folgende Ausgabe:

6

Wie erwartet beträgt die Dezimalstelle von USDT 6.

Eine letzte Information, die Sie sich merken sollten, ist, welches Token token0 und token1 ist und über Reserve0 bzw. Reserve1 verfügt.

Wenn wir uns den Erstellungscode für das Fabrikvertragspaar ansehen , können wir Folgendes sehen:

Funktion  createPair ( Adress-TokenA, Adress-TokenB ) externe Rückgabe (Adresspaar) { 
    require (tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES' ); 
    (Adress-Token0, Adress-Token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); 
    // [...]
 }

Wir können sehen, dass das Token mit der niedrigsten/kleinsten Adresse Token0 und das andere Token1 ist. In unserem Fall hat WETH die niedrigste Adresse, also Token0, und USDT ist Token1.

Einen Tausch durchführen

Der Kern eines Arbitrage-Bots besteht darin, Swaps der richtigen Größe zwischen den richtigen Token-Pools durchzuführen.

Wir werden sehen, wie man einen Swap auf Uniswap V2 aus einem Vertrag durchführt, der mit Remix , der webbasierten Referenz-IDE von Solidity, bereitgestellt wird. Die Transaktion wird über Anvil , den lokalen Testnetzknoten von Foundry , an einen lokalen Zweig des Ethereum-Hauptnetzes gesendet .

Für die Interaktion mit dem Knoten wird Python-Code verwendet.

Tauschen Sie Solidität aus

Bevor Sie mit einem Smart Contract interagieren, ist es immer eine gute Idee, einen Blick auf den Code des Vertrags zu werfen. Wenn wir uns den von Github gehosteten Code des UniswapV2Pair-Vertrags ansehen , können wir die folgende Funktion sehen:

// Diese Low-Level-Funktion sollte von einem Vertrag aufgerufen werden, der wichtige Sicherheitsüberprüfungen durchführt. 
Funktionsswap  ( uint amount0Out, uint amount1Out, Adresse an , Bytes calldata data ) external lock { 
    require (amount0Out > 0 || amount1Out > 0 , 'UniswapV2 : INSUFFICIENT_OUTPUT_AMOUNT' ); 
    (uint112 _reserve0, uint112 _reserve1,) = getReserves (); // Gaseinsparungen 
    erfordern (amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY' ); 
    
    uint balance0; 
    uint balance1; 
    {// Gültigkeitsbereich für _token{0,1}, vermeidet Stack-to-Deep-Fehler.
     Adresse _token0 = token0; 
    Adresse _token1 = token1; 
    require (to != _token0 && to != _token1, 'UniswapV2: INVALID_TO' ); 
    if (amount0Out > 0 ) _safeTransfer (_token0, to, amount0Out); // Token optimistisch übertragen, 
    wenn (amount1Out > 0 ) _safeTransfer (_token1, to, amount1Out); // Token optimistisch übertragen, 
    wenn (data. length > 0 ) IUniswapV 2Callee(to). uniswapV2Call (Nachricht Absender, Betrag0Out, Betrag1Out, Daten); 
    balance0 = IERC20 (_token0). balanceOf ( Adresse ( this )); 
    balance1 = IERC20 (_token1). balanceOf ( Adresse ( this )); 
    } 
    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'); 
    { // Spielraum für Reserve{0,1}Angepasst, vermeidet Stack-to-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' );
    } 
    _update (balance0, balance1, _reserve0, _reserve1); 
    emit Swap (msg. sender , amount0In, amount1In, amount0Out, amount1Out, to); 
}

Ohne zu sehr auf die Details einzugehen, können wir sehen, dass die Funktion vier Argumente benötigt:

  • amount0Out: die Menge an Token0, die vom Pool an den Empfänger gesendet wird to(wie immer in Wei)

  • amount1Out: die Menge an Token1, die vom Pool an den Empfänger gesendet wirdto

  • to: die Adresse des Empfängers der Ausgabe-Tokens des Swaps

  • data: optionaler Parameter (zum Ignorieren übergeben Sie ein leeres Byte-Array). Wird für Flash-Swaps verwendet, die wir später in diesem Artikel sehen werden.

Beachten Sie, dass es keine amount0Inund- amount1InParameter gibt. Eingabewerte sollten vom Aufrufer berechnet werden. Der Pool stellt lediglich sicher, dass die konstante Produktformel eingehalten wird (eigentlich ist der Tausch möglich, wenn die resultierende Liquidität kim Pool größer oder gleich als zuvor ist).

Die Eingabetoken müssen vor dem Aufruf der swap()Funktion an den Pool gesendet werden. Der Pool sendet dann die Ausgabe-Tokens an den Empfänger to, noch bevor er überprüft hat, ob genügend Eingabe-Tokens gesendet wurden. Dies dient dazu, Flash-Swaps zu ermöglichen.

Ein naiver Entwickler könnte versuchen, zuerst die Eingabetokens in einer Transaktion zu senden und dann die swap()Funktion in einer anderen Transaktion aufzurufen. Dies würde jedoch mit ziemlicher Sicherheit zu einem Verlust der Mittel führen, da es keine Garantie dafür gäbe, dass kein externer Akteur die Funktion aufruft, swap()bevor die zweite Transaktion abgebaut wird. Die swap()Funktion muss in derselben Transaktion aufgerufen werden wie die, in der die Eingabe-Tokens an den Pool gesendet werden.

Der folgende Smart-Vertrag muss aufgerufen werden, wobei die Eingabe Eth als aufrufender Transaktionswert weitergeleitet wird. Wenn die startSwap()Funktion mit dem vorberechneten Ausgabebetrag aufgerufen wird , verpackt sie die in der Transaktion erhaltene ETH in WETH, sendet sie an den Pool und ruft die swap()Funktion mit dem richtigen Ausgabebetrag auf. Der Pool sendet dann die ausgegebenen USDT-Token an das Konto, das die Transaktion gesendet hat.

// SPDX-License-Identifier: MIT
 Pragma Solidity ^ 0.8 .0 ; // Jede Solidity 0.8.x-Versionsschnittstelle
 IUniswapV 2Pair { // Die Verwendung von Schnittstellen hat die gleiche Rolle wie die Verwendung von ABIs in web3.py 
    function  swap ( uint amount0Out, uint amount1Out, address to, bytes calldata data ) external; 
} 
interface IWETH { // Um ​​den Code zu vereinfachen, nehmen wir nur die Funktionen auf, die wir in der Schnittstellendeklaration verwenden. 
    function  payment ( ) external payable; 
} 
Schnittstelle IERC20 { 
    Funktionsübertragung  ( _Adressempfänger, uint256 Betrag ) externe Rückgaben (bool); 
} 
Contract TestSwap { 
    // Speichern Sie die verwendeten Adressen zur besseren Lesbarkeit als Konstanten. Das kostet kein Benzin. 
    Adresskonstante USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7 ; 
    Adresskonstante WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 ; 
    Adresskonstante UNI_PAIR = 0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852 ; 
    // Definieren Sie die Funktion, die von web3.py aufgerufen wird 
    function  startSwap ( uint usdtOut ) external payable {// External gibt an, dass diese Funktion von außerhalb dieses Vertrags aufgerufen wird. Das Schlüsselwort payable ermöglicht es der Funktion, beim Aufruf ETH zu empfangen. 
        // Wickeln Sie die in der Transaktion erhaltene ETH in WETH ein. Der erhaltene ETH-Betrag ist in msg.value verfügbar 
        // Wir rufen die Funktion „deposit()“ des WETH-Vertrags auf und leiten den gleichen ETH-Betrag weiter, den wir in der Transaktion 
        IWETH ( WETH ) erhalten haben. Einzahlung { Wert : msg. Wert }(); // Die Syntax {value: msg.value} wird verwendet, um ETH beim Aufruf an einen Vertrag weiterzuleiten. Die Parameter stehen weiterhin in Klammern (hier gibt es keine). 
        // Da wir nun WETH haben, können wir es an den Uniswap Pair-Vertrag senden. 
        IERC20( WETH ). transfer ( UNI_PAIR , msg. value ); // Wir verwenden die ERC20-Funktion transfer(). Die Menge an WETH entspricht der erhaltenen ETH. 
        // Genau wie beim WETH-Vertrag schließen wir die Adresse an die Schnittstelle an, die wir verwenden möchten, und rufen die gewünschte Funktion auf. 
        // Denken Sie daran, dass USDT der Token1 des Paares ist. Da wir keine ETH austauschen wollen, übergeben wir als ersten Parameter 0. 
        // Um ​​den aktuellen Vertrag als Empfänger der Ausgabe anzugeben, hätten wir „address(this)“ anstelle von „msg.sender“ verwenden können. Hier haben wir die Ausgabe an die Adresse gesendet, die die aktuelle Transaktion aufgerufen hat. 
        IUniswapV 2Pair( UNI_PAIR ). tauschen (0 , usdtOut, msg. Absender , neue  Bytes ( 0 )); // Der letzte Parameter ist der Datenparameter, den wir hier nicht verwenden. Wir übergeben ein leeres Byte-Array. 
    } 
}

Beachten Sie, dass der Bot-Vertrag immer vorher über das erforderliche WETH verfügen sollte (oder Flash-Swaps/Darlehen nutzen), aber nicht gleichzeitig mit der Durchführung der Swaps abgeschlossen werden sollte. Damit soll vermieden werden, dass die Gaskosten für die Einbindung von Eth in die Arbitrage-Transaktion bezahlt werden, wodurch der Bot nicht mehr wettbewerbsfähig ist.

Dieser Smart Contract wird auf einem lokalen Fork-Knoten bereitgestellt. Dies ist ein Knoten, der eine parallele Ethereum-Blockchain lokal auf Ihrem Computer simuliert. Es muss eine Verbindung zu einem externen „echten“ Knoten hergestellt werden, um externe Daten wie den WETH-Vertrag oder den Uniswap-Pair-Vertrag abzurufen. Der lokale Knoten wählt einen bestimmten Block aus, woraufhin er keine Daten aus den tatsächlichen Transaktionen, sondern nur die von Ihnen erzeugten lokalen Transaktionen enthält. Es ist ein hervorragendes Tool zum Testen intelligenter Verträge, die mit externen Verträgen interagieren, da Sie jeden gewünschten Zustand der Blockchain simulieren können.

Derzeit wäre die empfohlene Methode zum Erstellen eines lokalen Fork-Knotens die Verwendung von Anvil.

Anvil ist im Lieferumfang von Foundry enthalten , einem vollständigen Entwicklungstoolset für Ethereum-kompatible Blockchains (EVM-kompatibel).

Um Foundry unter Windows zu installieren, muss WSL aktiviert oder Git Bash installiert sein. Führen Sie dann entweder im Git-Bash-Terminal oder im WSL-Terminal die folgenden Befehle aus:

curl -L https://foundry.paradigm.xyz | Bash 
Foundryup

Jetzt sollten Sie in der Lage sein, den folgenden Befehl auszuführen, um einen lokalen gezweigten Knoten zu starten:

anvil --fork-url https://mainnet.infura.io/v3/<YOUR_INFURA_PROJECT_ID>

Sie sollten dies in Ihrer Konsole sehen:

Startbotschaft von Anvil

Bitte beachten Sie, dass Infura-Knoten den Archivmodus für Konten der kostenlosen Stufe nicht unterstützen. Das bedeutet, dass Sie keine Daten ausblem zu lösen, starten Sie einfach den lokalen Knoten neu. Der neueste Block wird als Ausgangspunkt verwendet.

Um den Smart Contract, den wir gerade geschrieben haben, bereitzustellen, verwenden wir Remix . Erstellen Sie eine neue Codedatei, fügen Sie den obigen Code ein und kompilieren Sie ihn, um ihn auf Fehler zu prüfen.

Gehen Sie nach erfolgreicher Kompilierung zur Registerkarte „Bereitstellen und Ausführen“. Um den Vertrag auf dem lokalen gezweigten Knoten anstelle der JavaScript-VM (die keine Uniswap-Verträge hat) bereitzustellen, wählen Sie Dev - Foundry Providerals Umgebung aus. Geben Sie die URL ein (wahrscheinlich lautet sie

http://localhost:8545

).

Knotenauswahlliste

Foundry-Knotenparameter

Jetzt sollten Sie in der Lage sein, den Vertrag bereitzustellen. Die Remix-Konsole sollte Daten zur Transaktion anzeigen.

Der folgende Python-Code kann verwendet werden, um die startSwap()Funktion des oben genannten Smart Contracts aufzurufen. Es verwendet die web3.pyBibliothek, die das Python-Äquivalent von ist web3.js. Das zur Unterzeichnung der Transaktion verwendete Konto muss über genügend ETH verfügen, um den Tausch zu finanzieren und das Gas zu bezahlen.

Web3.py sendet die Transaktion an den im w3Objekt angegebenen Knoten. Hier verwenden wir den lokalen Anvil-Knoten, es könnte sich aber auch um einen beliebigen Knoten handeln, einschließlich Infura. Zum Testen empfiehlt sich die Verwendung eines lokalen Knotens, da dies schneller ist und kein Gas kostet.

Entlang des Codes werden Kommentare bereitgestellt, um zu erklären, was er tut.

Stellen Sie sicher, dass Sie die erforderlichen Bibliotheken mit installieren pip install web3 eth_account.

Ändern Sie die Parameter oben im Skript entsprechend Ihrem Setup.

from web3 import Web3 
from eth_account import Account 
import json 
##### Parameter/Konstanten des Skripts ##### 
# URL des lokalen Anvil-Knotens
 NODE_URL = "http://localhost:8545" 
# Privater Schlüssel des Kontos Dadurch wird die Transaktion unterzeichnet. 
# Anvil stellt private Schlüssel von Testkonten zur Verfügung, die bereits viel ETH enthalten. 
# Dieses Beispiel-PK enthält wahrscheinlich 10.000 ETH. 
SENDER_PK = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
# Adresse des WETH-Vertrags. Vergessen Sie nicht, Prüfsummenadressen zu verwenden (haben Sie Großbuchstaben. Sie können Web3.toChecksumAddress() verwenden, um eine Adresse in das Prüfsummenformat zu konvertieren, oder sie von EtherScan kopieren) WETH_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" # USDT ERC20
 - Vertragsadresse 
USDT_ADDRESS
 = " 0xdAC17F958D2ee523a2206206994597C13D831ec7" 
# Adresse des Uniswap WETH-USDT-Paares
 UNI_PAIR = "0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852" 
# Eingabebetrag der zu tauschenden ETH. 1 ETH = 10^18 wei
 ETH_INPUT = 10 ** 18  # Python gibt eine Ganzzahl zurück. Achten Sie darauf, keine Floats zu verwenden, wie dies bei anderen Sprachen der Fall ist. 
# Gebühr wird von Uniswap erhoben
UNI_FEE = 0,003  # 0,3 % 
# TestSwap-Vertragsadresse, wie von Remix nach der Bereitstellung oder im Anvil-Konsolenprotokoll angegeben. 
TESTSWAP_ADDRESS = "0x9D3DA37d36BB0B825CD319ed129c2872b893f538" 
########################################## # 
# Stellen Sie eine Verbindung zum lokalen Anvil-Knoten her.
 w3 = Web3(Web3.HTTPProvider(NODE_URL)) 
# Das zum Signieren der Transaktion verwendete Konto. Erstellt aus dem angegebenen privaten Schlüssel. Anvil stellt private Schlüssel von Testkonten zur Verfügung, die über viel ETH verfügen. 
signer = Account.from_key(SENDER_PK) 
address = signer.address # Berechnen Sie die Adresse aus dem privaten Schlüssel 
print ( „Using address:“ , Adresse)
# Zuerst finden wir die Reserven des Uniswap-Paarvertrags. Wir müssen dies tun, weil wir wissen müssen, wie viel USDT wir für die von uns gesendete ETH erhalten. 
# Laden Sie den ABI. Sie können es als JSON-Datei speichern oder direkt kopieren, wie wir es hier tun. Beachten Sie, dass wir einen unvollständigen ABI verwenden, da nur die Funktion getReserves() benötigt wird. 
#pairABI = json.load(open("UniswapV2Pair.json", "r"))
 pairABI = [ 
    { 
        "constant" : True , 
        "inputs" : [], 
        "name" : "getReserves" , 
        "outputs" : [ 
            { 
                "internalType" : "uint112" ,
                
                „type“ : „uint112“
             }, 
            { 
                „internalType“ : „uint112“ , 
                „name“ : „_reserve1“ , 
                „type“ : „uint112“
             }, 
            { 
                „internalType“ : „uint32“ , 
                „name“ : „_blockTimestampLast " , 
                "type" : "uint32"
             } 
        ], 
        "payable" : True , 
        "stateMutability" :„view“ , 
        „type“ : „function“
     }
] 
# Erstellen Sie ein Vertragsobjekt für den Uniswap-Paarvertrag. 
pairContract = w3.eth.contract(address=UNI_PAIR, abi=pairABI) 
# Rufen Sie die Funktion getReserves() des Uniswap-Paarvertrags auf. 
pairReserves =pairContract.functions.getReserves().call() 
# Verwenden Sie die Formel (1), um dy zu berechnen, den Betrag an USDT, den wir für die von uns gesendete ETH erhalten. 
# Denken Sie daran, dass Uniswap V2 eine Gebühr von 0,3 % auf den Eingabebetrag erhebt. 
# dy = y * (x/(x + dx) - 1) (1)
 x = pairReserves[ 0 ] # x ist die Menge an WETH im Paar
 y = pairReserves[ 1 ] # y ist die Menge an USDT im Paar
 dx = ETH_INPUT * ( 1 - UNI_FEE)# dx ist die Menge an WETH, die von der konstanten Produktformel verwendet wird
 usdtOutput = y * ( 1 - x/(x + dx)) # dy ist die Menge an USDT, die wir für die ETH erhalten, die wir senden
 usdtOutput = int ( usdtOutput) # Auf Ganzzahl kürzen, da die Formel einen Gleitkommawert zurückgibt und wir den Betrag niemals überschätzen dürfen, da sonst der Tausch fehlschlagen würde. Wir verwenden abs(), um die Zahl positiv zu machen, da die Formel die Reservedifferenz für den Pool zurückgibt. 
print ( "Expected USDT Output:" , usdtOutput) 
# Holen Sie sich den ABI des TestSwap-Vertrags. Der ABI kann von Remix kopiert werden. 
#tractABI = json.load(open("TestSwapABI.json",
        : [ 
            { 
                "internalType" : "uint256" , 
                "name" : "usdtOut" , 
                "type" : "uint256"
             } 
        ], 
        "name" : "startSwap" , 
        "outputs" : [], 
        "stateMutability" : "payable " , 
        "type" : "function"
     } 
] 
# Erstellen Sie ein Vertragsobjekt aus dem ABI und der Adresse des TestSwap-Vertrags. Der ABI kann von Remix kopiert werden. 
testswap = w3.eth.contract(address=TESTSWAP_ADDRESS,
# Erstellen Sie ein Transaktionsobjekt aus der Funktion, die wir aufrufen möchten, und ihren Parametern. 
# Hier rufen wir die Funktion startSwap() auf und übergeben den oben berechneten Ausgabe-USDT-Betrag als Parameter. 
txn = testswap.functions.startSwap(usdtOutput).buildTransaction({ 
    'chainId' : 1 , # Ketten-ID des Knotens. Ganache verwendet 1337, Anvil verwendet 1. 
    'gas' : 500000 , # Gaslimit. 
    'gasPrice' : w3 .eth.gas_price, # Gaspreis. Wir verwenden den Gaspreis des Knotens. 
    'nonce' : w3.eth.get_transaction_count(address), # Nonce ist die Anzahl der vom Konto gesendeten Transaktionen. Wird verwendet, um Wiederholungsangriffe zu verhindern . 
    'Wert': ETH_INPUT # Der ETH-Betrag, den wir an den Vertrag senden. Wir verwenden den gleichen Betrag wie die Menge an ETH, die wir tauschen möchten. 
}) 
# Signieren Sie die Transaktion mit dem privaten Schlüssel des Kontos. 
signiert_txn = w3.eth.account.sign_transaction(txn, private_key=SENDER_PK) 
# Senden Sie die Transaktion an den Knoten. 
tx_hash = w3.eth.send_raw_transaction(signed_txn.rawTransaction) 
print ( "Tx hash:" , tx_hash. hex ()) 
print ( "Warten auf Transaktion, die abgebaut wird..." ) 
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) 
print ( „Im Block abgebaute Transaktion“ , tx_receipt.blockNumber) 
drucken( „Transaktionsstatus:“ , tx_receipt.status, „(Erfolg)“  , wenn tx_receipt.status == 1 ,  sonst  „(Fehler)“ ) 
print ( „Verwendetes Gas:“ , tx_receipt.gasUsed)

Wenn alles korrekt eingerichtet ist, sollten Sie die folgende Ausgabe sehen:

Verwendete Adresse: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
 Erwartete USDT-Ausgabe: 1855484230
 Tx -Hash : 0xefa0800fdb19cc823c5705e12729a9f18e8df8919b4240b6eb2cfee52
 af36eb5 Warten auf das Mining der Transaktion ... 
Transaktion im Block 17381684
 abgebaut. Transaktionsstatus: 1 (Erfolg) 
Verbrauchtes Gas: 106897

Cooles Feature: Flash-Swaps

Leser mit etwas mehr Erfahrung könnten an einer coolen Funktion von Uniswap V2 interessiert sein: Flash-Swaps . Beachten Sie, dass die swap()Funktion des UniswapV2Pair-Vertrags in vier Schritte unterteilt werden kann:

  1. Übergabe der angeforderten Leistungsbeträge an den Empfänger

  2. Rückruf des Absendervertrags und Übergabe des dataParameters

  3. Überprüfung der Invariante

  4. Aktualisieren des Status des Paarvertrags

Dies kann zunächst unsicher erscheinen, da Gelder gesendet werden, bevor die Invariante überprüft wurde. Beachten Sie, dass die Transaktion rückgängig gemacht wird, wenn die Invariante nicht berücksichtigt wird. Wenn eine Transaktion rückgängig gemacht wird, werden alle Zustandsänderungen rückgängig gemacht, einschließlich des Geldtransfers, wodurch die Sicherheit der Gelder gewährleistet wird.

Dieser Mechanismus wird als optimistischer Transfer bezeichnet . Durch den Aufruf der Callback-Funktion an der Adresse des Anrufers ermöglicht Uniswap V2 sogenannte Flash-Darlehen (oder Flash-Swaps): Die ausgegebenen Gelder werden an den Angerufenen geliehen, der mit ihnen machen kann, was er will, solange sie es sind vor der Invariantenprüfung zurückgegeben. Ein Beispiel für die Verwendung einer solchen Funktion ist die Arbitrage großer Geldbeträge zwischen verschiedenen Börsen, wenn der Bot-Vertrag nicht über genügend Mittel verfügt, um die Arbitrage durchzuführen.

Beachten Sie, dass dadurch mehr Gas verbraucht wird und Ihr Bot möglicherweise nicht mehr konkurrenzfähig ist.

Hier ist ein vereinfachtes Solidity-Beispiel für die Implementierung eines Flash-Swaps in Ihrem Smart Contract auf Uniswap V2. Der Code geht davon aus, dass eine Arbitrage zwischen zwei Pools möglich ist, und führt diese durch, ohne dass Mittel im Vertrag erforderlich sind.

// [...] 
// Die Änderungen an Ihrem Vertrag müssen dieser Schnittstelle entsprechen. 
Schnittstelle IUniswapV 2Callee { 
    Funktion  uniswapV2Call ( Adresse Absender, uint Betrag0, uint Betrag1, Bytes Anrufdaten Daten ) external; 
} 
// Das Schlüsselwort „is“ bedeutet, dass der Vertrag von der IUniswapV2Callee-Schnittstelle erbt. 
Vertrag TestSwap ist UniswapV2Callee { 
    // Wir stellen der startSwap()-Funktion weitere Parameter zur Verfügung, um sie an die swap()-Funktion übergeben zu können, die sie wiederum an die Callback-Funktion übergibt. PairB ist die Adresse des zweiten V2-kompatiblen Paares. 
    Funktion  startSwap (AdresspaarA, AdresspaarB, uint wethInPairA, uint usdtOutPairA, uint wethOutPairB ) external payable { 
        // [...] 
        // Wir konvertieren die Parameter in ein Byte-Array, um sie an die swap()-Funktion übergeben zu können. Beachten Sie, dass die Verwendung von abi.encode() einfach, aber sehr gaseffizient ist. abi.encodePacked() ist effizienter, muss aber manuell dekodiert werden. 
        Bytes Speicherdaten = abi. encode (pairB, wethInPairA, wehtOutPairB); 
        // Wir senden die USDT-Gelder an das zweite Paar, damit wir die WETH-Ausgabe verlangen können, die das USDT-Darlehen des ersten Swaps zurückzahlen wird. 
        IUniswapV 2Pair(PaarA). swap ( 0 , usdtOutPairA, pairB, data); 
        // Die Invariante sollte an dieser Stelle respektiert werden.
        // Wir hätten von wethOutPairB - wethInPairA WETH profitieren sollen. 
    } 
    function  uniswapV2Call ( address sender, uint amount0, uint amount1, bytes calldata data ) external override { 
        // Diese Funktion wird vom UniswapV2Pair-Vertrag in der Mitte der Swap-Funktion aufgerufen. 
        // Wenn es in der Callback-Funktion keine Möglichkeit gibt, auf die an die startSwap()-Funktion übertragenen Informationen zuzugreifen, sind folgende Informationen zugänglich: // 
        – Die Adresse des Absenders der swap()-Funktion. Beachten Sie, dass msg.sender hier der UniswapV2Pair-Vertrag ist, nicht die Adresse des Benutzers, der startSwap() aufgerufen hat. 
        // - Die Werte token0Out und token1Out werden unverändert unter den Namen amount0 und amount1 übertragen.
        // – Der Datenparameter ist derselbe, der an die swap()-Funktion übergeben wird. Es kann beliebige Informationen enthalten. 
        // Wir entpacken die Daten, die zum Durchführen des zweiten Austauschs erforderlich sind
         (AdressenpaarB, uint wethInPairA, uint wethOutPairB) = abi. dekodieren (Daten, (Adresse, uint, uint)); 
        // Wir führen den zweiten Tausch durch. Wir erhalten das WETH für diesen Vertrag und zahlen das USDT-Darlehen in WETH vor der invarianten Prüfung des ersten Pools zurück (was genau dann geschieht, wenn wir von der aktuellen Funktion zurückkehren). 
        IUniswapV 2Pair(PaarB). swap (wethOutPairB, 0 , Adresse ( this ), neue  Bytes ( 0 ));// Keine Daten weiterzugeben, keine Notwendigkeit, die Mittel für diesen Swap zu leihen. 
        // Überweisen Sie das WETH, das zur Rückzahlung des Flash-Darlehens benötigt wird, und behalten Sie die Differenz. Die Adresse des ersten Pools ist der Aufrufer der aktuellen Funktion (msg.sender). 
        IERC20 ( WETH ). transfer (msg. sender , wethInPairA); 
    } 
}

Es gibt viele Kommentare, aber der Code ist für Anfänger möglicherweise immer noch schwer zu verstehen. Nach der Lektüre der folgenden Artikel dieser Serie wird es klarer erscheinen. Beachten Sie, dass der Code nicht für die Gaseffizienz optimiert ist. Es können viele Änderungen vorgenommen werden, um ihn zu verbessern.

Abschluss

In diesem langen Artikel wurden viele Informationen behandelt, die zum Durchführen von Swaps auf Uniswap V2 erforderlich sind. Wir haben gesehen, wie man die Reserven und den Preis eines Pools abruft, wie man die konstante Produktformel verwendet, um die Menge der aus einem V2-Swap bei einem bestimmten Eingabebetrag erhaltenen Token zu berechnen, wie man einen Swap mit dem UniswapV2Pair-Vertrag durchführt, und wie Flash-Swaps in Uniswap V2 implementiert werden können.

Im nächsten Artikel werden wir sehen, wie man effizient die Reserven von über 100.000 Pools auf einmal abruft, von Uniswap, aber auch von Sushiswap und ähnlichen Forks/V2-kompatiblen DEXes.

🌟 Eine Initiative, um die Web3-Bildung für die nächste Generation 
zugänglicher zu machen. 🌟

🌟 Feel free to check out: 
my.bio/blockliv3
Social Media: @blockliv3
E-Mail: blockliv3.nft@ud.me

Subscribe to blockliv3 | (GER)
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from blockliv3 | (GER)

Skeleton

Skeleton

Skeleton