phink

Introduction

Overview of Phink

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

Dashboard overview

phink

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(), you create properties that act as assertions. During testing, the fuzzer checks these properties against every input, which is a set of ink! messages. If a property’s assertion fails, this triggers a panic. 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. Feedback is transmitted to the pallet_contract via the debug_message. Although the fuzzer currently adds feedback on each line executed, Phink is designed to evolve. It will eventually monitor coverage across new edges and code branches.

Why use Phink

Phink addresses security concerns in these three main ways:

  1. Automatically generate and test a diverse range of inputs
  2. Detect edge cases, logical flaws, and bugs leading to contract state reversion
  3. Explore different execution paths by generating input mutation

This extensive testing identifies bugs and potential flaws early in the development cycle, empowering teams to fix vulnerabilities before deployment and deliver safer applications.

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 and IT environment. Let’s jump right into it!

Building from source

Follow these 5 easy steps:

  1. Clone the Repository

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

    You can also run the following command:

    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 To build the Phink Docker image, run the following command in your terminal:

    docker build -t phink .
    

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

Basic workflow

Follow these three high-level steps:

  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 your fuzzing campaign

    • 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. You will learn how to configure the general settings, specify some key paths and fuzzing options, and do some other essential tasks. Without further ado, let’s jump right into it!

Configuration file overview

Here’s how a configuration file looks like:

### 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

The General settings cover these 2 parameters:

  • 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

The Paths settings cover these 3 parameters:

  • 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 contain the source code of the initial contract, with the additional instrumentation instructions. It will also contain 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 contain the log file, the corpus entries, the crashes, and way more.

Deployment

The Deployment settings include these 4 parameters:

  • 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 has 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 a minimum amount of 3000 units during instantiation, set 3000 here.

Fuzzing options

These 4 parameters are important when you configure 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 of 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! is 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 limit

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. The setting ensures that users won’t unintentionally spend more than they wanted on storage allocation. Besides, it offers protection against excessive storage costs by defining an upper limit on how much can be spent on storage within that call.

Default gas limit configuration

The key Gas limit settings include:

  • ref_time: Specify the reference time for gas allocation.
  • proof_size: Define 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. Create another impl in your contract, and 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

First things first: Let’s enable your contract for fuzzing. Run the following command to instrument your ink! smart contract:

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.

The phink generate-seed command is an optional but powerful feature that enhances your fuzzing experience by generating initial seeds from your existing unit and end-to-end (E2E) tests.

What it Does

phink generate-seed executes the unit tests and E2E tests of your ink! smart contract, extracting seeds based on executed messages. These seeds are saved in the corpus/ directory, which highly helps to reach good coverage, as long as you have good tests. Therefore, we encourage to have good and various unit-tests and E2E tests in your contract.

How It Works

  • Unit Tests: The command runs through all defined unit tests and captures the invoked messages, with Alice as the origin and a value of 0.

  • End-to-End Tests: For E2E tests, Phink modifies the Cargo.toml to point to a custom ink! repository. This step ensures necessary modifications are included to print debug messages containing the message’s 4-byte hash and scale-encoded parameters to stdout.

  • If a test invokes at least one message, Phink extracts them all as seeds for use during fuzzing.

Usage

phink generate-seed <CONTRACT> [COMPILED_DIRECTORY]
  • <CONTRACT>: The root directory path of your ink! smart contract.
  • [COMPILED_DIRECTORY]: Optional path for where the temporary contract will be compiled. Defaults to tmp if unspecified.

This will generate a set of initial inputs, derived from your current tests, to kickstart fuzzing.

Why using generate-seed?

Generating seeds from your existing test suite can increase the efficiency of fuzz testing by:

  • Providing a good starting point for fuzzing inputs.
  • Ensuring that the fuzzing process begins with valid and meaningful test cases.

For more information on how seeds work with Phink, refer to the seeds documentation.

3. Run the fuzzer

After instrumenting your contract and writing properties and configuring your phink.toml, let’s get our hands on 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 the above command, you should get 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 the tested code areas. As a rule of thumb, the more green lines you can see there, the better it is for the code.

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!");
    }
}
}

Annotations explaination

  • #[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. Have a clone of Phink if you want to modify the source code.

Custom runtime storage

Phink allows developers to tailor the runtime environment by customizing the storage configuration. Let’s create some 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

  • runtime_storage: Use this function as 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, adjust these configurations to mirror your deployment scenarios closely. This flexibility lets you test how your ink! smart contract behaves 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.

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, customize the pallet_timestamp runtime parameters like this:

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 these 4 parts:

  • 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 enables comprehensive testing across multiple scenarios from a single seed.

Example

Here’s a breakdown 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. This is 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

To run all seeds sequentially, use the following command:

phink run

This command iterates over the corpus folder, executing each seed. This ensures a comprehensive fuzzing process that covers all previously discovered cases.

Generating a seed

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

Importance of seed generation

How can we detect and fix more potential vulnerabilities and edge cases faster? 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. If you need to generate the SCALE-encoded parameters, it’s best to utilize tools like cargo contract or Polkadot.js.

Adding a seed to the corpus

To add a custom seed to the corpus, 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, issue the following command:

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

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

To edit a seed, complete these 3 easy tasks:

  1. Open the hex file in your preferred editor, and edit it

    vim abc.out
    
  2. Save the 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

    phink execute seed.bin
    

Congratulations! We’re off to the races again.

Concepts and terminology

Concepts

Fuzzing

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 and assertion failures. These are intriguing because they pinpoint 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 makes it possible for developers to define properties directly within ink! smart contracts. Such properties are then tested against varied inputs. In this way, the contract maintains its invariants across all possible data conditions. But there is a final twist in the fuzzing tale.

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 fuzzing efforts by targeting less examined paths is what makes the game worth playing.

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 traces execution paths, enabling coverage-guided techniques to generate more informed and effective test cases.


Coverage

Coverage measures how much of a program’s code is tested during fuzzing. High coverage corresponds to a good assessment of the contract’s 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 the fuzzer 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 releases.

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. For each high-level Rust instructions, a feedback is returned via the debug_message map to the fuzzing engine, mapping each instruction to a unique u64 identifier. This map is then “expanded”, instrumented by AFL++ compiler, and ultimately updated the AFL++ shared map everytime a new edge is hit.

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 for debugging. You should only use it if you want to have a coverage of Phink itself. For instance, if you’re planning to contribute to Phink, or to debug it.

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

You can find us on Discord. Alternatively, you can message me on kevin[🎩]srlabs.de.

Happy fuzzing!

FAQ

Why the name ‘Phink’ ?

Mystère et boule de gomme.