phink_lib/
lib.rs

1#![feature(os_str_display)]
2#![feature(duration_millis_float)]
3#![recursion_limit = "1024"]
4
5extern crate core;
6use crate::{
7    cli::{
8        config::Configuration,
9        env::{
10            PhinkEnv,
11            PhinkEnv::FromZiggy,
12        },
13        format_error,
14        ziggy::ZiggyConfig,
15    },
16    cover::report::CoverageTracker,
17    fuzzer::fuzz::{
18        Fuzzer,
19        FuzzingMode::{
20            ExecuteOneInput,
21            Fuzz,
22        },
23    },
24    instrumenter::{
25        instrumentation::Instrumenter,
26        seedgen::generator::SeedExtractInjector,
27        traits::visitor::ContractVisitor,
28    },
29};
30use anyhow::{
31    bail,
32    Context,
33};
34use clap::Parser;
35use std::{
36    env::var,
37    path::PathBuf,
38};
39use PhinkEnv::FuzzingWithConfig;
40
41pub mod cli;
42pub mod contract;
43pub mod cover;
44pub mod fuzzer;
45pub mod instrumenter;
46
47/// This type is used to handle all the different errors on Phink. It's a simple binding to
48/// `anyhow`.
49pub type EmptyResult = anyhow::Result<()>;
50pub type ResultOf<T> = anyhow::Result<T>;
51
52/// This struct defines the command line arguments expected by Phink.
53#[derive(Parser, Debug)]
54#[clap(
55    author,
56    version,
57    about = "šŸ™ Phink: An ink! smart-contract property-based and coverage-guided fuzzer",
58    long_about = None
59)]
60#[command(
61    help_template = "{before-help}{about-with-newline}šŸ§‘ā€šŸŽØ {author-with-newline}\n{usage-heading}\n    {usage}\n\n{all-args}{after-help}"
62)]
63struct Cli {
64    /// Order to execute (if you start here, instrument then fuzz suggested)
65    #[clap(subcommand)]
66    command: Commands,
67    /// Path to the Phink configuration file.
68    #[clap(long, short, value_parser, default_value = "phink.toml")]
69    config: PathBuf,
70}
71
72#[derive(clap::Subcommand, Debug)]
73#[allow(deprecated)]
74enum Commands {
75    /// Starts the fuzzing campaign. Instrumentation required before!
76    Fuzz,
77    /// Run the tests of the ink! smart-contract to execute the
78    /// messages and extracts valid seeds from it. For instance, if a test call three messages,
79    /// those three messages will be extracted to be used as seeds inside the corpus directory
80    GenerateSeed {
81        /// Path where the contract is located. It must be the root directory of
82        /// the contract
83        contract: PathBuf,
84        /// Path where the temporary contract will be compiled to. Optionnal field, set to `tmp` if
85        /// not defined (or somewhere else, depending your OS)
86        compiled_directory: Option<PathBuf>,
87    },
88    /// Instrument the ink! contract, and compile it with Phink features
89    Instrument(Contract),
90    /// Run all the seeds from `corpus/`
91    Run,
92    /// Generate a coverage report, only of the harness. You won't have your contract coverage here
93    /// (mainly for debugging purposes only)
94    HarnessCover,
95    /// Generate a coverage report for your ink! smart-contract. This must me the path of the
96    /// *instrumented* contract !
97    Coverage(Contract),
98    /// Execute one seed
99    Execute {
100        /// Seed to be executed
101        seed: PathBuf,
102    },
103    /// Minimize the corpus taken from `corpus/` (unstable, not recommended)
104    Minimize,
105}
106
107#[derive(clap::Args, Debug, Clone)]
108struct Contract {
109    /// Path where the contract is located. It must be the root directory of
110    /// the contract
111    #[clap(value_parser)]
112    contract_path: PathBuf,
113}
114pub fn main() {
115    // We execute `handle_cli()` first, then re-enter into `main()`
116    if let Ok(config_str) = var(FuzzingWithConfig.to_string()) {
117        if var(FromZiggy.to_string()).is_ok() {
118            let config = ZiggyConfig::parse(config_str);
119            match Fuzzer::new(config) {
120                Ok(fuzzer) => {
121                    if let Err(e) = fuzzer.execute_harness(Fuzz) {
122                        eprintln!("{}", format_error(e));
123                    }
124                }
125                Err(e) => {
126                    eprintln!("{}", format_error(e));
127                }
128            }
129        }
130    } else if let Err(e) = handle_cli() {
131        eprintln!("{}", format_error(e));
132    }
133}
134
135fn handle_cli() -> EmptyResult {
136    let cli = Cli::parse();
137    let conf = &cli.config;
138    if !conf.exists() {
139        bail!(format!(
140            "No configuration found at {}, please create a phink.toml. You can get a sample at https://github.com/srlabs/phink/blob/main/phink.toml\
141            \nFeel free to `wget https://raw.githubusercontent.com/srlabs/phink/refs/heads/main/phink.toml` and customize it as you wish",
142            conf.to_str().unwrap(),
143        ))
144    }
145    let config: Configuration = Configuration::try_from(conf)?;
146
147    match cli.command {
148        Commands::Instrument(contract_path) => {
149            let z_config: ZiggyConfig = ZiggyConfig::new_with_contract(
150                config.to_owned(),
151                contract_path.contract_path.to_owned(),
152            )
153            .context("Couldn't generate handle the ZiggyConfig")?;
154
155            let engine = Instrumenter::new(z_config.to_owned());
156            engine
157                .to_owned()
158                .instrument()
159                .context("Couldn't instrument")?;
160
161            engine.build().context("Couldn't run the build")?;
162
163            println!(
164                "\nšŸ¤ž Contract '{}' has been instrumented and compiled.\nšŸ¤ž You can find the instrumented contract in `{}`",
165                z_config.contract_path()?.display(),
166                z_config.config().instrumented_contract().display()
167            );
168            Ok(())
169        }
170        Commands::Fuzz => {
171            ZiggyConfig::new(config)
172                .context("Couldn't generate handle the ZiggyConfig")?
173                .ziggy_fuzz()
174        }
175        Commands::Run => {
176            ZiggyConfig::new(config)
177                .context("Couldn't generate handle the ZiggyConfig")?
178                .ziggy_run()
179        }
180        Commands::Minimize => {
181            ZiggyConfig::new(config)
182                .context("Couldn't generate handle the ZiggyConfig")?
183                .ziggy_minimize()
184        }
185        Commands::Execute { seed } => {
186            let fuzzer = Fuzzer::new(ZiggyConfig::new(config))
187                .context("Creating a new fuzzer instance faled")?;
188            fuzzer.execute_harness(ExecuteOneInput(seed))
189        }
190        Commands::HarnessCover => {
191            ZiggyConfig::new(config)
192                .context("Couldn't generate handle the ZiggyConfig")?
193                .ziggy_cover()
194        }
195        Commands::Coverage(contract_path) => {
196            CoverageTracker::generate(
197                ZiggyConfig::new_with_contract(config, contract_path.contract_path)
198                    .context("Couldn't generate handle the ZiggyConfig")?,
199            )
200        }
201        Commands::GenerateSeed {
202            contract,
203            compiled_directory,
204        } => {
205            let mut seeder = SeedExtractInjector::new(&contract, compiled_directory)?;
206            seeder
207                .extract(&config.fuzz_output.unwrap_or_default())
208                .context(format!("Couldn't extract the seed from {contract:?}"))?;
209            Ok(())
210        }
211    }
212}