Zum Hauptinhalt springen

ERC-20-Vertrag im Detail

Solidity
erc-20
Anfänger
Ori Pomerantz
9. März 2021
27 Minuten Lesezeit

Einführung

Eine der häufigsten Anwendungen für Ethereum ist, dass eine Gruppe einen handelbaren Token erstellt, in gewissem Sinne ihre eigene Währung. Diese Token folgen typischerweise einem Standard, ERC-20. Dieser Standard macht es möglich, Werkzeuge wie Liquiditätspools und Wallets zu schreiben, die mit allen ERC-20-Token funktionieren. In diesem Artikel werden wir die ERC20-Implementierung in Solidity von OpenZeppelin (opens in a new tab) sowie die Schnittstellendefinition (opens in a new tab) analysieren.

Dies ist kommentierter Quellcode. Wenn Sie ERC-20 implementieren möchten, lesen Sie dieses Tutorial (opens in a new tab).

Die Schnittstelle

Der Zweck eines Standards wie ERC-20 ist es, viele Token-Implementierungen zu ermöglichen, die über Anwendungen hinweg interoperabel sind, wie Wallets und dezentrale Börsen. Um das zu erreichen, erstellen wir eine Schnittstelle (opens in a new tab). Jeder Code, der den Token-Vertrag nutzen muss, kann dieselben Definitionen in der Schnittstelle verwenden und mit allen Token-Verträgen kompatibel sein, die diese nutzen, sei es eine Wallet wie MetaMask, eine Dezentrale Anwendung (Dapp) wie etherscan.io oder ein anderer Vertrag wie ein Liquiditätspool.

Illustration of the ERC-20 interface

Wenn Sie ein erfahrener Programmierer sind, erinnern Sie sich wahrscheinlich daran, ähnliche Konstrukte in Java (opens in a new tab) oder sogar in C-Header-Dateien (opens in a new tab) gesehen zu haben.

Dies ist eine Definition der ERC-20-Schnittstelle (opens in a new tab) von OpenZeppelin. Es ist eine Übersetzung des menschenlesbaren Standards (opens in a new tab) in Solidity-Code. Natürlich definiert die Schnittstelle selbst nicht, wie etwas zu tun ist. Das wird im Vertragsquellcode weiter unten erklärt.

 

// SPDX-License-Identifier: MIT

Solidity-Dateien sollten eine Lizenzkennung enthalten. Sie können die Liste der Lizenzen hier einsehen (opens in a new tab). Wenn Sie eine andere Lizenz benötigen, erklären Sie dies einfach in den Kommentaren.

 

pragma solidity >=0.6.0 <0.8.0;

Die Sprache Solidity entwickelt sich immer noch schnell weiter, und neue Versionen sind möglicherweise nicht mit altem Code kompatibel (siehe hier (opens in a new tab)). Daher ist es eine gute Idee, nicht nur eine Mindestversion der Sprache anzugeben, sondern auch eine Höchstversion, die neueste, mit der Sie den Code getestet haben.

 

/**
 * @dev Schnittstelle des ERC-20-Standards, wie im EIP definiert.
 */

Das @dev im Kommentar ist Teil des NatSpec-Formats (opens in a new tab), das verwendet wird, um Dokumentation aus dem Quellcode zu erstellen.

 

interface IERC20 {

Konventionsgemäß beginnen Schnittstellennamen mit I.

 

    /**
     * @dev Gibt die Menge der existierenden Token zurück.
     */
    function totalSupply() external view returns (uint256);

Diese Funktion ist external, was bedeutet, dass sie nur von außerhalb des Vertrags aufgerufen werden kann (opens in a new tab). Sie gibt das Gesamtangebot an Token im Vertrag zurück. Dieser Wert wird unter Verwendung des häufigsten Typs in Ethereum zurückgegeben, vorzeichenlose 256 Bits (256 Bits ist die native Wortgröße der EVM). Diese Funktion ist auch ein view, was bedeutet, dass sie den Zustand nicht ändert, sodass sie auf einem einzelnen Knoten ausgeführt werden kann, anstatt dass jeder Knoten in der Blockchain sie ausführt. Diese Art von Funktion generiert keine Transaktion und kostet kein Gas.

Hinweis: Theoretisch könnte es so aussehen, als könnte der Ersteller eines Vertrags betrügen, indem er ein kleineres Gesamtangebot als den tatsächlichen Wert zurückgibt, wodurch jeder Token wertvoller erscheint, als er tatsächlich ist. Diese Befürchtung ignoriert jedoch die wahre Natur der Blockchain. Alles, was auf der Blockchain passiert, kann von jedem Knoten verifiziert werden. Um dies zu erreichen, sind der Maschinensprache-Code und der Speicher jedes Vertrags auf jedem Knoten verfügbar. Obwohl Sie nicht verpflichtet sind, den Solidity-Code für Ihren Vertrag zu veröffentlichen, würde Sie niemand ernst nehmen, es sei denn, Sie veröffentlichen den Quellcode und die Version von Solidity, mit der er kompiliert wurde, damit er gegen den von Ihnen bereitgestellten Maschinensprache-Code verifiziert werden kann. Sehen Sie sich zum Beispiel diesen Vertrag (opens in a new tab) an.

 

    /**
     * @dev Gibt die Menge der Token zurück, die dem `account` (Konto) gehören.
     */
    function balanceOf(address account) external view returns (uint256);

Wie der Name schon sagt, gibt balanceOf den Kontostand eines Kontos zurück. Ethereum-Konten werden in Solidity mit dem Typ address identifiziert, der 160 Bits fasst. Sie ist ebenfalls external und view.

 

    /**
     * @dev Bewegt `amount` Token vom Konto des Aufrufers zu `recipient`.
     *
     * Gibt einen booleschen Wert zurück, der angibt, ob die Operation erfolgreich war.
     *
     * Löst ein {Transfer}-Ereignis aus.
     */
    function transfer(address recipient, uint256 amount) external returns (bool);

Die Funktion transfer transferiert Token vom Aufrufer an eine andere Adresse. Dies beinhaltet eine Zustandsänderung, also ist es kein view. Wenn ein Benutzer diese Funktion aufruft, erstellt sie eine Transaktion und kostet Gas. Sie löst auch ein Ereignis aus, Transfer, um jeden auf der Blockchain über das Ereignis zu informieren.

Die Funktion hat zwei Arten von Ausgaben für zwei verschiedene Arten von Aufrufern:

  • Benutzer, die die Funktion direkt über eine Benutzeroberfläche aufrufen. Typischerweise reicht der Benutzer eine Transaktion ein und wartet nicht auf eine Antwort, was eine unbestimmte Zeit dauern könnte. Der Benutzer kann sehen, was passiert ist, indem er nach dem Transaktionsbeleg sucht (der durch den Transaktions-Hash identifiziert wird) oder nach dem Ereignis Transfer sucht.
  • Andere Verträge, die die Funktion als Teil einer Gesamttransaktion aufrufen. Diese Verträge erhalten das Ergebnis sofort, da sie in derselben Transaktion ausgeführt werden, sodass sie den Rückgabewert der Funktion verwenden können.

Die gleiche Art von Ausgabe wird von den anderen Funktionen erzeugt, die den Zustand des Vertrags ändern.

 

Freigabebeträge erlauben es einem Konto, einige Token auszugeben, die einem anderen Eigentümer gehören. Dies ist zum Beispiel nützlich für Verträge, die als Verkäufer agieren. Verträge können nicht auf Ereignisse überwachen. Wenn also ein Käufer Token direkt an den Verkäufervertrag transferieren würde, wüsste dieser Vertrag nicht, dass er bezahlt wurde. Stattdessen genehmigt der Käufer dem Verkäufervertrag, einen bestimmten Betrag auszugeben, und der Verkäufer transferiert diesen Betrag. Dies geschieht über eine Funktion, die der Verkäufervertrag aufruft, sodass der Verkäufervertrag wissen kann, ob er erfolgreich war.

    /**
     * @dev Gibt die verbleibende Anzahl von Token zurück, die `spender` im Namen von `owner` über {transferFrom} ausgeben darf. Dies ist standardmäßig null.
     *
     * Dieser Wert ändert sich, wenn {approve} oder {transferFrom} aufgerufen werden.
     */
    function allowance(address owner, address spender) external view returns (uint256);

Die Funktion allowance lässt jeden abfragen, wie hoch der Freigabebetrag ist, den eine Adresse (owner) einer anderen Adresse (spender) zum Ausgeben überlässt.

 

Die Funktion approve erstellt einen Freigabebetrag. Stellen Sie sicher, dass Sie die Nachricht darüber lesen, wie dies missbraucht werden kann. In Ethereum kontrollieren Sie die Reihenfolge Ihrer eigenen Transaktionen, aber Sie können nicht kontrollieren, in welcher Reihenfolge die Transaktionen anderer Personen ausgeführt werden, es sei denn, Sie reichen Ihre eigene Transaktion erst ein, wenn Sie sehen, dass die Transaktion der anderen Seite stattgefunden hat.

 

Schließlich wird transferFrom vom Ausgebenden verwendet, um den Freigabebetrag tatsächlich auszugeben.

 

Diese Ereignisse werden ausgelöst, wenn sich der Zustand des ERC-20-Vertrags ändert.

Der eigentliche Vertrag

Dies ist der eigentliche Vertrag, der den ERC-20-Standard implementiert, von hier entnommen (opens in a new tab). Er ist nicht dazu gedacht, unverändert verwendet zu werden, aber Sie können davon erben (opens in a new tab), um ihn zu etwas Brauchbarem zu erweitern.

// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.8.0;

 

Import-Anweisungen

Zusätzlich zu den obigen Schnittstellendefinitionen importiert die Vertragsdefinition zwei weitere Dateien:


import "../../GSN/Context.sol";
import "./IERC20.sol";
import "../../math/SafeMath.sol";
  • GSN/Context.sol sind die Definitionen, die erforderlich sind, um OpenGSN (opens in a new tab) zu verwenden, ein System, das es Benutzern ohne Ether ermöglicht, die Blockchain zu nutzen. Beachten Sie, dass dies eine alte Version ist. Wenn Sie OpenGSN integrieren möchten, verwenden Sie dieses Tutorial (opens in a new tab).
  • Die SafeMath-Bibliothek (opens in a new tab), die arithmetische Überläufe/Unterläufe für Solidity-Versionen <0.8.0 verhindert. In Solidity ≥0.8.0 werden arithmetische Operationen bei einem Überlauf/Unterlauf automatisch rückgängig gemacht, was SafeMath überflüssig macht. Dieser Vertrag verwendet SafeMath zur Abwärtskompatibilität mit älteren Compiler-Versionen.

 

Dieser Kommentar erklärt den Zweck des Vertrags.

Vertragsdefinition

contract ERC20 is Context, IERC20 {

Diese Zeile spezifiziert die Vererbung, in diesem Fall von IERC20 von oben und Context für OpenGSN.

 


    using SafeMath for uint256;

Diese Zeile bindet die Bibliothek SafeMath an den Typ uint256. Sie finden diese Bibliothek hier (opens in a new tab).

Variablendefinitionen

Diese Definitionen spezifizieren die Zustandsvariablen des Vertrags. Diese Variablen sind als private deklariert, aber das bedeutet nur, dass andere Verträge auf der Blockchain sie nicht lesen können. Es gibt keine Geheimnisse auf der Blockchain, die Software auf jedem Knoten hat den Zustand jedes Vertrags bei jedem Block. Konventionsgemäß werden Zustandsvariablen _<something> genannt.

Die ersten beiden Variablen sind Mappings (opens in a new tab), was bedeutet, dass sie sich in etwa wie assoziative Arrays (opens in a new tab) verhalten, außer dass die Schlüssel numerische Werte sind. Speicher wird nur für Einträge zugewiesen, die Werte haben, die vom Standard (Null) abweichen.

    mapping (address => uint256) private _balances;

Das erste Mapping, _balances, sind Adressen und ihre jeweiligen Kontostände dieses Tokens. Um auf den Kontostand zuzugreifen, verwenden Sie diese Syntax: _balances[<address>].

 

    mapping (address => mapping (address => uint256)) private _allowances;

Diese Variable, _allowances, speichert die zuvor erklärten Freigabebeträge. Der erste Index ist der Eigentümer der Token, und der zweite ist der Vertrag mit dem Freigabebetrag. Um auf den Betrag zuzugreifen, den Adresse A vom Konto der Adresse B ausgeben kann, verwenden Sie _allowances[B][A].

 

    uint256 private _totalSupply;

Wie der Name schon sagt, verfolgt diese Variable das Gesamtangebot an Token.

 

    string private _name;
    string private _symbol;
    uint8 private _decimals;

Diese drei Variablen werden verwendet, um die Lesbarkeit zu verbessern. Die ersten beiden sind selbsterklärend, aber _decimals ist es nicht.

Einerseits hat Ethereum keine Fließkomma- oder Bruchvariablen. Andererseits mögen es Menschen, Token teilen zu können. Ein Grund, warum sich die Menschen auf Gold als Währung geeinigt haben, war, dass es schwer war, Wechselgeld zu geben, wenn jemand den Gegenwert einer Ente in Kuh kaufen wollte.

Die Lösung besteht darin, Ganzzahlen zu verfolgen, aber anstelle des echten Tokens einen Bruchteil-Token zu zählen, der fast wertlos ist. Im Fall von Ether wird der Bruchteil-Token Wei genannt, und 10^18 Wei entsprechen einem ETH. Zum Zeitpunkt des Schreibens sind 10.000.000.000.000 Wei ungefähr ein US- oder Euro-Cent.

Anwendungen müssen wissen, wie der Token-Kontostand angezeigt werden soll. Wenn ein Benutzer 3.141.000.000.000.000.000 Wei hat, sind das 3,14 ETH? 31,41 ETH? 3.141 ETH? Im Fall von Ether ist definiert, dass 10^18 Wei einem ETH entsprechen, aber für Ihren Token können Sie einen anderen Wert wählen. Wenn das Teilen des Tokens keinen Sinn ergibt, können Sie einen _decimals-Wert von null verwenden. Wenn Sie denselben Standard wie ETH verwenden möchten, verwenden Sie den Wert 18.

Der Konstruktor

Der Konstruktor wird aufgerufen, wenn der Vertrag zum ersten Mal erstellt wird. Konventionsgemäß werden Funktionsparameter <something>_ genannt.

Benutzeroberflächen-Funktionen

Diese Funktionen, name, symbol und decimals, helfen Benutzeroberflächen, über Ihren Vertrag Bescheid zu wissen, damit sie ihn richtig anzeigen können.

Der Rückgabetyp ist string memory, was bedeutet, dass ein String zurückgegeben wird, der im Speicher abgelegt ist. Variablen, wie z. B. Strings, können an drei Orten gespeichert werden:

LebensdauerVertragszugriffGaskosten
MemoryFunktionsaufrufLesen/SchreibenZehner oder Hunderter (höher für höhere Speicherorte)
AufrufdatenFunktionsaufrufNur LesenKann nicht als Rückgabetyp verwendet werden, nur als Funktionsparametertyp
StorageBis zur ÄnderungLesen/SchreibenHoch (800 für Lesen, 20k für Schreiben)

In diesem Fall ist memory die beste Wahl.

Token-Informationen lesen

Dies sind Funktionen, die Informationen über den Token bereitstellen, entweder das Gesamtangebot oder den Kontostand eines Kontos.

    /**
     * @dev Siehe {IERC20-totalSupply}.
     */
    function totalSupply() public view override returns (uint256) {
        return _totalSupply;
    }

Die Funktion totalSupply gibt das Gesamtangebot an Token zurück.

 

    /**
     * @dev Siehe {IERC20-balanceOf}.
     */
    function balanceOf(address account) public view override returns (uint256) {
        return _balances[account];
    }

Den Kontostand eines Kontos lesen. Beachten Sie, dass es jedem erlaubt ist, den Kontostand jedes anderen abzurufen. Es hat keinen Sinn zu versuchen, diese Informationen zu verbergen, da sie ohnehin auf jedem Knoten verfügbar sind. Es gibt keine Geheimnisse auf der Blockchain.

Token transferieren

Die Funktion transfer wird aufgerufen, um Token vom Konto des Senders auf ein anderes zu transferieren. Beachten Sie, dass, obwohl sie einen booleschen Wert zurückgibt, dieser Wert immer true ist. Wenn der Transfer fehlschlägt, macht der Vertrag den Aufruf rückgängig.

 

        _transfer(_msgSender(), recipient, amount);
        return true;
    }

Die Funktion _transfer erledigt die eigentliche Arbeit. Es ist eine private Funktion, die nur von anderen Vertragsfunktionen aufgerufen werden kann. Konventionsgemäß werden private Funktionen _<something> genannt, genau wie Zustandsvariablen.

Normalerweise verwenden wir in Solidity msg.sender für den Absender der Nachricht. Das bricht jedoch OpenGSN (opens in a new tab). Wenn wir etherlose Transaktionen mit unserem Token erlauben wollen, müssen wir _msgSender() verwenden. Es gibt msg.sender für normale Transaktionen zurück, aber für etherlose gibt es den ursprünglichen Unterzeichner zurück und nicht den Vertrag, der die Nachricht weitergeleitet hat.

Freigabebetrag-Funktionen

Dies sind die Funktionen, die die Funktionalität für Freigabebeträge implementieren: allowance, approve, transferFrom und _approve. Zusätzlich geht die OpenZeppelin-Implementierung über den grundlegenden Standard hinaus und enthält einige Funktionen, die die Sicherheit verbessern: increaseAllowance und decreaseAllowance.

Die allowance-Funktion

    /**
     * @dev Siehe {IERC20-allowance}.
     */
    function allowance(address owner, address spender) public view virtual override returns (uint256) {
        return _allowances[owner][spender];
    }

Die Funktion allowance erlaubt es jedem, jeden Freigabebetrag zu überprüfen.

Die approve-Funktion

    /**
     * @dev Siehe {IERC20-approve}.
     *
     * Anforderungen:
     *
     * - `spender` darf nicht die Nulladresse sein.
     */
    function approve(address spender, uint256 amount) public virtual override returns (bool) {

Diese Funktion wird aufgerufen, um einen Freigabebetrag zu erstellen. Sie ist ähnlich wie die obige Funktion transfer:

  • Die Funktion ruft lediglich eine interne Funktion auf (in diesem Fall _approve), die die eigentliche Arbeit erledigt.
  • Die Funktion gibt entweder true zurück (wenn erfolgreich) oder wird rückgängig gemacht (wenn nicht).

 

        _approve(_msgSender(), spender, amount);
        return true;
    }

Wir verwenden interne Funktionen, um die Anzahl der Stellen zu minimieren, an denen Zustandsänderungen stattfinden. Jede Funktion, die den Zustand ändert, ist ein potenzielles Sicherheitsrisiko, das auf Sicherheit geprüft werden muss. Auf diese Weise haben wir weniger Chancen, Fehler zu machen.

Die transferFrom-Funktion

Dies ist die Funktion, die ein Ausgebender aufruft, um einen Freigabebetrag auszugeben. Dies erfordert zwei Operationen: den ausgegebenen Betrag transferieren und den Freigabebetrag um diesen Betrag reduzieren.

 

Der Funktionsaufruf a.sub(b, "message") tut zwei Dinge. Erstens berechnet er a-b, was der neue Freigabebetrag ist. Zweitens überprüft er, ob dieses Ergebnis nicht negativ ist. Wenn es negativ ist, wird der Aufruf mit der bereitgestellten Nachricht rückgängig gemacht. Beachten Sie, dass, wenn ein Aufruf rückgängig gemacht wird, jegliche Verarbeitung, die zuvor während dieses Aufrufs durchgeführt wurde, ignoriert wird, sodass wir _transfer nicht rückgängig machen müssen.

        _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,
             "ERC20: transfer amount exceeds allowance"));
        return true;
    }

Sicherheitserweiterungen von OpenZeppelin

Es ist gefährlich, einen Freigabebetrag ungleich null auf einen anderen Wert ungleich null zu setzen, da Sie nur die Reihenfolge Ihrer eigenen Transaktionen kontrollieren, nicht die von jemand anderem. Stellen Sie sich vor, Sie haben zwei Benutzer, Alice, die naiv ist, und Bill, der unehrlich ist. Alice möchte eine Dienstleistung von Bill, von der sie denkt, dass sie fünf Token kostet – also gibt sie Bill einen Freigabebetrag von fünf Token.

Dann ändert sich etwas und Bills Preis steigt auf zehn Token. Alice, die die Dienstleistung immer noch möchte, sendet eine Transaktion, die Bills Freigabebetrag auf zehn setzt. In dem Moment, in dem Bill diese neue Transaktion im Transaktionspool sieht, sendet er eine Transaktion, die Alices fünf Token ausgibt und einen viel höheren Gaspreis hat, damit sie schneller gemint wird. Auf diese Weise kann Bill zuerst fünf Token ausgeben und dann, sobald Alices neuer Freigabebetrag gemint ist, zehn weitere ausgeben, für einen Gesamtpreis von fünfzehn Token, mehr als Alice autorisieren wollte. Diese Technik wird Front-Running (opens in a new tab) genannt.

Alice-TransaktionAlice-NonceBill-TransaktionBill-NonceBills FreigabebetragBills Gesamteinnahmen von Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
approve(Bill, 10)11105
transferFrom(Alice, Bill, 10)10,124015

Um dieses Problem zu vermeiden, ermöglichen Ihnen diese beiden Funktionen (increaseAllowance und decreaseAllowance), den Freigabebetrag um einen bestimmten Betrag zu ändern. Wenn Bill also bereits fünf Token ausgegeben hätte, könnte er nur noch fünf weitere ausgeben. Abhängig vom Timing gibt es zwei Möglichkeiten, wie dies funktionieren kann, die beide damit enden, dass Bill nur zehn Token erhält:

A:

Alice-TransaktionAlice-NonceBill-TransaktionBill-NonceBills FreigabebetragBills Gesamteinnahmen von Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
increaseAllowance(Bill, 5)110+5 = 55
transferFrom(Alice, Bill, 5)10,124010

B:

Alice-TransaktionAlice-NonceBill-TransaktionBill-NonceBills FreigabebetragBills Gesamteinnahmen von Alice
approve(Bill, 5)1050
increaseAllowance(Bill, 5)115+5 = 100
transferFrom(Alice, Bill, 10)10,124010

Die Funktion a.add(b) ist eine sichere Addition. Im unwahrscheinlichen Fall, dass a+b>=2^256 ist, kommt es nicht zu einem Überlauf, wie es bei einer normalen Addition der Fall wäre.

Funktionen, die Token-Informationen ändern

Dies sind die vier Funktionen, die die eigentliche Arbeit erledigen: _transfer, _mint, _burn und _approve.

Die _transfer-Funktion

Diese Funktion, _transfer, transferiert Token von einem Konto auf ein anderes. Sie wird sowohl von transfer (für Transfers vom eigenen Konto des Senders) als auch von transferFrom (für die Verwendung von Freigabebeträgen zum Transferieren vom Konto einer anderen Person) aufgerufen.

 

        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");

Niemand besitzt tatsächlich die Nulladresse in Ethereum (das heißt, niemand kennt einen privaten Schlüssel, dessen passender öffentlicher Schlüssel in die Nulladresse umgewandelt wird). Wenn Leute diese Adresse verwenden, handelt es sich normalerweise um einen Softwarefehler – daher schlagen wir fehl, wenn die Nulladresse als Sender oder Empfänger verwendet wird.

 

        _beforeTokenTransfer(sender, recipient, amount);

Es gibt zwei Möglichkeiten, diesen Vertrag zu nutzen:

  1. Verwenden Sie ihn als Vorlage für Ihren eigenen Code
  2. Erben Sie davon (opens in a new tab) und überschreiben Sie nur die Funktionen, die Sie ändern müssen

Die zweite Methode ist viel besser, da der ERC-20-Code von OpenZeppelin bereits geprüft wurde und sich als sicher erwiesen hat. Wenn Sie Vererbung verwenden, ist klar, welche Funktionen Sie ändern, und um Ihrem Vertrag zu vertrauen, müssen die Leute nur diese spezifischen Funktionen prüfen.

Es ist oft nützlich, jedes Mal eine Funktion auszuführen, wenn Token den Besitzer wechseln. Jedoch ist _transfer eine sehr wichtige Funktion und es ist möglich, sie unsicher zu schreiben (siehe unten), daher ist es am besten, sie nicht zu überschreiben. Die Lösung ist _beforeTokenTransfer, eine Hook-Funktion (opens in a new tab). Sie können diese Funktion überschreiben, und sie wird bei jedem Transfer aufgerufen.

 

        _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");
        _balances[recipient] = _balances[recipient].add(amount);

Dies sind die Zeilen, die den Transfer tatsächlich durchführen. Beachten Sie, dass sich nichts dazwischen befindet und dass wir den transferierten Betrag vom Sender abziehen, bevor wir ihn dem Empfänger hinzufügen. Dies ist wichtig, denn wenn es in der Mitte einen Aufruf an einen anderen Vertrag gäbe, hätte dieser verwendet werden können, um diesen Vertrag zu betrügen. Auf diese Weise ist der Transfer atomar, in der Mitte kann nichts passieren.

 

        emit Transfer(sender, recipient, amount);
    }

Lösen Sie schließlich ein Ereignis Transfer aus. Ereignisse sind für Smart Contracts nicht zugänglich, aber Code, der außerhalb der Blockchain ausgeführt wird, kann auf Ereignisse lauschen und darauf reagieren. Zum Beispiel kann eine Wallet verfolgen, wann der Eigentümer mehr Token erhält.

Die _mint- und _burn-Funktionen

Diese beiden Funktionen (_mint und _burn) ändern das Gesamtangebot an Token. Sie sind intern und es gibt keine Funktion, die sie in diesem Vertrag aufruft, daher sind sie nur nützlich, wenn Sie vom Vertrag erben und Ihre eigene Logik hinzufügen, um zu entscheiden, unter welchen Bedingungen neue Token geprägt oder bestehende verbrannt werden sollen.

HINWEIS: Jeder ERC-20-Token hat seine eigene Geschäftslogik, die die Token-Verwaltung diktiert. Zum Beispiel könnte ein Vertrag mit festem Angebot _mint nur im Konstruktor aufrufen und niemals _burn aufrufen. Ein Vertrag, der Token verkauft, wird _mint aufrufen, wenn er bezahlt wird, und vermutlich irgendwann _burn aufrufen, um eine unkontrollierte Inflation zu vermeiden.

Stellen Sie sicher, dass Sie _totalSupply aktualisieren, wenn sich die Gesamtzahl der Token ändert.

 

Die Funktion _burn ist fast identisch mit _mint, außer dass sie in die andere Richtung geht.

Die _approve-Funktion

Dies ist die Funktion, die tatsächlich Freigabebeträge spezifiziert. Beachten Sie, dass sie es einem Eigentümer ermöglicht, einen Freigabebetrag anzugeben, der höher ist als der aktuelle Kontostand des Eigentümers. Das ist in Ordnung, da der Kontostand zum Zeitpunkt des Transfers überprüft wird, wenn er sich von dem Kontostand bei der Erstellung des Freigabebetrags unterscheiden könnte.

 

Lösen Sie ein Ereignis Approval aus. Je nachdem, wie die Anwendung geschrieben ist, kann der Ausgebenden-Vertrag entweder vom Eigentümer oder von einem Server, der auf diese Ereignisse lauscht, über die Genehmigung informiert werden.

        emit Approval(owner, spender, amount);
    }

Die Decimals-Variable ändern

Diese Funktion ändert die Variable _decimals, die verwendet wird, um Benutzeroberflächen mitzuteilen, wie der Betrag zu interpretieren ist. Sie sollten sie aus dem Konstruktor aufrufen. Es wäre unehrlich, sie zu einem späteren Zeitpunkt aufzurufen, und Anwendungen sind nicht darauf ausgelegt, damit umzugehen.

Hooks

Dies ist die Hook-Funktion, die während Transfers aufgerufen werden soll. Sie ist hier leer, aber wenn Sie möchten, dass sie etwas tut, überschreiben Sie sie einfach.

Fazit

Zur Wiederholung sind hier einige der wichtigsten Ideen in diesem Vertrag (meiner Meinung nach, Ihre wird wahrscheinlich abweichen):

  • Es gibt keine Geheimnisse auf der Blockchain. Jede Information, auf die ein Smart Contract zugreifen kann, ist für die ganze Welt verfügbar.
  • Sie können die Reihenfolge Ihrer eigenen Transaktionen kontrollieren, aber nicht, wann die Transaktionen anderer Personen stattfinden. Dies ist der Grund, warum das Ändern eines Freigabebetrags gefährlich sein kann, da es dem Ausgebenden ermöglicht, die Summe beider Freigabebeträge auszugeben.
  • Werte vom Typ uint256 laufen über. Mit anderen Worten, 0-1=2^256-1. Wenn das kein gewünschtes Verhalten ist, müssen Sie darauf prüfen (oder die SafeMath-Bibliothek verwenden, die das für Sie erledigt). Beachten Sie, dass sich dies in Solidity 0.8.0 (opens in a new tab) geändert hat.
  • Führen Sie alle Zustandsänderungen eines bestimmten Typs an einem bestimmten Ort durch, da dies die Prüfung erleichtert. Dies ist der Grund, warum wir zum Beispiel _approve haben, das von approve, transferFrom, increaseAllowance und decreaseAllowance aufgerufen wird.
  • Zustandsänderungen sollten atomar sein, ohne eine andere Aktion in ihrer Mitte (wie Sie in _transfer sehen können). Dies liegt daran, dass Sie während der Zustandsänderung einen inkonsistenten Zustand haben. Zum Beispiel existieren zwischen dem Zeitpunkt, an dem Sie vom Kontostand des Senders abziehen, und dem Zeitpunkt, an dem Sie dem Kontostand des Empfängers hinzufügen, weniger Token, als es geben sollte. Dies könnte potenziell missbraucht werden, wenn es dazwischen Operationen gibt, insbesondere Aufrufe an einen anderen Vertrag.

Da Sie nun gesehen haben, wie der ERC-20-Vertrag von OpenZeppelin geschrieben ist und insbesondere, wie er sicherer gemacht wird, gehen Sie hin und schreiben Sie Ihre eigenen sicheren Verträge und Anwendungen.

Weitere Arbeiten von mir finden Sie hier (opens in a new tab).