Přejít na hlavní obsah

ERC-20 s bezpečnostními pojistkami

erc-20
Začátečník
Ori Pomerantz
15. srpna 2022
8 minut čtení

Úvod

Jednou ze skvělých věcí na Ethereu je, že neexistuje žádná centrální autorita, která by mohla upravovat nebo rušit vaše transakce. Jedním z velkých problémů Etherea je, že neexistuje žádná centrální autorita s pravomocí zvrátit chyby uživatelů nebo nezákonné transakce. V tomto článku se dozvíte o některých běžných chybách, kterých se uživatelé dopouštějí u ERC-20 tokenů, a také o tom, jak vytvořit ERC-20 kontrakty, které uživatelům pomohou se těmto chybám vyhnout, nebo které dají centrální autoritě určitou pravomoc (například zmrazit účty).

Vezměte prosím na vědomí, že ačkoli budeme používat ERC-20 token kontrakt od OpenZeppelin (opens in a new tab), tento článek jej podrobně nevysvětluje. Tyto informace najdete zde.

Pokud chcete vidět kompletní zdrojový kód:

  1. Otevřete Remix IDE (opens in a new tab).
  2. Klikněte na ikonu klonování z GitHubu (clone github icon).
  3. Naklonujte repozitář na GitHubu https://github.com/qbzzt/20220815-erc20-safety-rails.
  4. Otevřete contracts > erc20-safety-rails.sol.

Vytvoření ERC-20 kontraktu

Než budeme moci přidat funkci bezpečnostních pojistek, potřebujeme ERC-20 kontrakt. V tomto článku použijeme OpenZeppelin Contracts Wizard (opens in a new tab). Otevřete jej v jiném prohlížeči a postupujte podle těchto pokynů:

  1. Vyberte ERC20.

  2. Zadejte tato nastavení:

    ParametrHodnota
    NázevSafetyRailsToken
    SymbolSAFE
    Premint1000
    FunkceŽádné
    Řízení přístupuOwnable
    Možnost aktualizaceŽádná
  3. Přejděte nahoru a klikněte na Open in Remix (pro Remix) nebo Download, pokud chcete použít jiné prostředí. Budu předpokládat, že používáte Remix, pokud používáte něco jiného, jednoduše proveďte příslušné změny.

  4. Nyní máme plně funkční ERC-20 kontrakt. Můžete rozbalit .deps > npm a podívat se na importovaný kód.

  5. Zkompilujte, nasaďte a vyzkoušejte si kontrakt, abyste viděli, že funguje jako ERC-20 kontrakt. Pokud se potřebujete naučit používat Remix, použijte tento tutoriál (opens in a new tab).

Běžné chyby

Chyby

Uživatelé někdy posílají tokeny na špatnou adresu. I když neumíme číst jejich myšlenky, abychom věděli, co měli v úmyslu udělat, existují dva typy chyb, které se stávají často a lze je snadno odhalit:

  1. Odeslání tokenů na vlastní adresu kontraktu. Například OP token sítě Optimism (opens in a new tab) dokázal nashromáždit více než 120 000 (opens in a new tab) OP tokenů za méně než dva měsíce. To představuje značné množství bohatství, o které lidé pravděpodobně prostě přišli.

  2. Odeslání tokenů na prázdnou adresu, která neodpovídá externě vlastněnému účtu ani chytrému kontraktu. Ačkoli nemám statistiky o tom, jak často se to stává, jeden incident mohl stát 20 000 000 tokenů (opens in a new tab).

Zabránění převodům

ERC-20 kontrakt od OpenZeppelin obsahuje hook (háček), _beforeTokenTransfer (opens in a new tab), který se volá před převodem tokenu. Ve výchozím nastavení tento hook nedělá nic, ale můžeme na něj navázat vlastní funkcionalitu, jako jsou kontroly, které transakci zvrátí, pokud nastane problém.

Chcete-li hook použít, přidejte tuto funkci za konstruktor:

    function _beforeTokenTransfer(address from, address to, uint256 amount)
        internal virtual
        override(ERC20)
    {
        super._beforeTokenTransfer(from, to, amount);
    }

Některé části této funkce pro vás mohou být nové, pokud nejste příliš obeznámeni se Solidity:

        internal virtual

Klíčové slovo virtual znamená, že stejně jako jsme zdědili funkcionalitu z ERC20 a přepsali tuto funkci, mohou z nás dědit další kontrakty a tuto funkci přepsat.

        override(ERC20)

Musíme explicitně specifikovat, že přepisujeme (opens in a new tab) definici _beforeTokenTransfer z ERC20 tokenu. Obecně platí, že explicitní definice jsou z hlediska bezpečnosti mnohem lepší než ty implicitní – nemůžete zapomenout, že jste něco udělali, když to máte přímo před sebou. To je také důvod, proč musíme specifikovat, ze které nadtřídy _beforeTokenTransfer přepisujeme.

        super._beforeTokenTransfer(from, to, amount);

Tento řádek volá funkci _beforeTokenTransfer kontraktu nebo kontraktů, ze kterých jsme dědili a které ji mají. V tomto případě je to pouze ERC20, Ownable tento hook nemá. I když v současné době ERC20._beforeTokenTransfer nedělá nic, voláme jej pro případ, že by byla v budoucnu přidána nějaká funkcionalita (a my se pak rozhodneme kontrakt znovu nasadit, protože kontrakty se po nasazení nemění).

Kódování požadavků

Do funkce chceme přidat tyto požadavky:

  • Adresa to se nesmí rovnat address(this), což je adresa samotného ERC-20 kontraktu.
  • Adresa to nesmí být prázdná, musí to být buď:
    • Externě vlastněný účet (EOA). Nemůžeme přímo zkontrolovat, zda je adresa EOA, ale můžeme zkontrolovat zůstatek ETH na adrese. EOA mají téměř vždy nějaký zůstatek, i když se již nepoužívají – je obtížné je vyprázdnit do posledního Wei.
    • Chytrý kontrakt. Testování, zda je adresa chytrý kontrakt, je o něco těžší. Existuje operační kód, který kontroluje délku externího kódu, nazvaný EXTCODESIZE (opens in a new tab), ale není dostupný přímo v Solidity. Musíme pro něj použít Yul (opens in a new tab), což je assembly pro EVM. Existují i další hodnoty, které bychom mohli použít ze Solidity (<address>.code a <address>.codehash (opens in a new tab)), ale stojí více gasu.

Pojďme si projít nový kód řádek po řádku:

        require(to != address(this), "Can't send tokens to the contract address");

Toto je první požadavek, zkontrolujte, že to a this(address) nejsou to samé.

        bool isToContract;
        assembly {
           isToContract := gt(extcodesize(to), 0)
        }

Takto kontrolujeme, zda je adresa kontrakt. Nemůžeme přijímat výstup přímo z Yul, takže místo toho definujeme proměnnou, která bude uchovávat výsledek (v tomto případě isToContract). Yul funguje tak, že každý operační kód je považován za funkci. Takže nejprve zavoláme EXTCODESIZE (opens in a new tab), abychom získali velikost kontraktu, a poté použijeme GT (opens in a new tab) ke kontrole, že není nula (pracujeme s celými čísly bez znaménka, takže samozřejmě nemůže být záporná). Výsledek pak zapíšeme do isToContract.

        require(to.balance != 0 || isToContract, "Can't send tokens to an empty address");

A nakonec tu máme samotnou kontrolu prázdných adres.

Administrativní přístup

Někdy je užitečné mít administrátora, který může zvrátit chyby. Aby se snížilo riziko zneužití, může být tímto administrátorem multisig (opens in a new tab), takže se na akci musí shodnout více lidí. V tomto článku budeme mít dvě administrativní funkce:

  1. Zmrazení a rozmrazení účtů. To může být užitečné například tehdy, když by mohl být účet kompromitován.

  2. Úklid aktiv.

    Podvodníci někdy posílají podvodné tokeny na kontrakt skutečného tokenu, aby získali legitimitu. Například viz zde (opens in a new tab). Legitimní ERC-20 kontrakt je 0x4200....0042 (opens in a new tab). Podvod, který se za něj vydává, je 0x234....bbe (opens in a new tab).

    Je také možné, že lidé omylem pošlou legitimní ERC-20 tokeny na náš kontrakt, což je další důvod, proč chtít mít způsob, jak je dostat ven.

OpenZeppelin poskytuje dva mechanismy pro umožnění administrativního přístupu:

Pro zjednodušení v tomto článku používáme Ownable.

Zmrazení a rozmrazení kontraktů

Zmrazení a rozmrazení kontraktů vyžaduje několik změn:

  • Mapování (mapping) (opens in a new tab) z adres na booleovské hodnoty (opens in a new tab) pro sledování, které adresy jsou zmrazené. Všechny hodnoty jsou zpočátku nula, což se u booleovských hodnot interpretuje jako nepravda (false). To je to, co chceme, protože ve výchozím nastavení účty nejsou zmrazené.

        mapping(address => bool) public frozenAccounts;
    
  • Události (opens in a new tab), které informují každého zájemce o tom, kdy je účet zmrazen nebo rozmrazen. Technicky vzato nejsou události pro tyto akce vyžadovány, ale pomáhá to offchain kódu, aby mohl těmto událostem naslouchat a vědět, co se děje. Považuje se za dobré mravy, když je chytrý kontrakt emituje, když se stane něco, co by mohlo být relevantní pro někoho jiného.

    Události jsou indexovány, takže bude možné vyhledat všechny případy, kdy byl účet zmrazen nebo rozmrazen.

      // Když jsou účty zmrazeny nebo rozmrazeny
      event AccountFrozen(address indexed _addr);
      event AccountThawed(address indexed _addr);
    
  • Funkce pro zmrazení a rozmrazení účtů. Tyto dvě funkce jsou téměř identické, takže si projdeme pouze funkci pro zmrazení.

        function freezeAccount(address addr)
          public
          onlyOwner
    

    Funkce označené jako public (opens in a new tab) mohou být volány z jiných chytrých kontraktů nebo přímo transakcí.

      {
          require(!frozenAccounts[addr], "Account already frozen");
          frozenAccounts[addr] = true;
          emit AccountFrozen(addr);
      }  // freezeAccount
    

    Pokud je účet již zmrazen, zvrátit transakci. V opačném případě jej zmrazte a emit událost.

  • Změňte _beforeTokenTransfer tak, aby se zabránilo přesunu peněz ze zmrazeného účtu. Všimněte si, že peníze lze na zmrazený účet stále převádět.

         require(!frozenAccounts[from], "The account is frozen");
    

Úklid aktiv

K uvolnění ERC-20 tokenů držených tímto kontraktem musíme zavolat funkci na kontraktu tokenu, ke kterému patří, a to buď transfer (opens in a new tab) nebo approve (opens in a new tab). V tomto případě nemá smysl plýtvat gasem na povolenky, můžeme rovnou provést přímý převod.

    function cleanupERC20(
        address erc20,
        address dest
    )
        public
        onlyOwner
    {
        IERC20 token = IERC20(erc20);

Toto je syntaxe pro vytvoření objektu pro kontrakt, když obdržíme adresu. Můžeme to udělat, protože máme definici pro ERC20 tokeny jako součást zdrojového kódu (viz řádek 4), a tento soubor obsahuje definici pro IERC20 (opens in a new tab), rozhraní pro ERC-20 kontrakt od OpenZeppelin.

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

Toto je funkce pro úklid, takže pravděpodobně nechceme zanechat žádné tokeny. Místo toho, abychom získávali zůstatek od uživatele ručně, můžeme tento proces rovnou zautomatizovat.

Závěr

Toto není dokonalé řešení – neexistuje žádné dokonalé řešení problému „uživatel udělal chybu“. Použití těchto druhů kontrol však může alespoň některým chybám zabránit. Schopnost zmrazit účty, ačkoli je nebezpečná, může být použita k omezení škod způsobených určitými hacky tím, že hackerovi odepře přístup k ukradeným prostředkům.

Zde najdete další mou práci (opens in a new tab).