Fuzzing
Fuzzing is the process of providing random data to programs to identify unexpected behavior, such as crashes and panics.
This page covers fuzz testing, primarily with cargo-fuzz. Property testing with proptest is a related but distinct approach: it also generates random input, but runs as ordinary tests under cargo test and asserts that some property holds, rather than only searching for crashes and panics. See How to Write Property Tests below.
The following steps can be used in any Stellar contract workspace. If experimenting, try them in the increment example. The contract has an increment function that increases a counter value by one on every invocation.
How to Write Fuzz Tests
-
Install the nightly Rust toolchain. Nightly Rust is required to run cargo-fuzz.
rustup install nightly -
Install
cargo-fuzz.cargo install --locked cargo-fuzz -
Initialize a fuzz project by running the following command inside your contract directory.
cargo fuzz init -
Open the contract's
Cargo.tomlfile. Addlibas acrate-type.[lib]-crate-type = ["cdylib"]+crate-type = ["lib", "cdylib"] -
Open the generated
fuzz/Cargo.tomlfile. Add thesoroban-sdkdependency.[dependencies]libfuzzer-sys = "0.4"+soroban-sdk = { version = "*", features = ["testutils"] } -
Open the generated
fuzz/src/fuzz_target_1.rsfile. It will look like the below.#![no_main]use libfuzzer_sys::fuzz_target;fuzz_target!(|data: &[u8]| {// fuzzed code goes here}); -
Fill out the
fuzz_target!call with test setup and assertions. For example, for the increment example:#![no_main]use libfuzzer_sys::fuzz_target;use soroban_increment_with_fuzz_contract::{IncrementContract, IncrementContractClient};use soroban_sdk::{testutils::arbitrary::{arbitrary, Arbitrary},Env,};#[derive(Debug, Arbitrary)]pub struct Input {pub by: u64,}fuzz_target!(|input: Input| {let env = Env::default();let id = env.register(IncrementContract, ());let client = IncrementContractClient::new(&env, &id);let mut last: Option<u32> = None;for _ in input.by.. {match client.try_increment() {Ok(Ok(current)) => assert!(Some(current) > last),Err(Ok(_)) => {} // Expected errorOk(Err(_)) => panic!("success with wrong type returned"),Err(Err(_)) => panic!("unrecognised error"),}}}); -
Execute the fuzz target.
cargo +nightly fuzz run --sanitizer=thread fuzz_target_1infoIf you're developing on MacOS you need to add the
--sanitizer=threadflag in order to work around a known issue.
This test uses the same patterns used in unit tests and integration tests:
- Create an environment, the
Env. - Register the contract to be tested.
- Invoke functions using a client.
- Assert expectations.
For a full detailed example, see the fuzzing example.
There is another tool for fuzzing Rust code, cargo-afl. See the Rust Fuzz book for a tutorial for how to use it.
How to Get Code Coverage of Fuzz Tests
Getting code coverage data for fuzz tests requires some different tooling than when doing the same for regular Rust tests.
-
Run the fuzz tests until it has produced a corpus, just as in step 7 above.
cargo +nightly fuzz run --sanitizer thread fuzz_target_1 -
Install the llvm-tools for the nightly compiler.
rustup component add --toolchain nightly llvm-tools-preview -
Run the fuzz coverage command that'll execute the corpus and write coverage data to the coverage directory in the
profdataformat.cargo +nightly fuzz coverage --sanitizer thread fuzz_target_1 -
Run the llvm-cov command to convert the profdata file to an lcov file.
$(find $(rustc --print sysroot) -name llvm-cov) export \-instr-profile=fuzz/coverage/fuzz_target_1/coverage.profdata \-object target/$(rustc -vV | sed -n 's|host: ||p')/coverage/$(rustc -vV | sed -n 's|host: ||p')/release/fuzz_target_1 \--ignore-filename-regex "rustc" \-format=lcov \> lcov.infoLoad the
lcov.infofile into your IDE using its coverage feature. In VSCode this can be done by installing the Coverage Gutters extension and executing theCoverage Gutters: Watchcommand.
To measure code coverage of regular Rust tests, see Code Coverage.
How to Write Property Tests
Property tests, like fuzz tests, exercise a contract with randomly generated input, but they run as ordinary #[test]s under cargo test. No nightly toolchain, no separate fuzz crate, and no special runner are required.
-
Add the
proptestandproptest-arbitrary-interopcrates as dev-dependencies of the contract crate.[dev-dependencies]proptest = "1"proptest-arbitrary-interop = "0.1" -
Soroban contract types can only be constructed from an
Env, which apropteststrategy doesn't have access to, so contract types are generated the same way as for fuzzing: through theSorobanArbitrary::Prototypepattern (see Accepting Soroban Types as Input with theSorobanArbitraryTrait). Butproptestgenerates values fromStrategys, not fromArbitraryimplementations, so it cannot consume aPrototypedirectly. The proptest-arbitrary-interop crate'sarbfunction bridges the gap, turning anyArbitrarytype, including everySorobanArbitrary::Prototype, into aStrategy.The pattern mirrors the fuzz test pattern: generate the prototype with
arb::<<T as SorobanArbitrary>::Prototype>(), then convert it to the real,Env-hosted contract type with.into_val(&env)inside the test body, where anEnvis available. -
Write the property test. For example, generating an
Address:use proptest::prelude::*;use proptest_arbitrary_interop::arb;use soroban_sdk::testutils::arbitrary::SorobanArbitrary;use soroban_sdk::{Address, Env, IntoVal};proptest! {#[test]fn test_deposit(address_proto in arb::<<Address as SorobanArbitrary>::Prototype>(),deposit_amount in 0i128..=i128::MAX,) {let env = Env::default();let address: Address = address_proto.into_val(&env);// call the contract with `address` and `deposit_amount`}}Generated
Addressprototypes always convert to contract addresses, never to account (G...) addresses. A property test that also needs account addresses must construct them itself withAddress::from_str. -
Run it like any other test.
cargo test
Shrinking is degraded across this bridge. When a property test fails, proptest tries to shrink the failing input to a smaller, simpler one, but it does so by truncating the generated bytes and rebuilding a new value from them, rather than shrinking the value itself. As a result, the "shrunk" failing input proptest reports is generally a different input, not a smaller version of the first one that failed.
See the SorobanArbitrary proptest module for the full reference documentation, including a second example that generates a custom #[contracttype] struct.
Guides in this category:
Unit Tests
Unit tests are small tests that test smart contracts.
Mocking
Mocking dependency contracts in tests.
Test Authorization
Write tests that test contract authorization.
Test Events
Write tests that test contract events.
Integration Tests
Integration testing uses dependency contracts instead of mocks.
Fork Testing
Integration testing using mainnet data.
Fuzzing
Fuzzing and property testing to find unexpected behavior.
Differential Tests
Differential testing detects unintended changes.
Differential Tests with Test Snapshots
Differential testing using automatic test snapshots.
Mutation Testing
Mutation testing finds code not tested.
Code Coverage
Code coverage tools find code not tested.
Testing with Ledger Snapshot
Use ledger snapshots to test contracts with ledger data