Detectors Quickstart

Aderyn makes it easy to build Static Analysis detectors that can adapt to any Solidity codebase and protocol. This guide will teach you how to build, test, and run your custom Aderyn detectors.

Prerequisites

Before starting to create your custom Aderyn detectors, ensure you have the following:

  • Rust: Aderyn is built in Rust, so you must install Rust and Cargo (Rust's package manager) before running it. If you haven't installed Rust yet, follow the instructions on the official Rust website.

Suggested VSCode extensions:

If you haven't yet, read the Aderyn quickstart and what is a detector documentation before getting started.


Step: 1 Clone the Aderyn repository

Navigate to the official Aderyn repository, clone the repository, and open it in your favorite code editor:

git clone https://github.com/Cyfrin/aderyn.git && cd aderyn

Once cloned, this is how, simplified, your folder structure will look like:

└── πŸ“aderyn
    └── report.md
    └── πŸ“.github
        └── πŸ“images
            └── aderyn_logo.png
        └── πŸ“workflows
            └── cargo.yml
    └── πŸ“.vscode
        └── settings.json
    └── report.md
    └── πŸ“src
        └── πŸ“ast
        └── πŸ“context
        └── πŸ“detect
            └── detector.rs
            └── _template.rs
            └── πŸ“high
                └── mod.rs
            └── πŸ“low
                └── mod.rs
            └── πŸ“medium
                └── mod.rs
            └── πŸ“nc
                └── mod.rs
            └── mod.rs
        └── πŸ“framework
        └── πŸ“report
        └── πŸ“visitor
    └── πŸ“tests
        └── πŸ“contract-playground

Source: https://github.com/Cyfrin/aderyn

The folders and files you should be familiar with when developing your custom Aderyn detectors are:

  • report.md: The standard report generated by Aderyn on the contracts contained in the tests directory

  • _template.rs: The template used to create new detectors

  • detect: The folder containing the detectors and dependencies

    • detectors.rs: exposes a list of detectors to be called by Aderyn

    • /high and /low folders: contains the detectors used by Aderyn divided by severity

      • mod.rs: exposes a list of detectors to be called inside the detectors.rs file

  • tests: contains sample contracts usually used to testing your detectors.

Now that our Aderyn cloned project is ready, let's see how to create our first detector.


Step 2: Create a new detector file using the template

Navigate to the aderyn_core/src/detect/low folder, and create a new copy of the _template.rs file named my_first_detector.rs

Inside the detector's template, you will find the following code:


#[derive(Default)]
pub struct TemplateDetector {
    // Keys are source file name and line number
    found_instances: BTreeMap<(String, usize, String), NodeID>,
}

impl IssueDetector for TemplateDetector {
    fn detect(&mut self, context: &WorkspaceContext) -> Result<bool, Box<dyn Error>> {
        // capture!(self, context, item);

        Ok(!self.found_instances.is_empty())
    }

    fn severity(&self) -> IssueSeverity {
        IssueSeverity::High
    }

    fn title(&self) -> String {
        String::from("High Issue Title")
    }

    fn description(&self) -> String {
        String::from("Description of the high issue.")
    }

    fn instances(&self) -> BTreeMap<(String, usize, String), NodeID> {
        self.found_instances.clone()
    }

    fn name(&self) -> String {
        format!("high-issue-template")
    }
}

As we've seen in the What is a detector section - in the code above, we are:

  • Declaring a new detector named TemplateDetector

  • Implementing an Issue detector with inside the following functions:

    • detect(): containing the logic to detect a given vulnerability pattern

    • severity(): the severity associated with the vulnerability that will appear in the report

    • title(): the title of the vulnerability that will be used for the report

    • description(): the description of the vulnerability

    • instances(): internal function that returns the instances found by the detector (don't change this)

    • name(): title of the vulnerability in kebab-case

Refer to the Detectors API Reference for an in-depth look at how every function works.

Before moving forward, let's rename our detector from TemplateDetector to MyFirstDetector.

Inside your my_first_detector.rs file, navigate to line 21, find the pub struct TemplateDetector and rename it to MyFirstDetector.

Then find the impl IssueDetector for TemplateDetector and rename it to impl IssueDetector for MyFirstDetector.

pub struct MyFirstDetector {
    // Keys are source file name and line number
    found_instances: BTreeMap<(String, usize, String), NodeID>,
}

and also in the implementation

impl IssueDetector for MyFirstDetector {

Step 3: Add your Detector to the mod.rs file

If you have installed the VSCode extensions suggested in the prerequisites, you will notice that the linter gives us some errors. To solve this, you will need to add the newly created detector to the mod.rs file, which will, in return, make the right dependencies available to your detector.

To do this, you'll first need to move the detector file inside the aderyn_code/src/detect/low folder, and open the low/mod.rs file.

Here, we will have to import our newly created detector:

pub(crate) mod arbitrary_transfer_from;
pub(crate) mod block_timestamp_deadline;
pub(crate) mod delege;
//Add the following line
pub(crate) mod my_first_detector;

pub use arbitrary_transfer_from::ArbitraryTransferFromDetector;
pub use block_timestamp_deadline::BlockTimestampDeadlineDetector;
pub use delegate_call_in_loop::DelegateCallInLoopDetector;
//Add the following line
pub use my_first_detector::MyFirstDetector;

This should make sure all the dependencies are available to your detector. With that, you can now start editing the detector:


Step 4: Write your custom detector

Let's say we want to build a detector that checks whether a function has or not "useless" modifiers.

Back in my_first_detector.rs, inside the IssueDetector, copy and paste the following code:

impl IssueDetector for MyFirstDetector {
    fn detect(&mut self, context: &WorkspaceContext) -> Result<bool, Box<dyn Error>> {
        for modifier in context.modifier_definitions() {
            let mut count = 0;
            for inv in context.modifier_invocations() {
                if let Some(id) = inv.modifier_name.referenced_declaration {
                    if id == modifier.id {
                        count += 1;
                    }
                }
            }
            if count == 1 {
                capture!(self, context, modifier);
            }
        }

        Ok(!self.found_instances.is_empty())
    }

    fn title(&self) -> String {
        String::from("Modifiers invoked only once can be shoe-horned into the function")
    }

    fn description(&self) -> String {
        String::from("")
    }

    fn severity(&self) -> IssueSeverity {
        IssueSeverity::Low
    }

    fn instances(&self) -> BTreeMap<(String, usize, String), NodeID> {
        self.found_instances.clone()
    }

    fn name(&self) -> String {
        format!("{}", IssueDetectorNamePool::UselessModifier)
    }
}

Source: https://github.com/Cyfrin/aderyn/blob/dev/aderyn_core/src/detect/low/useless_modifier.rs

The first thing you can notice is the detect() function, inside of which we're using our WorkspaceContext to find all modifier_definitions and modifier_invocations inside the AST.

For each definition, we then check all the invocations, and if a defined modifier has no invocations, it's redundant and should be removed from the code. When this happens, we call the capture() function that will add it to the final report, together with the information defined inside the functions:

You can find more examples of detectors inside the official GitHub repo.

Once that's done, we call the Ok function, passing true or false based on whether the found_instances array has items in it or not.

If you're nervous about whether or not this detector is working correctly, we suggest you learn how to test your Aderyn detectors. You'll want this to be your next step after following this quickstart anyways.


Step 5: Register your detector

Now that your detector is ready to be used, it's time to add it to Aderyn and make sure the new detector is called when the aderyn . the command is run.

First of all, open the detectors.rs file inside aderyn_core/src/detect/detector.rs and add your detector to the "low" vulnerability group as follows:

use crate::{
    ast::NodeID,
    context::workspace_context::WorkspaceContext,
    detect::{
        high::{
            ArbitraryTransferFromDetector, BlockTimestampDeadlineDetector,
            DelegateCallInLoopDetector,
        },
        low::{
            AvoidAbiEncodePackedDetector, CentralizationRiskDetector,
            ConstantsInsteadOfLiteralsDetector, DeprecatedOZFunctionsDetector, EcrecoverDetector,
            EmptyBlockDetector, InconsistentTypeNamesDetector, LargeLiteralValueDetector,
            NonReentrantBeforeOthersDetector, PushZeroOpcodeDetector, RequireWithStringDetector,
            SolmateSafeTransferLibDetector, UnindexedEventsDetector,
            UnprotectedInitializerDetector, UnsafeERC20FunctionsDetector, UnsafeERC721MintDetector,
            UnspecificSolidityPragmaDetector, UselessInternalFunctionDetector,
            UselessModifierDetector, UselessPublicFunctionDetector, ZeroAddressCheckDetector,
            // Add the following line of code
            MyFirstDetector,
            
        },
    },
};

Source: https://github.com/Cyfrin/aderyn/blob/dev/aderyn_core/src/detect/detector.rs#L4

Then find the get_all_issue_detectors function and add your detector to the vector returned by it:

pub fn get_all_issue_detectors() -> Vec<Box<dyn IssueDetector>> {
    vec![
        Box::<DelegateCallInLoopDetector>::default(),
        Box::<CentralizationRiskDetector>::default(),
        Box::<SolmateSafeTransferLibDetector>::default(),
        [...]
        Box::<UnprotectedInitializerDetector>::default(),
        // Add the following line of code
        Box::<MyFirstDetector>::default(),
    ]
}

Source:https://github.com/Cyfrin/aderyn/blob/dev/aderyn_core/src/detect/detector.rs#L31

Then, find IssueDetectorNamePool in detector.rs and add our MyFirstDetector to the list:

pub(crate) enum IssueDetectorNamePool {
.
.
.
    UnprotectedInitializer,
    // Add the following line of code
    MyFirstDetector,
    Undecided,
}
```

Lastly, add your custom detector to the detector_name match statement inside the request_issue_detector_by_name function:

 pub fn request_issue_detector_by_name(detector_name: &str) -> Option<Box<dyn IssueDetector>> {
    // Expects a valid detector_name
    let detector_name = IssueDetectorNamePool::from_str(detector_name).ok()?;
    match detector_name {
        IssueDetectorNamePool::DelegateCallInLoop => {
            Some(Box::<DelegateCallInLoopDetector>::default())
        }
        IssueDetectorNamePool::CentralizationRisk => {
            Some(Box::<CentralizationRiskDetector>::default())
        }
        IssueDetectorNamePool::SolmateSafeTransferLib => {
            Some(Box::<SolmateSafeTransferLibDetector>::default())
        }
        [...]
        IssueDetectorNamePool::UselessPublicFunction => {
            Some(Box::<UselessPublicFunctionDetector>::default())
        }
        // Add the following line of code
        IssueDetectorNamePool::MyFirstDetector => {
            Some(Box::<MyFirstDetector>::default())
        }
        IssueDetectorNamePool::Undecided => None,
    }
 }

Source: https://github.com/Cyfrin/aderyn/blob/dev/aderyn_core/src/detect/detector.rs#L106

Well done! Now your detector will be registered and run every time Aderyn is run locally πŸŽ‰

Now it's time to run our newly created detector!


Step 6: Run your custom detector locally

Once your detector has been registered, you can run it locally against any Foundry-based Solidity smart contract codebase.

Similarly to what is explained in the Aderyn quickstart guide, you can copy and paste the following command in your terminal to run your local Aderyn version, including your custom detectors:

cargo run -- ./tests/contract-playground 

Running the code above will call all Aderyn detectors, including your custom detectors. This will add a new vulnerability, a report.md file, as specified in the runner.rs.

If you want to run your custom detectors using the standard aderyn . command, check out the contributions guidelines and open a PR following the guidelines.

You'll now see your new detector printed out at the end of the report.md!

  - [L-22: Modifiers invoked only once can be shoe-horned into the function](#l-22-modifiers-invoked-only-once-can-be-shoe-horned-into-the-function)

Step 7: Test your detector

If you haven't already, learn how to test your Aderyn detectors. Your detector isn't done, and it won't be merged unless tests have been written for it!

Last updated