ERC-20 s bezpečnostními pojistkami
Úvod
Jednou ze skvělých věcí na Ethereu je to, že neexistuje žádná centrální autorita, která by mohla upravit nebo zvrátit vaše transakce. Jedním z velkých problémů Etherea je naopak to, že neexistuje žádná centrální autorita, která by měla pravomoc napravovat chyby uživatelů nebo nelegitimní 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 vytvářet ERC-20 kontrakty, které uživatelům pomáhají se těmto chybám vyhnout nebo které dávají centrální autoritě určitou pravomoc (například zmrazit účty).
Upozorňujeme, že ačkoli budeme používat kontrakt tokenu ERC-20 od OpenZeppelin (opens in a new tab), tento článek jej podrobně nevysvětluje. Tyto informace naleznete zde.
Pokud chcete vidět kompletní zdrojový kód:
- Otevřete Remix IDE (opens in a new tab).
- Klikněte na ikonu pro klonování z GitHubu (
). - Naklonujte repozitář z GitHubu:
https://github.com/qbzzt/20220815-erc20-safety-rails. - Otevřete contracts > erc20-safety-rails.sol.
Vytvoření ERC-20 kontraktu
Než budeme moci přidat funkcionalitu bezpečnostních pojistek, potřebujeme ERC-20 kontrakt. V tomto článku použijeme průvodce kontrakty od OpenZeppelin (opens in a new tab). Otevřete jej v jiném prohlížeči a postupujte podle těchto pokynů:
-
Vyberte ERC20.
-
Zadejte tato nastavení:
Parametr Hodnota Název SafetyRailsToken Symbol SAFE Premint 1000 Funkce Žádná Řízení přístupu Ownable Upgradovatelnost Žádná -
Přejděte nahoru a klikněte na Otevřít v Remixu (pro Remix) nebo Stáhnout pro použití jiného prostředí. Budu předpokládat, že používáte Remix. Pokud používáte něco jiného, proveďte příslušné změny.
-
Nyní máme plně funkční ERC-20 kontrakt. Importovaný kód uvidíte po rozbalení
.deps>npm. -
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. Ačkoli jim nevidíme do hlavy, abychom věděli, co měli v úmyslu, existují dva typy chyb, které se stávají často a dají se snadno odhalit:
-
Odeslání tokenů na vlastní adresu kontraktu. Například u tokenu OP od Optimism (opens in a new tab) se za necelé dva měsíce podařilo nashromáždit přes 120 000 (opens in a new tab) tokenů OP. To představuje značné jmění, o které lidé pravděpodobně jednoduše přišli.
-
Odeslání tokenů na prázdnou adresu, která neodpovídá ani externě vlastněnému účtu (EOA), ani chytrému kontraktu. Sice nemám statistiky o tom, jak často se to stává, ale jeden incident mohl stát 20 000 000 tokenů (opens in a new tab).
Zabránění převodům
Kontrakt ERC-20 od OpenZeppelin obsahuje „hook“ _beforeTokenTransfer (opens in a new tab), který je volán před převodem tokenu. Ve výchozím stavu tento hook nic nedělá, ale můžeme na něj navázat vlastní funkcionalitu, například kontroly, které v případě problému transakci vrátí.
Chcete-li tento hook použít, přidejte za konstruktor tuto funkci:
1 function _beforeTokenTransfer(address from, address to, uint256 amount)2 internal virtual3 override(ERC20)4 {5 super._beforeTokenTransfer(from, to, amount);6 }Některé části této funkce pro vás mohou být nové, pokud nejste se Solidity příliš obeznámeni:
1 internal virtualKlíčové slovo virtual znamená, že stejně jako jsme zdědili funkcionalitu z ERC20 a přepsali tuto funkci, mohou i další kontrakty dědit z našeho kontraktu a tuto funkci přepsat.
1 override(ERC20)Musíme explicitně uvést, že přepisujeme (opens in a new tab) definici _beforeTokenTransfer z tokenu ERC20. Z hlediska bezpečnosti jsou explicitní definice obecně mnohem lepší než implicitní – nemůžete zapomenout, že jste něco udělali, když to máte přímo před očima. To je také důvod, proč musíme určit, kterou nadtřídu _beforeTokenTransfer přepisujeme.
1 super._beforeTokenTransfer(from, to, amount);Tento řádek volá funkci _beforeTokenTransfer kontraktu nebo kontraktů, z nichž jsme dědili a které ji mají. V tomto případě je to pouze ERC20, kontrakt Ownable tento hook nemá. Přestože ERC20._beforeTokenTransfer v současné době nic nedělá, voláme ji pro případ, že by v budoucnu byla přidána nějaká funkcionalita (a my se pak rozhodli 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
tose nesmí rovnataddress(this), tedy adrese samotného kontraktu ERC-20. - Adresa
tonesmí 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 těžké je vyčistit do posledního wei.
- Chytrý kontrakt. Otestovat, zda je adresa chytrý kontrakt, je trochu těžší. Existuje operační kód, který kontroluje délku externího kódu, nazývaný
EXTCODESIZE(opens in a new tab), ale není přímo dostupný v Solidity. Musíme pro to použít Yul (opens in a new tab), což je assemblér pro EVM. Existují i další hodnoty, které bychom mohli použít ze Solidity (<address>.codea<address>.codehash(opens in a new tab)), ale stojí více.
Projděme si nový kód řádek po řádku:
1 require(to != address(this), "Tokeny nelze posílat na adresu kontraktu");Toto je první požadavek, zkontrolujte, že to a this(address) není totéž.
1 bool isToContract;2 assembly {3 isToContract := gt(extcodesize(to), 0)4 }Takto zkontrolujeme, zda je adresa kontrakt. Z Yulu nemůžeme přímo získat výstup, takže místo toho definujeme proměnnou, která bude obsahovat výsledek (v tomto případě isToContract). Yul funguje tak, že každý operační kód je považován za funkci. Nejprve tedy zavoláme EXTCODESIZE (opens in a new tab), abychom získali velikost kontraktu, a poté použijeme GT (opens in a new tab), abychom zkontrolovali, že není nulová (pracujeme s neznaménkovými celými čísly, takže samozřejmě nemůže být záporná). Poté zapíšeme výsledek do isToContract.
1 require(to.balance != 0 || isToContract, "Tokeny nelze posílat na prázdnou adresu");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 napravovat chyby. Aby se snížilo riziko zneužití, může být tento administrátor multisig (opens in a new tab), takže na akci se musí shodnout více lidí. V tomto článku budeme mít dvě administrativní funkce:
-
Zmrazování a rozmrazování účtů. To může být užitečné například v případě, že by účet mohl být kompromitován.
-
Vyčištění prostředků.
Někdy podvodníci posílají podvodné tokeny do kontraktu skutečného tokenu, aby získali legitimitu. Příklad naleznete zde (opens in a new tab). Legitimní kontrakt ERC-20 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č je dobré mít způsob, jak je dostat ven.
OpenZeppelin poskytuje dva mechanismy pro umožnění administrativního přístupu:
Ownable(opens in a new tab) kontrakty mají jediného vlastníka. Funkce, které mají modifikátor (opens in a new tab)onlyOwner, může volat pouze tento vlastník. Vlastníci mohou převést vlastnictví na někoho jiného nebo se ho úplně vzdát. Práva všech ostatních účtů jsou obvykle totožná.AccessControl(opens in a new tab) kontrakty mají řízení přístupu na základě rolí (RBAC) (opens in a new tab).
Pro zjednodušení v tomto článku používáme Ownable.
Zmrazování a rozmrazování účtů
Zmrazování a rozmrazování účtů vyžaduje několik změn:
-
Mapování (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 na začátku nulové, což se u booleovských hodnot interpretuje jako false. To je to, co chceme, protože ve výchozím nastavení nejsou účty zmrazeny.
1 mapping(address => bool) public frozenAccounts; -
Události (opens in a new tab), které informují všechny zúčastněné o zmrazení nebo rozmrazení účtu. 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 slušnost, když je chytrý kontrakt emituje, když se stane něco, co by mohlo být pro někoho jiného relevantní.
Události jsou indexovány, takže bude možné vyhledat všechny případy, kdy byl účet zmrazen nebo rozmrazen.
1 // Když jsou účty zmrazeny nebo rozmrazeny2 event AccountFrozen(address indexed _addr);3 event AccountThawed(address indexed _addr); -
Funkce pro zmrazování a rozmrazování účtů. Tyto dvě funkce jsou téměř totožné, takže si projdeme pouze funkci zmrazení.
1 function freezeAccount(address addr)2 public3 onlyOwnerFunkce označené jako
public(opens in a new tab) lze volat z jiných chytrých kontraktů nebo přímo transakcí.1 {2 require(!frozenAccounts[addr], "Účet je již zmrazen");3 frozenAccounts[addr] = true;4 emit AccountFrozen(addr);5 } // freezeAccountPokud je účet již zmrazen, transakce se vrátí. V opačném případě ho zmrazte a
emitujte událost. -
Změňte
_beforeTokenTransfer, abyste zabránili přesunu prostředků ze zmrazeného účtu. Všimněte si, že prostředky lze stále převádět na zmrazený účet.1 require(!frozenAccounts[from], "Účet je zmrazen");
Vyčištění prostředků
K uvolnění ERC-20 tokenů držených tímto kontraktem musíme zavolat funkci na kontraktu tokenu, ke kterému patří, buď transfer (opens in a new tab) nebo approve (opens in a new tab). V tomto případě nemá smysl plýtvat palivem na povolenky, můžeme rovnou provést přímý převod.
1 function cleanupERC20(2 address erc20,3 address dest4 )5 public6 onlyOwner7 {8 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 ERC-20 tokeny jako součást zdrojového kódu (viz řádek 4) a tento soubor obsahuje definici pro IERC20 (opens in a new tab), což je rozhraní pro kontrakt ERC-20 od OpenZeppelin.
1 uint balance = token.balanceOf(address(this));2 token.transfer(dest, balance);3 }Jedná se o funkci vyčištění, takže pravděpodobně nechceme zanechat žádné tokeny. Místo ručního získávání zůstatku od uživatele můžeme tento proces rovnou zautomatizovat.
Závěr
Toto není dokonalé řešení – na problém "uživatel udělal chybu" neexistuje dokonalé řešení. Použití těchto typů kontrol však může alespoň některým chybám zabránit. Schopnost zmrazit účty, i když je nebezpečná, může být použita k omezení škod způsobených některými útoky tím, že hackerovi odepře ukradené prostředky.
Více z mé práce najdete zde (opens in a new tab).
Stránka naposledy aktualizována: 4. září 2025