Przejdź do głównej treści

Przewodnik po kontrakcie ERC-20

Solidity
erc-20
Początkujący
Ori Pomerantz
9 marca 2021
25 minut czytania

Wprowadzenie

Jednym z najczęstszych zastosowań Ethereum jest tworzenie przez grupę zbywalnego tokena, w pewnym sensie własnej waluty. Tokeny te zazwyczaj są zgodne ze standardem ERC-20. Standard ten umożliwia pisanie narzędzi, takich jak pule płynności i portfele, które współpracują ze wszystkimi tokenami ERC-20. W tym artykule przeanalizujemy implementację ERC20 w Solidity od OpenZeppelin (opens in a new tab), a także definicję interfejsu (opens in a new tab).

To jest opatrzony komentarzami kod źródłowy. Jeśli chcesz zaimplementować ERC-20, przeczytaj ten samouczek (opens in a new tab).

Interfejs

Celem standardu takiego jak ERC-20 jest umożliwienie wielu implementacji tokenów, które są interoperacyjne w różnych aplikacjach, takich jak portfele i zdecentralizowane giełdy. Aby to osiągnąć, tworzymy interfejs (opens in a new tab). Każdy kod, który musi użyć kontraktu tokena, może korzystać z tych samych definicji w interfejsie i być kompatybilny ze wszystkimi kontraktami tokenów, które go używają, niezależnie od tego, czy jest to portfel taki jak MetaMask, zdecentralizowana aplikacja (dapp) taka jak etherscan.io, czy inny kontrakt, taki jak pula płynności.

Illustration of the ERC-20 interface

Jeśli jesteś doświadczonym programistą, prawdopodobnie pamiętasz podobne konstrukcje w Javie (opens in a new tab) lub nawet w plikach nagłówkowych C (opens in a new tab).

To jest definicja interfejsu ERC-20 (opens in a new tab) od OpenZeppelin. Jest to tłumaczenie czytelnego dla człowieka standardu (opens in a new tab) na kod Solidity. Oczywiście sam interfejs nie definiuje jak cokolwiek zrobić. Zostało to wyjaśnione w kodzie źródłowym kontraktu poniżej.

 

// SPDX-License-Identifier: MIT

Pliki Solidity powinny zawierać identyfikator licencji. Listę licencji możesz zobaczyć tutaj (opens in a new tab). Jeśli potrzebujesz innej licencji, po prostu wyjaśnij to w komentarzach.

 

pragma solidity >=0.6.0 <0.8.0;

Język Solidity wciąż szybko ewoluuje, a nowe wersje mogą nie być kompatybilne ze starym kodem (zobacz tutaj (opens in a new tab)). Dlatego dobrym pomysłem jest określenie nie tylko minimalnej wersji języka, ale także maksymalnej wersji, czyli najnowszej, z którą testowano kod.

 

/**
 * @dev Interfejs standardu ERC-20 zgodnie z definicją w EIP.
 */

@dev w komentarzu jest częścią formatu NatSpec (opens in a new tab), używanego do generowania dokumentacji z kodu źródłowego.

 

interface IERC20 {

Zgodnie z konwencją, nazwy interfejsów zaczynają się od I.

 

    /**
     * @dev Zwraca ilość istniejących tokenów.
     */
    function totalSupply() external view returns (uint256);

Ta funkcja to external, co oznacza, że może być wywołana tylko z zewnątrz kontraktu (opens in a new tab). Zwraca ona całkowitą podaż tokenów w kontrakcie. Wartość ta jest zwracana przy użyciu najpopularniejszego typu w Ethereum, 256-bitowej liczby bez znaku (256 bitów to natywny rozmiar słowa EVM). Funkcja ta jest również view, co oznacza, że nie zmienia stanu, więc może być wykonana na pojedynczym węźle, zamiast być uruchamiana przez każdy węzeł w blockchainie. Tego rodzaju funkcja nie generuje transakcji i nie kosztuje gazu.

Uwaga: W teorii mogłoby się wydawać, że twórca kontraktu może oszukiwać, zwracając mniejszą całkowitą podaż niż rzeczywista wartość, sprawiając, że każdy token wydaje się cenniejszy niż jest w rzeczywistości. Jednak obawa ta ignoruje prawdziwą naturę blockchaina. Wszystko, co dzieje się na blockchainie, może zostać zweryfikowane przez każdy węzeł. Aby to osiągnąć, kod w języku maszynowym i pamięć każdego kontraktu są dostępne na każdym węźle. Chociaż nie masz obowiązku publikowania kodu Solidity swojego kontraktu, nikt nie potraktuje Cię poważnie, dopóki nie opublikujesz kodu źródłowego i wersji Solidity, w której został skompilowany, aby można go było zweryfikować z dostarczonym kodem w języku maszynowym. Na przykład, zobacz ten kontrakt (opens in a new tab).

 

    /**
     * @dev Zwraca ilość tokenów posiadanych przez `account`.
     */
    function balanceOf(address account) external view returns (uint256);

Jak sama nazwa wskazuje, balanceOf zwraca saldo konta. Konta Ethereum są identyfikowane w Solidity za pomocą typu address, który przechowuje 160 bitów. Jest to również external i view.

 

    /**
     * @dev Przenosi `amount` tokenów z konta wywołującego do `recipient`.
     *
     * Zwraca wartość logiczną wskazującą, czy operacja się powiodła.
     *
     * Emituje zdarzenie {Transfer}.
     */
    function transfer(address recipient, uint256 amount) external returns (bool);

Funkcja transfer wykonuje transfer tokenów od wywołującego na inny adres. Wiąże się to ze zmianą stanu, więc nie jest to view. Kiedy użytkownik wywołuje tę funkcję, tworzy ona transakcję i kosztuje gaz. Emituje również zdarzenie, Transfer, aby poinformować wszystkich w blockchainie o tym zdarzeniu.

Funkcja ma dwa rodzaje wyjścia dla dwóch różnych typów wywołujących:

  • Użytkownicy, którzy wywołują funkcję bezpośrednio z interfejsu użytkownika. Zazwyczaj użytkownik przesyła transakcję i nie czeka na odpowiedź, co mogłoby zająć nieokreśloną ilość czasu. Użytkownik może zobaczyć, co się stało, szukając pokwitowania transakcji (które jest identyfikowane przez hash transakcji) lub szukając zdarzenia Transfer.
  • Inne kontrakty, które wywołują funkcję w ramach ogólnej transakcji. Kontrakty te otrzymują wynik natychmiast, ponieważ działają w tej samej transakcji, więc mogą użyć wartości zwracanej przez funkcję.

Ten sam typ wyjścia jest tworzony przez inne funkcje, które zmieniają stan kontraktu.

 

Limity wydatków pozwalają kontu na wydanie pewnej ilości tokenów należących do innego właściciela. Jest to przydatne na przykład w przypadku kontraktów pełniących rolę sprzedawców. Kontrakty nie mogą monitorować zdarzeń, więc gdyby kupujący przetransferował tokeny bezpośrednio do kontraktu sprzedawcy, kontrakt ten nie wiedziałby, że otrzymał zapłatę. Zamiast tego kupujący pozwala kontraktowi sprzedawcy wydać określoną kwotę, a sprzedawca transferuje tę kwotę. Odbywa się to poprzez funkcję wywoływaną przez kontrakt sprzedawcy, dzięki czemu kontrakt sprzedawcy może wiedzieć, czy operacja się powiodła.

    /**
     * @dev Zwraca pozostałą liczbę tokenów, które `spender` będzie
     * mógł wydać w imieniu `owner` poprzez {transferFrom}. Domyślnie wynosi
     * zero.
     *
     * Ta wartość zmienia się, gdy wywoływane są {approve} lub {transferFrom}.
     */
    function allowance(address owner, address spender) external view returns (uint256);

Funkcja allowance pozwala każdemu sprawdzić, jaki jest limit wydatków, który jeden adres (owner) pozwala wydać innemu adresowi (spender).

 

Funkcja approve tworzy limit wydatków. Koniecznie przeczytaj wiadomość o tym, jak może to zostać nadużyte. W Ethereum kontrolujesz kolejność własnych transakcji, ale nie możesz kontrolować kolejności, w jakiej będą wykonywane transakcje innych osób, chyba że nie prześlesz własnej transakcji, dopóki nie zobaczysz, że transakcja drugiej strony została zrealizowana.

 

Na koniec, transferFrom jest używane przez wydającego do faktycznego wydania limitu wydatków.

 

Te zdarzenia są emitowane, gdy zmienia się stan kontraktu ERC-20.

Właściwy kontrakt

To jest właściwy kontrakt, który implementuje standard ERC-20, pobrany stąd (opens in a new tab). Nie jest on przeznaczony do użycia w obecnej postaci, ale możesz po nim dziedziczyć (opens in a new tab), aby rozszerzyć go do czegoś użytecznego.

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

 

Instrukcje importu

Oprócz powyższych definicji interfejsu, definicja kontraktu importuje dwa inne pliki:


import "../../GSN/Context.sol";
import "./IERC20.sol";
import "../../math/SafeMath.sol";
  • GSN/Context.sol to definicje wymagane do korzystania z OpenGSN (opens in a new tab), systemu, który pozwala użytkownikom bez etheru na korzystanie z blockchaina. Zauważ, że jest to stara wersja, jeśli chcesz zintegrować się z OpenGSN, skorzystaj z tego samouczka (opens in a new tab).
  • Biblioteka SafeMath (opens in a new tab), która zapobiega przepełnieniom arytmetycznym (overflow/underflow) dla wersji Solidity <0.8.0. W Solidity ≥0.8.0 operacje arytmetyczne automatycznie powodują wycofanie w przypadku przepełnienia, co czyni SafeMath niepotrzebnym. Ten kontrakt używa SafeMath dla kompatybilności wstecznej ze starszymi wersjami kompilatora.

 

Ten komentarz wyjaśnia cel kontraktu.

Definicja kontraktu

contract ERC20 is Context, IERC20 {

Ta linia określa dziedziczenie, w tym przypadku z IERC20 z powyższego kodu oraz Context dla OpenGSN.

 


    using SafeMath for uint256;

Ta linia dołącza bibliotekę SafeMath do typu uint256. Możesz znaleźć tę bibliotekę tutaj (opens in a new tab).

Definicje zmiennych

Te definicje określają zmienne stanu kontraktu. Zmienne te są zadeklarowane jako private, ale oznacza to tylko tyle, że inne kontrakty na blockchainie nie mogą ich odczytać. Na blockchainie nie ma tajemnic, oprogramowanie na każdym węźle posiada stan każdego kontraktu w każdym bloku. Zgodnie z konwencją, zmienne stanu nazywane są _<something>.

Pierwsze dwie zmienne to mapowania (opens in a new tab), co oznacza, że zachowują się mniej więcej tak samo jak tablice asocjacyjne (opens in a new tab), z tą różnicą, że kluczami są wartości numeryczne. Pamięć jest przydzielana tylko dla wpisów, które mają wartości inne niż domyślne (zero).

    mapping (address => uint256) private _balances;

Pierwsze mapowanie, _balances, to adresy i ich odpowiednie salda tego tokena. Aby uzyskać dostęp do salda, użyj tej składni: _balances[<address>].

 

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

Ta zmienna, _allowances, przechowuje limity wydatków wyjaśnione wcześniej. Pierwszy indeks to właściciel tokenów, a drugi to kontrakt z limitem wydatków. Aby uzyskać dostęp do kwoty, którą adres A może wydać z konta adresu B, użyj _allowances[B][A].

 

    uint256 private _totalSupply;

Jak sama nazwa wskazuje, ta zmienna śledzi całkowitą podaż tokenów.

 

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

Te trzy zmienne służą do poprawy czytelności. Pierwsze dwie są oczywiste, ale _decimals nie jest.

Z jednej strony Ethereum nie ma zmiennych zmiennoprzecinkowych ani ułamkowych. Z drugiej strony ludzie lubią mieć możliwość dzielenia tokenów. Jednym z powodów, dla których ludzie zdecydowali się na złoto jako walutę, było to, że trudno było wydać resztę, gdy ktoś chciał kupić krowę o wartości kaczki.

Rozwiązaniem jest śledzenie liczb całkowitych, ale liczenie zamiast prawdziwego tokena ułamkowego tokena, który jest prawie bezwartościowy. W przypadku etheru ułamkowy token nazywa się wei, a 10^18 wei jest równe jednemu ETH. W momencie pisania tego tekstu 10 000 000 000 000 wei to w przybliżeniu jeden cent amerykański lub eurocent.

Aplikacje muszą wiedzieć, jak wyświetlić saldo tokenów. Jeśli użytkownik ma 3 141 000 000 000 000 000 wei, czy to jest 3,14 ETH? 31,41 ETH? 3 141 ETH? W przypadku etheru zdefiniowano 10^18 wei na ETH, ale dla swojego tokena możesz wybrać inną wartość. Jeśli dzielenie tokena nie ma sensu, możesz użyć wartości _decimals równej zero. Jeśli chcesz użyć tego samego standardu co ETH, użyj wartości 18.

Konstruktor

Konstruktor jest wywoływany przy pierwszym tworzeniu kontraktu. Zgodnie z konwencją, parametry funkcji nazywane są <something>_.

Funkcje interfejsu użytkownika

Te funkcje, name, symbol i decimals, pomagają interfejsom użytkownika dowiedzieć się o Twoim kontrakcie, aby mogły go poprawnie wyświetlić.

Typem zwracanym jest string memory, co oznacza zwrócenie ciągu znaków przechowywanego w pamięci. Zmienne, takie jak ciągi znaków, mogą być przechowywane w trzech lokalizacjach:

Czas życiaDostęp z kontraktuKoszt gazu
Pamięć (Memory)Wywołanie funkcjiOdczyt/ZapisDziesiątki lub setki (więcej dla wyższych lokalizacji)
Dane wywołania (Calldata)Wywołanie funkcjiTylko do odczytuNie może być użyte jako typ zwracany, tylko jako typ parametru funkcji
Pamięć trwała (Storage)Do momentu zmianyOdczyt/ZapisWysoki (800 za odczyt, 20k za zapis)

W tym przypadku memory jest najlepszym wyborem.

Odczyt informacji o tokenie

Są to funkcje, które dostarczają informacji o tokenie, czy to o całkowitej podaży, czy o saldzie konta.

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

Funkcja totalSupply zwraca całkowitą podaż tokenów.

 

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

Odczyt salda konta. Zauważ, że każdy może uzyskać saldo konta dowolnej innej osoby. Nie ma sensu próbować ukrywać tych informacji, ponieważ i tak są one dostępne na każdym węźle. Na blockchainie nie ma tajemnic.

Transfer tokenów

Funkcja transfer jest wywoływana w celu transferu tokenów z konta nadawcy na inne. Zauważ, że chociaż zwraca wartość logiczną, wartość ta jest zawsze true. Jeśli transfer się nie powiedzie, kontrakt wycofuje wywołanie.

 

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

Funkcja _transfer wykonuje właściwą pracę. Jest to funkcja prywatna, która może być wywoływana tylko przez inne funkcje kontraktu. Zgodnie z konwencją funkcje prywatne nazywane są _<something>, tak samo jak zmienne stanu.

Zazwyczaj w Solidity używamy msg.sender dla nadawcy wiadomości. Jednak to psuje OpenGSN (opens in a new tab). Jeśli chcemy zezwolić na transakcje bez etheru z naszym tokenem, musimy użyć _msgSender(). Zwraca to msg.sender dla normalnych transakcji, ale dla tych bez etheru zwraca oryginalnego sygnatariusza, a nie kontrakt, który przekazał wiadomość.

Funkcje limitu wydatków

Są to funkcje, które implementują funkcjonalność limitu wydatków: allowance, approve, transferFrom i _approve. Dodatkowo implementacja OpenZeppelin wykracza poza podstawowy standard, aby uwzględnić pewne funkcje poprawiające bezpieczeństwo: increaseAllowance i decreaseAllowance.

Funkcja allowance

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

Funkcja allowance pozwala każdemu sprawdzić dowolny limit wydatków.

Funkcja approve

    /**
     * @dev Zobacz {IERC20-approve}.
     *
     * Wymagania:
     *
     * - `spender` nie może być adresem zerowym.
     */
    function approve(address spender, uint256 amount) public virtual override returns (bool) {

Ta funkcja jest wywoływana w celu utworzenia limitu wydatków. Jest podobna do powyższej funkcji transfer:

  • Funkcja po prostu wywołuje funkcję wewnętrzną (w tym przypadku _approve), która wykonuje właściwą pracę.
  • Funkcja zwraca true (jeśli się powiedzie) lub wycofuje transakcję (jeśli nie).

 

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

Używamy funkcji wewnętrznych, aby zminimalizować liczbę miejsc, w których zachodzą zmiany stanu. Każda funkcja, która zmienia stan, jest potencjalnym zagrożeniem bezpieczeństwa, które musi zostać poddane audytowi. W ten sposób mamy mniejsze szanse na popełnienie błędu.

Funkcja transferFrom

Jest to funkcja, którą wywołuje wydający, aby wydać limit wydatków. Wymaga to dwóch operacji: transferu wydawanej kwoty i zmniejszenia limitu wydatków o tę kwotę.

 

Wywołanie funkcji a.sub(b, "message") robi dwie rzeczy. Po pierwsze, oblicza a-b, co jest nowym limitem wydatków. Po drugie, sprawdza, czy ten wynik nie jest ujemny. Jeśli jest ujemny, wywołanie zostaje wycofane z podaną wiadomością. Zauważ, że kiedy wywołanie zostaje wycofane, wszelkie przetwarzanie wykonane wcześniej podczas tego wywołania jest ignorowane, więc nie musimy cofać _transfer.

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

Dodatki bezpieczeństwa OpenZeppelin

Niebezpieczne jest ustawianie niezerowego limitu wydatków na inną niezerową wartość, ponieważ kontrolujesz tylko kolejność własnych transakcji, a nie cudzych. Wyobraź sobie, że masz dwóch użytkowników: naiwną Alice i nieuczciwego Billa. Alice chce od Billa jakiejś usługi, która jej zdaniem kosztuje pięć tokenów - więc daje Billowi limit wydatków w wysokości pięciu tokenów.

Potem coś się zmienia i cena Billa rośnie do dziesięciu tokenów. Alice, która nadal chce usługi, wysyła transakcję, która ustawia limit wydatków Billa na dziesięć. W momencie, gdy Bill widzi tę nową transakcję w puli transakcji, wysyła transakcję, która wydaje pięć tokenów Alice i ma znacznie wyższą cenę gazu, dzięki czemu zostanie wydobyta szybciej. W ten sposób Bill może wydać najpierw pięć tokenów, a następnie, gdy nowy limit wydatków Alice zostanie wydobyty, wydać kolejne dziesięć za łączną cenę piętnastu tokenów, czyli więcej, niż Alice zamierzała autoryzować. Ta technika nazywa się wyprzedzaniem transakcji (opens in a new tab)

Transakcja AliceNonce AliceTransakcja BillaNonce BillaLimit wydatków BillaCałkowity dochód Billa od Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
approve(Bill, 10)11105
transferFrom(Alice, Bill, 10)10,124015

Aby uniknąć tego problemu, te dwie funkcje (increaseAllowance i decreaseAllowance) pozwalają na modyfikację limitu wydatków o określoną kwotę. Więc jeśli Bill wydał już pięć tokenów, będzie mógł wydać tylko pięć kolejnych. W zależności od czasu, istnieją dwa sposoby, w jakie może to zadziałać, z których oba kończą się tym, że Bill otrzymuje tylko dziesięć tokenów:

A:

Transakcja AliceNonce AliceTransakcja BillaNonce BillaLimit wydatków BillaCałkowity dochód Billa od Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
increaseAllowance(Bill, 5)110+5 = 55
transferFrom(Alice, Bill, 5)10,124010

B:

Transakcja AliceNonce AliceTransakcja BillaNonce BillaLimit wydatków BillaCałkowity dochód Billa od Alice
approve(Bill, 5)1050
increaseAllowance(Bill, 5)115+5 = 100
transferFrom(Alice, Bill, 10)10,124010

Funkcja a.add(b) to bezpieczne dodawanie. W mało prawdopodobnym przypadku, gdy a+b>=2^256, nie zawija się ona w sposób, w jaki robi to normalne dodawanie.

Funkcje modyfikujące informacje o tokenie

Oto cztery funkcje, które wykonują właściwą pracę: _transfer, _mint, _burn i _approve.

Funkcja _transfer

Ta funkcja, _transfer, transferuje tokeny z jednego konta na drugie. Jest wywoływana zarówno przez transfer (dla transferów z własnego konta nadawcy), jak i transferFrom (dla użycia limitów wydatków do transferu z cudzego konta).

 

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

Nikt tak naprawdę nie jest właścicielem adresu zerowego w Ethereum (to znaczy nikt nie zna klucza prywatnego, którego pasujący klucz publiczny jest przekształcany na adres zerowy). Kiedy ludzie używają tego adresu, jest to zazwyczaj błąd oprogramowania - więc przerywamy działanie, jeśli adres zerowy jest używany jako nadawca lub odbiorca.

 

        _beforeTokenTransfer(sender, recipient, amount);

Istnieją dwa sposoby korzystania z tego kontraktu:

  1. Użyj go jako szablonu dla własnego kodu
  2. Dziedzicz po nim (opens in a new tab) i nadpisz tylko te funkcje, które musisz zmodyfikować

Druga metoda jest znacznie lepsza, ponieważ kod ERC-20 od OpenZeppelin został już poddany audytowi i wykazano, że jest bezpieczny. Kiedy używasz dziedziczenia, jasne jest, jakie funkcje modyfikujesz, a aby zaufać Twojemu kontraktowi, ludzie muszą jedynie przeprowadzić audyt tych konkretnych funkcji.

Często przydatne jest wykonanie funkcji za każdym razem, gdy tokeny zmieniają właściciela. Jednak _transfer jest bardzo ważną funkcją i można ją napisać w sposób niebezpieczny (patrz poniżej), więc najlepiej jej nie nadpisywać. Rozwiązaniem jest _beforeTokenTransfer, funkcja hook (opens in a new tab). Możesz nadpisać tę funkcję, a będzie ona wywoływana przy każdym transferze.

 

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

To są linie, które faktycznie wykonują transfer. Zauważ, że nie ma między nimi niczego, i że odejmujemy transferowaną kwotę od nadawcy przed dodaniem jej do odbiorcy. Jest to ważne, ponieważ gdyby w środku znajdowało się wywołanie innego kontraktu, mogłoby to zostać użyte do oszukania tego kontraktu. W ten sposób transfer jest atomowy, nic nie może się wydarzyć w jego trakcie.

 

        emit Transfer(sender, recipient, amount);
    }

Na koniec wyemituj zdarzenie Transfer. Zdarzenia nie są dostępne dla inteligentnych kontraktów, ale kod działający poza blockchainem może nasłuchiwać zdarzeń i reagować na nie. Na przykład portfel może śledzić, kiedy właściciel otrzymuje więcej tokenów.

Funkcje _mint i _burn

Te dwie funkcje (_mint i _burn) modyfikują całkowitą podaż tokenów. Są one wewnętrzne i w tym kontrakcie nie ma funkcji, która by je wywoływała, więc są przydatne tylko wtedy, gdy dziedziczysz po kontrakcie i dodajesz własną logikę, aby zdecydować, w jakich warunkach wybijać nowe tokeny lub spalić istniejące.

UWAGA: Każdy token ERC-20 ma własną logikę biznesową, która dyktuje zarządzanie tokenami. Na przykład kontrakt o stałej podaży może wywoływać _mint tylko w konstruktorze i nigdy nie wywoływać _burn. Kontrakt, który sprzedaje tokeny, wywoła _mint po otrzymaniu zapłaty i prawdopodobnie wywoła _burn w pewnym momencie, aby uniknąć niekontrolowanej inflacji.

Pamiętaj, aby zaktualizować _totalSupply, gdy zmieni się całkowita liczba tokenów.

 

Funkcja _burn jest prawie identyczna z _mint, z tą różnicą, że działa w odwrotnym kierunku.

Funkcja _approve

Jest to funkcja, która faktycznie określa limity wydatków. Zauważ, że pozwala ona właścicielowi określić limit wydatków, który jest wyższy niż obecne saldo właściciela. Jest to w porządku, ponieważ saldo jest sprawdzane w momencie transferu, kiedy może różnić się od salda w momencie tworzenia limitu wydatków.

 

Wyemituj zdarzenie Approval. W zależności od tego, jak napisana jest aplikacja, kontrakt wydającego może zostać poinformowany o zatwierdzeniu przez właściciela lub przez serwer, który nasłuchuje tych zdarzeń.

        emit Approval(owner, spender, amount);
    }

Modyfikacja zmiennej Decimals

Ta funkcja modyfikuje zmienną _decimals, która służy do informowania interfejsów użytkownika, jak interpretować kwotę. Powinieneś wywołać ją z konstruktora. Nieuczciwe byłoby wywoływanie jej w jakimkolwiek późniejszym momencie, a aplikacje nie są zaprojektowane do jej obsługi.

Hooki

To jest funkcja hook, która ma być wywoływana podczas transferów. Tutaj jest pusta, ale jeśli potrzebujesz, aby coś robiła, po prostu ją nadpisz.

Wnioski

Dla podsumowania, oto niektóre z najważniejszych pomysłów w tym kontrakcie (moim zdaniem, Twoje mogą się różnić):

  • Na blockchainie nie ma tajemnic. Wszelkie informacje, do których inteligentny kontrakt ma dostęp, są dostępne dla całego świata.
  • Możesz kontrolować kolejność własnych transakcji, ale nie to, kiedy mają miejsce transakcje innych osób. Z tego powodu zmiana limitu wydatków może być niebezpieczna, ponieważ pozwala wydającemu wydać sumę obu limitów wydatków.
  • Wartości typu uint256 zawijają się. Innymi słowy, 0-1=2^256-1. Jeśli nie jest to pożądane zachowanie, musisz to sprawdzić (lub użyć biblioteki SafeMath, która zrobi to za Ciebie). Zauważ, że zmieniło się to w Solidity 0.8.0 (opens in a new tab).
  • Dokonuj wszystkich zmian stanu określonego typu w określonym miejscu, ponieważ ułatwia to audyt. Z tego powodu mamy na przykład _approve, które jest wywoływane przez approve, transferFrom, increaseAllowance i decreaseAllowance
  • Zmiany stanu powinny być atomowe, bez żadnych innych akcji w ich trakcie (jak widać w _transfer). Dzieje się tak, ponieważ podczas zmiany stanu masz niespójny stan. Na przykład, między czasem odjęcia od salda nadawcy a czasem dodania do salda odbiorcy istnieje mniej tokenów, niż powinno. Mogłoby to zostać potencjalnie nadużyte, gdyby pomiędzy nimi znajdowały się operacje, zwłaszcza wywołania innego kontraktu.

Teraz, gdy już wiesz, jak napisany jest kontrakt ERC-20 od OpenZeppelin, a zwłaszcza jak jest on zabezpieczony, idź i napisz własne bezpieczne kontrakty i aplikacje.

Zobacz tutaj więcej moich prac (opens in a new tab).