Přeskočit na hlavní obsah
Change page

Bezpečnost chytrých smluv

Smart kontrakty jsou velmi flexibilní a schopné ovládat velké množství hodnot a dat, přičemž běží nezměnitelnou logikou založenou na kódu spuštěném na blockchainu. To vytvořilo živý ekosystém decentralizovaných aplikací bez nutnosti důvěry, které mají oproti tradičním systémům spoustu výhod. Zároveň však představují příležitosti pro útočníky, kteří se snaží vydělat zneužitím zranitelností ve smart kontraktech.

Veřejné blockchainy, jako je Ethereum, dále komplikují otázku zabezpečení smart kontraktů. Nasazený kód kontraktu obvykle není možné změnit, aby se opravily bezpečnostní chyby, a majetek odcizený ze smart kontraktů je kvůli nezměnitelnosti extrémně obtížné sledovat a prakticky nemožné získat zpět.

I když se údaje liší, odhaduje se, že celková hodnota odcizených nebo ztracených prostředků z důvodu bezpečnostních chyb ve smart kontraktech dnes přesahuje 1 miliardu dolarů. To zahrnuje incidenty s vysokým profilem, jako je hack DAO(opens in a new tab) (ukradeno 3,6 milionu ETH, což v dnešní ceně představuje více než 1 miliardu dolarů), hack multi-sig peněženky Parity(opens in a new tab) (škoda ve výši 30 milionů dolarů kvůli hackerům) a problém se zmrazením peněženek Parity(opens in a new tab) (přes 300 milionů dolarů v ETH zůstalo zamčeno navždy).

Výše zmíněné problémy ukazují na důležitost zajištění bezpečnosti smart kontraktů a dělají z ní nezbytnost, do které by měli vývojáři investovat úsilí. Zabezpečení smart kontraktů je vážnou záležitostí, kterou by se měl každý vývojář naučit. Tento průvodce pokrývá bezpečnostní aspekty pro vývojáře Etherea a poskytuje zdroje pro zvýšení bezpečnosti smart kontraktů.

Předpoklady

Než se pustíte do studia bezpečnosti, ujistěte se, že jste obeznámeni se základy vývoje smart kontraktů.

Pokyny pro vytváření bezpečných smart kontraktů na Ethereu

1. Navrhněte správná omezení přístupu

Ve smart kontraktech mohou funkce označené jako public nebo external volat jakýkoli externí (EOA) nebo kontraktový účet. Specifikace veřejné viditelnosti funkcí je nezbytná, pokud chcete, aby mohli s vaším kontraktem interagovat i ostatní. Funkce označené jako private mohou být volány pouze funkcemi v rámci smart kontraktu a nikoliv externími účty. Pokud umožníte každému účastníkovi sítě přístup ke všem funkcím kontraktu, můžete tím způsobit problémy, zejména pokud to znamená, že kdokoliv může provádět citlivé operace (např. vydávání nových tokenů).

Aby se zabránilo neoprávněnému použití funkcí smart kontraktu, je nutné implementovat bezpečné přístupové kontroly. Mechanismy přístupové kontroly omezují schopnost používat určité funkce ve smart kontraktu na schválené subjekty, jako jsou účty odpovědné za správu kontraktu. Vzor Ownable a kontrola přístupu založená na rolích jsou dva vzory užitečné pro implementaci přístupové kontroly ve smart kontraktech:

Vzor Ownable

Tento vzor přiřadí při vytváření kontraktu adresu jako „vlastníka“ kontraktu. Chráněným funkcím je přiřazen modifikátor OnlyOwner, který zajistí, že kontrakt ověří identitu volající adresy před spuštěním funkce. Volání chráněných funkcí z jiných adres, než je adresa vlastníka kontraktu, se vždy vrátí zpět, čímž se zabrání přístupu nežádoucích adres.

Kontrola přístupu založená na rolích

Registrace jediné adresy jako vlastníka smart kontraktu představuje riziko centralizace a jediného bodu selhání. Pokud jsou kompromitovány klíče účtu vlastníka, útočníci mohou napadnout i kontrakt, který vlastní. Proto může být lepší možností použít kontrolu přístupu založenou na rolích s účty několika správců.

V řízení přístupu na základě rolí je přístup k citlivým funkcím rozdělen mezi několik důvěryhodných adres. Jeden účet může být například zodpovědný za vydávání nových tokenů, zatímco jiný účet provádí vylepšení nebo kontrakt pozastavuje. Decentralizace řízení přístupu tímto způsobem eliminuje jednotlivé body selhání a snižuje nutnost uživatele vašemu kontraktu slepě důvěřovat.

Použití multi-signature peněženek

Dalším přístupem k implementaci bezpečné přístupové kontroly je možnost ke správě kontraktu použít multi-signature účet. Na rozdíl od běžného EOA jsou multi-signature účty vlastněny několika subjekty a k provedení transakcí vyžadují podpisy od předem daného minimálního počtu účtů – například alespoň 3 z 5.

Použití multisigu pro řízení přístupu přináší další vrstvu zabezpečení, protože akce na smart kontraktu vyžadují souhlas více stran. To je užitečné zejména v případě, že je použití vzoru Ownable nezbytné, protože útočníkovi nebo i insiderovi se špatným úmyslem komplikuje manipulaci s citlivými funkcemi kontraktu.

2. Použijte příkazy require(), assert() a revert() k ochraně akcí kontraktu

Jakmile je váš smart kontrakt nasazen na blockchain, volat jeho veřejné funkce může kdokoliv. Jelikož nemůžete předem vědět, jak budou externí účty s kontraktem interagovat, je ideální volbou implementovat interní zabezpečení proti problematickým operacím ještě před nasazením kontraktu. Pomocí příkazů require(), assert() a revert() můžete nastavit požadované chování smart kontraktu za účelem vyvolání výjimek a vrácení změn stavu, pokud exekuce funkce nebude úspěšná.

require(): `Vyžaduje, aby byly předem definované podmínky splněny před spuštěním volané funkce. Příkaz require` lze použít k ověření uživatelských vstupů, kontrole stavových proměnných nebo ověření identity volajícího účtu před pokračováním exekuce funkce.

assert(): assert() se používá k detekci interních chyb a kontrole porušení „invariantů“ v kódu. Invariant je logické tvrzení o stavu kontraktu, které by mělo platit pro všechny exekuce funkcí. Příkladem invariantu je maximální celková nabídka nebo zůstatek tokenového kontraktu. Použití funkce assert() zajišťuje, že váš kontrakt nikdy nedosáhne zranitelného stavu, a pokud ano, všechny změny stavových proměnných budou vráceny zpět.

revert(): revert() může být použit v if-else příkazu k vyvolání výjimky, pokud není splněna požadovaná podmínka. Ukázkový kontrakt níže používá revert() k ochraně exekuce funkcí:

1pragma solidity ^0.8.4;
2
3contract VendingMachine {
4 address owner;
5 error Unauthorized();
6 function buy(uint amount) public payable {
7 if (amount > msg.value / 2 ether)
8 revert("Not enough Ether provided.");
9 // Perform the purchase.
10 }
11 function withdraw() public {
12 if (msg.sender != owner)
13 revert Unauthorized();
14
15 payable(msg.sender).transfer(address(this).balance);
16 }
17}
Zobrazit vše

3. Testování smart kontraktů a ověřování správnosti kódu

Neměnnost kódu běžícího na Virtuálním stroji Etherea (EVM) znamená, že smart kontrakty vyžadují vyšší úroveň kontroly kvality během vývojové fáze. Důkladné testování vašeho kontraktu a sledování jakýchkoli neočekávaných výsledků výrazně zvýší bezpečnost a dlouhodobě ochrání vaše uživatele.

Obvyklou metodou je psát malé jednotkové testy (unit tests) pomocí mock dat, která kontrakt očekává od uživatelů. Jednotkové testování je dobré pro testování funkčnosti určitých funkcí a zajištění, že smart kontrakt funguje podle očekávání.

Jednotkové testování je při zajištění bezpečnosti smart kontraktů účinné bohužel jen minimálně, pokud se používá izolovaně. Jednotkový test může prokázat, že funkce správně provádí operace s mock daty, ale obecně jsou jednotkové testy účinné pouze do té míry, jak jsou napsány. To ztěžuje detekci opomenutých hraničních případů a zranitelností, které by mohly ohrozit bezpečnost vašeho smart kontraktu.

Lepším přístupem je kombinovat jednotkové testování s testováním založeným na vlastnostech (property-based testing) prováděným pomocí statické a dynamické analýzy. Statická analýza se spoléhá na nízkoúrovňové reprezentace, jako jsou grafy toku kontroly (control flow graphs)(opens in a new tab) a abstraktní syntaktické stromy(opens in a new tab) (abstract syntax trees), za účelem analýzy dosažitelných stavů programu a možných exekučních cest. Mezitím dynamické analytické techniky, jako je fuzzing smart kontraktů(opens in a new tab), provádějí kód kontraktu s náhodnými vstupními hodnotami, aby detekovaly operace, které porušují bezpečnostní vlastnosti.

Formální verifikace je dalším postupem pro ověřování bezpečnostních vlastností ve smart kontraktech. Na rozdíl od běžného testování může formální verifikace definitivně prokázat nepřítomnost chyb ve smart kontraktu. Toho lze docílit vytvořením formální specifikace, která zachycuje požadované bezpečnostní vlastnosti, a prokázáním, že formální model kontraktů odpovídá této specifikaci.

4. Požádejte o nezávislé přezkoumání vašeho kódu

Po testování vašeho kontraktu je dobré požádat jiné osoby, aby zkontrolovaly zdrojový kód a odhalily případné bezpečnostní problémy. Testování neodhalí všechny chyby ve smart kontraktu, ale nezávislé přezkoumání zvyšuje možnost odhalení zranitelností.

Audity

Zajištění auditu smart kontraktů je jedním ze způsobů, jak provést nezávislé přezkoumání kódu. Auditoři hrají důležitou roli při zajišťování, že smart kontrakty jsou bezpečné a bez defektů kvality a návrhových chyb.

Je však důležité nenahlížet na audity jako na všelék. Audity smart kontraktů neodhalí každou chybu a jsou navrženy především pro poskytování dalšího kola kontrol, které mohou pomoci odhalit problémy, které vývojáři během počátečního vývoje a testování přehlédli. Doporučuje se dodržovat osvědčené postupy při práci s auditory, jako je řádné dokumentování kódu a přidávání komentářů, aby se přínos takového auditu maximalizoval.

Odměna za vyřešení chyby

Zřízení programu bug bounty, tedy programu odměn za vyřešení chyb, je dalším přístupem k implementaci externích kontrol kódu. Bug bounty je finanční odměna poskytovaná jednotlivcům (obvykle whitehat hackerům), kteří objeví zranitelnosti v aplikaci.

Při správném použití bug bounty motivuje členy hackerské komunity k inspekci vašeho kódu a odhalení kritických chyb. Příkladem z reálného světa je „infinite money bug“, který by útočníkovi umožnil vytvořit neomezené množství Etheru na platformě Optimism(opens in a new tab), druhé vrstvě protokolu na Ethereu. Naštěstí whitehat hacker odhalil tuto chybu(opens in a new tab) a informoval tým, čímž si vysloužil velkou odměnu(opens in a new tab).

Užitečnou strategií je nastavit odměnu v programu bug bounty v poměru k výši prostředků, které jsou v sázce. Tento přístup, popisovaný jako „škálovatelná bug bounty(opens in a new tab)“, finančně motivuje jednotlivce zodpovědně zveřejnit zranitelnosti namísto jejich zneužití.

5. Při vývoji smart kontraktů dodržujte osvědčené postupy

Existence auditů a bug bounty vás nezbavuje odpovědnosti za psaní kvalitního kódu. Bezpečnost smart kontraktů začíná dodržováním správných návrhových a vývojových procesů:

6. Implementujte robustní plány obnovy po nehodě

Navrhování bezpečných přístupových kontrol, implementace modifikátorů funkcí a další doporučení mohou zlepšit bezpečnost smart kontraktů, ale nemohou vyloučit možnost zlovolných útoků. Budování bezpečných smart kontraktů vyžaduje „přípravu na selhání“ a mít záložní plán pro efektivní reakci na útoky. Správný plán obnovy po nehodě zahrnuje některé nebo všechny z následujících komponent:

Aktualizace kontraktů

I když jsou smart kontrakty na Ethereu ve výchozím nastavení neměnné, pomocí vzorů aktualizace je možné dosáhnout určité míry změny. Aktualizace kontraktů je nezbytná v případech, kdy kritická chyba činí váš starý kontrakt nepoužitelným a nasazení nové logiky je nejrealističtější možností.

Mechanismy aktualizace kontraktů fungují různě, ale jedním z populárních přístupů je „proxy vzor“. Proxy vzory(opens in a new tab) rozdělují stav a logiku aplikace mezi dva kontrakty. První kontrakt (tzv. „proxy kontrakt“) uchovává stavové proměnné (např. zůstatky uživatelů), zatímco druhý kontrakt (tzv. „logický kontrakt“) obsahuje kód pro vykonávání funkcí kontraktu.

Účty interagují s proxy kontraktem, který přesměruje všechna volání funkcí na logický kontrakt pomocí nízkoúrovňového volání delegatecall()(opens in a new tab). Na rozdíl od běžného volání zpráv delegatecall() zajišťuje, že kód spuštěný na adrese logického kontraktu je prováděn v kontextu volajícího kontraktu. To znamená, že logický kontrakt vždy zapisuje do úložiště proxy kontraktu (místo vlastního úložiště) a původní hodnoty msg.sender a msg.value jsou zachovány.

Delegování volání na logický kontrakt vyžaduje uložení jeho adresy do úložiště proxy kontraktu. Proto je aktualizace logiky kontraktu pouze otázkou nasazení nového logického kontraktu a uložení nové adresy do proxy kontraktu. Následná volání proxy kontraktu jsou pak automaticky směrována na nový logický kontrakt, čímž dojde k „aktualizaci“ kontraktu bez skutečné modifikace kódu.

Další informace o aktualizaci kontraktu.

Nouzové zastavení

Jak již bylo zmíněno, i přes rozsáhlé audity a testování není možné objevit úplně všechny chyby ve smart kontraktu. Pokud se po nasazení objeví ve vašem kódu zranitelnost, její oprava je nemožná, protože nemůžete změnit kód běžící na adrese kontraktu. Navíc implementace mechanismů aktualizace (např. proxy vzory) může nějakou chvíli trvat (často vyžaduje schválení několika subjektů), což dává útočníkům více času a mohou tak způsobit větší škody.

Nejradikálnější možností je implementovat funkci „nouzového zastavení“, která zablokuje volání zranitelných funkcí v kontraktu. Nouzové zastavení obvykle zahrnuje následující komponenty:

  1. Globální Booleanovská proměnná, která indikuje, zda je smart kontrakt zastaven, či nikoli. Tato proměnná je při spouštění kontraktu nastavena na false, ale při zastavení kontraktu se změní na true.

  2. Funkce, které odkazují na Booleanovskou proměnnou při jejich provádění. Tyto funkce jsou přístupné, když smart kontrakt není pozastaven, a stávají se nepřístupnými, když je funkce nouzového zastavení aktivována.

  3. Entita, která má přístup k funkci nouzového zastavení a která nastavuje Booleanovskou proměnnou na true. Aby se zabránilo zlovolným akcím, lze volání této funkce omezit na důvěryhodnou adresu (např. vlastníka kontraktu).

Jakmile je nouzové zastavení aktivováno, určité funkce nebudou volatelné. Toho lze dosáhnout obalením vybraných funkcí v modifikátoru, který odkazuje na globální proměnnou. Níže je uveden příklad(opens in a new tab) implementace tohoto vzoru v kontraktech:

1// This code has not been professionally audited and makes no promises about safety or correctness. Use at your own risk.
2
3contract EmergencyStop {
4
5 bool isStopped = false;
6
7 modifier stoppedInEmergency {
8 require(!isStopped);
9 _;
10 }
11
12 modifier onlyWhenStopped {
13 require(isStopped);
14 _;
15 }
16
17 modifier onlyAuthorized {
18 // Check for authorization of msg.sender here
19 _;
20 }
21
22 function stopContract() public onlyAuthorized {
23 isStopped = true;
24 }
25
26 function resumeContract() public onlyAuthorized {
27 isStopped = false;
28 }
29
30 function deposit() public payable stoppedInEmergency {
31 // Deposit logic happening here
32 }
33
34 function emergencyWithdraw() public onlyWhenStopped {
35 // Emergency withdraw happening here
36 }
37}
Zobrazit vše
Kopírovat

Tento příklad ukazuje základní funkce nouzového zastavení:

  • Proměnná isStopped je Booleanovská hodnota, která se na začátku vyhodnocuje jako false a změní se na true, když kontrakt vstoupí do nouzového režimu.

  • Modifikátory funkcí onlyWhenStopped a stoppedInEmergency kontrolují proměnnou isStopped. Modifikátor stoppedInEmergency se používá k ovládání funkcí, které by měly být nepřístupné, když je kontrakt zranitelný (např. funkce deposit()). Volání těchto funkcí se jednoduše zruší.

Modifikátor onlyWhenStopped se používá pro funkce, které by měly být volatelné během nouze (např. funkce emergencyWithdraw()). Tyto funkce mohou pomoci situaci vyřešit, a proto jsou vyloučeny ze seznamu „omezených funkcí“.

Použití funkce nouzového zastavení poskytuje efektivní řešení vážných zranitelností smart kontraktu. Nicméně to zvyšuje nutnost uživatelů důvěřovat vývojářům, že tuto funkci neaktivují z vlastních sobeckých důvodů. K řešení tohoto problému je možné decentralizovat kontrolu nad nouzovým zastavením, například tím, že ho podrobíte mechanismu hlasování na blockchainu, časovému zámku nebo schválení z multisig peněženky.

Monitorování událostí

Události(opens in a new tab) umožňují sledovat volání funkcí smart kontraktů a monitorovat změny stavových proměnných. Ideální je naprogramovat váš smart kontrakt tak, aby zapsal událost pokaždé, když nějaká strana provede bezpečnostně kritickou akci (např. výběr prostředků).

Protokolování událostí a jejich monitorování mimo řetězec poskytuje přehled o činnostech kontraktu a pomáhá rychleji odhalit škodlivé akce. To znamená, že váš tým může rychleji reagovat na útoky a podniknout kroky ke zmírnění dopadu na uživatele, jako je pozastavení funkcí nebo provedení aktualizace.

Můžete si také vybrat hotový nástroj pro monitorování, který automaticky přeposílá upozornění pokaždé, když někdo interaguje s vašimi kontrakty. Tyto nástroje vám umožní vytvářet vlastní upozornění na základě různých spouštěcích událostí, jako je objem transakcí, četnost volání funkcí nebo specifické funkce zapojené do transakce. Například byste mohli naprogramovat upozornění, které se objeví, když částka vybraná v jedné transakci překročí určitou prahovou hodnotu.

7. Návrh bezpečných řídicích systémů

Možná budete chtít decentralizovat svou aplikaci tím, že předáte kontrolu nad hlavními smart kontrakty členům komunity. V takovém případě bude systém smart kontraktů zahrnovat řídicí modul, což je mechanismus, který umožňuje členům komunity schvalovat administrativní akce prostřednictvím řídicího systému na blockchainu. Například návrh na aktualizaci proxy kontraktu na novou implementaci může být odhlasován držiteli tokenů.

Decentralizované řízení může být prospěšné, zejména proto, že sladí zájmy vývojářů a koncových uživatelů. Nicméně mechanismy správy smart kontraktů mohou při nesprávné implementaci představovat nové riziko. Pravděpodobný scénář je, že útočník získá obrovskou hlasovací sílu (měřenou počtem držených tokenů) tím, že si vezme bleskovou půjčku a protlačí škodlivý návrh.

Jedním ze způsobů, jak předcházet problémům spojeným s řízením na blockchainu, je použití časového zámku (timelock)(opens in a new tab). Časový zámek brání smart kontraktu v provádění předem daných akcí, dokud neuplyne stanovené množství času. Další strategie zahrnují přiřazení „váhy hlasu“ každému tokenu na základě toho, jak dlouho ho adresa držela, nebo měření hlasovací síly adresy v historickém období (například 2–3 bloky v minulosti) namísto aktuálního bloku. Obě metody snižují možnosti rychlého získávání hlasovací síly za účelem ovlivnění hlasování na blockchainu.

Více o návrhu systémů bezpečného řízení(opens in a new tab), různých hlasovacích mechanismech v DAO(opens in a new tab) a běžných vektorech útoku DAO využívajících DeFi(opens in a new tab) najdete ve sdílených odkazech.

8. Snižte složitost kódu na minimum

Vývojáři tradičních softwarů znají princip KISS („Keep It Simple, Stupid“), který doporučuje zbytečně softwarový design nekomplikovat. Tento princip vychází z myšlenky, že „komplexní systémy selhávají složitým způsobem“ a jsou náchylnější k nákladným chybám.

Udržování jednoduchosti je zvláště důležité při psaní smart kontraktů, vzhledem k tomu, že smart kontrakty potenciálně spravují velké objemy aktiv. Tipem pro docílení jednoduchosti při psaní smart kontraktů je opětovné použití stávajících knihoven, jako je OpenZeppelin Contracts(opens in a new tab), kdekoliv je to možné. Protože tyto knihovny byly důkladně auditovány a testovány vývojáři, jejich použití snižuje pravděpodobnost chyb při psaní nové funkcionality od začátku.

Dalším běžným doporučením je psát malé funkce a udržovat smart kontrakty modulární rozdělením obchodní logiky mezi více kontraktů. Nejenže psaní jednoduššího kódu snižuje možnosti útoku na váš smart kontrakt, ale také usnadňuje ověřování správnosti celého systému a včasné odhalení možných návrhových chyb.

9. Chraňte se před běžnými zranitelnostmi smart kontraktů

Opětovný vstup

EVM neumožňuje souběžné zpracování (konkurenci), což znamená, že dva kontrakty zapojené do volání zprávy nemohou běžet současně. Externí volání pozastaví exekuci a paměť volajícího kontraktu, dokud se volání nevrátí, načež provádění pokračuje normálně. Tento proces lze formálně popsat jako předání řízení(opens in a new tab) dalšímu kontraktu.

Ačkoli je tento proces většinou neškodný, předání řízení nedůvěryhodným kontraktům může způsobit problémy, jako je reentrancy (opětovný vstup). Útok opětovným vstupem nastane, když škodlivý kontrakt znovu volá do zranitelného kontraktu, než je původní funkce dokončena. Tento typ útoku je nejlepší vysvětlovat na příkladu.

Zvažte jednoduchý smart kontrakt (Victim), který umožňuje komukoli vložit a vybrat Ether:

1// This contract is vulnerable. Do not use in production
2
3contract Victim {
4 mapping (address => uint256) public balances;
5
6 function deposit() external payable {
7 balances[msg.sender] += msg.value;
8 }
9
10 function withdraw() external {
11 uint256 amount = balances[msg.sender];
12 (bool success, ) = msg.sender.call.value(amount)("");
13 require(success);
14 balances[msg.sender] = 0;
15 }
16}
Zobrazit vše
Kopírovat

Tento kontrakt nabízí funkci withdraw(), která umožňuje uživatelům vybrat ETH, které do kontraktu vložili v minulosti. Při zpracování výběru kontrakt provádí následující operace:

  1. Zjistí zůstatek ETH uživatele
  2. Odešle prostředky na volající adresu
  3. Resetuje jejich zůstatek na 0, aby zabránil dalším výběrům od uživatele

Funkce withdraw() v kontraktu Victim následuje vzor „kontroly-interakce-efekty“ (checks-interactions-effects). Nejprve zkontroluje, zda jsou splněny podmínky pro exekuci (tzn. uživatel má kladný zůstatek ETH), poté provede interakci odesláním ETH na adresu volajícího a nakonec aplikuje efekty transakce (tj. sníží zůstatek uživatele).

Pokud je funkce withdraw() volána z účtu vlastněného externí osobou (EOA), funkce se provede podle očekávání: msg.sender.call.value() odešle ETH volajícímu. Avšak pokud je msg.sender účet smart kontraktu, který volá withdraw(), odeslání prostředků pomocí msg.sender.call.value() spustí i kód uložený na této adrese.

Představte si, že toto je kód na adrese kontraktu:

1 contract Attacker {
2 function beginAttack() external payable {
3 Victim(victim_address).deposit.value(1 ether)();
4 Victim(victim_address).withdraw();
5 }
6
7 function() external payable {
8 if (gasleft() > 40000) {
9 Victim(victim_address).withdraw();
10 }
11 }
12}
Zobrazit vše
Kopírovat

Tento kontrakt je navržen tak, aby dělal tři věci:

  1. Přijal vklad z jiného účtu (pravděpodobně z EOA útočníka)
  2. Uložil 1 ETH do kontraktu Victim
  3. Vybral 1 ETH uložený ve smart kontraktu

Na první pohled není na tomto kontraktu nic špatného, až na to, že kontrakt Attacker má další funkci, která znovu volá withdraw() v kontraktu Victim, pokud je zbývající palivo (gas) z příchozího volání msg.sender.call.value více než 40 000. To dává kontraktu Attacker schopnost znovu vstoupit do kontraktu Victim a vybrat více prostředků před tím, než se dokončí první volání withdraw. Tento cyklus vypadá následovně:

1- Attacker's EOA calls `Attacker.beginAttack()` with 1 ETH
2- `Attacker.beginAttack()` deposits 1 ETH into `Victim`
3- `Attacker` calls `withdraw() in `Victim`
4- `Victim` checks `Attacker`’s balance (1 ETH)
5- `Victim` sends 1 ETH to `Attacker` (which triggers the default function)
6- `Attacker` calls `Victim.withdraw()` again (note that `Victim` hasn’t reduced `Attacker`’s balance from the first withdrawal)
7- `Victim` checks `Attacker`’s balance (which is still 1 ETH because it hasn’t applied the effects of the first call)
8- `Victim` sends 1 ETH to `Attacker` (which triggers the default function and allows `Attacker` to reenter the `withdraw` function)
9- The process repeats until `Attacker` runs out of gas, at which point `msg.sender.call.value` returns without triggering additional withdrawals
10- `Victim` finally applies the results of the first transaction (and subsequent ones) to its state, so `Attacker`’s balance is set to 0
Zobrazit vše
Kopírovat

Výsledkem je, že následná vyvolání budou úspěšná a umožní volajícímu vybrat svůj zůstatek vícekrát, protože zůstatek volajícího není nastaven na 0, dokud se nedokončí provedení funkce. Tento druh útoku může být použit k vybrání prostředků smart kontraktu, jako se to stalo při DAO hacku v roce 2016(opens in a new tab). Útoky opětovným vstupem (reentrancy) jsou stále kritickým problémem smart kontraktů, jak ukazují veřejné seznamy exploitů reentrancy útoků(opens in a new tab).

Jak zabránit útokům opětovným vstupem

Jedním z přístupů, jak se vypořádat s reentrancy útoky, je dodržovat vzor kontroly-efekty-interakce(opens in a new tab). Tento vzor uspořádává provádění funkcí takovým způsobem, že kód, který provádí nezbytné kontroly před pokračováním v exekuci, je proveden jako první, následovaný kódem, který manipuluje se stavem kontraktu, a kód, který interaguje s jinými kontrakty nebo EOA, přichází na řadu jako poslední.

Vzor kontroly-efekty-interakce je použit v revidované verzi kontraktu Victim, která je uvedena níže:

1contract NoLongerAVictim {
2 function withdraw() external {
3 uint256 amount = balances[msg.sender];
4 balances[msg.sender] = 0;
5 (bool success, ) = msg.sender.call.value(amount)("");
6 require(success);
7 }
8}
Kopírovat

Tento kontrakt provádí kontrolu zůstatku uživatele, aplikuje efekty funkce withdraw() (nastavením zůstatku uživatele na 0) a poté pokračuje v interakci (odesláním ETH na uživatelovu adresu). Tímto způsobem kontrakt aktualizuje svůj stav před externím voláním, čímž eliminuje podmínku opětovného vstupu, která umožňovala původní útok. Kontrakt Attacker stále může znovu volat funkci withdraw() v kontraktu NoLongerAVictim, ale protože <0>balances[msg.sender]</0> byla nastavena na 0, pokus o opětovné výběry by vyvolal chybu.

Další možností je použití zámku pro vzájemné vyloučení (běžně označovaného jako „mutex“), který uzamkne část stavu kontraktu, dokud se nedokončí volání funkce. To je implementováno pomocí proměnné typu Boolean, která je nastavena na true před provedením funkce a po dokončení volání se vrací na hodnotu false. Jak je vidět v níže uvedeném příkladu, použití mutexu chrání funkci před rekurzivními voláními, zatímco původní volání je stále zpracováváno, což účinně zastavuje reentrancy.

1pragma solidity ^0.7.0;
2
3contract MutexPattern {
4 bool locked = false;
5 mapping(address => uint256) public balances;
6
7 modifier noReentrancy() {
8 require(!locked, "Blocked from reentrancy.");
9 locked = true;
10 _;
11 locked = false;
12 }
13 // This function is protected by a mutex, so reentrant calls from within `msg.sender.call` cannot call `withdraw` again.
14 // The `return` statement evaluates to `true` but still evaluates the `locked = false` statement in the modifier
15 function withdraw(uint _amount) public payable noReentrancy returns(bool) {
16 require(balances[msg.sender] >= _amount, "No balance to withdraw.");
17
18 balances[msg.sender] -= _amount;
19 bool (success, ) = msg.sender.call{value: _amount}("");
20 require(success);
21
22 return true;
23 }
24}
Zobrazit vše
Kopírovat

Můžete také použít systém pull payments(opens in a new tab), který vyžaduje, aby sami uživatelé vybrali prostředky ze smart kontraktů, namísto systému „push payments“, který prostředky na účty odesílá sám. Tím se eliminuje možnost neúmyslného spuštění kódu na neznámých adresách (a může také zabránit určitým útokům typu denial-of-service).

Přetečení a podtečení celých čísel

Přetečení celého čísla nastává, když výsledek aritmetické operace překročí přijatelný rozsah hodnot, což způsobí „přetočení“ na nejnižší reprezentovatelnou hodnotu. Například uint8 může uložit hodnoty až do 2^8-1=255. Aritmetické operace, které vedou k hodnotám vyšším než 255, přetečou a nastaví uint na 0, podobně jako se tachometr automobilu resetuje na 0, když dosáhne maximálního nájezdu (999999).

Podtečení celého čísla se děje ze stejných důvodů: výsledek aritmetické operace klesne pod přijatelný rozsah. Pokud byste například zkusili snížit hodnotu 0 v uint8, výsledek by se jednoduše přetočil na maximální reprezentovatelnou hodnotu (255).

Přetečení i podtečení celých čísel může vést k neočekávaným změnám proměnných stavu kontraktu a způsobit neplánovanou exekuci. Níže je uveden příklad, jak může útočník zneužít aritmetické přetečení ve smart kontraktu k provedení neplatné operace:

1pragma solidity ^0.7.6;
2
3// This contract is designed to act as a time vault.
4// User can deposit into this contract but cannot withdraw for at least a week.
5// User can also extend the wait time beyond the 1 week waiting period.
6
7/*
81. Deploy TimeLock
92. Deploy Attack with address of TimeLock
103. Call Attack.attack sending 1 ether. You will immediately be able to
11 withdraw your ether.
12
13What happened?
14Attack caused the TimeLock.lockTime to overflow and was able to withdraw
15before the 1 week waiting period.
16*/
17
18contract TimeLock {
19 mapping(address => uint) public balances;
20 mapping(address => uint) public lockTime;
21
22 function deposit() external payable {
23 balances[msg.sender] += msg.value;
24 lockTime[msg.sender] = block.timestamp + 1 weeks;
25 }
26
27 function increaseLockTime(uint _secondsToIncrease) public {
28 lockTime[msg.sender] += _secondsToIncrease;
29 }
30
31 function withdraw() public {
32 require(balances[msg.sender] > 0, "Insufficient funds");
33 require(block.timestamp > lockTime[msg.sender], "Lock time not expired");
34
35 uint amount = balances[msg.sender];
36 balances[msg.sender] = 0;
37
38 (bool sent, ) = msg.sender.call{value: amount}("");
39 require(sent, "Failed to send Ether");
40 }
41}
42
43contract Attack {
44 TimeLock timeLock;
45
46 constructor(TimeLock _timeLock) {
47 timeLock = TimeLock(_timeLock);
48 }
49
50 fallback() external payable {}
51
52 function attack() public payable {
53 timeLock.deposit{value: msg.value}();
54 /*
55 if t = current lock time then we need to find x such that
56 x + t = 2**256 = 0
57 so x = -t
58 2**256 = type(uint).max + 1
59 so x = type(uint).max + 1 - t
60 */
61 timeLock.increaseLockTime(
62 type(uint).max + 1 - timeLock.lockTime(address(this))
63 );
64 timeLock.withdraw();
65 }
66}
Zobrazit vše
Jak zabránit podtečení a přetečení celých čísel

Od verze 0.8.0 kompilátor Solidity odmítá kód, který by vedl k podtečení nebo přetečení celých čísel. A smart kontrakty, které jsou kompilovány pomocí starší verze kompilátoru, by měly buď provádět kontroly ve funkcích zahrnujících aritmetické operace, nebo použít knihovnu (např. SafeMath(opens in a new tab)), která provádí kontroly podtečení/přetečení.

Manipulace s orákly

Orákly získávají informace mimo blockchain (off-chain) a posílají je na blockchain, aby je mohly používat smart kontrakty. Pomocí oráklů můžete navrhovat smart kontrakty, které spolupracují se systémy mimo blockchain, jako jsou kapitálové trhy, čímž se výrazně rozšiřují jejich aplikace.

Pokud je však orákulum poškozeno a posílá nesprávné informace na blockchain, kód smart kontraktů bude vykonáván na základě chybných vstupů, což může způsobit problémy. To je podstatou „problému orákulí“, který se týká úkolu zajistit, aby informace z blockchainového orákula byly přesné, aktuální a včasné.

Další bezpečnostní problém je používání on-chain orákula, jako je decentralizovaná burza (DEX), k získání aktuální ceny aktiva. Platformy na půjčování prostředků v odvětví decentralizovaných financí (DeFi) to často dělají, aby určily hodnotu zástavy uživatele a zjistily, kolik si může půjčit.

Ceny na DEX jsou často přesné, zejména díky arbitrážím, které obnovují paritu na trzích. Nicméně jsou otevřené manipulacím, zejména pokud on-chain orákulum vypočítává ceny aktiv na základě historických obchodních vzorců (což je obvyklý případ).

Útočník by mohl například uměle zvýšit spotovou cenu aktiva tím, že si vezme bleskovou půjčku těsně před interakcí s vaším půjčovacím kontraktem. Dotazování na cenu aktiva na DEX by vrátilo vyšší než normální hodnotu (kvůli velké „nákupní objednávce“ útočníka, která zkresluje poptávku po aktivu), což by mu umožnilo si půjčit více, než by měl. Takovéto „útoky na bleskové půjčky“ byly použity ke zneužití závislosti na cenových oráklech mezi aplikacemi DeFi, což stálo protokoly miliony finančních prostředků.

Jak zabránit manipulaci s orákly

Minimálním požadavkem na zabránění manipulaci s orákly(opens in a new tab) je použití decentralizované sítě orákulí, která dotazuje informace z více zdrojů, aby se zabránilo jednotlivým bodům selhání. Ve většině případů mají decentralizovaná orákula vestavěné kryptoekonomické pobídky, které motivují orákulové uzly k hlášení správných informací, což je činí bezpečnějšími než centralizovaná orákula.

Pokud plánujete dotazovat on-chain orákulum na ceny aktiv, zvažte použití takového, které implementuje mechanismus časově váženého průměru ceny (TWAP). TWAP orákulum(opens in a new tab) dotazuje cenu aktiva ve dvou různých časových bodech (které můžete upravit) a vypočítá spotovou cenu na základě získaného průměru. Volba delších časových období chrání váš protokol proti manipulaci s cenami, protože velké objednávky provedené nedávno nemohou ovlivnit ceny aktiv.

Zdroje pro zabezpečení smart kontraktů pro vývojáře

Nástroje pro analýzu smart kontraktů a ověřování správnosti kódu

  • Testovací nástroje a knihovnysbírka profesionálních nástrojů a knihoven pro provádění unit testů, statické analýzy a dynamické analýzy na smart kontraktech.

  • Nástroje pro formální ověřovánínástroje pro ověřování funkční správnosti ve smart kontraktech a kontrolu invariantů.

  • Služby auditu smart kontraktůseznam organizací poskytujících služby auditu smart kontraktů pro vývojové projekty Etherea.

  • Platformy na odměny za řešení chybplatformy pro koordinaci odměn za řešení chyb a odměňování odpovědného odhalování kritických zranitelností ve smart kontraktech.

  • Kontrola forku(opens in a new tab)bezplatný online nástroj pro kontrolu všech dostupných informací o forkovaném kontraktu.

  • ABI Kodér(opens in a new tab)bezplatná online služba pro kódování funkcí a argumentů konstruktorů kontraktů Solidity.

  • Aderyn(opens in a new tab)statický analyzér Solidity, který prochází abstraktní syntaktické stromy (AST) za účelem zjištění podezřelých zranitelností a vypisuje problémy v přehledném markdown formátu.

Nástroje pro monitorování smart kontraktů

Nástroje pro bezpečnou správu smart kontraktů

Služby auditu smart kontraktů

Platformy pro odměny za řešení chyb

Publikace známých zranitelností a zneužití smart kontraktů

Výzvy určené k učení se zabezpečení smart kontraktů

Osvědčené postupy pro zabezpečení smart kontraktů

Výukové programy o zabezpečení smart kontraktů

  • Jak psát bezpečné smart kontrakty

  • Jak používat Slither k hledání chyb ve smart kontraktech

  • Jak používat Manticore k vyhledávání chyb v chytrých kontraktech

  • Pokyny pro zabezpečení smart kontraktů

  • Jak bezpečně integrovat tokenový kontrakt s libovolnými tokeny

  • Cyfrin Updraft – zabezpečení a auditování smart kontraktů, celý kurz(opens in a new tab)

Byl tento článek užitečný?