QA-töölaud jälgib nutikate lepingute olekut. Eelmine postitus käis läbi lõpust lõpuni toimiva rakenduse: minimaalse tokeni lepingu ja välisahela oleku taastamise.QA-töölaud jälgib nutikate lepingute olekut. Eelmine postitus käis läbi lõpust lõpuni toimiva rakenduse: minimaalse tokeni lepingu ja välisahela oleku taastamise.

Ethereum’i kontoseis: QA-torustik minimaalse tokeni jaoks

2026/04/09 13:48
7 minutiline lugemine
Selle sisu kohta tagasiside või murede korral võtke meiega ühendust aadressil [email protected]
QA töölaualt jälgitakse nutikate lepingute olekut

Eelmine postitus läbis lõpuni implementatsiooni: minimaalse tokeni lepingu, väljastpoolset oleku taastamist ja Reacti esiletasandit – kogu tee `mint()`-ist MetaMaskini. See postitus jätkab eelmise punktist: kuidas seda sellist asja QA-testida?

Ma ei ole veel blockchain-insener, kuid QA-mustrid on ülevalt domäänide vahel hästi ülekantavad ja ma õpin kõige kiiremini, kasutades neid, mis juba mujal toimivad.

Leping teeb vaid kolm asja: `mint`, `transfer` ja `burn`, kuid isegi see on piisav, et harjutada täielikku QA-tööriistariba: staatiline analüüs, mutatsioonitestid, gaasiprofiilimine, formaalne verifitseerimine.

Kood asub kaustas `egpivo/ethereum-account-state`.

Blockchaini QA-püramiid: alusel staatiline analüüs, tippu formaalne verifitseerimine

Mis me alustasime

Enne uute asjade lisamist oli projektil juba:

  • 21 Foundry ühiktesti, mis katavad iga olekuülemineku (edukas käivitus, tagasipöördumine kehtetu sisendi puhul, sündmuste saatmine)
  • 3 invariantsed testid läbi `TokenHandler`-i, mis käivitab juhuslikke `mint`/`transfer`/`burn`-järjestusi 10 aktori kohta (igaühe kohta 128 000 käsku)
  • Fuzz-testid, mis kontrollivad `sum(balances) == totalSupply` juhuslike summade puhul
  • TypeScripti domeenitestid (Vitest), mis peegeldavad ahelas olevat olekumasinat
  • CI: kompileerimine, testid, lintimine (Prettier + solhint)

Kõik testid läbisid. Katvus näis korras. Miks siis veel rohkem teha?

Sellepärast, et „kõik testid läbisid“ ei tähenda „kõiki vigu on avastatud“. 100% reakatvus võib ikka jätta tegeliku vea silmata, kui ükski väide ei kontrolli õiget asja.

Faas 1: Nutikate lepingute staatiline analüüs ja katvus

Slither

Slither (Trail of Bits) tuvastab probleeme, mida testid ei näe: reentrancy, kontrollimata tagastusväärtused, liidese sobimatuse.

./scripts/run-qa.sh slither

Tulemus: 1 keskmise tõsiduse leidmine: `erc20-interface`: `transfer()` ei tagasta `bool`-väärtust.

See on oodatud. Leping pole intensionaalselt täielik ERC20: see on õppetöö eesmärgil loodud olekumasin. Kuid leidmine ei ole akadeemiline:

Kui keegi hiljem impordib selle tokeni protokolli, mis ootab ERC20-d, siis liidese sobimatuse tõttu toimub vaikne nurjumine. Slither tähistab selle juba nüüd, et otsus oleks teadlik.

Katvus

./scripts/run-qa.sh coverageKatvuse tulemus.

Üks katmata funktsioon: `BalanceLib.gt()`. Me pöördume selle juurde tagasi.

forge coverage väljund: 24 testi läbisid, Token.sol katvustabel

Gaasipildid

./scripts/run-qa.sh gas

Kolme operatsiooni baasgaaskulud:

Gaas operatsioonite järgi

Järgmistel käsklustel võrdleb `forge snapshot — diff` baaspilti. 20% gaasiregressioon `transfer()`-is on igale kasutajale reaalne kulutus – selle avastamine enne ühendamist on odav.

Faas 2: Mutatsioonitestid ja formaalne verifitseerimine

Mutatsioonitestid (Gambit)

Siin sai asi huvitavaks. Gambit (Certora) genereerib mutandid: `Token.sol` koopiad, milles on väikesed teadlikud vead (`+=` → `-=`, `>=` → `>`, tingimuste eitamine). Toru käivitab täieliku testikomplekti iga mutandi vastu. Kui mutand ellu jääb (kõik testid läbivad ikka), siis on see konkreetne testilüng.

./scripts/run-qa.sh mutation

Tulemus: 97,0% mutatsiooniskoor — 32 tapetud, 1 ellu jäänud 33-st mutandist.

Gambiti väljundlogis on iga mutand ja see, mida see muutis. Mõned näited:

Genereeritud mutand nr 7: BinaryOpMutation — Token.sol:168
totalSupply = totalSupply.add(amountBalance) → totalSupply = totalSupply.sub(amountBalance)
TAPETUD test_Mint_Success-i poolt
Genereeritud mutand nr 19: RelationalOpMutation — Token.sol:196
if (!fromBalance.gte(amountBalance)) → if (fromBalance.gte(amountBalance))
TAPETUD test_Transfer_Success-i poolt
Genereeritud mutand nr 28: SwapArgumentsMutation — Token.sol:81
return Balance.unwrap(a) > Balance.unwrap(b) → return Balance.unwrap(b) > Balance.unwrap(a)
ELLU JÄÄNUD ← ükski test seda ei avastanudGambiti mutatsioonitestid: 32 tapetud, 1 ellu jäänud, mutatsiooniskoor 97,0%

Ellu jäänud mutand vahetas `a > b` `b > a`-ks `BalanceLib.gt()`-is. Ükski test seda ei avastanud, sest `gt()` on surnud kood. See ei ole kusagil `Token.sol`-is kunagi välja kutsutud.

Katvus näitas 91,67% funktsioone, kuid ei suutnud selgitada lünga. Mutatsioonitestid seda tegid: `gt()` on surnud kood, seda ei kutsuta kuskil välja ning kui see vale oleks, ei märkaks seda keegi.

Surnud või kaitsemata kood nutikates lepingutes on juba tõesti esinenud.

Funktsioon polnud mõeldud väljakutsumiseks, kuid keegi ei testinud seda eeldust. Meie `gt()` on sellest võrreldes harmooniline, kuid muster on sama: kood, mis eksisteerib, kuid mida ei kasutata, on kood, millele keegi ei pööra tähelepanu.

Formaalne verifitseerimine (Halmos)

Halmos (a16z) analüüsib kõiki võimalikke sisendeid sümboliliselt. Kus fuzz-testid proovivad juhuslikke väärtusi ja lootavad äärmuslike olukordade tabamist, tõestab Halmos omadusi põhjalikult.

./scripts/run-qa.sh halmos

Tulemus: 9/9 sümbolilist testi läbisid — kõik omadused tõestatud kõigi sisendite jaoks.

Tõestatud omadused:

Tõestatud omadused

Üks praktiline märkus: Halmos 0.3.3 ei toeta `vm.expectRevert()`-i, seega ei saanud ma revert-teste kirjutada tavapärase Foundry viisi järgi. Alternatiiviks on try/catch-muster — kui kõne õnnestub siis, kui peaks tagasipöörduma, siis `assert(false)` teeb tõestuse ebaõnnestunuks:

function check_mint_reverts_on_zero_address(uint256 amount) public {
vm.assume(amount > 0);
try token.mint(address(0), amount) {
assert(false); // ei tohiks siia jõuda
} catch {
// oodatud tagasipöördumine – Halmos tõestab, et see tee on alati valitud
}
}

Ei ole ilusaim, kuid see töötab — Halmos tõestab ikka omaduse kõigi sisendite jaoks. Sellised asjad selguvad ainult tööriista tegelikku käivitamist proovides.

Selle kontekstis, miks formaalne verifitseerimine on oluline:

Vulnerability oli koodis, mida igaüks saab üle vaadata, kuid ükski tööriist ega test ei avastanud seda enne paigaldamist. Sümbolilised tõestajad nagu Halmos on just selle lünka kinnitamiseks loodud — nad ei proovi, nad läbivad kogu sisendruumi.

Halmosi väljund: 9 testi läbisid, 0 nurjusid, sümboliliste testide tulemused

Testifail on `contracts/test/Token.halmos.t.sol`.

Faas 3: Ristkihilised omadustestid

Eelneva postituse arhitektuuril on TypeScripti domeenikiht, mis peegeldab ahelas olevat olekumasinat. See faas testib, kas need kaks tegelikult kokku sobivad.

Omadustestid fast-check’iga

Ma lisasin TypeScripti domeenikihi jaoks fast-check’i omadusteste, mis peegeldavad seda, mida Foundry fuzzer teeb Solidity jaoks:

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

Tulemus: 9/9 omadustesti läbisid pärast tegeliku vea parandamist.

Testitud omadused:

  • `Balance`: kommutatiivsus, assotsiatiivsus, identiteet, pöördväärtus, võrdluskooskõla
  • `Token`: invariant `sum(balances) == totalSupply` juhuslike operatsioonijärjestuste all (200 käiku, igaühes 50 operatsiooni)
  • `Token`: `totalSupply` mitte-negatiivne pärast juhuslikke järjestusi
  • `mint` õnnestub alati kehtivate sisendite puhul
  • `transfer` säilitab `totalSupply`

Viga, mille fast-check avastas

fast-check avastas tegeliku ristkihilise kooskõlavuse vea `Token.ts` `transfer()`-is. Kokkupressitud kontranaide oli kohe selge:

Omadus ebaõnnestus pärast 3 testi
Kokkupressitud 2 korda
Kontranaide: transfer(from=0xaaa…, to=0xaaa…, amount=1n)
→ from == to (iseleülekanne)
→ verifyInvariant() tagastas false

Iseleülekanne (`from == to`) rikkus `sum(balances) == totalSupply` invarianti. `toBalance` loeti enne, kui `fromBalance` värskendati, seega kui `from == to`, siis vananenud väärtus kirjutas lahutamise üle:

// Enne (vigane)
const fromBalance = this.getBalance(from);
const toBalance = this.getBalance(to); // ← vananenud, kui from == to
this.accounts.set(from.getValue(), fromBalance.subtract(amount));
this.accounts.set(to.getValue(), toBalance.add(amount)); // ← kirjutab lahutamise üle

Parandus: loe `toBalance` pärast `fromBalance` kirjutamist, vastavalt Solidity salvestussemantikale:

// Pärast (parandatud)
const fromBalance = this.getBalance(from);
this.accounts.set(from.getValue(), fromBalance.subtract(amount));
const toBalance = this.getBalance(to); // ← loeb nüüd värskendatud väärtust
this.accounts.set(to.getValue(), toBalance.add(amount));

Solidity lepingut ei mõjutanud: see loeb salvestust iga kirjutamise järel uuesti. Kuid TypeScripti peegel oli subtiilselt sõltuv järjestusest, mida ükski olemasolev ühiktest ei kattanud.

Suuremas mahus esinenud ristkihilised mittekooskõlad on olnud katastroofilised.

Meie iseülekande viga ei oleks kellegi rahalisi kaotusi põhjustanud, kuid vea tüüp on struktuuris sama: kaks kihti, mis peaksid kokku sobima, ei sobi.

Teed takistavad kivid

QA-tööriistade käivitamine olemasolevas projektsis ei ole kunagi lihtsalt „paigalda ja käivita“. Mõned asjad läksid enne tööle minekut katki:

  • 0% katvus, sest `foundry.toml`-is puudus testitee: Esimene `forge coverage` käivitus andis kogu ulatuses 0%. Selgus, et `foundry.toml`-is ei olnud määratud `test = “contracts/test”` ega `script = “contracts/script”`, seega ei leidnud Forge ühtegi testi. Katvuskäsk edukalt läbis — aga tal polnud midagi kattevaks. See oli kõige petlikum nurjumine: roheline käivitus ilma kasuliku väljundita.
  • `InvariantTest` import kadus forge-std v1.14.0-s: `Invariant.t.sol` importis `InvariantTest`-i `forge-std`-ist, mille eemaldati hiljuti. Kompileerimine nurjus häguse „sümbolit ei leitud“ veategaga. Parandus oli importi eemaldada — `Test` üksi on Foundry invariantsel testidel praegu piisav.
  • `uint256(token.totalSupply())` vs `Balance.unwrap()`: Testid kasutasid otseseid castitusi, et eraldada kasutaja defineeritud `Balance` tüübist aluseks olev `uint256`. See kompileerus, kuid see ei ole õige idioom — `Balance.unwrap(token.totalSupply())` on just see, milleks UDVT-süsteem on loodud. Rakendatud `Token.t.sol`, `Invariant.t.sol` ja `DeploySepolia.s.sol`-is.

Toru kujundus

Kõik käivitub kahe skripti kaudu:

  • scripts/setup-qa-tools.sh`: paigaldab Slitheri, Halmose, Gambiti (idempotentne)
  • `scripts/run-qa.sh`: käivitab kontrollid, salvestab ajatähistega tulemused kausta `qa-results/`

./scripts/run-qa.sh slither gas # ainult staatiline analüüs + gaas
./scripts/run-qa.sh mutation # ainult mutatsioonitestid
./scripts/run-qa.sh all # kõik

Ei iga kontroll pole kiire. Slither ja katvus käivitatakse iga commiti järel. Mutatsioonitestid ja Halmos on aeglasemad — paremini sobivad nädalaselt või enne versiooni avaldamist.

Kokkuvõte

Blockchaini QA-tööriistariba: mida iga kiht avastab — staatilisest analüüsist ristkihiliste omadustestideni

Viis QA-kihti, millest igaüks avastab teistsuguse probleemiklassi.

Kihi seletus

Gambit ja fast-check andsid selles voorus kõige tegutsemad tulemused.

CI-toru

QA-kontrollid on nüüd ühendatud GitHub Actionsi kui kuuefaasilise toru:

CI-toru: Build & Lint jaguneb Test, Coverage, Gas, Slither ja Audit faasidesse

GitHub Actionsi toru: Build & Lint blokeerib kõiki järgnevaid faase.

Faasi seletus

Viited

  • Ethereumi konto oleku lähtekood: [github.com/egpivo/ethereum-account-state](https://github.com/egpivo/ethereum-account-state)
  • Eelmine postitus: 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

Märkused

  • See postitus on kohandatud minu originaalblogipostitusest.

Ethereumi konto olek: Minimaalse tokeni QA-toru avaldati esmakordselt Coinmonksis Mediumis, kus inimesed jätkavad vestlust, esiletõstes ja reageerides sellele loomule.

Lahtiütlus: Sellel saidil taasavaldatud artiklid pärinevad avalikelt platvormidelt ja on esitatud ainult informatiivsel eesmärgil. Need ei kajasta tingimata MEXC seisukohti. Kõik õigused jäävad algsetele autoritele. Kui arvate, et sisu rikub kolmandate isikute õigusi, võtke selle eemaldamiseks ühendust aadressil [email protected]. MEXC ei garanteeri sisu täpsust, täielikkust ega ajakohasust ega vastuta esitatud teabe põhjal võetud meetmete eest. Sisu ei ole finants-, õigus- ega muu professionaalne nõuanne ega seda tohiks pidada MEXC soovituseks ega toetuseks.

$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!