Using Cheatcodes
ℹ️ Info To use cheatcodes you need to add
snforge_std
package as a dependency in yourScarb.toml
using the appropriate version.[dev-dependencies] snforge_std = "0.33.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.
⚠️ Warning
These examples make use of
assert_macros
, so it's recommended to get familiar with them first. Learn more aboutassert_macros
The Test Contract
In this tutorial, we will be using the following Starknet contract:
#[starknet::interface]
pub trait ICheatcodeChecker<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]
pub mod CheatcodeChecker {
use core::box::BoxTrait;
use starknet::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 ICheatcodeCheckerImpl of super::ICheatcodeChecker<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');
}
}
Writing Tests
We can try to create a test that will increase and verify the balance.
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait};
use using_cheatcodes::{ICheatcodeCheckerDispatcher, ICheatcodeCheckerDispatcherTrait};
#[test]
fn call_and_invoke() {
let contract = declare("CheatcodeChecker").unwrap().contract_class();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
let dispatcher = ICheatcodeCheckerDispatcher { contract_address };
let balance = dispatcher.get_balance();
assert(balance == 0, 'balance == 0');
dispatcher.increase_balance(100);
let balance = dispatcher.get_balance();
assert(balance == 100, 'balance == 100');
}
This test fails, which means that increase_balance
method panics as we expected.
$ snforge test
Output:
Collected 1 test(s) from using_cheatcodes package
Running 0 test(s) from src/
Running 1 test(s) from tests/
[FAIL] using_cheatcodes_tests::call_and_invoke
Failure data:
0x75736572206973206e6f7420616c6c6f776564 ('user is not allowed')
Tests: 0 passed, 1 failed, 0 skipped, 0 ignored, 0 filtered out
Failures:
using_cheatcodes_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_cheat_caller_address
cheatcode to change the caller
address, so it passes our validation.
Cheating an Address
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address};
use using_cheatcodes_cheat_address::{ICheatcodeCheckerDispatcher, ICheatcodeCheckerDispatcherTrait};
#[test]
fn call_and_invoke() {
let contract = declare("CheatcodeChecker").unwrap().contract_class();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
let dispatcher = ICheatcodeCheckerDispatcher { 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_cheat_caller_address(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
Output:
Collected 1 test(s) from using_cheatcodes_cheat_address package
Running 0 test(s) from src/
Running 1 test(s) from tests/
[PASS] using_cheatcodes_cheat_address_tests::call_and_invoke (gas: ~239)
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
Cancelling the Cheat
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_cheat_caller_address
, we can cancel the address change
using stop_cheat_caller_address
.
We will demonstrate its behavior using SafeDispatcher
to show when exactly the fail occurs:
use snforge_std::{
declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address,
stop_cheat_caller_address
};
use using_cheatcodes_cancelling_cheat::{
ICheatcodeCheckerSafeDispatcher, ICheatcodeCheckerSafeDispatcherTrait
};
#[test]
#[feature("safe_dispatcher")]
fn call_and_invoke() {
let contract = declare("CheatcodeChecker").unwrap().contract_class();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
let dispatcher = ICheatcodeCheckerSafeDispatcher { contract_address };
let balance = dispatcher.get_balance().unwrap();
assert(balance == 0, 'balance == 0');
// Change the caller address to 123 when calling the contract at the `contract_address` address
start_cheat_caller_address(contract_address, 123.try_into().unwrap());
// Call to method with caller restriction succeeds
dispatcher.increase_balance(100).expect('First call failed!');
let balance = dispatcher.get_balance();
assert_eq!(balance, Result::Ok(100));
// Cancel the cheat
stop_cheat_caller_address(contract_address);
// The call fails now
dispatcher.increase_balance(100).expect('Second call failed!');
let balance = dispatcher.get_balance();
assert_eq!(balance, Result::Ok(100));
}
$ snforge test
Output:
Collected 1 test(s) from using_cheatcodes_cancelling_cheat package
Running 1 test(s) from tests/
[FAIL] using_cheatcodes_cancelling_cheat_tests::call_and_invoke
Failure data:
0x5365636f6e642063616c6c206661696c656421 ('Second call failed!')
Running 0 test(s) from src/
Tests: 0 passed, 1 failed, 0 skipped, 0 ignored, 0 filtered out
Failures:
using_cheatcodes_cancelling_cheat_tests::call_and_invoke
We see that the second increase_balance
fails since we cancelled the cheatcode.
Cheating Addresses Globally
In case you want to cheat the caller address for all contracts, you can use the global cheatcode which has the _global
suffix. Note, that we don't specify target, nor the span, because this cheatcode type works globally and indefinitely.
For more see Cheating Globally.
use snforge_std::{
declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address_global,
stop_cheat_caller_address_global
};
use using_cheatcodes_others::{ICheatcodeCheckerDispatcher, ICheatcodeCheckerDispatcherTrait};
#[test]
fn call_and_invoke_global() {
let contract = declare("CheatcodeChecker").unwrap().contract_class();
let (contract_address_a, _) = contract.deploy(@array![]).unwrap();
let (contract_address_b, _) = contract.deploy(@array![]).unwrap();
let dispatcher_a = ICheatcodeCheckerDispatcher { contract_address: contract_address_a };
let dispatcher_b = ICheatcodeCheckerDispatcher { contract_address: contract_address_b };
let balance_a = dispatcher_a.get_balance();
let balance_b = dispatcher_b.get_balance();
assert_eq!(balance_a, 0);
assert_eq!(balance_b, 0);
// Change the caller address to 123, both targets a and b will be affected
// global cheatcodes work indefinitely until stopped
start_cheat_caller_address_global(123.try_into().unwrap());
dispatcher_a.increase_balance(100);
dispatcher_b.increase_balance(100);
let balance_a = dispatcher_a.get_balance();
let balance_b = dispatcher_b.get_balance();
assert_eq!(balance_a, 100);
assert_eq!(balance_b, 100);
// Cancel the cheat
stop_cheat_caller_address_global();
}
Cheating the Constructor
Most of the cheatcodes like cheat_caller_address
, mock_call
, cheat_block_timestamp
, cheat_block_number
, 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 cheat_caller_address
the constructor, you need to start_cheat_caller_address
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_cheat_caller_address
as an argument:
use snforge_std::{
declare, ContractClassTrait, DeclareResultTrait, start_cheat_block_number,
start_cheat_block_timestamp
};
use using_cheatcodes_others::{ICheatcodeCheckerDispatcher, ICheatcodeCheckerDispatcherTrait};
#[test]
fn call_and_invoke() {
let contract = declare("CheatcodeChecker").unwrap().contract_class();
// Precalculate the address to obtain the contract address before the constructor call (deploy)
// itself
let contract_address = contract.precalculate_address(@array![]);
// Change the block number and timestamp before the call to contract.deploy
start_cheat_block_number(contract_address, 0x420_u64);
start_cheat_block_timestamp(contract_address, 0x2137_u64);
// Deploy as normally
contract.deploy(@array![]).unwrap();
// Construct a dispatcher with the precalculated address
let dispatcher = ICheatcodeCheckerDispatcher { contract_address };
let block_number = dispatcher.get_block_number_at_construction();
let block_timestamp = dispatcher.get_block_timestamp_at_construction();
assert_eq!(block_number, 0x420_u64);
assert_eq!(block_timestamp, 0x2137_u64);
}
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 cheat_caller_address
/ cheat_block_timestamp
/ cheat_block_number
/ etc.
cheat_caller_address(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 contract address, 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_cheat_caller_address
on the target manually.
ℹ️ Info
Using
start_cheat_caller_address
is equivalent to usingcheat_caller_address
withCheatSpan::Indefinite
.
To better understand the functionality of CheatSpan
, here's a full example:
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, cheat_caller_address, CheatSpan};
use starknet::ContractAddress;
use using_cheatcodes_others::{
ICheatcodeCheckerSafeDispatcher, ICheatcodeCheckerSafeDispatcherTrait
};
#[test]
#[feature("safe_dispatcher")]
fn call_and_invoke() {
let contract = declare("CheatcodeChecker").unwrap().contract_class();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
let safe_dispatcher = ICheatcodeCheckerSafeDispatcher { 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 spoofed_caller: ContractAddress = 123.try_into().unwrap();
// Change the caller address for the contract_address for a span of 2 target calls (here, calls
// to contract_address)
cheat_caller_address(contract_address, spoofed_caller, 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 cheat_caller_address cheatcode has been canceled
let call_3_result = safe_dispatcher.increase_balance(100);
assert_eq!(call_3_result, Result::Err(array!['user is not allowed']));
let balance = safe_dispatcher.get_balance().unwrap();
assert_eq!(balance, 200);
}