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.
Watch video tutorial to learn the basics 🎥
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.
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
.
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
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
.
├── README.md
├── Scarb.toml
├── src
└── tests
3 directories
src/
contains source code of all your contracts.tests/
contains tests.Scarb.toml
contains configuration of the project as well as ofsnforge
And run tests with snforge test
$ snforge test
Collected 2 test(s) from test_name package
Running 0 test(s) from src/
Running 2 test(s) from tests/
[PASS] tests::test_contract::test_increase_balance
[PASS] tests::test_contract::test_cannot_increase_balance_with_zero_value
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.12.0" }
Make sure that the version in tag
matches snforge
. You can check the currently installed version with
$ snforge --version
snforge 0.12.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.12.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
Scarb Workspaces Support
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.
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 {
#[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',))]
.
#[test]
#[should_panic(expected: ('panic message', ))]
fn should_panic_check_data() {
panic_with_felt252('panic message');
}
$ snforge test
Collected 1 test(s) from package_name package
Running 0 test(s) from src/
Running 1 test(s) from tests/
[PASS] tests::should_panic_check_data
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
Ignoring Some Tests Unless Specifically Requested
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.
#[test]
#[ignore]
fn ignored_test() {
// test code
}
$ snforge test
Collected 1 test(s) from package_name package
Running 0 test(s) from src/
Running 1 test(s) from tests/
[IGNORE] 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
.
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.
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
#[starknet::interface]
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 };
#[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) {
let mut arr = ArrayTrait::new();
arr.append('PANIC');
arr.append('DAYTAH');
panic(arr);
}
fn do_a_string_panic(self: @ContractState) {
assert!(false, "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]
#[feature("safe_dispatcher")]
fn failing() {
// ...
let (contract_address, _) = contract.deploy(@calldata).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.
#[test]
#[feature("safe_dispatcher")]
fn handling_errors() {
// ...
let (contract_address, _) = contract.deploy(@calldata).unwrap();
let safe_dispatcher = IHelloStarknetSafeDispatcher { contract_address };
match safe_dispatcher.do_a_panic() {
Result::Ok(_) => panic_with_felt252('shouldve panicked'),
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 string as an argument (like an assert!
macro)
// Necessary struct and trait imports for string errors mapping
use snforge_std::errors::{ SyscallResultStringErrorTrait, PanicDataOrString };
// ...
#[test]
#[feature("safe_dispatcher")]
fn handling_string_errors() {
// ...
let (contract_address, _) = contract.deploy(@calldata).unwrap();
let safe_dispatcher = IHelloStarknetSafeDispatcher { contract_address };
// Notice the `map_error_to_string` helper usage here, and different error type
match safe_dispatcher.do_a_string_panic().map_error_to_string() {
Result::Ok(_) => panic_with_felt252('shouldve panicked'),
Result::Err(panic_data) => {
match x {
PanicDataOrString::PanicData(_) => panic_with_felt252('wrong format'),
PanicDataOrString::String(str) => {
assert(
str == "This a panicking with a string, which can be longer",
'wrong string received'
);
}
}
}
};
}
You also could skip the de-serialization of the panic_data
, and not use map_error_to_string
, 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 annotage 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 (
prank
,warp
,roll
,spoof
) - Spy for events emitted in the test
Example usages:
1. Mocking the context info
Example for roll
, same can be implemented for prank
/spoof
/warp
/elect
etc.
use result::ResultTrait;
use box::BoxTrait;
use starknet::ContractAddress;
use snforge_std::{
CheatTarget,
start_roll, stop_roll,
test_address
};
#[test]
fn test_roll_test_state() {
let test_address: ContractAddress = test_address();
let old_block_number = starknet::get_block_info().unbox().block_number;
start_roll(CheatTarget::One(test_address), 234);
let new_block_number = starknet::get_block_info().unbox().block_number;
assert(new_block_number == 234, 'Wrong block number');
stop_roll(CheatTarget::One(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, EventFetcher,
EventAssertions, Event, SpyOn, test_address
};
#[test]
fn test_expect_event() {
let contract_address = test_address();
let mut spy = spy_events(SpyOn::One(contract_address));
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, EventFetcher,
EventAssertions, Event, SpyOn, test_address };
#[test]
fn test_expect_events_simple() {
let test_address = test_address();
let mut spy = spy_events(SpyOn::One(test_address));
assert(spy._id == 0, 'Id should be 0');
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_prank
cheatcode to change the caller
address, so it passes our validation.
Pranking the Address
use snforge_std::{ declare, ContractClassTrait, start_prank, CheatTarget };
#[test]
fn call_and_invoke() {
let contract = declare("HelloStarknet").unwrap();
let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap();
let dispatcher = IHelloStarknetDispatcher { contract_address };
let balance = dispatcher.get_balance();
assert(balance == 0, 'balance == 0');
// Change the caller address to 123 when calling the contract at the `contract_address` address
start_prank(CheatTarget::One(contract_address), 123.try_into().unwrap());
dispatcher.increase_balance(100);
let balance = dispatcher.get_balance();
assert(balance == 100, 'balance == 100');
}
The test will now pass without an error
$ snforge test
Collected 1 test(s) from package_name package
Running 0 test(s) from src/
Running 1 test(s) from tests/
[PASS] tests::call_and_invoke
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out
Canceling the Prank
Most cheatcodes come with corresponding start_
and stop_
functions that can be used to start and stop the state
change.
In case of the start_prank
, we can cancel the address change
using stop_prank
use snforge_std::{stop_prank, CheatTarget};
#[test]
fn call_and_invoke() {
// ...
// The address when calling contract at the `contract_address` address will no longer be changed
stop_prank(CheatTarget::One(contract_address));
// This will fail
dispatcher.increase_balance(100);
let balance = dispatcher.get_balance();
assert(balance == 100, 'balance == 100');
}
$ snforge test
Collected 1 test(s) from package_name package
Running 0 test(s) from src/
Running 1 test(s) from tests/
[FAIL] tests::call_and_invoke, 0 ignored, 0 filtered out
Failure data:
0x75736572206973206e6f7420616c6c6f776564 ('user is not allowed')
Tests: 0 passed, 1 failed, 0 skipped, 0 ignored, 0 filtered out
Failures:
tests::call_and_invoke
Pranking the Constructor
Most of the cheatcodes like prank
, mock_call
, warp
, roll
, elect
do work in the constructor of the contracts.
Let's say, that you have a contract that saves the caller address (deployer) in the constructor, and you want it to be pre-set to a certain value.
To prank
the constructor, you need to start_prank
before it is invoked, with the right address. To achieve this, you need to precalculate the address of the contract by using the precalculate_address
function of ContractClassTrait
on the declared contract, and then use it in start_prank
as an argument:
use snforge_std::{ declare, ContractClassTrait, start_prank, CheatTarget };
#[test]
fn mock_constructor_with_prank() {
let contract = declare("HelloStarknet").unwrap();
let constructor_arguments = @ArrayTrait::new();
// Precalculate the address to obtain the contract address before the constructor call (deploy) itself
let contract_address = contract.precalculate_address(constructor_arguments);
// Change the caller address to 123 before the call to contract.deploy
start_prank(CheatTarget::One(contract_address), 123.try_into().unwrap());
// The constructor will have 123 set as the caller address
contract.deploy(constructor_arguments).unwrap();
}
Setting Cheatcode Span
Sometimes it's useful to have a cheatcode work only for a certain number of target calls.
That's where CheatSpan
comes in handy.
enum CheatSpan {
Indefinite: (),
TargetCalls: usize,
}
To set span for a cheatcode, use prank
/ warp
/ roll
/ etc.
prank(CheatTarget::One(contract_address), new_caller_address, CheatSpan::TargetCalls(1))
Calling a cheatcode with CheatSpan::TargetCalls(N)
is going to activate the cheatcode for N
calls to a specified CheatTarget
, after which it's going to be automatically canceled.
Of course the cheatcode can still be canceled before its CheatSpan
goes down to 0 - simply call stop_prank
on the target manually.
ℹ️ Info
Using
start_prank
is equivalent to usingprank
withCheatSpan::Indefinite
.
To better understand the functionality of CheatSpan
, here's a full example:
use snforge_std::{
declare, ContractClass, ContractClassTrait, prank, CheatSpan, CheatTarget
};
#[test]
#[feature("safe_dispatcher")]
fn call_and_invoke() {
let contract = declare("HelloStarknet").unwrap();
let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap();
let safe_dispatcher = IHelloStarknetSafeDispatcher { contract_address };
let balance = safe_dispatcher.get_balance().unwrap();
assert_eq!(balance, 0);
// Function `increase_balance` from HelloStarknet contract
// requires the caller_address to be 123
let pranked_address: ContractAddress = 123.try_into().unwrap();
// Prank the contract_address for a span of 2 target calls (here, calls to contract_address)
prank(CheatTarget::One(contract_address), pranked_address, CheatSpan::TargetCalls(2));
// Call #1 should succeed
let call_1_result = safe_dispatcher.increase_balance(100);
assert!(call_1_result.is_ok());
// Call #2 should succeed
let call_2_result = safe_dispatcher.increase_balance(100);
assert!(call_2_result.is_ok());
// Call #3 should fail, as the prank cheatcode has been canceled
let call_3_result = safe_dispatcher.increase_balance(100);
assert!(call_3_result.is_err());
let balance = safe_dispatcher.get_balance().unwrap();
assert_eq!(balance, 200);
}
Testing events
Examples are based on the following SpyEventsChecker
contract implementation:
#[starknet::contract]
mod SpyEventsChecker {
// ...
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
FirstEvent: FirstEvent
}
#[derive(Drop, starknet::Event)]
struct FirstEvent {
some_data: felt252
}
// ...
}
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, SpyOn, EventSpy,
EventAssertions};
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(SpyOn::One(contract_address));
dispatcher.emit_one_event(123);
spy.assert_emitted(@array![
(
contract_address,
SpyEventsChecker::Event::FirstEvent(
SpyEventsChecker::FirstEvent { some_data: 123 }
)
)
]);
assert(spy.events.len() == 0, 'There should be no events');
}
Let's go through the code:
- After the contract is called, we don't have to call
fetch_events
on the spy (it is done inside theassert_emitted
method). assert_emitted
takes the array snapshot of tuples(ContractAddress, event)
we expect were emitted.- After the assertion, found events are removed from the spy. It stays clean and ready for the next events.
📝 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
You can also use the event
field directly and assert data selectively, if you don't want to assert the whole thing.
This however, requires you to fetch the events manually.
use snforge_std::{declare, ContractClassTrait, spy_events, SpyOn, EventSpy, EventFetcher, 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(SpyOn::One(contract_address)); // Ad 1.
dispatcher.emit_one_event(123);
spy.fetch_events(); // Ad 2.
assert(spy.events.len() == 1, 'There should be one event');
let (from, event) = spy.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');
dispatcher.emit_one_event(123);
assert(spy.events.len() == 1, 'There should be one event'); // Ad 5. - Still one event
spy.fetch_events();
assert(spy.events.len() == 2, 'There should be two events');
}
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
fetch_events
method on the created spy to load emitted events into it. - When events are fetched they are loaded into the
events
property of our spy, and we can assert them. - If the event is emitted by calling
self.emit
method, its hashed name is saved under thekeys.at(0)
(this way Starknet handles events) - It is worth noting that when we call the method which emits an event,
spy
is not updated immediately.
📝 Note To assert the
name
property we have to hash a string with theselector!
macro.
Splitting Events Between Multiple Spies
Sometimes it is easier to split events between multiple spies. For example - one spy for ERC20 contract, and one for your own contracts. Let's do it.
use snforge_std::{declare, ContractClassTrait, spy_events, SpyOn, EventSpy, EventAssertions};
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 (first_address, _) = contract.deploy(array![]).unwrap();
let (second_address, _) = contract.deploy(array![]).unwrap();
let (third_address, _) = contract.deploy(array![]).unwrap();
let first_dispatcher = ISpyEventsCheckerDispatcher { first_address };
let second_dispatcher = ISpyEventsCheckerDispatcher { second_address };
let third_dispatcher = ISpyEventsCheckerDispatcher { third_address };
let mut spy_one = spy_events(SpyOn::One(first_address));
let mut spy_two = spy_events(SpyOn::Multiple(array![second_address, third_address]));
first_dispatcher.emit_one_event(123);
second_dispatcher.emit_one_event(234);
third_dispatcher.emit_one_event(345);
spy_one.assert_emitted(@array![
(
first_address,
SpyEventsChecker::Event::FirstEvent(
SpyEventsChecker::FirstEvent { some_data: 123 }
)
)
]);
spy_two.assert_emitted(@array![
(
second_address,
SpyEventsChecker::Event::FirstEvent(
SpyEventsChecker::FirstEvent { some_data: 234 }
)
),
(
third_address,
SpyEventsChecker::Event::FirstEvent(
SpyEventsChecker::FirstEvent { some_data: 345 }
)
)
]);
}
The first spy gets events emitted by the first contract only. Second one gets events emitted by the rest.
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 consider such a method in the SpyEventsChecker
contract.
fn emit_event_syscall(ref self: ContractState, some_key: felt252, some_data: felt252) {
starknet::emit_event_syscall(array![some_key].span(), array![some_data].span()).unwrap_syscall();
}
And the test.
use snforge_std::{ declare, ContractClassTrait, spy_events, EventSpy, EventFetcher,
EventAssertions, Event, SpyOn };
#[starknet::interface]
trait ISpyEventsChecker<TContractState> {
fn emit_event_syscall(ref self: TContractState, some_key: felt252, 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(SpyOn::One(contract_address));
dispatcher.emit_event_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.
⚠️ Warning
Spying on the same contract with multiple spies can result in unexpected behavior — avoid it if possible.
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
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
Each one of these files can then be used as an input for the cairo-profiler.
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, only OpenZeppelin accounts are supported.
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 Currently, only OpenZeppelin account creation is supported.
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 \ --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.
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.
Custom Account Contract
By default, sncast
creates/deploys an account using openzeppelin contract's class hash.
It is possible to create an account using custom openzeppelin contract declared to starknet. This can be achieved
with --class-hash
flag:
$ sncast \
account create \
--name some-name \
--class-hash 0x058d97f7d76e78f44905cc30cb65b91ea49a4b908a76703c54197bca90f81773
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 \
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 \
--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.
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 \
--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.
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 \
--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 \
--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 \
--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 \
--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.
Invoking Function Without Arguments
Not every function accepts parameters. Here is how to call it.
$ sncast invoke \
--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
, 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
command: multicall
transaction_hash: 0x38fb8a0432f71bf2dae746a1b4f159a75a862e253002b48599c9611fa271dcb
💡 Info Max fee will be automatically computed if
--max-fee <MAX_FEE>
is not passed.
multicall new
Example
You can also generate multicall template with multicall new
command.
$ sncast multicall new
[[call]]
call_type = "deploy"
class_hash = ""
inputs = []
id = ""
unique = false
[[call]]
call_type = "invoke"
contract_address = ""
function = ""
inputs = []
multicall new
With output-path
Argument
Template can be automatically saved to file.
$ sncast multicall new \
--output-path ./new_multicall_template.toml
Multicall template successfully saved in ./new_multicall_template.toml
multicall new
With overwrite
Argument
If there is a file with the same name as passed in the --output-path
argument it can be overwritten.
$ sncast multicall new \
--output-path ./new_multicall_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", 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, DisplayContractAddress, DisplayClassHash
};
fn main() {
let max_fee = 99999999999999999;
let salt = 0x3;
let declare_result = declare("Map", Option::Some(max_fee), Option::None).expect('contract already declared');
let nonce = get_nonce('latest');
let class_hash = declare_result.class_hash;
println!("Class hash of the declared contract: {}", declare_result.class_hash);
let deploy_result = deploy(
class_hash, ArrayTrait::new(), Option::Some(salt), true, Option::Some(max_fee), Option::Some(nonce)
).expect('deploy failed');
println!("Deployed the contract to address: {}", deploy_result.contract_address);
let invoke_nonce = get_nonce('pending');
let invoke_result = invoke(
deploy_result.contract_address, selector!("put"), array![0x1, 0x2], Option::Some(max_fee), Option::Some(invoke_nonce)
).expect('invoke failed');
println!("Invoke tx hash is: {}", invoke_result.transaction_hash);
let call_result = call(deploy_result.contract_address, selector!("get"), array![0x1]).expect('call failed');
println!("Call result: {}", call_result);
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.22.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
};
fn main() {
let max_fee = 9999999999999999999999999999999999;
let declare_nonce = get_nonce('latest');
let declare_result = declare("Map", 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.
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
💡 Info
If you previously installed Scarb using official installer, you may need to remove that installation or modify your
PATH
to make sure the version installed by asdf is always used.
Starknet Devnet
Install it by running ./scripts/install_devnet.sh
Universal sierra compiler
Install the latest universal-sierra-compiler version.
Environmental variables
Set SEPOLIA_RPC_URL
environmental variable to a Sepolia testnet node URL:
- either manually in your shell
$ export SEPOLIA_RPC_URL="https://example.com/rpc/v0_7"
- or inside
.env
file (example found in.env.template
file)SEPOLIA_RPC_URL="https://example.com/rpc/v0_7"
Running Tests
After performing these steps, you can run tests with:
$ cargo test
❗️ 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.
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:
$ cargo clippy --all-targets --all-features -- --no-deps -W clippy::pedantic -A clippy::module_name_repetitions -A clippy::missing_errors_doc -A clippy::missing_panics_doc -A clippy::default_trait_access```
or using our defined alias:
```shell
$ 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
CheatTarget
- enum for selecting contracts to target with cheatcodesCheatSpan
- enum for specifying the number of target calls for a cheatprank
- changes the caller address for contracts, for a number of callsstart_prank
- changes the caller address for contractsstop_prank
- cancels theprank
/start_prank
for contractsroll
- changes the block number for contracts, for a number of callsstart_roll
- changes the block number for contractsstop_roll
- cancels theroll
/start_roll
for contractswarp
- changes the block timestamp for contracts, for a number of callsstart_warp
- changes the block timestamp for contractsstop_warp
- cancels thewarp
/start_warp
for contractselect
- changes the sequencer address for contracts, for a number of callsstart_elect
- changes the sequencer address for contractsstop_elect
- cancels theelect
/start_elect
for contractsspoof
- changes the transaction context for contracts, for a number of callsstart_spoof
- changes the transaction context for contractsstop_spoof
- cancels thespoof
/start_spoof
for contractsmock_call
- mocks a number of contract calls to an entry pointstart_mock_call
- mocks contract call to an entry pointstop_mock_call
- cancels themock_call
/start_mock_call
for an entry pointget_class_hash
- retrieves a class hash of a contractreplace_bytecode
- replace the class hash of a contractl1_handler_execute
- executes a#[l1_handler]
function to mock a message arriving from Ethereumspy_events
- createsEventSpy
instance which spies on events emitted by contractsstore
- stores values in targeted contact's storageload
- loads values directly from targeted contact's storage
ℹ️ 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" }
CheatTarget
enum CheatTarget {
All: (),
One: ContractAddress,
Multiple: Array<ContractAddress>
}
CheatTarget
is an enum used to designate the contracts to which a cheat should be applied.
All
applies the cheatcode to all contract addresses.One
applies the cheatcode to the given contract address.Multiple
applies the cheatcode to each given address.
📝 Note
CheatTarget::Multiple
acts as a helper for targeting every specified address separately withCheatTarget::One
.
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_warp
).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).
📝 Note
CheatTarget::All
can only be used withCheatSpan::Indefinite
.
caller_address
Cheatcodes modifying caller_address
:
prank
fn prank(target: CheatTarget, caller_address: ContractAddress, span: CheatSpan)
Changes the caller address for the given target and span.
start_prank
fn start_prank(target: CheatTarget, caller_address: ContractAddress)
Changes the caller address for the given target.
stop_prank
fn stop_prank(target: CheatTarget)
Cancels the prank
/ start_prank
for the given target.
block_number
Cheatcodes modifying block_number
:
roll
fn roll(target: CheatTarget, block_number: u64, span: CheatSpan)
Changes the block number for the given target and span.
start_roll
fn start_roll(target: CheatTarget, block_number: u64)
Changes the block number for the given target.
stop_roll
fn stop_roll(target: CheatTarget)
Cancels the roll
/ start_roll
for the given target.
block_timestamp
Cheatcodes modifying block_timestamp
:
warp
fn warp(target: CheatTarget, block_timestamp: u64, span: CheatSpan)
Changes the block timestamp for the given target and span.
start_warp
fn start_warp(target: CheatTarget, block_timestamp: u64)
Changes the block timestamp for the given target.
stop_warp
fn stop_warp(target: CheatTarget)
Cancels the warp
/ start_warp
for the given target.
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.
tx_info
Cheatcodes modifying tx_info
:
spoof
fn spoof(target: CheatTarget, tx_info_mock: TxInfoMock, span: CheatSpan)
Changes TxInfo
returned by get_tx_info()
for the targeted contract and span.
start_spoof
fn start_spoof(target: CheatTarget, tx_info_mock: TxInfoMock)
Changes TxInfo
returned by get_tx_info()
for the targeted contract until the spoof is canceled with stop_spoof
.
stop_spoof
fn stop_spoof(target: CheatTarget)
Cancels the spoof
/ start_spoof
for the given target.
TxInfoMock
A structure used for setting individual fields in TxInfo
All fields are optional, with optional value meaning as defined:
None
means that the field is going to be reset to the initial valueSome(n)
means that the value will be set to then
value
struct TxInfoMock {
version: Option<felt252>,
account_contract_address: Option<ContractAddress>,
max_fee: Option<u128>,
signature: Option<Span<felt252>>,
transaction_hash: Option<felt252>,
chain_id: Option<felt252>,
nonce: Option<felt252>,
// starknet::info::v2::TxInfo fields
resource_bounds: Option<Span<starknet::info::v2::ResourceBounds>>,
tip: Option<u128>,
paymaster_data: Option<Span<felt252>>,
nonce_data_availability_mode: Option<u32>,
fee_data_availability_mode: Option<u32>,
account_deployment_data: Option<Span<felt252>>,
}
starknet::info::v2::ResourceBounds
pub struct ResourceBounds {
resource: felt252,
max_amount: u64,
max_price_per_unit: u128,
}
A struct responsible for setting the resource bounds, used in TxInfoMock
.
TxInfoMockTrait
trait TxInfoMockTrait {
fn default() -> TxInfoMock;
}
Returns a default object initialized with Option::None
for each field.
Useful for setting only a few of the fields instead of all of them.
sequencer_address
Cheatcodes modifying sequencer_address
:
elect
fn elect(target: CheatTarget, sequencer_address: ContractAddress, span: CheatSpan)
Changes the sequencer address for the given target and span.
start_elect
fn start_elect(target: CheatTarget, sequencer_address: ContractAddress)
Changes the sequencer address for a given target.
stop_elect
fn stop_elect(target: CheatTarget)
Cancels the elect
/ start_elect
for the given target.
get_class_hash
fn get_class_hash(contract_address: ContractAddress) -> ClassHash
💡 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)
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.
l1_handler_execute
fn execute(self: L1Handler) -> SyscallResult<()>
Executes a #[l1_handler]
function to mock a
message
arriving from Ethereum.
📝 Note
Execution of the
#[l1_handler]
function may panic like any other function. It works like a regularSafeDispatcher
would with a function call. For more info about asserting panic data check out handling panic errors
struct L1Handler {
contract_address: ContractAddress,
function_selector: felt252,
from_address: felt252,
payload: Span::<felt252>,
}
where:
contract_address
- The target contract addressfunction_selector
- Selector of the#[l1_handler]
functionfrom_address
- Ethereum address of the contract that sends the messagepayload
- The message payload that may contain any Cairo data structure that can be serialized with Serde
It is important to note that when executing the l1_handler
,
the from_address
may be checked as any L1 contract can call any L2 contract.
For a contract implementation:
// ...
#[storage]
struct Storage {
l1_allowed: felt252,
//...
}
//...
#[l1_handler]
fn process_l1_message(ref self: ContractState, from_address: felt252, data: Span<felt252>) {
assert(from_address == self.l1_allowed.read(), 'Unauthorized l1 contract');
}
// ...
We can use execute
method to test the execution of the #[l1_handler]
function that is
not available through contracts dispatcher:
use snforge_std::L1Handler;
#[test]
fn test_l1_handler_execute() {
// ...
let data: Array<felt252> = array![1, 2];
let mut payload_buffer: Array<felt252> = ArrayTrait::new();
// Note the serialization here.
data.serialize(ref payload_buffer);
let mut l1_handler = L1HandlerTrait::new(
contract_address,
selector!("process_l1_message")
);
l1_handler.from_address = 0x123;
l1_handler.payload = payload.span();
l1_handler.execute().unwrap();
//...
}
spy_events
fn spy_events(spy_on: SpyOn) -> EventSpy
Creates EventSpy
instance which spies on events emitted by contracts defined under the spy_on
argument.
struct EventSpy {
events: Array<(ContractAddress, Event)>,
}
An event spy structure, along with the events collected so far in the test.
events
are mutable and can be updated with fetch_events
.
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.
enum SpyOn {
All: (),
One: ContractAddress,
Multiple: Array<ContractAddress>
}
Allows specifying which contracts you want to capture events from.
Implemented traits
EventFetcher
trait EventFetcher {
fn fetch_events(ref self: EventSpy);
}
Allows to update the structs' events field, from the spied contracts.
EventAssertions
trait EventAssertions<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 spy.
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" }
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) -> U;
}
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);
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);
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);
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.
--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.
--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. When not used, defaults to auto-estimation.
--class-hash, -c
Optional.
Class hash of a custom OpenZeppelin account contract declared to the network.
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
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.
Max fee for transaction. If not provided, max fee will be automatically estimated.
--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.
Max fee for the transaction. If not provided, max fee will be automatically estimated.
--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.
Max fee for the transaction. If not provided, it will be automatically estimated.
--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 either outputs it to a new file or to the standard output.
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.
Max fee for the transaction. If not provided, max fee will be automatically estimated.
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.
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 tagerrors
- 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, max_fee: Option<felt252>, 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,
}
contract_name
- name of a contract as Cairo string. It is a name of the contract (part aftermod
keyword) e.g."HelloStarknet"
.max_fee
- max fee for declare transaction. If not provided, max fee will be automatically estimated.nonce
- nonce for declare transaction. If not provided, nonce will be set automatically.
use sncast_std::{declare, DeclareResult};
fn main() {
let max_fee = 9999999;
let declare_result = declare("HelloStarknet", 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, max_fee: Option<felt252>, 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,
}
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.max_fee
- max fee for declare transaction. If not provided, max fee will be automatically estimated.nonce
- nonce for declare transaction. If not provided, nonce will be set automatically.
use sncast_std::{deploy, DeployResult};
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,
Option::Some(max_fee),
Option::Some(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>, max_fee: Option<felt252>, nonce: Option<felt252> ) -> Result<InvokeResult, ScriptCommandError>
Invokes a contract and returns InvokeResult
.
#[derive(Drop, Clone, Debug)]
pub struct InvokeResult {
pub transaction_hash: felt252,
}
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.max_fee
- max fee for declare transaction. If not provided, max fee will be automatically estimated.nonce
- nonce for declare transaction. If not provided, nonce will be set automatically.
use sncast_std::{invoke, InvokeResult};
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], 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);
}