Overview

Documentation for inkscope-fuzzer users and developers.

Inkscope fuzzer is a property-based fuzzing tool designed to find bugs and vulnerabilities in Ink! smart contracts during the development phase. It utilizes the ink-sandbox runtime emulation engine to execute and test Polkadot smart contracts against user-defined properties.

These properties are written in ink! and the fuzzer starts from a .contract file produced from the compilation. The fuzzer generates random inputs and checks if the provided properties hold true for the smart contract under test.

If the fuzzer discovers a property violation, it prints the complete execution trace, including the contract deployment process, all the messages called, and the violated properties. This detailed output assists developers in identifying and fixing issues within their contracts.

By incorporating property-based testing through inkscope fuzzer, developers can enhance the reliability and security of their smart contracts before deployment on a live network.

Installation

Using inkscope fuzzer with Docker

Inkscope fuzzer can also be used entirely within a Docker container. If you don’t have it, Docker can be installed directly from Docker’s website.

You have to build the docker image locally. From the inkscope-fuzzer repository, execute:

    docker build -t inkscope-fuzzer -f ./.docker/inkscope-fuzzer/Dockerfile .

After that, you are all set to use the fuzzer. To run it:

    docker run -v ".:/contract" inkscope-fuzzer file.contract fuzz

Optionally, you can add an alias to make it easier to run the fuzzer.

    alias inkscope-fuzzer-docker="docker run -v ".:/contract" inkscope-fuzzer"
    inkscope-fuzzer-docker file.contract fuzz

Building from source

Pre-requisites

You will need the Rust compiler and Cargo, the Rust package manager. The easiest way to install both is with rustup.rs.

Building

You can install it directly from crates.io

    cargo install inkscope-fuzzer

The inkscope-fuzzer executable will be ready to use in your system.
cargo inkscope-fuzzer

Or, by manually building from a local copy of the inkscope-fuzzer repository:

    # Clone the repository
    git clone https://github.com/inkscopexyz/inkscope-fuzzer.git
    cd inkscope-fuzzer

    # Build the project
    cargo build --release

The inkscope-fuzzer executable will be ready to use in the following path:
./target/release/inkscope-fuzzer

Writing properties

Inkscope fuzzer supports 3 approaches for writing properties to detect bugs in ink! smart contracts:

  1. Dedicated Property Messages (Recommended):
    The recommended approach is to write a new message that performs a specific action and always returns a boolean value. If the function returns false, it indicates that the property has been violated, signaling the presence of a bug. These properties can also execute other functions that modify the contract state, but the state changes are not saved as they are performed in a dry run during the fuzzing process.
    For convention, these messages name should start with inkscope_ to differentiate them from regular contract messages.

    Example:

    #![allow(unused)]
    #![cfg_attr(not(feature = "std"), no_std, no_main)]
    
    fn main() {
    #[ink::contract]
    mod example_contract {
      
        #[ink(storage)]
        pub struct ExampleContract {
           // State variables
        }
    
        impl ExampleContract {
            // Regular contract constructors and messages
            ...
        }
    
        // Dedicated property messages for fuzz testing
        #[cfg(feature = "fuzz-testing")]
        #[ink(impl)]
        impl ExampleContract {
            #[cfg(feature = "fuzz-testing")]
            #[ink(message)]
            pub fn inkscope_bug(&self) -> bool {
                // Property logic that returns true or false
            }
        }
    
    }
    }
  2. Assertions in Existing Calls:
    An alternative method is to incorporate assert statements within the existing contract calls. If any of these assertions fail, it means a bug has been discovered in the contract.

    Example:

    #![allow(unused)]
    #![cfg_attr(not(feature = "std"), no_std, no_main)]
    
    fn main() {
    #[ink::contract]
    mod example_contract {
      
        #[ink(storage)]
        pub struct ExampleContract {
           value: u128,
        }
    
        impl ExampleContract {
            // Regular contract constructors and messages
            #[ink(constructor)]
            pub fn new(init_value: u128) -> Self {
                Self { value: init_value }
            }
    
            #[ink(message)]
            pub fn incr_value(&self, value: u128) {
                self.value += value;
    
                // Write assertions to check property
                assert!(self.value <= 10, "Value must be less than or equal to 10");
            }
            
            ...
    
        }
    
    }
    }
  3. Panic-based Properties:
    The third approach involves making the contract panic when a specific condition is met. This signals to the fuzzer that a property has been violated and a bug has been found.

    Example:

    #![allow(unused)]
    #![cfg_attr(not(feature = "std"), no_std, no_main)]
    
    fn main() {
    #[ink::contract]
    mod example_contract {
      
        #[ink(storage)]
        pub struct ExampleContract {
           value: u128,
        }
    
        impl ExampleContract {
            // Regular contract constructors and messages
            #[ink(constructor)]
            pub fn new(init_value: u128) -> Self {
                Self { value: init_value }
            }
    
            #[ink(message)]
            pub fn incr_value(&self, value: u128) {
                self.value += value;
    
                // Check property and panic if violated
                if self.value > 10 {
                    panic!("Value must be less than or equal to 10");
                }
            }
            
            ...
    
        }
    
    }
    }

The choice of property-writing approach depends on the specific requirements and complexity of the smart contract being tested. The dedicated property messages method is generally recommended as it provides a clear separation of concerns and makes the testing process more organized and maintainable.

Ityfuzz

Let's start with a simple example to understand how the fuzzer works.

Check section 3 Figure 2 for the pseudo-code of the contract.

Here, we reproduce the contract in ink!:

#![allow(unused)]
#![cfg_attr(not(feature = "std"), no_std, no_main)]

fn main() {
#[ink::contract]
mod ityfuzz {

    const BUG_VALUE: u128 = 15;

    #[ink(storage)]
    pub struct Ityfuzz {
        counter: u128,
        bug_flag: bool,
    }

    impl Ityfuzz {
        #[ink(constructor)]
        pub fn default() -> Self {
            Self {
                counter: 0,
                bug_flag: true,
            }
        }

        #[ink(message)]
        pub fn incr(&mut self, value: u128) -> Result<(), ()> {
            if value > self.counter {
                return Err(());
            }
            self.counter = self.counter.checked_add(1).ok_or(())?;
            Ok(())
        }

        #[ink(message)]
        pub fn decr(&mut self, value: u128) -> Result<(), ()> {
            if value < self.counter {
                return Err(());
            }
            self.counter = self.counter.checked_sub(1).ok_or(())?;
            Ok(())
        }

        #[ink(message)]
        pub fn buggy(&mut self) {
            if self.counter == BUG_VALUE {
                self.bug_flag = false;
            }
        }

        #[ink(message)]
        pub fn get_counter(&self) -> u128 {
            self.counter
        }
    }
}
}

In this contract, the incr and decr functions increment and decrement the counter variable based on the condition compared to the provided value, respectively. The buggy function sets the bug_flag variable to false if the counter variable is equal to BUG_VALUE.

Fuzzing the contract

To test the contract we will write a property as an ink! message that checks the value of the bug_flag variable. If the message returns 'false', it means that property was violated, and the fuzzer will print the execution trace and the violated property.

Note that this message is wrapped in a #[cfg(feature = "fuzz-testing")] attribute to avoid compiling it in the final contract. In order for this to work, the fuzz-testing feature must be enabled in the Cargo.toml file.

[features]
...
fuzz-testing = []

And this is how the property looks like in the ink! contract:

#![allow(unused)]
#![cfg_attr(not(feature = "std"), no_std, no_main)]

fn main() {
#[ink::contract]
mod ityfuzz {
    ...

    #[cfg(feature = "fuzz-testing")]
    #[ink(impl)]
    impl Ityfuzz {
        #[cfg(feature = "fuzz-testing")]
        #[ink(message)]
        pub fn inkscope_bug(&self) -> bool {
            self.bug_flag
        }
    }
}
}

Once the property is written, we can compile the contract:

    cd test-contracts/ityfuzz
    cargo contract build --features fuzz-testing

And then, execute the fuzzer against it and check the output

    ./target/release/inkscope-fuzzer ./test-contracts/ityfuzz/target/ink/ityfuzz.contract fuzz

If the fuzzer finds a property violation, it will print the execution trace and the violated property as shown below:

Property check failed ❌
  Deploy: default()
  Message0:   Deploy: incr(UInt(0))
  Message1:   Deploy: buggy()
  Message2:   Deploy: incr(UInt(1))
  Message3:   Deploy: buggy()
  Message4:   Deploy: incr(UInt(1))
  Message5:   Deploy: buggy()
  Message6:   Deploy: buggy()
  Message7:   Deploy: buggy()
  Message8:   Deploy: buggy()
  Message9:   Deploy: incr(UInt(1))
  Message10:   Deploy: buggy()
  Message11:   Deploy: incr(UInt(0))
  Message12:   Deploy: incr(UInt(1))
  Message13:   Deploy: buggy()
  Message14:   Deploy: incr(UInt(1))
  Message15:   Deploy: incr(UInt(2))
  Message16:   Deploy: buggy()
  Message17:   Deploy: buggy()
  Message18:   Deploy: incr(UInt(1))
  Message19:   Deploy: incr(UInt(0))
  Message20:   Deploy: incr(UInt(2))
  Message21:   Deploy: incr(UInt(1))
  Message22:   Deploy: buggy()
  Message23:   Deploy: buggy()
  Message24:   Deploy: buggy()
  Message25:   Deploy: incr(UInt(2))
  Message26:   Deploy: incr(UInt(1))
  Message27:   Deploy: decr(UInt(340282366920938463463374607431768211455))
  Message28:   Deploy: incr(UInt(1))
  Message29:   Deploy: incr(UInt(1))
  Message30:   Deploy: buggy()
  Property: inkscope_bug()

Executing the fuzzer