Fork Testing
snforge
supports testing in a forked environment.
Forking allows using state and contracts from a real instance Starknet network, including Mainnet and Sepolia.
Each test can fork the state of a specified real
network and perform actions on top of it.
📝 Note
Actions are performed on top of the
forked
state which means real network is not affected.
Test Contract
We will demonstrate fork testing on an example of the Pokemons
contract deployed on Sepolia network.
We are going to use a free, open RPC endpoint - Blast.
We first need to define the contract's interface along with all the structures used by its externals:
#[derive(Clone, Debug, PartialEq, Drop, Serde, starknet::Store)]
pub struct Pokemon {
pub name: ByteArray,
pub element: Element,
pub likes: felt252,
pub owner: starknet::ContractAddress,
}
#[derive(Copy, Debug, PartialEq, Drop, Serde, starknet::Store)]
pub enum Element {
Fire,
Water,
Grass,
}
#[starknet::interface]
pub trait IPokemonGallery<TContractState> {
fn like(ref self: TContractState, name: ByteArray);
fn all(self: @TContractState) -> Array<Pokemon>;
fn pokemon(self: @TContractState, name: ByteArray) -> Option<Pokemon>;
fn liked(self: @TContractState) -> Array<Pokemon>;
}
Fork Configuration
There are two ways of configuring a fork:
- by specifying
url
and block-related parameters in the#[fork(...)]
attribute - or by providing a fork name defined in your
Scarb.toml
to the#[fork(...)]
attribute
📝 Note Using fork tests means
snforge
will make (often multiple) requests to the configured RPC URL. These requests are relatively slow as they happen over the network.
snforge
can cache these requests automatically in.snfoundry_cache
but only ifblock_hash
orblock_number
is provided. Usingblock_tag
, especially"latest"
disables the caching functionality.
Configure a Fork in the Attribute
It is possible to pass url
and only one of block_number
, block_hash
, block_tag
arguments to the fork
attribute:
url
— RPC URLblock_number
— number of a block which fork will be pinned toblock_hash
— hash of block which fork will be pinned toblock_tag
— tag of block which fork will be pinned to. Currently onlylatest
is supported
📝 Note
block_hash
andblock_number
can be provided as a decimal or hex number.
Once such a configuration is passed, it is possible to use state and contracts defined on the specified network.
We are going to test a basic scenario:
- Obtain a dispatcher generated by the
#[starknet::interface]
- Instantiate it with a real contract address present in the forked state
- Call a method modifying the contract's state
- Make some assertion about the changed state
Example uses of all methods:
block_number
// import dispatcher generated by the interface we wrote
use fork_testing::{IPokemonGalleryDispatcher, IPokemonGalleryDispatcherTrait};
// take an address of a real network contract
const CONTRACT_ADDRESS: felt252 =
0x0522dc7cbe288037382a02569af5a4169531053d284193623948eac8dd051716;
#[test]
#[fork(url: "https://starknet-sepolia.public.blastapi.io/rpc/v0_7", block_number: 77864)]
fn test_using_forked_state() {
// instantiate the dispatcher
let dispatcher = IPokemonGalleryDispatcher {
contract_address: CONTRACT_ADDRESS.try_into().unwrap(),
};
// call the mutating method
dispatcher.like("Charizard");
// check if the contract's state has changed
let pokemon = dispatcher.pokemon("Charizard");
assert!(pokemon.is_some());
assert_eq!(pokemon.unwrap().likes, 1);
}
block_hash
use fork_testing::{IPokemonGalleryDispatcher, IPokemonGalleryDispatcherTrait};
const CONTRACT_ADDRESS: felt252 =
0x0522dc7cbe288037382a02569af5a4169531053d284193623948eac8dd051716;
#[test]
#[fork(
url: "https://starknet-sepolia.public.blastapi.io/rpc/v0_7",
block_hash: 0x0690f8d584b52c2798d76b3346217a516778abee9b1bd8e400beb4f05dd9a4e7,
)]
fn test_using_forked_state() {
let dispatcher = IPokemonGalleryDispatcher {
contract_address: CONTRACT_ADDRESS.try_into().unwrap(),
};
dispatcher.like("Charizard");
let pokemon = dispatcher.pokemon("Charizard");
assert!(pokemon.is_some());
assert_eq!(pokemon.unwrap().likes, 1);
}
block_tag
use fork_testing::{IPokemonGalleryDispatcher, IPokemonGalleryDispatcherTrait};
const CONTRACT_ADDRESS: felt252 =
0x0522dc7cbe288037382a02569af5a4169531053d284193623948eac8dd051716;
#[test]
#[fork(url: "https://starknet-sepolia.public.blastapi.io/rpc/v0_7", block_tag: latest)]
fn test_using_forked_state() {
let dispatcher = IPokemonGalleryDispatcher {
contract_address: CONTRACT_ADDRESS.try_into().unwrap(),
};
dispatcher.like("Charizard");
let pokemon = dispatcher.pokemon("Charizard");
assert!(pokemon.is_some());
assert_eq!(pokemon.unwrap().likes, 1);
}
Configure Fork in Scarb.toml
Although passing named arguments works fine, you have to copy-paste it each time you want to use the same fork in tests.
snforge
solves this issue by allowing fork configuration inside the Scarb.toml
file.
[[tool.snforge.fork]]
name = "SEPOLIA_LATEST"
url = "https://starknet-sepolia.public.blastapi.io/rpc/v0_7"
block_id.tag = "latest"
From this moment forks can be set using their name in the fork
attribute.
use fork_testing::{IPokemonGalleryDispatcher, IPokemonGalleryDispatcherTrait};
const CONTRACT_ADDRESS: felt252 =
0x0522dc7cbe288037382a02569af5a4169531053d284193623948eac8dd051716;
#[test]
#[fork("SEPOLIA_LATEST")]
fn test_using_forked_state() {
let dispatcher = IPokemonGalleryDispatcher {
contract_address: CONTRACT_ADDRESS.try_into().unwrap(),
};
dispatcher.like("Charizard");
let pokemon = dispatcher.pokemon("Charizard");
assert!(pokemon.is_some());
assert_eq!(pokemon.unwrap().likes, 1);
}
In some cases you may want to override block_id
defined in the Scarb.toml
file.
You can do it by passing block_number
, block_hash
, block_tag
arguments to the fork
attribute.
use fork_testing::{IPokemonGalleryDispatcher, IPokemonGalleryDispatcherTrait};
const CONTRACT_ADDRESS: felt252 =
0x0522dc7cbe288037382a02569af5a4169531053d284193623948eac8dd051716;
#[test]
#[fork("SEPOLIA_LATEST", block_number: 200000)]
fn test_using_forked_state() {
let dispatcher = IPokemonGalleryDispatcher {
contract_address: CONTRACT_ADDRESS.try_into().unwrap(),
};
dispatcher.like("Charizard");
let pokemon = dispatcher.pokemon("Charizard");
assert!(pokemon.is_some());
assert_eq!(pokemon.unwrap().likes, 1);
}
Testing Forked Contracts
Once the fork is configured, the test will run on top of the forked state, meaning that it will have access to every contract deployed on the real network.
With that, you can now interact with any contract from the chain the same way you would in a standard test.
⚠️ Warning
Some cheats aren't supported and won't work for forked contracts written in Cairo 0. Only those cheats are going to have an effect:
caller_address
block_number
block_timestamp
sequencer_address
spy_events
spy_messages_to_l1