Introduction
Recon is a tool for automatically scaffolding and running invariant testing in the cloud.
Creating An Account
To get started with Recon, you'll need to create an account using your GitHub account.
Once you've created an account, you'll be redirected to the dashboard where you'll have access to all of the tools in the Recon suite. To run a job you'll need a Recon Pro account. The other tools in the Recon suite are free to use without a Pro account.
Learn Invariant Testing
Most of our guides and documentation are focused on using fuzzing tools, primarily Echidna and Medusa because we use them internally at Recon. However, we also support running other tools on our cloud infrastructure such as Foundry (fuzzing), Halmos (formal verification), and Kontrol (formal verification).
After having chosen a tool best suited to your needs and downloading it locally, you can get started by running jobs directly on the jobs pge.
If you're new to invariant testing, we recommend you start with the following posts before getting started:
- First Day At Invariant School
- How To Define Invariants
- Implementing Your First Smart Contract Invariants: A Practical Guide
If you prefer a full end-to-end video bootcamp, checkout this series on everything you need to know about invariant testing.
Once you've grasped the basics of invariant testing you can setup your first suite and run it on Recon. For a step-by-step guide on how to do this, check out the First Steps guide.
If you have any questions about how to use Recon or invariant testing in general, you can reach out to our team on Discord.
Additional Learning Resources
If you're looking for more resources to help you get started with invariant testing, please see the following:
For more resources on Echidna and Medusa, please see the following:
First Steps
To make getting started with Invariant Testing as easy as possible, we created the create-chimera-app repository which serves as a template that you can use to create a new project with invariant testing built in.
This template uses our Chimera framework to let you run invariant tests with Echidna and Medusa that can be easily debugged using Foundry. We've also incorporated contracts from our Setup Helpers repo which make managing the test suite setup much simpler.
Creating A New Project
If you haven't already, first create a recon account as described in the create an account section and install either Echidna or Medusa.
Next you'll need to create a new project using the create-chimera-app template. You can use the Use this template button on GitHub to do this. This will automatically create a new repository using the create-chimera-app template on your GitHub account.
Create-chimera-app ships with the default foundry project as an example. This is a simple Counter.sol
contract that you can use to test the fuzzing capabilities of Echidna and Medusa.
Testing
An example invariant is implemented in the Properties
contract and some in-line properties are defined in the TargetFunctions
contract.
You can run the default fuzzing tests using the following commands
Echidna
echidna . --contract CryticTester --config echidna.yaml
Medusa
medusa fuzz
Expanding The Example Project
Create-chimera-app is meant to be a starting point for your own project. You can expand the example project by adding your own contracts and properties. See the Chimera Framework section on what the role of each contract is and how to expand the example project.
This substack post walks you through the process of converting invariants from English to Solidity using a ERC4626 vault on an example project using Chimera.
To add new contracts just delete the existing Counter.sol
and Counter.t.sol
files and add your own.
After adding your new contracts, you can add handlers for the contracts you want to test using invariant testing by using the Build Handlers page on Recon (see this section for how to do this).
Chimera Framework
The Chimera framework lets you run invariant tests with Echidna and Medusa that can be easily debugged using Foundry.
The framework is made up of the following files:
Setup.sol
TargetFunctions.sol
Properties.sol
CryticToFoundry.sol
BeforeAfter.sol
CryticTester.sol
When you build your handlers using Recon these files get automatically generated and populated for you. To use the framework in your project, you just need to download these files that get generated for you and add the Chimera dependency to your project:
forge install Recon-Fuzz/chimera
The Contracts
We'll now look at the role each of the above contracts serve in building an extensible and maintainable fuzzing suite. We'll be looking at examples using the create-chimera-app template project.
Setup
This contract is used to deploy and initialize the state of your target contracts. It's called by the fuzzer before any of the target functions are called.
Any contracts you want to track in your fuzzing suite should live here.
In our create-chimera-app
template project, the Setup
contract is used to deploy the Counter
contract:
abstract contract Setup is BaseSetup {
Counter counter;
function setup() internal virtual override {
counter = new Counter();
}
}
TargetFunctions
This is perhaps the most important file in your fuzzing suite, it defines the target function handlers that will be called by the fuzzer to manipulate the state of your target contracts.
Note: These are the only functions that will be called by the fuzzer.
Because these functions have the aim of changing the state of the target contract, they usually only include non-view and non-pure functions.
Target functions make calls to the target contracts deployed in the Setup
contract. The handler that wraps the target function allows you to add clamping (reducing the possible fuzzed input values) before the call to the target contract and properties (assertions about the state of the target contract) after the call to the target contract.
In our create-chimera-app
template project, the TargetFunctions
contract is used to define the increment
and setNumber
functions:
abstract contract TargetFunctions is
BaseTargetFunctions,
Properties
{
function counter_increment() public {
counter.increment();
}
function counter_setNumber1(uint256 newNumber) public {
// clamping can be added here before the call to the target contract
// ex: newNumber = newNumber % 100;
// example assertion test replicating testFuzz_SetNumber
try counter.setNumber(newNumber) {
if (newNumber != 0) {
t(counter.number() == newNumber, "number != newNumber");
}
} catch {
t(false, "setNumber reverts");
}
}
function counter_setNumber2(uint256 newNumber) public {
// same example assertion test as counter_setNumber1 using ghost variables
__before();
counter.setNumber(newNumber);
__after();
if (newNumber != 0) {
t(_after.counter_number == newNumber, "number != newNumber");
}
}
}
Properties
This contract is used to define the properties that will be checked after the target functions are called.
At Recon our preference is to define these as Echidna/Medusa assertion properties but they can also be defined as boolean properties.
In our create-chimera-app
template project, the Properties
contract is used to define a property that states that the number can never be 0:
abstract contract Properties is BeforeAfter, Asserts {
// example property test
function invariant_number_never_zero() public {
t(counter.number() != 0, "number is 0");
}
}
CryticToFoundry
This contract is used to debug broken properties by converting the breaking call sequence from Echidna/Medusa into a Foundry unit test. When running jobs on Recon this is done automatically for all broken properties using the fuzzer logs.
If you are running the fuzzers locally you can use the Echidna and Medusa tools on Recon to convert the breaking call sequence from the logs into a Foundry unit test.
This contract is also useful for debugging issues related to the setup
function and allows testing individual handlers in isolation to verify if they're working as expected for specific inputs.
In our create-chimera-app
template project, the CryticToFoundry
contract doesn't include any reproducer tests because all the properties pass.
The test_crytic
function demonstrates the template for adding a reproducer test:
contract CryticToFoundry is Test, TargetFunctions, FoundryAsserts {
function setUp() public {
setup();
targetContract(address(counter));
}
function test_crytic() public {
// TODO: add failing property tests here for debugging
}
}
BeforeAfter
This contract is used to store the state of the target contract before and after the target functions are called in a Vars
struct.
These variables can be used in property definitions to check if function calls have modified the state of the target contract in an unexpected way.
In our create-chimera-app
template project, the BeforeAfter
contract is used to track the counter_number
variable:
// ghost variables for tracking state variable values before and after function calls
abstract contract BeforeAfter is Setup {
struct Vars {
uint256 counter_number;
}
Vars internal _before;
Vars internal _after;
function __before() internal {
_before.counter_number = counter.number();
}
function __after() internal {
_after.counter_number = counter.number();
}
}
CryticTester
This is the entrypoint for the fuzzer into the suite. All target functions will be called on an instance of this contract since it inherits from the TargetFunctions
contract.
In our create-chimera-app
template project, the CryticTester
contract is used to call the counter_increment
and counter_setNumber1
functions:
// echidna . --contract CryticTester --config echidna.yaml
// medusa fuzz
contract CryticTester is TargetFunctions, CryticAsserts {
constructor() payable {
setup();
}
}
Assertions
When using assertions from Chimera in your properties, they use a different interface than the standard assertions from foundry's forge-std
.
The following assertions are available in Chimera's Asserts
contract:
abstract contract Asserts {
// greater than
function gt(uint256 a, uint256 b, string memory reason) internal virtual;
// greater than or equal to
function gte(uint256 a, uint256 b, string memory reason) internal virtual;
// less than
function lt(uint256 a, uint256 b, string memory reason) internal virtual;
// less than or equal to
function lte(uint256 a, uint256 b, string memory reason) internal virtual;
// equal to
function eq(uint256 a, uint256 b, string memory reason) internal virtual;
// true
function t(bool b, string memory reason) internal virtual;
// between uint256
function between(uint256 value, uint256 low, uint256 high) internal virtual returns (uint256);
// between int256
function between(int256 value, int256 low, int256 high) internal virtual returns (int256);
// precondition
function precondition(bool p) internal virtual;
}
Setup Helpers
These contracts were created with the intention of speeding up the setup process for an invariant testing suite.
For an example implementation of these contracts in use see the create-chimera-app repo.
ActorManager
The ActorManager
contract serves as the source of truth for actors that are being used in the fuzzing suite setup.
The primary functions of interest when setting up a suite are:
_addActor
- allows adding a new address as an actor that can be tracked_switchActor
- this should be exposed in a target function that can be called by the fuzzer to randomly switch between actors
To use the actors stored in the ActorManager, add the asActor
modifier on all of your target function handlers which pranks as the currently set actor.
For privileged functions you can use the asAdmin
modifier which calls the target functions as the tester contract (address(this)
). The tester contract is typically set as the default admin address by convention.
AssetManager
The AssetManager
allows tracking all assets being used in the fuzz suite setup.
Similar to the ActorManager
this serves as the source of truth for all assets used in the test suite and therefore no target function should be called that may transfer an asset not tracked in the AssetManager
.
The primary functions of interest when setting up a suite are:
_newAsset
- deploys an instance of theMockERC20
contract and adds it to tracking so it can be accessed as needed_getAsset
- used to clamp values used for tokens in calls to target functions_finalizeAssetDeployment
- a utility for minting tokens added via_newAsset
to all actors that have been setup and approving it to all contracts in the the system that may need to calltransferFrom
on it
Utils
Provides utilities for invariant testing
checkError
- allows checking if a revert reason from a function call is equivalent to the reason passed in as theexpected
argument
Panic
A library that provides named variables corresponding to compiler panic messages. Used to more easily access these messages when using the checkError
utility.
library Panic {
// compiler panics
string constant assertionPanic = "Panic(1)";
string constant arithmeticPanic = "Panic(17)";
string constant divisionPanic = "Panic(18)";
string constant enumPanic = "Panic(33)";
string constant arrayPanic = "Panic(34)";
string constant emptyArrayPanic = "Panic(49)";
string constant outOfBoundsPanic = "Panic(50)";
string constant memoryPanic = "Panic(65)";
string constant functionPanic = "Panic(81)";
}
MockERC20
A minimal MockERC20
contract that lets you mock any standard ERC20 tokens that will be interacting with the system without requiring external dependencies.