Uniswap-v2 Contract Walk-Through
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:
- Tauschen zwischen verschiedenen Token
- Liquidität zum Markt hinzufügen und mit ERC-20-Liquiditäts-Token der Tauschbörse belohnt werden
- 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
- Dem Peripherie-Konto eine Freigabe (Allowance) in Höhe des zu tauschenden Betrags erteilen.
- 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)
- Die Beträge identifizieren, die an jeder Tauschbörse entlang des Pfades gehandelt werden müssen.
- Ü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)
- Überprüfen, ob der Kernvertrag nicht betrogen wird und nach dem Tauschen ausreichend Liquidität aufrechterhalten kann.
- 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.
- Die Ausgabe-Token an das Ziel senden.
_updateaufrufen, um die Reservebeträge zu aktualisieren
Zurück im Peripherie-Vertrag (UniswapV2Router02.sol)
- 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
- Dem Peripherie-Konto eine Freigabe (Allowance) in den Beträgen erteilen, die dem Liquiditätspool hinzugefügt werden sollen.
- Eine der
addLiquidity-Funktionen des Peripherie-Vertrags aufrufen.
Im Peripherie-Vertrag (UniswapV2Router02.sol)
- Bei Bedarf eine neue Tauschbörse für das Paar erstellen
- 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.
- Ü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)
- Den Kernvertrag aufrufen.
Im Kernvertrag (UniswapV2Pair.sol)
- Liquiditäts-Token prägen und an den Aufrufer senden
_updateaufrufen, um die Reservebeträge zu aktualisieren
Liquidität entfernen
Aufrufer
- Dem Peripherie-Konto eine Freigabe (Allowance) für Liquiditäts-Token erteilen, die im Austausch gegen die zugrunde liegenden Token verbrannt werden sollen.
- Eine der
removeLiquidity-Funktionen des Peripherie-Vertrags aufrufen.
Im Peripherie-Vertrag (UniswapV2Router02.sol)
- Die Liquiditäts-Token an die Tauschbörse des Paares senden
Im Kernvertrag (UniswapV2Pair.sol)
- 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.
- Die Liquiditäts-Token verbrennen
_updateaufrufen, 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;23import './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 anzeigenDies 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 getReserves2 uint112 private reserve1; // nutzt einen einzigen Speicherplatz, zugänglich über getReservesDie 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 getReservesDer 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ätsereignisDie 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.
| Ereignis | reserve0 | reserve1 | reserve0 * reserve1 | Durchschnittlicher Wechselkurs (token1 / token0) |
|---|---|---|---|---|
| Initiale Einrichtung | 1,000.000 | 1,000.000 | 1,000,000 | |
| Händler A tauscht 50 token0 gegen 47.619 token1 | 1,050.000 | 952.381 | 1,000,000 | 0.952 |
| Händler B tauscht 10 token0 gegen 8.984 token1 | 1,060.000 | 943.396 | 1,000,000 | 0.898 |
| Händler C tauscht 40 token0 gegen 34.305 token1 | 1,100.000 | 909.090 | 1,000,000 | 0.858 |
| Händler D tauscht 100 token1 gegen 109.01 token0 | 990.990 | 1,009.090 | 1,000,000 | 0.917 |
| Händler E tauscht 10 token0 gegen 10.079 token1 | 1,000.990 | 999.010 | 1,000,000 | 1.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:
- Revert (Rückgängig machen). Wenn ein Aufruf an einen externen Smart Contract rückgängig gemacht wird, ist der boolesche Rückgabewert
false - 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 to8 );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 aufgerufen2 function initialize(address _token0, address _token1) external {3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // ausreichende Prüfung4 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, Preisakkumulatoren2 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ünscht3 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ünscht2 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:
| Ereignis | reserve0 | reserve1 | Zeitstempel | Marginaler Wechselkurs (reserve1 / reserve0) | price0CumulativeLast |
|---|---|---|---|---|---|
| Initiale Einrichtung | 1,000.000 | 1,000.000 | 5,000 | 1.000 | 0 |
| Händler A zahlt 50 token0 ein und erhält 47.619 token1 zurück | 1,050.000 | 952.381 | 5,020 | 0.907 | 20 |
| Händler B zahlt 10 token0 ein und erhält 8.984 token1 zurück | 1,060.000 | 943.396 | 5,030 | 0.890 | 20+10*0.907 = 29.07 |
| Händler C zahlt 40 token0 ein und erhält 34.305 token1 zurück | 1,100.000 | 909.090 | 5,100 | 0.826 | 29.07+70*0.890 = 91.37 |
| Händler D zahlt 100 token1 ein und erhält 109.01 token0 zurück | 990.990 | 1,009.090 | 5,110 | 1.018 | 91.37+10*0.826 = 99.63 |
| Händler E zahlt 10 token0 ein und erhält 10.079 token1 zurück | 1,000.990 | 999.010 | 5,150 | 0.998 | 99.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; // GaseinsparungenDie 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ührt2 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(); // GaseinsparungenDies 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 kann2 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 TokenWenn 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.
| Ereignis | reserve0 | reserve1 | reserve0 * reserve1 | Wert des Pools (reserve0 + reserve1) |
|---|---|---|---|---|
| Initiale Einrichtung | 8 | 32 | 256 | 40 |
| Händler zahlt 8 Token0-Token ein, erhält 16 Token1 zurück | 16 | 16 | 256 | 32 |
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).
| Ereignis | reserve0 | reserve1 | reserve0 * reserve1 | Pool-Wert (reserve0 + reserve1) | Für diese Einzahlung geprägte Liquiditäts-Token | Gesamte Liquiditäts-Token | Wert jedes Liquiditäts-Tokens |
|---|---|---|---|---|---|---|---|
| Initiale Einrichtung | 8.000 | 8.000 | 64 | 16.000 | 8 | 8 | 2.000 |
| Einzahlung von vier jedes Typs | 12.000 | 12.000 | 144 | 24.000 | 4 | 12 | 2.000 |
| Einzahlung von zwei jedes Typs | 14.000 | 14.000 | 196 | 28.000 | 2 | 14 | 2.000 |
| Einzahlung mit ungleichem Wert | 18.000 | 14.000 | 252 | 32.000 | 0 | 14 | ~2.286 |
| Nach Arbitrage | ~15.874 | ~15.874 | 252 | ~31.748 | 0 | 14 | ~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.
12 _update(balance0, balance1, _reserve0, _reserve1);3 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 und reserve1 sind aktuell4 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ührt2 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(); // Gaseinsparungen2 address _token0 = token0; // Gaseinsparungen3 address _token1 = token1; // Gaseinsparungen4 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 kann3 amount0 = liquidity.mul(balance0) / _totalSupply; // die Verwendung von Salden stellt eine anteilige Verteilung sicher4 amount1 = liquidity.mul(balance1) / _totalSupply; // die Verwendung von Salden stellt eine anteilige Verteilung sicher5 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));67 _update(balance0, balance1, _reserve0, _reserve1);8 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 und reserve1 sind aktuell9 emit Burn(msg.sender, amount0, amount1, to);10 }11Alle anzeigenDer 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ührt2 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(); // Gaseinsparungen3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');45 uint balance0;6 uint balance1;7 { // Gültigkeitsbereich für _token{0,1}, vermeidet 'Stack too deep'-FehlerLokale 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 optimistisch5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // überträgt Token optimistischDieser 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'-Fehler5 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 }23 _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 Saldenskim, heben Sie den zusätzlichen Betrag ab. Beachten Sie, dass jedes Kontoskimaufrufen 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 entsprechen2 function skim(address to) external lock {3 address _token0 = token0; // Gaseinsparungen4 address _token1 = token1; // Gaseinsparungen5 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));6 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));7 }891011 // erzwingt, dass Reserven den Salden entsprechen12 function sync() external lock {13 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);14 }15}Alle anzeigenUniswapV2Factory.sol
Dieser Smart Contract (opens in a new tab) erstellt die Paar-Börsen.
1pragma solidity =0.5.16;23import './interfaces/IUniswapV2Factory.sol';4import './UniswapV2Pair.sol';56contract 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 ausreichendGroß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 Richtung3 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 }56 function setFeeToSetter(address _feeToSetter) external {7 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');8 feeToSetter = _feeToSetter;9 }10}Alle anzeigenDiese 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 := chainid5 }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 anzeigenBerechnen 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 }4Wenn 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;23import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';4import '@uniswap/lib/contracts/libraries/TransferHelper.sol';56import './interfaces/IUniswapV2Router02.sol';7import './libraries/UniswapV2Library.sol';8import './libraries/SafeMath.sol';9import './interfaces/IERC20.sol';10import './interfaces/IWETH.sol';Alle anzeigenDen 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;34 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-Vertrag3 }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.
12 // **** 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 amountBMinDies 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:
| Parameter | Wert |
|---|---|
| amountADesired | 1000 |
| amountBDesired | 1000 |
| amountAMin | 900 |
| amountBMin | 800 |
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 existiert2 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.
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 deadlineAlle anzeigenDiese 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 deadline5 ) 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 amountETHMin13 );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 anzeigenUm 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 vorhanden3 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 deadline10 ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {Alle anzeigenDer 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 Paar3 (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 deadline8 ) 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 deadline17 );18 TransferHelper.safeTransfer(token, to, amountToken);19 IWETH(WETH).withdraw(amountETH);20 TransferHelper.safeTransferETH(to, amountETH);21 }Alle anzeigenDas 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 s10 ) 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 }161718 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 s26 ) 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 anzeigenDiese Funktionen leiten Meta-Transaktionen weiter, um Benutzern ohne Ether zu ermöglichen, sich aus dem Pool zurückzuziehen, indem sie den Permit-Mechanismus verwenden.
12 // **** 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 deadline10 ) 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 deadline19 );20 TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));21 IWETH(WETH).withdraw(amountETH);22 TransferHelper.safeTransferETH(to, amountETH);23 }24Alle anzeigenDiese 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.
123 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 s11 ) 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, deadline17 );18 }Alle anzeigenDie 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 wurde3 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.
- Die Ausgangssituation
- Ein Händler verkauft 24,695 A-Token und erhält 25,305 B-Token.
- Der Händler verkauft 24,695 B-Token für 25,305 C-Token und behält etwa 0,61 B-Token als Gewinn.
- 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).
| Schritt | A-B-Börse | B-C-Börse | A-C-Börse |
|---|---|---|---|
| 1 | A:1000 B:1050 A/B=1.05 | B:1000 C:1050 B/C=1.05 | A:1050 C:1000 C/A=1.05 |
| 2 | A:1024.695 B:1024.695 A/B=1 | B:1000 C:1050 B/C=1.05 | A:1050 C:1000 C/A=1.05 |
| 3 | A:1024.695 B:1024.695 A/B=1 | B:1024.695 C:1024.695 B/C=1 | A:1050 C:1000 C/A=1.05 |
| 4 | A:1024.695 B:1024.695 A/B=1 | B:1024.695 C:1024.695 B/C=1 | A: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.
12 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 deadline3 ) 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 deadline7 ) 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 anzeigenDie 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 external3 virtual4 override5 payable6 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 }161718 function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)19 external20 virtual21 override22 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 }35363738 function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)39 external40 virtual41 override42 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 }555657 function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)58 external59 virtual60 override61 payable62 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 vorhanden72 if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);73 }Alle anzeigenDiese 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 wurde3 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 vermeiden8 (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 anzeigenAufgrund 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 }789 function swapExactTokensForTokensSupportingFeeOnTransferTokens(10 uint amountIn,11 uint amountOutMin,12 address[] calldata path,13 address to,14 uint deadline15 ) external virtual override ensure(deadline) {16 TransferHelper.safeTransferFrom(17 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn18 );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 }262728 function swapExactETHForTokensSupportingFeeOnTransferTokens(29 uint amountOutMin,30 address[] calldata path,31 address to,32 uint deadline33 )34 external35 virtual36 override37 payable38 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 }515253 function swapExactTokensForETHSupportingFeeOnTransferTokens(54 uint amountIn,55 uint amountOutMin,56 address[] calldata path,57 address to,58 uint deadline59 )60 external61 virtual62 override63 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]), amountIn68 );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 anzeigenDies 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 }56 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)7 public8 pure9 virtual10 override11 returns (uint amountOut)12 {13 return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);14 }1516 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)17 public18 pure19 virtual20 override21 returns (uint amountIn)22 {23 return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);24 }2526 function getAmountsOut(uint amountIn, address[] memory path)27 public28 view29 virtual30 override31 returns (uint[] memory amounts)32 {33 return UniswapV2Library.getAmountsOut(factory, amountIn, path);34 }3536 function getAmountsIn(uint amountOut, address[] memory path)37 public38 view39 virtual40 override41 returns (uint[] memory amounts)42 {43 return UniswapV2Library.getAmountsIn(factory, amountOut, path);44 }45}Alle anzeigenDiese 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;23// eine Bibliothek zur Durchführung verschiedener mathematischer Operationen45library Math {6 function min(uint x, uint y) internal pure returns (uint z) {7 z = x < y ? x : y;8 }910 // 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 anzeigenBeginnen 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;23// eine Bibliothek zur Handhabung binärer Festkommazahlen (https://wikipedia.org/wiki/Q_(number_format))45// Bereich: [0, 2**112 - 1]6// Auflösung: 1 / 2**11278library UQ112x112 {9 uint224 constant Q112 = 2**112;Alle anzeigenQ112 ist die Kodierung für eins.
1 // kodiert einen uint112 als UQ112x1122 function encode(uint112 y) internal pure returns (uint224 z) {3 z = uint224(y) * Q112; // läuft nie über4 }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ück2 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;23import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';45import "./SafeMath.sol";67library UniswapV2Library {8 using SafeMath for uint;910 // gibt sortierte Token-Adressen zurück, wird verwendet, um Rückgabewerte von in dieser Reihenfolge sortierten Paaren zu verarbeiten11 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 anzeigenSortieren 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ätigen2 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-Hash9 ))));10 }Alle anzeigenDiese 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 sie2 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ück2 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ück2 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.
12 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ück2 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.
12 // führt verkettete getAmountOut-Berechnungen für eine beliebige Anzahl von Paaren durch3 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 }1213 // führt verkettete getAmountIn-Berechnungen für eine beliebige Anzahl von Paaren durch14 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 anzeigenDiese 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-later23pragma solidity >=0.6.0;45// Hilfsmethoden für die Interaktion mit ERC20-Token und das Senden von ETH, die nicht konsistent true/false zurückgeben6library TransferHelper {7 function safeApprove(8 address token,9 address to,10 uint256 value11 ) internal {12 // bytes4(keccak256(bytes('approve(address,uint256)')));13 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));14Alle anzeigenWir können einen anderen Vertrag auf eine von zwei Arten aufrufen:
- Verwenden einer Schnittstellendefinition (Interface), um einen Funktionsaufruf zu erstellen
- Verwenden des Application Binary Interface (ABI) (opens in a new tab) „manuell“, um den Aufruf zu erstellen. Dafür hat sich der Autor des Codes entschieden.
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).
123 function safeTransfer(4 address token,5 address to,6 uint256 value7 ) 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 anzeigenDiese 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.
12 function safeTransferFrom(3 address token,4 address from,5 address to,6 uint256 value7 ) 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 anzeigenDiese 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.
12 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
