Przejdź do treści

Wiedza

Smart contract audit: checklista gotowości do audytu

30 punktów przed audytem smart kontraktów: zakres i dokumentacja, testy, fuzzing i niezmienniki w Foundry, triage Slithera, kontrola dostępu, wdrożenie.

Zespół inżynierów sigmacode.io8 min czytania

Na tej stronie (9)
  1. 1. Zakres i dokumentacja
  2. 2. Higiena kodu i analiza statyczna
  3. 3. Testy
  4. 4. Fuzzing i testy niezmienników
  5. 5. Kontrola dostępu i upgrade’owalność
  6. 6. Integracje zewnętrzne
  7. 7. Wdrożenie i utrzymanie
  8. 8. Logistyka audytu
  9. Jak możemy pomóc

Audyt smart kontraktów jest drogi i ściśle ograniczony czasowo. Audytorzy dostają określoną liczbę dni, a każda godzina, którą spędzają na ustalaniu, co kod ma właściwie robić, na gonieniu zmieniającego się commita albo na pisaniu testów, których u Ciebie zabrakło, to godzina odebrana logice, przez którą naprawdę można stracić środki. Lista znalezisk w raporcie zapełnia się wtedy brakującym NatSpec, pływającymi pragmami i nieprzetestowanymi ścieżkami revertu zamiast problemów, za których znalezienie płacisz.

Dobrze przygotowany kod dostaje głębszy audyt w tym samym budżecie. Ta checklista zbiera to, co porządkujemy przed przekazaniem kodu zewnętrznemu audytorowi: 30 konkretnych punktów obejmujących dokumentację, higienę kodu, testy, kontrolę dostępu, integracje, wdrożenie i logistykę. Nie zastępuje audytu; pilnuje, żeby czas audytu trafił tam, gdzie ma znaczenie.

1. Zakres i dokumentacja#

Audytorzy mogą weryfikować zachowanie tylko względem intencji, którą rozumieją, więc intencja musi być spisana.

  • Zamroź hash commita. Daj audytorom jeden otagowany commit i nie zmieniaj audytowanego kodu w trakcie zlecenia. Poprawki trafiają na osobny branch i są sprawdzane w rundzie weryfikacji poprawek.
  • Opublikuj listę plików w zakresie wraz z nSLOC. Wypisz każdy kontrakt w zakresie ze ścieżką i znormalizowaną liczbą linii kodu źródłowego oraz jawnie wskaż, co jest poza zakresem (biblioteki, mocki, skrypty, pliki audytowane wcześniej). Audytorzy wyceniają i planują na podstawie nSLOC, więc dokładna liczba oszczędza niespodzianek obu stronom.
  • Napisz przegląd architektury. Jedna lub dwie strony opisujące kontrakty, to, jak się nawzajem wywołują, które z nich trzymają środki, oraz główne ścieżki użytkownika. Prosty diagram wywołań i przepływów tokenów oszczędza audytorom godzin reverse engineeringu.
  • Opisz prozą zachowanie i niezmienniki. Opisz, co musi robić każda funkcja zewnętrzna, oraz właściwości, które muszą zachodzić zawsze, na przykład „suma sald użytkowników równa się totalAssets” albo „tylko timelock może zmieniać opłaty”. Te stwierdzenia stają się podstawą zarówno przeglądu audytorów, jak i Twoich własnych testów niezmienników.
  • Udokumentuj znane problemy i zaakceptowane ryzyka. Wypisz ograniczenia, o których już wiesz, i decyzje projektowe, które akceptujesz, takie jak kompromisy związane z centralizacją czy nieobsługiwane typy tokenów. Dzięki temu nie trafią na listę znalezisk w raporcie, a audytorzy zobaczą, co zostało już przemyślane.

2. Higiena kodu i analiza statyczna#

Szum w kodzie kosztuje czas audytu i generuje znaleziska o niskiej wartości, więc usuń go, zanim zobaczą go audytorzy.

  • Przypnij kompilator i zejdź do zera ostrzeżeń. Użyj dokładnego pragma solidity 0.8.x; zamiast pływającego ^0.8.0 i zadbaj, by wersja, liczba przebiegów optymalizatora, via_ir i evm_version w foundry.toml odpowiadały temu, co wdrożysz. Każde ostrzeżenie kompilatora powinno zostać naprawione albo świadomie wyjaśnione.
  • Przypnij i wypisz zależności. Zablokuj OpenZeppelin Contracts i każdą inną bibliotekę na dokładnym wydaniu, na przykład jako otagowany submoduł v5.x albo dokładną wersję w package.json. Wypisz wszystkie zależności w dokumentacji audytowej i oznacz pliki skopiowane lub zmodyfikowane przez Ciebie.
  • Usuń martwy kod, TODO i wyjście debugowe. Skasuj z kontraktów w zakresie nieużywane funkcje, zakomentowany kod, importy console.log i pozostałości po helperach testowych. Otwarte TODO sygnalizują niedokończoną logikę i tak właśnie zostaną zgłoszone.
  • Uzupełnij NatSpec dla funkcji external i public. Każda funkcja external i public powinna mieć @notice, @param, @return oraz, tam gdzie to istotne, uwagę o kontroli dostępu i revertach. Używaj custom errors i emituj zdarzenia przy każdej zmianie stanu, która ma znaczenie off-chain.
  • Uruchom Slither i przejdź triage każdego znaleziska. Uruchom slither na zamrożonym commicie i zaklasyfikuj każde znalezisko jako naprawione, false positive albo zaakceptowane, z jednozdaniowym uzasadnieniem. Drugie narzędzie, na przykład Aderyn, często wyłapuje inne wzorce, a udostępnienie wyniku po triage’u mówi audytorom, które automatyczne znaleziska są już obsłużone.

3. Testy#

Testy pokazują audytorom, jak kod ma być używany, i pozwalają im szybko pisać proof of concept.

  • Pokryj każdą funkcję zewnętrzną i opublikuj raport. Każda funkcja external i public potrzebuje co najmniej jednego testu ścieżki pozytywnej. Wygeneruj raport pokrycia linii i gałęzi poleceniem forge coverage i wyjaśnij niepokryte gałęzie, zamiast je ukrywać.
  • Testuj ścieżki revertu i przypadki brzegowe. Sprawdzaj przez vm.expectRevert, że nieautoryzowane wywołania, błędne parametry, kwoty zerowe i wartości graniczne kończą się revertem z oczekiwanym custom error. To w nieprzetestowanych ścieżkach revertu kryją się błędy walidacji i kontroli dostępu.
  • Uruchamiaj testy na forku z prawdziwymi integracjami. Jeśli protokół rozmawia z zewnętrznymi kontraktami, takimi jak DEX-y, rynki pożyczkowe czy oracle, testuj na forku mainnetu lub L2 z przypiętymi numerami bloków. Mocki dowodzą jedynie, że Twój kod działa z Twoimi założeniami o drugiej stronie.

4. Fuzzing i testy niezmienników#

Fuzzing znajduje kombinacje danych wejściowych, o których nikt nie pomyślał, a testy niezmienników sprawdzają, czy system pozostaje spójny w sekwencjach wywołań.

  • Fuzzuj każdą funkcję, która przyjmuje dane liczbowe. Napisz w Foundry testy fuzz dla kwot, znaczników czasu, obliczeń udziałów i matematyki opłat, a dane wejściowe ograniczaj przez bound(), a nie nadmiarowe vm.assume. Typowe cele to kierunek zaokrągleń i przepełnienie przy wartościach skrajnych.
  • Pisz stanowe testy niezmienników z handlerami. Korzystaj z testów niezmienników Foundry z kontraktami-handlerami, które wywołują system w realistycznych sekwencjach z perspektywy kilku aktorów. Po każdej sekwencji wywołań sprawdzaj wypłacalność, zachowanie wartości i właściwości kontroli dostępu.
  • Utrzymuj zgodność spisanych niezmienników z zestawem testów. Każdy niezmiennik ze specyfikacji powinien mieć swój test niezmiennika, a każdy test niezmiennika powinien dać się powiązać ze specyfikacją. Uruchamiaj zestaw z sensowną liczbą przebiegów i głębokością w CI, a nie tylko lokalnie z ustawieniami domyślnymi.

5. Kontrola dostępu i upgrade’owalność#

Funkcje uprzywilejowane i upgrade’y mogą przenieść albo zablokować każde aktywo w systemie, więc model zaufania musi być jawny.

  • Wypisz każdą uprzywilejowaną rolę i funkcję. Udokumentuj każdą rolę, funkcje, które może wywoływać, to, kto ją posiada, i najgorszy scenariusz po przejęciu jej klucza. To sekcja o założeniach zaufania, o którą audytorzy poproszą w pierwszej kolejności.
  • Schowaj władzę admina za multisigiem i timelockiem. Krytyczne parametry i upgrade’y powinien kontrolować multisig, na przykład Safe, z TimelockController albo równoważnym opóźnieniem dla zmian, które dotyczą środków użytkowników. Udokumentuj próg podpisów i opóźnienie.
  • Sprawdź układ storage w kontraktach upgradeable. Dla proxy używaj kontraktów upgradeable z OpenZeppelin v5 z namespaced storage według ERC-7201 i waliduj upgrade’y narzędziami OpenZeppelin upgrades. Jeśli w zakresie jest upgrade, dołącz diff układu storage między wersjami.
  • Zabezpiecz initializery i funkcje upgrade’u. Wywołaj _disableInitializers() w konstruktorze implementacji, poprawnie stosuj modyfikatory initializer i reinitializer i ogranicz _authorizeUpgrade w proxy UUPS (ERC-1822). Niezabezpieczony kontrakt implementacji to znalezisko, którego audytorzy nigdy nie powinni musieć zgłaszać.

6. Integracje zewnętrzne#

Każde wywołanie zewnętrzne to założenie o cudzym kodzie, a każde założenie powinno być spisane i przetestowane.

  • Obsłuż nieaktualność i awarię oracle. Porównuj updatedAt z heartbeatem feedu, odrzucaj ceny zerowe i ujemne, a na L2 sprawdzaj feed dostępności sekwencera. Zdecyduj, co protokół robi, gdy oracle jest niedostępny, i to udokumentuj.
  • Uwzględnij osobliwości tokenów. Określ, jakie typy tokenów są obsługiwane, i obsłuż albo jawnie wyklucz tokeny fee-on-transfer, rebasing, tokeny o liczbie miejsc dziesiętnych innej niż 18 oraz niestandardowe ERC-20. Do transferów używaj SafeERC20 i mierz różnice sald tam, gdzie liczy się faktycznie otrzymana kwota.
  • Zmapuj powierzchnie reentrancy. Wypisz każde wywołanie zewnętrzne i każdy transfer tokenów, trzymaj się checks-effects-interactions i stosuj ReentrancyGuard (albo ReentrancyGuardTransient z EIP-1153) tam, gdzie stan jest współdzielony. Weź pod uwagę reentrancy między funkcjami i typu read-only oraz callbacki z tokenów ERC-721, ERC-1155 i ERC-777.
  • Weź pod uwagę MEV i front-running. Dodaj limity slippage’u i deadline’y do swapów i depozytów, zabezpiecz vaulty ERC-4626 przed atakiem inflacyjnym na pierwszego deponenta i przejrzyj każdą logikę zależną od kolejności transakcji. Udokumentuj, które ryzyka związane z kolejnością akceptujesz.

7. Wdrożenie i utrzymanie#

Zaudytowany kod jest tylko tak bezpieczny, jak sposób, w jaki się go wdraża i utrzymuje.

  • Zadbaj o powtarzalne skrypty wdrożeniowe i udokumentowane parametry. Wdrażaj skryptami Foundry z zamrożonego commita, trzymaj każdy parametr konstruktora i initializera w konfiguracji pod kontrolą wersji i włącz skrypty do zakresu audytu albo przynajmniej do pakietu przekazania. Poprawny kontrakt wdrożony z błędnymi parametrami nadal jest zepsuty.
  • Zrób próbę generalną na testnecie ze zweryfikowanymi źródłami. Uruchom dokładnie ten sam skrypt wdrożeniowy na testnecie i na forku mainnetu, zweryfikuj źródła w eksploratorze bloków i sprawdź, czy role trafiły pod zamierzone adresy. Daj audytorom adresy z testnetu, żeby mogli popracować z prawdziwym wdrożeniem.
  • Przygotuj pauzę, runbook i monitoring. Zdecyduj, kto może co pauzować, napisz krótki runbook na wypadek incydentu i ustaw alerty na zmiany ról, upgrade’y, duże wypłaty i stany pauzy. Audytorzy przejrzą ścieżki awaryjne, więc muszą one istnieć przed audytem.

8. Logistyka audytu#

Dobra logistyka utrzymuje tempo audytu i sprawia, że runda poprawek zostaje faktycznie wykorzystana.

  • Wskaż techniczną osobę kontaktową i kanał. Wyznacz jednego developera, który zna kod i potrafi odpowiedzieć na pytania w ciągu kilku godzin, i uzgodnij wspólny kanał na czas audytu. Każde pytanie bez odpowiedzi wstrzymuje przegląd.
  • Zarezerwuj czas na poprawki i rundę ich weryfikacji. Zaplanuj dostępność developerów zaraz po otrzymaniu raportu i z góry uzgodnij, że audytorzy sprawdzą poprawki. Niesprawdzone poprawki mogą wprowadzić nowe błędy już po zakończeniu audytu.
  • Ujawnij wcześniejsze audyty i przeglądy. Udostępnij wcześniejsze raporty z audytów, status ich poprawek oraz wszelkie przeglądy wewnętrzne. Audytorzy mogą się wtedy skupić na tym, co się zmieniło, i sprawdzić, czy wcześniejsze znaleziska nie wróciły.

Jak możemy pomóc#

Oferujemy Audit-Readiness Sprint o stałym zakresie, trwający od jednego do dwóch tygodni: model zagrożeń, analiza luk w zestawie testów, testy fuzz i testy niezmienników w Foundry, triage wyników Slithera, priorytetyzowana lista poprawek oraz pakiet przekazania dla audytorów z zakresem, dokumentacją i znanymi problemami. Nie jesteśmy firmą audytorską, a sprint nie zastępuje zewnętrznego audytu; przygotowuje Twój kod tak, by audytorzy mogli poświęcić swój czas logice. Szczegóły znajdziesz na stronie cennika i w opisie naszych usług Blockchain i Web3.

Jeśli chcesz otrzymać bezpłatną wycenę dla swojego kodu, opowiedz nam o swoim projekcie. Chętnie podpiszemy wzajemne NDA, zanim udostępnisz jakikolwiek kod.

Masz pomysł na projekt?

Opowiedz nam, co budujesz. Zwykle w ciągu kilku dni roboczych dostajesz uczciwą ocenę, jasny zakres i ofertę w stałej cenie albo z rozliczeniem za kamienie milowe.

Wolisz najpierw napisać? Napisz do nas

Twój rozmówca: Ing. Ismet Mesic, Tech lead.