Foundry's EVM support is mainly dedicated to testing and exploration, it features a set of cheatcodes which can manipulate the environment in which the execution is run.
Most of the time, simply testing your smart contracts outputs isn't enough. To manipulate the state of the EVM, as well as test for specific reverts and events, Foundry is shipped with a set of cheatcodes.
To understand how cheatcodes are implemented, we first need to look at revm::Inspector
,
a trait that provides a set of callbacks to be notified at certain stages of EVM execution.
For example, Inspector::call
is called when the EVM is about to execute a call:
fn call(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &mut CallInputs,
is_static: bool,
) -> (InstructionResult, Gas, Bytes) { ... }
The evm
crate has a variety of inspectors for different use cases, such as
- coverage
- tracing
- debugger
- logging
The concept of cheatcodes and cheatcode inspector is very simple.
Cheatcodes are calls to a specific address, the cheatcode handler address, defined as
address(uint160(uint256(keccak256("hevm cheat code"))))
(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D
).
In Solidity, this can be initialized as Vm constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
,
but generally this is inherited from forge-std/Test.sol
.
Since cheatcodes are bound to a constant address, the cheatcode inspector listens for that address:
impl Inspector for Cheatcodes {
fn call(
&mut self,
data: &mut EVMData<'_, DB>,
call: &mut CallInputs,
is_static: bool,
) -> (Return, Gas, Bytes) {
if call.contract == CHEATCODE_ADDRESS {
// intercepted cheatcode call
// --snip--
}
}
}
When a call to a cheatcode is intercepted we try to decode the calldata into a known cheatcode.
Rust bindings for the cheatcode interface are generated via the Alloy sol!
macro.
If a call was successfully decoded into the VmCalls
enum that the sol!
macro generates, the
last step is a large match
over the decoded function call structs, which serves as the
implementation handler for the cheatcode. This is also automatically generated, in part, by the
sol!
macro, through the use of a custom internal derive procedural macro.
All the cheatcodes are defined in a large sol!
macro call in cheatcodes/spec/src/vm.rs
:
sol! {
#[derive(Cheatcode)]
interface Vm {
// ======== Types ========
/// Error thrown by a cheatcode.
error CheatcodeError(string message);
// ...
// ======== EVM ========
/// Gets the address for a given private key.
#[cheatcode(group = Evm, safety = Safe)]
function addr(uint256 privateKey) external pure returns (address keyAddr);
/// Gets the nonce of an account.
#[cheatcode(group = Evm, safety = Safe)]
function getNonce(address account) external view returns (uint64 nonce);
// ...
}
}
This, combined with the use of an internal Cheatcode
derive macro,
allows us to generate both the Rust definitions and the JSON specification of the cheatcodes.
Cheatcodes are manually implemented through the Cheatcode
trait, which is
called in the Cheatcodes
inspector implementation.
Generates the raw Rust bindings for the cheatcodes, as well as lets us specify custom attributes individually for each item, such as functions and structs, or for entire interfaces.
The way bindings are generated and extra information can be found in the sol!
documentation.
We leverage this macro to apply the Cheatcode
derive macro on the Vm
interface.
Cheatcode
derive macro
This is derived once on the Vm
interface declaration, which recursively applies it to all of the
interface's items, as well as the sol!
-generated items, such as the VmCalls
enum.
This macro performs extra checks on functions and structs at compile time to make sure they are
documented and have named parameters, and generates a macro which is later used to implement the
match { ... }
function that is to be used to dispatch the cheatcode implementations after a call is
decoded.
The latter is what fails compilation when adding a new cheatcode, and is fixed by implementing the
Cheatcode
trait to the newly-generated function call struct(s).
The Cheatcode
derive macro also parses the #[cheatcode(...)]
attributes on functions, which are
used to specify additional properties of the JSON interface.
These are all the attributes that can be specified on cheatcode functions:
#[cheatcode(group = <ident>)]
: The group that the cheatcode belongs to. Required.#[cheatcode(status = <ident>)]
: The current status of the cheatcode. E.g. whether it is stable or experimental, etc. Defaults toStable
.#[cheatcode(safety = <ident>)]
: Whether the cheatcode is safe to use inside of scripts. E.g. it does not change state in an unexpected way. Defaults to the group's safety if unspecified. If the group is ambiguous, then it must be specified manually.
Multiple attributes can be specified by separating them with commas, e.g. #[cheatcode(group = "evm", status = "unstable")]
.
This trait defines the interface that all cheatcode implementations must implement. There are two methods that can be implemented:
apply
: implemented when the cheatcode is pure and does not need to access EVM dataapply_full
: implemented when the cheatcode needs to access EVM data
Only one of these methods can be implemented.
This trait is implemented manually for each cheatcode in the foundry-cheatcodes
crate on the sol!
-generated function call structs.
The JSON interface and schema
are automatically generated from the sol!
macro call by running cargo cheats
.
The initial execution of this command, following the addition of a new cheat code, will result in an update to the JSON files, which is expected to fail. This failure is necessary for the CI system to detect that changes have occurred. Subsequent executions should pass, confirming the successful update of the files.
- Add its Solidity definition(s) in
cheatcodes/spec/src/vm.rs
. Ensure that all structs and functions are documented, and that all function parameters are named. This will initially fail to compile because of the automatically generatedmatch { ... }
expression. This is expected, and will be fixed in the next step - Implement the cheatcode in
cheatcodes
in its category's respective module. Follow the existing implementations as a guide. - If a struct, enum, error, or event was added to
Vm
, updatespec::Cheatcodes::new
- Update the JSON interface by running
cargo cheats
twice. This is expected to fail the first time that this is run after adding a new cheatcode; see JSON interface - Write an integration test for the cheatcode in
testdata/cheats/