Using Cheatcodes
ℹ️ Info To use cheatcodes you need to add
snforge_std
package as a dependency in yourScarb.toml
using appropriate release tag.[dev-dependencies] snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.9.0" }
When testing smart contracts, often there are parts of code that are dependent on a specific blockchain state. Instead of trying to replicate these conditions in tests, you can emulate them using cheatcodes.
The Test Contract
In this tutorial, we will be using the following Starknet contract:
#[starknet::interface]
trait IHelloStarknet<TContractState> {
fn increase_balance(ref self: TContractState, amount: felt252);
fn get_balance(self: @TContractState) -> felt252;
fn get_block_number_at_construction(self: @TContractState) -> u64;
fn get_block_timestamp_at_construction(self: @TContractState) -> u64;
}
#[starknet::contract]
mod HelloStarknet {
use box::BoxTrait;
use starknet::{Into, get_caller_address};
#[storage]
struct Storage {
balance: felt252,
blk_nb: u64,
blk_timestamp: u64,
}
#[constructor]
fn constructor(ref self: ContractState) {
// store the current block number
self.blk_nb.write(starknet::get_block_info().unbox().block_number);
// store the current block timestamp
self.blk_timestamp.write(starknet::get_block_info().unbox().block_timestamp);
}
#[abi(embed_v0)]
impl IHelloStarknetImpl of super::IHelloStarknet<ContractState> {
// Increases the balance by the given amount.
fn increase_balance(ref self: ContractState, amount: felt252) {
assert_is_allowed_user();
self.balance.write(self.balance.read() + amount);
}
// Gets the balance.
fn get_balance(self: @ContractState) -> felt252 {
self.balance.read()
}
// Gets the block number
fn get_block_number_at_construction(self: @ContractState) -> u64 {
self.blk_nb.read()
}
// Gets the block timestamp
fn get_block_timestamp_at_construction(self: @ContractState) -> u64 {
self.blk_timestamp.read()
}
}
fn assert_is_allowed_user() {
// checks if caller is '123'
let address = get_caller_address();
assert(address.into() == 123, 'user is not allowed');
}
}
Please note that this contract example is a continuation of the same contract as in the Testing Smart Contracts page.
Writing Tests
We can try to create a test that will increase and verify the balance.
#[test]
fn call_and_invoke() {
// ...
let balance = dispatcher.get_balance();
assert(balance == 0, 'balance == 0');
dispatcher.increase_balance(100);
let balance = dispatcher.get_balance();
assert(balance == 100, 'balance == 100');
}
However, when running this test, we will get a failure with a message
$ snforge test
Collected 1 test(s) from package_name package
Running 0 test(s) from src/
Running 1 test(s) from tests/
[FAIL] tests::call_and_invoke
Failure data:
0x75736572206973206e6f7420616c6c6f776564 ('user is not allowed')
Tests: 0 passed, 1 failed, 0 skipped, 0 ignored, 0 filtered out
Failures:
tests::call_and_invoke
Our user validation is not letting us call the contract, because the default caller address is not 123
.
Using Cheatcodes in Tests
By using cheatcodes, we can change various properties of transaction info, block info, etc.
For example, we can use the start_prank
cheatcode to change the caller
address, so it passes our validation.
Pranking the Address
use snforge_std::{ declare, ContractClassTrait, start_prank, CheatTarget };
#[test]
fn call_and_invoke() {
let contract = declare("HelloStarknet").unwrap();
let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap();
let dispatcher = IHelloStarknetDispatcher { contract_address };
let balance = dispatcher.get_balance();
assert(balance == 0, 'balance == 0');
// Change the caller address to 123 when calling the contract at the `contract_address` address
start_prank(CheatTarget::One(contract_address), 123.try_into().unwrap());
dispatcher.increase_balance(100);
let balance = dispatcher.get_balance();
assert(balance == 100, 'balance == 100');
}
The test will now pass without an error
$ snforge test
Collected 1 test(s) from package_name package
Running 0 test(s) from src/
Running 1 test(s) from tests/
[PASS] tests::call_and_invoke
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
Canceling the Prank
Most cheatcodes come with corresponding start_
and stop_
functions that can be used to start and stop the state
change.
In case of the start_prank
, we can cancel the address change
using stop_prank
use snforge_std::{stop_prank, CheatTarget};
#[test]
fn call_and_invoke() {
// ...
// The address when calling contract at the `contract_address` address will no longer be changed
stop_prank(CheatTarget::One(contract_address));
// This will fail
dispatcher.increase_balance(100);
let balance = dispatcher.get_balance();
assert(balance == 100, 'balance == 100');
}
$ snforge test
Collected 1 test(s) from package_name package
Running 0 test(s) from src/
Running 1 test(s) from tests/
[FAIL] tests::call_and_invoke, 0 ignored, 0 filtered out
Failure data:
0x75736572206973206e6f7420616c6c6f776564 ('user is not allowed')
Tests: 0 passed, 1 failed, 0 skipped, 0 ignored, 0 filtered out
Failures:
tests::call_and_invoke
Pranking the Constructor
Most of the cheatcodes like prank
, mock_call
, warp
, roll
, elect
do work in the constructor of the contracts.
Let's say, that you have a contract that saves the caller address (deployer) in the constructor, and you want it to be pre-set to a certain value.
To prank
the constructor, you need to start_prank
before it is invoked, with the right address. To achieve this, you need to precalculate the address of the contract by using the precalculate_address
function of ContractClassTrait
on the declared contract, and then use it in start_prank
as an argument:
use snforge_std::{ declare, ContractClassTrait, start_prank, CheatTarget };
#[test]
fn mock_constructor_with_prank() {
let contract = declare("HelloStarknet").unwrap();
let constructor_arguments = @ArrayTrait::new();
// Precalculate the address to obtain the contract address before the constructor call (deploy) itself
let contract_address = contract.precalculate_address(constructor_arguments);
// Change the caller address to 123 before the call to contract.deploy
start_prank(CheatTarget::One(contract_address), 123.try_into().unwrap());
// The constructor will have 123 set as the caller address
contract.deploy(constructor_arguments).unwrap();
}
Setting Cheatcode Span
Sometimes it's useful to have a cheatcode work only for a certain number of target calls.
That's where CheatSpan
comes in handy.
enum CheatSpan {
Indefinite: (),
TargetCalls: usize,
}
To set span for a cheatcode, use prank
/ warp
/ roll
/ etc.
prank(CheatTarget::One(contract_address), new_caller_address, CheatSpan::TargetCalls(1))
Calling a cheatcode with CheatSpan::TargetCalls(N)
is going to activate the cheatcode for N
calls to a specified CheatTarget
, after which it's going to be automatically canceled.
Of course the cheatcode can still be canceled before its CheatSpan
goes down to 0 - simply call stop_prank
on the target manually.
ℹ️ Info
Using
start_prank
is equivalent to usingprank
withCheatSpan::Indefinite
.
To better understand the functionality of CheatSpan
, here's a full example:
use snforge_std::{
declare, ContractClass, ContractClassTrait, prank, CheatSpan, CheatTarget
};
#[test]
#[feature("safe_dispatcher")]
fn call_and_invoke() {
let contract = declare("HelloStarknet").unwrap();
let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap();
let safe_dispatcher = IHelloStarknetSafeDispatcher { contract_address };
let balance = safe_dispatcher.get_balance().unwrap();
assert_eq!(balance, 0);
// Function `increase_balance` from HelloStarknet contract
// requires the caller_address to be 123
let pranked_address: ContractAddress = 123.try_into().unwrap();
// Prank the contract_address for a span of 2 target calls (here, calls to contract_address)
prank(CheatTarget::One(contract_address), pranked_address, CheatSpan::TargetCalls(2));
// Call #1 should succeed
let call_1_result = safe_dispatcher.increase_balance(100);
assert!(call_1_result.is_ok());
// Call #2 should succeed
let call_2_result = safe_dispatcher.increase_balance(100);
assert!(call_2_result.is_ok());
// Call #3 should fail, as the prank cheatcode has been canceled
let call_3_result = safe_dispatcher.increase_balance(100);
assert!(call_3_result.is_err());
let balance = safe_dispatcher.get_balance().unwrap();
assert_eq!(balance, 200);
}