hacktricks/src/blockchain/smart-contract-security/mutation-testing-with-slither.md

5.8 KiB
Raw Blame History

Mutation Testing dla Solidity z Slither (slither-mutate)

{{#include ../../../banners/hacktricks-training.md}}

Mutation testing "tests your tests" poprzez systematyczne wprowadzanie drobnych zmian (mutantów) w twoim kodzie Solidity i ponowne uruchamianie zestawu testów. Jeśli test się nie powiedzie, mutant zostaje zabity. Jeśli testy nadal przechodzą, mutant przeżywa, ujawniając ślepy punkt w twoim zestawie testów, którego pokrycie linii/gałęzi nie wykryje.

Kluczowa myśl: Pokrycie pokazuje, że kod został wykonany; mutation testing pokazuje, czy zachowanie jest faktycznie sprawdzone.

Dlaczego pokrycie może wprowadzać w błąd

Rozważ ten prosty warunek progowy:

function verifyMinimumDeposit(uint256 deposit) public returns (bool) {
if (deposit >= 1 ether) {
return true;
} else {
return false;
}
}

Testy jednostkowe, które sprawdzają tylko wartość poniżej oraz wartość powyżej progu, mogą osiągnąć 100% pokrycia linii/gałęzi, jednocześnie nie asercjonując granicy równości (==). Refaktoring do deposit >= 2 ether nadal przejdzie takie testy, cicho łamiąc logikę protokołu.

Mutation testing ujawnia tę lukę, mutując warunek i weryfikując, że testy nie przejdą.

Najczęstsze operatory mutacji w Solidity

Slithers mutation engine stosuje wiele drobnych modyfikacji zmieniających semantykę, takich jak:

  • Zamiana operatorów: +-, */, itp.
  • Zamiana przypisania: +==, -==
  • Zamiana stałych: niezerowe → 0, truefalse
  • Negacja/zamiana warunku w if/pętlach
  • Komentowanie całych linii (CR: Comment Replacement)
  • Zamiana linii na revert()
  • Zamiany typów danych: np. int128int64

Cel: Zabić 100% wygenerowanych mutantów lub uzasadnić ocalałe przypadki jasnym wyjaśnieniem.

Uruchamianie Mutation testing za pomocą slither-mutate

Wymagania: Slither v0.10.2+.

  • Wypisz opcje i mutatory:
slither-mutate --help
slither-mutate --list-mutators
  • Przykład Foundry (przechwyć wyniki i zachowaj pełny log):
slither-mutate ./src/contracts --test-cmd="forge test" &> >(tee mutation.results)
  • Jeśli nie używasz Foundry, zamień --test-cmd na komendę uruchamiającą testy (np. npx hardhat test, npm test).

Artefakty i raporty są domyślnie przechowywane w ./mutation_campaign. Niewykryte (przetrwałe) mutanty są tam kopiowane do wglądu.

Zrozumienie wyników

Wiersze raportu wyglądają tak:

INFO:Slither-Mutate:Mutating contract ContractName
INFO:Slither-Mutate:[CR] Line 123: 'original line' ==> '//original line' --> UNCAUGHT
  • The tag in brackets is the mutator alias (e.g., CR = Comment Replacement).
  • UNCAUGHT means tests passed under the mutated behavior → missing assertion.

Reducing runtime: prioritize impactful mutants

Mutation campaigns can take hours or days. Tips to reduce cost:

  • Scope: Start with critical contracts/directories only, then expand.
  • Prioritize mutators: If a high-priority mutant on a line survives (e.g., entire line commented), you can skip lower-priority variants for that line.
  • Parallelize tests if your runner allows it; cache dependencies/builds.
  • Fail-fast: stop early when a change clearly demonstrates an assertion gap.

Triage workflow for surviving mutants

  1. Inspect the mutated line and behavior.
  • Reproduce locally by applying the mutated line and running a focused test.
  1. Strengthen tests to assert state, not only return values.
  • Add equality-boundary checks (e.g., test threshold ==).
  • Assert post-conditions: balances, total supply, authorization effects, and emitted events.
  1. Replace overly permissive mocks with realistic behavior.
  • Ensure mocks enforce transfers, failure paths, and event emissions that occur on-chain.
  1. Add invariants for fuzz tests.
  • E.g., conservation of value, non-negative balances, authorization invariants, monotonic supply where applicable.
  1. Re-run slither-mutate until survivors are killed or explicitly justified.

Case study: revealing missing state assertions (Arkis protocol)

A mutation campaign during an audit of the Arkis DeFi protocol surfaced survivors like:

INFO:Slither-Mutate:[CR] Line 33: 'cmdsToExecute.last().value = _cmd.value' ==> '//cmdsToExecute.last().value = _cmd.value' --> UNCAUGHT

Zakomentowanie przypisania nie złamało testów, co dowodzi brakujących asercji stanu po wykonaniu. Przyczyna: kod zaufał kontrolowanej przez użytkownika zmiennej _cmd.value zamiast walidować rzeczywiste transfery tokenów. Atakujący mógłby rozdesynchronizować oczekiwane i rzeczywiste transfery, aby opróżnić środki. Skutek: ryzyko wysokiej wagi dla wypłacalności protokołu.

Wytyczne: Traktuj mutanty, które przetrwały i wpływają na transfery wartości, księgowość lub kontrolę dostępu, jako wysokiego ryzyka, dopóki nie zostaną wyeliminowane.

Praktyczna lista kontrolna

  • Uruchom ukierunkowaną kampanię:
  • slither-mutate ./src/contracts --test-cmd="forge test"
  • Przejrzyj mutanty, które przetrwały, i napisz testy/inwarianty, które zawiodłyby przy zmienionym zachowaniu.
  • Zweryfikuj salda, podaż, uprawnienia i zdarzenia.
  • Dodaj testy brzegowe (==, overflows/underflows, zero-address, zero-amount, empty arrays).
  • Zastąp nierealistyczne mocki; zasymuluj tryby awarii.
  • Powtarzaj, aż wszystkie mutanty zostaną wyeliminowane lub uzasadnione komentarzami i wyjaśnieniem.

Źródła

{{#include ../../../banners/hacktricks-training.md}}