Deep EVM #19: Eigenschaftsbasiertes Testen fuer Smart Contracts — Fuzzing mit Foundry
Engineering Team
Ueber Unit-Tests hinaus
Unit-Tests verifizieren spezifische Szenarien, aber Smart Contracts sind adversarialen Eingaben ausgesetzt — von jeder Adresse, mit jedem Wert, in jeder Reihenfolge. Eigenschaftsbasiertes Testen dreht das Paradigma um: Statt erwartete Ausgaben fuer spezifische Eingaben festzulegen, definieren Sie Eigenschaften, die fuer ALLE Eingaben gelten muessen.
Fuzz-Testing mit Foundry
Foundry generiert automatisch zufaellige Eingaben fuer Testfunktionen:
// Foundry erkennt Fuzz-Parameter automatisch
function testFuzz_transfer(
address to,
uint256 amount
) public {
vm.assume(to != address(0));
vm.assume(amount <= totalSupply);
deal(token, address(this), amount);
uint256 balBefore = token.balanceOf(address(this));
token.transfer(to, amount);
uint256 balAfter = token.balanceOf(address(this));
// Eigenschaft: Balance muss um genau amount sinken
assertEq(balBefore - balAfter, amount);
}
Foundry fuehrt diesen Test standardmaessig 256 Mal mit unterschiedlichen Zufallswerten aus.
Invarianten-Tests
Invarianten sind Bedingungen, die IMMER gelten muessen, unabhaengig davon, welche Funktionen in welcher Reihenfolge aufgerufen werden:
contract InvariantTest is Test {
TokenHandler handler;
function setUp() public {
handler = new TokenHandler(token);
targetContract(address(handler));
}
// Diese Funktion wird nach jeder zufaelligen Aufrufsequenz geprueft
function invariant_totalSupplyConstant() public {
assertEq(token.totalSupply(), INITIAL_SUPPLY);
}
function invariant_balanceSumEqualsTotal() public {
uint256 sum = 0;
for (uint i = 0; i < actors.length; i++) {
sum += token.balanceOf(actors[i]);
}
assertEq(sum, token.totalSupply());
}
}
Differentielles Fuzzing: Huff vs. Solidity
function testFuzz_differential_balanceOf(
address account,
uint256 initialBalance
) public {
vm.assume(initialBalance <= type(uint128).max);
// Gleiche Anfangsbedingungen setzen
deal(huffToken, account, initialBalance);
deal(solidityToken, account, initialBalance);
// Beide abfragen
(bool s1, bytes memory r1) = huffToken.staticcall(
abi.encodeWithSignature("balanceOf(address)", account)
);
(bool s2, bytes memory r2) = solidityToken.staticcall(
abi.encodeWithSignature("balanceOf(address)", account)
);
// Ergebnisse muessen identisch sein
assertEq(s1, s2);
assertEq(r1, r2);
}
Best Practices
- vm.assume() sparsam verwenden — Zu viele Annahmen reduzieren die Testabdeckung
- Eigene Handler schreiben — Handler steuern, welche Funktionen der Fuzzer aufruft
- Ghost-Variablen — Eigene Buchhaltung neben dem Contract fuehren
- Genug Durchlaeufe konfigurieren — foundry.toml:
[fuzz] runs = 10000 - Fehlschlaege reproduzieren — Foundry speichert Seeds fuer reproduzierbare Fehler
Fazit
Eigenschaftsbasiertes Testen und Fuzzing finden Fehler, die Unit-Tests uebersehen. Fuer Huff-Contracts, die kein Sicherheitsnetz haben, ist differentielles Fuzzing gegen eine Solidity-Referenz die staerkste Waffe in Ihrem Testarsenal.