How to Write a Detector in Aderyn Step by Step

How to Write a Detector in Aderyn Step by Step

Creating the division_before_multiplication Detector: Our Process Explained

In this post, you'll learn how to develop a custom detector in Aderyn, a Rust-based static analyzer for Solidity smart contracts. We'll guide you through creating the `division_before_multiplication` detector, from understanding the vulnerability and writing a test contract to analyzing the AST and implementing the detector in Rust. By the end, you'll be equipped to identify and capture instances where division operations precede multiplication, potentially causing precision loss in Solidity code.

What will you find here?

In this post, you will learn from scratch how to write a detector in Aderyn. Specifically, we will explain how to create the division_before_multiplication detector step by step, so you can try writing your own based on this information.

Introduction

What is Aderyn?

Aderyn is an open-source Rust-based static analyzer for Solidity smart contracts. It helps protocol engineers and security researchers identify vulnerabilities in Solidity codebases, highlighting potential issues and integrating seamlessly into development workflows with fast command-line functionality and custom detector frameworks.

Step-by-Step Guide to Developing thedivision_before_multiplication Detector

In this section, we need to identify the vulnerability we want to detect and understand how to identify it in our AST (Abstract Syntax Tree).

An AST is a structured representation of code that compilers primarily use to read code and generate target binaries. It is often stored as JSON.

In this case, we are going to automate the detection of a mathematical vulnerability in Solidity, which involves performing divisions before multiplications. This is a significant error due to the loss of precision in mathematical operations caused by the compiler's handling of decimals in the results.

  • Step Two: Write Our Own Contract Containing the Vulnerability

In this step, we will create a smart contract that includes the specific vulnerability we want to detect. This will allow us to test and refine our detector. Here is an example of a Solidity contract with the division-before-multiplication issue:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

contract DivisionBeforeMultiplication {
    uint public result;

    function calculateWrong(uint a, uint b, uint c, uint d) external {
        result = a * d + b / c * b / d; 
    }

    function calculateAlsoWrong(uint a, uint b, uint c) external {
        result = (a + b / c * b) * c; 
    }

    function calculateAl(uint a, uint b, uint c) external {
        result = (a / b * c); 
    }

    function calculateStillWrong(uint a, uint b, uint c) external {
        result = a + b / c * b * c; 
    }

    function calculateCorrect(uint a, uint b, uint c) external {
        result = a + b * b / c + b * c; 
    }

    function calculateAlsoCorrect(uint a, uint b, uint c, uint d) external {
        result = (a + ((b * d) / (c * b))) * d; 
    }
}
  • Step Three: Understand the .json AST File Result

To begin understanding how we can extract information about the problem and write our detector to identify it, we need to analyze the .json AST file.

The AST (Abstract Syntax Tree) provides a structured representation of the Solidity code, which can be used to pinpoint where the vulnerability occurs. By examining the AST, we can determine how divisions and multiplications are represented and identify patterns that indicate the division-before-multiplication issue.

  1. Generate the .json AST File: Use the Solidity compiler to generate the AST for your contract.

  2. Examine the AST Structure: Open the generated DivisionBeforeMultiplication.json file and look for the nodes representing division and multiplication operations. These nodes will contain information about the operation type and the order of execution.

  3. Identify Relevant Nodes: Look for BinaryOperation nodes in the AST. Each BinaryOperation node will have details about the operation (/ for division and * for multiplication) and the operands involved.

  4. Determine the Pattern: Identify the pattern where a division operation is followed by a multiplication operation without proper handling to ensure precision. This pattern will be the basis for your detector.

  • Step Four: Writing the Detector

Let's break down the implementation of the DivisionBeforeMultiplicationDetector step by step.

  1. Define the Detector Structure

We define a structure to hold instances of the detected issue:

pub struct DivisionBeforeMultiplicationDetector {
    found_instances: BTreeMap<(String, usize, String), NodeID>,
}

This structure uses a BTreeMap to store instances of the vulnerability. The keys are tuples consisting of the source file name, line number, and a description of the issue. NodeID represents the node in the AST where the issue was found.

  1. Implement theIssueDetector Trait

We implement the IssueDetector trait for our detector. This trait requires a detect method that performs the analysis.

impl IssueDetector for DivisionBeforeMultiplicationDetector {
    fn detect(&mut self, context: &WorkspaceContext) -> Result<bool, Box<dyn Error>> {
        // Iterate over all binary operations in the context and filter for multiplication operations (*)
        for op in context.binary_operations().iter().filter(|op| op.operator == "*") {
            // Check if the left operand of the multiplication is a binary operation (i.e., another operation)
            if let Expression::BinaryOperation(left_op) = op.left_expression.as_ref() {
                // Check if this left operation is a division
                if left_op.operator == "/" {
                    // Capture the instance of the vulnerability
                    capture!(self, context, left_op)
                }
            }
        }
        // Return true if any instances were found, otherwise false
        Ok(!self.found_instances.is_empty())
    }
}
  1. Detailed Logic
  • Iterate Over Binary Operations: We use context.binary_operations() to get all binary operations and filter to select only multiplication operations.

  • Check the Left Operand: This checks if the left operand of the multiplication is another binary operation.

  • Identify Division Operation: We then check if this left binary operation is a division.

Capture the Instance: If both conditions are met, we capture this instance as a potential vulnerability.

  • Step five: Add your Detector to the mod.rs file

  • Step six: Register your detector

  • Step seven: Run your custom detector locally

cargo run -- ./tests/contract-playground

Code complete:

use std::collections::BTreeMap;
use std::error::Error;

use crate::ast::NodeID;

use crate::capture;
use crate::detect::detector::IssueDetectorNamePool;
use crate::{
    ast::Expression,
    context::workspace_context::WorkspaceContext,
    detect::detector::{IssueDetector, IssueSeverity},
};
use eyre::Result;

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

impl IssueDetector for DivisionBeforeMultiplicationDetector {
    fn detect(&mut self, context: &WorkspaceContext) -> Result<bool, Box<dyn Error>> {
        for op in context
            .binary_operations()
            .iter()
            .filter(|op| op.operator == "*")
        {
            if let Expression::BinaryOperation(left_op) = op.left_expression.as_ref() {
                if left_op.operator == "/" {
                    capture!(self, context, left_op)
                }
            }
        }

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

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

    fn title(&self) -> String {
        String::from("Incorrect Order of Division and Multiplication")
    }

    fn description(&self) -> String {
        String::from("Division operations followed directly by multiplication operations can lead to precision loss due to the way integer arithmetic is handled in Solidity.")
    }

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

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

#[cfg(test)]
mod division_before_multiplication_detector_tests {
    use super::DivisionBeforeMultiplicationDetector;
    use crate::detect::detector::{detector_test_helpers::load_contract, IssueDetector};

    #[test]
    fn test_template_detector() {
        let context = load_contract(
            "../tests/contract-playground/out/DivisionBeforeMultiplication.sol/DivisionBeforeMultiplication.json",
        );

        let mut detector = DivisionBeforeMultiplicationDetector::default();
        let found = detector.detect(&context).unwrap();
        assert!(found);
        assert_eq!(detector.instances().len(), 4);
        assert_eq!(
            detector.severity(),
            crate::detect::detector::IssueSeverity::Low
        );
        assert_eq!(
            detector.title(),
            String::from("Incorrect Order of Division and Multiplication")
        );
        assert_eq!(
            detector.description(),
            String::from("Division operations followed directly by multiplication operations can lead to precision loss due to the way integer arithmetic is handled in Solidity.")
        );
    }
}

Resources: