Fuzz Testing
The Neo DevPack for Solidity compiler uses a multi-layered fuzzing strategy to catch crashes, semantic regressions, and runtime parity issues before they reach production.
Overview
| Layer | Framework | Target | Scope |
|---|---|---|---|
| Proptest (Rust) | proptest | Compiler properties, runtime verification, storage, differential checks | Case count is controlled by PROPTEST_CASES and cargo defaults |
| Cargo-fuzz | libFuzzer | Registered targets in fuzz/Cargo.toml | Coverage-guided crash finding across compiler, NEF, manifest, runtime, and Yul surfaces |
| Differential checks | proptest | Reference crates and expected Neo behavior | Runs as part of cargo test --test fuzz_tests |
Running the Fuzz Suite
One-Shot Proptest Run
# Full suite (case counts come from each proptest config unless PROPTEST_CASES overrides them)
cargo test --test fuzz_tests
# Full suite with more cases (slower, deeper)
PROPTEST_CASES=200 cargo test --test fuzz_tests
# Quick smoke (fewer cases, faster feedback)
PROPTEST_CASES=10 cargo test --test fuzz_tests -- --test-threads=4Continuous Background Fuzzing
# Start the continuous fuzz loop (proptest + cargo-fuzz, indefinite)
make test-fuzz-start
# Check status, coverage, and crash count
make test-fuzz-status
# Stop the background fuzzer
make test-fuzz-stopThe continuous runner executes in rounds:
- Proptest suite (
PROPTEST_CASES=50by default in the continuous runner) - Every target reported by
cargo +nightly fuzz list - A per-target
CARGO_FUZZ_TIMEburst, defaulting to 300 seconds - A short sleep before the next round
CI / Scheduled Deep Runs
# Deep proptest run: PROPTEST_CASES=100
make test-fuzz-continuous
# Or run the suite script directly
bash scripts/run_fuzz_suite.sh deepUse make test-fuzz-start or ./scripts/run_continuous_fuzz.sh for the background loop that cycles through cargo-fuzz targets.
A GitHub Actions workflow (.github/workflows/fuzz.yml) runs the deep proptest suite nightly at 02:00 UTC, then runs 60-second cargo-fuzz smoke bursts for the workflow's configured target subset.
What the Fuzz Suite Covers
Compiler Properties (optimizer_props.rs)
- Semantic equivalence: O0 vs O3 produce identical execution results for pure arithmetic, loops, recursion, internal calls, modifiers, storage+events
- NEF format validity: Generated
.neffiles have correct magic header, checksum, and method tokens - Manifest correctness: Generated
.manifest.jsonhas valid ABI, permissions, and supported standards - Determinism: Same source produces identical bytecode across compilations
Runtime Verification (compiler_props.rs)
- Neo N3 native contracts: Oracle, Policy, Ledger, RoleManagement, ContractManagement
- Precompile helpers: sha256, ripemd160, identity, and the typed uint256 modexp helper are checked against reference implementations. The modexp helper is not full EIP-198 raw byte-payload parity, and Neo BLS12-381 adaptations for 0x06-0x08 are not Ethereum BN256 precompile parity.
- EVM globals:
gasleft(),block.timestamp,address.balance,selfdestruct - ABI encode/decode: Roundtrip correctness (test-runtime only; see P0 warning)
Storage Properties (storage_props.rs)
- Isolation between accounts
- Roundtrip preservation
- Key ordering
- Overwrite semantics
- Large value handling
- Empty value handling
Syntax Resilience (arithmetic_props.rs)
- Keyword case variants
- Nested ternary with side effects
- Interleaved comments
- Very large hex literals
- Scientific notation extremes
- Escape sequence edge cases
- Empty/whitespace strings
Edge Features (compiler_props.rs)
- User-defined value types
- Nested struct arrays
- Anonymous events
- Try/catch with custom errors
- Enum casts
- Function pointers
type(I).interfaceId- Global
using for
Precompile Runtime Verification (batches_100_105.rs, Batch #100)
- ecrecover (0x01): compilation and dispatch verification
- SHA-256 (0x02): digest matches Rust sha2 reference
- RIPEMD-160 (0x03): digest matches Rust ripemd reference
- Identity (0x04): data passthrough roundtrip
- ModExp (0x05): compilation and dispatch verification
- BLS12-381 adaptations (0x06-0x08): Neo-native curve helpers only; not Ethereum BN256/alt_bn128 parity
- BLAKE2f (0x09): unavailable on Neo N3 and expected to revert if called through the devpack helper
Neo N3 Syscall Verification (batches_100_105.rs, Batch #101)
Runtime.checkWitness— dispatch and return verificationRuntime.getNetwork— network magic numberRuntime.getPlatform— platform stringRuntime.getEntryScriptHash— script hash verificationStorage.find— iterator-based prefix search
Solidity Feature Runtime (batches_100_105.rs, Batches #102-#105)
uncheckedoverflow wrap semanticsfallback()dispatch routingmsg.sigselector verificationimmutablewrite rejectionreceive()→onNEP17Paymentmanifest remapping- NEF/manifest
supportedstandardsauto-detection - Manifest
groupsandpermissionsshape - Multi-contract manifest isolation
- NEF bytecode determinism
abi.encodePackedoutput shapeabi.encodeWithSignatureselector prefixbytes.concat/string.concatruntime- try/catch with string return binding
Native Contract & OOP (batches_106_110.rs, Batches #106-#110)
- ContractManagement.getContract/hasMethod/getMinimumDeploymentFee
- Policy.getExecFeeFactor/getStoragePrice
- Storage multi-key-type mappings, delete-then-get, nested mappings
- Struct with mixed fields (uint/address/bool)
- Array push/pop/length
- Event emission with indexed params
- Custom error revert with ABI-encoded args
requirewith string and custom error messagesassertfailure (Panic 0x01)- Inheritance with
supercalls - Interface implementation and dispatch
- Abstract contract rejection
using fordirective- Constructor chaining across 3 levels
- Empty contract deployment
- Event-only contract
- Long function names (100 chars)
- 50 state variables with auto-getters
- 5 contracts in one source file
Interpreting Coverage
Cargo-fuzz prints coverage after each run:
cov: 7356 ← edges covered in the target
ft: 7356 ← features (same as cov for libfuzzer)
corp: 127 ← corpus size (seed inputs)For fuzz_target_1, coverage grows as the fuzzer discovers new Solidity syntax constructs. Saturation (single-digit gains per round) is expected after several hours.
For fuzz_target_2, coverage plateaus early because the NEF parser is strict and most random bytes are rejected at the magic header check.
Other registered targets cover bytecode disassembly, NEF roundtrips, manifest JSON, Standard JSON, structured Solidity input generation, runtime execution, method tokens, NEF/manifest mutation, and Yul assembly handling.
Crash Triage
If the fuzzer finds a crash:
Locate the crash artifact:
bashls fuzz/artifacts/<target>/crash-*Reproduce with the artifact:
bashcargo +nightly fuzz run <target> fuzz/artifacts/<target>/crash-<hash>Minimize the crash input:
bashcargo +nightly fuzz tmin <target> fuzz/artifacts/<target>/crash-<hash>Convert to a regression test in the appropriate
tests/module.
Known Fuzz Gaps
These areas are not yet deeply fuzzed and represent future expansion targets:
- Differential testing — compare neo-solc output against a reference EVM compiler
- Gas oracle differential — compare embedded runtime gas against Neo-Express
- Cross-contract call sequences — multi-contract interaction fuzzing
- Manifest permission inference — fuzz dynamic call sites to stress permission analysis
- Dynamic ABI encoding —
abi.encodewith dynamic types (string, bytes, arrays) falls back to pseudo-native StdLib which faults on real Neo N3 - Full NEP-17 transfer flow — end-to-end token transfer with onNEP17Payment callback chain
- ContractManagement.deploy/update — dynamic contract deployment and upgrade lifecycle
