Przejdź do głównej zawartości

ERC-20 z zabezpieczeniami

erc-20
Początkujący
Ori Pomerantz
15 sierpnia 2022
8 minuta czytania

Wprowadzenie

Jedną z wielkich zalet Ethereum jest to, że nie ma centralnego organu, który mógłby modyfikować lub cofać Twoje transakcje. Jednym z wielkich problemów z Ethereum jest to, że nie ma centralnego organu z uprawnieniami do cofania błędów użytkowników lub nielegalnych transakcji. W tym artykule dowiesz się o niektórych typowych błędach popełnianych przez użytkowników z tokenami ERC-20, a także o tym, jak tworzyć kontrakty ERC-20, które pomagają użytkownikom unikać tych błędów lub które dają centralnemu organowi pewne uprawnienia (na przykład do zamrażania kont).

Pamiętaj, że chociaż będziemy używać kontraktu tokena ERC-20 OpenZeppelin (opens in a new tab), ten artykuł nie wyjaśnia go szczegółowo. Możesz znaleźć te informacje tutaj.

Jeśli chcesz zobaczyć pełny kod źródłowy:

  1. Otwórz Remix IDE (opens in a new tab).
  2. Kliknij ikonę klonowania z GitHuba (ikona klonowania z GitHuba).
  3. Sklonuj repozytorium z GitHuba https://github.com/qbzzt/20220815-erc20-safety-rails.
  4. Otwórz contracts > erc20-safety-rails.sol.

Tworzenie kontraktu ERC-20

Zanim dodamy funkcjonalność zabezpieczeń, potrzebujemy kontraktu ERC-20. W tym artykule użyjemy kreatora kontraktów OpenZeppelin (opens in a new tab). Otwórz go w innej przeglądarce i postępuj zgodnie z poniższymi instrukcjami:

  1. Wybierz ERC20.

  2. Wprowadź te ustawienia:

    ParametrWartość
    NazwaSafetyRailsToken
    SymbolSAFE
    Premint1000
    FunkcjeBrak
    Kontrola dostępuOwnable
    Możliwość aktualizacjiBrak
  3. Przewiń w górę i kliknij Otwórz w Remix (dla Remix) lub Pobierz, aby użyć innego środowiska. Zakładam, że używasz Remix, jeśli używasz czegoś innego, po prostu wprowadź odpowiednie zmiany.

  4. Mamy teraz w pełni funkcjonalny kontrakt ERC-20. Możesz rozwinąć .deps > npm, aby zobaczyć zaimportowany kod.

  5. Skompiluj, wdróż i pobaw się kontraktem, aby zobaczyć, że działa on jak kontrakt ERC-20. Jeśli chcesz dowiedzieć się, jak używać Remix, skorzystaj z tego samouczka (opens in a new tab).

Częste błędy

Błędy

Użytkownicy czasami wysyłają tokeny na zły adres. Chociaż nie możemy czytać w ich myślach, aby wiedzieć, co zamierzali zrobić, istnieją dwa rodzaje błędów, które zdarzają się często i są łatwe do wykrycia:

  1. Wysyłanie tokenów na własny adres kontraktu. Na przykład, token OP Optimism (opens in a new tab) zdołał zgromadzić ponad 120 000 (opens in a new tab) tokenów OP w mniej niż dwa miesiące. Reprezentuje to znaczną ilość bogactwa, które ludzie przypuszczalnie po prostu stracili.

  2. Wysyłanie tokenów na pusty adres, taki, który nie odpowiada kontu zewnętrznemu (EOA) lub inteligentnemu kontraktowi. Chociaż nie mam statystyk dotyczących tego, jak często to się zdarza, jeden incydent mógł kosztować 20 000 000 tokenów (opens in a new tab).

Zapobieganie transferom

Kontrakt ERC-20 OpenZeppelin zawiera hak (hook), _beforeTokenTransfer (opens in a new tab), który jest wywoływany przed transferem tokena. Domyślnie ten hak nic nie robi, ale możemy podpiąć do niego naszą własną funkcjonalność, taką jak kontrole, które cofają transakcję w razie problemu.

Aby użyć tego haka, dodaj tę funkcję po konstruktorze:

1 function _beforeTokenTransfer(address from, address to, uint256 amount)
2 internal virtual
3 override(ERC20)
4 {
5 super._beforeTokenTransfer(from, to, amount);
6 }

Niektóre części tej funkcji mogą być nowe, jeśli nie znasz dobrze języka Solidity:

1 internal virtual

Słowo kluczowe virtual oznacza, że tak jak odziedziczyliśmy funkcjonalność z ERC20 i nadpisaliśmy tę funkcję, inne kontrakty mogą dziedziczyć po nas i nadpisywać tę funkcję.

1 override(ERC20)

Musimy jawnie określić, że nadpisujemy (opens in a new tab) definicję _beforeTokenTransfer z tokena ERC20. Ogólnie rzecz biorąc, jawne definicje są znacznie lepsze z punktu widzenia bezpieczeństwa niż te niejawne – nie możesz zapomnieć, że coś zrobiłeś, jeśli masz to tuż przed oczami. To jest również powód, dla którego musimy określić, którą _beforeTokenTransfer z klasy nadrzędnej nadpisujemy.

1 super._beforeTokenTransfer(from, to, amount);

Ta linia wywołuje funkcję _beforeTokenTransfer kontraktu lub kontraktów, z których dziedziczyliśmy i które ją posiadają. W tym przypadku jest to tylko ERC20, Ownable nie ma tego haka. Mimo że obecnie ERC20._beforeTokenTransfer nic nie robi, wywołujemy ją na wypadek, gdyby w przyszłości została dodana jakaś funkcjonalność (i zdecydujemy się wtedy na ponowne wdrożenie kontraktu, ponieważ kontrakty nie zmieniają się po wdrożeniu).

Kodowanie wymagań

Chcemy dodać do funkcji następujące wymagania:

  • Adres to nie może być równy address(this), czyli adresowi samego kontraktu ERC-20.
  • Adres to nie może być pusty, musi być to:
    • Konto zewnętrzne (EOA). Nie możemy bezpośrednio sprawdzić, czy adres jest EOA, ale możemy sprawdzić saldo ETH adresu. Konta EOA prawie zawsze mają jakieś saldo, nawet jeśli nie są już używane – trudno jest je wyczyścić do ostatniego wei.
    • Inteligentny kontrakt. Sprawdzanie, czy adres jest inteligentnym kontraktem, jest nieco trudniejsze. Istnieje kod operacyjny, który sprawdza długość kodu zewnętrznego, o nazwie EXTCODESIZE (opens in a new tab), ale nie jest on dostępny bezpośrednio w Solidity. Musimy do tego użyć Yul (opens in a new tab), który jest asemblerem EVM. Istnieją inne wartości, których moglibyśmy użyć z Solidity (<address>.code i <address>.codehash (opens in a new tab)), ale kosztują więcej.

Przeanalizujmy nowy kod linia po linii:

1 require(to != address(this), "Nie można wysyłać tokenów na adres kontraktu");

To jest pierwsze wymaganie, sprawdzić, czy to i this(address) nie są tym samym.

1 bool isToContract;
2 assembly {
3 isToContract := gt(extcodesize(to), 0)
4 }

W ten sposób sprawdzamy, czy adres jest kontraktem. Nie możemy otrzymać wyniku bezpośrednio z Yul, więc zamiast tego definiujemy zmienną do przechowywania wyniku (w tym przypadku isToContract). Yul działa w ten sposób, że każdy kod operacyjny jest traktowany jako funkcja. Najpierw więc wywołujemy EXTCODESIZE (opens in a new tab), aby uzyskać rozmiar kontraktu, a następnie używamy GT (opens in a new tab), aby sprawdzić, czy nie jest on zerowy (mamy do czynienia z liczbami całkowitymi bez znaku, więc oczywiście nie może być ujemny). Następnie zapisujemy wynik do isToContract.

1 require(to.balance != 0 || isToContract, "Nie można wysyłać tokenów na pusty adres");

I na koniec mamy faktyczne sprawdzanie pustych adresów.

Dostęp administracyjny

Czasami przydatne jest posiadanie administratora, który może cofać błędy. Aby zmniejszyć potencjał nadużyć, ten administrator może być multisigiem (opens in a new tab), dzięki czemu wiele osób musi zgodzić się na daną akcję. W tym artykule omówimy dwie funkcje administracyjne:

  1. Zamrażanie i odmrażanie kont. Może to być przydatne, na przykład, gdy konto może być zagrożone.

  2. Czyszczenie aktywów.

    Czasami oszuści wysyłają fałszywe tokeny do kontraktu prawdziwego tokena, aby zyskać na wiarygodności. Na przykład, zobacz tutaj (opens in a new tab). Prawdziwy kontrakt ERC-20 to 0x4200....0042 (opens in a new tab). Oszustwo, które go udaje, to 0x234....bbe (opens in a new tab).

    Możliwe jest również, że ludzie przez pomyłkę wyślą prawdziwe tokeny ERC-20 do naszego kontraktu, co jest kolejnym powodem, dla którego warto mieć sposób na ich wyciągnięcie.

OpenZeppelin dostarcza dwa mechanizmy umożliwiające dostęp administracyjny:

Dla uproszczenia, w tym artykule używamy Ownable.

Zamrażanie i rozmrażanie kontraktów

Zamrażanie i rozmrażanie kontraktów wymaga kilku zmian:

  • Mapowanie (opens in a new tab) adresów na wartości logiczne (opens in a new tab), aby śledzić, które adresy są zamrożone. Wszystkie wartości są początkowo zerowe, co dla wartości logicznych jest interpretowane jako fałsz (false). Jest to pożądane, ponieważ domyślnie konta nie są zamrożone.

    1 mapping(address => bool) public frozenAccounts;
  • Zdarzenia (opens in a new tab), aby informować wszystkich zainteresowanych, gdy konto zostanie zamrożone lub rozmrożone. Technicznie rzecz biorąc, zdarzenia nie są wymagane do tych działań, ale pomaga to kodowi offchain nasłuchiwać tych zdarzeń i wiedzieć, co się dzieje. Uważa się za dobry zwyczaj, aby inteligentny kontrakt je emitował, gdy dzieje się coś, co może być istotne dla kogoś innego.

    Zdarzenia są indeksowane, więc będzie można wyszukać wszystkie przypadki zamrożenia lub rozmrożenia konta.

    1 // Gdy konta są zamrażane lub odmrażane
    2 event AccountFrozen(address indexed _addr);
    3 event AccountThawed(address indexed _addr);
  • Funkcje do zamrażania i rozmrażania kont. Te dwie funkcje są prawie identyczne, więc omówimy tylko funkcję zamrażania.

    1 function freezeAccount(address addr)
    2 public
    3 onlyOwner

    Funkcje oznaczone jako public (opens in a new tab) mogą być wywoływane z innych inteligentnych kontraktów lub bezpośrednio przez transakcję.

    1 {
    2 require(!frozenAccounts[addr], "Konto jest już zamrożone");
    3 frozenAccounts[addr] = true;
    4 emit AccountFrozen(addr);
    5 } // freezeAccount

    Jeśli konto jest już zamrożone, cofnij transakcję. W przeciwnym razie zamroź je i emituj zdarzenie.

  • Zmień _beforeTokenTransfer, aby uniemożliwić przenoszenie pieniędzy z zamrożonego konta. Pamiętaj, że pieniądze nadal mogą być przelewane na zamrożone konto.

    1 require(!frozenAccounts[from], "Konto jest zamrożone");

Czyszczenie aktywów

Aby uwolnić tokeny ERC-20 przechowywane przez ten kontrakt, musimy wywołać funkcję na kontrakcie tokena, do którego należą, albo transfer (opens in a new tab), albo approve (opens in a new tab). W tym przypadku nie ma sensu marnować gazu na przydziały (allowances), możemy równie dobrze przenieść je bezpośrednio.

1 function cleanupERC20(
2 address erc20,
3 address dest
4 )
5 public
6 onlyOwner
7 {
8 IERC20 token = IERC20(erc20);

To jest składnia do tworzenia obiektu dla kontraktu, gdy otrzymamy jego adres. Możemy to zrobić, ponieważ mamy definicję tokenów ERC20 jako część kodu źródłowego (patrz linia 4), a ten plik zawiera definicję dla IERC20 (opens in a new tab), interfejs dla kontraktu ERC-20 OpenZeppelin.

1 uint balance = token.balanceOf(address(this));
2 token.transfer(dest, balance);
3 }

To jest funkcja czyszcząca, więc przypuszczalnie nie chcemy zostawiać żadnych tokenów. Zamiast ręcznego pobierania salda od użytkownika, możemy równie dobrze zautomatyzować ten proces.

Wnioski

To nie jest idealne rozwiązanie – nie ma idealnego rozwiązania problemu „użytkownik popełnił błąd”. Jednak używanie tego rodzaju kontroli może przynajmniej zapobiec niektórym błędom. Możliwość zamrażania kont, chociaż niebezpieczna, może być użyta do ograniczenia szkód niektórych hacków poprzez uniemożliwienie hakerowi dostępu do skradzionych środków.

Zobacz więcej mojej pracy tutaj (opens in a new tab).

Strona ostatnio zaktualizowana: 3 marca 2026

Czy ten samouczek był pomocny?