![logo](images/logo.png)
The Starknet Foundry
Starknet Foundry is a toolchain for developing Starknet smart contracts. It helps with writing, deploying, and testing your smart contracts. It is inspired by Foundry.
Installation
Starknet Foundry is easy to install on Linux, Mac and Windows systems. In this section, we will walk through the process of installing Starknet Foundry.
Requirements
To use Starknet Foundry, you need:
both installed and added to your PATH
environment variable.
📝 Note
Universal-Sierra-Compiler
will be automatically installed if you usesnfoundryup
orasdf
. You can also createUNIVERSAL_SIERRA_COMPILER
env var to make it visible forsnforge
.
Install via snfoundryup
Snfoundryup is the Starknet Foundry toolchain installer.
You can install it by running:
curl -L https://raw.githubusercontent.com/foundry-rs/starknet-foundry/master/scripts/install.sh | sh
Follow the instructions and then run:
snfoundryup
See snfoundryup --help
for more options.
To verify that the Starknet Foundry is installed correctly, run snforge --version
and sncast --version
.
Installation via asdf
First, add the Starknet Foundry plugin to asdf:
asdf plugin add starknet-foundry
Install the latest version:
asdf install starknet-foundry latest
See asdf guide for more details.
Installation on Windows
As for now, Starknet Foundry on Windows needs manual installation, but necessary steps are kept to minimum:
- Download the release archive matching your CPU architecture.
- Extract it to a location where you would like to have Starknet Foundry installed. A folder named snfoundry in
your
%LOCALAPPDATA%\Programs
directory will suffice:
%LOCALAPPDATA%\Programs\snfoundry
- Add path to the snfoundry\bin directory to your PATH environment variable.
- Verify installation by running the following command in new terminal session:
snforge --version
sncast --version
Universal-Sierra-Compiler update
If you would like to bump the USC manually (e.g. when the new Sierra version is released) you can do it by running:
curl -L https://raw.githubusercontent.com/software-mansion/universal-sierra-compiler/master/scripts/install.sh | sh
How to build Starknet Foundry from source code
If you are unable to install Starknet Foundry using the instructions above, you can try building it from the source code as follows:
- Set up a development environment.
- Run
cd starknet-foundry && cargo build --release
. This will create atarget
directory. - Move the
target
directory to the desired location (e.g.~/.starknet-foundry
). - Add
DESIRED_LOCATION/target/release/
to yourPATH
.
First Steps With Starknet Foundry
In this section we provide an overview of Starknet Foundry snforge
command line tool.
We demonstrate how to create a new project, compile, and test it.
To start a new project with Starknet Foundry, run snforge init
$ snforge init project_name
Let's check out the project structure
$ cd project_name
$ tree . -L 1
.
├── Scarb.lock
├── Scarb.toml
├── src
└── tests
2 directories, 2 files
src/
contains source code of all your contracts.tests/
contains tests.Scarb.toml
contains configuration of the project as well as ofsnforge
Scarb.lock
a locking mechanism to achieve reproducible dependencies when installing the project locally
And run tests with snforge test
$ snforge test
Compiling project_name v0.1.0 (project_name/Scarb.toml)
Finished release target(s) in 1 second
Collected 2 test(s) from project_name package
Running 0 test(s) from src/
Running 2 test(s) from tests/
[PASS] tests::test_contract::test_increase_balance (gas: ~170)
[PASS] tests::test_contract::test_cannot_increase_balance_with_zero_value (gas: ~104)
Tests: 2 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
Using snforge
With Existing Scarb Projects
To use snforge
with existing Scarb projects, make sure you have declared the snforge_std
package as your project
development dependency.
Add the following line under [dev-dependencies]
section in the Scarb.toml
file.
# ...
[dev-dependencies]
snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.27.0" }
Make sure that the version in tag
matches snforge
. You can check the currently installed version with
$ snforge --version
snforge 0.27.0
It is also possible to add this dependency
using scarb add
command.
$ scarb add snforge_std \
--dev \
--git https://github.com/foundry-rs/starknet-foundry.git \
--tag v0.27.0
Additionally, ensure that starknet-contract target is enabled in the Scarb.toml
file.
# ...
[[target.starknet-contract]]
Scarb
Scarb is the package manager and build toolchain for Starknet ecosystem. Those coming from Rust ecosystem will find Scarb very similar to Cargo.
Starknet Foundry uses Scarb to:
One of the core concepts of Scarb is its manifest file - Scarb.toml
.
It can be also used to provide configuration for Starknet Foundry Forge.
Moreover, you can modify behaviour of scarb test
to run snforge test
as
described here.
📝 Note
Scarb.toml
is specifically designed for configuring scarb packages and, by extension, is suitable forsnforge
configurations, which are package-specific. On the other hand,sncast
can operate independently of scarb workspaces/packages and therefore utilizes a different configuration file,snfoundry.toml
. This distinction ensures that configurations are appropriately aligned with their respective tools' operational contexts.
Last but not least, remember that in order to use Starknet Foundry, you must have Scarb
installed and added to the PATH
environment variable.
Project Configuration
snforge
Configuring snforge
Settings in Scarb.toml
It is possible to configure snforge
for all test runs through Scarb.toml
.
Instead of passing arguments in the command line, set them directly in the file.
# ...
[tool.snforge]
exit_first = true
# ...
snforge
automatically looks for Scarb.toml
in the directory you are running the tests in or in any of its parents.
sncast
Defining Profiles in snfoundry.toml
To be able to work with the network, you need to supply sncast
with a few parameters —
namely the rpc node url and an account name that should be used to interact with it.
This can be done
by either supplying sncast
with those parameters directly see more detailed CLI description,
or you can put them into snfoundry.toml
file:
# ...
[sncast.myprofile]
account = "user"
accounts-file = "~/my_accounts.json"
url = "http://127.0.0.1:5050/rpc"
# ...
With snfoundry.toml
configured this way, we can just pass --profile myprofile
argument to make sure sncast
uses parameters
defined in the profile.
📝 Note
snfoundry.toml
file has to be present in current or any of the parent directories.
📝 Note If there is a profile with the same name in Scarb.toml, scarb will use this profile. If not, scarb will default to using the dev profile. (This applies only to subcommands using scarb - namely
declare
andscript
).
💡 Info Not all parameters have to be present in the configuration - you can choose to include only some of them and supply the rest of them using CLI flags. You can also override parameters from the configuration using CLI flags.
$ sncast --profile myprofile \
call \
--contract-address 0x38b7b9507ccf73d79cb42c2cc4e58cf3af1248f342112879bfdf5aa4f606cc9 \
--function get \
--calldata 0x0 \
--block-id latest
command: call
response: [0x0]
Multiple Profiles
You can have multiple profiles defined in the snfoundry.toml
.
Default Profile
There is also an option to set up a default profile, which can be utilized without the need to specify a --profile
. Here's an example:
# ...
[sncast.default]
account = "user123"
accounts-file = "~/my_accounts.json"
url = "http://127.0.0.1:5050/rpc"
# ...
With this, there's no need to include the --profile
argument when using sncast
.
$ sncast call \
--contract-address 0x38b7b9507ccf73d79cb42c2cc4e58cf3af1248f342112879bfdf5aa4f606cc9 \
--function get \
--calldata 0x0 \
--block-id latest
command: call
response: [0x1, 0x23, 0x4]
Environmental variables
Programmers can use environmental variables in both Scarb.toml::tool::snforge
and in snfoundry.toml
. To use an environmental variable as a value, use its name prefixed with $
.
This might be useful, for example, to hide node urls in the public repositories.
As an example:
# ...
[sncast.default]
account = "my_account"
accounts-file = "~/my_accounts.json"
url = "$NODE_URL"
# ...
Variable value are automatically resolved to numbers and booleans (strings true
, false
) if it is possible.
Running Tests
To run tests with snforge
, simply run the snforge test
command from the package directory.
$ snforge test
Collected 3 test(s) from package_name package
Running 3 test(s) from src/
[PASS] package_name::tests::executing
[PASS] package_name::tests::calling
[PASS] package_name::tests::calling_another
Tests: 3 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
Filtering Tests
You can pass a filter string after the snforge test
command to filter tests.
By default, any test with an absolute module tree path
matching the filter will be run.
$ snforge test calling
Collected 2 test(s) from package_name package
Running 2 test(s) from src/
[PASS] package_name::tests::calling
[PASS] package_name::tests::calling_another
Tests: 2 passed, 0 failed, 0 skipped, 0 ignored, 1 filtered out
Running a Specific Test
To run a specific test, you can pass a filter string along with an --exact
flag.
Note, you have to use a fully qualified test name, including a module name.
$ snforge test package_name::tests::calling --exact
Collected 1 test(s) from package_name package
Running 1 test(s) from src/
[PASS] package_name::tests::calling
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 2 filtered out
Stopping Test Execution After First Failed Test
To stop the test execution after first failed test, you can pass an --exit-first
flag along with snforge test
command.
$ snforge test --exit-first
Collected 6 test(s) from package_name package
Running 6 test(s) from src/
[PASS] package_name::tests::executing
[PASS] package_name::tests::calling
[PASS] package_name::tests::calling_another
[FAIL] package_name::tests::failing
Failure data:
0x6661696c696e6720636865636b ('failing check')
Tests: 3 passed, 1 failed, 2 skipped, 0 ignored, 0 filtered out
Failures:
package_name::tests::failing
Displaying Resources Used During Tests
To track resources like builtins
/ syscalls
that are used when running tests, use snforge test --detailed-resources
.
$ snforge test --detailed-resources
Collected 1 test(s) from package_name package
Running 1 test(s) from src/
[PASS] package_name::tests::resources (gas: ~2213)
steps: 881
memory holes: 36
builtins: ("range_check_builtin": 32)
syscalls: (StorageWrite: 1, StorageRead: 1, CallContract: 1)
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
For more information about how starknet-foundry calculates those, see gas and resource estimation section.
Writing Tests
snforge
lets you test standalone functions from your smart contracts. This technique is referred to as unit testing. You
should write as many unit tests as possible as these are faster than integration tests.
Writing Your First Test
First, add the following code to the src/lib.cairo
file:
fn sum(a: felt252, b: felt252) -> felt252 {
return a + b;
}
#[cfg(test)]
mod tests {
use super::sum;
#[test]
fn test_sum() {
assert(sum(2, 3) == 5, 'sum incorrect');
}
}
It is a common practice to keep your unit tests in the same file as the tested code.
Keep in mind that all tests in src
folder have to be in a module annotated with #[cfg(test)]
.
When it comes to integration tests, you can keep them in separate files in the tests
directory.
You can find a detailed explanation of how snforge
collects tests here.
Now run snforge
using a command:
$ snforge test
Collected 1 test(s) from package_name package
Running 1 test(s) from src/
[PASS] package_name::tests::test_sum
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
Failing Tests
If your code panics, the test is considered failed. Here's an example of a failing test.
fn panicking_function() {
let mut data = array![];
data.append('aaa');
panic(data)
}
#[cfg(test)]
mod tests {
use super::panicking_function;
#[test]
fn failing() {
panicking_function();
assert(2 == 2, '2 == 2');
}
}
$ snforge test
Collected 1 test(s) from package_name package
Running 1 test(s) from src/
[FAIL] package_name::tests::failing
Failure data:
0x616161 ('aaa')
Tests: 0 passed, 1 failed, 0 skipped, 0 ignored, 0 filtered out
Failures:
package_name::tests::failing
Expected Failures
Sometimes you want to mark a test as expected to fail. This is useful when you want to verify that an action fails as expected.
To mark a test as expected to fail, use the #[should_panic]
attribute. You can pass the expected failure message as an
argument to the attribute to verify that the test fails with the expected message
with #[should_panic(expected: ('panic message', 'eventual second message',))]
.
#[cfg(test)]
mod tests {
use core::panic_with_felt252;
#[should_panic(expected: ('panic message', ))]
#[test]
fn should_panic_check_data() {
panic_with_felt252('panic message');
}
}
$ snforge test
Collected 1 test(s) from package_name package
Running 1 test(s) from src/
Running 0 test(s) from tests/
[PASS] package_name::tests::should_panic_check_data
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
Ignoring Tests
Sometimes you may have tests that you want to exclude during most runs of snforge test
.
You can achieve it using #[ignore]
- tests marked with this attribute will be skipped by default.
#[cfg(test)]
mod tests {
#[test]
#[ignore]
fn ignored_test() {
// test code
}
}
$ snforge test
Collected 1 test(s) from package_name package
Running 1 test(s) from src/
Running 0 test(s) from tests/
[IGNORE] package_name::tests::ignored_test
Tests: 0 passed, 0 failed, 0 skipped, 1 ignored, 0 filtered out
To run only tests marked with the #[ignore]
attribute use snforge test --ignored
.
To run all tests regardless of the #[ignore]
attribute use snforge test --include-ignored
.
Testing Smart Contracts
ℹ️ Info
To use the library functions designed for testing smart contracts, 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.12.0" }
Using unit testing as much as possible is a good practice, as it makes your test suites run faster. However, when writing smart contracts, you often want to test their interactions with the blockchain state and with other contracts.
The Test Contract
In this tutorial we will be using this Starknet contract, in a new using_dispatchers
package.
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn increase_balance(ref self: TContractState, amount: felt252);
fn get_balance(self: @TContractState) -> felt252;
}
#[starknet::contract]
mod HelloStarknet {
#[storage]
struct Storage {
balance: felt252,
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
// Increases the balance by the given amount.
fn increase_balance(ref self: ContractState, amount: felt252) {
self.balance.write(self.balance.read() + amount);
}
// Gets the balance.
fn get_balance(self: @ContractState) -> felt252 {
self.balance.read()
}
}
}
Note that the name after mod
will be used as the contract name for testing purposes.
Writing Tests
Let's write a test that will deploy the HelloStarknet
contract and call some functions.
use snforge_std::{ declare, ContractClassTrait };
use using_dispatchers::{ IHelloStarknetDispatcher, IHelloStarknetDispatcherTrait };
#[test]
fn call_and_invoke() {
// First declare and deploy a contract
let contract = declare("HelloStarknet").unwrap();
// Alternatively we could use `deploy_syscall` here
let (contract_address, _) = contract.deploy(@array![]).unwrap();
// Create a Dispatcher object that will allow interacting with the deployed contract
let dispatcher = IHelloStarknetDispatcher { contract_address };
// Call a view function of the contract
let balance = dispatcher.get_balance();
assert(balance == 0, 'balance == 0');
// Call a function of the contract
// Here we mutate the state of the storage
dispatcher.increase_balance(100);
// Check that transaction took effect
let balance = dispatcher.get_balance();
assert(balance == 100, 'balance == 100');
}
📝 Note
Notice that the arguments to the contract's constructor (the
deploy
'scalldata
argument) need to be serialized withSerde
.
HelloStarknet
contract has no constructor, so the calldata remains empty in the example above.
$ snforge test
Collected 1 test(s) from using_dispatchers 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
Handling Errors
Sometimes we want to test contracts functions that can panic, like testing that function that verifies caller address
panics on invalid address. For that purpose Starknet also provides a SafeDispatcher
, that returns a Result
instead of
panicking.
First, let's add a new, panicking function to our contract.
#[starknet::interface]
trait IHelloStarknet<TContractState> {
// ...
fn do_a_panic(self: @TContractState);
fn do_a_string_panic(self: @TContractState);
}
#[starknet::contract]
mod HelloStarknet {
use array::ArrayTrait;
// ...
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
// ...
// Panics
fn do_a_panic(self: @ContractState) {
panic(array!['PANIC', 'DAYTAH']);
}
fn do_a_string_panic(self: @ContractState) {
// A macro which allows panicking with a ByteArray (string) instance
panic!("This is panicking with a string, which can be longer than 31 characters");
}
}
}
If we called this function in a test, it would result in a failure.
#[test]
fn failing() {
let contract = declare("HelloStarknet").unwrap();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
let dispatcher = IHelloStarknetDispatcher { contract_address };
dispatcher.do_a_panic();
}
$ snforge test
Collected 1 test(s) from package_name package
Running 0 test(s) from src/
Running 1 test(s) from tests/
[FAIL] tests::failing
Failure data:
(0x50414e4943 ('PANIC'), 0x444159544148 ('DAYTAH'))
Tests: 0 passed, 1 failed, 0 skipped, 0 ignored, 0 filtered out
Failures:
tests::failing
SafeDispatcher
Using SafeDispatcher
we can test that the function in fact panics with an expected message.
Safe dispatcher is a special kind of dispatcher, which are not allowed in contracts themselves,
but are available for testing purposes.
They allow using the contract without automatically unwrapping the result, which allows to catch the error like shown below.
// Add those to import safe dispatchers, which are autogenerated, like regular dispatchers
use using_dispatchers::{ IHelloStarknetSafeDispatcher, IHelloStarknetSafeDispatcherTrait };
#[test]
#[feature("safe_dispatcher")]
fn handling_errors() {
// ...
let contract = declare("HelloStarknet").unwrap();
let (contract_address, _) = contract.deploy(@calldata).unwrap();
let safe_dispatcher = IHelloStarknetSafeDispatcher { contract_address };
match safe_dispatcher.do_a_panic() {
Result::Ok(_) => panic!("Entrypoint did not panic"),
Result::Err(panic_data) => {
assert(*panic_data.at(0) == 'PANIC', *panic_data.at(0));
assert(*panic_data.at(1) == 'DAYTAH', *panic_data.at(1));
}
};
}
Now the test passes as expected.
$ snforge test
Collected 1 test(s) from package_name package
Running 0 test(s) from src/
Running 1 test(s) from tests/
[PASS] tests::handling_errors
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
Similarly, you can handle the panics which use ByteArray
as an argument (like an assert!
or panic!
macro)
// Necessary utility function import
use snforge_std::byte_array::try_deserialize_bytearray_error;
#[test]
#[feature("safe_dispatcher")]
fn handling_string_errors() {
// ...
let contract = declare("HelloStarknet").unwrap();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
let safe_dispatcher = IHelloStarknetSafeDispatcher { contract_address };
match safe_dispatcher.do_a_string_panic() {
Result::Ok(_) => panic!("Entrypoint did not panic"),
Result::Err(panic_data) => {
let str_err = try_deserialize_bytearray_error(panic_data.span()).expect('wrong format');
assert(
str_err == "This is panicking with a string, which can be longer than 31 characters",
'wrong string received'
);
}
};
}
You also could skip the de-serialization of the panic_data
, and not use try_deserialize_bytearray_error
, but this way you can actually use assertions on the ByteArray
that was used to panic.
📝 Note
To operate with
SafeDispatcher
it's required to annotate its usage with#[feature("safe_dispatcher")]
.There are 3 options:
- module-level declaration
#[feature("safe_dispatcher")] mod my_module;
- function-level declaration
#[feature("safe_dispatcher")] fn my_function() { ... }
- directly before the usage
#[feature("safe_dispatcher")] let result = safe_dispatcher.some_function();
Expecting Test Failure
Sometimes the test code failing can be a desired behavior.
Instead of manually handling it, you can simply mark your test as #[should_panic(...)]
.
See here for more details.
Testing Contracts' Internals
Sometimes, you want to test a function which uses Starknet context (like block number, timestamp, storage access) without deploying the actual contract.
Since every test is treated like a contract, using the aforementioned pattern you can test:
- functions which are not available through the interface (but your contract uses them)
- functions which are internal
- functions performing specific operations on the contracts' storage or context data
- library calls directly in the tests
Utilities For Testing Internals
To facilitate such use cases, we have a handful of utilities which make a test behave like a contract.
contract_state_for_testing()
- State of Test Contract
This is a function generated by the #[starknet::contract]
macro.
It can be used to test some functions which accept the state as an argument, see the example below:
#[starknet::contract]
mod Contract {
#[storage]
struct Storage {
balance: felt252,
}
#[generate_trait]
impl InternalImpl of InternalTrait {
fn internal_function(self: @ContractState) -> felt252 {
self.balance.read()
}
}
fn other_internal_function(self: @ContractState) -> felt252 {
self.balance.read() + 5
}
}
use Contract::balanceContractMemberStateTrait; // <--- Ad. 1
use Contract::{ InternalTrait, other_internal_function }; // <--- Ad. 2
#[test]
fn test_internal() {
let mut state = Contract::contract_state_for_testing(); // <--- Ad. 3
state.balance.write(10);
let value = state.internal_function();
assert(value == 10, 'Incorrect storage value');
let other_value = other_internal_function(@state);
assert(value == 15, 'Incorrect return value');
}
This code contains some caveats:
- To access
read/write
methods of the state fields (in this case it'sbalance
) you need to also import<member_name>ContractMemberStateTrait
from your contract, where<member_name>
is the name of the storage variable insideStorage
struct. - To access functions implemented directly on the state you need to also import an appropriate trait or function.
- This function will always return the struct keeping track of the state of the test. It means that within one test every result of
contract_state_for_testing
actually points to the same state.
snforge_std::test_address()
- Address of Test Contract
That function returns the contract address of the test. It is useful, when you want to:
- Mock the context (
cheat_caller_address
,cheat_block_timestamp
,cheat_block_number
, ...) - Spy for events emitted in the test
Example usages:
1. Mocking the context info
Example for cheat_block_number
, same can be implemented for cheat_caller_address
/cheat_block_timestamp
/elect
etc.
use result::ResultTrait;
use box::BoxTrait;
use starknet::ContractAddress;
use snforge_std::{
start_cheat_block_number, stop_cheat_block_number,
test_address
};
#[test]
fn test_cheat_block_number_test_state() {
let test_address: ContractAddress = test_address();
let old_block_number = starknet::get_block_info().unbox().block_number;
start_cheat_block_number(test_address, 234);
let new_block_number = starknet::get_block_info().unbox().block_number;
assert(new_block_number == 234, 'Wrong block number');
stop_cheat_block_number(test_address);
let new_block_number = starknet::get_block_info().unbox().block_number;
assert(new_block_number == old_block_number, 'Block num did not change back');
}
2. Spying for events
You can use both starknet::emit_event_syscall
, and the spies will capture the events,
emitted in a #[test]
function, if you pass the test_address()
as a spy parameter (or spy on all events).
Given the emitting contract implementation:
#[starknet::contract]
mod Emitter {
use result::ResultTrait;
use starknet::ClassHash;
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
ThingEmitted: ThingEmitted
}
#[derive(Drop, starknet::Event)]
struct ThingEmitted {
thing: felt252
}
#[storage]
struct Storage {}
#[external(v0)]
fn emit_event(
ref self: ContractState,
) {
self.emit(Event::ThingEmitted(ThingEmitted { thing: 420 }));
}
}
You can implement this test:
use array::ArrayTrait;
use snforge_std::{
declare, ContractClassTrait, spy_events,
EventSpy, EventSpyTrait, EventSpyAssertionsTrait,
Event, test_address
};
#[test]
fn test_expect_event() {
let contract_address = test_address();
let mut spy = spy_events();
let mut testing_state = Emitter::contract_state_for_testing();
Emitter::emit_event(ref testing_state);
spy.assert_emitted(
@array![
(
contract_address,
Emitter::Event::ThingEmitted(Emitter::ThingEmitted { thing: 420 })
)
]
)
}
You can also use the starknet::emit_event_syscall
directly in the tests:
use array::ArrayTrait;
use result::ResultTrait;
use starknet::SyscallResultTrait;
use starknet::ContractAddress;
use snforge_std::{ declare, ContractClassTrait, spy_events, EventSpy, EventSpyTrait,
EventSpyAssertionsTrait, Event, test_address };
#[test]
fn test_expect_events_simple() {
let test_address = test_address();
let mut spy = spy_events();
starknet::emit_event_syscall(array![1234].span(), array![2345].span()).unwrap_syscall();
spy.assert_emitted(@array![
(
contract_address,
Event { keys: array![1234], data: array![2345] }
)
]);
assert(spy.events.len() == 0, 'There should be no events left');
}
Using Library Calls With the Test State Context
Using the above utilities, you can avoid deploying a mock contract, to test a library_call
with a LibraryCallDispatcher
.
For contract implementation:
#[starknet::contract]
mod LibraryContract {
use result::ResultTrait;
use starknet::ClassHash;
use starknet::library_call_syscall;
#[storage]
struct Storage {
value: felt252
}
#[external(v0)]
fn get_value(
self: @ContractState,
) -> felt252 {
self.value.read()
}
#[external(v0)]
fn set_value(
ref self: ContractState,
number: felt252
) {
self.value.write(number);
}
}
We use the SafeLibraryDispatcher
like this:
use result::ResultTrait;
use starknet::{ ClassHash, library_call_syscall, ContractAddress };
use snforge_std::{ declare };
#[starknet::interface]
trait ILibraryContract<TContractState> {
fn get_value(
self: @TContractState,
) -> felt252;
fn set_value(
ref self: TContractState,
number: felt252
);
}
#[test]
fn test_library_calls() {
let class_hash = declare("LibraryContract").class_hash;
let lib_dispatcher = ILibraryContractSafeLibraryDispatcher { class_hash };
let value = lib_dispatcher.get_value().unwrap();
assert(value == 0, 'Incorrect state');
lib_dispatcher.set_value(10);
let value = lib_dispatcher.get_value().unwrap();
assert(value == 10, 'Incorrect state');
}
⚠️ Warning
This library call will write to the
test_address
memory segment, so it can potentially overwrite the changes you make to the memory throughcontract_state_for_testing
object and vice-versa.
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_cheat_caller_address
cheatcode to change the caller
address, so it passes our validation.
Cheating an Address
use snforge_std::{ declare, ContractClassTrait, start_cheat_caller_address };
#[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_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
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 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
use snforge_std::{stop_cheat_caller_address};
#[test]
fn call_and_invoke() {
// ...
// The address when calling contract at the `contract_address` address will no longer be changed
stop_cheat_caller_address(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
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, start_cheat_caller_address };
#[test]
fn mock_constructor_with_cheat_caller_address() {
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_cheat_caller_address(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 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, ContractClass, ContractClassTrait, cheat_caller_address, CheatSpan
};
#[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 cheat_caller_addressed_address: 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, cheat_caller_addressed_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 cheat_caller_address 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);
}
Testing events
Examples are based on the following SpyEventsChecker
contract implementation:
#[starknet::contract]
pub mod SpyEventsChecker {
// ...
#[storage]
struct Storage {}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
FirstEvent: FirstEvent
}
#[derive(Drop, starknet::Event)]
pub struct FirstEvent {
pub some_data: felt252
}
#[external(v0)]
fn emit_one_event(ref self: ContractState, some_data: felt252) {
self.emit(FirstEvent { some_data });
}
// ...
}
Asserting emission with assert_emitted
method
This is the simpler way, in which you don't have to fetch the events explicitly. See the below code for reference:
use snforge_std::{
declare, ContractClassTrait,
spy_events,
EventSpyAssertionsTrait, // Add for assertions on the EventSpy
};
use SpyEventsChecker;
#[starknet::interface]
trait ISpyEventsChecker<TContractState> {
fn emit_one_event(ref self: TContractState, some_data: felt252);
}
#[test]
fn test_simple_assertions() {
let contract = declare("SpyEventsChecker").unwrap();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
let dispatcher = ISpyEventsCheckerDispatcher { contract_address };
let mut spy = spy_events(); // Ad. 1
dispatcher.emit_one_event(123);
spy.assert_emitted(@array![ // Ad. 2
(
contract_address,
SpyEventsChecker::Event::FirstEvent(
SpyEventsChecker::FirstEvent { some_data: 123 }
)
)
]);
}
Let's go through the code:
- After contract deployment, we created the spy using
spy_events
cheatcode. From this moment all emitted events will be spied. - Asserting is done using the
assert_emitted
method. It takes an array snapshot of(ContractAddress, event)
tuples we expect that were emitted.
📝 Note We can pass events defined in the contract and construct them like in the
self.emit
method!
Asserting lack of event emission with assert_not_emitted
In cases where you want to test an event was not emitted, use the assert_not_emitted
function.
It works similarly as assert_emitted
with the only difference that it panics if an event was emitted during the execution.
Given the example above, we can check that a different FirstEvent
was not emitted:
spy.assert_not_emitted(@array![
(
contract_address,
SpyEventsChecker::Event::FirstEvent(
SpyEventsChecker::FirstEvent { some_data: 456 }
)
)
]);
Note that both the event name and event data are checked.
If a function emitted an event with the same name but a different payload, the assert_not_emitted
function will pass.
Asserting the events manually
If you wish to assert the data manually, you can do that on the Events
structure.
Simply call get_events()
on your EventSpy
and access events
field on the returned Events
value.
Then, you can access the events and assert data by yourself.
use snforge_std::{
declare, ContractClassTrait,
spy_events,
EventSpyAssertionsTrait,
EventSpyTrait, // Add for fetching events directly
Event, // A structure describing a raw `Event`
};
#[starknet::interface]
trait ISpyEventsChecker<TContractState> {
fn emit_one_event(ref self: TContractState, some_data: felt252);
}
#[test]
fn test_complex_assertions() {
let contract = declare("SpyEventsChecker").unwrap();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
let dispatcher = ISpyEventsCheckerDispatcher { contract_address };
let mut spy = spy_events(); // Ad 1.
dispatcher.emit_one_event(123);
let events = spy.get_events(); // Ad 2.
assert(events.events.len() == 1, 'There should be one event');
let (from, event) = events.events.at(0); // Ad 3.
assert(from == @contract_address, 'Emitted from wrong address');
assert(event.keys.len() == 1, 'There should be one key');
assert(event.keys.at(0) == @selector!("FirstEvent"), 'Wrong event name'); // Ad 4.
assert(event.data.len() == 1, 'There should be one data');
}
Let's go through important parts of the provided code:
- After contract deployment we created the spy with
spy_events
cheatcode. From this moment all events emitted by theSpyEventsChecker
contract will be spied. - We have to call
get_events
method on the created spy to fetch our events and get theEvents
structure. - To get our particular event, we need to access the
events
property and get the event under an index. Sinceevents
is an array holding a tuple ofContractAddress
andEvent
, we unpack it usinglet (from, event)
. - If the event is emitted by calling
self.emit
method, its hashed name is saved under thekeys.at(0)
(this way Starknet handles events)
📝 Note To assert the
name
property we have to hash a string with theselector!
macro.
Filtering Events
Sometimes, when you assert the events manually, you might not want to get all the events, but only ones from
a particular address. You can address that by using the method emitted_by
on the Events
structure.
use snforge_std::{
declare, ContractClassTrait,
spy_events,
EventSpyAssertionsTrait,
EventSpyTrait,
Event,
EventsFilterTrait, // Add for filtering the Events object (result of `get_events`)
};
use SpyEventsChecker;
#[starknet::interface]
trait ISpyEventsChecker<TContractState> {
fn emit_one_event(ref self: TContractState, some_data: felt252);
}
#[test]
fn test_assertions_with_filtering() {
let contract = declare("SpyEventsChecker").unwrap();
let (first_address, _) = contract.deploy(@array![]).unwrap();
let (second_address, _) = contract.deploy(@array![]).unwrap();
let first_dispatcher = ISpyEventsCheckerDispatcher { contract_address: first_address };
let second_dispatcher = ISpyEventsCheckerDispatcher { contract_address: second_address };
let mut spy = spy_events();
first_dispatcher.emit_one_event(123);
second_dispatcher.emit_one_event(234);
second_dispatcher.emit_one_event(345);
let events_from_first_address = spy.get_events().emitted_by(first_address);
let events_from_second_address = spy.get_events().emitted_by(second_address);
let (from_first, event_from_first) = events_from_first_address.events.at(0);
assert(from_first == @first_address, 'Emitted from wrong address');
assert(event_from_first.data.at(0) == @123.into(), 'Data should be 123');
let (from_second_one, event_from_second_one) = events_from_second_address.events.at(0);
assert(from_second_one == @second_address, 'Emitted from wrong address');
assert(event_from_second_one.data.at(0) == @234.into(), 'Data should be 234');
let (from_second_two, event_from_second_two) = events_from_second_address.events.at(1);
assert(from_second_two == @second_address, 'Emitted from wrong address');
assert(event_from_second_two.data.at(0) == @345.into(), 'Data should be 345');
}
events_from_first_address
has events emitted by the first contract only.
Similarly, events_from_second_address
has events emitted by the second contract.
Asserting Events Emitted With emit_event_syscall
Events emitted with emit_event_syscall
could have nonstandard (not defined anywhere) keys and data.
They can also be asserted with spy.assert_emitted
method.
Let's add such a method in the SpyEventsChecker
contract:
use core::starknet::syscalls::emit_event_syscall;
use core::starknet::SyscallResultTrait;
#[external(v0)]
fn emit_event_with_syscall(ref self: ContractState, some_key: felt252, some_data: felt252) {
emit_event_syscall(array![some_key].span(), array![some_data].span()).unwrap_syscall();
}
And add a test for it:
use snforge_std::{
declare, ContractClassTrait,
spy_events,
EventSpyAssertionsTrait,
EventSpyTrait,
Event,
EventsFilterTrait,
};
#[starknet::interface]
trait ISpyEventsChecker<TContractState> {
fn emit_event_with_syscall(ref self: TContractState, some_key: felt252, some_data: felt252);
}
#[test]
fn test_nonstandard_events() {
let contract = declare("SpyEventsChecker").unwrap();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
let dispatcher = ISpyEventsCheckerDispatcher { contract_address };
let mut spy = spy_events();
dispatcher.emit_event_with_syscall(123, 456);
spy.assert_emitted(@array![
(
contract_address,
Event { keys: array![123], data: array![456] }
)
]);
}
Using Event
struct from the snforge_std
library we can easily assert nonstandard events.
This also allows for testing the events you don't have the code of, or you don't want to import those.
Testing messages to L1
There exists a functionality allowing you to spy on messages sent to L1, similar to spying events.
Check the appendix for an exact API, structures and traits reference
Asserting messages to L1 is much simpler, since they are not wrapped with any structures in Cairo code (they are a plain felt252
array and an L1 address).
In snforge
they are expressed with a structure:
/// Raw message to L1 format (as seen via the RPC-API), can be used for asserting the sent messages.
struct MessageToL1 {
/// An ethereum address where the message is destined to go
to_address: EthAddress,
/// Actual payload which will be delivered to L1 contract
payload: Array<felt252>
}
Similarly, you can use snforge
library and call spy_messages_to_l1()
to initiate a spy:
use snforge_std::{spy_messages_to_l1};
// ...
#[test]
fn test_spying_l1_messages() {
let mut spy = spy_messages_to_l1();
// ...
}
With the spy ready to use, you can execute some code, and make the assertions:
- Either with the spy directly by using
assert_sent
/assert_not_sent
methods fromMessageToL1SpyAssertionsTrait
trait:
use snforge_std::{spy_messages_to_l1, MessageToL1SpyAssertionsTrait, MessageToL1};
// ...
#[test]
fn test_spying_l1_messages() {
let mut spy = spy_messages_to_l1();
// ...
spy.assert_sent(
@array![
(
contract_address, // Message sender
MessageToL1 { // Message content (receiver and payload)
to_address: 0x123.try_into().unwrap(),
payload: array![123, 321, 420]
}
)
]
);
}
- Or use the messages' contents directly via
get_messages()
method of theMessageToL1Spy
trait:
use snforge_std::{
spy_messages_to_l1, MessageToL1,
MessageToL1SpyAssertionsTrait,
MessageToL1FilterTrait,
MessageToL1SpyTrait
};
// ...
#[test]
fn test_spying_l1_messages() {
let mut spy = spy_messages_to_l1();
let messages = spy.get_messages();
// Use filtering optionally on MessagesToL1 instance
let messages_from_specific_address = messages.sent_by(sender_address);
let messages_to_specific_address = messages_from_specific_address.sent_to(receiver_eth_address);
// Get the messages from the MessagesToL1 structure
let (from, message) = messages_to_specific_address.messages.at(0);
// Assert the sender
assert!(from == sender_address, "Sent from wrong address");
// Assert the MessageToL1 fields
assert!(message.to_address == receiver_eth_address, "Wrong eth address of the receiver");
assert!(message.payload.len() == 3, "There should be 3 items in the data");
assert!(*message.payload.at(1) == 421, "Expected 421 in payload");
}
Testing Scarb Workspaces
snforge
supports Scarb Workspaces.
To make sure you know how workspaces work,
check Scarb documentation here.
Workspaces With Root Package
When running snforge test
in a Scarb workspace with a root package, it will only run tests inside the root package.
For a project structure like this
$ tree . -L 3
.
├── Scarb.toml
├── crates
│ ├── addition
│ │ ├── Scarb.toml
│ │ ├── src
│ │ └── tests
│ └── fibonacci
│ ├── Scarb.toml
│ └── src
├── tests
│ └── test.cairo
└── src
└── lib.cairo
only the tests in ./src
and ./tests
folders will be executed.
$ snforge test
Collected 1 test(s) from hello_workspaces package
Running 1 test(s) from src/
[PASS] hello_workspaces::tests::test_simple
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
To select the specific package to test, pass a --package package_name
(or -p package_name
for short) flag.
You can also run snforge test
from the package directory to achieve the same effect.
$ snforge test --package addition
Collected 2 test(s) from addition package
Running 1 test(s) from src/
[PASS] addition::tests::it_works
Running 1 test(s) from tests/
[PASS] tests::test_simple::simple_case
Tests: 2 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
You can also pass --workspace
flag to run tests for all packages in the workspace.
$ snforge test --workspace
Collected 2 test(s) from addition package
Running 1 test(s) from src/
[PASS] addition::tests::it_works
Running 1 test(s) from tests/
[PASS] tests::test_simple::simple_case
Tests: 2 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
Collected 1 test(s) from fibonacci package
Running 1 test(s) from src/
[PASS] fibonacci::tests::it_works
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
Collected 1 test(s) from hello_workspaces package
Running 1 test(s) from src/
[PASS] hello_workspaces::tests::test_simple
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
--package
and --workspace
flags are mutually exclusive, adding both of them to a snforge test
command will result in an error.
Virtual Workspaces
Running snforge test
command in a virtual workspace (a workspace without a root package)
outside any package will by default run tests for all the packages.
It is equivalent to running snforge test
with the --workspace
flag.
To select a specific package to test,
you can use the --package
flag the same way as in regular workspaces or run snforge test
from the package directory.
Test Collection
snforge
considers all functions in your project marked with #[test]
attribute as tests.
By default, test functions run without any arguments.
However, adding any arguments to function signature will enable fuzz testing for this
test case.
snforge
will collect tests only from these places:
- any files reachable from the package root (declared as
mod
inlib.cairo
or its children) - these have to be in a module annotated with#[cfg(test)]
- files inside the
tests
directory
The tests
Directory
snforge
collects tests from tests
directory.
Depending on the presence of tests/lib.cairo
file, the behavior of the test collector will be different.
With tests/lib.cairo
If there is a lib.cairo
file in tests
folder,
then it is treated as an entrypoint to the tests
package from which tests are collected.
For example, for a package structured this way:
$ tree .
.
├── Scarb.toml
├── tests/
│ ├── lib.cairo
│ ├── common/
│ │ └── utils.cairo
│ ├── common.cairo
│ ├── test_contract.cairo
│ └── not_included.cairo
└── src/
└── lib.cairo
with tests/lib.cairo
content:
mod common;
mod test_contract;
and tests/common.cairo
content:
mod utils;
tests from tests/lib.cairo
, tests/test_contract.cairo
, tests/common.cairo
and tests/common/utils.cairo
will be collected.
Without tests/lib.cairo
When there is no lib.cairo
present in tests
folder,
all test files directly in tests
directory (i.e., not in its subdirectories)
are treated as modules and added to a single virtual lib.cairo
.
Then this virtual lib.cairo
is treated as an entrypoint to the tests
package from which tests are collected.
For example, for a package structured this way:
$ tree .
.
├── Scarb.toml
├── tests/
│ ├── common/
│ │ └── utils.cairo
│ ├── common.cairo
│ ├── test_contract.cairo
│ └── not_included/
│ └── ignored.cairo
└── src/
└── lib.cairo
and tests/common.cairo
content:
mod utils;
tests from tests/test_contract.cairo
, tests/common.cairo
and tests/common/utils.cairo
will be collected.
Sharing Code Between Tests
Sometimes you may want a share some code between tests to organize them.
The package structure of tests makes it easy!
In both of the above examples, you can
make the functions from tests/common/utils.cairo
available in tests/test_contract.cairo
by using a relative import: use super::common::utils;
.
How Contracts Are Collected
When you call snforge test
, one of the things that snforge
does is that it calls Scarb, particularly scarb build
.
It makes Scarb build all contracts from your package and save them to the target/{current_profile}
directory
(read more on Scarb website).
Then, snforge
loads compiled contracts from the package your tests are in, allowing you to declare the contracts in tests.
⚠️ Warning
Make sure to define
[[target.starknet-contract]]
section in yourScarb.toml
, otherwise Scarb won't build your contracts.
Using External Contracts In Tests
If you wish to use contracts from your dependencies inside your tests (e.g. an ERC20 token, an account contract),
you must first make Scarb build them. You can do that by using build-external-contracts
property in Scarb.toml
, e.g.:
[[target.starknet-contract]]
build-external-contracts = ["openzeppelin::account::account::Account"]
For more information about build-external-contracts
, see Scarb documentation.
Gas and VM Resources Estimation
snforge
supports gas and other VM resources estimation for each individual test case.
It does not calculate the final transaction fee, for details on how fees are calculated, please refer to fee mechanism in Starknet documentation.
Gas Estimation
Single Test
When the test passes with no errors, estimated gas is displayed this way:
[PASS] tests::simple_test (gas: ~1)
This gas calculation is based on the estimated VM resources (that you can display additionally on demand), deployed contracts, storage updates, events and l1 <> l2 messages.
Fuzzed Tests
While using the fuzzing feature additional gas statistics will be displayed:
[PASS] tests::fuzzing_test (runs: 256, gas: {max: ~126, min: ~1, mean: ~65.00, std deviation: ~37.31})
📝 Note
Starknet-Foundry uses blob-based gas calculation formula in order to calculate gas usage. For details on the exact formula, see the docs.
VM Resources estimation
It is possible to enable more detailed breakdown of resources, on which the gas calculations are based on.
Usage
In order to run tests with this feature, run the test
command with the appropriate flag:
$ snforge test --detailed-resources
...
[PASS] package_name::tests::resources (gas: ~2213)
steps: 881
memory holes: 36
builtins: ("range_check_builtin": 32)
syscalls: (StorageWrite: 1, StorageRead: 1, CallContract: 1)
...
This displays the resources used by the VM during the test execution.
Analyzing the results
Normally in transaction receipt (or block explorer transaction details), you would see some additional OS resources that starknet-foundry does not include for a test (since it's not a normal transaction per-se):
Not included in the gas/resource estimations
- Fee transfer costs
- Transaction type related resources - in real Starknet additional cost depending on the transaction type (e.g.,
Invoke
/Declare
/DeployAccount
) is added - Declaration gas costs (CASM/Sierra bytecode or ABIs)
- Call validation gas costs (if you did not call
__validate__
endpoint explicitly)
Included in the gas/resource estimations
- Cost of syscalls (additional steps or builtins needed for syscalls execution)
Fork Testing
snforge
supports testing in a forked environment. 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.
Fork Configuration
There are two ways of configuring a fork:
- by specifying
url
andblock_id
parameters in the#[fork(...)]
attribute - or by passing a fork name defined in your
Scarb.toml
to the#[fork(...)]
attribute
Configure a Fork in the Attribute
It is possible to pass url
and block_id
arguments to the fork
attribute:
url
- RPC URL (short string)block_id
- id of block which will be pin to fork (BlockId
enum)
enum BlockId {
Tag: BlockTag,
Hash: felt252,
Number: u64,
}
enum BlockTag {
Latest,
}
use snforge_std::BlockId;
#[test]
#[fork(url: "http://your.rpc.url", block_id: BlockId::Number(123))]
fn test_using_forked_state() {
// ...
}
Once such a configuration is passed, it is possible to use state and contracts defined on the specified network.
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 = "SOME_NAME"
url = "http://your.rpc.url"
block_id.tag = "Latest"
[[tool.snforge.fork]]
name = "SOME_SECOND_NAME"
url = "http://your.second.rpc.url"
block_id.number = "123"
[[tool.snforge.fork]]
name = "SOME_THIRD_NAME"
url = "http://your.third.rpc.url"
block_id.hash = "0x123"
From this moment forks can be set using their name in the fork
attribute.
#[test]
#[fork("SOME_NAME")]
fn test_using_first_fork() {
// ...
}
#[test]
#[fork("SOME_SECOND_NAME")]
fn test_using_second_fork() {
// ...
}
// ...
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
The following cheatcodes won't work for forked contracts written in Cairo 0:
- start_spoof / stop_spoof
- spy_events
Fuzz Testing
In many cases, a test needs to verify function behavior for multiple possible values. While it is possible to come up with these cases on your own, it is often impractical, especially when you want to test against a large number of possible arguments.
ℹ️ Info Currently,
snforge
fuzzer only supports using randomly generated values. This way of fuzzing doesn't support any kind of value generation based on code analysis, test coverage or results of other fuzzer runs. In the future, more advanced fuzzing execution modes will be added.
Random Fuzzing
To convert a test to a random fuzz test, simply add arguments to the test function. These arguments can then be used in the test body. The test will be run many times against different randomly generated values.
fn sum(a: felt252, b: felt252) -> felt252 {
return a + b;
}
#[test]
fn test_sum(x: felt252, y: felt252) {
assert(sum(x, y) == x + y, 'sum incorrect');
}
Then run snforge test
like usual.
$ snforge test
Collected 1 test(s) from package_name package
Running 0 test(s) from src/
Running 1 test(s) from tests/
[PASS] tests::test_sum (runs: 256)
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
Fuzzer seed: [..]
Types Supported by the Fuzzer
Fuzzer currently supports generating values of these types
u8
u16
u32
u64
u128
u256
felt252
Trying to use arguments of different type in test definition will result in an error.
Fuzzer Configuration
It is possible to configure the number of runs of the random fuzzer as well as its seed for a specific test case:
#[test]
#[fuzzer(runs: 22, seed: 38)]
fn test_sum(x: felt252, y: felt252) {
assert(sum(x, y) == x + y, 'sum incorrect');
}
It can also be configured globally, via command line arguments:
$ snforge test --fuzzer-runs 1234 --fuzzer-seed 1111
Or in Scarb.toml
file:
# ...
[tool.snforge]
fuzzer_runs = 1234
fuzzer_seed = 1111
# ...
Direct Storage Access
In some instances, it's not possible for contracts to expose API that we'd like to use in order to initialize
the contracts before running some tests. For those cases snforge
exposes storage-related cheatcodes,
which allow manipulating the storage directly (reading and writing).
In order to obtain the variable address that you'd like to write to, or read from, you need to use either:
selector!
macro - if the variable is not a mappingmap_entry_address
function in tandem withselector!
- for key-value pair of a map variablestarknet::storage_access::storage_address_from_base
Example: Felt-only storage
This example uses only felts for simplicity
#[starknet::contract]
mod Contract {
#[storage]
struct Storage {
plain_felt: felt252,
mapping: LegacyMap<felt252, felt252>,
}
}
// ...
use snforge_std::{ store, load, map_entry_address };
#[test]
fn store_and_load_with_plain_felt() {
// ...
store(contract_address, selector!("plain_felt"), array![123].span());
// plain_felt = 123
let loaded = load(contract_address, selector!("plain_felt"), 1);
assert(loaded.len() == 1, 'Wrong loaded vector');
assert(*loaded.at(0) == 123, 'Wrong loaded value');
}
#[test]
fn store_and_load_map_entry() {
// ...
store(
contract_address,
map_entry_address(
selector!("mapping"), // Providing variable name
array![123].span(), // Providing mapping key
),
array![321].span()
);
// mapping = { 123: 321, ... }
let loaded = load(
contract_address,
map_entry_address(
selector!("mapping"), // Providing variable name
array![123].span(), // Providing mapping key
),
1,
);
assert(loaded.len() == 1, 'Expected 1 felt loaded');
assert(*loaded.at(0) == 321, 'Expected 321 value loaded');
}
Example: Complex structures in storage
This example uses a complex key and value, with default derived serialization methods (via #[derive(starknet::Store)]
).
use snforge_std::{ store, load, map_entry_address };
#[starknet::contract]
mod Contract {
#[derive(Serde)]
struct MapKey {
a: felt252,
b: felt252,
}
// Required for lookup of complex_mapping values
// This is consistent with `map_entry_address`, which uses pedersen hashing of keys
impl StructuredKeyHash of LegacyHash<MapKey> {
fn hash(state: felt252, value: MapKey) -> felt252 {
let state = LegacyHash::<felt252>::hash(state, value.a);
LegacyHash::<felt252>::hash(state, value.b)
}
}
#[derive(Serde, starknet::Store)]
struct MapValue {
a: felt252,
b: felt252,
}
// Serialization of keys and values with `Serde` to make usage of `map_entry_address` easier
impl MapKeyIntoSpan of Into<MapKey, Span<felt252>> {
fn into(self: MapKey) -> Span<felt252> {
let mut serialized_struct: Array<felt252> = array![];
self.serialize(ref serialized_struct);
serialized_struct.span()
}
}
impl MapValueIntoSpan of Into<MapValue, Span<felt252>> {
fn into(self: MapValue) -> Span<felt252> {
let mut serialized_struct: Array<felt252> = array![];
self.serialize(ref serialized_struct);
serialized_struct.span()
}
}
#[storage]
struct Storage {
complex_mapping: LegacyMap<MapKey, MapValue>,
}
}
// ...
#[test]
fn store_in_complex_mapping() {
// ...
let k = MapKey { a: 111, b: 222 };
let v = MapValue { a: 123, b: 456 };
store(
contract_address,
map_entry_address( // Uses pedersen hashing for address calculation
selector!("mapping"), // Providing variable name
k.into() // Providing mapping key
),
v.into()
);
// complex_mapping = {
// hash(k): 123,
// hash(k) + 1: 456
// ...
// }
let loaded = load(
contract_address,
selector!("elaborate_struct"), // Providing variable name
2, // Size of the struct in felts
);
assert(loaded.len() == 2, 'Expected 1 felt loaded');
assert(*loaded.at(0) == 123, 'Expected 123 value loaded');
assert(*loaded.at(1) == 456, 'Expected 456 value loaded');
}
⚠️ Warning
Complex data can often times be packed in a custom manner (see this pattern) to optimize costs. If that's the case for your contract, make sure to handle deserialization properly - standard methods might not work. Use those cheatcode as a last-resort, for cases that cannot be handled via contract's API!
📝 Note
The
load
cheatcode will return zeros for memory you haven't written into yet (it is a default storage value for Starknet contracts' storage).
Example with storage_address_from_base
This example uses storage_address_from_base
with address
function of the storage variable.
To retrieve storage address of a given field
, you need to import {field_name}ContractMemberStateTrait
from the contract.
#[starknet::contract]
mod Contract {
#[storage]
struct Storage {
map: LegacyMap::<(u8, u32), u32>,
}
}
// ...
use starknet::storage_access::storage_address_from_base;
use snforge_std::{ store, load };
use Contract::mapContractMemberStateTrait;
#[test]
fn update_mapping() {
let key = (1_u8, 10_u32);
let data = 42_u32;
// ...
let mut state = Contract::contract_state_for_testing();
let storage_address: felt252 = storage_address_from_base(
state.map.address(key)
)
.into();
let storage_value: Span<felt252> = array![data.into()].span();
store(contract_address, storage_address, storage_value);
let read_data: u32 = load(contract_address, storage_address, 1).at(0).try_into().unwrap():
assert_eq!(read_data, data, "Storage update failed")
}
Profiling
Profiling is what allows developers to get more insight into how the transaction is executed. You can inspect the call tree, see how many resources are used for different parts of the execution, and more!
Integration with cairo-profiler
snforge
is able to produce a file with a trace for each passing test (excluding fuzz tests).
All you have to do is use the --save-trace-data
flag:
$ snforge test --save-trace-data
The files with traces will be saved to snfoundry_trace
directory. Each one of these files can then be used as an input
for the cairo-profiler.
If you want snforge
to call cairo-profiler
on generated files automatically, use --build-profile
flag:
$ snforge test --build-profile
sncast
Overview
Starknet Foundry sncast
is a command line tool for performing Starknet RPC calls. With it, you can easily interact with Starknet contracts!
💡 Info At the moment,
sncast
only supports contracts written in Cairo v1 and v2.
⚠️ Warning Currently, support is only provided for accounts that use the default signature based on the Stark curve.
How to Use sncast
To use sncast
, run the sncast
command followed by a subcommand (see available commands):
$ sncast <subcommand>
If snfoundry.toml
is present and configured with [sncast.default]
, url
, accounts-file
and account
name will be taken from it.
You can, however, overwrite their values by supplying them as flags directly to sncast
cli.
💡 Info Some transactions (like declaring, deploying or invoking) require paying a fee, and they must be signed.
Examples
General Example
Let's use sncast
to call a contract's function:
$ sncast --account myuser \
--url http://127.0.0.1:5050 \
call \
--contract-address 0x38b7b9507ccf73d79cb42c2cc4e58cf3af1248f342112879bfdf5aa4f606cc9 \
--function get \
--calldata 0x0 \
--block-id latest
command: call
response: [0x0]
📝 Note In the above example we supply
sncast
with--account
and--url
flags. Ifsnfoundry.toml
is present, and have these properties set, values provided using these flags will override values fromsnfoundry.toml
. Learn more aboutsnfoundry.toml
configuration here.
How to Use --wait
Flag
Let's invoke a transaction and wait for it to be ACCEPTED_ON_L2
.
$ sncast --account myuser \
--url http://127.0.0.1:5050 \
--wait \
deploy \
--class-hash 0x8448a68b5ea1affc45e3fd4b8b480ea36a51dc34e337a16d2567d32d0c6f8a
Transaction hash: 0x3062310a1e40d4b66d8987ba7447d1c7317381d0295d62cb12f2fe3f11e6983
Waiting for transaction to be received. Retries left: 11
Waiting for transaction to be received. Retries left: 10
Waiting for transaction to be received. Retries left: 9
Waiting for transaction to be received. Retries left: 8
Waiting for transaction to be received. Retries left: 7
Received transaction. Status: Pending
Received transaction. Status: Pending
Received transaction. Status: Pending
Received transaction. Status: Pending
Received transaction. Status: Pending
Received transaction. Status: Pending
command: deploy
contract_address: 0x1d91599ec661e97fdcbb10c642a1c4f920986f1a7a9659d157d0db09baaa29e
transaction_hash: 0x3062310a1e40d4b66d8987ba7447d1c7317381d0295d62cb12f2fe3f11e6983
As you can see command waited for the transaction until it was ACCEPTED_ON_L2
.
After setting up the --wait
flag, command waits 60 seconds for a transaction to be received and (another not specified
amount of time) to be included in the block.
📝 Note By default, all commands don't wait for transactions.
Creating And Deploying Accounts
Account is required to perform interactions with Starknet (only calls can be done without it). Starknet Foundry sncast
supports
entire account management flow with the sncast account create
and sncast account deploy
commands.
Difference between those two commands is that the first one creates account information (private key, address and more) and the second one deploys it to the network. After deployment, account can be used to interact with Starknet.
To remove an account from the accounts file, you can use sncast account delete
. Please note this only removes the account information stored locally - this will not remove the account from Starknet.
💡 Info Accounts creation and deployment is supported for
- OpenZeppelin
- Argent (with guardian set to 0)
- Braavos
Examples
General Example
Do the following to start interacting with the Starknet:
-
create account with the
sncast account create
command$ sncast \ --url http://127.0.0.1:5050 \ account create \ --name some-name Account successfully created. Prefund generated address with at least 432300000000 tokens. It is good to send more in the case of higher demand, max_fee * 2 = 864600000000 command: account create max_fee: 0x64a7168300 address: 0x7a949e83b243068d0cbedd8d5b8b32fafea66c54de23c40e68b126b5c845b61
You can also pass common
--accounts-file
argument with a path to (existing or not existing) file where you want to save account info.For a detailed CLI description, see account create command reference.
-
prefund generated address with tokens
You can do it both by sending tokens from another starknet account or by bridging them with StarkGate.
-
deploy account with the
sncast account deploy
command$ sncast \ --url http://127.0.0.1:5050 \ account deploy --name some-name \ --fee-token strk \ --max-fee 9999999999999 command: account deploy transaction_hash: 0x20b20896ce63371ef015d66b4dd89bf18c5510a840b4a85a43a983caa6e2579
Note that you don't have to pass
url
,accounts-file
andnetwork
parameters ifadd-profile
flag was set in theaccount create
command. Just passprofile
argument with the account name.For a detailed CLI description, see account deploy command reference.
💡 Info You can also choose to pay in Ether by setting
--fee-token
toeth
.
account create
With Salt Argument
Salt will not be randomly generated if it's specified with --salt
.
$ sncast \
account create \
--name some-name \
--salt 0x1
Account successfully created. Prefund generated address with at least 432300000000 tokens. It is good to send more in the case of higher demand, max_fee * 2 = 864600000000
command: account create
max_fee: 0x64a7168300
address: 0x7a949e83b243068d0cbedd8d5b8b32fafea66c54de23c40e68b126b5c845b61
account delete
Delete an account from accounts-file
and its associated Scarb profile.
$ sncast \
--accounts-file my-account-file.json \
account delete \
--name some-name \
--network alpha-sepolia
Do you want to remove account some-name from network alpha-sepolia? (Y/n)
Y
command: account delete
result: Account successfully removed
For a detailed CLI description, see account delete command reference.
account list
List all accounts saved in accounts file
, grouped based on the networks they are defined on.
$ sncast --accounts-file my-account-file.json account list
Available accounts (at <current-directory>/my-account-file.json):
- user0
public key: 0x2f91ed13f8f0f7d39b942c80bfcd3d0967809d99e0cc083606cbe59033d2b39
network: alpha-sepolia
address: 0x4f5f24ceaae64434fa2bc2befd08976b51cf8f6a5d8257f7ec3616f61de263a
type: OpenZeppelin
deployed: false
legacy: false
- user1
[...]
To show private keys too, run with --display-private-keys or -p
$ sncast --accounts-file my-account-file.json account list --display-private-keys
Available accounts (at <current-directory>/my-account-file.json):
- user0
private key: 0x1e9038bdc68ce1d27d54205256988e85
public key: 0x2f91ed13f8f0f7d39b942c80bfcd3d0967809d99e0cc083606cbe59033d2b39
network: alpha-sepolia
address: 0x4f5f24ceaae64434fa2bc2befd08976b51cf8f6a5d8257f7ec3616f61de263a
type: OpenZeppelin
deployed: false
legacy: false
- user1
[...]
Custom Account Contract
By default, sncast
creates/deploys an account using OpenZeppelin's account contract class hash.
It is possible to create an account using custom openzeppelin, argent or braavos contract declared to starknet. This can be achieved
with --class-hash
flag:
$ sncast \
account create \
--name some-name \
--class-hash 0x00e2eb8f5672af4e6a4e8a8f1b44989685e668489b0a25437733756c5a34a1d6
Account successfully created. Prefund generated address with at least 432300000000 tokens. It is good to send more in the case of higher demand, max_fee * 2 = 864600000000
command: account create
max_fee: 0x64a7168300
address: 0x7a949e83b243068d0cbedd8d5b8b32fafea66c54de23c40e68b126b5c845b61
$ sncast \
account deploy \
--name some-name \
--max-fee 864600000000
command: account deploy
transaction_hash: 0x20b20896ce63371ef015d66b4dd89bf18c5510a840b4a85a43a983caa6e2579
Using Keystore and Starkli Account
Accounts created and deployed with starkli can be used by specifying the --keystore
argument.
💡 Info When passing the
--keystore
argument,--account
argument must be a path to the starkli account JSON file.
$ sncast \
--url http://127.0.0.1:5050 \
--keystore path/to/keystore.json \
--account path/to/account.json \
declare \
--contract-name my_contract
Importing an Account
To import an account into the file holding the accounts info (~/.starknet_accounts/starknet_open_zeppelin_accounts.json
by default), use the account add
command.
$ sncast \
--url http://127.0.0.1:5050 \
account add \
--name my_imported_account \
--address 0x1 \
--private-key 0x2 \
--class-hash 0x3 \
--type oz
For a detailed CLI description, see account add command reference.
Creating an Account With Starkli-Style Keystore
It is possible to create an openzeppelin account with keystore in a similar way starkli does.
$ sncast \
--url http://127.0.0.1:5050 \
--keystore my_key.json \
--account my_account.json \
account create
The command above will generate a keystore file containing the private key, as well as an account file containing the openzeppelin account info that can later be used with starkli.
Declaring New Contracts
Starknet provides a distinction between contract class and instance. This is similar to the difference between writing the code of a class MyClass {}
and creating a new instance of it let myInstance = MyClass()
in object-oriented programming languages.
Declaring a contract is a necessary step to have your contract available on the network. Once a contract is declared, it then can be deployed and then interacted with.
For a detailed CLI description, see declare command reference.
Examples
General Example
📝 Note Building a contract before running
declare
is not required. Starknet Foundrysncast
builds a contract during declaration under the hood using Scarb.
First make sure that you have created a Scarb.toml
file for your contract (it should be present in project directory or one of its parent directories).
Then run:
$ sncast --account myuser \
--url http://127.0.0.1:5050/rpc \
declare \
--fee-token strk \
--contract-name SimpleBalance
command: declare
class_hash: 0x8448a68b5ea1affc45e3fd4b8b480ea36a51dc34e337a16d2567d32d0c6f8a
transaction_hash: 0x7ad0d6e449e33b6581a4bb8df866c0fce3919a5ee05a30840ba521dafee217f
📝 Note Contract name is a part after the
mod
keyword in your contract file. It may differ from package name defined inScarb.toml
file.
📝 Note In the above example we supply
sncast
with--account
and--url
flags. Ifsnfoundry.toml
is present, and has the properties set, values provided using these flags will override values fromsnfoundry.toml
. Learn more aboutsnfoundry.toml
configuration here.
💡 Info Max fee will be automatically computed if
--max-fee <MAX_FEE>
is not passed.
💡 Info You can also choose to pay in Ether by setting
--fee-token
toeth
.
Deploying New Contracts
Overview
Starknet Foundry sncast
supports deploying smart contracts to a given network with the sncast deploy
command.
It works by invoking a Universal Deployer Contract, which deploys the contract with the given class hash and constructor arguments.
For detailed CLI description, see deploy command reference.
Usage Examples
General Example
After declaring your contract, you can deploy it the following way:
$ sncast \
--account myuser \
--url http://127.0.0.1:5050/rpc \
deploy \
--fee-token strk \
--class-hash 0x8448a68b5ea1affc45e3fd4b8b480ea36a51dc34e337a16d2567d32d0c6f8a
command: Deploy
contract_address: 0x301316d47a81b39c5e27cca4a7b8ca4773edbf1103218588d6da4d3ed53035a
transaction_hash: 0x64a62a000240e034d1862c2bbfa154aac6a8195b4b2e570f38bf4fd47a5ab1e
💡 Info Max fee will be automatically computed if
--max-fee <MAX_FEE>
is not passed.
💡 Info You can also choose to pay in Ether by setting
--fee-token
toeth
.
Deploying Contract With Constructor
For such a constructor in the declared contract
#[constructor]
fn constructor(ref self: ContractState, first: felt252, second: u256) {
...
}
you have to pass constructor calldata to deploy it.
$ sncast deploy \
--fee-token strk \
--class-hash 0x8448a68b5ea1affc45e3fd4b8b480ea36a51dc34e337a16d2567d32d0c6f8a \
--constructor-calldata 0x1 0x1 0x0
command: deploy
contract_address: 0x301316d47a81b39c5e27cca4a7b8ca4773edbf1103218588d6da4d3ed53035a
transaction_hash: 0x64a62a000240e034d1862c2bbfa154aac6a8195b4b2e570f38bf4fd47a5ab1e
📝 Note Although the constructor has only two params you have to pass more because u256 is serialized to two felts. It is important to know how types are serialized because all values passed as constructor calldata are interpreted as a field elements (felt252).
Passing salt
Argument
Salt is a parameter which modifies contract's address, if not passed it will be automatically generated.
$ sncast deploy \
--fee-token strk \
--class-hash 0x8448a68b5ea1affc45e3fd4b8b480ea36a51dc34e337a16d2567d32d0c6f8a \
--salt 0x123
command: deploy
contract_address: 0x301316d47a81b39c5e27cca4a7b8ca4773edbf1103218588d6da4d3ed5303bc
transaction_hash: 0x64a62a000240e034d1862c2bbfa154aac6a8195b4b2e570f38bf4fd47a5ab1e
Passing unique
Argument
Unique is a parameter which modifies contract's salt with the deployer address.
It can be passed even if the salt
argument was not provided.
$ sncast deploy \
--fee-token strk \
--class-hash 0x8448a68b5ea1affc45e3fd4b8b480ea36a51dc34e337a16d2567d32d0c6f8a \
--unique
command: deploy
contract_address: 0x301316d47a81b39c5e27cca4a7b8ca4773edbf1103218588d6da4d3ed5303aa
transaction_hash: 0x64a62a000240e034d1862c2bbfa154aac6a8195b4b2e570f38bf4fd47a5ab1e
Invoking Contracts
Overview
Starknet Foundry sncast
supports invoking smart contracts on a given network with the sncast invoke
command.
In most cases, you have to provide:
- Contract address
- Function name
- Function arguments
For detailed CLI description, see invoke command reference.
Examples
General Example
$ sncast \
--url http://127.0.0.1:5050 \
--account example_user \
invoke \
--fee-token strk \
--contract-address 0x4a739ab73aa3cac01f9da5d55f49fb67baee4919224454a2e3f85b16462a911 \
--function "some_function" \
--calldata 1 2 0x1e
command: invoke
transaction_hash: 0x7ad0d6e449e33b6581a4bb8df866c0fce3919a5ee05a30840ba521dafee217f
💡 Info Max fee will be automatically computed if
--max-fee <MAX_FEE>
is not passed.
💡 Info You can also choose to pay in Ether by setting
--fee-token
toeth
.
Invoking Function Without Arguments
Not every function accepts parameters. Here is how to call it.
$ sncast invoke \
--fee-token strk \
--contract-address 0x4a739ab73aa3cac01f9da5d55f49fb67baee4919224454a2e3f85b16462a911 \
--function "function_without_params"
command: invoke
transaction_hash: 0x7ad0d6e449e33b6581a4bb8df866c0fce3919a5ee05a30840ba521dafee217f
Calling Contracts
Overview
Starknet Foundry sncast
supports calling smart contracts on a given network with the sncast call
command.
The basic inputs that you need for this command are:
- Contract address
- Function name
- Inputs to the function
For a detailed CLI description, see the call command reference.
Examples
General Example
$ sncast \
--url http://127.0.0.1:5050 \
call \
--contract-address 0x4a739ab73aa3cac01f9da5d55f49fb67baee4919224454a2e3f85b16462a911 \
--function "some_function" \
--calldata 1 2 3
command: call
response: [0x1, 0x23, 0x4]
📝 Note Call does not require passing account-connected parameters (
account
andaccounts-file
) because it doesn't create a transaction.
Passing block-id
Argument
You can call a contract at the specific block by passing --block-id
argument.
$ sncast call \
--contract-address 0x4a739ab73aa3cac01f9da5d55f49fb67baee4919224454a2e3f85b16462a911 \
--function "some_function" \
--calldata 1 2 3 \
--block-id 1234
command: call
response: [0x1, 0x23]
Performing Multicall
Overview
Starknet Foundry sncast
supports executing multiple deployments or calls with the sncast multicall run
command.
📝 Note
sncast multicall run
executes only one transaction containing all the prepared calls. Which means the fee is paid once.
You need to provide a path to a .toml
file with declarations of desired operations that you want to execute.
You can also compose such config .toml
file with the sncast multicall new
command.
For a detailed CLI description, see the multicall command reference.
Example
multicall run
Example
Example file:
[[call]]
call_type = "deploy"
class_hash = "0x076e94149fc55e7ad9c5fe3b9af570970ae2cf51205f8452f39753e9497fe849"
inputs = []
id = "map_contract"
unique = false
[[call]]
call_type = "invoke"
contract_address = "map_contract"
function = "put"
inputs = ["0x123", "234"]
After running sncast multicall run --path file.toml --fee-token strk
, a declared contract will be first deployed, and then its function put
will be invoked.
📝 Note The example above demonstrates the use of the
id
property in a deploy call, which is then referenced as thecontract address
in an invoke call. Additionally, theid
can be referenced in the inputs of deploy and invoke calls 🔥
$ sncast multicall run --path /Users/john/Desktop/multicall_example.toml --fee-token strk
command: multicall
transaction_hash: 0x38fb8a0432f71bf2dae746a1b4f159a75a862e253002b48599c9611fa271dcb
💡 Info Max fee will be automatically computed if
--max-fee <MAX_FEE>
is not passed.
💡 Info You can also choose to pay in Ether by setting
--fee-token
toeth
.
multicall new
Example
You can also generate multicall template with multicall new
command, specifying output path.
$ sncast multicall new ./template.toml
Multicall template successfully saved in ./template.toml
Resulting in output:
[[call]]
call_type = "deploy"
class_hash = ""
inputs = []
id = ""
unique = false
[[call]]
call_type = "invoke"
contract_address = ""
function = ""
inputs = []
⚠️ Warning Trying to pass any existing file as an output for
multicall new
will result in error, as the command doesn't overwrite by default.
multicall new
With overwrite
Argument
If there is a file with the same name as provided, it can be overwritten.
$ sncast multicall new ./template.toml --overwrite
Multicall template successfully saved in ./new_multicall_template.toml
Cairo Deployment Scripts
Overview
⚠️⚠️⚠️ Highly experimental code, a subject to change ⚠️⚠️⚠️
Starknet Foundry cast can be used to run deployment scripts written in Cairo, using script run
subcommand.
It aims to provide similar functionality to Foundry's forge script
.
To start writing a deployment script in Cairo just add sncast_std
as a dependency to you scarb package and make sure to
have a main
function in the module you want to run. sncast_std
docs can be found here.
Please note that sncast script
is in development. While it is already possible to declare, deploy, invoke and call
contracts from within Cairo, its interface, internals and feature set can change rapidly each version.
⚠️⚠️ By default, the nonce for each transaction is being taken from the pending block ⚠️⚠️
Some RPC nodes can be configured with higher poll intervals, which means they may return "older" nonces in pending blocks, or even not be able to obtain pending blocks at all. This might be the case if you get an error like "Invalid transaction nonce" when running a script, and you may need to manually set both nonce and max_fee for transactions.
Example:
let declare_result = declare( "Map", FeeSettings::Eth(EthFeeSettings { max_fee: Option::Some(max_fee) }), Option::Some(nonce) ) .expect('declare failed');
Some of the planned features that will be included in future versions are:
- dispatchers support
- logging
- account creation/deployment
- multicall support
- dry running the scripts
and more!
State file
By default, when you run a script a state file containing information about previous runs will be created. This file can later be used to skip making changes to the network if they were done previously.
To determine if an operation (a function like declare, deploy or invoke) has to be sent to the network, the script will first check if such operation with given arguments already exists in state file. If it does, and previously ended with a success, its execution will be skipped. Otherwise, sncast will attempt to execute this function, and will write its status to the state file afterwards.
To prevent sncast from using the state file, you can set the --no-state-file flag.
A state file is typically named in a following manner:
{script name}_{network name}_state.json
Suggested directory structures
As sncast scripts are just regular scarb packages, there are multiple ways to incorporate scripts into your existing scarb workspace. Most common directory structures include:
1. scripts
directory with all the scripts in the same workspace with cairo contracts (default for sncast script init
)
$ tree
.
├── scripts
│ └── my_script
│ ├── Scarb.toml
│ └── src
│ ├── my_script.cairo
│ └── lib.cairo
├── src
│ ├── my_contract.cairo
│ └── lib.cairo
└── Scarb.toml
📝 Note You should add
scripts
tomembers
field in your top-level Scarb.toml to be able to run the script from anywhere in the workspace - otherwise you will have to run the script from within its directory. To learn more consult Scarb documentation.
You can also have multiple scripts as separate packages, or multiple modules inside one package, like so:
1a. multiple scripts in one package
$ tree
.
├── scripts
│ └── my_script
│ ├── Scarb.toml
│ └── src
│ ├── my_script1.cairo
│ ├── my_script2.cairo
│ └── lib.cairo
├── src
│ ├── my_contract.cairo
│ └── lib.cairo
└── Scarb.toml
1b. multiple scripts as separate packages
$ tree
.
├── scripts
│ ├── Scarb.toml
│ ├── first_script
│ │ ├── Scarb.toml
│ │ └── src
│ │ ├── first_script.cairo
│ │ └── lib.cairo
│ └── second_script
│ ├── Scarb.toml
│ └── src
│ ├── second_script.cairo
│ └── lib.cairo
├── src
│ ├── my_contract.cairo
│ └── lib.cairo
└── Scarb.toml
1c. single script with flat directory structure
$ tree
.
├── Scarb.toml
├── scripts
│ ├── Scarb.toml
│ └── src
│ ├── my_script.cairo
│ └── lib.cairo
└── src
└── lib.cairo
2. scripts disjointed from the workspace with cairo contracts
$ tree
.
├── Scarb.toml
└── src
├── lib.cairo
└── my_script.cairo
In order to use this directory structure you must set any contracts you're using as dependencies in script's Scarb.toml,
and override build-external-contracts
property to build those contracts. To learn more consult Scarb documentation.
This setup can be seen in action in Full Example below.
Examples
Initialize a script
To get started, a deployment script with all required elements can be initialized using the following command:
$ sncast script init my_script
For more details, see init command.
📝 Note To include a newly created script in an existing workspace, it must be manually added to the members list in the
Scarb.toml
file, under the defined workspace. For more detailed information about workspaces, please refer to the Scarb documentation.
Minimal Example (Without Contract Deployment)
This example shows how to call an already deployed contract. Please find full example with contract deployment here.
use sncast_std::{invoke, call, CallResult};
fn main() {
let eth = 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7;
let addr = 0x0089496091c660345BaA480dF76c1A900e57cf34759A899eFd1EADb362b20DB5;
let call_result = call(eth.try_into().unwrap(), selector!("allowance"), array![addr, addr]).expect('call failed');
let call_result = *call_result.data[0];
assert(call_result == 0, call_result);
let call_result = call(eth.try_into().unwrap(), selector!("decimals"), array![]).expect('call failed');
let call_result = *call_result.data[0];
assert(call_result == 18, call_result);
}
The script should be included in a scarb package. The directory structure and config for this example looks like this:
$ tree
.
├── src
│ ├── my_script.cairo
│ └── lib.cairo
└── Scarb.toml
[package]
name = "my_script"
version = "0.1.0"
[dependencies]
starknet = ">=2.3.0"
sncast_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.22.0" }
To run the script, do:
$ sncast \
--url http://127.0.0.1:5050 \
script run my_script
command: script run
status: success
Full Example (With Contract Deployment)
This example script declares, deploys and interacts with an example map contract:
use sncast_std::{
declare, deploy, invoke, call, DeclareResult, DeployResult, InvokeResult, CallResult, get_nonce,
FeeSettings, EthFeeSettings
};
fn main() {
let max_fee = 99999999999999999;
let salt = 0x3;
let declare_nonce = get_nonce('latest');
let declare_result = declare(
"Map",
FeeSettings::Eth(EthFeeSettings { max_fee: Option::Some(max_fee) }),
Option::Some(declare_nonce)
)
.expect('map declare failed');
let class_hash = declare_result.class_hash;
let deploy_nonce = get_nonce('pending');
let deploy_result = deploy(
class_hash,
ArrayTrait::new(),
Option::Some(salt),
true,
FeeSettings::Eth(EthFeeSettings { max_fee: Option::Some(max_fee) }),
Option::Some(deploy_nonce)
)
.expect('map deploy failed');
assert(deploy_result.transaction_hash != 0, deploy_result.transaction_hash);
let invoke_nonce = get_nonce('pending');
let invoke_result = invoke(
deploy_result.contract_address,
selector!("put"),
array![0x1, 0x2],
FeeSettings::Eth(EthFeeSettings { max_fee: Option::Some(max_fee) }),
Option::Some(invoke_nonce)
)
.expect('map invoke failed');
assert(invoke_result.transaction_hash != 0, invoke_result.transaction_hash);
let call_result = call(deploy_result.contract_address, selector!("get"), array![0x1])
.expect('map call failed');
assert(call_result.data == array![0x2], *call_result.data.at(0));
}
The script should be included in a scarb package. The directory structure and config for this example looks like this:
$ tree
.
├── contracts
│ ├── Scarb.toml
│ └── src
│ └── lib.cairo
└── scripts
├── Scarb.toml
└── src
├── lib.cairo
└── map_script.cairo
[package]
name = "map_script"
version = "0.1.0"
[dependencies]
starknet = ">=2.3.0"
sncast_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.27.0" }
map = { path = "../contracts" }
[lib]
sierra = true
casm = true
[[target.starknet-contract]]
build-external-contracts = [
"map::Map"
]
Please note that map
contract was specified as the dependency. In our example, it resides in the filesystem. To generate the artifacts for it that will be accessible from the script you need to use the build-external-contracts
property.
To run the script, do:
$ sncast \
--url http://127.0.0.1:5050 \
--account example_user \
script run map_script
Class hash of the declared contract: 685896493695476540388232336434993540241192267040651919145140488413686992233
...
Deployed the contract to address: 2993684914933159551622723238457226804366654523161908704282792530334498925876
...
Invoke tx hash is: 2455538849277152825594824366964313930331085452149746033747086127466991639149
Call result: [2]
command: script run
status: success
As an idempotency feature is turned on by default, executing the same script once again ends with a success
and only call
functions are being executed (as they do not change the network state):
$ sncast \
--url http://127.0.0.1:5050 \
--account example_user \
script run map_script
Class hash of the declared contract: 1922774777685257258886771026518018305931014651657879651971507142160195873652
Deployed the contract to address: 3478557462226312644848472512920965457566154264259286784215363579593349825684
Invoke tx hash is: 1373185562410761200747829131886166680837022579434823960660735040169785115611
Call result: [2]
command: script run
status: success
whereas, when we run the same script once again with --no-state-file
flag set, it fails (as the Map
contract is already declared):
$ sncast \
--url http://127.0.0.1:5050 \
--account example_user \
script run map_script --no-state-file
command: script run
message:
0x636f6e747261637420616c7265616479206465636c61726564 ('contract already declared')
status: script panicked
Error handling
Each of declare
, deploy
, invoke
, call
functions return Result<T, ScriptCommandError>
, where T
is a corresponding response struct.
This allows for various script errors to be handled programmatically.
Script errors implement Debug
trait, allowing the error to be printed to stdout.
Minimal example with assert and print
use sncast_std::{
get_nonce, declare, DeclareResult, ScriptCommandError, ProviderError, StarknetError,
FeeSettings, EthFeeSettings
};
fn main() {
let max_fee = 9999999999999999999999999999999999;
let declare_nonce = get_nonce('latest');
let declare_result = declare(
"Map",
FeeSettings::Eth(EthFeeSettings { max_fee: Option::Some(max_fee) }),
Option::Some(declare_nonce)
)
.unwrap_err();
println!("{:?}", declare_result);
assert(
ScriptCommandError::ProviderError(
ProviderError::StarknetError(StarknetError::InsufficientAccountBalance)
) == declare_result,
'ohno'
)
}
stdout:
...
ScriptCommandError::ProviderError(ProviderError::StarknetError(StarknetError::InsufficientAccountBalance(())))
command: script
status: success
Some errors may contain an error message in the form of ByteArray
Minimal example with an error msg:
use sncast_std::{call, CallResult, ScriptCommandError, ProviderError, StarknetError, ErrorData};
fn main() {
let eth = 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7.try_into().expect('bad address');
let call_err: ScriptCommandError = call(
eth, selector!("gimme_money"), array![]
)
.unwrap_err();
println!("{:?}", call_err);
}
stdout:
...
ScriptCommandError::ProviderError(ProviderError::StarknetError(StarknetError::ContractError(ErrorData { msg: "Entry point EntryPointSelector(StarkFelt( ... )) not found in contract." })))
command: script
status: success
More on deployment scripts errors here.
Inspecting Transactions
Overview
Starknet Foundry sncast
supports the inspection of transaction statuses on a given network with the sncast tx-status
command.
For a detailed CLI description, refer to the tx-status command reference.
Usage Examples
Inspecting Transaction Status
You can track the details about the execution and finality status of a transaction in the given network by using the transaction hash as shown below:
$ sncast \
--url http://127.0.0.1:5050 \
tx-status \
0x07d2067cd7675f88493a9d773b456c8d941457ecc2f6201d2fe6b0607daadfd1
command: tx-status
execution_status: Succeeded
finality_status: AcceptedOnL2
Fees
and versions
Historically, fees for transactions on Starknet had to be paid exclusively with ETH. However, with the rollout of v3 transactions, users now have the additional option to pay these fees using STRK.
💡 Info V3 transactions have additional options, that give you more control over transaction fee. You can specify the maximum gas unit price and the maximum gas for the transaction. This is done using the
--max-gas
and--max-gas-unit-price
flags.
Cast allows you to specify either the version of the transaction you want to send or the fee token you want to pay the fees in. This is done using
the --version
and --fee-token
flags.
💡 Info Don't worry if you're not sure which version to use, it will be inferred automatically based on the fee token you provide. The same goes for the fee token, if you provide a version, the fee token will be inferred.
sncast account deploy
When deploying an account, you can specify the version of the transaction and the fee token to use. The table below shows which token is used for which version of the transaction:
Version | Fee Token |
---|---|
v1 | eth |
v3 | strk |
When paying in STRK, you need to either set --fee-token
to strk
:
$ sncast account deploy \
--name some-name \
--fee-token strk \
--max-fee 9999999999999
or set --version
to v3
:
$ sncast account deploy \
--name some-name \
--version v3 \
--max-fee 9999999999999
In case of paying in ETH, same rules apply. You need to set either --fee-token
to eth
:
$ sncast account deploy \
--name some-name \
--fee-token eth \
--max-fee 9999999999999
or set --version
to v1
:
$ sncast account deploy \
--name some-name \
--version v1 \
--max-fee 9999999999999
📝 Note The unit used in
--max-fee
flag is the smallest unit of the given fee token. For ETH it is Wei, for STRK it is Fri.
sncast deploy
Currently, there are two versions of the deployment transaction: v1 and v3. The table below shows which token is used for which version of the transaction:
Version | Fee Token |
---|---|
v1 | eth |
v3 | strk |
sncast declare
Currently, there are two versions of the declare transaction: v2 and v3. The table below shows which token is used for which version of the transaction:
Version | Fee Token |
---|---|
v2 | eth |
v3 | strk |
sncast invoke and sncast multicall run
Currently, there are two versions of invoke transaction: v1 and v3. The table below shows which token is used for which version of the transaction:
Version | Fee Token |
---|---|
v1 | eth |
v3 | strk |
Verifying Contracts
Overview
Starknet Foundry sncast
supports verifying Cairo contract classes with the sncast verify
command by submitting the source code to a selected verification provider. Verification provides transparency, making the code accessible to users and aiding debugging tools.
The verification provider guarantees that the submitted source code aligns with the deployed contract class on the network by compiling the source code into Sierra bytecode and comparing it with the network-deployed Sierra bytecode.
For detailed CLI description, see verify command reference.
⚠️ Warning Please be aware that submitting the source code means it will be publicly exposed through the provider's APIs.
Verification Providers
Walnut
Walnut is a tool for step-by-step debugging of Starknet transactions. You can learn more about Walnut here walnut.dev. Note that Walnut requires you to specify the Starknet version in your Scarb.toml
config file.
Example
First, ensure that you have created a Scarb.toml
file for your contract (it should be present in the project directory or one of its parent directories). Make sure the contract has already been deployed on the network.
Then run:
$ sncast --url http://127.0.0.1:5050/rpc \
verify \
--contract-address 0x8448a68b5ea1affc45e3fd4b8b480ea36a51dc34e337a16d2567d32d0c6f8b \
--contract-name SimpleBalance \
--verifier walnut \
--network mainnet
You are about to submit the entire workspace's code to the third-party chosen verifier at walnut, and the code will be publicly available through walnut's APIs. Are you sure? (Y/n) Y
command: verify
message: Contract has been successfully verified. You can check the verification status at the following link: https://api.walnut.dev/v1/sn_main/classes/0x03498e7edbc5f953315db118401fe7ea1eef637f63c56b45bd54e35150929ca3
📝 Note Contract name is a part after the
mod
keyword in your contract file. It may differ from package name defined inScarb.toml
file.
Environment Setup
💡 Info This tutorial is only relevant if you wish to contribute to Starknet Foundry. If you plan to only use it as a tool for your project, you can skip this part.
Prerequisites
Rust
Install the latest stable Rust version. If you already have Rust installed make sure to upgrade it by running
$ rustup update
Scarb
You can read more about installing Scarb here
Please make sure you're using Scarb installed via asdf - otherwise some tests may fail.
To verify, run:
$ which scarb
the result of which should be:
$HOME/.asdf/shims/scarb
If you previously installed scarb using official installer, you may need to remove this installation or modify your PATH to make sure asdf installed one is always used.
❗️ Warning
If you haven't pushed your branch to the remote yet (you've been working only locally), two tests will fail:
e2e::running::init_new_project_test
e2e::running::simple_package_with_git_dependency
After pushing the branch to the remote, those tests should pass.
Starknet Devnet
To install it run ./scripts/install_devnet.sh
Universal sierra compiler
Install the latest universal-sierra-compiler version.
Running Tests
Tests can be run with:
$ cargo test
Formatting and Lints
Starknet Foundry uses rustfmt for formatting. You can run the formatter with
$ cargo fmt
For linting, it uses clippy. You can run it with this command:
$ cargo clippy --all-targets --all-features -- --no-deps -W clippy::pedantic -A clippy::missing_errors_doc -A clippy::missing_panics_doc -A clippy::default_trait_access
Or using our defined alias
$ cargo lint
Spelling
Starknet Foundry uses typos for spelling checks.
You can run the checker with
$ typos
Some typos can be automatically fixed by running
$ typos -w
Contributing
Read the general contribution guideline here
snforge
CLI Reference
You can check your version of snforge
via snforge --version
.
To display help run snforge --help
.
snforge test
Run tests for a project in the current directory.
[TEST_FILTER]
Passing a test filter will only run tests with an absolute module tree path containing this filter.
-e
, --exact
Will only run a test with a name exactly matching the test filter.
Test filter must be a whole qualified test name e.g. package_name::my_test
instead of just my_test
.
-x
, --exit-first
Stop executing tests after the first failed test.
-p
, --package <SPEC>
Packages to run this command on, can be a concrete package name (foobar
) or a prefix glob (foo*
).
-w
, --workspace
Run tests for all packages in the workspace.
-r
, --fuzzer-runs
<FUZZER_RUNS>
Number of fuzzer runs.
-s
, --fuzzer-seed
<FUZZER_SEED>
Seed for the fuzzer.
--ignored
Run only tests marked with #[ignore]
attribute.
--include-ignored
Run all tests regardless of #[ignore]
attribute.
--rerun-failed
Run tests that failed during the last run
--color
<WHEN>
Control when colored output is used. Valid values:
auto
(default): automatically detect if color support is available on the terminal.always
: always display colors.never
: never display colors.
--detailed-resources
Display additional info about used resources for passed tests.
--save-trace-data
Saves execution traces of test cases which pass and are not fuzz tests. You can use traces for profiling purposes.
--build-profile
Saves trace data and then builds profiles of test cases which pass and are not fuzz tests.
You need cairo-profiler installed on your system. You can set a custom path to cairo-profiler with CAIRO_PROFILER
env variable. Profile can be read with pprof, more information: cairo-profiler, pprof
--max-n-steps
<MAX_N_STEPS>
Number of maximum steps during a single test. For fuzz tests this value is applied to each subtest separately.
-h
, --help
Print help.
snforge init
Create a new directory with a snforge
project.
<NAME>
Name of a new project.
-h
, --help
Print help.
snforge clean-cache
Clean snforge
cache directory.
-h
, --help
Print help.
Cheatcodes Reference
-
mock_call
- mocks a number of contract calls to an entry point -
start_mock_call
- mocks contract call to an entry point -
stop_mock_call
- cancels themock_call
/start_mock_call
for an entry point -
get_class_hash
- retrieves a class hash of a contract -
replace_bytecode
- replace the class hash of a contract -
l1_handler
- executes a#[l1_handler]
function to mock a message arriving from Ethereum -
spy_events
- createsEventSpy
instance which spies on events emitted by contracts -
spy_messages_to_l1
- createsL1MessageSpy
instance which spies on messages to L1 sent by contracts -
store
- stores values in targeted contact's storage -
load
- loads values directly from targeted contact's storage -
CheatSpan
- enum for specifying the number of target calls for a cheat
Execution Info
Caller Address
cheat_caller_address
- changes the caller address for contracts, for a number of callsstart_cheat_caller_address_global
- changes the caller address for all contractsstart_cheat_caller_address
- changes the caller address for contractsstop_cheat_caller_address
- cancels thecheat_caller_address
/start_cheat_caller_address
for contractsstop_cheat_caller_address_global
- cancels thestart_cheat_caller_address_global
Block Info
Block Number
cheat_block_number
- changes the block number for contracts, for a number of callsstart_cheat_block_number_global
- changes the block number for all contractsstart_cheat_block_number
- changes the block number for contractsstop_cheat_block_number
- cancels thecheat_block_number
/start_cheat_block_number
for contractsstop_cheat_block_number_global
- cancels thestart_cheat_block_number_global
Block Timestamp
cheat_block_timestamp
- changes the block timestamp for contracts, for a number of callsstart_cheat_block_timestamp_global
- changes the block timestamp for all contractsstart_cheat_block_timestamp
- changes the block timestamp for contractsstop_cheat_block_timestamp
- cancels thecheat_block_timestamp
/start_cheat_block_timestamp
for contractsstop_cheat_block_timestamp_global
- cancels thestart_cheat_block_timestamp_global
Sequencer Address
cheat_sequencer_address
- changes the sequencer address for contracts, for a number of callsstart_cheat_sequencer_address_global
- changes the sequencer address for all contractsstart_cheat_sequencer_address
- changes the sequencer address for contractsstop_cheat_sequencer_address
- cancels thecheat_sequencer_address
/start_cheat_sequencer_address
for contractsstop_cheat_sequencer_address_global
- cancels thestart_cheat_sequencer_address_global
Transaction Info
Transaction Version
cheat_transaction_version
- changes the transaction version for contracts, for a number of callsstart_cheat_transaction_version_global
- changes the transaction version for all contractsstart_cheat_transaction_version
- changes the transaction version for contractsstop_cheat_transaction_version
- cancels thecheat_transaction_version
/start_cheat_transaction_version
for contractsstop_cheat_transaction_version_global
- cancels thestart_cheat_transaction_version_global
Transaction Max Fee
cheat_max_fee
- changes the transaction max fee for contracts, for a number of callsstart_cheat_max_fee_global
- changes the transaction max fee for all contractsstart_cheat_max_fee
- changes the transaction max fee for contractsstop_cheat_max_fee
- cancels thecheat_max_fee
/start_cheat_max_fee
for contractsstop_cheat_max_fee_global
- cancels thestart_cheat_max_fee_global
Transaction Signature
cheat_signature
- changes the transaction signature for contracts, for a number of callsstart_cheat_signature_global
- changes the transaction signature for all contractsstart_cheat_signature
- changes the transaction signature for contractsstop_cheat_signature
- cancels thecheat_signature
/start_cheat_signature
for contractsstop_cheat_signature_global
- cancels thestart_cheat_signature_global
Transaction Hash
cheat_transaction_hash
- changes the transaction hash for contracts, for a number of callsstart_cheat_transaction_hash_global
- changes the transaction hash for all contractsstart_cheat_transaction_hash
- changes the transaction hash for contractsstop_cheat_transaction_hash
- cancels thecheat_transaction_hash
/start_cheat_transaction_hash
for contractsstop_cheat_transaction_hash_global
- cancels thestart_cheat_transaction_hash_global
Transaction Chain ID
cheat_chain_id
- changes the transaction chain_id for contracts, for a number of callsstart_cheat_chain_id_global
- changes the transaction chain_id for all contractsstart_cheat_chain_id
- changes the transaction chain_id for contractsstop_cheat_chain_id
- cancels thecheat_chain_id
/start_cheat_chain_id
for contractsstop_cheat_chain_id_global
- cancels thestart_cheat_chain_id_global
Transaction Nonce
cheat_nonce
- changes the transaction nonce for contracts, for a number of callsstart_cheat_nonce_global
- changes the transaction nonce for all contractsstart_cheat_nonce
- changes the transaction nonce for contractsstop_cheat_nonce
- cancels thecheat_nonce
/start_cheat_nonce
for contractsstop_cheat_nonce_global
- cancels thestart_cheat_nonce_global
Transaction Resource Bounds
cheat_resource_bounds
- changes the transaction resource bounds for contracts, for a number of callsstart_cheat_resource_bounds_global
- changes the transaction resource bounds for all contractsstart_cheat_resource_bounds
- changes the transaction resource bounds for contractsstop_cheat_resource_bounds
- cancels thecheat_resource_bounds
/start_cheat_resource_bounds
for contractsstop_cheat_resource_bounds_global
- cancels thestart_cheat_resource_bounds_global
Transaction Tip
cheat_tip
- changes the transaction tip for contracts, for a number of callsstart_cheat_tip_global
- changes the transaction tip for all contractsstart_cheat_tip
- changes the transaction tip for contractsstop_cheat_tip
- cancels thecheat_tip
/start_cheat_tip
for contractsstop_cheat_tip_global
- cancels thestart_cheat_tip_global
Transaction Paymaster Data
cheat_paymaster_data
- changes the transaction paymaster data for contracts, for a number of callsstart_cheat_paymaster_data_global
- changes the transaction paymaster data for all contractsstart_cheat_paymaster_data
- changes the transaction paymaster data for contractsstop_cheat_paymaster_data
- cancels thecheat_paymaster_data
/start_cheat_paymaster_data
for contractsstop_cheat_paymaster_data_global
- cancels thestart_cheat_paymaster_data_global
Transaction Nonce Data Availability Mode
cheat_nonce_data_availability_mode
- changes the transaction nonce data availability mode for contracts, for a number of callsstart_cheat_nonce_data_availability_mode_global
- changes the transaction nonce data availability mode for all contractsstart_cheat_nonce_data_availability_mode
- changes the transaction nonce data availability mode for contractsstop_cheat_nonce_data_availability_mode
- cancels thecheat_nonce_data_availability_mode
/start_cheat_nonce_data_availability_mode
for contractsstop_cheat_nonce_data_availability_mode_global
- cancels thestart_cheat_nonce_data_availability_mode_global
Transaction Fee Data Availability Mode
cheat_fee_data_availability_mode
- changes the transaction fee data availability mode for contracts, for a number of callsstart_cheat_fee_data_availability_mode_global
- changes the transaction fee data availability mode for all contractsstart_cheat_fee_data_availability_mode
- changes the transaction fee data availability mode for contractsstop_cheat_fee_data_availability_mode
- cancels thecheat_fee_data_availability_mode
/start_cheat_fee_data_availability_mode
for contractsstop_cheat_fee_data_availability_mode_global
- cancels thestart_cheat_fee_data_availability_mode_global
Transaction Account Deployment
cheat_account_deployment_data
- changes the transaction account deployment data for contracts, for a number of callsstart_cheat_account_deployment_data_global
- changes the transaction account deployment data for all contractsstart_cheat_account_deployment_data
- changes the transaction account deployment data for contractsstop_cheat_account_deployment_data
- cancels thecheat_account_deployment_data
/start_cheat_account_deployment_data
for contractsstop_cheat_account_deployment_data_global
- cancels thestart_cheat_account_deployment_data_global
Account Contract Address
cheat_account_contract_address
- changes the address of an account which the transaction originates from, for the given target and spanstart_cheat_account_contract_address_global
- changes the address of an account which the transaction originates from, for all targetsstart_cheat_account_contract_address
- changes the address of an account which the transaction originates from, for the given targetstop_cheat_account_contract_address
- cancels thecheat_account_deployment_data
/start_cheat_account_deployment_data
for the given targetstop_cheat_account_contract_address_global
- cancels thestart_cheat_account_contract_address_global
ℹ️ Info To use cheatcodes you need to add
snforge_std
package as a development dependency in yourScarb.toml
using appropriate release tag.[dev-dependencies] snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.12.0" }
CheatSpan
enum CheatSpan {
Indefinite: (),
TargetCalls: usize
}
CheatSpan
is an enum used to specify for how long the target should be cheated for.
Indefinite
applies the cheatcode indefinitely, until the cheat is canceled manually (e.g. usingstop_cheat_block_timestamp
).TargetCalls
applies the cheatcode for a specified number of calls to the target, after which the cheat is canceled (or until the cheat is canceled manually).
caller_address
Cheatcodes modifying caller_address
:
cheat_caller_address
fn cheat_caller_address(target: ContractAddress, caller_address: ContractAddress, span: CheatSpan)
Changes the caller address for the given target and span.
start_cheat_caller_address_global
fn start_cheat_caller_address_global(caller_address: ContractAddress)
Changes the caller address for all targets.
start_cheat_caller_address
fn start_cheat_caller_address(target: ContractAddress, caller_address: ContractAddress)
Changes the caller address for the given target.
stop_cheat_caller_address
fn stop_cheat_caller_address(target: ContractAddress)
Cancels the cheat_caller_address
/ start_cheat_caller_address
for the given target.
stop_cheat_caller_address_global
fn stop_cheat_caller_address_global(target: ContractAddress)
Cancels the start_cheat_caller_address_global
.
block_number
Cheatcodes modifying block_number
:
cheat_block_number
fn cheat_block_number(target: ContractAddress, block_number: u64, span: CheatSpan)
Changes the block number for the given target and span.
start_cheat_block_number_global
fn start_cheat_block_number_global(block_number: u64)
Changes the block number for all targets.
start_cheat_block_number
fn start_cheat_block_number(target: ContractAddress, block_number: u64)
Changes the block number for the given target.
stop_cheat_block_number
fn stop_cheat_block_number(target: ContractAddress)
Cancels the cheat_block_number
/ start_cheat_block_number
for the given target.
stop_cheat_block_number_global
fn stop_cheat_block_number_global(target: ContractAddress)
Cancels the start_cheat_block_number_global
.
block_timestamp
Cheatcodes modifying block_timestamp
:
cheat_block_timestamp
fn cheat_block_timestamp(target: ContractAddress, block_timestamp: u64, span: CheatSpan)
Changes the block timestamp for the given target and span.
start_cheat_block_timestamp_global
fn start_cheat_block_timestamp_global(block_timestamp: u64)
Changes the block timestamp for all targets.
start_cheat_block_timestamp
fn start_cheat_block_timestamp(target: ContractAddress, block_timestamp: u64)
Changes the block timestamp for the given target.
stop_cheat_block_timestamp
fn stop_cheat_block_timestamp(target: ContractAddress)
Cancels the cheat_block_timestamp
/ start_cheat_block_timestamp
for the given target.
stop_cheat_block_timestamp_global
fn stop_cheat_block_timestamp_global(target: ContractAddress)
Cancels the start_cheat_block_timestamp_global
.
sequencer_address
Cheatcodes modifying sequencer_address
:
cheat_sequencer_address
fn cheat_sequencer_address(target: ContractAddress, sequencer_address: ContractAddress, span: CheatSpan)
Changes the sequencer address for the given target and span.
start_cheat_sequencer_address_global
fn start_cheat_sequencer_address_global(sequencer_address: ContractAddress)
Changes the sequencer address for all targets.
start_cheat_sequencer_address
fn start_cheat_sequencer_address(target: ContractAddress, sequencer_address: ContractAddress)
Changes the sequencer address for the given target.
stop_cheat_sequencer_address
fn stop_cheat_sequencer_address(target: ContractAddress)
Cancels the cheat_sequencer_address
/ start_cheat_sequencer_address
for the given target.
stop_cheat_sequencer_address_global
fn stop_cheat_sequencer_address_global(target: ContractAddress)
Cancels the start_cheat_sequencer_address_global
.
Transaction version
Cheatcodes modifying transaction version
:
cheat_transaction_version
fn cheat_transaction_version(target: ContractAddress, version: felt252, span: CheatSpan)
Changes the transaction version for the given target and span.
start_cheat_transaction_version_global
fn start_cheat_transaction_version_global(version: felt252)
Changes the transaction version for all targets.
start_cheat_transaction_version
fn start_cheat_transaction_version(target: ContractAddress, version: felt252)
Changes the transaction version for the given target.
stop_cheat_transaction_version
fn stop_cheat_transaction_version(target: ContractAddress)
Cancels the cheat_transaction_version
/ start_cheat_transaction_version
for the given target.
stop_cheat_transaction_version_global
fn stop_cheat_transaction_version_global(target: ContractAddress)
Cancels the start_cheat_transaction_version_global
.
account_contract_address
Cheatcodes modifying account_contract_address
:
cheat_account_contract_address
fn cheat_account_contract_address(target: ContractAddress, account_contract_address: ContractAddress, span: CheatSpan)
Changes the address of an account which the transaction originates from, for the given target and span.
start_cheat_account_contract_address_global
fn start_cheat_account_contract_address_global(account_contract_address: ContractAddress)
Changes the address of an account which the transaction originates from, for all targets.
start_cheat_account_contract_address
fn start_cheat_account_contract_address(target: ContractAddress, account_contract_address: ContractAddress)
Changes the address of an account which the transaction originates from, for the given target.
stop_cheat_account_contract_address
fn stop_cheat_account_contract_address(target: ContractAddress)
Cancels the cheat_account_contract_address
/ start_cheat_account_contract_address
for the given target.
stop_cheat_account_contract_address_global
fn stop_cheat_account_contract_address_global(target: ContractAddress)
Cancels the start_cheat_account_contract_address_global
.
max_fee
Cheatcodes modifying max_fee
:
cheat_max_fee
fn cheat_max_fee(target: ContractAddress, max_fee: u128, span: CheatSpan)
Changes the transaction max fee for the given target and span.
start_cheat_max_fee_global
fn start_cheat_max_fee_global(max_fee: u128)
Changes the transaction max fee for all targets.
start_cheat_max_fee
fn start_cheat_max_fee(target: ContractAddress, max_fee: u128)
Changes the transaction max fee for the given target.
stop_cheat_max_fee
fn stop_cheat_max_fee(target: ContractAddress)
Cancels the cheat_max_fee
/ start_cheat_max_fee
for the given target.
stop_cheat_max_fee_global
fn stop_cheat_max_fee_global(target: ContractAddress)
Cancels the start_cheat_max_fee_global
.
signature
Cheatcodes modifying signature
:
cheat_signature
fn cheat_signature(target: ContractAddress, signature: Span<felt252>, span: CheatSpan)
Changes the transaction signature for the given target and span.
start_cheat_signature_global
fn start_cheat_signature_global(signature: Span<felt252>)
Changes the transaction signature for all targets.
start_cheat_signature
fn start_cheat_signature(target: ContractAddress, signature: Span<felt252>)
Changes the transaction signature for the given target.
stop_cheat_signature
fn stop_cheat_signature(target: ContractAddress)
Cancels the cheat_signature
/ start_cheat_signature
for the given target.
stop_cheat_signature_global
fn stop_cheat_signature_global(target: ContractAddress)
Cancels the start_cheat_signature_global
.
transaction_hash
Cheatcodes modifying transaction_hash
:
cheat_transaction_hash
fn cheat_transaction_hash(target: ContractAddress, transaction_hash: felt252, span: CheatSpan)
Changes the transaction hash for the given target and span.
start_cheat_transaction_hash_global
fn start_cheat_transaction_hash_global(transaction_hash: felt252)
Changes the transaction hash for all targets.
start_cheat_transaction_hash
fn start_cheat_transaction_hash(target: ContractAddress, transaction_hash: felt252)
Changes the transaction hash for the given target.
stop_cheat_transaction_hash
fn stop_cheat_transaction_hash(target: ContractAddress)
Cancels the cheat_transaction_hash
/ start_cheat_transaction_hash
for the given target.
stop_cheat_transaction_hash_global
fn stop_cheat_transaction_hash_global(target: ContractAddress)
Cancels the start_cheat_transaction_hash_global
.
chain_id
Cheatcodes modifying chain_id
:
cheat_chain_id
fn cheat_chain_id(target: ContractAddress, chain_id: felt252, span: CheatSpan)
Changes the transaction chain_id for the given target and span.
start_cheat_chain_id_global
fn start_cheat_chain_id_global(chain_id: felt252)
Changes the transaction chain_id for all targets.
start_cheat_chain_id
fn start_cheat_chain_id(target: ContractAddress, chain_id: felt252)
Changes the transaction chain_id for the given target.
stop_cheat_chain_id
fn stop_cheat_chain_id(target: ContractAddress)
Cancels the cheat_chain_id
/ start_cheat_chain_id
for the given target.
stop_cheat_chain_id_global
fn stop_cheat_chain_id_global(target: ContractAddress)
Cancels the start_cheat_chain_id_global
.
nonce
Cheatcodes modifying nonce
:
cheat_nonce
fn cheat_nonce(target: ContractAddress, nonce: felt252, span: CheatSpan)
Changes the transaction nonce for the given target and span.
start_cheat_nonce_global
fn start_cheat_nonce_global(nonce: felt252)
Changes the transaction nonce for all targets.
start_cheat_nonce
fn start_cheat_nonce(target: ContractAddress, nonce: felt252)
Changes the transaction nonce for the given target.
stop_cheat_nonce
fn stop_cheat_nonce(target: ContractAddress)
Cancels the cheat_nonce
/ start_cheat_nonce
for the given target.
stop_cheat_nonce_global
fn stop_cheat_nonce_global(target: ContractAddress)
Cancels the start_cheat_nonce_global
.
resource_bounds
Cheatcodes modifying resource_bounds
:
cheat_resource_bounds
fn cheat_resource_bounds(target: ContractAddress, resource_bounds: Span<ResourceBounds>, span: CheatSpan)
Changes the transaction resource bounds for the given target and span.
start_cheat_resource_bounds_global
fn start_cheat_resource_bounds_global(resource_bounds: Span<ResourceBounds>)
Changes the transaction resource bounds for all targets.
start_cheat_resource_bounds
fn start_cheat_resource_bounds(target: ContractAddress, resource_bounds: Span<ResourceBounds>)
Changes the transaction resource bounds for the given target.
stop_cheat_resource_bounds
fn stop_cheat_resource_bounds(target: ContractAddress)
Cancels the cheat_resource_bounds
/ start_cheat_resource_bounds
for the given target.
stop_cheat_resource_bounds_global
fn stop_cheat_resource_bounds_global(target: ContractAddress)
Cancels the start_cheat_resource_bounds_global
.
tip
Cheatcodes modifying tip
:
cheat_tip
fn cheat_tip(target: ContractAddress, tip: u128, span: CheatSpan)
Changes the transaction tip for the given target and span.
start_cheat_tip_global
fn start_cheat_tip_global(tip: u128)
Changes the transaction tip for all targets.
start_cheat_tip
fn start_cheat_tip(target: ContractAddress, tip: u128)
Changes the transaction tip for the given target.
stop_cheat_tip
fn stop_cheat_tip(target: ContractAddress)
Cancels the cheat_tip
/ start_cheat_tip
for the given target.
stop_cheat_tip_global
fn stop_cheat_tip_global(target: ContractAddress)
Cancels the start_cheat_tip_global
.
paymaster_data
Cheatcodes modifying paymaster_data
:
cheat_paymaster_data
fn cheat_paymaster_data(target: ContractAddress, paymaster_data: Span<felt252>, span: CheatSpan)
Changes the transaction paymaster data for the given target and span.
start_cheat_paymaster_data_global
fn start_cheat_paymaster_data_global(paymaster_data: Span<felt252>)
Changes the transaction paymaster data for all targets.
start_cheat_paymaster_data
fn start_cheat_paymaster_data(target: ContractAddress, paymaster_data: Span<felt252>)
Changes the transaction paymaster data for the given target.
stop_cheat_paymaster_data
fn stop_cheat_paymaster_data(target: ContractAddress)
Cancels the cheat_paymaster_data
/ start_cheat_paymaster_data
for the given target.
stop_cheat_paymaster_data_global
fn stop_cheat_paymaster_data_global(target: ContractAddress)
Cancels the start_cheat_paymaster_data_global
.
nonce_data_availability_mode
Cheatcodes modifying nonce_data_availability_mode
:
cheat_nonce_data_availability_mode
fn cheat_nonce_data_availability_mode(target: ContractAddress, nonce_data_availability_mode: u32, span: CheatSpan)
Changes the transaction nonce data availability mode for the given target and span.
start_cheat_nonce_data_availability_mode_global
fn start_cheat_nonce_data_availability_mode_global(nonce_data_availability_mode: u32)
Changes the transaction nonce data availability mode for all targets.
start_cheat_nonce_data_availability_mode
fn start_cheat_nonce_data_availability_mode(target: ContractAddress, nonce_data_availability_mode: u32)
Changes the transaction nonce data availability mode for the given target.
stop_cheat_nonce_data_availability_mode
fn stop_cheat_nonce_data_availability_mode(target: ContractAddress)
Cancels the cheat_nonce_data_availability_mode
/ start_cheat_nonce_data_availability_mode
for the given target.
stop_cheat_nonce_data_availability_mode_global
fn stop_cheat_nonce_data_availability_mode_global(target: ContractAddress)
Cancels the start_cheat_nonce_data_availability_mode_global
.
fee_data_availability_mode
Cheatcodes modifying fee_data_availability_mode
:
cheat_fee_data_availability_mode
fn cheat_fee_data_availability_mode(target: ContractAddress, fee_data_availability_mode: u32, span: CheatSpan)
Changes the transaction fee data availability mode for the given target and span.
start_cheat_fee_data_availability_mode_global
fn start_cheat_fee_data_availability_mode_global(fee_data_availability_mode: u32)
Changes the transaction fee data availability mode for all targets.
start_cheat_fee_data_availability_mode
fn start_cheat_fee_data_availability_mode(target: ContractAddress, fee_data_availability_mode: u32)
Changes the transaction fee data availability mode for the given target.
stop_cheat_fee_data_availability_mode
fn stop_cheat_fee_data_availability_mode(target: ContractAddress)
Cancels the cheat_fee_data_availability_mode
/ start_cheat_fee_data_availability_mode
for the given target.
stop_cheat_fee_data_availability_mode_global
fn stop_cheat_fee_data_availability_mode_global(target: ContractAddress)
Cancels the start_cheat_fee_data_availability_mode_global
.
account_deployment_data
Cheatcodes modifying account_deployment_data
:
cheat_account_deployment_data
fn cheat_account_deployment_data(target: ContractAddress, account_deployment_data: Span<felt252>, span: CheatSpan)
Changes the transaction account deployment data for the given target and span.
start_cheat_account_deployment_data_global
fn start_cheat_account_deployment_data_global(account_deployment_data: Span<felt252>)
Changes the transaction account deployment data for all targets.
start_cheat_account_deployment_data
fn start_cheat_account_deployment_data(target: ContractAddress, account_deployment_data: Span<felt252>)
Changes the transaction account deployment data for the given target.
stop_cheat_account_deployment_data
fn stop_cheat_account_deployment_data(target: ContractAddress)
Cancels the cheat_account_deployment_data
/ start_cheat_account_deployment_data
for the given target.
stop_cheat_account_deployment_data_global
fn stop_cheat_account_deployment_data_global(target: ContractAddress)
Cancels the start_cheat_account_deployment_data_global
.
mock_call
Cheatcodes mocking contract entry point calls:
mock_call
fn mock_call<T, impl TSerde: serde::Serde<T>, impl TDestruct: Destruct<T>>( contract_address: ContractAddress, function_selector: felt252, ret_data: T, n_times: u32 )
Mocks contract call to a function_selector
of a contract at the given address, for n_times
first calls that are made
to the contract.
A call to function function_selector
will return data provided in ret_data
argument.
An address with no contract can be mocked as well.
An entrypoint that is not present on the deployed contract is also possible to mock.
Note that the function is not meant for mocking internal calls - it works only for contract entry points.
start_mock_call
fn start_mock_call<T, impl TSerde: serde::Serde<T>, impl TDestruct: Destruct<T>>( contract_address: ContractAddress, function_selector: felt252, ret_data: T )
Mocks contract call to a function_selector
of a contract at the given address, indefinitely.
See mock_call
for comprehensive definition of how it can be used.
stop_mock_call
fn stop_mock_call(contract_address: ContractAddress, function_selector: felt252)
Cancels the mock_call
/ start_mock_call
for the function function_selector
of a contract at the given address.
get_class_hash
fn get_class_hash(contract_address: ContractAddress) -> ClassHash
Returns a class hash of a contract at the specified address.
💡 Tip
This cheatcode can be used to test if your contract upgrade procedure is correct
replace_bytecode
fn replace_bytecode(contract: ContractAddress, new_class: ClassHash) -> Result<(), ReplaceBytecodeError>
Replaces class for given contract address.
The new_class
hash has to be declared in order for the replacement class to execute the code when interacting with the contract.
Returns Result::Ok
if the replacement succeeded, and a ReplaceBytecodeError
with appropriate error type otherwise
ReplaceBytecodeError
An enum with appropriate type of replacement failure
pub enum ReplaceBytecodeError {
/// Means that the contract does not exist, and thus bytecode cannot be replaced
ContractNotDeployed,
/// Means that the given class for replacement is not declared
UndeclaredClassHash,
}
l1_handler
fn new(target: ContractAddress, selector: felt252) -> L1Handler
Returns a structure referring to an L1 handler function.
fn execute(self: L1Handler) -> SyscallResult<()>
Mocks an L1 -> L2 message from Ethereum handled by the given L1 handler function.
spy_events
fn spy_events() -> EventSpy
Creates EventSpy
instance which spies on events emitted after its creation.
struct EventSpy {
...
}
An event spy structure.
struct Events {
events: Array<(ContractAddress, Event)>
}
A wrapper structure on an array of events to handle event filtering.
struct Event {
keys: Array<felt252>,
data: Array<felt252>
}
Raw event format (as seen via the RPC-API), can be used for asserting the emitted events.
Implemented traits
EventSpyTrait
trait EventSpyTrait {
fn get_events(ref self: EventSpy) -> Events;
}
Gets all events since the creation of the given EventSpy
.
EventSpyAssertionsTrait
trait EventSpyAssertionsTrait<T, impl TEvent: starknet::Event<T>, impl TDrop: Drop<T>> {
fn assert_emitted(ref self: EventSpy, events: @Array<(ContractAddress, T)>);
fn assert_not_emitted(ref self: EventSpy, events: @Array<(ContractAddress, T)>);
}
Allows to assert the expected events emission (or lack thereof), in the scope of the EventSpy
structure.
EventsFilterTrait
trait EventsFilterTrait {
fn emitted_by(self: @Events, contract_address: ContractAddress) -> Events;
}
Filters events emitted by a given ContractAddress
.
spy_messages_to_l1
fn spy_messages_to_l1() -> MessageToL1Spy
Creates MessageToL1Spy
instance that spies on all messages sent to L1 after its creation.
struct MessageToL1Spy {
// ..
}
Message spy structure allowing to get messages emitted only after its creation.
struct MessagesToL1 {
messages: Array<(ContractAddress, MessageToL1)>
}
A wrapper structure on an array of messages to handle filtering smoothly.
messages
is an array of (l2_sender_address, message)
tuples.
struct MessageToL1 {
/// An ethereum address where the message is destined to go
to_address: EthAddress,
/// Actual payload which will be delivered to L1 contract
payload: Array<felt252>
}
Raw message to L1 format (as seen via the RPC-API), can be used for asserting the sent messages.
Implemented traits
MessageToL1SpyTrait
trait MessageToL1SpyTrait {
/// Gets all messages given [`MessageToL1Spy`] spies for.
fn get_messages(ref self: MessageToL1Spy) -> MessagesToL1;
}
Gets all messages since the creation of the given MessageToL1Spy
.
MessageToL1SpyAssertionsTrait
trait MessageToL1SpyAssertionsTrait {
fn assert_sent(ref self: MessageToL1Spy, messages: @Array<(ContractAddress, MessageToL1)>);
fn assert_not_sent(ref self: MessageToL1Spy, messages: @Array<(ContractAddress, MessageToL1)>);
}
Allows to assert the expected sent messages (or lack thereof), in the scope of MessageToL1Spy
structure.
MessageToL1FilterTrait
trait MessageToL1FilterTrait {
/// Filter messages emitted by a sender of a given [`ContractAddress`]
fn sent_by(self: @MessagesToL1, contract_address: ContractAddress) -> MessagesToL1;
/// Filter messages emitted by a receiver of a given ethereum address
fn sent_to(self: @MessagesToL1, to_address: EthAddress) -> MessagesToL1;
}
Filters messages emitted by a given ContractAddress
, or sent to given EthAddress
.
store
fn store(target: ContractAddress, storage_address: felt252, serialized_value: Span<felt252>)
Stores felts from serialized_value
in target
contract's storage, starting at storage_address
.
load
fn load(target: ContractAddress, storage_address: felt252, size: felt252) -> Array<felt252>
Loads size
felts from target
contract's storage into an Array
, starting at storage_address
.
Library Functions References
declare
- declares a contract and returns aContractClass
which can be interacted with laterget_call_trace
- gets current test call trace (with contracts interactions included)fs
- module containing functions for interacting with the filesystemenv
- module containing functions for interacting with the system environmentsignature
- module containing struct and trait for creatingecdsa
signatures
ℹ️ Info To use cheatcodes you need to add
snforge_std
package as a development dependency in yourScarb.toml
using appropriate release tag.[dev-dependencies] snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.12.0" }
byte_array
Module
Module containing utilities for manipulating ByteArray
s.
Functions
fn try_deserialize_bytearray_error(x: Span<felt252>) -> Result<ByteArray, ByteArray>
This function is meant to transform a serialized output from a contract call into a ByteArray
.
Returns the parsed ByteArray
, or an Err
with reason, if the parsing failed.
declare
fn declare(contract: ByteArray) -> Result<ContractClass, Array<felt252>>
Declares a contract for later deployment.
See docs of ContractClass
for more info about the resulting struct.
ContractClass
A struct which enables interaction with given class hash.
It can be obtained by using declare, or created with an arbitrary ClassHash
.
struct ContractClass {
class_hash: ClassHash,
}
Implemented traits
ContractClassTrait
trait ContractClassTrait {
fn precalculate_address(
self: @ContractClass, constructor_calldata: @Array::<felt252>
) -> ContractAddress;
fn deploy(
self: @ContractClass, constructor_calldata: @Array::<felt252>
) -> SyscallResult<(ContractAddress, Span<felt252>)>;
fn deploy_at(
self: @ContractClass,
constructor_calldata: @Array::<felt252>,
contract_address: ContractAddress
) -> SyscallResult<(ContractAddress, Span<felt252>)>;
fn new<T, +Into<T, ClassHash>>(class_hash: T) -> ContractClass;
}
get_call_trace
fn get_call_trace() -> CallTrace;
(For whole structure definition, please refer
to snforge-std
source)
Gets current call trace of the test, up to the last call made to a contract.
The whole structure is represented as a tree of calls, in which each contract interaction is a new execution scope - thus resulting in a new nested trace.
📝 Note
The topmost-call is representing the test call, which will always be present if you're running a test.
Displaying the trace
The CallTrace
structure implements a Display
trait, for a pretty-print with indentations
println!("{}", get_call_trace());
fs
Module
Module containing functions for interacting with the filesystem.
File
trait FileTrait {
fn new(path: ByteArray) -> File;
}
FileParser<T>
trait FileParser<T, +Serde<T>> {
fn parse_txt(file: @File) -> Option<T>;
fn parse_json(file: @File) -> Option<T>;
}
read_txt
& read_json
fn read_txt(file: @File) -> Array<felt252>;
fn read_json(file: @File) -> Array<felt252>;
File format
Some rules have to be checked when providing a file for snforge, in order for correct parsing behavior. Different ones apply for JSON and plain text files.
Plain text files
- Elements have to be separated with newlines
- Elements have to be either:
- integers in range of
[0, P)
where P isCairo Prime
either in decimal or0x
prefixed hex format - single line short strings (
felt252
) of length<=31
surrounded by''
i.e.,'short string'
, new lines can be used with\n
and'
with\'
- single line strings (
ByteArray
) surrounded by""
i.e.,"very very very very loooooong string"
, new lines can be used with\n
and"
with\"
- integers in range of
JSON files
- Elements have to be either:
- integers in range of
[0, P)
where P isCairo Prime
- single line strings (
ByteArray
) i.e."very very very very loooooong string"
, new lines can be used with\n
and"
with\"
- array of integers or strings fulfilling the above conditions
- integers in range of
⚠️ Warning
A JSON object is an unordered data structure. To make reading JSONs deterministic, the values are read from the JSON in an order that is alphabetical in respect to JSON keys. Nested JSON values are sorted by the flattened format keys
(a.b.c)
.
Example
For example, this plain text file content:
1
2
'hello'
10
"world"
or this JSON file content:
{
"a": 1,
"nested": {
"b": 2,
"c": 448378203247
},
"d": 10,
"e": "world"
}
(note that short strings cannot be used in JSON file)
could be parsed to the following struct in cairo, via parse_txt
/parse_json
:
A {
a: 1,
nested: B {
b: 2,
c: 'hello',
},
d: 10,
e: "world"
}
or to an array, via read_txt
/read_json
:
array![1, 2, 'hello', 10, 0, 512970878052, 5]
env
Module
Module containing functions for interacting with the system environment.
var
fn var(name: ByteArray) -> Array<felt252>
Reads an environment variable, without parsing it.
The serialized output is correlated with the inferred input type, same as during reading from a file.
📝 Note
If you want snfoundry to treat your variable like a short string, surround it with 'single quotes'.
If you would like it to be serialized as a
ByteArray
, use "double quoting". It will be then de-serializable withSerde
.
signature
Module
Module containing KeyPair
struct and interface for creating ecdsa
signatures.
signature::stark_curve
- implementation ofKeyPairTrait
for the STARK curvesignature::secp256k1_curve
- implementation ofKeyPairTrait
for Secp256k1 Curvesignature::secp256r1_curve
- implementation ofKeyPairTrait
for Secp256r1 Curve
⚠️ Security Warning
Please note that cryptography in Starknet Foundry is still experimental and has not been audited.
Use at your own risk!
KeyPair
struct KeyPair<SK, PK> {
secret_key: SK,
public_key: PK,
}
KeyPairTrait
trait KeyPairTrait<SK, PK> {
fn generate() -> KeyPair<SK, PK>;
fn from_secret_key(secret_key: SK) -> KeyPair<SK, PK>;
}
SignerTrait
trait SignerTrait<T, H, U> {
fn sign(self: T, message_hash: H) -> Result<U, SignError> ;
}
VerifierTrait
trait VerifierTrait<T, H, U> {
fn verify(self: T, message_hash: H, signature: U) -> bool;
}
Example
use snforge_std::signature::KeyPairTrait;
use snforge_std::signature::secp256r1_curve::{Secp256r1CurveKeyPairImpl, Secp256r1CurveSignerImpl, Secp256r1CurveVerifierImpl};
use snforge_std::signature::secp256k1_curve::{Secp256k1CurveKeyPairImpl, Secp256k1CurveSignerImpl, Secp256k1CurveVerifierImpl};
use snforge_std::signature::stark_curve::{StarkCurveKeyPairImpl, StarkCurveSignerImpl, StarkCurveVerifierImpl};
use starknet::secp256r1::{Secp256r1Point, Secp256r1PointImpl};
use starknet::secp256k1::{Secp256k1Point, Secp256k1PointImpl};
use core::starknet::SyscallResultTrait;
#[test]
fn test_using_curves() {
// Secp256r1
let key_pair = KeyPairTrait::<u256, Secp256r1Point>::generate();
let (r, s): (u256, u256) = key_pair.sign(msg_hash).unwrap();
let is_valid = key_pair.verify(msg_hash, (r, s));
// Secp256k1
let key_pair = KeyPairTrait::<u256, Secp256k1Point>::generate();
let (r, s): (u256, u256) = key_pair.sign(msg_hash).unwrap();
let is_valid = key_pair.verify(msg_hash, (r, s));
// StarkCurve
let key_pair = KeyPairTrait::<felt252, felt252>::generate();
let (r, s): (felt252, felt252) = key_pair.sign(msg_hash).unwrap();
let is_valid = key_pair.verify(msg_hash, (r, s));
}
sncast
CLI Reference
sncast
common flags
--profile, -p <PROFILE_NAME>
Optional.
Used for both snfoundry.toml
and Scarb.toml
if specified.
Defaults to default
(snfoundry.toml
) and dev
(Scarb.toml
).
--url, -u <RPC_URL>
Optional.
Starknet RPC node url address.
Overrides url from snfoundry.toml
.
--account, -a <ACCOUNT_NAME>
Optional.
Account name used to interact with the network, aliased in open zeppelin accounts file.
Overrides account from snfoundry.toml
.
If used with --keystore
, should be a path to starkli account JSON file.
--accounts-file, -f <PATH_TO_ACCOUNTS_FILE>
Optional.
Path to the open zeppelin accounts file holding accounts info. Defaults to ~/.starknet_accounts/starknet_open_zeppelin_accounts.json
.
--keystore, -k <PATH_TO_KEYSTORE_FILE>
Optional.
Path to keystore file. When specified, the --account argument must be a path to starkli account JSON file.
--int-format
Optional.
If passed, values will be displayed in decimal format. Default is addresses as hex and fees as int.
--hex-format
Optional.
If passed, values will be displayed in hex format. Default is addresses as hex and fees as int.
--json, -j
Optional.
If passed, output will be displayed in json format.
--wait, -w
Optional.
If passed, command will wait until transaction is accepted or rejected.
--wait-timeout <TIME_IN_SECONDS>
Optional.
If --wait
is passed, this will set the time after which sncast
times out. Defaults to 60s.
--wait-retry-timeout <TIME_IN_SECONDS>
Optional.
If --wait
is passed, this will set the retry interval - how often sncast
should fetch tx info from the node. Defaults to 5s.
--version, -v
Prints out sncast
version.
--help, -h
Prints out help.
account
Provides a set of account management commands.
It has the following subcommands:
add
Import an account to accounts file.
Account information will be saved to the file specified by --accounts-file
argument,
which is ~/.starknet_accounts/starknet_open_zeppelin_accounts.json
by default.
Required Common Arguments — Passed By CLI or Specified in snfoundry.toml
--name, -n <NAME>
Required.
Name of the account to be added.
--address, -a <ADDRESS>
Required.
Address of the account.
--type, -t <ACCOUNT_TYPE>
Required.
Type of the account. Possible values: oz, argent, braavos.
--class-hash, -c <CLASS_HASH>
Optional.
Class hash of the account.
--private-key <PRIVATE_KEY>
Optional. Required if --private-key-file
is not passed.
Account private key.
--private-key-file <PRIVATE_KEY_FILE_PATH>
Optional. Required if --private-key-file
is not passed.
Path to the file holding account private key.
--public-key <PUBLIC_KEY>
Optional.
Account public key.
If not passed, will be computed from --private-key
.
--salt, -s <SALT>
Optional.
Salt for the account address.
--add-profile <NAME>
Optional.
If passed, a profile with corresponding name will be added to snfoundry.toml.
create
Prepare all prerequisites for account deployment.
Account information will be saved to the file specified by --accounts-file
argument,
which is ~/.starknet_accounts/starknet_open_zeppelin_accounts.json
by default.
Required Common Arguments — Passed By CLI or Specified in snfoundry.toml
--name, -n <ACCOUNT_NAME>
Required.
Account name under which account information is going to be saved.
--type, -t <ACCOUNT_TYPE>
Optional. Required if --class-hash
is passed.
Type of the account. Possible values: oz, argent, braavos. Defaults to oz.
Versions of the account contracts:
Account Contract | Version | Class Hash |
---|---|---|
oz | v0.14.0 | 0x00e2eb8f5672af4e6a4e8a8f1b44989685e668489b0a25437733756c5a34a1d6 |
argent | v0.3.1 | 0x029927c8af6bccf3f6fda035981e765a7bdbf18a2dc0d630494f8758aa908e2b |
braavos | v1.0.0 | 0x00816dd0297efc55dc1e7559020a3a825e81ef734b558f03c83325d4da7e6253 |
--salt, -s <SALT>
Optional.
Salt for the account address. If omitted random one will be generated.
--add-profile <NAME>
Optional.
If passed, a profile with corresponding name will be added to snfoundry.toml.
--class-hash, -c
Optional.
Class hash of a custom openzeppelin account contract declared to the network.
deploy
Deploy previously created account to Starknet.
Required Common Arguments — Passed By CLI or Specified in snfoundry.toml
--name, -n <ACCOUNT_NAME>
Required.
Name of the (previously created) account to be deployed.
--max-fee, -m <MAX_FEE>
Optional.
Maximum fee for the deploy_account
transaction in Fri or Wei depending on fee token or transaction version. When not used, defaults to auto-estimation.
--fee-token <FEE_TOKEN>
Optional. Required if --version
is not provided.
Token used for fee payment. Possible values: ETH, STRK.
--max-gas <MAX_GAS>
Optional.
Maximum gas for the deploy_account
transaction. When not used, defaults to auto-estimation. (Only for STRK fee payment)
--max-gas-unit-price <MAX_GAS_UNIT_PRICE>
Optional.
Maximum gas unit price for the deploy_account
transaction paid in Fri. When not used, defaults to auto-estimation. (Only for STRK fee payment)
--version, -v <VERSION>
Optional. Required if --fee-token
is not provided.
Version of the account deployment transaction. Possible values: v1, v3.
delete
Delete an account from accounts-file
and its associated snfoundry profile.
Required Common Arguments — Passed By CLI or Specified in snfoundry.toml
--name, -n <ACCOUNT_NAME>
Required.
Account name which is going to be deleted.
--network
Optional.
Network in accounts-file
associated with the account. By default, the network of rpc node.
--yes
Optional.
If passed, assume "yes" as answer to confirmation prompt and run non-interactively
list
List all available accounts.
Account information will be retrieved from the file specified in user's environment.
The output format is dependent on user's configuration, either provided via CLI or specified in snfoundry.toml
.
Hides user's private keys by default.
⚠️ Warning This command outputs cryptographic information about accounts, e.g. user's private key. Use it responsibly to not cause any vulnerabilities to your environment and confidential data.
Required Common Arguments — Passed By CLI or Specified in snfoundry.toml
Optional Common Arguments — Passed By CLI or Specified in snfoundry.toml
--display-private-keys
, -p
Optional.
If passed, show private keys along with the rest of the account information.
declare
Send a declare transaction of Cairo contract to Starknet.
Required Common Arguments — Passed By CLI or Specified in snfoundry.toml
--contract-name, -c <CONTRACT_NAME>
Required.
Name of the contract. Contract name is a part after the mod keyword in your contract file.
--max-fee, -m <MAX_FEE>
Optional.
Maximum fee for the declare
transaction in Fri or Wei depending on fee token or transaction version. When not used, defaults to auto-estimation.
--fee-token <FEE_TOKEN>
Optional. Required if --version
is not provided.
Token used for fee payment. Possible values: ETH, STRK.
--max-gas <MAX_GAS>
Optional.
Maximum gas for the declare
transaction. When not used, defaults to auto-estimation. (Only for STRK fee payment)
--max-gas-unit-price <MAX_GAS_UNIT_PRICE>
Optional.
Maximum gas unit price for the declare
transaction paid in Fri. When not used, defaults to auto-estimation. (Only for STRK fee payment)
--version, -v <VERSION>
Optional. Required if --fee-token
is not provided.
Version of the deployment transaction. Possible values: v2, v3.
--nonce, -n <NONCE>
Optional.
Nonce for transaction. If not provided, nonce will be set automatically.
--package <NAME>
Optional.
Name of the package that should be used.
If supplied, a contract from this package will be used. Required if more than one package exists in a workspace.
deploy
Deploy a contract to Starknet.
Required Common Arguments — Passed By CLI or Specified in snfoundry.toml
--class-hash, -g <CLASS_HASH>
Required.
Class hash of contract to deploy.
--constructor-calldata, -c <CONSTRUCTOR_CALLDATA>
Optional.
Calldata for the contract constructor.
--salt, -s <SALT>
Optional.
Salt for the contract address.
--unique, -u
Optional.
If passed, the salt will be additionally modified with an account address.
--max-fee, -m <MAX_FEE>
Optional.
Maximum fee for the deploy
transaction in Fri or Wei depending on fee token or transaction version. When not used, defaults to auto-estimation.
--fee-token <FEE_TOKEN>
Optional. Required if --version
is not provided.
Token used for fee payment. Possible values: ETH, STRK.
--max-gas <MAX_GAS>
Optional.
Maximum gas for the deploy
transaction. When not used, defaults to auto-estimation. (Only for STRK fee payment)
--max-gas-unit-price <MAX_GAS_UNIT_PRICE>
Optional.
Maximum gas unit price for the deploy
transaction paid in Fri. When not used, defaults to auto-estimation. (Only for STRK fee payment)
--version, -v <VERSION>
Optional. Required if --fee-token
is not provided.
Version of the deployment transaction. Possible values: v1, v3.
--nonce, -n <NONCE>
Optional.
Nonce for transaction. If not provided, nonce will be set automatically.
invoke
Send an invoke transaction to Starknet.
Required Common Arguments — Passed By CLI or Specified in snfoundry.toml
--contract-address, -a <CONTRACT_ADDRESS>
Required.
The address of the contract being called in hex (prefixed with '0x') or decimal representation.
--function, -f <FUNCTION_NAME>
Required.
The name of the function to call.
--calldata, -c <CALLDATA>
Optional.
Inputs to the function, represented by a list of space-delimited values 0x1 2 0x3
.
Calldata arguments may be either 0x hex or decimal felts.
--max-fee, -m <MAX_FEE>
Optional.
Maximum fee for the invoke
transaction in Fri or Wei depending on fee token or transaction version. When not used, defaults to auto-estimation.
--fee-token <FEE_TOKEN>
Optional. Required if --version
is not provided.
Token used for fee payment. Possible values: ETH, STRK.
--max-gas <MAX_GAS>
Optional.
Maximum gas for the invoke
transaction. When not used, defaults to auto-estimation. (Only for STRK fee payment)
--max-gas-unit-price <MAX_GAS_UNIT_PRICE>
Optional.
Maximum gas unit price for the invoke
transaction paid in Fri. When not used, defaults to auto-estimation. (Only for STRK fee payment)
--version, -v <VERSION>
Optional. Required if --fee-token
is not provided.
Version of the deployment transaction. Possible values: v1, v3.
--nonce, -n <NONCE>
Optional.
Nonce for transaction. If not provided, nonce will be set automatically.
call
Call a smart contract on Starknet with the given parameters.
Required Common Arguments — Passed By CLI or Specified in snfoundry.toml
--contract-address, -a <CONTRACT_ADDRESS>
Required.
The address of the contract being called in hex (prefixed with '0x') or decimal representation.
--function, -f <FUNCTION_NAME>
Required.
The name of the function being called.
--calldata, -c <CALLDATA>
Optional.
Inputs to the function, represented by a list of space-delimited values, e.g. 0x1 2 0x3
.
Calldata arguments may be either 0x hex or decimal felts.
--block-id, -b <BLOCK_ID>
Optional.
Block identifier on which call should be performed.
Possible values: pending
, latest
, block hash (0x prefixed string), and block number (u64).
pending
is used as a default value.
multicall
Provides utilities for performing multicalls on Starknet.
Multicall has the following subcommands:
new
Generates an empty template for the multicall .toml
file that may be later used with the run
subcommand. It writes it to a file provided as a required argument.
Usage
multicall new <OUTPUT-PATH> [OPTIONS]
Arguments
OUTPUT-PATH
- a path to a file to write the generated .toml
to.
Required Common Arguments — Passed By CLI or Specified in snfoundry.toml
--output-path, -p <PATH>
Optional.
Specifies a file path where the template should be saved. If omitted, the template contents will be printed out to the stdout.
--overwrite, -o <OVERWRITE>
Optional.
If the file specified by --output-path
already exists, this parameter overwrites it.
run
Execute a single multicall transaction containing every call from passed file.
Required Common Arguments — Passed By CLI or Specified in snfoundry.toml
--path, -p <PATH>
Required.
Path to a TOML file with call declarations.
--max-fee, -m <MAX_FEE>
Optional.
Maximum fee for the invoke
transaction in Fri or Wei depending on fee token or transaction version. When not used, defaults to auto-estimation.
--fee-token <FEE_TOKEN>
Optional. Required if --version
is not provided.
Token used for fee payment. Possible values: ETH, STRK.
--max-gas <MAX_GAS>
Optional.
Maximum gas for the invoke
transaction. When not used, defaults to auto-estimation. (Only for STRK fee payment)
--max-gas-unit-price <MAX_GAS_UNIT_PRICE>
Optional.
Maximum gas unit price for the invoke
transaction paid in Fri. When not used, defaults to auto-estimation. (Only for STRK fee payment)
--version, -v <VERSION>
Optional. Required if --fee-token
is not provided.
Version of the deployment transaction. Possible values: v1, v3.
File example:
[[call]]
call_type = "deploy"
class_hash = "0x076e94149fc55e7ad9c5fe3b9af570970ae2cf51205f8452f39753e9497fe849"
inputs = []
id = "map_contract"
unique = false
[[call]]
call_type = "invoke"
contract_address = "0x38b7b9507ccf73d79cb42c2cc4e58cf3af1248f342112879bfdf5aa4f606cc9"
function = "put"
inputs = ["0x123", "234"]
[[call]]
call_type = "invoke"
contract_address = "map_contract"
function = "put"
inputs = ["0x123", "234"]
[[call]]
call_type = "deploy"
class_hash = "0x2bb3d35dba2984b3d0cd0901b4e7de5411daff6bff5e072060bcfadbbd257b1"
inputs = ["0x123", "map_contract"]
unique = false
show_config
Prints the config currently being used
Required Common Arguments — Passed By CLI or Specified in snfoundry.toml
This doesn't take any arguments of its own.
script
Provides a set of commands to manage deployment scripts.
Script has the following subcommands:
init
Create a deployment script template.
The command creates the following file and directory structure:
.
└── scripts
└── my_script
├── Scarb.toml
└── src
├── lib.cairo
└── my_script.cairo
<SCRIPT_NAME>
Required.
Name of a script to create.
run
Compile and run a cairo deployment script.
Required Common Arguments — Passed By CLI or Specified in snfoundry.toml
<MODULE_NAME>
Required.
Script module name that contains the 'main' function that will be executed.
--package <NAME>
Optional.
Name of the package that should be used.
If supplied, a script from this package will be used. Required if more than one package exists in a workspace.
--no-state-file
Optional.
Do not read/write state from/to the state file.
If set, a script will not read the state from the state file, and will not write a state to it.
tx-status
Get the status of a transaction
Required Common Arguments — Passed By CLI or Specified in snfoundry.toml
<TRANSACTION_HASH>
Required.
Hash of the transaction
verify
Verify Cairo contract on a chosen verification provider.
--contract-address, -a <CONTRACT_ADDRESS>
Required.
The address of the contract that is to be verified.
--contract-name <CONTRACT_NAME>
Required.
The name of the contract. The contract name is the part after the mod
keyword in your contract file.
--verifier, -v <VERIFIER>
Optional.
The verification provider to use for the verification. Possible values are:
walnut
--network, -n <NETWORK>
Required.
The network on which block explorer will perform the verification. Possible values are:
mainnet
sepolia
--package <NAME>
Optional.
Name of the package that should be used.
If supplied, a contract from this package will be used. Required if more than one package exists in a workspace.
--confirm-verification
Optional.
If passed, assume "yes" as answer to confirmation prompt and run non-interactively.
Library Reference
declare
- declares a contractdeploy
- deploys a contractinvoke
- invokes a contract's functioncall
- calls a contract's functionget_nonce
- gets account's nonce for a given block tagtx_status
- gets the status of a transaction using its hasherrors
- sncast_std error types reference
ℹ️ Info To use the library functions you need to add
sncast_std
package as a dependency in yourScarb.toml
using appropriate release tag.[dependencies] sncast_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.22.0" }
declare
pub fn declare(contract_name: ByteArray, fee_settings: FeeSettings, nonce: Option<felt252>) -> Result<DeclareResult, ScriptCommandError>
Declares a contract and returns DeclareResult
.
#[derive(Drop, Clone, Debug)]
pub struct DeclareResult {
pub class_hash: ClassHash,
pub transaction_hash: felt252,
}
#[derive(Drop, Clone, Debug, Serde, PartialEq)]
pub struct EthFeeSettings {
pub max_fee: Option<felt252>,
}
#[derive(Drop, Clone, Debug, Serde, PartialEq)]
pub struct StrkFeeSettings {
pub max_fee: Option<felt252>,
pub max_gas: Option<u64>,
pub max_gas_unit_price: Option<u128>,
}
contract_name
- name of a contract as Cairo string. It is a name of the contract (part aftermod
keyword) e.g."HelloStarknet"
.fee_settings
- fee settings for the transaction. Can beEth
orStrk
. Read more about it herenonce
- nonce for declare transaction. If not provided, nonce will be set automatically.
use sncast_std::{declare, DeclareResult, FeeSettings, EthFeeSettings};
fn main() {
let max_fee = 9999999;
let declare_result = declare(
"HelloStarknet",
FeeSettings::Eth(EthFeeSettings { max_fee: Option::Some(max_fee) }),
Option::None
)
.expect('declare failed');
println!("declare_result: {}", declare_result);
println!("debug declare_result: {:?}", declare_result);
}
deploy
pub fn deploy( class_hash: ClassHash, constructor_calldata: Array::<felt252>, salt: Option<felt252>, unique: bool, fee_settings: FeeSettings, nonce: Option<felt252> ) -> Result<DeployResult, ScriptCommandError>
Deploys a contract and returns DeployResult
.
#[derive(Drop, Clone, Debug)]
pub struct DeployResult {
pub contract_address: ContractAddress,
pub transaction_hash: felt252,
}
#[derive(Drop, Clone, Debug, Serde, PartialEq)]
pub enum FeeSettings {
Eth: EthFeeSettings,
Strk: StrkFeeSettings
}
#[derive(Drop, Clone, Debug, Serde, PartialEq)]
pub struct EthFeeSettings {
pub max_fee: Option<felt252>,
}
#[derive(Drop, Clone, Debug, Serde, PartialEq)]
pub struct StrkFeeSettings {
pub max_fee: Option<felt252>,
pub max_gas: Option<u64>,
pub max_gas_unit_price: Option<u128>,
}
class_hash
- class hash of a contract to deploy.constructor_calldata
- calldata for the contract constructor.salt
- salt for the contract address.unique
- determines if salt should be further modified with the account address.fee_settings
- fee settings for the transaction. Can beEth
orStrk
. Read more about it herenonce
- nonce for declare transaction. If not provided, nonce will be set automatically.
use sncast_std::{deploy, DeployResult, FeeSettings, EthFeeSettings};
fn main() {
let max_fee = 9999999;
let salt = 0x1;
let nonce = 0x1;
let class_hash: ClassHash = 0x03a8b191831033ba48ee176d5dde7088e71c853002b02a1cfa5a760aa98be046
.try_into()
.expect('Invalid class hash value');
let deploy_result = deploy(
class_hash,
ArrayTrait::new(),
Option::Some(salt),
true,
FeeSettings::Eth(EthFeeSettings {max_fee: Option::Some(max_fee)}),
Option::Some(deploy_nonce)
).expect('deploy failed');
println!("deploy_result: {}", deploy_result);
println!("debug deploy_result: {:?}", deploy_result);
}
invoke
pub fn invoke( contract_address: ContractAddress, entry_point_selector: felt252, calldata: Array::<felt252>, fee_settings: FeeSettings, nonce: Option<felt252> ) -> Result<InvokeResult, ScriptCommandError>
Invokes a contract and returns InvokeResult
.
#[derive(Drop, Clone, Debug)]
pub struct InvokeResult {
pub transaction_hash: felt252,
}
#[derive(Drop, Clone, Debug, Serde, PartialEq)]
pub struct EthFeeSettings {
pub max_fee: Option<felt252>,
}
#[derive(Drop, Clone, Debug, Serde, PartialEq)]
pub struct StrkFeeSettings {
pub max_fee: Option<felt252>,
pub max_gas: Option<u64>,
pub max_gas_unit_price: Option<u128>,
}
contract_address
- address of the contract to invoke.entry_point_selector
- the selector of the function to invoke.calldata
- inputs to the function to be invoked.fee_settings
- fee settings for the transaction. Can beEth
orStrk
. Read more about it herenonce
- nonce for declare transaction. If not provided, nonce will be set automatically.
use sncast_std::{invoke, InvokeResult, FeeSettings, EthFeeSettings};
use starknet::{ContractAddress};
fn main() {
let contract_address: ContractAddress =
0x1e52f6ebc3e594d2a6dc2a0d7d193cb50144cfdfb7fdd9519135c29b67e427
.try_into()
.expect('Invalid contract address value');
let invoke_result = invoke(
contract_address,
selector!("put"),
array![0x1, 0x2],
FeeSettings::Eth(EthFeeSettings { max_fee: Option::None }),
Option::None
)
.expect('invoke failed');
println!("invoke_result: {}", invoke_result);
println!("debug invoke_result: {:?}", invoke_result);
}
call
pub fn call( contract_address: ContractAddress, function_selector: felt252, calldata: Array::<felt252> ) -> Result<CallResult, ScriptCommandError>
Calls a contract and returns CallResult
.
#[derive(Drop, Clone, Debug)]
pub struct CallResult {
pub data: Array::<felt252>,
}
contract_address
- address of the contract to call.function_selector
- the selector of the function to call.calldata
- inputs to the function to be called.
use sncast_std::{call, CallResult};
use starknet::{ContractAddress};
fn main() {
let contract_address: ContractAddress = 0x1e52f6ebc3e594d2a6dc2a0d7d193cb50144cfdfb7fdd9519135c29b67e427
.try_into()
.expect('Invalid contract address value');
let call_result = call(contract_address, selector!("get"), array![0x1]).expect('call failed');
println!("call_result: {}", call_result);
println!("debug call_result: {:?}", call_result);
}
get_nonce
pub fn get_nonce(block_tag: felt252) -> felt252
Gets nonce of an account for a given block tag (pending
or latest
) and returns nonce as felt252
.
block_tag
- block tag name, one ofpending
orlatest
.
use sncast_std::{get_nonce};
fn main() {
let nonce = get_nonce('latest');
println!("nonce: {}", nonce);
println!("debug nonce: {:?}", nonce);
}
tx_status
pub fn tx_status(transaction_hash: felt252) -> Result<TxStatusResult, ScriptCommandError>
Gets the status of a transaction using its hash and returns TxStatusResult
.
#[derive(Drop, Clone, Debug, Serde, PartialEq)]
pub enum FinalityStatus {
Received,
Rejected,
AcceptedOnL2,
AcceptedOnL1
}
#[derive(Drop, Copy, Debug, Serde, PartialEq)]
pub enum ExecutionStatus {
Succeeded,
Reverted,
}
#[derive(Drop, Clone, Debug, Serde, PartialEq)]
pub struct TxStatusResult {
pub finality_status: FinalityStatus,
pub execution_status: Option<ExecutionStatus>
}
transaction_hash
- hash of the transaction
use sncast_std::{tx_status};
fn main() {
let transaction_hash = 0x00ae35dacba17cde62b8ceb12e3b18f4ab6e103fa2d5e3d9821cb9dc59d59a3c;
let status = tx_status(transaction_hash).expect("Failed to get transaction status");
println!("transaction status: {:?}", status);
}
errors
#[derive(Drop, PartialEq, Serde, Debug)]
pub struct ErrorData {
msg: ByteArray
}
#[derive(Drop, PartialEq, Serde, Debug)]
pub struct TransactionExecutionErrorData {
transaction_index: felt252,
execution_error: ByteArray,
}
#[derive(Drop, Serde, PartialEq, Debug)]
pub enum StarknetError {
/// Failed to receive transaction
FailedToReceiveTransaction,
/// Contract not found
ContractNotFound,
/// Block not found
BlockNotFound,
/// Invalid transaction index in a block
InvalidTransactionIndex,
/// Class hash not found
ClassHashNotFound,
/// Transaction hash not found
TransactionHashNotFound,
/// Contract error
ContractError: ErrorData,
/// Transaction execution error
TransactionExecutionError: TransactionExecutionErrorData,
/// Class already declared
ClassAlreadyDeclared,
/// Invalid transaction nonce
InvalidTransactionNonce,
/// Max fee is smaller than the minimal transaction cost (validation plus fee transfer)
InsufficientMaxFee,
/// Account balance is smaller than the transaction's max_fee
InsufficientAccountBalance,
/// Account validation failed
ValidationFailure: ErrorData,
/// Compilation failed
CompilationFailed,
/// Contract class size it too large
ContractClassSizeIsTooLarge,
/// Sender address in not an account contract
NonAccount,
/// A transaction with the same hash already exists in the mempool
DuplicateTx,
/// the compiled class hash did not match the one supplied in the transaction
CompiledClassHashMismatch,
/// the transaction version is not supported
UnsupportedTxVersion,
/// the contract class version is not supported
UnsupportedContractClassVersion,
/// An unexpected error occurred
UnexpectedError: ErrorData,
}
#[derive(Drop, Serde, PartialEq, Debug)]
pub enum ProviderError {
StarknetError: StarknetError,
RateLimited,
UnknownError: ErrorData,
}
#[derive(Drop, Serde, PartialEq, Debug)]
pub enum TransactionError {
Rejected,
Reverted: ErrorData,
}
#[derive(Drop, Serde, PartialEq, Debug)]
pub enum WaitForTransactionError {
TransactionError: TransactionError,
TimedOut,
ProviderError: ProviderError,
}
#[derive(Drop, Serde, PartialEq, Debug)]
pub enum ScriptCommandError {
UnknownError: ErrorData,
ContractArtifactsNotFound: ErrorData,
WaitForTransactionError: WaitForTransactionError,
ProviderError: ProviderError,
}