Reentrancy a chyby v kontraktech

Jak poznat v kódu chybu, která stála stovky milionů.

Co se naučíš: Naučíš se poznat v kódu chybu, která stála stovky milionů, a znát pořadí, které jí předchází.

5 min čtení + cvičeníNavazuje na:⚙️ EVM a gas

Tahle kapitola je psaná jako review checklist, ne jako návod. Cílem je, abys chybu poznal v kódu dřív, než se nasadí, protože po nasazení už se opravit nedá.

Reentrancy je nejznámější třída chyb v kontraktech a stála dohromady stovky milionů. Přitom je to jeden nesprávně seřazený řádek.


Jak vzniká

Kontrakt pošle peníze a až potom si poznamená, že je poslal. Jenže odeslání na adresu kontraktu spustí kód příjemce, a ten může zavolat zpátky do funkce, která ještě nestihla aktualizovat stav.

Kontrola v kroku 1 pořád vidí zůstatek 10, protože krok 3 nikdy neproběhl. Smyčka se opakuje, dokud je v kontraktu co brát.


Jak to poznat v kódu

Hledej jakoukoli změnu stavu po volání ven. To je celé pravidlo. Konkrétně:

Podezřelý vzorProč
odeslání prostředků a až pak zápis do úložištěklasická reentrancy
volání cizího kontraktu uprostřed výpočtuten kontrakt může zavolat zpět
více funkcí sdílejících stav, kde jedna volá venreentrancy může přijít i jinou funkcí
cyklus, který v každém průchodu volá venjedno zlé volání zablokuje nebo zneužije celý cyklus

Obrana, v pořadí podle spolehlivosti

1. Vzor „kontroluj, zapiš, jednej". Nejdřív ověř podmínky, potom změň stav, teprve nakonec volej ven. Když je stav aktualizovaný před voláním, opakovaný vstup narazí na kontrolu. Tohle samo o sobě odstraní naprostou většinu případů.

2. Zámek proti opakovanému vstupu. Příznak, který se nastaví na začátku funkce a shodí na konci; při opakovaném vstupu funkce skončí. Je to pojistka, ne náhrada bodu 1.

3. Nech uživatele vyzvedávat. Místo aby kontrakt rozesílal, jen si poznamená nárok a každý si přijde sám. Odstraní to celou třídu problémů, protože kontrakt přestane volat cizí adresy.


Další chyby, které stojí za pozornost při review

  • Chybějící kontrola oprávnění. Funkce, která má být jen pro správce, ji nemá. Zní to hloupě, ale je to jedna z nejčastějších příčin skutečných incidentů.
  • Neošetřený návratový kód. Odeslání se nemusí povést a když se výsledek ignoruje, kontrakt pokračuje, jako by peníze odešly.
  • Přetečení a podtečení. V novějších prostředích to hlídá jazyk, ve starším kódu ne. Odečtení od nuly udělá z nuly obrovské číslo.
  • Spoléhání na okamžitou cenu z jednoho zdroje. Viz oracles.
  • Předpoklad, že volání přijde z tvého rozhraní. Kdokoli může zavolat kteroukoli veřejnou funkci s libovolnými parametry, v libovolném pořadí a v libovolnou chvíli.

Proč se to opakuje

Kontrakty se píší jako běžné programy, ale běží v prostředí, kde je každý vnější účastník potenciální útočník s neomezeným počtem pokusů a kde chyba znamená okamžitou finanční ztrátu. K tomu se přidává neměnnost: opravit to po nasazení nejde.

Proto se v téhle oblasti používají postupy, které jinde vypadají přehnaně: formální ověřování, několik nezávislých auditů, postupné zvyšování limitů a odměny za nalezení chyb.


Cvičení

  1. Máš funkci pro výběr. Napiš pořadí kroků, které je bezpečné, a vysvětli proč.
  2. Proč zámek proti opakovanému vstupu nestačí jako jediná obrana?
  3. Kontrakt rozesílá výplaty cyklem. Jaké dva různé problémy tím vznikají?
Náčrt řešení: rozbal, až si cvičení zkusíš sám
  1. Nejdřív ověř, že má nárok, potom sniž zůstatek, teprve pak pošli. Když je stav aktualizovaný před voláním ven, opakovaný vstup narazí na kontrolu, protože zůstatek už je nulový. Zásada se dá shrnout jako kontroluj, zapiš, jednej, a je to nejlevnější a nejspolehlivější obrana, protože nevyžaduje žádný zvláštní mechanismus, jen správné pořadí.
  2. Protože chrání jen tu jednu funkci a jen před přímým opakováním. Reentrancy může přijít jinou funkcí, která sdílí tentýž stav, nebo přes několik kontraktů dokola. Zámek je proto pojistka pro případ, kdy se pořadí nedá dodržet, ne náhrada správného pořadí. Navíc přidává stav, na který se dá zapomenout, a v kombinaci s aktualizovatelným kontraktem může přestat fungovat.
  3. Zaprvé cena roste s počtem příjemců a jednou překročí limit na blok, takže se výplata stane trvale neproveditelnou. Zadruhé jeden příjemce, jehož kód při přijetí selže nebo se zacyklí, zablokuje celý cyklus pro všechny ostatní. Obojí se řeší tím, že se rozesílání nahradí vyzvedáváním: každý si přijde sám, platí za svou transakci a jeho selhání se nikoho jiného netýká.

Shrnutí

  • Reentrancy vzniká, když se stav aktualizuje až po volání ven.
  • Pravidlo při review: hledej jakoukoli změnu stavu následující po volání cizí adresy.
  • Nejlepší obrana je pořadí kontroluj, zapiš, jednej. Zámek je pojistka, ne náhrada.
  • Rozesílání nahraď vyzvedáváním, odstraní to celou třídu problémů naráz.
  • Předpokládej, že kdokoli zavolá kteroukoli veřejnou funkci v libovolném pořadí.
Jak vzniká reentrancy?

Kontrakt pošle prostředky dřív, než si zapíše, že je poslal. Odeslání na adresu kontraktu spustí kód příjemce a ten může zavolat zpět do funkce, která ještě neaktualizovala stav, takže kontrola zůstatku projde znovu. Smyčka pokračuje, dokud je co odčerpat.

Co je pravidlo kontroluj, zapiš, jednej?

Pořadí kroků ve funkci: nejdřív ověř všechny podmínky, potom aktualizuj stav a teprve nakonec volej ven nebo posílej prostředky. Když je stav změněný před voláním, případný opakovaný vstup narazí na kontrolu. Je to nejspolehlivější obrana proti reentrancy a nevyžaduje žádný zvláštní mechanismus.

Proč je lepší nechat uživatele vyzvednout si prostředky než je rozesílat?

Protože kontrakt tím přestane volat cizí adresy, čímž zmizí celá třída chyb včetně reentrancy. Navíc odpadá riziko, že cena cyklu překročí limit bloku nebo že jeden problematický příjemce zablokuje výplatu všem ostatním. Každý uživatel také platí za svou transakci sám.