QA-Dashboard zur Überwachung des Smart-Contract-Status Der vorherige Beitrag führte durch eine End-to-End-Implementierung: ein minimaler Token-Contract, Off-Chain-State-RecoQA-Dashboard zur Überwachung des Smart-Contract-Status Der vorherige Beitrag führte durch eine End-to-End-Implementierung: ein minimaler Token-Contract, Off-Chain-State-Reco

Ethereum-Konto-Status: QA-Pipeline für einen minimalen Token

2026/04/09 13:48
8 Min. Lesezeit
Bei Feedback oder Anliegen zu diesem Inhalt kontaktieren Sie uns bitte unter [email protected]
QA-Dashboard zur Überwachung des Smart-Contract-Zustands

Der vorherige Beitrag führte durch eine End-to-End-Implementierung: ein minimaler Token-Kontrakt, Off-Chain-Zustandsrekonstruktion und ein React-Frontend – den ganzen Weg von `mint()` bis MetaMask. Dieser Beitrag knüpft dort an: Wie führt man QA für so etwas durch?

Ich bin (noch) kein Blockchain-Ingenieur, aber QA-Muster lassen sich gut über Domänen hinweg übertragen, und das Ausleihen dessen, was anderswo bereits funktioniert, ist meine schnellste Lernmethode.

Der Kontrakt macht nur drei Dinge: `mint`, `transfer` und `burn`, aber selbst das reicht aus, um die vollständige QA-Toolchain zu praktizieren: statische Analyse, Mutationstests, Gas-Profiling, formale Verifizierung.

Der Code befindet sich in `egpivo/ethereum-account-state`.

Blockchain-QA-Pyramide: von der statischen Analyse an der Basis bis zur formalen Verifizierung an der Spitze

Womit wir begonnen haben

Bevor etwas Neues hinzugefügt wurde, hatte das Projekt bereits:

  • 21 Foundry-Unit-Tests, die jeden Zustandsübergang abdecken (Erfolg, Rückgängigmachung bei illegaler Eingabe, Ereignisemission)
  • 3 Invarianten-Tests über einen `TokenHandler`, der zufällige Sequenzen von `mint`/`transfer`/`burn` auf 10 Akteuren ausführt (jeweils 128.000 Aufrufe)
  • Fuzz-Tests, die `sum(balances) == totalSupply` für zufällige Beträge prüfen
  • TypeScript-Domain-Tests (Vitest), die die On-Chain-Zustandsmaschine spiegeln
  • CI: Kompilieren, Testen, Lint (Prettier + solhint)

Alle Tests bestanden. Die Abdeckung sah gut aus. Warum also mehr Aufwand?

Weil „alle Tests bestehen" nicht bedeutet „alle Fehler werden gefangen". 100% Zeilenabdeckung kann immer noch einen echten Fehler übersehen, wenn keine Assertion das Richtige überprüft.

Phase 1: Smart-Contract-statische Analyse und Abdeckung

Slither

Slither (Trail of Bits) erfasst Probleme, die für Tests unsichtbar sind: Reentrancy, ungeprüfte Rückgabewerte, Schnittstelleninkompatibilitäten.

./scripts/run-qa.sh slither

Ergebnis: 1 mittlerer Befund: `erc20-interface`: `transfer()` gibt kein `bool` zurück.

Das ist zu erwarten. Der Kontrakt ist absichtlich kein vollständiges ERC20: Er ist eine pädagogische Zustandsmaschine. Aber der Befund ist nicht akademisch:

Wenn jemand später diesen Token in ein Protokoll importiert, das ERC20 erwartet, würde die Schnittstelleninkompatibilität stillschweigend fehlschlagen. Slither markiert es jetzt, damit die Entscheidung bewusst ist.

Abdeckung

./scripts/run-qa.sh coverage Abdeckungsergebnis.

Eine nicht abgedeckte Funktion: `BalanceLib.gt()`. Darauf kommen wir zurück.

forge coverage-Ausgabe: 24 Tests bestanden, Token.sol-Abdeckungstabelle

Gas-Snapshots

./scripts/run-qa.sh gas

Basis-Gaskosten für die drei Operationen:

Gas in Bezug auf Operationen

Bei nachfolgenden Durchläufen vergleicht `forge snapshot — diff` mit der Basislinie. Eine 20%-Gas-Regression bei `transfer()` bedeutet reale Kosten für jeden Benutzer – sie vor dem Merge zu erfassen, ist günstig.

Phase 2: Mutationstests und formale Verifizierung

Mutationstests (Gambit)

Hier wurde es interessant. Gambit (Certora) generiert Mutanten: Kopien von `Token.sol` mit kleinen absichtlichen Fehlern (`+=` zu `-=`, `>=` zu `>`, Bedingungen negiert). Die Pipeline führt die vollständige Testsuite gegen jeden Mutanten aus. Wenn ein Mutant überlebt (alle Tests bestehen weiterhin), ist das eine konkrete Testlücke.

./scripts/run-qa.sh mutation

Ergebnis: 97,0% Mutations-Score – 32 getötet, 1 überlebt von 33 Mutanten.

Gambits Ausgabeprotokoll zeigt jeden Mutanten und was geändert wurde. Ein paar Beispiele:

Generated mutant #7: BinaryOpMutation — Token.sol:168
totalSupply = totalSupply.add(amountBalance) → totalSupply = totalSupply.sub(amountBalance)
KILLED by test_Mint_Success
Generated mutant #19: RelationalOpMutation — Token.sol:196
if (!fromBalance.gte(amountBalance)) → if (fromBalance.gte(amountBalance))
KILLED by test_Transfer_Success
Generated mutant #28: SwapArgumentsMutation — Token.sol:81
return Balance.unwrap(a) > Balance.unwrap(b) → return Balance.unwrap(b) > Balance.unwrap(a)
SURVIVED ← kein Test hat dies erfasst Gambit-Mutationstests: 32 getötet, 1 überlebt, Mutations-Score 97,0%

Der überlebende Mutant tauschte `a > b` gegen `b > a` in `BalanceLib.gt()`. Kein Test erfasste es, weil `gt()` toter Code ist. Es wird nirgendwo in `Token.sol` aufgerufen.

Die Abdeckung markierte 91,67% der Funktionen, konnte aber die Lücke nicht erklären. Mutationstests taten es: `gt()` ist toter Code, nichts ruft ihn auf, und niemand würde es bemerken, wenn er falsch wäre.

Toter oder ungeschützter Code in Smart-Contracts hat einen realen Präzedenzfall.

Die Funktion sollte nicht aufrufbar sein, aber niemand testete diese Annahme. Unser `gt()` ist im Vergleich harmlos, aber das Muster ist dasselbe: Code, der existiert, aber nie ausgeführt wird, ist Code, den niemand beobachtet.

Formale Verifizierung (Halmos)

Halmos (a16z) argumentiert symbolisch über alle möglichen Eingaben. Wo Fuzz-Tests zufällige Werte sampeln und hoffen, Randfälle zu treffen, beweist Halmos Eigenschaften erschöpfend.

./scripts/run-qa.sh halmos

Ergebnis: 9/9 symbolische Tests bestehen – alle Eigenschaften für alle Eingaben bewiesen.

Verifizierte Eigenschaften:

Verifizierte Eigenschaften

Eine praktische Anmerkung: Halmos 0.3.3 unterstützt `vm.expectRevert()` nicht, daher konnte ich Revert-Tests nicht auf normale Foundry-Weise schreiben. Die Problemumgehung ist ein try/catch-Muster – wenn der Aufruf erfolgreich ist, obwohl er rückgängig gemacht werden sollte, schlägt `assert(false)` den Beweis fehl:

function check_mint_reverts_on_zero_address(uint256 amount) public {
vm.assume(amount > 0);
try token.mint(address(0), amount) {
assert(false); // sollte hier nicht ankommen
} catch {
// erwarteter Revert - Halmos beweist, dass dieser Pfad immer genommen wird
}
}

Nicht das Hübscheste, aber es funktioniert – Halmos beweist die Eigenschaft immer noch für alle Eingaben. Das ist die Art von Sache, die man nur herausfindet, indem man das Tool tatsächlich ausführt.

Zum Kontext, warum formale Verifizierung wichtig ist:

Die Schwachstelle war im Code, von jedem überprüfbar, aber kein Tool oder Test erfasste sie vor der Bereitstellung. Symbolische Beweiser wie Halmos existieren genau, um diese Lücke zu schließen – sie sampeln nicht; sie erschöpfen den Eingaberaum.

Halmos-Ausgabe: 9 Tests bestanden, 0 fehlgeschlagen, symbolische Testergebnisse

Die Testdatei ist `contracts/test/Token.halmos.t.sol`.

Phase 3: Cross-Layer-Property-Testing

Die Architektur des ersten Beitrags hat eine TypeScript-Domain-Schicht, die die On-Chain-Zustandsmaschine spiegelt. Diese Phase testet, ob die beiden tatsächlich übereinstimmen.

Property-basiertes Testen mit fast-check

Ich habe fast-check Property-Tests für die TypeScript-Domain-Schicht hinzugefügt, die spiegeln, was Foundrys Fuzzer für Solidity tut:

npm test - tests/unit/property.test.ts

Ergebnis: 9/9 Property-Tests bestehen nach Behebung eines echten Fehlers.

Getestete Eigenschaften:

  • `Balance`: Kommutativität, Assoziativität, Identität, Inverse, Vergleichskonsistenz
  • `Token`: Invariante `sum(balances) == totalSupply` unter zufälligen Operationssequenzen (200 Durchläufe, jeweils 50 Ops)
  • `Token`: `totalSupply` nicht-negativ nach zufälligen Sequenzen
  • `mint` ist immer erfolgreich für gültige Eingaben
  • `transfer` bewahrt `totalSupply`

Der Fehler, den fast-check fand

fast-check fand einen echten Cross-Layer-Konsistenzfehler in `Token.ts` `transfer()`. Das verkleinerte Gegenbeispiel war sofort klar:

Property failed after 3 tests
Shrunk 2 time(s)
Counterexample: transfer(from=0xaaa…, to=0xaaa…, amount=1n)
→ from == to (Selbstübertragung)
→ verifyInvariant() returned false

Selbstübertragung (`from == to`) brach die `sum(balances) == totalSupply`-Invariante. `toBalance` wurde gelesen, bevor `fromBalance` aktualisiert wurde, sodass bei `from == to` der veraltete Wert die Abzug überschrieb:

// Vorher (fehlerhaft)
const fromBalance = this.getBalance(from);
const toBalance = this.getBalance(to); // ← veraltet, wenn from == to
this.accounts.set(from.getValue(), fromBalance.subtract(amount));
this.accounts.set(to.getValue(), toBalance.add(amount)); // ← überschreibt die Subtraktion

Fix: `toBalance` nach dem Schreiben von `fromBalance` lesen, entsprechend Soliditys Storage-Semantik:

// Nachher (behoben)
const fromBalance = this.getBalance(from);
this.accounts.set(from.getValue(), fromBalance.subtract(amount));
const toBalance = this.getBalance(to); // ← liest jetzt aktualisierten Wert
this.accounts.set(to.getValue(), toBalance.add(amount));

Der Solidity-Kontrakt war nicht betroffen: Er liest Storage nach jedem Schreibvorgang neu. Aber der TypeScript-Spiegel hatte eine subtile Reihenfolgeabhängigkeit, die kein existierender Unit-Test abdeckte.

Cross-Layer-Inkompatibilitäten in größerem Maßstab waren katastrophal.

Unser Selbstübertragungs-Fehler hätte niemandem Geld gekostet, aber der Fehlermodus ist strukturell derselbe: zwei Schichten, die übereinstimmen sollen, tun es nicht.

Fallstricke auf dem Weg

QA-Tools auf einem bestehenden Projekt auszuführen ist nie nur „installieren und ausführen". Ein paar Dinge sind kaputt gegangen, bevor sie funktionierten:

  • 0% Abdeckung, weil `foundry.toml` keinen Testpfad hatte: Der erste `forge coverage`-Durchlauf lieferte 0% durchgehend. Es stellte sich heraus, dass `foundry.toml` weder `test = "contracts/test"` noch `script = "contracts/script"` spezifizierte, sodass Forge keine Tests entdeckte. Der Coverage-Befehl war erfolgreich – er hatte nur nichts zu erfassen. Das war der irreführendste Fehler: ein grüner Durchlauf ohne nützliche Ausgabe.
  • `InvariantTest`-Import in forge-std v1.14.0 entfernt: `Invariant.t.sol` importierte `InvariantTest` aus `forge-std`, was in einem aktuellen Release entfernt wurde. Die Kompilierung schlug mit einem undurchsichtigen „Symbol nicht gefunden"-Fehler fehl. Die Lösung bestand darin, den Import zu entfernen – `Test` allein reicht jetzt für Foundrys Invariantentests aus.
  • `uint256(token.totalSupply())` vs `Balance.unwrap()`: Tests verwendeten eine explizite Umwandlung, um den zugrunde liegenden `uint256` aus dem benutzerdefinierten `Balance`-Typ zu extrahieren. Es kompilierte, aber es ist das falsche Idiom – `Balance.unwrap(token.totalSupply())` ist das, wofür das UDVT-System konzipiert ist. Angewendet in `Token.t.sol`, `Invariant.t.sol` und `DeploySepolia.s.sol`.

Pipeline-Design

Alles läuft über zwei Skripte:

  • scripts/setup-qa-tools.sh`: installiert Slither, Halmos, Gambit (idempotent)
  • `scripts/run-qa.sh`: führt Prüfungen aus, speichert zeitgestempelte Ergebnisse in `qa-results/`

./scripts/run-qa.sh slither gas # nur statische Analyse + Gas
./scripts/run-qa.sh mutation # nur Mutationstests
./scripts/run-qa.sh all # alles

Nicht jede Prüfung ist schnell. Slither und Coverage laufen bei jedem Commit. Mutationstests und Halmos sind langsamer – besser geeignet für wöchentliche oder Pre-Release-Durchläufe.

Zusammenfassung

Blockchain-QA-Toolchain: was jede Schicht erfasst – von der statischen Analyse bis zum Cross-Layer-Property-Testing

Fünf QA-Schichten, jede erfasst eine andere Problemklasse.

Schichterklärung

Gambit und fast-check lieferten in dieser Runde die umsetzbarsten Ergebnisse.

CI-Pipeline

Die QA-Prüfungen sind jetzt als sechsstufige Pipeline in GitHub Actions integriert:

CI-Pipeline: Build & Lint verzweigt zu Test, Coverage, Gas, Slither und Audit-Stufen

GitHub Actions-Pipeline: Build & Lint steuert alle nachgelagerten Stufen.

Stufenerklärung

Referenzen

  • Ethereum Account State-Quelle: [github.com/egpivo/ethereum-account-state](https://github.com/egpivo/ethereum-account-state)
  • Vorheriger Beitrag: Ethereum Account State
  • Slither: github.com/crytic/slither
  • Gambit: github.com/Certora/gambit
  • Halmos: github.com/a16z/halmos
  • fast-check: github.com/dubzzz/fast-check
  • Foundry: getfoundry.sh

Hinweise

  • Dieser Beitrag ist eine Adaption meines ursprünglichen Blog-Beitrags.

Ethereum Account State: QA Pipeline for a Minimal Token wurde ursprünglich in Coinmonks auf Medium veröffentlicht, wo die Leute die Konversation fortsetzen, indem sie diese Geschichte hervorheben und darauf reagieren.

Haftungsausschluss: Die auf dieser Website veröffentlichten Artikel stammen von öffentlichen Plattformen und dienen ausschließlich zu Informationszwecken. Sie spiegeln nicht unbedingt die Ansichten von MEXC wider. Alle Rechte verbleiben bei den ursprünglichen Autoren. Sollten Sie der Meinung sein, dass Inhalte die Rechte Dritter verletzen, wenden Sie sich bitte an [email protected] um die Inhalte entfernen zu lassen. MEXC übernimmt keine Garantie für die Richtigkeit, Vollständigkeit oder Aktualität der Inhalte und ist nicht verantwortlich für Maßnahmen, die aufgrund der bereitgestellten Informationen ergriffen werden. Die Inhalte stellen keine finanzielle, rechtliche oder sonstige professionelle Beratung dar und sind auch nicht als Empfehlung oder Billigung von MEXC zu verstehen.

$30,000 in PRL + 15,000 USDT

$30,000 in PRL + 15,000 USDT$30,000 in PRL + 15,000 USDT

Deposit & trade PRL to boost your rewards!