Dasbor QA memantau status smart contract Postingan sebelumnya membahas implementasi end-to-end: kontrak token minimal, rekonstruksi status off-chainDasbor QA memantau status smart contract Postingan sebelumnya membahas implementasi end-to-end: kontrak token minimal, rekonstruksi status off-chain

Status Akun Ethereum: Pipeline QA untuk Token Minimal

2026/04/09 13:48
durasi baca 8 menit
Untuk memberikan masukan atau menyampaikan kekhawatiran terkait konten ini, silakan hubungi kami di crypto.news@mexc.com
Dashboard QA memantau status smart contract

Postingan sebelumnya membahas implementasi end-to-end: kontrak token minimal, rekonstruksi status off-chain, dan frontend React — dari `mint()` hingga MetaMask. Postingan ini melanjutkan dari situ: bagaimana melakukan QA terhadap sesuatu seperti ini?

Saya bukan insinyur blockchain (belum), tetapi pola QA dapat diterapkan dengan baik di berbagai domain, dan meminjam apa yang sudah berhasil di tempat lain adalah cara tercepat saya untuk belajar.

Kontrak hanya melakukan tiga hal: `mint`, `transfer`, dan `burn`, tetapi bahkan itu sudah cukup untuk mempraktikkan toolchain QA lengkap: analisis statis, mutation testing, profiling gas, verifikasi formal.

Kodenya ada di `egpivo/ethereum-account-state`.

Piramida QA Blockchain: dari analisis statis di dasar hingga verifikasi formal di puncak

Apa yang sudah ada sebelumnya

Sebelum menambahkan hal baru, proyek sudah memiliki:

  • 21 unit test Foundry yang mencakup setiap transisi status (sukses, revert pada input ilegal, emisi event)
  • 3 invariant test melalui `TokenHandler` yang menjalankan urutan acak `mint`/`transfer`/`burn` pada 10 aktor (masing-masing 128k panggilan)
  • Fuzz test yang memeriksa `sum(balances) == totalSupply` untuk jumlah acak
  • Test domain TypeScript (Vitest) yang mencerminkan state machine on-chain
  • CI: compile, test, lint (Prettier + solhint)

Semua test berhasil. Coverage terlihat baik. Jadi mengapa repot-repot menambahkan lebih banyak?

Karena "semua test berhasil" tidak berarti "semua bug tertangkap." 100% line coverage masih bisa melewatkan bug nyata jika tidak ada assertion yang memeriksa hal yang benar.

Fase 1: Analisis statis dan coverage smart contract

Slither

Slither(Trail of Bits) menangkap masalah yang tidak terlihat oleh test: reentrancy, nilai return yang tidak diperiksa, ketidakcocokan interface.

./scripts/run-qa.sh slither

Hasil: 1 temuan Medium: `erc20-interface`: `transfer()` tidak mengembalikan `bool`.

Ini sudah diharapkan. Kontrak memang sengaja bukan ERC20 lengkap: ini adalah state machine edukatif. Tetapi temuan ini bukan hanya akademis:

Jika seseorang nanti mengimpor token ini ke dalam protokol yang mengharapkan ERC20, ketidakcocokan interface akan gagal secara diam-diam. Slither menandainya sekarang agar keputusannya disadari.

Coverage

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

Satu fungsi yang tidak tercakup: `BalanceLib.gt()`. Kita akan kembali ke ini.

output forge coverage: 24 test berhasil, tabel coverage Token.sol

Gas snapshot

./scripts/run-qa.sh gas

Biaya gas baseline untuk tiga operasi:

Gas dalam hal operasi

Pada eksekusi berikutnya, `forge snapshot — diff` membandingkan dengan baseline. Regresi gas 20% pada `transfer()` adalah biaya nyata untuk setiap pengguna — menangkapnya sebelum merge sangat murah.

Fase 2: Mutation testing dan verifikasi formal

Mutation testing (Gambit)

Di sinilah hal menjadi menarik. Gambit(Certora) menghasilkan mutan: salinan `Token.sol` dengan bug kecil yang disengaja (`+=` menjadi `-=`, `>=` menjadi `>`, kondisi dinegasikan). Pipeline menjalankan suite test lengkap terhadap setiap mutan. Jika mutan bertahan (semua test masih berhasil), itu adalah celah test konkret.

./scripts/run-qa.sh mutation

Hasil: skor mutation 97,0% — 32 terbunuh, 1 bertahan dari 33 mutan.

Log output Gambit menunjukkan setiap mutan dan apa yang diubahnya. Beberapa contoh:

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 ← tidak ada test yang menangkap iniMutation testing Gambit: 32 terbunuh, 1 bertahan, skor mutation 97,0%

Mutan yang bertahan menukar `a > b` menjadi `b > a` di `BalanceLib.gt()`. Tidak ada test yang menangkapnya karena `gt()` adalah dead code. Tidak pernah dipanggil di mana pun dalam `Token.sol`.

Coverage menandai 91,67% fungsi tetapi tidak bisa menjelaskan celahnya. Mutation testing bisa: `gt()` adalah dead code, tidak ada yang memanggilnya, dan tidak ada yang akan menyadari jika salah.

Dead code atau kode yang tidak terlindungi dalam smart contract memiliki preseden nyata.

Fungsi itu tidak dimaksudkan untuk dapat dipanggil, tetapi tidak ada yang menguji asumsi itu. `gt()` kita tidak berbahaya dibandingkan, tetapi polanya sama: kode yang ada tetapi tidak pernah dijalankan adalah kode yang tidak diawasi siapa pun.

Verifikasi formal (Halmos)

Halmos(a16z) bernalar tentang semua input yang mungkin secara simbolis. Di mana fuzz test mengambil sampel nilai acak dan berharap mencapai edge case, Halmos membuktikan properti secara menyeluruh.

./scripts/run-qa.sh halmos

Hasil: 9/9 symbolic test berhasil — semua properti terbukti untuk semua input.

Properti yang diverifikasi:

Properti yang diverifikasi

Satu catatan praktis: Halmos 0.3.3 tidak mendukung `vm.expectRevert()`, jadi saya tidak bisa menulis revert test dengan cara Foundry normal. Solusinya adalah pola try/catch — jika panggilan berhasil padahal seharusnya revert, `assert(false)` gagalkan buktinya:

function check_mint_reverts_on_zero_address(uint256 amount) public {
vm.assume(amount > 0);
try token.mint(address(0), amount) {
assert(false); // seharusnya tidak mencapai di sini
} catch {
// expected revert - Halmos membuktikan jalur ini selalu diambil
}
}

Tidak terlalu cantik, tetapi berhasil — Halmos masih membuktikan properti untuk semua input. Ini jenis hal yang hanya Anda ketahui dengan benar-benar menjalankan tool-nya.

Untuk konteks mengapa verifikasi formal penting:

Kerentanan ada dalam kode, dapat ditinjau oleh siapa pun, tetapi tidak ada tool atau test yang menangkapnya sebelum deployment. Symbolic prover seperti Halmos ada justru untuk menutup celah itu — mereka tidak mengambil sampel; mereka menghabiskan ruang input.

Output Halmos: 9 test berhasil, 0 gagal, hasil symbolic test

File test ada di `contracts/test/Token.halmos.t.sol`.

Fase 3: Property testing lintas-layer

Arsitektur postingan pertama memiliki layer domain TypeScript yang mencerminkan state machine on-chain. Fase ini menguji apakah keduanya benar-benar setuju.

Property-based testing dengan fast-check

Saya menambahkan property test fast-check untuk layer domain TypeScript, mencerminkan apa yang dilakukan fuzzer Foundry untuk Solidity:

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

Hasil: 9/9 property test berhasil setelah memperbaiki bug nyata.

Properti yang diuji:

  • `Balance`: komutatif, asosiatif, identitas, invers, konsistensi perbandingan
  • `Token`: invarian `sum(balances) == totalSupply` di bawah urutan operasi acak (200 run, masing-masing 50 ops)
  • `Token`: `totalSupply` non-negatif setelah urutan acak
  • `mint` selalu berhasil untuk input valid
  • `transfer` mempertahankan `totalSupply`

Bug yang ditemukan fast-check

fast-check menemukan bug konsistensi lintas-layer nyata dalam `Token.ts` `transfer()`. Counterexample yang diperkecil langsung jelas:

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

Self-transfer (`from == to`) merusak invarian `sum(balances) == totalSupply`. `toBalance` dibaca sebelum `fromBalance` diperbarui, jadi ketika `from == to`, nilai lama menimpa pengurangan:

// Sebelum (buggy)
const fromBalance = this.getBalance(from);
const toBalance = this.getBalance(to); // ← lama ketika from == to
this.accounts.set(from.getValue(), fromBalance.subtract(amount));
this.accounts.set(to.getValue(), toBalance.add(amount)); // ← menimpa pengurangan

Perbaikan: baca `toBalance` setelah menulis `fromBalance`, sesuai dengan semantik storage Solidity:

// Setelah (diperbaiki)
const fromBalance = this.getBalance(from);
this.accounts.set(from.getValue(), fromBalance.subtract(amount));
const toBalance = this.getBalance(to); // ← sekarang membaca nilai yang diperbarui
this.accounts.set(to.getValue(), toBalance.add(amount));

Kontrak Solidity tidak terpengaruh: ia membaca ulang storage setelah setiap penulisan. Tetapi mirror TypeScript memiliki ketergantungan pengurutan halus yang tidak tercakup oleh unit test yang ada.

Ketidakcocokan lintas-layer pada skala yang lebih besar telah menjadi bencana.

Bug self-transfer kami tidak akan menyebabkan siapa pun kehilangan uang, tetapi mode kegagalannya secara struktural sama: dua layer yang seharusnya setuju, tidak setuju.

Jebakan yang ditemui di sepanjang jalan

Menjalankan tool QA pada proyek yang ada tidak pernah hanya "instal dan jalankan." Beberapa hal rusak sebelum mereka berhasil:

  • Coverage 0% karena `foundry.toml` tidak memiliki path test: Run `forge coverage` pertama mengembalikan 0% di semua papan. Ternyata `foundry.toml` tidak menentukan `test = "contracts/test"` atau `script = "contracts/script"`, jadi Forge tidak menemukan test apa pun. Perintah coverage berhasil secara diam-diam — hanya tidak ada yang dicakup. Ini adalah kegagalan paling menyesatkan: run hijau tanpa output berguna.
  • Import `InvariantTest` hilang di forge-std v1.14.0: `Invariant.t.sol` mengimpor `InvariantTest` dari `forge-std`, yang dihapus dalam rilis terbaru. Kompilasi gagal dengan error "symbol not found" yang tidak jelas. Perbaikannya adalah menghapus import — `Test` saja sudah cukup untuk invariant testing Foundry sekarang.
  • `uint256(token.totalSupply())` vs `Balance.unwrap()`: Test menggunakan cast eksplisit untuk mengekstrak `uint256` yang mendasari dari tipe `Balance` yang ditentukan pengguna. Ini dikompilasi, tetapi idiom yang salah — `Balance.unwrap(token.totalSupply())` adalah apa yang dirancang untuk sistem UDVT. Diterapkan di `Token.t.sol`, `Invariant.t.sol`, dan `DeploySepolia.s.sol`.

Desain pipeline

Semuanya berjalan melalui dua script:

  • scripts/setup-qa-tools.sh`: menginstal Slither, Halmos, Gambit (idempoten)
  • `scripts/run-qa.sh`: menjalankan pemeriksaan, menyimpan hasil dengan timestamp ke `qa-results/`

./scripts/run-qa.sh slither gas # hanya analisis statis + gas
./scripts/run-qa.sh mutation # hanya mutation testing
./scripts/run-qa.sh all # semuanya

Tidak setiap pemeriksaan cepat. Slither dan coverage berjalan pada setiap commit. Mutation testing dan Halmos lebih lambat — lebih cocok untuk run mingguan atau pra-rilis.

Ringkasan

Toolchain QA Blockchain: apa yang ditangkap setiap layer — dari analisis statis hingga property testing lintas-layer

Lima layer QA, masing-masing menangkap kelas masalah yang berbeda.

Penjelasan layer

Gambit dan fast-check memberikan hasil paling dapat ditindaklanjuti dalam putaran ini.

Pipeline CI

Pemeriksaan QA sekarang terhubung ke GitHub Actions sebagai pipeline enam tahap:

CI Pipeline: Build & Lint bercabang ke tahap Test, Coverage, Gas, Slither, dan Audit

Pipeline GitHub Actions: Build & Lint mengontrol semua tahap downstream.

Penjelasan tahap

Referensi

  • Sumber Ethereum Account State: [github.com/egpivo/ethereum-account-state](https://github.com/egpivo/ethereum-account-state)
  • Postingan sebelumnya: 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

Catatan

  • Postingan ini diadaptasi dari postingan blog asli saya.

Ethereum Account State: QA Pipeline for a Minimal Token awalnya diterbitkan di Coinmonks di Medium, di mana orang-orang melanjutkan percakapan dengan menyoroti dan merespons cerita ini.

Penafian: Artikel yang diterbitkan ulang di situs web ini bersumber dari platform publik dan disediakan hanya sebagai informasi. Artikel tersebut belum tentu mencerminkan pandangan MEXC. Seluruh hak cipta tetap dimiliki oleh penulis aslinya. Jika Anda meyakini bahwa ada konten yang melanggar hak pihak ketiga, silakan hubungi crypto.news@mexc.com agar konten tersebut dihapus. MEXC tidak menjamin keakuratan, kelengkapan, atau keaktualan konten dan tidak bertanggung jawab atas tindakan apa pun yang dilakukan berdasarkan informasi yang diberikan. Konten tersebut bukan merupakan saran keuangan, hukum, atau profesional lainnya, juga tidak boleh dianggap sebagai rekomendasi atau dukungan oleh MEXC.

PRL $30.000 + 15.000 USDT

PRL $30.000 + 15.000 USDTPRL $30.000 + 15.000 USDT

Deposit & berdagang PRL untuk meningkatkan hadiah!