phink

Introduction

Overview of Phink

Phink is a blazing-fast⚡, property-based, coverage-guided fuzzer for ink! smart contracts. It enables developers to embed inviolable properties into their smart contract testing workflows, equipping them with automatic tools to detect vulnerabilities and ensure contract reliability before deployment.

Dashboard Overview

dashboard

Key Features

Property-based Testing

Phink requires developers to define properties directly within ink! smart contracts. By prefixing functions with phink, such as fn phink_assert_abc_always_true(), developers create properties that act as assertions. During testing, these properties are checked against every input (a set of ink! messages). If a property’s assertion fails, it triggers a panic, signaling that an invariant has been broken. This method ensures thorough validation of contract logic and behavior.

Coverage-guided Fuzzing

In order to become coverage-guided, Phink needs to instrument the ink! smart contract. Although currently adding feedback on each line executed, Phink is designed to evolve, eventually monitoring coverage across new edges and code branches. Feedback is transmitted to the pallet_contract via the debug_message.

Why Use Phink

Phink addresses security concerns by:

  • Automatically generating and testing a diverse range of inputs
  • Uncovering edge cases, logical flaws, and bugs leading to contract state reversion
  • Exploring different execution paths by generating input mutation

This extensive testing identifies bugs and potential vulnerabilities in the development cycle, enabling developers to address issues before deployment.

Getting Started with Phink

Installation

System Requirements

To successfully install and run Phink, ensure your system meets the following requirements:

  • Operating System:

    • Linux: Recommended for compatibility.
    • macOS: Not recommended as it doesn’t support some AFL++ plugins.
    • Windows: Untested.
  • Rust:

    • Version: Rust nightly
    • Current Compatibility: cargo 1.83.0-nightly (ad074abe3 2024-10-04)

Installation Guide

You can install Phink by building it from the source or by using Docker. Choose the method that best suits your setup.

Building from Source
  1. Clone the Repository

    git clone https://github.com/srlabs/phink && cd phink/
    

    You can also use:

    cargo +nightly install --git https://github.com/srlabs/phink
    
  2. Install Dependencies

    cargo install --force ziggy cargo-afl honggfuzz grcov cargo-contract --locked
    
  3. Configure AFL++

    cargo afl config --build --plugins --verbose --force
    sudo cargo-afl afl system-config
    
  4. Build Phink

    cargo build --release
    
  5. Run Phink

    ./target/release/phink --help
    phink --help # if installed via `cargo install`
    
Using Docker
  1. Build the Docker Image
    docker build -t phink .
    

For detailed Docker instructions, refer to README.Docker.md.

Basic Workflow

  1. Instrument the Contract

    • Use Phink to instrument your ink! smart contract for fuzzing.
  2. Configure Fuzzing Parameters

    • Edit the phink.toml file to set paths, deployment settings, and fuzzing options according to your project needs.
  3. Run Fuzzing

    • Execute fuzzing with your configured settings to identify vulnerabilities early in the development cycle.

Phink Configuration Guide

This guide provides an overview of the Phink configuration settings.

Configuration File Overview

### Phink Configuration

# General Settings
cores = 4                # Set to 1 for single-core execution
max_messages_per_exec = 1 # Maximum number of message calls per input

# Paths
instrumented_contract_path.path = "toooooooooooz"  # Path to the instrumented contract, after `phink instrument my_contract` is invoked
report_path = "output/phink/contract_coverage" # Directory for coverage HTML files
fuzz_output = "output"                         # Directory for fuzzing output

# Deployment
deployer_address = "5C62Ck4UrFPiBtoCmeSrgF7x9yv9mn38446dhCpsi2mLHiFT" # Contract deployer address (Alice by default)
constructor_payload = "9BAE9D5E"                                     # Hexadecimal scale-encoded data for contract instantiation
storage_deposit_limit = "100000000000"                              # Storage deposit limit
instantiate_initial_value = "0"                                     # Value transferred during instantiation, if needed

# Fuzzing Options
fuzz_origin = false  # Attempt to call each message as a different user (affects performance)
verbose = true       # Print detailed debug messages
show_ui = true       # Display advanced UI
use_honggfuzz = false # Use Honggfuzz (set as false)
catch_trapped_contract = false # Not setting trapped contract as a bug, only detecting invariant-based bugs

# Gas Limits
[default_gas_limit]
ref_time = 100_000_000_000      # Reference time for gas
proof_size = 3_145_728          # Proof size (3 * 1024 * 1024 bytes)

General Settings

  • cores: Allocate the number of CPU cores for fuzzing. Setting this to 1 enables single-core execution.
  • max_messages_per_exec: Define the maximum number of message calls allowed per fuzzing input. If you want to fuzz one function per one function, set this number to 1. Setting it to zero will fuzz zero message. Setting it for example to 4 will generate 4 different messages in one input, run all the invariants, and go to the next input.

Paths

  • instrumented_contract_path.path: Specify the path to the instrumented contract, which should be set post-invocation of phink instrument my_contract. This path will contains the source code of the initial contract, with the additional instrumentation instructions. It also will contians the instrumented compiled contract.
  • report_path: Designate the directory where HTML coverage reports will be generated, if the user wishes to generate a coverage report.
  • fuzz_output: Indicate the directory for storing all fuzzing output. This output is important as it will contains the log file, the corpus entries, the crashes, and way more.

Deployment

  • deployer_address: Set the address of the smart contract deployer. The default is Alice’s address.
  • constructor_payload: Hexadecimal scale-encoded data necessary for contract instantiation. This is used when calling bare_instantiate extrinsic to instantiate the contract. You can use https://ui.use.ink/ to generate this payload. By default, Phink will deploy the contract using the constructor that have no arguments new().
  • storage_deposit_limit: Limit for storage deposits during contract deployment. It represents an optional cap on the amount of blockchain storage (measured in balance units) that can be used or reserved by the contract call.
  • instantiate_initial_value: Initial value to be transferred upon contract instantiation, if required. So if the contract requires during instantiation a minimum amount of 3000 units, set 3000 here.

Fuzzing Options

  • fuzz_origin: A Boolean option to try calling each message as a different user, which may impact performance. If set to false, the fuzzer will fuzz any message with the one input (Alice).
  • verbose: Enables detailed debugging messages when set to true. This will just output more logs.
  • show_ui: Toggle for displaying the advanced user interface.
  • use_honggfuzz: Determines whether to use Honggfuzz; remains false by default. (let it false! not handled currently )
  • catch_trapped_contract: Indicate whether the fuzzer should treat trapped contracts as bugs.
    • When set to true: The fuzzer will identify any contracts that become trapped (ContractTrapped) as bugs. This is useful for an examination of potential issues, as it covers all types of bugs, not just ones related to logic or state invariants.
    • When set to false: Focuses only on catching bugs related to invariant violations, ignoring trapped contract scenarios. This is preferable when you are only interested in logical correctness and not in trapping errors.

Gas Limits

Default Gas Limit Configuration

The gas limit refers to the maximum amount of computational effort (or weight) that an execution is allowed to use when performing a call to a contract. It controls how much balance a contract is allowed to use for expanding its state storage during execution, ensures that users won’t unintentionally spend more than they wanted on storage allocation, and offers protection against excessive storage costs by defining an upper limit on how much can be spent on storage within that call.

  • ref_time: Specifies the reference time for gas allocation.
  • proof_size: Defines the proof size (e.g., 3145728 corresponds to 3 MB).

Writing Properties for ink! Contracts

Adding Properties

Inside your Cargo.toml

First, you need to add the phink feature to your Cargo.toml, such as:

[features]
phink = []

Inside your file.rs

Then, you can use the following example to create invariants. You need to create another impl if your contract, and to put it under the feature of phink. Use assert! or panic! for your properties.

#![allow(unused)]
fn main() {
#[cfg(feature = "phink")]
#[ink(impl)]
impl DomainNameService {
    // This invariant ensures that nobody registed the forbidden number
    #[ink(message)]
    #[cfg(feature = "phink")]
    pub fn phink_assert_dangerous_number(&self) {
        let forbidden_number = 42;
        assert_ne!(self.dangerous_number, forbidden_number);
    }
}
}

You can find more informations in the page dedicated to invariants.

Running Phink

1. Instrument the Contract

Run the following command to instrument your ink! smart contract, enabling it for fuzzing:

phink instrument my_contract/

This step modifies the contract to include necessary hooks for Phink’s fuzzing process. It creates a fork of the contract, so you don’t have to make a copy before.

2. Run the fuzzer

After instrumenting your contract and writing properties and configuring your phink.toml, execute the fuzzing process:

phink fuzz

After executing this command, your fuzzing tests will begin based on the configurations specified in your phink.toml file. You should see a user interface appear.

If you’re utilizing the advanced UI, you’ll receive real-time updates on the fuzzed messages at the bottom of the screen. For more detailed log information, you can use the following command:

watch -c -t -n 0.1 "clear && cat output/phink/logs/last_seed.phink" # `output` is the default, but it depends of your `phink.toml`

This will provide you with clearer logs by continuously updating them every 0.1 seconds.

Analyzing Results

Crashes

In case of crashes, you should see something like the following.

crash

To analyze the crash, you can run phink execute <your_crash>, for instance phink execute output/phink/crashes/1729082451630/id:000000,sig:06,src:000008,time:627512,execs:3066742,op:havoc,rep:2

ComponentDescription
1729082451630Timestamp representing when the crash was recorded.
id:000000Unique identifier for the crash.
sig:06Signal number that triggered the crash.
src:000008Source test case number.
time:627512Execution time since the start of the testing process.
execs:3066742Cumulative number of executions performed until the crash.
op:havoc,rep:2Type of fuzzing operation (havoc) and its repetition number.

By running this, you should have an output similar to the screenshot below:

crash

Coverage (this feature is in alpha and unstable)

Generating a coverage report

First, you need to create a traces.cov file. For this, execute the command below.

phink run  

Once done, generate coverage reports to analyze which parts of the contract were tested:

phink coverage my_contract/

Some HTML files should then be generated in the path you’ve configured inside your phink.toml. The coverage report provides a visual representation of tested code areas. Basically, the more green lines, the better. You can find below an example of the coverage report.

Coverage Report Example

Green Lines: Code that has been tested.

Coverage Report Part 1

Figure 1: Coverage Report of one specific file.

coverage_2

Figure 2: List of fuzzed Rust files from the ink! smart-contract.

Invariants

Invariants are fundamental properties that must always hold true in a smart-contract, regardless of any operations performed. They help ensure that certain logical conditions remain constant throughout the execution of the contract, preventing potential vulnerabilities and ensuring its reliability.

We suggest to use integrity and unit tests from your codebase to get inspiration to generate good invariants.

Creating good invariants for Ink! smart-contracts

Below are some guidelines to help you design robust invariants:

  1. Understand the Contract’s Logic: Before crafting invariants, deeply understand the core logic and expected behaviors of your smart contract.

  2. Identify Critical Properties: Determine critical properties or conditions that must hold true. This could involve state variables, transaction outcomes, or other interdependent conditions.

  3. Consider Corner Cases: Think about edge cases and potential attack vectors. Invariants should be designed to capture unexpected or extreme scenarios.

  4. Focus on Consistency: Consider properties that ensure data consistency across state changes. This might involve ensuring balances are correctly updated or ownership is properly maintained.

  5. Keep it Simple: While considering complex scenarios, ensure your invariants are straightforward to encourage maintainability and clarity.

Example invariant in ink! smart-contracts

Here is a template to get you started on writing invariants for ink! smart contracts:

#![allow(unused)]
fn main() {
#[cfg(feature = "phink")]
#[ink(impl)]
impl DomainNameService {
    /// Example invariant:
    #[ink(message)]
    #[cfg(feature = "phink")]
    pub fn phink_balance_invariant(&self) {
        // Ensure total supply equals sum of individual balances
        assert_eq!(self.total_supply, self.calculate_total_balances(), "Balance invariant violated!");
    }
}
}

Important Annotations Explained

  • #[cfg(feature = "phink")]: Ensures the function is only compiled when the “phink” feature is enabled.
  • #[ink(message)]: Marks the function as an executable entry defined by the ink! framework.
  • Function Naming: Begin with “phink_” to indicate the purpose and correlation to fuzz testing.

Creating Invariants with LLM

Large Language Models (LLMs) offer a good (lazy, yes…) approach to generate invariants by interpreting the logic and identifying properties from the contract code. Here is an example prompt system you could use to generate a base of invariants

System Prompt
You are provided with Rust files containing an ink! smart contract. Your task is to generate invariants, which are
inviolable properties that a fuzzer will check to ensure the contract's quality and correctness. Please adhere to the
following requirements while writing the invariants:

1. Ensure that the `impl` block is annotated with `#[cfg(feature = "phink")] #[ink(impl)]`.
2. Confirm that the `impl DomainNameService` is the main implementation block of the contract.
3. Each invariant must be annotated with:
    - `#[ink(message)]`
    - `#[cfg(feature = "phink")]`
    - Function names must start with "phink_".
4. Each invariant function must contain at least one assertion statement, such as `assert`, `assert_ne`, `panic`, etc.
5. Be creative and consider corner cases to ensure the thoroughness of the invariants.

Output example:

```rust
#[cfg(feature = "phink")]
#[ink(impl)]
impl DomainNameService {
    // This invariant ensures that `domains` doesn't contain the forbidden domain that nobody should register 
    #[ink(message)]
    #[cfg(feature = "phink")]
    pub fn phink_assert_hash42_cant_be_registered(&self) {
        for i in 0..self.domains.len() {
            if let Some(domain) = self.domains.get(i) {
                // Invariant triggered! We caught an invalid domain in the storage...
                assert_ne!(domain.clone().as_mut(), FORBIDDEN_DOMAIN);
            }
        }
    }
}
`` `
Sources in the Prompt

If your contract is small enough and contains multiple Rust files, you could use the following snippet, to put everything inside everything.rs.

find . -name "*.rs" -not -path "./target/*" -exec cat {} + > everything.rs

Copy paste the content after your system prompt, and examine the LLM invariants. Otherwise, simply copy paste the code from your lib.rs

Runtime Integration

Phink provides developers with the flexibility to customize their fuzzing environment through a simple interface. By editing contract/custom/custom.rs and contract/custom/preferences.rs, developers can tailor the runtime storage and contract initialization processes to suit their testing needs. Having a clone of Phink is required, in order to modify the source code.

Custom Runtime Storage

Phink allows developers to tailor the runtime environment by customizing the storage configuration. This enables the creation of versatile and realistic testing scenarios.

Example

impl DevelopperPreferences for Preferences {
    fn runtime_storage() -> Storage {
        let storage = RuntimeGenesisConfig {
            balances: BalancesConfig {
                balances: (0..u8::MAX) // Allocates substantial balance to accounts
                    .map(|i| [i; 32].into())
                    .collect::<Vec<_>>()
                    .iter()
                    .cloned()
                    .map(|k| (k, 10000000000000000000 * 2))
                    .collect(),
            },
            ..Default::default()
        }
            .build_storage()
            .unwrap();
        storage
    }
}

Customization Points

  • runtime_storage: This function is your gateway to defining any mocks or RuntimeGenesisConfig settings needed for your testing environment. Whether it’s allocating funds, initializing storage items, or setting up custom storage, you can adjust these configurations to mirror your deployment scenarios closely. This flexibility allows you to test how your ink! smart contract behave in various simulated network states.

Contract Initialization

The on_contract_initialize function can be adapted to execute additional initialization logic, such as uploading supplementary contracts or handling dependencies.

Usage Example

fn on_contract_initialize() -> anyhow::Result<()> {
    Contracts::bare_upload_code(
        AccountId32::new([1; 32]),
        fs::read("adder.wasm")?,
        None,
        Determinism::Enforced,
    );
    Ok(())
}

Customization Points

  • on_contract_initialize: Use this function to automate contract uploads, configure dependencies, or perform any setup necessary before testing.

Custom Runtime Parameters

Phink provides default runtime configurations, but developers can provide their own runtime parameters in contract/runtime.rs. This can be particularly useful if you wish to connect your fuzzing environment to your own Substrate runtime, so Phink can be adapted to work with your specific runtime. You can edit the runtime configure here.

Example: Custom Runtime Configuration

For instance, users could customize the pallet_timestamp runtime parameters:

impl pallet_timestamp::Config for Runtime {
    type MinimumPeriod = CustomMinimumPeriod;
    ...
}

Seed Format

In Phink, a seed is structured to guide the fuzzing process effectively. The seed is composed of:

  • 4 bytes: Represents the balance value to be transferred to the message if it’s payable.
  • 1 byte: Specifies the origin, applicable if fuzzing origin is enabled in the configuration.
  • 4 bytes: Identifies the message selector.
  • Remaining bytes: Contains the SCALE-encoded parameters for the message.

If your configuration allows more than one message per input, Phink uses the delimiter "********" to separate multiple messages within a single input. This allows for comprehensive testing across multiple scenarios from a single seed.

Example

Here’s an example explanation for the seed 0000000001fa80c2f6002a2a2a2a2a2a2a2a0000000103ba70c3aa18040008000f00100017002a00 :

SegmentBytesDescription
Balance Transfer000000004 bytes for balance (no transfer in this case).
Origin011 byte indicating the origin (Alice) (enabled in config).
Message Selector 1fa80c2f64 bytes for the first message selector.
Parameters 100SCALE-encoded parameters for the first message.
Message Delimiter2a2a2a2a2a2a2a2aDelimits the first and second messages (********).
Balance Transfer000000014 bytes for balance (1 unit transfered).
Origin031 byte indicating the origin (Charlie) for the second message.
Message Selector 2ba70c3aa4 bytes for the second message selector.
Parameters 218040008000f00100017002a00SCALE-encoded vector: [4, 8, 15, 16, 23, 42]

Explanation

  • Balance Transfer: The 4 bytes representing the balance transfer amount (set to 00000000 for the first message), indicating no value is being transferred for either message.
  • Origin: A single byte is used (01 for the first message and 03 for the second) to specify the origin of the call, useful for testing scenarios with different origins.
  • Message Selector: The first message for example begins with a 4-byte identifier (fa80c2f6), indicating which message within the contract is being invoked.
  • Parameters: Following the message selector, SCALE-encoded parameters are specified (example: 00), representing the input data for each message.
  • Message Delimiter: This seed uses the delimiter ******** (represented as 2a2a2a2a2a2a2a2a) to separate multiple messages within a single input, allowing more complex interactions to be tested.

Running One Seed

To execute a single seed, use the following command:

phink execute my_seed.bin

This command runs the specific seed my_seed.bin, providing targeted fuzzing for individual transaction testing.

Running All the Seeds

Run all seeds sequentially using:

phink run

This command iterates over the corpus folder, executing each seed to ensure a comprehensive fuzzing process covering all previously discovered cases.

Generating a Seed

To generate a new seed, you need to construct it using the prescribed format. Start with the required byte sequences for balance, origin, message selector, and parameters, then save it in your designated seed directory.

Importance of seed generation

The ability to manually create seeds is crucial for enhancing the effectiveness of the fuzz testing process. By creating custom seeds, developers can guide the fuzzer to explore paths and scenarios that might not be easily discovered through automated means. This, in turn, increases the overall coverage of the fuzzing campaign, ensuring that more potential vulnerabilities and edge cases are identified and addressed. For generating the SCALE-encoded parameters, developers can utilize tools like cargo contract or Polkadot.js.

Adding a Seed to the Corpus

To add a custom seed to the corpus, you can use the following command:

cargo ziggy add-seeds -i my_custom_seeds/ -z output/
  • my_custom_seeds/: Directory containing your custom seeds.
  • output/: Directory where the fuzzing output is stored.

Once added, the corpus will use these seeds in subsequent fuzzing processes.

Viewing and Editing Seeds

To view the hexadecimal content of a seed, use:

xxd -c 3000 -p output/phink/corpus/one_seed.bin > abc.out

This command converts the binary seed file into hex for easier reading and editing.

To edit a seed:

  1. Open the hex file in your editor:

    vim abc.out
    
  2. Save changes and revert the hex file to binary:

    rm seed.bin # used to bypass cached seed
    xxd -r -p abc.out seed.bin
    
  3. Execute the updated seed with:

    phink execute seed.bin
    

Concepts and Terminology

Concepts

Fuzzing in general

Fuzzing is an automated software testing technique that involves providing random data inputs to a program. The primary goal is to uncover anomalies, such as crashes, assertion failures, that signify potential vulnerabilities.

Property-based Fuzzing

Property-based testing involves specifying properties or invariants that your ink! contract should always satisfy. In Phink, these properties act as assertions. Phink uses this approach by allowing developers to define properties directly within ink! smart contracts. Such properties are then tested against varied inputs, ensuring the contract maintains its invariants across all possible data conditions.

Coverage-guided Fuzzing

Coverage-guided fuzzing is a fuzzing strategy that focuses on maximizing code coverage during testing. It uses feedback from code execution paths to guide input generation, focusing on unexplored parts of the code. Phink instruments ink! smart contracts to track code coverage, optimizing its fuzzing efforts by targeting less examined paths.

Terminology

Corpus

A corpus refers to the collection of all input samples used during the testing process. It is continuously updated with new inputs that lead to unique execution paths.


Seed

A seed is an initial input provided to the fuzzer to start the testing process. Seeds serve as the starting point for generating new test cases and are crucial for initializing a diverse and effective fuzzing campaign. A strong set of seed inputs can significantly enhance the fuzzing campaign.


Invariants

Invariants are conditions or properties that must remain true at all times during the execution of a program or contract. In property-based testing, invariants are used as assertions to verify the consistent behavior of smart contracts under various input conditions. Breaking an invariant indicates a potential bug or vulnerability.


Instrumentation

Instrumentation involves modifying a program to collect runtime information such as code coverage data. In fuzzing, instrumentation is used to trace execution paths, enabling coverage-guided techniques to generate more informed and effective test cases.


Coverage

Coverage is a measure of how much of a program’s code is tested during fuzzing. High coverage corresponds to a good assessment of the contracts logic.


Contract Selectors

ink! contract selectors are unique identifiers for functions within ink! smart contracts. Selectors are derived from function signatures and are used to call specific functions within a contract deployed on the blockchain.


How Phink Works

Phink is built on top of AFL++, leveraging its capabilities to provide effective fuzz testing for ink! smart contracts. Here’s an overview of how it operates:

AFL++ Integration

Phink utilizes AFL++ through two key components:

  • ziggy: A multifuzzing crate that enables integration with multiple fuzzers.
  • afl.rs: A crate that spawns AFL++ fuzzers, facilitating seamless mutation and coverage tracking.

AFL++ Mechanics

AFL++ mutates the input bytes and evaluates whether these mutations increase code coverage. If a mutation results in new execution paths, the modified seed is retained in the corpus. This iterative process enhances the likelihood of discovering hidden vulnerabilities.

Monitoring Execution

Users can monitor the execution logs using familiar AFL++ tools. For instance, by using tail, you can view real-time fuzzer logs and activity:

tail -f output/phink/logs/afl.log
tail -f output/phink/logs/afl_1.log #if multi-threaded

Additionally, tools like afl_showmap allow developers to debug and visualize the coverage maps.

Coverage-Guided Strategy

Currently, Phink employs a partially coverage-guided approach. While full coverage feedback from low-level instrumentation is not available yet, plans are underway to integrate this capability via WASMI or PolkaVM in future versions.

Execution and Validation

For each generated seed, Phink executes the associated input on a mock-emulated ‘node’. This setup ensures that invariants are verified : known selectors are checked to ensure that invariants hold across different message calls.

Contract Instrumentation

Phink instruments contracts using the syn crate, allowing for precise modification and analysis of the smart contract code. This instrumentation is crucial for identifying potential vulnerabilities and ensuring the integrity of the fuzz testing process.

Troubleshooting

Debugging Phink

AFL++ Logs

If you encounter unexpected behavior, examining the AFL++ logs can provide good insights. In most cases, developers will find more information by executing:

tail -f your_output/phink/logs/afl.log

Replace your_output with the directory defined in your phink.toml under fuzz_output. This will give you a real-time view of the log output, helping you identify any issues during the fuzzing process.

Executing a Single Seed

To debug specific cases where a contract crashes, you can execute a single seed. This method allows you to instantiate a contract and identify crash points more easily:

phink execute output/phink/corpus/selector_1.bin

This command runs a single fuzzing input, making it easier to pinpoint problems.

Harness Coverage

Use the harness coverage feature if you need insights into Phink’s functionality, particularly if you plan to contribute or debug the tool itself:

phink harness-cover

Be aware that this is primarily for those who want to dive deeper into the coverage of Phink and is not generally necessary for regular debugging.

Support Channels

For additional help, you can join us on Discord where our community and team are active. Alternatively, feel free to message me at kevin[🎩]srlabs.de.

Happy fuzzing!

FAQ

Why the name ‘Phink’ ?

Mystère et boule de gomme.