Przewodnik po kontrakcie ERC-20
Wprowadzenie
Jednym z najczęstszych zastosowań Ethereum jest tworzenie przez grupę wymienialnych tokenów, 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 działają 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 kod źródłowy z adnotacjami. 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żywać kontraktu tokenu, może używać tych samych definicji w interfejsie i być kompatybilnym ze wszystkimi kontraktami tokenów, które go używają, niezależnie od tego, czy jest to portfel, taki jak MetaMask, dapka, taka jak etherscan.io, czy inny kontrakt, taki jak pula płynności.
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).
Jest to definicja interfejsu ERC-20 (opens in a new tab) pochodząca z OpenZeppelin. Jest to tłumaczenie standardu czytelnego dla człowieka (opens in a new tab) na kod Solidity. Oczywiście, sam interfejs nie definiuje jak cokolwiek zrobić. Jest to wyjaśnione w poniższym kodzie źródłowym kontraktu.
1// SPDX-License-Identifier: MITPliki Solidity powinny zawierać identyfikator licencji. Listę licencji można zobaczyć tutaj (opens in a new tab). Jeśli potrzebujesz innej licencji, po prostu wyjaśnij to w komentarzach.
1pragma solidity >=0.6.0 <0.8.0;Język Solidity wciąż szybko się rozwija, 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, najnowszej, z którą testowano kod.
1/**2 * @dev Interfejs standardu ERC20 zdefiniowany w EIP.3 */Zapis @dev w komentarzu jest częścią formatu NatSpec (opens in a new tab), używanego do tworzenia
dokumentacji z kodu źródłowego.
1interface IERC20 {Zgodnie z konwencją, nazwy interfejsów zaczynają się od I.
1 /**2 * @dev Zwraca liczbę istniejących tokenów.3 */4 function totalSupply() external view returns (uint256);Ta funkcja jest zewnętrzna, co oznacza, że może być wywoływana tylko spoza kontraktu (opens in a new tab).
Zwraca całkowitą podaż tokenów w kontrakcie. Wartość ta jest zwracana przy użyciu najpopularniejszego typu w Ethereum, 256-bitowej liczby całkowitej bez znaku (256 bitów to
natywny rozmiar słowa EVM). Ta funkcja jest również typu view, co oznacza, że nie zmienia ona stanu, więc może być wykonana na pojedynczym węźle zamiast być uruchamiana przez
każdy węzeł w blockchainie. Ten rodzaj funkcji nie generuje transakcji i nie kosztuje gazu.
Uwaga: Teoretycznie mogłoby się wydawać, że twórca kontraktu mógłby oszukiwać, zwracając mniejszą całkowitą podaż niż rzeczywista wartość, sprawiając, że każdy token wydaje się bardziej wartościowy, niż jest w rzeczywistości. Jednakże obawa ta ignoruje prawdziwą naturę blockchaina. Wszystko, co dzieje się w blockchainie, może zostać zweryfikowane przez każdy węzeł. Aby to osiągnąć, kod maszynowy i pamięć masowa każdego kontraktu są dostępne na każdym węźle. Chociaż nie jest wymagane publikowanie kodu Solidity kontraktu, nikt nie potraktuje Cię poważnie, chyba że opublikujesz kod źródłowy i wersję Solidity, za pomocą której został on skompilowany, aby można go było zweryfikować z dostarczonym kodem maszynowym. Na przykład zobacz ten kontrakt (opens in a new tab).
1 /**2 * @dev Zwraca liczbę tokenów posiadanych przez `konto`.3 */4 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 adres, który przechowuje 160 bitów.
Jest również zewnętrzna i typu view.
1 /**2 * @dev Przenosi tokeny w `ilości` z konta wywołującego do `odbiorcy`.3 *4 * Zwraca wartość logiczną wskazującą, czy operacja się powiodła.5 *6 * Emituje zdarzenie {Transfer}.7 */8 function transfer(address recipient, uint256 amount) external returns (bool);Funkcja transfer przenosi tokeny od wywołującego na inny adres. Wiąże się to ze zmianą stanu, więc nie jest to widok.
Gdy użytkownik wywołuje tę funkcję, tworzy transakcję i ponosi koszty gazu. Emituje również zdarzenie Transfer, aby poinformować wszystkich w
blockchainie o tym zdarzeniu.
Funkcja ma dwa rodzaje danych wyjściowych dla dwóch różnych typów wywołujących:
- Użytkownicy wywołujący funkcję bezpośrednio z interfejsu użytkownika. Zazwyczaj użytkownik przesyła transakcję
i nie czeka na odpowiedź, co może zająć nieokreśloną ilość czasu. Użytkownik może zobaczyć, co się stało
, szukając potwierdzenia transakcji (które jest identyfikowane przez hasz 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 zwrotnej funkcji.
Ten sam typ danych wyjściowych jest tworzony przez inne funkcje, które zmieniają stan kontraktu.
Zezwolenia pozwalają kontu na wydanie pewnej liczby tokenów należących do innego właściciela. Jest to przydatne na przykład w przypadku kontraktów, które działają jako sprzedawcy. Kontrakty nie mogą monitorować zdarzeń, więc jeśli kupujący miałby przenieść tokeny do kontraktu sprzedawcy bezpośrednio, kontrakt ten nie wiedziałby, że został opłacony. Zamiast tego, kupujący zezwala kontraktowi sprzedawcy na wydanie określonej kwoty, a sprzedawca transferuje tę kwotę. Odbywa się to za pomocą funkcji wywoływanej przez kontrakt sprzedawcy, więc kontrakt sprzedawcy może wiedzieć, czy się to udało.
1 /**2 * @dev Zwraca pozostałą liczbę tokenów, które `spender` będzie mógł3 * wydać w imieniu `owner` poprzez {transferFrom}. Domyślnie4 * jest to zero.5 *6 * Wartość ta zmienia się po wywołaniu {approve} lub {transferFrom}.7 */8 function allowance(address owner, address spender) external view returns (uint256);Funkcja allowance pozwala każdemu sprawdzić, jakie jest zezwolenie, które jeden
adres (właściciel) daje innemu adresowi (wydającemu) do wydania.
1 /**2 * @dev Ustawia `amount` jako zezwolenie `spendera` na tokeny wywołującego.3 *4 * Zwraca wartość logiczną wskazującą, czy operacja się powiodła.5 *6 * WAŻNE: Należy pamiętać, że zmiana zezwolenia za pomocą tej metody niesie ze sobą ryzyko,7 * że ktoś może użyć zarówno starego, jak i nowego zezwolenia przez niefortunne8 * uporządkowanie transakcji. Jednym z możliwych rozwiązań w celu złagodzenia tego stanu wyścigu9 * jest najpierw zmniejszenie zezwolenia wydającego do 0 i ustawienie10 * pożądanej wartości później:11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-26352472912 *13 * Emituje zdarzenie {Approval}.14 */15 function approve(address spender, uint256 amount) external returns (bool);Funkcja approve tworzy zezwolenie. Pamiętaj, aby przeczytać wiadomość o tym,
jak można ją nadużywać. W Ethereum kontrolujesz kolejność własnych transakcji,
ale nie możesz kontrolować kolejności, w jakiej transakcje innych osób będą
wykonywane, chyba że nie prześlesz własnej transakcji, dopóki nie zobaczysz, że
transakcja drugiej strony miała miejsce.
1 /**2 * @dev Przenosi tokeny o `wartości` `amount` z `nadawcy` do `odbiorcy` przy użyciu3 * mechanizmu zezwoleń. `Kwota` jest następnie odejmowana od zezwolenia wywołującego4 * .5 *6 * Zwraca wartość logiczną wskazującą, czy operacja się powiodła.7 *8 * Emituje zdarzenie {Transfer}.9 */10 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);Na koniec transferFrom jest używane przez wydającego do faktycznego wydania zezwolenia.
1
2 /**3 * @dev Emitowane, gdy tokeny `value` są przenoszone z jednego konta (`from`) na drugie (`to`).4 *5 * Zauważ, że `value` może być zerowe.6 */7 event Transfer(address indexed from, address indexed to, uint256 value);8
9 /**10 * @dev Emitowane, gdy pozwolenie `spendera` dla `owner` jest ustawiane przez wywołanie {approve}. `value` to nowe pozwolenie.11 */12 event Approval(address indexed owner, address indexed spender, uint256 value);13}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 formie, ale można go odziedziczyć (opens in a new tab), aby rozszerzyć go do czegoś użytecznego.
1// SPDX-License-Identifier: MIT2pragma solidity >=0.6.0 <0.8.0;
Instrukcje importu
Oprócz powyższych definicji interfejsu, definicja kontraktu importuje dwa inne pliki:
1
2import "../../GSN/Context.sol";3import "./IERC20.sol";4import "../../math/SafeMath.sol";GSN/Context.solto definicje wymagane do użycia OpenGSN (opens in a new tab), systemu, który pozwala użytkownikom bez etheru korzystać z blockchainu. Zauważ, że jest to stara wersja. Jeśli chcesz zintegrować z OpenGSN, użyj tego samouczka (opens in a new tab).- Biblioteka SafeMath (opens in a new tab), która zapobiega przepełnieniom/niedopełnieniom arytmetycznym dla wersji Solidity <0.8.0. W Solidity ≥0.8.0, operacje arytmetyczne automatycznie cofają się w przypadku przepełnienia/niedopełnienia, co sprawia, że biblioteka SafeMath jest niepotrzebna. Ten kontrakt używa SafeMath dla wstecznej kompatybilności ze starszymi wersjami kompilatora.
Ten komentarz wyjaśnia cel kontraktu.
1/**2 * @dev Implementacja interfejsu {IERC20}.3 *4 * Ta implementacja jest niezależna od sposobu tworzenia tokenów. Oznacza to, że mechanizm zasilania musi zostać dodany w kontrakcie pochodnym za pomocą {_mint}.5 * Ogólny mechanizm można znaleźć w {ERC20PresetMinterPauser}.6 *7 * WSKAZÓWKA: Szczegółowy opis można znaleźć w naszym przewodniku8 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[Jak9 * wdrażać mechanizmy dostaw].10 *11 * Postępowaliśmy zgodnie z ogólnymi wytycznymi OpenZeppelin: funkcje w przypadku niepowodzenia cofają się, zamiast zwracać „fałsz”.12 * To zachowanie jest jednak konwencjonalne i nie jest sprzeczne z oczekiwaniami aplikacji ERC20.13 *14 * Dodatkowo zdarzenie {Approval} jest emitowane przy wywołaniach {transferFrom}.15 * Umożliwia to aplikacjom odtworzenie uprawnień dla wszystkich kont poprzez nasłuchiwanie tych zdarzeń.16 * Inne implementacje EIP mogą nie emitować tych zdarzeń, ponieważ nie jest to wymagane przez specyfikację.17 *18 * Wreszcie, niestandardowe funkcje {decreaseAllowance} i {increaseAllowance} zostały dodane w celu złagodzenia dobrze znanych problemów związanych z ustawianiem przydziałów.19 * Zobacz {IERC20-approve}.20 */21
Definicja kontraktu
1contract ERC20 is Context, IERC20 {Ta linia określa dziedziczenie, w tym przypadku z IERC20 z góry i Context, dla OpenGSN.
1
2 using SafeMath for uint256;3
Ta linia dołącza bibliotekę SafeMath do typu uint256. Tę bibliotekę można znaleźć tutaj (opens in a new tab).
Definicje zmiennych
Te definicje określają zmienne stanu kontraktu. Te zmienne są zadeklarowane jako private, ale oznacza to tylko, że inne kontrakty na blockchainie nie mogą ich odczytać. Na blockchainie nie ma sekretów, oprogramowanie na każdym węźle ma stan każdego kontraktu w każdym bloku. Zgodnie z konwencją zmienne stanu nazywane są _<coś>.
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 tym wyjątkiem, że klucze są wartościami numerycznymi. Pamięć jest przydzielana tylko dla wpisów, które mają wartości różne od domyślnej (zero).
1 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[<adres>].
1 mapping (address => mapping (address => uint256)) private _allowances;Ta zmienna, _allowances, przechowuje wyjaśnione wcześniej uprawnienia. Pierwszy indeks to właściciel tokenów, a drugi to kontrakt z przydziałem. Aby uzyskać dostęp do kwoty, jaką adres A może wydać z konta adresu B, użyj _allowances[B][A].
1 uint256 private _totalSupply;Jak sama nazwa wskazuje, ta zmienna śledzi całkowitą podaż tokenów.
1 string private _name;2 string private _symbol;3 uint8 private _decimals;Te trzy zmienne są używane w celu poprawy czytelności. Dwie pierwsze 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ć kaczkę za część krowy.
Rozwiązaniem jest śledzenie liczb całkowitych, ale zamiast prawdziwego tokena liczenie tokena ułamkowego, który jest prawie bezwartościowy. W przypadku etheru token ułamkowy 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 tokena. Jeśli użytkownik ma 3 141 000 000 000 000 000 wei, czy to 3,14 ETH? 31,41 ETH? 3141 ETH? W przypadku etheru zdefiniowano 10^18 wei na ETH, ale dla Twojego 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żywać tego samego standardu co ETH, użyj wartości 18.
Konstruktor
1 /**2 * @dev Ustawia wartości dla {name} i {symbol}, inicjalizuje {decimals} z domyślną wartością 18.3 *4 * Aby wybrać inną wartość dla {decimals}, użyj {_setupDecimals}.5 *6 * Wszystkie trzy z tych wartości są niezmienne: można je ustawić tylko raz podczas tworzenia.7 */8 constructor (string memory name_, string memory symbol_) public {9 // W Solidity ≥0.7.0, „public” jest domyślne i można je pominąć.10
11 _name = name_;12 _symbol = symbol_;13 _decimals = 18;14 }Konstruktor jest wywoływany przy pierwszym utworzeniu kontraktu. Zgodnie z konwencją parametry funkcji są nazywane <coś>_.
Funkcje interfejsu użytkownika
1 /**2 * @dev Zwraca nazwę tokena.3 */4 function name() public view returns (string memory) {5 return _name;6 }7
8 /**9 * @dev Zwraca symbol tokena, zazwyczaj krótszą wersję nazwy.10 */11 function symbol() public view returns (string memory) {12 return _symbol;13 }14
15 /**16 * @dev Zwraca liczbę miejsc po przecinku używaną do uzyskania jego reprezentacji przez użytkownika.17 * Na przykład, jeśli `decimals` wynosi `2`, saldo `505` tokenów powinno być wyświetlone użytkownikowi jako `5,05` (`505 / 10 ** 2`).18 *19 * Tokeny zazwyczaj wybierają wartość 18, naśladując związek między etherem a wei.20 * Jest to wartość, której używa {ERC20}, chyba że zostanie wywołana funkcja {_setupDecimals}.21 *22 * UWAGA: Informacje te są wykorzystywane wyłącznie do celów _wyświetlania_: w żaden sposób nie wpływają na arytmetykę kontraktu,23 * w tym {IERC20-balanceOf} i {IERC20-transfer}.24 */25 function decimals() public view returns (uint8) {26 return _decimals;27 }Te funkcje, name, symbol i decimals, pomagają interfejsom użytkownika dowiedzieć się o Twoim kontrakcie, aby mogły go poprawnie wyświetlać.
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 życia | Dostęp do kontraktu | Koszt gazu | |
|---|---|---|---|
| Pamięć | Wywołanie funkcji | Odczyt/Zapis | Dziesiątki lub setki (więcej dla wyższych lokalizacji) |
| Calldata | Wywołanie funkcji | Tylko do odczytu | Nie może być używany jako typ zwracany, tylko jako typ parametru funkcji |
| Przechowywanie | Do zmiany | Odczyt/Zapis | Wysoki (800 za odczyt, 20 tys. za zapis) |
W tym przypadku najlepszym wyborem jest memory.
Odczyt informacji o tokenie
Są to funkcje, które dostarczają informacji o tokenie, albo o całkowitej podaży, albo o saldzie konta.
1 /**2 * @dev Zobacz {IERC20-totalSupply}.3 */4 function totalSupply() public view override returns (uint256) {5 return _totalSupply;6 }Funkcja totalSupply zwraca całkowitą podaż tokenów.
1 /**2 * @dev Zobacz {IERC20-balanceOf}.3 */4 function balanceOf(address account) public view override returns (uint256) {5 return _balances[account];6 }Odczytaj saldo konta. Zauważ, że każdy może sprawdzić saldo konta każdego innego. Nie ma sensu próbować ukrywać tych informacji, ponieważ i tak są one dostępne w każdym węźle. Na blockchainie nie ma żadnych tajemnic.
Przelewanie tokenów
1 /**2 * @dev Zobacz {IERC20-transfer}.3 *4 * Wymagania:5 *6 * - `recipient` nie może być adresem zerowym.7 * - wywołujący musi mieć saldo co najmniej `amount`.8 */9 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {Funkcja transfer jest wywoływana w celu przesłania tokenów z konta nadawcy na inne. Zauważ, że chociaż zwraca ona wartość logiczną, to zawsze jest to prawda. Jeśli przelew się nie powiedzie, kontrakt cofa wywołanie.
1 _transfer(_msgSender(), recipient, amount);2 return true;3 }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 są nazywane _<coś>, tak samo jak zmienne stanu.
Zazwyczaj w Solidity używamy msg.sender dla nadawcy wiadomości. Jednakże psuje to OpenGSN (opens in a new tab). Jeśli chcemy zezwolić na transakcje bez etheru za pomocą naszego tokena, musimy użyć _msgSender(). Zwraca msg.sender dla normalnych transakcji, ale dla transakcji bez etheru zwraca oryginalnego sygnatariusza, a nie kontrakt, który przekazał wiadomość.
Funkcje przydziału
Są to funkcje, które implementują funkcjonalność przydziału: allowance, approve, transferFrom i _approve. Dodatkowo implementacja OpenZeppelin wykracza poza podstawowy standard, zawierając pewne funkcje poprawiające bezpieczeństwo: increaseAllowance i decreaseAllowance.
Funkcja przydziału
1 /**2 * @dev Zobacz {IERC20-allowance}.3 */4 function allowance(address owner, address spender) public view virtual override returns (uint256) {5 return _allowances[owner][spender];6 }Funkcja allowance pozwala każdemu sprawdzić dowolny przydział.
Funkcja approve
1 /**2 * @dev Zobacz {IERC20-approve}.3 *4 * Wymagania:5 *6 * - `spender` nie może być adresem zerowym.7 */8 function approve(address spender, uint256 amount) public virtual override returns (bool) {Ta funkcja jest wywoływana w celu utworzenia przydziału. Jest podobna do powyższej funkcji transfer:
- Funkcja po prostu wywołuje funkcję wewnętrzną (w tym przypadku
_approve), która wykonuje prawdziwą pracę. - Funkcja albo zwraca
true(jeśli się powiedzie), albo cofa (jeśli nie).
1 _approve(_msgSender(), spender, amount);2 return true;3 }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 być poddane audytowi bezpieczeństwa. W ten sposób mamy mniejsze szanse na popełnienie błędu.
Funkcja transferFrom
Jest to funkcja, którą wydający wywołuje, aby wydać przydział. Wymaga to dwóch operacji: przelania wydawanej kwoty i zmniejszenia o nią przydziału.
1 /**2 * @dev Zobacz {IERC20-transferFrom}.3 *4 * Emituje zdarzenie {Approval} wskazujące zaktualizowany przydział. Nie jest to wymagane przez EIP.5 * Zobacz notatkę na początku {ERC20}.6 *7 * Wymagania:8 *9 * - `sender` i `recipient` nie mogą być adresem zerowym.10 * - `sender` musi mieć saldo co najmniej `amount`.11 * - wywołujący musi mieć przydział na tokeny ``sender`` w wysokości co najmniej12 * `amount`.13 */14 function transferFrom(address sender, address recipient, uint256 amount) public virtual15 override returns (bool) {16 _transfer(sender, recipient, amount);
Wywołanie funkcji a.sub(b, "message") robi dwie rzeczy. Po pierwsze, oblicza a-b, co jest nowym przydziałem.
Po drugie, sprawdza, czy ten wynik nie jest ujemny. Jeśli jest ujemny, wywołanie cofa się z podaną wiadomością. Zauważ, że gdy wywołanie jest cofane, wszelkie przetwarzanie wykonane wcześniej podczas tego wywołania jest ignorowane, więc nie musimy cofać _transfer.
1 _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,2 "ERC20: kwota transferu przekracza przydział"));3 return true;4 }Dodatki bezpieczeństwa OpenZeppelin
Niebezpieczne jest ustawianie niezerowego przydziału na inną niezerową wartość, ponieważ kontrolujesz tylko kolejność własnych transakcji, a nie cudzych. Wyobraź sobie, że masz dwóch użytkowników, naiwną Alicję i nieuczciwego Billa. Alicja chce skorzystać z usługi Billa, która jej zdaniem kosztuje pięć tokenów – więc daje Billowi przydział w wysokości pięciu tokenów.
Następnie coś się zmienia i cena Billa wzrasta do dziesięciu tokenów. Alicja, która nadal chce skorzystać z usługi, wysyła transakcję, która ustala przydział dla Billa na dziesięć. W momencie, gdy Bill zobaczy tę nową transakcję w puli transakcji, wysyła transakcję, która wydaje pięć tokenów Alicji i ma znacznie wyższą cenę gazu, dzięki czemu zostanie szybciej wykopana. W ten sposób Bill może najpierw wydać pięć tokenów, a następnie, gdy nowe zezwolenie Alicji zostanie wydobyte, wydać kolejne dziesięć, co daje łączną cenę piętnastu tokenów, czyli więcej niż chciała autoryzować Alicja. Ta technika nazywa się front-runningiem (opens in a new tab)
| Transakcja Alicji | Nonce Alicji | Transakcja Billa | Nonce Billa | Zezwolenie Billa | Całkowity dochód Billa od Alicji |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
| approve(Bill, 10) | 11 | 10 | 5 | ||
| transferFrom(Alice, Bill, 10) | 10,124 | 0 | 15 |
Aby uniknąć tego problemu, te dwie funkcje (increaseAllowance i decreaseAllowance) pozwalają
na modyfikację zezwolenia o określoną kwotę. Więc jeśli Bill wydał już pięć tokenów, będzie mógł
wydać tylko pięć więcej. W zależności od czasu, istnieją dwa sposoby, na które może to zadziałać, oba
z których kończą się tym, że Bill dostaje tylko dziesięć tokenów:
A:
| Transakcja Alicji | Nonce Alicji | Transakcja Billa | Nonce Billa | Zezwolenie Billa | Całkowity dochód Billa od Alicji |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
| increaseAllowance(Bill, 5) | 11 | 0+5 = 5 | 5 | ||
| transferFrom(Alice, Bill, 5) | 10,124 | 0 | 10 |
B:
| Transakcja Alicji | Nonce Alicji | Transakcja Billa | Nonce Billa | Zezwolenie Billa | Całkowity dochód Billa od Alicji |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| increaseAllowance(Bill, 5) | 11 | 5+5 = 10 | 0 | ||
| transferFrom(Alice, Bill, 10) | 10,124 | 0 | 10 |
1 /**2 * @dev Atomowo zwiększa zezwolenie udzielone `spenderowi` przez wywołującego.3 *4 * Jest to alternatywa dla {approve}, która może być używana jako środek zaradczy na5 * problemy opisane w {IERC20-approve}.6 *7 * Emituje zdarzenie {Approval} wskazujące zaktualizowane zezwolenie.8 *9 * Wymagania:10 *11 * - `spender` nie może być adresem zerowym.12 */13 function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {14 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue));15 return true;16 }Funkcja a.add(b) to bezpieczne dodawanie. W mało prawdopodobnym przypadku, gdy a+b>=2^256, nie zawija się ono
w sposób, w jaki robi to normalne dodawanie.
1
2 /**3 * @dev Atomowo zmniejsza zezwolenie udzielone `spenderowi` przez wywołującego.4 *5 * Jest to alternatywa dla {approve}, która może być używana jako środek zaradczy na6 * problemy opisane w {IERC20-approve}.7 *8 * Emituje zdarzenie {Approval} wskazujące zaktualizowane zezwolenie.9 *10 * Wymagania:11 *12 * - `spender` nie może być adresem zerowym.13 * - `spender` musi mieć zezwolenie dla wywołującego w wysokości co najmniej14 * `subtractedValue`.15 */16 function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {17 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue,18 "ERC20: decreased allowance below zero"));19 return true;20 }Funkcje modyfikujące informacje o tokenie
Są to cztery funkcje, które wykonują rzeczywistą pracę: _transfer, _mint, _burn i _approve.
Funkcja _transfer
1 /**2 * @dev Przenosi tokeny o wartości `amount` z `nadawcy` do `odbiorcy`.3 *4 * Ta wewnętrzna funkcja jest odpowiednikiem {transfer} i może być używana do5 * np. implementacji automatycznych opłat za tokeny, mechanizmów cięcia itp.6 *7 * Emituje zdarzenie {Transfer}.8 *9 * Wymagania:10 *11 * - `nadawca` nie może być adresem zerowym.12 * - `odbiorca` nie może być adresem zerowym.13 * - `nadawca` musi mieć saldo co najmniej `amount`.14 */15 function _transfer(address sender, address recipient, uint256 amount) internal virtual {Ta funkcja, _transfer, transferuje tokeny z jednego konta na drugie. Jest ona wywoływana zarówno przez
transfer (dla transferów z własnego konta nadawcy), jak i transferFrom (dla używania zezwoleń
do transferu z konta kogoś innego).
1 require(sender != address(0), "ERC20: transfer from the zero address");2 require(recipient != address(0), "ERC20: transfer to the zero address");Nikt w rzeczywistości nie posiada adresu zerowego w Ethereum (tzn. nikt nie zna klucza prywatnego, którego pasujący klucz publiczny jest przekształcany na adres zerowy). Gdy ludzie używają tego adresu, jest to zwykle błąd oprogramowania - więc kończymy niepowodzeniem, jeśli adres zerowy jest używany jako nadawca lub odbiorca.
1 _beforeTokenTransfer(sender, recipient, amount);2
Istnieją dwa sposoby wykorzystania tego kontraktu:
- Użyj go jako szablonu dla własnego kodu
- Dziedzicz po nim (opens in a new tab) i nadpisuj tylko te funkcje, które musisz zmodyfikować
Druga metoda jest znacznie lepsza, ponieważ kod OpenZeppelin ERC-20 został już poddany audytowi i wykazano, że jest bezpieczny. Gdy używasz dziedziczenia, jasne jest, jakie funkcje modyfikujesz, a aby zaufać twojemu kontraktowi, ludzie muszą tylko poddać audytowi te konkretne funkcje.
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żliwe jest
napisanie jej w sposób niebezpieczny (patrz poniżej), więc najlepiej jej nie nadpisywać. Rozwiązaniem jest _beforeTokenTransfer,
funkcja typu hook (opens in a new tab). Możesz nadpisać tę funkcję, a będzie ona wywoływana przy każdym transferze.
1 _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");2 _balances[recipient] = _balances[recipient].add(amount);To są linie, które faktycznie wykonują transfer. Zauważ, że nic nie ma między nimi, i że odejmujemy przekazaną kwotę od nadawcy przed dodaniem jej do odbiorcy. Jest to ważne, ponieważ gdyby w środku było wywołanie innego kontraktu, mogłoby to zostać wykorzystane do oszukania tego kontraktu. W ten sposób transfer jest atomowy, nic nie może się zdarzyć w jego środku.
1 emit Transfer(sender, recipient, amount);2 }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 na nie reagować. Na przykład portfel może śledzić, kiedy właściciel dostaje 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 emitować nowe tokeny lub spalać 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, gdy zostanie opłacony, i prawdopodobnie wywoła _burn w pewnym momencie,
aby uniknąć niekontrolowanej inflacji.
1 /** @dev Tworzy `amount` tokenów i przypisuje je do `account`, zwiększając2 * całkowitą podaż.3 *4 * Emituje zdarzenie {Transfer} z `from` ustawionym na adres zerowy.5 *6 * Wymagania:7 *8 * - `to` nie może być adresem zerowym.9 */10 function _mint(address account, uint256 amount) internal virtual {11 require(account != address(0), "ERC20: mint to the zero address");12 _beforeTokenTransfer(address(0), account, amount);13 _totalSupply = _totalSupply.add(amount);14 _balances[account] = _balances[account].add(amount);15 emit Transfer(address(0), account, amount);16 }Pamiętaj o aktualizacji _totalSupply, gdy zmieni się całkowita liczba tokenów.
1 /**2 * @dev Niszczy tokeny o `wartości` `amount` z `konta`, zmniejszając3 * całkowitą podaż.4 *5 * Emituje zdarzenie {Transfer} z `to` ustawionym na adres zerowy.6 *7 * Wymagania:8 *9 * - `konto` nie może być adresem zerowym.10 * - `konto` musi mieć co najmniej `amount` tokenów.11 */12 function _burn(address account, uint256 amount) internal virtual {13 require(account != address(0), "ERC20: burn from the zero address");14
15 _beforeTokenTransfer(account, address(0), amount);16
17 _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance");18 _totalSupply = _totalSupply.sub(amount);19 emit Transfer(account, address(0), amount);20 }Funkcja _burn jest prawie identyczna z _mint, z tym że działa w przeciwnym kierunku.
Funkcja _approve
To jest funkcja, która faktycznie określa zezwolenia. Zauważ, że pozwala ona właścicielowi określić zezwolenie, które jest wyższe niż bieżące saldo właściciela. Jest to w porządku, ponieważ saldo jest sprawdzane w momencie transferu, kiedy może być inne niż saldo w momencie tworzenia zezwolenia .
1 /**2 * @dev Ustawia `amount` jako zezwolenie `spendera` na tokeny `właściciela`.3 *4 * Ta wewnętrzna funkcja jest odpowiednikiem `approve` i może być używana do5 * np. ustawiania automatycznych zezwoleń dla niektórych podsystemów itp.6 *7 * Emituje zdarzenie {Approval}.8 *9 * Wymagania:10 *11 * - `właściciel` nie może być adresem zerowym.12 * - `spender` nie może być adresem zerowym.13 */14 function _approve(address owner, address spender, uint256 amount) internal virtual {15 require(owner != address(0), "ERC20: approve from the zero address");16 require(spender != address(0), "ERC20: approve to the zero address");17
18 _allowances[owner][spender] = amount;
Emituj zdarzenie Approval. W zależności od tego, jak napisana jest aplikacja, kontrakt wydającego może być poinformowany o
zatwierdzeniu przez właściciela lub przez serwer, który nasłuchuje tych zdarzeń.
1 emit Approval(owner, spender, amount);2 }3
Modyfikacja zmiennej Decimals
1
2
3 /**4 * @dev Ustawia {decimals} na wartość inną niż domyślna 18.5 *6 * OSTRZEŻENIE: Ta funkcja powinna być wywoływana tylko z konstruktora. Większość7 * aplikacji, które wchodzą w interakcję z kontraktami tokenów, nie spodziewa się,8 * że {decimals} kiedykolwiek się zmieni i może działać niepoprawnie, jeśli tak się stanie.9 */10 function _setupDecimals(uint8 decimals_) internal {11 _decimals = decimals_;12 }Ta funkcja modyfikuje zmienną _decimals, która służy do informowania interfejsów użytkownika, jak interpretować kwotę.
Powinieneś ją wywołać z konstruktora. Byłoby nieuczciwe wywoływać ją w dowolnym późniejszym momencie, a aplikacje
nie są zaprojektowane do obsługi tego.
Hooki
1
2 /**3 * @dev Hook, który jest wywoływany przed każdym transferem tokenów. Obejmuje to4 * minting i burning.5 *6 * Warunki wywołania:7 *8 * - gdy `from` i `to` są oba niezerowe, `amount` tokenów ``from``9 * zostanie przetransferowane do `to`.10 * - gdy `from` jest zerem, `amount` tokenów zostanie wyemitowane dla `to`.11 * - gdy `to` jest zerem, `amount` tokenów ``from`` zostanie spalonych.12 * - `from` i `to` nigdy nie są oba zerami.13 *14 * Aby dowiedzieć się więcej o hookach, przejdź do xref:ROOT:extending-contracts.adoc#using-hooks[Używanie hooków].15 */16 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }17}To jest funkcja hook, która ma być wywoływana podczas transferów. Jest tutaj pusta, ale jeśli potrzebujesz, aby coś robiła, po prostu ją nadpisujesz.
Wnioski
Dla przypomnienia, oto niektóre z najważniejszych pomysłów w tym kontrakcie (moim zdaniem, twoje prawdopodobnie będą się różnić):
- W 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 odbywają się transakcje innych osób . To jest powód, dla którego zmiana zezwolenia może być niebezpieczna, ponieważ pozwala wydającemu wydać sumę obu zezwoleń.
- Wartości typu
uint256zawijają 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 robi to za Ciebie). Zauważ, że to się zmieniło w Solidity 0.8.0 (opens in a new tab). - Wykonuj wszystkie zmiany stanu określonego typu w określonym miejscu, ponieważ ułatwia to audyt.
To jest powód, dla którego mamy, na przykład,
_approve, które jest wywoływane przezapprove,transferFrom,increaseAllowanceidecreaseAllowance - Zmiany stanu powinny być atomowe, bez żadnych innych działań w ich środku (jak widać
w
_transfer). Dzieje się tak, ponieważ podczas zmiany stanu masz niespójny stan. Na przykład, między czasem, w którym odejmujesz od salda nadawcy, a czasem, w którym dodajesz do salda odbiorcy, istnieje mniej tokenów, niż powinno być. Może to być potencjalnie nadużywane, jeśli pomiędzy nimi są operacje, zwłaszcza wywołania innego kontraktu.
Teraz, gdy zobaczyłeś, jak napisany jest kontrakt OpenZeppelin ERC-20, a zwłaszcza jak jest on uczyniony bardziej bezpiecznym, idź i napisz własne bezpieczne kontrakty i aplikacje.
Zobacz więcej mojej pracy tutaj (opens in a new tab).
Strona ostatnio zaktualizowana: 3 marca 2026
