logo

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 use snfoundryup or asdf. You can also create UNIVERSAL_SIERRA_COMPILER env var to make it visible for snforge.

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:

  1. Set up a development environment.
  2. Run cd starknet-foundry && cargo build --release. This will create a target directory.
  3. Move the target directory to the desired location (e.g. ~/.starknet-foundry).
  4. Add DESIRED_LOCATION/target/release/ to your PATH.

Installation on Windows

As for now, Starknet Foundry on Windows needs manual installation, but necessary steps are kept to minimum:

  1. Download the release archive matching your CPU architecture.
  2. 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
  1. Add path to the snfoundry\bin directory to your PATH environment variable.
  2. 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 of snforge

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 for snforge 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 and script).

💡 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 your Scarb.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's calldata argument) need to be serialized with Serde.

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:

  1. To access read/write methods of the state fields (in this case it's balance) you need to also import <member_name>ContractMemberStateTrait from your contract, where <member_name> is the name of the storage variable inside Storage struct.
  2. To access functions implemented directly on the state you need to also import an appropriate trait or function.
  3. 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 through contract_state_for_testing object and vice-versa.

Using Cheatcodes

ℹ️ Info To use cheatcodes you need to add snforge_std package as a dependency in your Scarb.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 using prank with CheatSpan::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:

  1. After the contract is called, we don't have to call fetch_events on the spy (it is done inside the assert_emitted method).
  2. assert_emitted takes the array snapshot of tuples (ContractAddress, event) we expect were emitted.
  3. 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:

  1. After contract deployment we created the spy with spy_events cheatcode. From this moment all events emitted by the SpyEventsChecker contract will be spied.
  2. We have to call fetch_events method on the created spy to load emitted events into it.
  3. When events are fetched they are loaded into the events property of our spy, and we can assert them.
  4. If the event is emitted by calling self.emit method, its hashed name is saved under the keys.at(0) (this way Starknet handles events)
  5. 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 the selector! 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 in lib.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 your Scarb.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 and block_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. If snfoundry.toml is present, and have these properties set, values provided using these flags will override values from snfoundry.toml. Learn more about snfoundry.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 and network parameters if add-profile flag was set in the account create command. Just pass profile 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 Foundry sncast 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 in Scarb.toml file.

📝 Note In the above example we supply sncast with --account and --url flags. If snfoundry.toml is present, and has the properties set, values provided using these flags will override values from snfoundry.toml. Learn more about snfoundry.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 and accounts-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 the contract address in an invoke call. Additionally, the id 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 to members 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 cheatcodes
  • CheatSpan - enum for specifying the number of target calls for a cheat
  • prank - changes the caller address for contracts, for a number of calls
  • start_prank - changes the caller address for contracts
  • stop_prank - cancels the prank / start_prank for contracts
  • roll - changes the block number for contracts, for a number of calls
  • start_roll - changes the block number for contracts
  • stop_roll - cancels the roll / start_roll for contracts
  • warp - changes the block timestamp for contracts, for a number of calls
  • start_warp - changes the block timestamp for contracts
  • stop_warp - cancels the warp / start_warp for contracts
  • elect - changes the sequencer address for contracts, for a number of calls
  • start_elect - changes the sequencer address for contracts
  • stop_elect - cancels the elect / start_elect for contracts
  • spoof - changes the transaction context for contracts, for a number of calls
  • start_spoof - changes the transaction context for contracts
  • stop_spoof - cancels the spoof / start_spoof for contracts
  • mock_call - mocks a number of contract calls to an entry point
  • start_mock_call - mocks contract call to an entry point
  • stop_mock_call - cancels the mock_call / start_mock_call for an entry point
  • get_class_hash - retrieves a class hash of a contract
  • replace_bytecode - replace the class hash of a contract
  • l1_handler_execute - executes a #[l1_handler] function to mock a message arriving from Ethereum
  • spy_events - creates EventSpy instance which spies on events emitted by contracts
  • store - stores values in targeted contact's storage
  • load - loads values directly from targeted contact's storage

ℹ️ Info To use cheatcodes you need to add snforge_std package as a development dependency in your Scarb.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 with CheatTarget::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. using stop_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 with CheatSpan::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 value
  • Some(n) means that the value will be set to the n 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 regular SafeDispatcher 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 address
  • function_selector - Selector of the #[l1_handler] function
  • from_address - Ethereum address of the contract that sends the message
  • payload - 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 a ContractClass which can be interacted with later
  • get_call_trace - gets current test call trace (with contracts interactions included)
  • fs - module containing functions for interacting with the filesystem
  • env - module containing functions for interacting with the system environment
  • signature - module containing struct and trait for creating ecdsa signatures

ℹ️ Info To use cheatcodes you need to add snforge_std package as a development dependency in your Scarb.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 is Cairo Prime either in decimal or 0x 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 \"

JSON files

  • Elements have to be either:
    • integers in range of [0, P) where P is Cairo 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

⚠️ 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 with Serde.

signature Module

Module containing KeyPair struct and interface for creating ecdsa signatures.

  • signature::stark_curve - implementation of KeyPairTrait for the STARK curve
  • signature::secp256k1_curve - implementation of KeyPairTrait for Secp256k1 Curve
  • signature::secp256r1_curve - implementation of KeyPairTrait 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 contract
  • deploy - deploys a contract
  • invoke - invokes a contract's function
  • call - calls a contract's function
  • get_nonce - gets account's nonce for a given block tag
  • errors - sncast_std error types reference

ℹ️ Info To use the library functions you need to add sncast_std package as a dependency in your Scarb.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 after mod 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 of pending or latest.
use sncast_std::{get_nonce};

fn main() {
    let nonce = get_nonce('latest');
    println!("nonce: {}", nonce);
    println!("debug nonce: {:?}", nonce);
}