After the release of ZIION 23.1, it’s important to learn how one can leverage ZIION VM to perform blockchain development and security auditing. Today, in this blog, we will learn how to perform fuzzing on smart contracts using ZIION in order to find vulnerabilities.
Fuzzing or Fuzz Testing is an automated testing technique (once the fuzzing harness has been created), used to discover potential vulnerabilities or issues in a program by feeding it with vast amounts of random, semi-random, or invalid data.
It helps to identify potential vulnerabilities and weaknesses that may not be apparent through other testing methods. By subjecting the blockchain protocol or smart contract to many input scenarios, fuzzing can exhibit how it handles malformed inputs or unexpected data, which can help to discover potential attack vectors or issues related to scalability and performance.
A significant advantage of fuzzing and why it's critical to smart contracts is that, by design, it proves a vulnerability exists and is not a false positive when found. Furthermore, once it’s running, it's fully automated and can run for days or weeks, easily sifting through millions of input variation possibilities.
To fuzz EVM-based smart contracts, we can use tools like:
Echidna
HEVM
To fuzz Rust-based smart contracts, we can use tools like:
honggfuzz
afl++
cargo-fuzz (libfuzzer)
Most fuzzers are only fully usable on amd64 architecture. We usually create the fuzzing harness with an IDE like VSCodium but one can do it in any text editor.
All the above-mentioned tools are pre-installed and pre-configured in ZIION (amd64). If you have not downloaded and installed ZIION 23.1 (amd64), then please download ZIION and use it to perform fuzzing.
The smart contracts could be written in Solidity and Rust and require different tools to perform fuzzing on them. Let’s discuss fuzzing an EVM-based smart contract and a Rust-based smart contract separately.
To perform fuzzing on a smart contract, you can use specialized tools designed for Ethereum smart contract analysis, which are already pre-installed and configured in ZIION VM.
Here, we will use Echidna EVM Fuzzer to fuzz the smart contract.
Step 1: Let’s take a sample smart contract named, MyToken.sol and ensure it is saved and is available in ZIION VM.
pragma solidity ^0.8.0;
contract MyToken {
// Track token balances in a mapping
mapping(address => uint256) public balances;
// Declare the total supply of tokens
uint256 public totalSupply;
// Event emitted when tokens are transferred
event Transfer(address indexed from, address indexed to, uint256 _value);
// Initialize the contract and assign the total supply of tokens
constructor(uint256 _initialSupply) {
totalSupply = _initialSupply;
balances[msg.sender] = _initialSupply; // Assign the entire supply to the contract creator
}
// Function to check the token balance of an address
function balanceOf(address _owner) public view returns (uint256) {
return balances[_owner];
}
// Function to transfer tokens from one address to another
function transfer(address to, uint256 value) public returns (bool success) {
// Check if the sender has enough tokens
require(balances[msg.sender] >= _value);
// Update the token balances
balances[msg.sender] -= _value;
balances[_to] += _value;
// Emit the Transfer event
emit Transfer(msg.sender, to, value);
return true;
}
}
Now, let us start fuzzing the above-written smart contract using Echidna.
Step 2: Write Properties for Your Smart Contract
Next, you need to write properties for your smart contract that Echidna will try to falsify. A property is a function that returns a boolean value, representing an assertion about the contract's behavior. In Solidity, you can define properties in your smart contract using the following syntax:
function echidna_property_name() public view returns (bool) {
// Your property logic here
}
For example, for the "MyToken" contract, you might write a property that checks if the total supply of tokens remains constant after transfers:
function echidna_totalSupplyInvariant() public view returns (bool) {
return total_Supply == initialSupply;
}
Step 3: Prepare Your Smart Contract for Testing in ZIION VM
Before testing your smart contract with Echidna, make sure it's written in Solidity with a version compatible with Echidna (e.g., ^0.6.0 or ^0.8.0). You also need to compile your contract using the solc compiler.
Step 4: Run Echidna in ZIION VM
To run Echidna on your smart contract, use the echidna command, providing the path to your smart contract file:
echidna /path/to/your/contract.sol
Echidna will start fuzz-testing your contract, attempting to falsify the properties you defined. By default, Echidna will run for a very long time, so you might want to stop the process after some time (e.g. 30 minutes or a few hours) if it doesn't find any issues.
Step 5: Analyze the Results
If Echidna finds a counterexample that falsifies one of your properties, it will report it along with a sequence of transactions that led to the issue. You can use this information to debug and fix your smart contract.
If Echidna doesn't find any issues, it doesn't mean your contract is bug-free, but it's a good indication that your contract might be robust against some common issues.
Step 1: Initialize cargo-fuzz in ZIION:
Navigate to the directory of your Rust smart contract project and initialize cargo-fuzz by running:
cargo fuzz init
This command will create a ‘fuzz’ directory inside your project, containing a ‘Cargo.toml’ file and a ‘fuzz_targets’ folder.
Step 2: Define your fuzz target:
Create a new Rust file inside the ‘fuzz_targets’ directory, e.g., ‘fuzz_smart_contract.rs’. In this file, define a fuzz target function that takes a ‘&[u8]’ input and processes it using your smart contract functions. The cargo-fuzz tool will generate random input data and call this function with it.
For example, considering a simple counter smart contract [simple_counter.rs]:
#![no_main]
use libfuzzer_sys::fuzz_target;
use simple_counter::SimpleCounter;
fuzz_target!(|data: &[u8]| {
if data.len() < 5 {
return;
}
// Use the first byte of the data as an operation selector
let operation = data[0] % 3;
// Use the next 4 bytes as input to the constructor
let init_value = u32::from_le_bytes([data[1], data[2], data[3], data[4]]);
let mut contract = SimpleCounter::new(init_value);
match operation {
0 => contract.increment(),
1 => contract.decrement(),
_ => (),
}
// No need to assert the state, as we are checking for panics and other errors
});
Make sure to replace simple_counter::SimpleCounter with the appropriate path to your smart contract module and struct.
Step 3: Build and run the fuzzer in ZIION VM:
Run the fuzzer by executing the following command:
cargo fuzz run fuzz_smart_contract
Replace fuzz_smart_contract with the name of the fuzz target you created in step 4.
Step 4: Run cargo-fuzz in ZIION VM
To run the fuzzer, use the following command:
cargo fuzz run fuzz_target_name
Replace fuzz_target_name with the name of your fuzz target file without the .rs extension.
cargo-fuzz will now start generating random inputs and running your fuzz target. It will continue running until you stop it manually or it encounters an issue, like a panic or a bug.
Step 5: Analyze the results
If the fuzzer finds a bug, it will report the input that caused the issue. Use this information to debug and fix your Rust-based smart contract. If the fuzzer does not find any issues, it does not guarantee your contract is bug-free, but it provides some level of confidence in your contract's correctness.
Understand the Contract: Before starting fuzzing, it is essential to have a good understanding of the smart contract being audited. This includes understanding the contract's functionality, the data it handles, and the expected behavior of the contract under different conditions. It will help identify which functions are great fuzzing targets.
Well-defined Test Cases: Create a set of test cases that cover different scenarios and input variations. The test cases should include both valid and invalid input data, such as edge cases and unexpected inputs. Test cases that include the largest amount of different functions you want to abuse are great candidates for fuzzing.
Triaging and debugging results: Analyze fuzzing results to identify potential vulnerabilities and security issues. Prioritize the identified issues based on their severity and impact, and provide detailed recommendations for remediation. The triaging and debugging phase must not be underestimated.
Setup a VPS: Once your fuzzing harness is well set up and tested to work, don’t hesitate to use a VPS or reliable server to let the fuzzing run for days. Just don’t forget to triage and debug the results!
Remember that while fuzzing can help identify issues, it does not guarantee that a smart contract is secure. Always combine fuzz testing with other testing techniques, code review, and formal verification, when possible, to ensure the security of your smart contracts.
Fuzzing requires you to run various tools, and installing those tools and configuring them individually would be time-consuming. To save precious time, you can install ZIION, which contains all the necessary tools used for Fuzzing. Download ZIION 23.1 now!