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.

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.

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

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.

First Steps With Starknet Foundry

In this section we provide an overview of Starknet Foundry snforge command line tool. We demonstrate how to create a new project, compile, and test it.

To start a new project with Starknet Foundry, run snforge init

$ snforge init project_name

Let's check out the project structure

$ cd project_name
$ tree . -L 1
.
├── Scarb.lock
├── Scarb.toml
├── src
└── tests

2 directories, 2 files
  • src/ contains source code of all your contracts.
  • tests/ contains tests.
  • Scarb.toml contains configuration of the project as well as of snforge
  • Scarb.lock a locking mechanism to achieve reproducible dependencies when installing the project locally

And run tests with snforge test

$ snforge test
   Compiling project_name v0.1.0 (project_name/Scarb.toml)
    Finished release target(s) in 1 second

Collected 2 test(s) from project_name package
Running 0 test(s) from src/
Running 2 test(s) from tests/
[PASS] tests::test_contract::test_increase_balance (gas: ~170)
[PASS] tests::test_contract::test_cannot_increase_balance_with_zero_value (gas: ~104)
Tests: 2 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out

Using snforge With Existing Scarb Projects

To use snforge with existing Scarb projects, make sure you have declared the snforge_std package as your project development dependency.

Add the following line under [dev-dependencies] section in the Scarb.toml file.

# ...

[dev-dependencies]
snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.27.0" }

Make sure that the version in tag matches snforge. You can check the currently installed version with

$ snforge --version
snforge 0.27.0

It is also possible to add this dependency using scarb add command.

$ scarb add snforge_std \ 
 --dev \
 --git https://github.com/foundry-rs/starknet-foundry.git \
 --tag v0.27.0

Additionally, ensure that starknet-contract target is enabled in the Scarb.toml file.

# ...
[[target.starknet-contract]]

Scarb

Scarb is the package manager and build toolchain for Starknet ecosystem. Those coming from Rust ecosystem will find Scarb very similar to Cargo.

Starknet Foundry uses Scarb to:

One of the core concepts of Scarb is its manifest file - Scarb.toml. It can be also used to provide configuration for Starknet Foundry Forge. Moreover, you can modify behaviour of scarb test to run snforge test as described here.

📝 Note

Scarb.toml is specifically designed for configuring scarb packages and, by extension, is suitable 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

Displaying Resources Used During Tests

To track resources like builtins / syscalls that are used when running tests, use snforge test --detailed-resources.

$ snforge test --detailed-resources
Collected 1 test(s) from package_name package
Running 1 test(s) from src/
[PASS] package_name::tests::resources (gas: ~2213)
        steps: 881
        memory holes: 36
        builtins: ("range_check_builtin": 32)
        syscalls: (StorageWrite: 1, StorageRead: 1, CallContract: 1)

Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out

For more information about how starknet-foundry calculates those, see gas and resource estimation section.

Writing Tests

snforge lets you test standalone functions from your smart contracts. This technique is referred to as unit testing. You should write as many unit tests as possible as these are faster than integration tests.

Writing Your First Test

First, add the following code to the src/lib.cairo file:

fn sum(a: felt252, b: felt252) -> felt252 {
    return a + b;
}

#[cfg(test)]
mod tests {
    use super::sum;

    #[test]
    fn test_sum() {
        assert(sum(2, 3) == 5, 'sum incorrect');
    }
}

It is a common practice to keep your unit tests in the same file as the tested code. Keep in mind that all tests in src folder have to be in a module annotated with #[cfg(test)]. When it comes to integration tests, you can keep them in separate files in the tests directory. You can find a detailed explanation of how snforge collects tests here.

Now run snforge using a command:

$ snforge test
Collected 1 test(s) from package_name package
Running 1 test(s) from src/
[PASS] package_name::tests::test_sum
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out

Failing Tests

If your code panics, the test is considered failed. Here's an example of a failing test.

fn panicking_function() {
    let mut data = array![];
    data.append('aaa');
    panic(data)
}

#[cfg(test)]
mod tests {
    use super::panicking_function;
    
    #[test]
    fn failing() {
        panicking_function();
        assert(2 == 2, '2 == 2');
    }
}
$ snforge test
Collected 1 test(s) from package_name package
Running 1 test(s) from src/
[FAIL] package_name::tests::failing

Failure data:
    0x616161 ('aaa')

Tests: 0 passed, 1 failed, 0 skipped, 0 ignored, 0 filtered out

Failures:
    package_name::tests::failing

Expected Failures

Sometimes you want to mark a test as expected to fail. This is useful when you want to verify that an action fails as expected.

To mark a test as expected to fail, use the #[should_panic] attribute. You can pass the expected failure message as an argument to the attribute to verify that the test fails with the expected message with #[should_panic(expected: ('panic message', 'eventual second message',))].

#[cfg(test)]
mod tests {
    use core::panic_with_felt252;

    #[should_panic(expected: ('panic message', ))]
    #[test]
    fn should_panic_check_data() {
        panic_with_felt252('panic message');
    }
}
$ snforge test
Collected 1 test(s) from package_name package
Running 1 test(s) from src/
Running 0 test(s) from tests/
[PASS] package_name::tests::should_panic_check_data
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out

Ignoring Tests

Sometimes you may have tests that you want to exclude during most runs of snforge test. You can achieve it using #[ignore] - tests marked with this attribute will be skipped by default.

#[cfg(test)]
mod tests {
    #[test]
    #[ignore]
    fn ignored_test() {
        // test code
    }
}
$ snforge test
Collected 1 test(s) from package_name package
Running 1 test(s) from src/
Running 0 test(s) from tests/
[IGNORE] package_name::tests::ignored_test
Tests: 0 passed, 0 failed, 0 skipped, 1 ignored, 0 filtered out

To run only tests marked with the #[ignore] attribute use snforge test --ignored. To run all tests regardless of the #[ignore] attribute use snforge test --include-ignored.

Testing Smart Contracts

ℹ️ Info

To use the library functions designed for testing smart contracts, you need to add snforge_std package as a dependency in 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, in a new using_dispatchers package.

#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
    fn increase_balance(ref self: TContractState, amount: felt252);
    fn get_balance(self: @TContractState) -> felt252;
}

#[starknet::contract]
mod HelloStarknet {
    #[storage]
    struct Storage {
        balance: felt252,
    }

    #[abi(embed_v0)]
    impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
        // Increases the balance by the given amount.
        fn increase_balance(ref self: ContractState, amount: felt252) {
            self.balance.write(self.balance.read() + amount);
        }

        // Gets the balance.
        fn get_balance(self: @ContractState) -> felt252 {
            self.balance.read()
        }
    }
}

Note that the name after mod will be used as the contract name for testing purposes.

Writing Tests

Let's write a test that will deploy the HelloStarknet contract and call some functions.

use snforge_std::{ declare, ContractClassTrait };
use using_dispatchers::{ IHelloStarknetDispatcher, IHelloStarknetDispatcherTrait };

#[test]
fn call_and_invoke() {
    // First declare and deploy a contract
    let contract = declare("HelloStarknet").unwrap();
    // Alternatively we could use `deploy_syscall` here
    let (contract_address, _) = contract.deploy(@array![]).unwrap();

    // Create a Dispatcher object that will allow interacting with the deployed contract
    let dispatcher = IHelloStarknetDispatcher { contract_address };

    // Call a view function of the contract
    let balance = dispatcher.get_balance();
    assert(balance == 0, 'balance == 0');

    // Call a function of the contract
    // Here we mutate the state of the storage
    dispatcher.increase_balance(100);

    // Check that transaction took effect
    let balance = dispatcher.get_balance();
    assert(balance == 100, 'balance == 100');
}

📝 Note

Notice that the arguments to the contract's constructor (the deploy'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) {
            panic(array!['PANIC', 'DAYTAH']);
        }

        fn do_a_string_panic(self: @ContractState) {
            // A macro which allows panicking with a ByteArray (string) instance
            panic!("This is panicking with a string, which can be longer than 31 characters");
        }
    }
}

If we called this function in a test, it would result in a failure.

#[test]
fn failing() {
    let contract = declare("HelloStarknet").unwrap();
    let (contract_address, _) = contract.deploy(@array![]).unwrap();
    let dispatcher = IHelloStarknetDispatcher { contract_address };

    dispatcher.do_a_panic();
}
$ snforge test
Collected 1 test(s) from package_name package
Running 0 test(s) from src/
Running 1 test(s) from tests/
[FAIL] tests::failing

Failure data:
    (0x50414e4943 ('PANIC'), 0x444159544148 ('DAYTAH'))

Tests: 0 passed, 1 failed, 0 skipped, 0 ignored, 0 filtered out

Failures:
    tests::failing

SafeDispatcher

Using SafeDispatcher we can test that the function in fact panics with an expected message. Safe dispatcher is a special kind of dispatcher, which are not allowed in contracts themselves, but are available for testing purposes.

They allow using the contract without automatically unwrapping the result, which allows to catch the error like shown below.

// Add those to import safe dispatchers, which are autogenerated, like regular dispatchers
use using_dispatchers::{ IHelloStarknetSafeDispatcher, IHelloStarknetSafeDispatcherTrait };

#[test]
#[feature("safe_dispatcher")]
fn handling_errors() {
    // ...
    let contract = declare("HelloStarknet").unwrap();
    let (contract_address, _) = contract.deploy(@calldata).unwrap();
    let safe_dispatcher = IHelloStarknetSafeDispatcher { contract_address };

    match safe_dispatcher.do_a_panic() {
        Result::Ok(_) => panic!("Entrypoint did not panic"),
        Result::Err(panic_data) => {
            assert(*panic_data.at(0) == 'PANIC', *panic_data.at(0));
            assert(*panic_data.at(1) == 'DAYTAH', *panic_data.at(1));
        }
    };
}

Now the test passes as expected.

$ snforge test
Collected 1 test(s) from package_name package
Running 0 test(s) from src/
Running 1 test(s) from tests/
[PASS] tests::handling_errors
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out

Similarly, you can handle the panics which use ByteArray as an argument (like an assert! or panic! macro)

// Necessary utility function import
use snforge_std::byte_array::try_deserialize_bytearray_error;

#[test]
#[feature("safe_dispatcher")]
fn handling_string_errors() {
    // ...
    let contract = declare("HelloStarknet").unwrap();
    let (contract_address, _) = contract.deploy(@array![]).unwrap();
    let safe_dispatcher = IHelloStarknetSafeDispatcher { contract_address };
    
    match safe_dispatcher.do_a_string_panic() {
        Result::Ok(_) => panic!("Entrypoint did not panic"),
        Result::Err(panic_data) => {
            let str_err = try_deserialize_bytearray_error(panic_data.span()).expect('wrong format');
            assert(
                str_err == "This is panicking with a string, which can be longer than 31 characters", 
                'wrong string received'
            );
        }
    };
}

You also could skip the de-serialization of the panic_data, and not use try_deserialize_bytearray_error, but this way you can actually use assertions on the ByteArray that was used to panic.

📝 Note

To operate with SafeDispatcher it's required to annotate its usage with #[feature("safe_dispatcher")].

There are 3 options:

  • module-level declaration
    #[feature("safe_dispatcher")]
    mod my_module;    
  • function-level declaration
    #[feature("safe_dispatcher")]
    fn my_function() { ... }    
  • directly before the usage
    #[feature("safe_dispatcher")]
    let result = safe_dispatcher.some_function();

Expecting Test Failure

Sometimes the test code failing can be a desired behavior. Instead of manually handling it, you can simply mark your test as #[should_panic(...)]. See here for more details.

Testing Contracts' Internals

Sometimes, you want to test a function which uses Starknet context (like block number, timestamp, storage access) without deploying the actual contract.

Since every test is treated like a contract, using the aforementioned pattern you can test:

  • functions which are not available through the interface (but your contract uses them)
  • functions which are internal
  • functions performing specific operations on the contracts' storage or context data
  • library calls directly in the tests

Utilities For Testing Internals

To facilitate such use cases, we have a handful of utilities which make a test behave like a contract.

contract_state_for_testing() - State of Test Contract

This is a function generated by the #[starknet::contract] macro. It can be used to test some functions which accept the state as an argument, see the example below:

#[starknet::contract]
mod Contract {
    #[storage]
    struct Storage {
        balance: felt252, 
    }
    
    #[generate_trait]
    impl InternalImpl of InternalTrait {
        fn internal_function(self: @ContractState) -> felt252 {
            self.balance.read()
        }
    }

    fn other_internal_function(self: @ContractState) -> felt252 {
        self.balance.read() + 5
    }
}

use Contract::balanceContractMemberStateTrait;   // <--- Ad. 1
use Contract::{ InternalTrait, other_internal_function };   // <--- Ad. 2

#[test]
fn test_internal() {
    let mut state = Contract::contract_state_for_testing();        // <--- Ad. 3
    state.balance.write(10);
    
    let value = state.internal_function();
    assert(value == 10, 'Incorrect storage value');
    
    let other_value = other_internal_function(@state);
    assert(value == 15, 'Incorrect return value');
}

This code contains some caveats:

  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 (cheat_caller_address, cheat_block_timestamp, cheat_block_number, ...)
  • Spy for events emitted in the test

Example usages:

1. Mocking the context info

Example for cheat_block_number, same can be implemented for cheat_caller_address/cheat_block_timestamp/elect etc.

use result::ResultTrait;
use box::BoxTrait;
use starknet::ContractAddress;
use snforge_std::{
    start_cheat_block_number, stop_cheat_block_number,
    test_address
};

#[test]
fn test_cheat_block_number_test_state() {
    let test_address: ContractAddress = test_address();
    let old_block_number = starknet::get_block_info().unbox().block_number;

    start_cheat_block_number(test_address, 234);
    let new_block_number = starknet::get_block_info().unbox().block_number;
    assert(new_block_number == 234, 'Wrong block number');

    stop_cheat_block_number(test_address);
    let new_block_number = starknet::get_block_info().unbox().block_number;
    assert(new_block_number == old_block_number, 'Block num did not change back');
}

2. Spying for events

You can use both starknet::emit_event_syscall, and the spies will capture the events, emitted in a #[test] function, if you pass the test_address() as a spy parameter (or spy on all events).

Given the emitting contract implementation:

#[starknet::contract]
mod Emitter {
    use result::ResultTrait;
    use starknet::ClassHash;
    
    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        ThingEmitted: ThingEmitted
    }
    
    #[derive(Drop, starknet::Event)]
    struct ThingEmitted {
        thing: felt252
    }

    #[storage]
    struct Storage {}

    #[external(v0)]
    fn emit_event(
        ref self: ContractState,
    ) {
        self.emit(Event::ThingEmitted(ThingEmitted { thing: 420 }));
    }
}

You can implement this test:

use array::ArrayTrait;
use snforge_std::{ 
    declare, ContractClassTrait, spy_events, 
    EventSpy, EventSpyTrait, EventSpyAssertionsTrait, 
    Event, test_address 
};
#[test]
fn test_expect_event() {
    let contract_address = test_address();
    let mut spy = spy_events();
    
    let mut testing_state = Emitter::contract_state_for_testing();
    Emitter::emit_event(ref testing_state);
    
    spy.assert_emitted(
        @array![
            (
                contract_address,
                Emitter::Event::ThingEmitted(Emitter::ThingEmitted { thing: 420 })
            )
        ]
    )
}

You can also use the starknet::emit_event_syscall directly in the tests:

use array::ArrayTrait;
use result::ResultTrait;
use starknet::SyscallResultTrait;
use starknet::ContractAddress;
use snforge_std::{ declare, ContractClassTrait, spy_events, EventSpy, EventSpyTrait,
    EventSpyAssertionsTrait, Event, test_address };

#[test]
fn test_expect_events_simple() {
    let test_address = test_address();
    let mut spy = spy_events();

    starknet::emit_event_syscall(array![1234].span(), array![2345].span()).unwrap_syscall();

    spy.assert_emitted(@array![
        (
            contract_address,
            Event { keys: array![1234], data: array![2345] }
        )
    ]);

    assert(spy.events.len() == 0, 'There should be no events left');
}

Using Library Calls With the Test State Context

Using the above utilities, you can avoid deploying a mock contract, to test a library_call with a LibraryCallDispatcher.

For contract implementation:

 #[starknet::contract]
mod LibraryContract {
    use result::ResultTrait;
    use starknet::ClassHash;
    use starknet::library_call_syscall;

    #[storage]
    struct Storage {
        value: felt252
    }

    #[external(v0)]
    fn get_value(
        self: @ContractState,
    ) -> felt252 {
       self.value.read()
    }

    #[external(v0)]
    fn set_value(
        ref self: ContractState,
        number: felt252
    ) {
       self.value.write(number);
    }
}

We use the SafeLibraryDispatcher like this:

use result::ResultTrait;
use starknet::{ ClassHash, library_call_syscall, ContractAddress };
use snforge_std::{ declare };

#[starknet::interface]
trait ILibraryContract<TContractState> {
    fn get_value(
        self: @TContractState,
    ) -> felt252;

    fn set_value(
        ref self: TContractState,
        number: felt252
    );
}

#[test]
fn test_library_calls() {
    let class_hash = declare("LibraryContract").class_hash;
    let lib_dispatcher = ILibraryContractSafeLibraryDispatcher { class_hash };
    let value = lib_dispatcher.get_value().unwrap();
    assert(value == 0, 'Incorrect state');
    lib_dispatcher.set_value(10);
    let value = lib_dispatcher.get_value().unwrap();
    assert(value == 10, 'Incorrect state');
}

⚠️ Warning

This library call will write to the test_address memory segment, so it can potentially overwrite the changes you make to the memory 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_cheat_caller_address cheatcode to change the caller address, so it passes our validation.

Cheating an Address

use snforge_std::{ declare, ContractClassTrait, start_cheat_caller_address };

#[test]
fn call_and_invoke() {
    let contract = declare("HelloStarknet").unwrap();
    let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap();
    let dispatcher = IHelloStarknetDispatcher { contract_address };

    let balance = dispatcher.get_balance();
    assert(balance == 0, 'balance == 0');

    // Change the caller address to 123 when calling the contract at the `contract_address` address
    start_cheat_caller_address(contract_address, 123.try_into().unwrap());

    dispatcher.increase_balance(100);

    let balance = dispatcher.get_balance();
    assert(balance == 100, 'balance == 100');
}

The test will now pass without an error

$ snforge test
Collected 1 test(s) from package_name package
Running 0 test(s) from src/
Running 1 test(s) from tests/
[PASS] tests::call_and_invoke
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out

Canceling the Cheat

Most cheatcodes come with corresponding start_ and stop_ functions that can be used to start and stop the state change. In case of the start_cheat_caller_address, we can cancel the address change using stop_cheat_caller_address

use snforge_std::{stop_cheat_caller_address};

#[test]
fn call_and_invoke() {
    // ...

    // The address when calling contract at the `contract_address` address will no longer be changed
    stop_cheat_caller_address(contract_address);

    // This will fail
    dispatcher.increase_balance(100);

    let balance = dispatcher.get_balance();
    assert(balance == 100, 'balance == 100');
}
$ snforge test
Collected 1 test(s) from package_name package
Running 0 test(s) from src/
Running 1 test(s) from tests/
[FAIL] tests::call_and_invoke, 0 ignored, 0 filtered out

Failure data:
    0x75736572206973206e6f7420616c6c6f776564 ('user is not allowed')

Tests: 0 passed, 1 failed, 0 skipped, 0 ignored, 0 filtered out

Failures:
    tests::call_and_invoke

Cheating the Constructor

Most of the cheatcodes like cheat_caller_address, mock_call, cheat_block_timestamp, cheat_block_number, elect do work in the constructor of the contracts.

Let's say, that you have a contract that saves the caller address (deployer) in the constructor, and you want it to be pre-set to a certain value.

To cheat_caller_address the constructor, you need to start_cheat_caller_address before it is invoked, with the right address. To achieve this, you need to precalculate the address of the contract by using the precalculate_address function of ContractClassTrait on the declared contract, and then use it in start_cheat_caller_address as an argument:

use snforge_std::{ declare, ContractClassTrait, start_cheat_caller_address };

#[test]
fn mock_constructor_with_cheat_caller_address() {
    let contract = declare("HelloStarknet").unwrap();
    let constructor_arguments = @ArrayTrait::new();

    // Precalculate the address to obtain the contract address before the constructor call (deploy) itself
    let contract_address = contract.precalculate_address(constructor_arguments);

    // Change the caller address to 123 before the call to contract.deploy
    start_cheat_caller_address(contract_address, 123.try_into().unwrap());

    // The constructor will have 123 set as the caller address
    contract.deploy(constructor_arguments).unwrap();
}

Setting Cheatcode Span

Sometimes it's useful to have a cheatcode work only for a certain number of target calls.

That's where CheatSpan comes in handy.

enum CheatSpan {
    Indefinite: (),
    TargetCalls: usize,
}

To set span for a cheatcode, use cheat_caller_address / cheat_block_timestamp / cheat_block_number / etc.

cheat_caller_address(contract_address, new_caller_address, CheatSpan::TargetCalls(1))

Calling a cheatcode with CheatSpan::TargetCalls(N) is going to activate the cheatcode for N calls to a specified contract address, after which it's going to be automatically canceled.

Of course the cheatcode can still be canceled before its CheatSpan goes down to 0 - simply call stop_cheat_caller_address on the target manually.

ℹ️ Info

Using start_cheat_caller_address is equivalent to using cheat_caller_address with CheatSpan::Indefinite.

To better understand the functionality of CheatSpan, here's a full example:

use snforge_std::{
    declare, ContractClass, ContractClassTrait, cheat_caller_address, CheatSpan
};

#[test]
#[feature("safe_dispatcher")]
fn call_and_invoke() {
    let contract = declare("HelloStarknet").unwrap();
    let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap();
    let safe_dispatcher = IHelloStarknetSafeDispatcher { contract_address };

    let balance = safe_dispatcher.get_balance().unwrap();
    assert_eq!(balance, 0);

    // Function `increase_balance` from HelloStarknet contract
    // requires the caller_address to be 123
    let cheat_caller_addressed_address: ContractAddress = 123.try_into().unwrap();

    // Change the caller address for the contract_address for a span of 2 target calls (here, calls to contract_address)
    cheat_caller_address(contract_address, cheat_caller_addressed_address, CheatSpan::TargetCalls(2));

    // Call #1 should succeed
    let call_1_result = safe_dispatcher.increase_balance(100);
    assert!(call_1_result.is_ok());

    // Call #2 should succeed
    let call_2_result = safe_dispatcher.increase_balance(100);
    assert!(call_2_result.is_ok());

    // Call #3 should fail, as the cheat_caller_address cheatcode has been canceled
    let call_3_result = safe_dispatcher.increase_balance(100);
    assert!(call_3_result.is_err());

    let balance = safe_dispatcher.get_balance().unwrap();
    assert_eq!(balance, 200);
}

Testing events

Examples are based on the following SpyEventsChecker contract implementation:

#[starknet::contract]
pub mod SpyEventsChecker {
    // ...
    #[storage]
    struct Storage {}
    
    #[event]
    #[derive(Drop, starknet::Event)]
    pub enum Event {
        FirstEvent: FirstEvent
    }

    #[derive(Drop, starknet::Event)]
    pub struct FirstEvent {
        pub some_data: felt252
    }
    
    #[external(v0)]
    fn emit_one_event(ref self: ContractState, some_data: felt252) {
        self.emit(FirstEvent { some_data });
    }

    // ...
}

Asserting emission with assert_emitted method

This is the simpler way, in which you don't have to fetch the events explicitly. See the below code for reference:

use snforge_std::{
    declare, ContractClassTrait, 
    spy_events,
    EventSpyAssertionsTrait,  // Add for assertions on the EventSpy 
};

use SpyEventsChecker;

#[starknet::interface]
trait ISpyEventsChecker<TContractState> {
    fn emit_one_event(ref self: TContractState, some_data: felt252);
}

#[test]
fn test_simple_assertions() {
    let contract = declare("SpyEventsChecker").unwrap();
    let (contract_address, _) = contract.deploy(@array![]).unwrap();
    let dispatcher = ISpyEventsCheckerDispatcher { contract_address };

    let mut spy = spy_events();  // Ad. 1

    dispatcher.emit_one_event(123);

    spy.assert_emitted(@array![  // Ad. 2
        (
            contract_address,
            SpyEventsChecker::Event::FirstEvent(
                SpyEventsChecker::FirstEvent { some_data: 123 }
            )
        )
    ]);
}

Let's go through the code:

  1. After contract deployment, we created the spy using spy_events cheatcode. From this moment all emitted events will be spied.
  2. Asserting is done using the assert_emitted method. It takes an array snapshot of (ContractAddress, event) tuples we expect that were emitted.

📝 Note We can pass events defined in the contract and construct them like in the self.emit method!

Asserting lack of event emission with assert_not_emitted

In cases where you want to test an event was not emitted, use the assert_not_emitted function. It works similarly as assert_emitted with the only difference that it panics if an event was emitted during the execution.

Given the example above, we can check that a different FirstEvent was not emitted:

spy.assert_not_emitted(@array![
    (
        contract_address,
        SpyEventsChecker::Event::FirstEvent(
            SpyEventsChecker::FirstEvent { some_data: 456 }
        )
    )
]);

Note that both the event name and event data are checked. If a function emitted an event with the same name but a different payload, the assert_not_emitted function will pass.

Asserting the events manually

If you wish to assert the data manually, you can do that on the Events structure. Simply call get_events() on your EventSpy and access events field on the returned Events value. Then, you can access the events and assert data by yourself.

use snforge_std::{
    declare, ContractClassTrait, 
    spy_events, 
    EventSpyAssertionsTrait, 
    EventSpyTrait,  // Add for fetching events directly  
    Event,          // A structure describing a raw `Event`
};

#[starknet::interface]
trait ISpyEventsChecker<TContractState> {
    fn emit_one_event(ref self: TContractState, some_data: felt252);
}

#[test]
fn test_complex_assertions() {
    let contract = declare("SpyEventsChecker").unwrap();
    let (contract_address, _) = contract.deploy(@array![]).unwrap();
    let dispatcher = ISpyEventsCheckerDispatcher { contract_address };

    let mut spy = spy_events(); // Ad 1.

    dispatcher.emit_one_event(123);

    let events = spy.get_events();  // Ad 2.

    assert(events.events.len() == 1, 'There should be one event');

    let (from, event) = events.events.at(0); // Ad 3.
    assert(from == @contract_address, 'Emitted from wrong address');
    assert(event.keys.len() == 1, 'There should be one key');
    assert(event.keys.at(0) == @selector!("FirstEvent"), 'Wrong event name'); // Ad 4.
    assert(event.data.len() == 1, 'There should be one data');
}

Let's go through important parts of the provided code:

  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 get_events method on the created spy to fetch our events and get the Events structure.
  3. To get our particular event, we need to access the events property and get the event under an index. Since events is an array holding a tuple of ContractAddress and Event, we unpack it using let (from, event).
  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)

📝 Note To assert the name property we have to hash a string with the selector! macro.

Filtering Events

Sometimes, when you assert the events manually, you might not want to get all the events, but only ones from a particular address. You can address that by using the method emitted_by on the Events structure.

use snforge_std::{
    declare, ContractClassTrait, 
    spy_events, 
    EventSpyAssertionsTrait, 
    EventSpyTrait,  
    Event,
    EventsFilterTrait, // Add for filtering the Events object (result of `get_events`) 
};

use SpyEventsChecker;

#[starknet::interface]
trait ISpyEventsChecker<TContractState> {
    fn emit_one_event(ref self: TContractState, some_data: felt252);
}

#[test]
fn test_assertions_with_filtering() {
    let contract = declare("SpyEventsChecker").unwrap();
    let (first_address, _) = contract.deploy(@array![]).unwrap();
    let (second_address, _) = contract.deploy(@array![]).unwrap();

    let first_dispatcher = ISpyEventsCheckerDispatcher { contract_address: first_address };
    let second_dispatcher = ISpyEventsCheckerDispatcher { contract_address: second_address };

    let mut spy = spy_events();

    first_dispatcher.emit_one_event(123);
    second_dispatcher.emit_one_event(234);
    second_dispatcher.emit_one_event(345);

    let events_from_first_address = spy.get_events().emitted_by(first_address);
    let events_from_second_address = spy.get_events().emitted_by(second_address);

    let (from_first, event_from_first) = events_from_first_address.events.at(0);
    assert(from_first == @first_address, 'Emitted from wrong address');
    assert(event_from_first.data.at(0) == @123.into(), 'Data should be 123');

    let (from_second_one, event_from_second_one) = events_from_second_address.events.at(0);
    assert(from_second_one == @second_address, 'Emitted from wrong address');
    assert(event_from_second_one.data.at(0) == @234.into(), 'Data should be 234');

    let (from_second_two, event_from_second_two) = events_from_second_address.events.at(1);
    assert(from_second_two == @second_address, 'Emitted from wrong address');
    assert(event_from_second_two.data.at(0) == @345.into(), 'Data should be 345');
}

events_from_first_address has events emitted by the first contract only. Similarly, events_from_second_address has events emitted by the second contract.

Asserting Events Emitted With emit_event_syscall

Events emitted with emit_event_syscall could have nonstandard (not defined anywhere) keys and data. They can also be asserted with spy.assert_emitted method.

Let's add such a method in the SpyEventsChecker contract:

use core::starknet::syscalls::emit_event_syscall;
use core::starknet::SyscallResultTrait;

#[external(v0)]
fn emit_event_with_syscall(ref self: ContractState, some_key: felt252, some_data: felt252) {
    emit_event_syscall(array![some_key].span(), array![some_data].span()).unwrap_syscall();
}

And add a test for it:

use snforge_std::{
    declare, ContractClassTrait, 
    spy_events, 
    EventSpyAssertionsTrait, 
    EventSpyTrait,  
    Event,
    EventsFilterTrait, 
};

#[starknet::interface]
trait ISpyEventsChecker<TContractState> {
    fn emit_event_with_syscall(ref self: TContractState, some_key: felt252, some_data: felt252);
}

#[test]
fn test_nonstandard_events() {
    let contract = declare("SpyEventsChecker").unwrap();
    let (contract_address, _) = contract.deploy(@array![]).unwrap();
    let dispatcher = ISpyEventsCheckerDispatcher { contract_address };

    let mut spy = spy_events();
    dispatcher.emit_event_with_syscall(123, 456);

    spy.assert_emitted(@array![
        (
            contract_address,
            Event { keys: array![123], data: array![456] }
        )
    ]);
}

Using Event struct from the snforge_std library we can easily assert nonstandard events. This also allows for testing the events you don't have the code of, or you don't want to import those.

Testing messages to L1

There exists a functionality allowing you to spy on messages sent to L1, similar to spying events.

Check the appendix for an exact API, structures and traits reference

Asserting messages to L1 is much simpler, since they are not wrapped with any structures in Cairo code (they are a plain felt252 array and an L1 address). In snforge they are expressed with a structure:

/// Raw message to L1 format (as seen via the RPC-API), can be used for asserting the sent messages.
struct MessageToL1 {
    /// An ethereum address where the message is destined to go
    to_address: EthAddress,
    /// Actual payload which will be delivered to L1 contract
    payload: Array<felt252>
}

Similarly, you can use snforge library and call spy_messages_to_l1() to initiate a spy:

use snforge_std::{spy_messages_to_l1};

// ...

#[test]
fn test_spying_l1_messages() {
    let mut spy = spy_messages_to_l1();
    // ...
}

With the spy ready to use, you can execute some code, and make the assertions:

  1. Either with the spy directly by using assert_sent/assert_not_sent methods from MessageToL1SpyAssertionsTrait trait:
use snforge_std::{spy_messages_to_l1, MessageToL1SpyAssertionsTrait, MessageToL1};

// ...

#[test]
fn test_spying_l1_messages() {
    let mut spy = spy_messages_to_l1();
    // ...
    spy.assert_sent(
        @array![
            (
                contract_address, // Message sender
                MessageToL1 {     // Message content (receiver and payload)
                    to_address: 0x123.try_into().unwrap(), 
                    payload: array![123, 321, 420]
                }
            )
        ]
    );
}
  1. Or use the messages' contents directly via get_messages() method of the MessageToL1Spy trait:
use snforge_std::{
    spy_messages_to_l1, MessageToL1, 
    MessageToL1SpyAssertionsTrait, 
    MessageToL1FilterTrait, 
    MessageToL1SpyTrait
};

// ...

#[test]
fn test_spying_l1_messages() {
    let mut spy = spy_messages_to_l1();
    
    let messages = spy.get_messages();
    
    // Use filtering optionally on MessagesToL1 instance
    let messages_from_specific_address = messages.sent_by(sender_address);
    let messages_to_specific_address   = messages_from_specific_address.sent_to(receiver_eth_address);

    // Get the messages from the MessagesToL1 structure 
    let (from, message) = messages_to_specific_address.messages.at(0);

    // Assert the sender
    assert!(from == sender_address, "Sent from wrong address");
    // Assert the MessageToL1 fields
    assert!(message.to_address == receiver_eth_address, "Wrong eth address of the receiver");
    assert!(message.payload.len() == 3, "There should be 3 items in the data");
    assert!(*message.payload.at(1) == 421, "Expected 421 in payload");
}

Testing Scarb Workspaces

snforge supports Scarb Workspaces. To make sure you know how workspaces work, check Scarb documentation here.

Workspaces With Root Package

When running snforge test in a Scarb workspace with a root package, it will only run tests inside the root package.

For a project structure like this

$ tree . -L 3
.
├── Scarb.toml
├── crates
│   ├── addition
│   │   ├── Scarb.toml
│   │   ├── src
│   │   └── tests
│   └── fibonacci
│       ├── Scarb.toml
│       └── src
├── tests
│   └── test.cairo
└── src
    └── lib.cairo

only the tests in ./src and ./tests folders will be executed.


$ snforge test
Collected 1 test(s) from hello_workspaces package
Running 1 test(s) from src/
[PASS] hello_workspaces::tests::test_simple
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out

To select the specific package to test, pass a --package package_name (or -p package_name for short) flag. You can also run snforge test from the package directory to achieve the same effect.

$ snforge test --package addition
Collected 2 test(s) from addition package
Running 1 test(s) from src/
[PASS] addition::tests::it_works
Running 1 test(s) from tests/
[PASS] tests::test_simple::simple_case
Tests: 2 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out

You can also pass --workspace flag to run tests for all packages in the workspace.

$ snforge test --workspace
Collected 2 test(s) from addition package
Running 1 test(s) from src/
[PASS] addition::tests::it_works
Running 1 test(s) from tests/
[PASS] tests::test_simple::simple_case
Tests: 2 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out


Collected 1 test(s) from fibonacci package
Running 1 test(s) from src/
[PASS] fibonacci::tests::it_works
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out


Collected 1 test(s) from hello_workspaces package
Running 1 test(s) from src/
[PASS] hello_workspaces::tests::test_simple
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out

--package and --workspace flags are mutually exclusive, adding both of them to a snforge test command will result in an error.

Virtual Workspaces

Running snforge test command in a virtual workspace (a workspace without a root package) outside any package will by default run tests for all the packages. It is equivalent to running snforge test with the --workspace flag.

To select a specific package to test, you can use the --package flag the same way as in regular workspaces or run snforge test from the package directory.

Test Collection

snforge considers all functions in your project marked with #[test] attribute as tests. By default, test functions run without any arguments. However, adding any arguments to function signature will enable fuzz testing for this test case.

snforge will collect tests only from these places:

  • any files reachable from the package root (declared as mod 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

In some instances, it's not possible for contracts to expose API that we'd like to use in order to initialize the contracts before running some tests. For those cases snforge exposes storage-related cheatcodes, which allow manipulating the storage directly (reading and writing).

In order to obtain the variable address that you'd like to write to, or read from, you need to use either:

  • selector! macro - if the variable is not a mapping
  • map_entry_address function in tandem with selector! - for key-value pair of a map variable
  • starknet::storage_access::storage_address_from_base

Example: Felt-only storage

This example uses only felts for simplicity

#[starknet::contract]
mod Contract {
    #[storage]
    struct Storage {
        plain_felt: felt252,
        mapping: LegacyMap<felt252, felt252>,
    }
}

// ...
use snforge_std::{ store, load, map_entry_address };

#[test]
fn store_and_load_with_plain_felt() {
    // ...
    store(contract_address, selector!("plain_felt"), array![123].span());
    // plain_felt = 123
    let loaded = load(contract_address, selector!("plain_felt"), 1);
    assert(loaded.len() == 1, 'Wrong loaded vector');
    assert(*loaded.at(0) == 123, 'Wrong loaded value');
}


#[test]
fn store_and_load_map_entry() {
    // ...
    store(
        contract_address, 
        map_entry_address(
            selector!("mapping"), // Providing variable name
            array![123].span(),   // Providing mapping key 
        ),
        array![321].span()
    );
    
    // mapping = { 123: 321, ... }
    let loaded = load(
        contract_address, 
        map_entry_address(
            selector!("mapping"), // Providing variable name
            array![123].span(),   // Providing mapping key 
        ),
        1,
    );
    
    assert(loaded.len() == 1, 'Expected 1 felt loaded');
    assert(*loaded.at(0) == 321, 'Expected 321 value loaded');
}

Example: Complex structures in storage

This example uses a complex key and value, with default derived serialization methods (via #[derive(starknet::Store)]).

use snforge_std::{ store, load, map_entry_address };

#[starknet::contract]
mod Contract {
    #[derive(Serde)]
    struct MapKey {
        a: felt252,
        b: felt252,
    }

    // Required for lookup of complex_mapping values
    // This is consistent with `map_entry_address`, which uses pedersen hashing of keys
    impl StructuredKeyHash of LegacyHash<MapKey> {
        fn hash(state: felt252, value: MapKey) -> felt252 {
            let state = LegacyHash::<felt252>::hash(state, value.a);
            LegacyHash::<felt252>::hash(state, value.b)
        }
    }

    #[derive(Serde, starknet::Store)]
    struct MapValue {
        a: felt252,
        b: felt252,
    }
    
    // Serialization of keys and values with `Serde` to make usage of `map_entry_address` easier 
    impl MapKeyIntoSpan of Into<MapKey, Span<felt252>> {
        fn into(self: MapKey) -> Span<felt252> {
            let mut serialized_struct: Array<felt252> = array![];
            self.serialize(ref serialized_struct);
            serialized_struct.span()
        }
    }
    impl MapValueIntoSpan of Into<MapValue, Span<felt252>> {
        fn into(self: MapValue) -> Span<felt252> {
            let mut serialized_struct: Array<felt252> = array![];
            self.serialize(ref serialized_struct);
            serialized_struct.span()
        }
    }
    
    #[storage]
    struct Storage {
        complex_mapping: LegacyMap<MapKey, MapValue>,
    }
}

// ...

#[test]
fn store_in_complex_mapping() {
    // ...
    let k = MapKey { a: 111, b: 222 };
    let v = MapValue { a: 123, b: 456 };
    
    store(
        contract_address, 
        map_entry_address(        // Uses pedersen hashing for address calculation
            selector!("mapping"), // Providing variable name
            k.into()              // Providing mapping key
        ),
        v.into()
    );
    
    // complex_mapping = { 
    //     hash(k): 123,
    //     hash(k) + 1: 456 
    //     ...
    // }
    
    let loaded = load(
        contract_address, 
        selector!("elaborate_struct"), // Providing variable name
        2,                             // Size of the struct in felts
    );
    
    assert(loaded.len() == 2, 'Expected 1 felt loaded');
    assert(*loaded.at(0) == 123, 'Expected 123 value loaded');
    assert(*loaded.at(1) == 456, 'Expected 456 value loaded');
}

⚠️ Warning

Complex data can often times be packed in a custom manner (see this pattern) to optimize costs. If that's the case for your contract, make sure to handle deserialization properly - standard methods might not work. Use those cheatcode as a last-resort, for cases that cannot be handled via contract's API!

📝 Note

The load cheatcode will return zeros for memory you haven't written into yet (it is a default storage value for Starknet contracts' storage).

Example with storage_address_from_base

This example uses storage_address_from_base with address function of the storage variable.

To retrieve storage address of a given field, you need to import {field_name}ContractMemberStateTrait from the contract.

#[starknet::contract]
mod Contract {
    #[storage]
    struct Storage {
        map: LegacyMap::<(u8, u32), u32>,
    }
}

// ...
use starknet::storage_access::storage_address_from_base;
use snforge_std::{ store, load };
use Contract::mapContractMemberStateTrait;

#[test]
fn update_mapping() {
    let key = (1_u8, 10_u32);
    let data = 42_u32;

    // ...
    let mut state = Contract::contract_state_for_testing();
    let storage_address: felt252 = storage_address_from_base(
        state.map.address(key)
    )
    .into();
    let storage_value: Span<felt252> = array![data.into()].span();
    store(contract_address, storage_address, storage_value);

    let read_data: u32 = load(contract_address, storage_address, 1).at(0).try_into().unwrap():
    assert_eq!(read_data, data, "Storage update failed")
}

Profiling

Profiling is what allows developers to get more insight into how the transaction is executed. You can inspect the call tree, see how many resources are used for different parts of the execution, and more!

Integration with cairo-profiler

snforge is able to produce a file with a trace for each passing test (excluding fuzz tests). All you have to do is use the --save-trace-data flag:

$ snforge test --save-trace-data

The files with traces will be saved to snfoundry_trace directory. Each one of these files can then be used as an input for the cairo-profiler.

If you want snforge to call cairo-profiler on generated files automatically, use --build-profile flag:

$ snforge test --build-profile

sncast Overview

Starknet Foundry sncast is a command line tool for performing Starknet RPC calls. With it, you can easily interact with Starknet contracts!

💡 Info At the moment, sncast only supports contracts written in Cairo v1 and v2.

⚠️ Warning Currently, support is only provided for accounts that use the default signature based on the Stark curve.

How to Use sncast

To use sncast, run the sncast command followed by a subcommand (see available commands):

$ sncast <subcommand>

If snfoundry.toml is present and configured with [sncast.default], url, accounts-file and account name will be taken from it. You can, however, overwrite their values by supplying them as flags directly to sncast cli.

💡 Info Some transactions (like declaring, deploying or invoking) require paying a fee, and they must be signed.

Examples

General Example

Let's use sncast to call a contract's function:

$ sncast --account myuser \
    --url http://127.0.0.1:5050 \
    call \
    --contract-address 0x38b7b9507ccf73d79cb42c2cc4e58cf3af1248f342112879bfdf5aa4f606cc9 \
    --function get \
    --calldata 0x0 \
    --block-id latest

command: call
response: [0x0]

📝 Note In the above example we supply sncast with --account and --url flags. 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 Accounts creation and deployment is supported for

  • OpenZeppelin
  • Argent (with guardian set to 0)
  • Braavos

Examples

General Example

Do the following to start interacting with the Starknet:

  • create account with the sncast account create command

    $ sncast \
      --url http://127.0.0.1:5050 \
      account create \
      --name some-name
      
    Account successfully created. Prefund generated address with at least 432300000000 tokens. It is good to send more in the case of higher demand, max_fee * 2 = 864600000000
    command: account create
    max_fee: 0x64a7168300
    address: 0x7a949e83b243068d0cbedd8d5b8b32fafea66c54de23c40e68b126b5c845b61
    

    You can also pass common --accounts-file argument with a path to (existing or not existing) file where you want to save account info.

    For a detailed CLI description, see account create command reference.

  • prefund generated address with tokens

    You can do it both by sending tokens from another starknet account or by bridging them with StarkGate.

  • deploy account with the sncast account deploy command

    $ sncast \
      --url http://127.0.0.1:5050 \
      account deploy
      --name some-name \
      --fee-token strk \
      --max-fee 9999999999999
    
    command: account deploy
    transaction_hash: 0x20b20896ce63371ef015d66b4dd89bf18c5510a840b4a85a43a983caa6e2579
    

    Note that you don't have to pass url, accounts-file 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.

💡 Info You can also choose to pay in Ether by setting --fee-token to eth.

account create With Salt Argument

Salt will not be randomly generated if it's specified with --salt.

$ sncast \
    account create \
    --name some-name \
    --salt 0x1
  
Account successfully created. Prefund generated address with at least 432300000000 tokens. It is good to send more in the case of higher demand, max_fee * 2 = 864600000000
command: account create
max_fee: 0x64a7168300
address: 0x7a949e83b243068d0cbedd8d5b8b32fafea66c54de23c40e68b126b5c845b61

account delete

Delete an account from accounts-file and its associated Scarb profile.

$ sncast \
    --accounts-file my-account-file.json \
    account delete \
    --name some-name \
    --network alpha-sepolia
  
Do you want to remove account some-name from network alpha-sepolia? (Y/n)
Y
command: account delete
result: Account successfully removed

For a detailed CLI description, see account delete command reference.

account list

List all accounts saved in accounts file, grouped based on the networks they are defined on.

$ sncast --accounts-file my-account-file.json account list
Available accounts (at <current-directory>/my-account-file.json):
- user0
public key: 0x2f91ed13f8f0f7d39b942c80bfcd3d0967809d99e0cc083606cbe59033d2b39
network: alpha-sepolia
address: 0x4f5f24ceaae64434fa2bc2befd08976b51cf8f6a5d8257f7ec3616f61de263a
type: OpenZeppelin
deployed: false
legacy: false

- user1
[...]

To show private keys too, run with --display-private-keys or -p

$ sncast --accounts-file my-account-file.json account list --display-private-keys
Available accounts (at <current-directory>/my-account-file.json):
- user0
private key: 0x1e9038bdc68ce1d27d54205256988e85
public key: 0x2f91ed13f8f0f7d39b942c80bfcd3d0967809d99e0cc083606cbe59033d2b39
network: alpha-sepolia
address: 0x4f5f24ceaae64434fa2bc2befd08976b51cf8f6a5d8257f7ec3616f61de263a
type: OpenZeppelin
deployed: false
legacy: false

- user1
[...]

Custom Account Contract

By default, sncast creates/deploys an account using OpenZeppelin's account contract class hash. It is possible to create an account using custom openzeppelin, argent or braavos contract declared to starknet. This can be achieved with --class-hash flag:

$ sncast \
    account create \
    --name some-name \
    --class-hash 0x00e2eb8f5672af4e6a4e8a8f1b44989685e668489b0a25437733756c5a34a1d6

Account successfully created. Prefund generated address with at least 432300000000 tokens. It is good to send more in the case of higher demand, max_fee * 2 = 864600000000
command: account create
max_fee: 0x64a7168300
address: 0x7a949e83b243068d0cbedd8d5b8b32fafea66c54de23c40e68b126b5c845b61

$ sncast \
  account deploy \
  --name some-name \
  --max-fee 864600000000

command: account deploy
transaction_hash: 0x20b20896ce63371ef015d66b4dd89bf18c5510a840b4a85a43a983caa6e2579

Using Keystore and Starkli Account

Accounts created and deployed with starkli can be used by specifying the --keystore argument.

💡 Info When passing the --keystore argument, --account argument must be a path to the starkli account JSON file.

$ sncast \
    --url http://127.0.0.1:5050 \
    --keystore path/to/keystore.json \
    --account path/to/account.json  \
    declare \
    --contract-name my_contract

Importing an Account

To import an account into the file holding the accounts info (~/.starknet_accounts/starknet_open_zeppelin_accounts.json by default), use the account add command.

$ sncast \
    --url http://127.0.0.1:5050 \
    account add \
    --name my_imported_account \
    --address 0x1 \
    --private-key 0x2 \
    --class-hash 0x3 \
    --type oz

For a detailed CLI description, see account add command reference.

Creating an Account With Starkli-Style Keystore

It is possible to create an openzeppelin account with keystore in a similar way starkli does.

$ sncast \
    --url http://127.0.0.1:5050 \
    --keystore my_key.json \
    --account my_account.json \
    account create

The command above will generate a keystore file containing the private key, as well as an account file containing the openzeppelin account info that can later be used with starkli.

Declaring New Contracts

Starknet provides a distinction between contract class and instance. This is similar to the difference between writing the code of a class MyClass {} and creating a new instance of it let myInstance = MyClass() in object-oriented programming languages.

Declaring a contract is a necessary step to have your contract available on the network. Once a contract is declared, it then can be deployed and then interacted with.

For a detailed CLI description, see declare command reference.

Examples

General Example

📝 Note Building a contract before running declare is not required. Starknet 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 \
    --fee-token strk \
    --contract-name SimpleBalance

command: declare
class_hash: 0x8448a68b5ea1affc45e3fd4b8b480ea36a51dc34e337a16d2567d32d0c6f8a
transaction_hash: 0x7ad0d6e449e33b6581a4bb8df866c0fce3919a5ee05a30840ba521dafee217f

📝 Note Contract name is a part after the mod keyword in your contract file. It may differ from package name defined 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.

💡 Info You can also choose to pay in Ether by setting --fee-token to eth.

Deploying New Contracts

Overview

Starknet Foundry sncast supports deploying smart contracts to a given network with the sncast deploy command.

It works by invoking a Universal Deployer Contract, which deploys the contract with the given class hash and constructor arguments.

For detailed CLI description, see deploy command reference.

Usage Examples

General Example

After declaring your contract, you can deploy it the following way:

$ sncast \
    --account myuser \
    --url http://127.0.0.1:5050/rpc \
    deploy \
    --fee-token strk \
    --class-hash 0x8448a68b5ea1affc45e3fd4b8b480ea36a51dc34e337a16d2567d32d0c6f8a

command: Deploy
contract_address: 0x301316d47a81b39c5e27cca4a7b8ca4773edbf1103218588d6da4d3ed53035a
transaction_hash: 0x64a62a000240e034d1862c2bbfa154aac6a8195b4b2e570f38bf4fd47a5ab1e

💡 Info Max fee will be automatically computed if --max-fee <MAX_FEE> is not passed.

💡 Info You can also choose to pay in Ether by setting --fee-token to eth.

Deploying Contract With Constructor

For such a constructor in the declared contract

#[constructor]
fn constructor(ref self: ContractState, first: felt252, second: u256) {
    ...
}

you have to pass constructor calldata to deploy it.

$ sncast deploy \
    --fee-token strk \
    --class-hash 0x8448a68b5ea1affc45e3fd4b8b480ea36a51dc34e337a16d2567d32d0c6f8a \
    --constructor-calldata 0x1 0x1 0x0
    
command: deploy
contract_address: 0x301316d47a81b39c5e27cca4a7b8ca4773edbf1103218588d6da4d3ed53035a
transaction_hash: 0x64a62a000240e034d1862c2bbfa154aac6a8195b4b2e570f38bf4fd47a5ab1e

📝 Note Although the constructor has only two params you have to pass more because u256 is serialized to two felts. It is important to know how types are serialized because all values passed as constructor calldata are interpreted as a field elements (felt252).

Passing salt Argument

Salt is a parameter which modifies contract's address, if not passed it will be automatically generated.

$ sncast deploy \
    --fee-token strk \
    --class-hash 0x8448a68b5ea1affc45e3fd4b8b480ea36a51dc34e337a16d2567d32d0c6f8a \
    --salt 0x123
    
command: deploy
contract_address: 0x301316d47a81b39c5e27cca4a7b8ca4773edbf1103218588d6da4d3ed5303bc
transaction_hash: 0x64a62a000240e034d1862c2bbfa154aac6a8195b4b2e570f38bf4fd47a5ab1e

Passing unique Argument

Unique is a parameter which modifies contract's salt with the deployer address. It can be passed even if the salt argument was not provided.

$ sncast deploy \
    --fee-token strk \
    --class-hash 0x8448a68b5ea1affc45e3fd4b8b480ea36a51dc34e337a16d2567d32d0c6f8a \
    --unique
    
command: deploy
contract_address: 0x301316d47a81b39c5e27cca4a7b8ca4773edbf1103218588d6da4d3ed5303aa
transaction_hash: 0x64a62a000240e034d1862c2bbfa154aac6a8195b4b2e570f38bf4fd47a5ab1e

Invoking Contracts

Overview

Starknet Foundry sncast supports invoking smart contracts on a given network with the sncast invoke command.

In most cases, you have to provide:

  • Contract address
  • Function name
  • Function arguments

For detailed CLI description, see invoke command reference.

Examples

General Example

$ sncast \
  --url http://127.0.0.1:5050 \
  --account example_user \
  invoke \
  --fee-token strk \
  --contract-address 0x4a739ab73aa3cac01f9da5d55f49fb67baee4919224454a2e3f85b16462a911 \
  --function "some_function" \
  --calldata 1 2 0x1e
  
command: invoke
transaction_hash: 0x7ad0d6e449e33b6581a4bb8df866c0fce3919a5ee05a30840ba521dafee217f

💡 Info Max fee will be automatically computed if --max-fee <MAX_FEE> is not passed.

💡 Info You can also choose to pay in Ether by setting --fee-token to eth.

Invoking Function Without Arguments

Not every function accepts parameters. Here is how to call it.

$ sncast invoke \
  --fee-token strk \
  --contract-address 0x4a739ab73aa3cac01f9da5d55f49fb67baee4919224454a2e3f85b16462a911 \
  --function "function_without_params"
  
command: invoke
transaction_hash: 0x7ad0d6e449e33b6581a4bb8df866c0fce3919a5ee05a30840ba521dafee217f

Calling Contracts

Overview

Starknet Foundry sncast supports calling smart contracts on a given network with the sncast call command.

The basic inputs that you need for this command are:

  • Contract address
  • Function name
  • Inputs to the function

For a detailed CLI description, see the call command reference.

Examples

General Example

$ sncast \
  --url http://127.0.0.1:5050 \
  call \
  --contract-address 0x4a739ab73aa3cac01f9da5d55f49fb67baee4919224454a2e3f85b16462a911 \
  --function "some_function" \
  --calldata 1 2 3

command: call
response: [0x1, 0x23, 0x4]

📝 Note Call does not require passing account-connected parameters (account 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 --fee-token strk, a declared contract will be first deployed, and then its function put will be invoked.

📝 Note The example above demonstrates the use of the id property in a deploy call, which is then referenced as 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 --fee-token strk

command: multicall
transaction_hash: 0x38fb8a0432f71bf2dae746a1b4f159a75a862e253002b48599c9611fa271dcb

💡 Info Max fee will be automatically computed if --max-fee <MAX_FEE> is not passed.

💡 Info You can also choose to pay in Ether by setting --fee-token to eth.

multicall new Example

You can also generate multicall template with multicall new command, specifying output path.

$ sncast multicall new ./template.toml

Multicall template successfully saved in ./template.toml

Resulting in output:

[[call]]
call_type = "deploy"
class_hash = ""
inputs = []
id = ""
unique = false

[[call]]
call_type = "invoke"
contract_address = ""
function = ""
inputs = []

⚠️ Warning Trying to pass any existing file as an output for multicall new will result in error, as the command doesn't overwrite by default.

multicall new With overwrite Argument

If there is a file with the same name as provided, it can be overwritten.

$ sncast multicall new ./template.toml --overwrite

Multicall template successfully saved in ./new_multicall_template.toml

Cairo Deployment Scripts

Overview

⚠️⚠️⚠️ Highly experimental code, a subject to change ⚠️⚠️⚠️

Starknet Foundry cast can be used to run deployment scripts written in Cairo, using script run subcommand. It aims to provide similar functionality to Foundry's forge script.

To start writing a deployment script in Cairo just add sncast_std as a dependency to you scarb package and make sure to have a main function in the module you want to run. sncast_std docs can be found here.

Please note that sncast script is in development. While it is already possible to declare, deploy, invoke and call contracts from within Cairo, its interface, internals and feature set can change rapidly each version.

⚠️⚠️ By default, the nonce for each transaction is being taken from the pending block ⚠️⚠️

Some RPC nodes can be configured with higher poll intervals, which means they may return "older" nonces in pending blocks, or even not be able to obtain pending blocks at all. This might be the case if you get an error like "Invalid transaction nonce" when running a script, and you may need to manually set both nonce and max_fee for transactions.

Example:

     let declare_result = declare(
       "Map",
       FeeSettings::Eth(EthFeeSettings { max_fee: Option::Some(max_fee) }),
       Option::Some(nonce)
   )
       .expect('declare failed');

Some of the planned features that will be included in future versions are:

  • dispatchers support
  • logging
  • account creation/deployment
  • multicall support
  • dry running the scripts

and more!

State file

By default, when you run a script a state file containing information about previous runs will be created. This file can later be used to skip making changes to the network if they were done previously.

To determine if an operation (a function like declare, deploy or invoke) has to be sent to the network, the script will first check if such operation with given arguments already exists in state file. If it does, and previously ended with a success, its execution will be skipped. Otherwise, sncast will attempt to execute this function, and will write its status to the state file afterwards.

To prevent sncast from using the state file, you can set the --no-state-file flag.

A state file is typically named in a following manner:

{script name}_{network name}_state.json

Suggested directory structures

As sncast scripts are just regular scarb packages, there are multiple ways to incorporate scripts into your existing scarb workspace. Most common directory structures include:

1. scripts directory with all the scripts in the same workspace with cairo contracts (default for sncast script init)

$ tree
.
├── scripts
│   └── my_script
│       ├── Scarb.toml
│       └── src
│           ├── my_script.cairo
│           └── lib.cairo
├── src
│   ├── my_contract.cairo
│   └── lib.cairo
└── Scarb.toml

📝 Note You should add scripts 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,
    FeeSettings, EthFeeSettings
};

fn main() {
    let max_fee = 99999999999999999;
    let salt = 0x3;

    let declare_nonce = get_nonce('latest');
    let declare_result = declare(
        "Map",
        FeeSettings::Eth(EthFeeSettings { max_fee: Option::Some(max_fee) }),
        Option::Some(declare_nonce)
    )
        .expect('map declare failed');

    let class_hash = declare_result.class_hash;
    let deploy_nonce = get_nonce('pending');
    let deploy_result = deploy(
        class_hash,
        ArrayTrait::new(),
        Option::Some(salt),
        true,
        FeeSettings::Eth(EthFeeSettings { max_fee: Option::Some(max_fee) }),
        Option::Some(deploy_nonce)
    )
        .expect('map deploy failed');
    assert(deploy_result.transaction_hash != 0, deploy_result.transaction_hash);

    let invoke_nonce = get_nonce('pending');
    let invoke_result = invoke(
        deploy_result.contract_address,
        selector!("put"),
        array![0x1, 0x2],
        FeeSettings::Eth(EthFeeSettings { max_fee: Option::Some(max_fee) }),
        Option::Some(invoke_nonce)
    )
        .expect('map invoke failed');
    assert(invoke_result.transaction_hash != 0, invoke_result.transaction_hash);

    let call_result = call(deploy_result.contract_address, selector!("get"), array![0x1])
        .expect('map call failed');
    assert(call_result.data == array![0x2], *call_result.data.at(0));
}

The script should be included in a scarb package. The directory structure and config for this example looks like this:

$ tree
.
├── contracts
│    ├── Scarb.toml
│    └── src
│        └── lib.cairo
└── scripts
    ├── Scarb.toml
    └── src
        ├── lib.cairo
        └── map_script.cairo
[package]
name = "map_script"
version = "0.1.0"

[dependencies]
starknet = ">=2.3.0"
sncast_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.27.0" }
map = { path = "../contracts" }

[lib]
sierra = true
casm = true

[[target.starknet-contract]]
build-external-contracts = [
    "map::Map"
]

Please note that map contract was specified as the dependency. In our example, it resides in the filesystem. To generate the artifacts for it that will be accessible from the script you need to use the build-external-contracts property.

To run the script, do:

$ sncast \
  --url http://127.0.0.1:5050 \
  --account example_user \
  script run map_script

Class hash of the declared contract: 685896493695476540388232336434993540241192267040651919145140488413686992233
...
Deployed the contract to address: 2993684914933159551622723238457226804366654523161908704282792530334498925876
...
Invoke tx hash is: 2455538849277152825594824366964313930331085452149746033747086127466991639149
Call result: [2]

command: script run
status: success

As an idempotency feature is turned on by default, executing the same script once again ends with a success and only call functions are being executed (as they do not change the network state):

$ sncast \
  --url http://127.0.0.1:5050 \
  --account example_user \
  script run map_script

Class hash of the declared contract: 1922774777685257258886771026518018305931014651657879651971507142160195873652
Deployed the contract to address: 3478557462226312644848472512920965457566154264259286784215363579593349825684
Invoke tx hash is: 1373185562410761200747829131886166680837022579434823960660735040169785115611
Call result: [2]
command: script run
status: success

whereas, when we run the same script once again with --no-state-file flag set, it fails (as the Map contract is already declared):

$ sncast \
  --url http://127.0.0.1:5050 \
  --account example_user \
  script run map_script --no-state-file

command: script run
message:
    0x636f6e747261637420616c7265616479206465636c61726564 ('contract already declared')

status: script panicked

Error handling

Each of declare, deploy, invoke, call functions return Result<T, ScriptCommandError>, where T is a corresponding response struct. This allows for various script errors to be handled programmatically. Script errors implement Debug trait, allowing the error to be printed to stdout.

Minimal example with assert and print

use sncast_std::{
    get_nonce, declare, DeclareResult, ScriptCommandError, ProviderError, StarknetError,
    FeeSettings, EthFeeSettings
};

fn main() {
    let max_fee = 9999999999999999999999999999999999;

    let declare_nonce = get_nonce('latest');
    let declare_result = declare(
        "Map",
        FeeSettings::Eth(EthFeeSettings { max_fee: Option::Some(max_fee) }),
        Option::Some(declare_nonce)
    )
        .unwrap_err();
    println!("{:?}", declare_result);

    assert(
        ScriptCommandError::ProviderError(
            ProviderError::StarknetError(StarknetError::InsufficientAccountBalance)
        ) == declare_result,
        'ohno'
    )
}

stdout:

...
ScriptCommandError::ProviderError(ProviderError::StarknetError(StarknetError::InsufficientAccountBalance(())))
command: script
status: success

Some errors may contain an error message in the form of ByteArray

Minimal example with an error msg:

use sncast_std::{call, CallResult, ScriptCommandError, ProviderError, StarknetError, ErrorData};

fn main() {
    let eth = 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7.try_into().expect('bad address');
    let call_err: ScriptCommandError = call(
        eth, selector!("gimme_money"), array![]
    )
        .unwrap_err();

    println!("{:?}", call_err);
}

stdout:

...
ScriptCommandError::ProviderError(ProviderError::StarknetError(StarknetError::ContractError(ErrorData { msg: "Entry point EntryPointSelector(StarkFelt( ... )) not found in contract." })))
command: script
status: success

More on deployment scripts errors here.

Inspecting Transactions

Overview

Starknet Foundry sncast supports the inspection of transaction statuses on a given network with the sncast tx-status command.

For a detailed CLI description, refer to the tx-status command reference.

Usage Examples

Inspecting Transaction Status

You can track the details about the execution and finality status of a transaction in the given network by using the transaction hash as shown below:

$ sncast \
 --url http://127.0.0.1:5050  \
 tx-status \
 0x07d2067cd7675f88493a9d773b456c8d941457ecc2f6201d2fe6b0607daadfd1 

command: tx-status
execution_status: Succeeded
finality_status: AcceptedOnL2

Fees and versions

Historically, fees for transactions on Starknet had to be paid exclusively with ETH. However, with the rollout of v3 transactions, users now have the additional option to pay these fees using STRK.

💡 Info V3 transactions have additional options, that give you more control over transaction fee. You can specify the maximum gas unit price and the maximum gas for the transaction. This is done using the --max-gas and --max-gas-unit-price flags.

Cast allows you to specify either the version of the transaction you want to send or the fee token you want to pay the fees in. This is done using the --version and --fee-token flags.

💡 Info Don't worry if you're not sure which version to use, it will be inferred automatically based on the fee token you provide. The same goes for the fee token, if you provide a version, the fee token will be inferred.

sncast account deploy

When deploying an account, you can specify the version of the transaction and the fee token to use. The table below shows which token is used for which version of the transaction:

VersionFee Token
v1eth
v3strk

When paying in STRK, you need to either set --fee-token to strk:

$ sncast account deploy \
    --name some-name \
    --fee-token strk \
    --max-fee 9999999999999

or set --version to v3:

$ sncast account deploy \
    --name some-name \
    --version v3 \
    --max-fee 9999999999999

In case of paying in ETH, same rules apply. You need to set either --fee-token to eth:

$ sncast account deploy \
    --name some-name \
    --fee-token eth \
    --max-fee 9999999999999

or set --version to v1:

$ sncast account deploy \
    --name some-name \
    --version v1 \
    --max-fee 9999999999999

📝 Note The unit used in --max-fee flag is the smallest unit of the given fee token. For ETH it is Wei, for STRK it is Fri.

sncast deploy

Currently, there are two versions of the deployment transaction: v1 and v3. The table below shows which token is used for which version of the transaction:

VersionFee Token
v1eth
v3strk

sncast declare

Currently, there are two versions of the declare transaction: v2 and v3. The table below shows which token is used for which version of the transaction:

VersionFee Token
v2eth
v3strk

sncast invoke and sncast multicall run

Currently, there are two versions of invoke transaction: v1 and v3. The table below shows which token is used for which version of the transaction:

VersionFee Token
v1eth
v3strk

Verifying Contracts

Overview

Starknet Foundry sncast supports verifying Cairo contract classes with the sncast verify command by submitting the source code to a selected verification provider. Verification provides transparency, making the code accessible to users and aiding debugging tools.

The verification provider guarantees that the submitted source code aligns with the deployed contract class on the network by compiling the source code into Sierra bytecode and comparing it with the network-deployed Sierra bytecode.

For detailed CLI description, see verify command reference.

⚠️ Warning Please be aware that submitting the source code means it will be publicly exposed through the provider's APIs.

Verification Providers

Walnut

Walnut is a tool for step-by-step debugging of Starknet transactions. You can learn more about Walnut here walnut.dev. Note that Walnut requires you to specify the Starknet version in your Scarb.toml config file.

Example

First, ensure that you have created a Scarb.toml file for your contract (it should be present in the project directory or one of its parent directories). Make sure the contract has already been deployed on the network.

Then run:

$ sncast --url http://127.0.0.1:5050/rpc \
    verify \
    --contract-address 0x8448a68b5ea1affc45e3fd4b8b480ea36a51dc34e337a16d2567d32d0c6f8b \
    --contract-name SimpleBalance \
    --verifier walnut \
    --network mainnet

You are about to submit the entire workspace's code to the third-party chosen verifier at walnut, and the code will be publicly available through walnut's APIs. Are you sure? (Y/n) Y

command: verify
message: Contract has been successfully verified. You can check the verification status at the following link: https://api.walnut.dev/v1/sn_main/classes/0x03498e7edbc5f953315db118401fe7ea1eef637f63c56b45bd54e35150929ca3

📝 Note Contract name is a part after the mod keyword in your contract file. It may differ from package name defined in Scarb.toml file.

Environment Setup

💡 Info This tutorial is only relevant if you wish to contribute to Starknet Foundry. If you plan to only use it as a tool for your project, you can skip this part.

Prerequisites

Rust

Install the latest stable Rust version. If you already have Rust installed make sure to upgrade it by running

$ rustup update

Scarb

You can read more about installing Scarb here

Please make sure you're using Scarb installed via asdf - otherwise some tests may fail.

To verify, run:

$ which scarb

the result of which should be:

$HOME/.asdf/shims/scarb

If you previously installed scarb using official installer, you may need to remove this installation or modify your PATH to make sure asdf installed one is always used.

❗️ Warning

If you haven't pushed your branch to the remote yet (you've been working only locally), two tests will fail:

  • e2e::running::init_new_project_test
  • e2e::running::simple_package_with_git_dependency

After pushing the branch to the remote, those tests should pass.

Starknet Devnet

To install it run ./scripts/install_devnet.sh

Universal sierra compiler

Install the latest universal-sierra-compiler version.

Running Tests

Tests can be run with:

$ cargo test

Formatting and Lints

Starknet Foundry uses rustfmt for formatting. You can run the formatter with

$ cargo fmt

For linting, it uses clippy. You can run it with this command:

$ cargo clippy --all-targets --all-features -- --no-deps -W clippy::pedantic -A clippy::missing_errors_doc -A clippy::missing_panics_doc -A clippy::default_trait_access

Or using our defined alias

$ cargo lint

Spelling

Starknet Foundry uses typos for spelling checks.

You can run the checker with

$ typos

Some typos can be automatically fixed by running

$ typos -w

Contributing

Read the general contribution guideline here

snforge CLI Reference

You can check your version of snforge via snforge --version. To display help run snforge --help.

snforge test

Run tests for a project in the current directory.

[TEST_FILTER]

Passing a test filter will only run tests with an absolute module tree path containing this filter.

-e, --exact

Will only run a test with a name exactly matching the test filter. Test filter must be a whole qualified test name e.g. package_name::my_test instead of just my_test.

-x, --exit-first

Stop executing tests after the first failed test.

-p, --package <SPEC>

Packages to run this command on, can be a concrete package name (foobar) or a prefix glob (foo*).

-w, --workspace

Run tests for all packages in the workspace.

-r, --fuzzer-runs <FUZZER_RUNS>

Number of fuzzer runs.

-s, --fuzzer-seed <FUZZER_SEED>

Seed for the fuzzer.

--ignored

Run only tests marked with #[ignore] attribute.

--include-ignored

Run all tests regardless of #[ignore] attribute.

--rerun-failed

Run tests that failed during the last run

--color <WHEN>

Control when colored output is used. Valid values:

  • auto (default): automatically detect if color support is available on the terminal.
  • always: always display colors.
  • never: never display colors.

--detailed-resources

Display additional info about used resources for passed tests.

--save-trace-data

Saves execution traces of test cases which pass and are not fuzz tests. You can use traces for profiling purposes.

--build-profile

Saves trace data and then builds profiles of test cases which pass and are not fuzz tests. You need cairo-profiler installed on your system. You can set a custom path to cairo-profiler with CAIRO_PROFILER env variable. Profile can be read with pprof, more information: cairo-profiler, pprof

--max-n-steps <MAX_N_STEPS>

Number of maximum steps during a single test. For fuzz tests this value is applied to each subtest separately.

-h, --help

Print help.

snforge init

Create a new directory with a snforge project.

<NAME>

Name of a new project.

-h, --help

Print help.

snforge clean-cache

Clean snforge cache directory.

-h, --help

Print help.

Cheatcodes Reference

  • mock_call - mocks a number of contract calls to an entry point

  • start_mock_call - mocks contract call to an entry point

  • stop_mock_call - cancels 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 - executes a #[l1_handler] function to mock a message arriving from Ethereum

  • spy_events - creates EventSpy instance which spies on events emitted by contracts

  • spy_messages_to_l1 - creates L1MessageSpy instance which spies on messages to L1 sent by contracts

  • store - stores values in targeted contact's storage

  • load - loads values directly from targeted contact's storage

  • CheatSpan - enum for specifying the number of target calls for a cheat

Execution Info

Caller Address

Block Info

Block Number

Block Timestamp

Sequencer Address

Transaction Info

Transaction Version

Transaction Max Fee

Transaction Signature

Transaction Hash

Transaction Chain ID

Transaction Nonce

Transaction Resource Bounds

Transaction Tip

Transaction Paymaster Data

Transaction Nonce Data Availability Mode

Transaction Fee Data Availability Mode

Transaction Account Deployment

Account Contract Address

ℹ️ 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" }

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_cheat_block_timestamp).
  • TargetCalls applies the cheatcode for a specified number of calls to the target, after which the cheat is canceled (or until the cheat is canceled manually).

caller_address

Cheatcodes modifying caller_address:

cheat_caller_address

fn cheat_caller_address(target: ContractAddress, caller_address: ContractAddress, span: CheatSpan)

Changes the caller address for the given target and span.

start_cheat_caller_address_global

fn start_cheat_caller_address_global(caller_address: ContractAddress)

Changes the caller address for all targets.

start_cheat_caller_address

fn start_cheat_caller_address(target: ContractAddress, caller_address: ContractAddress)

Changes the caller address for the given target.

stop_cheat_caller_address

fn stop_cheat_caller_address(target: ContractAddress)

Cancels the cheat_caller_address / start_cheat_caller_address for the given target.

stop_cheat_caller_address_global

fn stop_cheat_caller_address_global(target: ContractAddress)

Cancels the start_cheat_caller_address_global.

block_number

Cheatcodes modifying block_number:

cheat_block_number

fn cheat_block_number(target: ContractAddress, block_number: u64, span: CheatSpan)

Changes the block number for the given target and span.

start_cheat_block_number_global

fn start_cheat_block_number_global(block_number: u64)

Changes the block number for all targets.

start_cheat_block_number

fn start_cheat_block_number(target: ContractAddress, block_number: u64)

Changes the block number for the given target.

stop_cheat_block_number

fn stop_cheat_block_number(target: ContractAddress)

Cancels the cheat_block_number / start_cheat_block_number for the given target.

stop_cheat_block_number_global

fn stop_cheat_block_number_global(target: ContractAddress)

Cancels the start_cheat_block_number_global.

block_timestamp

Cheatcodes modifying block_timestamp:

cheat_block_timestamp

fn cheat_block_timestamp(target: ContractAddress, block_timestamp: u64, span: CheatSpan)

Changes the block timestamp for the given target and span.

start_cheat_block_timestamp_global

fn start_cheat_block_timestamp_global(block_timestamp: u64)

Changes the block timestamp for all targets.

start_cheat_block_timestamp

fn start_cheat_block_timestamp(target: ContractAddress, block_timestamp: u64)

Changes the block timestamp for the given target.

stop_cheat_block_timestamp

fn stop_cheat_block_timestamp(target: ContractAddress)

Cancels the cheat_block_timestamp / start_cheat_block_timestamp for the given target.

stop_cheat_block_timestamp_global

fn stop_cheat_block_timestamp_global(target: ContractAddress)

Cancels the start_cheat_block_timestamp_global.

sequencer_address

Cheatcodes modifying sequencer_address:

cheat_sequencer_address

fn cheat_sequencer_address(target: ContractAddress, sequencer_address: ContractAddress, span: CheatSpan)

Changes the sequencer address for the given target and span.

start_cheat_sequencer_address_global

fn start_cheat_sequencer_address_global(sequencer_address: ContractAddress)

Changes the sequencer address for all targets.

start_cheat_sequencer_address

fn start_cheat_sequencer_address(target: ContractAddress, sequencer_address: ContractAddress)

Changes the sequencer address for the given target.

stop_cheat_sequencer_address

fn stop_cheat_sequencer_address(target: ContractAddress)

Cancels the cheat_sequencer_address / start_cheat_sequencer_address for the given target.

stop_cheat_sequencer_address_global

fn stop_cheat_sequencer_address_global(target: ContractAddress)

Cancels the start_cheat_sequencer_address_global.

Transaction version

Cheatcodes modifying transaction version:

cheat_transaction_version

fn cheat_transaction_version(target: ContractAddress, version: felt252, span: CheatSpan)

Changes the transaction version for the given target and span.

start_cheat_transaction_version_global

fn start_cheat_transaction_version_global(version: felt252)

Changes the transaction version for all targets.

start_cheat_transaction_version

fn start_cheat_transaction_version(target: ContractAddress, version: felt252)

Changes the transaction version for the given target.

stop_cheat_transaction_version

fn stop_cheat_transaction_version(target: ContractAddress)

Cancels the cheat_transaction_version / start_cheat_transaction_version for the given target.

stop_cheat_transaction_version_global

fn stop_cheat_transaction_version_global(target: ContractAddress)

Cancels the start_cheat_transaction_version_global.

account_contract_address

Cheatcodes modifying account_contract_address:

cheat_account_contract_address

fn cheat_account_contract_address(target: ContractAddress, account_contract_address: ContractAddress, span: CheatSpan)

Changes the address of an account which the transaction originates from, for the given target and span.

start_cheat_account_contract_address_global

fn start_cheat_account_contract_address_global(account_contract_address: ContractAddress)

Changes the address of an account which the transaction originates from, for all targets.

start_cheat_account_contract_address

fn start_cheat_account_contract_address(target: ContractAddress, account_contract_address: ContractAddress)

Changes the address of an account which the transaction originates from, for the given target.

stop_cheat_account_contract_address

fn stop_cheat_account_contract_address(target: ContractAddress)

Cancels the cheat_account_contract_address / start_cheat_account_contract_address for the given target.

stop_cheat_account_contract_address_global

fn stop_cheat_account_contract_address_global(target: ContractAddress)

Cancels the start_cheat_account_contract_address_global.

max_fee

Cheatcodes modifying max_fee:

cheat_max_fee

fn cheat_max_fee(target: ContractAddress, max_fee: u128, span: CheatSpan)

Changes the transaction max fee for the given target and span.

start_cheat_max_fee_global

fn start_cheat_max_fee_global(max_fee: u128)

Changes the transaction max fee for all targets.

start_cheat_max_fee

fn start_cheat_max_fee(target: ContractAddress, max_fee: u128)

Changes the transaction max fee for the given target.

stop_cheat_max_fee

fn stop_cheat_max_fee(target: ContractAddress)

Cancels the cheat_max_fee / start_cheat_max_fee for the given target.

stop_cheat_max_fee_global

fn stop_cheat_max_fee_global(target: ContractAddress)

Cancels the start_cheat_max_fee_global.

signature

Cheatcodes modifying signature:

cheat_signature

fn cheat_signature(target: ContractAddress, signature: Span<felt252>, span: CheatSpan)

Changes the transaction signature for the given target and span.

start_cheat_signature_global

fn start_cheat_signature_global(signature: Span<felt252>)

Changes the transaction signature for all targets.

start_cheat_signature

fn start_cheat_signature(target: ContractAddress, signature: Span<felt252>)

Changes the transaction signature for the given target.

stop_cheat_signature

fn stop_cheat_signature(target: ContractAddress)

Cancels the cheat_signature / start_cheat_signature for the given target.

stop_cheat_signature_global

fn stop_cheat_signature_global(target: ContractAddress)

Cancels the start_cheat_signature_global.

transaction_hash

Cheatcodes modifying transaction_hash:

cheat_transaction_hash

fn cheat_transaction_hash(target: ContractAddress, transaction_hash: felt252, span: CheatSpan)

Changes the transaction hash for the given target and span.

start_cheat_transaction_hash_global

fn start_cheat_transaction_hash_global(transaction_hash: felt252)

Changes the transaction hash for all targets.

start_cheat_transaction_hash

fn start_cheat_transaction_hash(target: ContractAddress, transaction_hash: felt252)

Changes the transaction hash for the given target.

stop_cheat_transaction_hash

fn stop_cheat_transaction_hash(target: ContractAddress)

Cancels the cheat_transaction_hash / start_cheat_transaction_hash for the given target.

stop_cheat_transaction_hash_global

fn stop_cheat_transaction_hash_global(target: ContractAddress)

Cancels the start_cheat_transaction_hash_global.

chain_id

Cheatcodes modifying chain_id:

cheat_chain_id

fn cheat_chain_id(target: ContractAddress, chain_id: felt252, span: CheatSpan)

Changes the transaction chain_id for the given target and span.

start_cheat_chain_id_global

fn start_cheat_chain_id_global(chain_id: felt252)

Changes the transaction chain_id for all targets.

start_cheat_chain_id

fn start_cheat_chain_id(target: ContractAddress, chain_id: felt252)

Changes the transaction chain_id for the given target.

stop_cheat_chain_id

fn stop_cheat_chain_id(target: ContractAddress)

Cancels the cheat_chain_id / start_cheat_chain_id for the given target.

stop_cheat_chain_id_global

fn stop_cheat_chain_id_global(target: ContractAddress)

Cancels the start_cheat_chain_id_global.

nonce

Cheatcodes modifying nonce:

cheat_nonce

fn cheat_nonce(target: ContractAddress, nonce: felt252, span: CheatSpan)

Changes the transaction nonce for the given target and span.

start_cheat_nonce_global

fn start_cheat_nonce_global(nonce: felt252)

Changes the transaction nonce for all targets.

start_cheat_nonce

fn start_cheat_nonce(target: ContractAddress, nonce: felt252)

Changes the transaction nonce for the given target.

stop_cheat_nonce

fn stop_cheat_nonce(target: ContractAddress)

Cancels the cheat_nonce / start_cheat_nonce for the given target.

stop_cheat_nonce_global

fn stop_cheat_nonce_global(target: ContractAddress)

Cancels the start_cheat_nonce_global.

resource_bounds

Cheatcodes modifying resource_bounds:

cheat_resource_bounds

fn cheat_resource_bounds(target: ContractAddress, resource_bounds: Span<ResourceBounds>, span: CheatSpan)

Changes the transaction resource bounds for the given target and span.

start_cheat_resource_bounds_global

fn start_cheat_resource_bounds_global(resource_bounds: Span<ResourceBounds>)

Changes the transaction resource bounds for all targets.

start_cheat_resource_bounds

fn start_cheat_resource_bounds(target: ContractAddress, resource_bounds: Span<ResourceBounds>)

Changes the transaction resource bounds for the given target.

stop_cheat_resource_bounds

fn stop_cheat_resource_bounds(target: ContractAddress)

Cancels the cheat_resource_bounds / start_cheat_resource_bounds for the given target.

stop_cheat_resource_bounds_global

fn stop_cheat_resource_bounds_global(target: ContractAddress)

Cancels the start_cheat_resource_bounds_global.

tip

Cheatcodes modifying tip:

cheat_tip

fn cheat_tip(target: ContractAddress, tip: u128, span: CheatSpan)

Changes the transaction tip for the given target and span.

start_cheat_tip_global

fn start_cheat_tip_global(tip: u128)

Changes the transaction tip for all targets.

start_cheat_tip

fn start_cheat_tip(target: ContractAddress, tip: u128)

Changes the transaction tip for the given target.

stop_cheat_tip

fn stop_cheat_tip(target: ContractAddress)

Cancels the cheat_tip / start_cheat_tip for the given target.

stop_cheat_tip_global

fn stop_cheat_tip_global(target: ContractAddress)

Cancels the start_cheat_tip_global.

paymaster_data

Cheatcodes modifying paymaster_data:

cheat_paymaster_data

fn cheat_paymaster_data(target: ContractAddress, paymaster_data: Span<felt252>, span: CheatSpan)

Changes the transaction paymaster data for the given target and span.

start_cheat_paymaster_data_global

fn start_cheat_paymaster_data_global(paymaster_data: Span<felt252>)

Changes the transaction paymaster data for all targets.

start_cheat_paymaster_data

fn start_cheat_paymaster_data(target: ContractAddress, paymaster_data: Span<felt252>)

Changes the transaction paymaster data for the given target.

stop_cheat_paymaster_data

fn stop_cheat_paymaster_data(target: ContractAddress)

Cancels the cheat_paymaster_data / start_cheat_paymaster_data for the given target.

stop_cheat_paymaster_data_global

fn stop_cheat_paymaster_data_global(target: ContractAddress)

Cancels the start_cheat_paymaster_data_global.

nonce_data_availability_mode

Cheatcodes modifying nonce_data_availability_mode:

cheat_nonce_data_availability_mode

fn cheat_nonce_data_availability_mode(target: ContractAddress, nonce_data_availability_mode: u32, span: CheatSpan)

Changes the transaction nonce data availability mode for the given target and span.

start_cheat_nonce_data_availability_mode_global

fn start_cheat_nonce_data_availability_mode_global(nonce_data_availability_mode: u32)

Changes the transaction nonce data availability mode for all targets.

start_cheat_nonce_data_availability_mode

fn start_cheat_nonce_data_availability_mode(target: ContractAddress, nonce_data_availability_mode: u32)

Changes the transaction nonce data availability mode for the given target.

stop_cheat_nonce_data_availability_mode

fn stop_cheat_nonce_data_availability_mode(target: ContractAddress)

Cancels the cheat_nonce_data_availability_mode / start_cheat_nonce_data_availability_mode for the given target.

stop_cheat_nonce_data_availability_mode_global

fn stop_cheat_nonce_data_availability_mode_global(target: ContractAddress)

Cancels the start_cheat_nonce_data_availability_mode_global.

fee_data_availability_mode

Cheatcodes modifying fee_data_availability_mode:

cheat_fee_data_availability_mode

fn cheat_fee_data_availability_mode(target: ContractAddress, fee_data_availability_mode: u32, span: CheatSpan)

Changes the transaction fee data availability mode for the given target and span.

start_cheat_fee_data_availability_mode_global

fn start_cheat_fee_data_availability_mode_global(fee_data_availability_mode: u32)

Changes the transaction fee data availability mode for all targets.

start_cheat_fee_data_availability_mode

fn start_cheat_fee_data_availability_mode(target: ContractAddress, fee_data_availability_mode: u32)

Changes the transaction fee data availability mode for the given target.

stop_cheat_fee_data_availability_mode

fn stop_cheat_fee_data_availability_mode(target: ContractAddress)

Cancels the cheat_fee_data_availability_mode / start_cheat_fee_data_availability_mode for the given target.

stop_cheat_fee_data_availability_mode_global

fn stop_cheat_fee_data_availability_mode_global(target: ContractAddress)

Cancels the start_cheat_fee_data_availability_mode_global.

account_deployment_data

Cheatcodes modifying account_deployment_data:

cheat_account_deployment_data

fn cheat_account_deployment_data(target: ContractAddress, account_deployment_data: Span<felt252>, span: CheatSpan)

Changes the transaction account deployment data for the given target and span.

start_cheat_account_deployment_data_global

fn start_cheat_account_deployment_data_global(account_deployment_data: Span<felt252>)

Changes the transaction account deployment data for all targets.

start_cheat_account_deployment_data

fn start_cheat_account_deployment_data(target: ContractAddress, account_deployment_data: Span<felt252>)

Changes the transaction account deployment data for the given target.

stop_cheat_account_deployment_data

fn stop_cheat_account_deployment_data(target: ContractAddress)

Cancels the cheat_account_deployment_data / start_cheat_account_deployment_data for the given target.

stop_cheat_account_deployment_data_global

fn stop_cheat_account_deployment_data_global(target: ContractAddress)

Cancels the start_cheat_account_deployment_data_global.

mock_call

Cheatcodes mocking contract entry point calls:

mock_call

fn mock_call<T, impl TSerde: serde::Serde<T>, impl TDestruct: Destruct<T>>( contract_address: ContractAddress, function_selector: felt252, ret_data: T, n_times: u32 )

Mocks contract call to a function_selector of a contract at the given address, for n_times first calls that are made to the contract. A call to function function_selector will return data provided in ret_data argument. An address with no contract can be mocked as well. An entrypoint that is not present on the deployed contract is also possible to mock. Note that the function is not meant for mocking internal calls - it works only for contract entry points.

start_mock_call

fn start_mock_call<T, impl TSerde: serde::Serde<T>, impl TDestruct: Destruct<T>>( contract_address: ContractAddress, function_selector: felt252, ret_data: T )

Mocks contract call to a function_selector of a contract at the given address, indefinitely. See mock_call for comprehensive definition of how it can be used.

stop_mock_call

fn stop_mock_call(contract_address: ContractAddress, function_selector: felt252)

Cancels the mock_call / start_mock_call for the function function_selector of a contract at the given address.

get_class_hash

fn get_class_hash(contract_address: ContractAddress) -> ClassHash

Returns a class hash of a contract at the specified address.

💡 Tip

This cheatcode can be used to test if your contract upgrade procedure is correct

replace_bytecode

fn replace_bytecode(contract: ContractAddress, new_class: ClassHash) -> Result<(), ReplaceBytecodeError>

Replaces class for given contract address. The new_class hash has to be declared in order for the replacement class to execute the code when interacting with the contract. Returns Result::Ok if the replacement succeeded, and a ReplaceBytecodeError with appropriate error type otherwise

ReplaceBytecodeError

An enum with appropriate type of replacement failure

pub enum ReplaceBytecodeError {
    /// Means that the contract does not exist, and thus bytecode cannot be replaced
    ContractNotDeployed,
    /// Means that the given class for replacement is not declared
    UndeclaredClassHash,
}

l1_handler

fn new(target: ContractAddress, selector: felt252) -> L1Handler

Returns a structure referring to an L1 handler function.

fn execute(self: L1Handler) -> SyscallResult<()>

Mocks an L1 -> L2 message from Ethereum handled by the given L1 handler function.

spy_events

fn spy_events() -> EventSpy

Creates EventSpy instance which spies on events emitted after its creation.

struct EventSpy {
    ...
}

An event spy structure.

struct Events {
    events: Array<(ContractAddress, Event)>
}

A wrapper structure on an array of events to handle event filtering.

struct Event {
    keys: Array<felt252>,
    data: Array<felt252>
}

Raw event format (as seen via the RPC-API), can be used for asserting the emitted events.

Implemented traits

EventSpyTrait

trait EventSpyTrait {
    fn get_events(ref self: EventSpy) -> Events;
}

Gets all events since the creation of the given EventSpy.

EventSpyAssertionsTrait

trait EventSpyAssertionsTrait<T, impl TEvent: starknet::Event<T>, impl TDrop: Drop<T>> {
    fn assert_emitted(ref self: EventSpy, events: @Array<(ContractAddress, T)>);
    fn assert_not_emitted(ref self: EventSpy, events: @Array<(ContractAddress, T)>);
}

Allows to assert the expected events emission (or lack thereof), in the scope of the EventSpy structure.

EventsFilterTrait

trait EventsFilterTrait {
    fn emitted_by(self: @Events, contract_address: ContractAddress) -> Events;
}

Filters events emitted by a given ContractAddress.

spy_messages_to_l1

fn spy_messages_to_l1() -> MessageToL1Spy

Creates MessageToL1Spy instance that spies on all messages sent to L1 after its creation.

struct MessageToL1Spy {
    // ..
}

Message spy structure allowing to get messages emitted only after its creation.

struct MessagesToL1 {
    messages: Array<(ContractAddress, MessageToL1)>
}

A wrapper structure on an array of messages to handle filtering smoothly. messages is an array of (l2_sender_address, message) tuples.

struct MessageToL1 {
    /// An ethereum address where the message is destined to go
    to_address: EthAddress,
    /// Actual payload which will be delivered to L1 contract
    payload: Array<felt252>
}

Raw message to L1 format (as seen via the RPC-API), can be used for asserting the sent messages.

Implemented traits

MessageToL1SpyTrait

trait MessageToL1SpyTrait {
    /// Gets all messages given [`MessageToL1Spy`] spies for.
    fn get_messages(ref self: MessageToL1Spy) -> MessagesToL1;
}

Gets all messages since the creation of the given MessageToL1Spy.

MessageToL1SpyAssertionsTrait

trait MessageToL1SpyAssertionsTrait {
    fn assert_sent(ref self: MessageToL1Spy, messages: @Array<(ContractAddress, MessageToL1)>);
    fn assert_not_sent(ref self: MessageToL1Spy, messages: @Array<(ContractAddress, MessageToL1)>);
}

Allows to assert the expected sent messages (or lack thereof), in the scope of MessageToL1Spy structure.

MessageToL1FilterTrait

trait MessageToL1FilterTrait {
    /// Filter messages emitted by a sender of a given [`ContractAddress`]
    fn sent_by(self: @MessagesToL1, contract_address: ContractAddress) -> MessagesToL1;
    /// Filter messages emitted by a receiver of a given ethereum address
    fn sent_to(self: @MessagesToL1, to_address: EthAddress) -> MessagesToL1;
}

Filters messages emitted by a given ContractAddress, or sent to given EthAddress.

store

fn store(target: ContractAddress, storage_address: felt252, serialized_value: Span<felt252>)

Stores felts from serialized_value in target contract's storage, starting at storage_address.

load

fn load(target: ContractAddress, storage_address: felt252, size: felt252) -> Array<felt252>

Loads size felts from target contract's storage into an Array, starting at storage_address.

Library Functions References

  • declare - declares a contract and returns 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" }

byte_array Module

Module containing utilities for manipulating ByteArrays.

Functions

fn try_deserialize_bytearray_error(x: Span<felt252>) -> Result<ByteArray, ByteArray>

This function is meant to transform a serialized output from a contract call into a ByteArray. Returns the parsed ByteArray, or an Err with reason, if the parsing failed.

declare

fn declare(contract: ByteArray) -> Result<ContractClass, Array<felt252>>

Declares a contract for later deployment.

See docs of ContractClass for more info about the resulting struct.

ContractClass

A struct which enables interaction with given class hash. It can be obtained by using declare, or created with an arbitrary ClassHash.

struct ContractClass {
    class_hash: ClassHash,
}

Implemented traits

ContractClassTrait

trait ContractClassTrait {
    fn precalculate_address(
        self: @ContractClass, constructor_calldata: @Array::<felt252>
    ) -> ContractAddress;

    fn deploy(
        self: @ContractClass, constructor_calldata: @Array::<felt252>
    ) -> SyscallResult<(ContractAddress, Span<felt252>)>;

    fn deploy_at(
        self: @ContractClass,
        constructor_calldata: @Array::<felt252>,
        contract_address: ContractAddress
    ) -> SyscallResult<(ContractAddress, Span<felt252>)>;

    fn new<T, +Into<T, ClassHash>>(class_hash: T) -> ContractClass;
}

get_call_trace

fn get_call_trace() -> CallTrace;

(For whole structure definition, please refer to snforge-std source)

Gets current call trace of the test, up to the last call made to a contract.

The whole structure is represented as a tree of calls, in which each contract interaction is a new execution scope - thus resulting in a new nested trace.

📝 Note

The topmost-call is representing the test call, which will always be present if you're running a test.

Displaying the trace

The CallTrace structure implements a Display trait, for a pretty-print with indentations

println!("{}", get_call_trace());

fs Module

Module containing functions for interacting with the filesystem.

File

trait FileTrait {
    fn new(path: ByteArray) -> File;
}

FileParser<T>

trait FileParser<T, +Serde<T>> {
    fn parse_txt(file: @File) -> Option<T>;
    fn parse_json(file: @File) -> Option<T>;
}

read_txt & read_json

fn read_txt(file: @File) -> Array<felt252>;
fn read_json(file: @File) -> Array<felt252>;

File format

Some rules have to be checked when providing a file for snforge, in order for correct parsing behavior. Different ones apply for JSON and plain text files.

Plain text files

  • Elements have to be separated with newlines
  • Elements have to be either:
    • integers in range of [0, P) where P 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) -> Result<U, SignError> ;
}

VerifierTrait

trait VerifierTrait<T, H, U> {
    fn verify(self: T, message_hash: H, signature: U) -> bool;
}

Example

use snforge_std::signature::KeyPairTrait;

use snforge_std::signature::secp256r1_curve::{Secp256r1CurveKeyPairImpl, Secp256r1CurveSignerImpl, Secp256r1CurveVerifierImpl};
use snforge_std::signature::secp256k1_curve::{Secp256k1CurveKeyPairImpl, Secp256k1CurveSignerImpl, Secp256k1CurveVerifierImpl};
use snforge_std::signature::stark_curve::{StarkCurveKeyPairImpl, StarkCurveSignerImpl, StarkCurveVerifierImpl};

use starknet::secp256r1::{Secp256r1Point, Secp256r1PointImpl};
use starknet::secp256k1::{Secp256k1Point, Secp256k1PointImpl};
use core::starknet::SyscallResultTrait;

#[test]
fn test_using_curves() {
    // Secp256r1
    let key_pair = KeyPairTrait::<u256, Secp256r1Point>::generate();
    let (r, s): (u256, u256) = key_pair.sign(msg_hash).unwrap();
    let is_valid = key_pair.verify(msg_hash, (r, s));
    
    // Secp256k1
    let key_pair = KeyPairTrait::<u256, Secp256k1Point>::generate();
    let (r, s): (u256, u256) = key_pair.sign(msg_hash).unwrap();
    let is_valid = key_pair.verify(msg_hash, (r, s));
    
    // StarkCurve
    let key_pair = KeyPairTrait::<felt252, felt252>::generate();
    let (r, s): (felt252, felt252) = key_pair.sign(msg_hash).unwrap();
    let is_valid = key_pair.verify(msg_hash, (r, s));
}

sncast CLI Reference

sncast common flags

--profile, -p <PROFILE_NAME>

Optional.

Used for both snfoundry.toml and Scarb.toml if specified. Defaults to default (snfoundry.toml) and dev (Scarb.toml).

--url, -u <RPC_URL>

Optional.

Starknet RPC node url address.

Overrides url from snfoundry.toml.

--account, -a <ACCOUNT_NAME>

Optional.

Account name used to interact with the network, aliased in open zeppelin accounts file.

Overrides account from snfoundry.toml.

If used with --keystore, should be a path to starkli account JSON file.

--accounts-file, -f <PATH_TO_ACCOUNTS_FILE>

Optional.

Path to the open zeppelin accounts file holding accounts info. Defaults to ~/.starknet_accounts/starknet_open_zeppelin_accounts.json.

--keystore, -k <PATH_TO_KEYSTORE_FILE>

Optional.

Path to keystore file. When specified, the --account argument must be a path to starkli account JSON file.

--int-format

Optional.

If passed, values will be displayed in decimal format. Default is addresses as hex and fees as int.

--hex-format

Optional.

If passed, values will be displayed in hex format. Default is addresses as hex and fees as int.

--json, -j

Optional.

If passed, output will be displayed in json format.

--wait, -w

Optional.

If passed, command will wait until transaction is accepted or rejected.

--wait-timeout <TIME_IN_SECONDS>

Optional.

If --wait is passed, this will set the time after which sncast times out. Defaults to 60s.

--wait-retry-timeout <TIME_IN_SECONDS>

Optional.

If --wait is passed, this will set the retry interval - how often sncast should fetch tx info from the node. Defaults to 5s.

--version, -v

Prints out sncast version.

--help, -h

Prints out help.

account

Provides a set of account management commands.

It has the following subcommands:

add

Import an account to accounts file.

Account information will be saved to the file specified by --accounts-file argument, which is ~/.starknet_accounts/starknet_open_zeppelin_accounts.json by default.

Required Common Arguments — Passed By CLI or Specified in snfoundry.toml

--name, -n <NAME>

Required.

Name of the account to be added.

--address, -a <ADDRESS>

Required.

Address of the account.

--type, -t <ACCOUNT_TYPE>

Required.

Type of the account. Possible values: oz, argent, braavos.

--class-hash, -c <CLASS_HASH>

Optional.

Class hash of the account.

--private-key <PRIVATE_KEY>

Optional. Required if --private-key-file is not passed.

Account private key.

--private-key-file <PRIVATE_KEY_FILE_PATH>

Optional. Required if --private-key-file is not passed.

Path to the file holding account private key.

--public-key <PUBLIC_KEY>

Optional.

Account public key. If not passed, will be computed from --private-key.

--salt, -s <SALT>

Optional.

Salt for the account address.

--add-profile <NAME>

Optional.

If passed, a profile with corresponding name will be added to snfoundry.toml.

create

Prepare all prerequisites for account deployment.

Account information will be saved to the file specified by --accounts-file argument, which is ~/.starknet_accounts/starknet_open_zeppelin_accounts.json by default.

Required Common Arguments — Passed By CLI or Specified in snfoundry.toml

--name, -n <ACCOUNT_NAME>

Required.

Account name under which account information is going to be saved.

--type, -t <ACCOUNT_TYPE>

Optional. Required if --class-hash is passed.

Type of the account. Possible values: oz, argent, braavos. Defaults to oz.

Versions of the account contracts:

--salt, -s <SALT>

Optional.

Salt for the account address. If omitted random one will be generated.

--add-profile <NAME>

Optional.

If passed, a profile with corresponding name will be added to snfoundry.toml.

--class-hash, -c

Optional.

Class hash of a custom openzeppelin account contract declared to the network.

deploy

Deploy previously created account to Starknet.

Required Common Arguments — Passed By CLI or Specified in snfoundry.toml

--name, -n <ACCOUNT_NAME>

Required.

Name of the (previously created) account to be deployed.

--max-fee, -m <MAX_FEE>

Optional.

Maximum fee for the deploy_account transaction in Fri or Wei depending on fee token or transaction version. When not used, defaults to auto-estimation.

--fee-token <FEE_TOKEN>

Optional. Required if --version is not provided.

Token used for fee payment. Possible values: ETH, STRK.

--max-gas <MAX_GAS>

Optional.

Maximum gas for the deploy_account transaction. When not used, defaults to auto-estimation. (Only for STRK fee payment)

--max-gas-unit-price <MAX_GAS_UNIT_PRICE>

Optional.

Maximum gas unit price for the deploy_account transaction paid in Fri. When not used, defaults to auto-estimation. (Only for STRK fee payment)

--version, -v <VERSION>

Optional. Required if --fee-token is not provided.

Version of the account deployment transaction. Possible values: v1, v3.

delete

Delete an account from accounts-file and its associated snfoundry profile.

Required Common Arguments — Passed By CLI or Specified in snfoundry.toml

--name, -n <ACCOUNT_NAME>

Required.

Account name which is going to be deleted.

--network

Optional.

Network in accounts-file associated with the account. By default, the network of rpc node.

--yes

Optional.

If passed, assume "yes" as answer to confirmation prompt and run non-interactively

list

List all available accounts.

Account information will be retrieved from the file specified in user's environment. The output format is dependent on user's configuration, either provided via CLI or specified in snfoundry.toml. Hides user's private keys by default.

⚠️ Warning This command outputs cryptographic information about accounts, e.g. user's private key. Use it responsibly to not cause any vulnerabilities to your environment and confidential data.

Required Common Arguments — Passed By CLI or Specified in snfoundry.toml

Optional Common Arguments — Passed By CLI or Specified in snfoundry.toml

--display-private-keys, -p

Optional.

If passed, show private keys along with the rest of the account information.

declare

Send a declare transaction of Cairo contract to Starknet.

Required Common Arguments — Passed By CLI or Specified in snfoundry.toml

--contract-name, -c <CONTRACT_NAME>

Required.

Name of the contract. Contract name is a part after the mod keyword in your contract file.

--max-fee, -m <MAX_FEE>

Optional.

Maximum fee for the declare transaction in Fri or Wei depending on fee token or transaction version. When not used, defaults to auto-estimation.

--fee-token <FEE_TOKEN>

Optional. Required if --version is not provided.

Token used for fee payment. Possible values: ETH, STRK.

--max-gas <MAX_GAS>

Optional.

Maximum gas for the declare transaction. When not used, defaults to auto-estimation. (Only for STRK fee payment)

--max-gas-unit-price <MAX_GAS_UNIT_PRICE>

Optional.

Maximum gas unit price for the declare transaction paid in Fri. When not used, defaults to auto-estimation. (Only for STRK fee payment)

--version, -v <VERSION>

Optional. Required if --fee-token is not provided.

Version of the deployment transaction. Possible values: v2, v3.

--nonce, -n <NONCE>

Optional.

Nonce for transaction. If not provided, nonce will be set automatically.

--package <NAME>

Optional.

Name of the package that should be used.

If supplied, a contract from this package will be used. Required if more than one package exists in a workspace.

deploy

Deploy a contract to Starknet.

Required Common Arguments — Passed By CLI or Specified in snfoundry.toml

--class-hash, -g <CLASS_HASH>

Required.

Class hash of contract to deploy.

--constructor-calldata, -c <CONSTRUCTOR_CALLDATA>

Optional.

Calldata for the contract constructor.

--salt, -s <SALT>

Optional.

Salt for the contract address.

--unique, -u

Optional.

If passed, the salt will be additionally modified with an account address.

--max-fee, -m <MAX_FEE>

Optional.

Maximum fee for the deploy transaction in Fri or Wei depending on fee token or transaction version. When not used, defaults to auto-estimation.

--fee-token <FEE_TOKEN>

Optional. Required if --version is not provided.

Token used for fee payment. Possible values: ETH, STRK.

--max-gas <MAX_GAS>

Optional.

Maximum gas for the deploy transaction. When not used, defaults to auto-estimation. (Only for STRK fee payment)

--max-gas-unit-price <MAX_GAS_UNIT_PRICE>

Optional.

Maximum gas unit price for the deploy transaction paid in Fri. When not used, defaults to auto-estimation. (Only for STRK fee payment)

--version, -v <VERSION>

Optional. Required if --fee-token is not provided.

Version of the deployment transaction. Possible values: v1, v3.

--nonce, -n <NONCE>

Optional.

Nonce for transaction. If not provided, nonce will be set automatically.

invoke

Send an invoke transaction to Starknet.

Required Common Arguments — Passed By CLI or Specified in snfoundry.toml

--contract-address, -a <CONTRACT_ADDRESS>

Required.

The address of the contract being called in hex (prefixed with '0x') or decimal representation.

--function, -f <FUNCTION_NAME>

Required.

The name of the function to call.

--calldata, -c <CALLDATA>

Optional.

Inputs to the function, represented by a list of space-delimited values 0x1 2 0x3. Calldata arguments may be either 0x hex or decimal felts.

--max-fee, -m <MAX_FEE>

Optional.

Maximum fee for the invoke transaction in Fri or Wei depending on fee token or transaction version. When not used, defaults to auto-estimation.

--fee-token <FEE_TOKEN>

Optional. Required if --version is not provided.

Token used for fee payment. Possible values: ETH, STRK.

--max-gas <MAX_GAS>

Optional.

Maximum gas for the invoke transaction. When not used, defaults to auto-estimation. (Only for STRK fee payment)

--max-gas-unit-price <MAX_GAS_UNIT_PRICE>

Optional.

Maximum gas unit price for the invoke transaction paid in Fri. When not used, defaults to auto-estimation. (Only for STRK fee payment)

--version, -v <VERSION>

Optional. Required if --fee-token is not provided.

Version of the deployment transaction. Possible values: v1, v3.

--nonce, -n <NONCE>

Optional.

Nonce for transaction. If not provided, nonce will be set automatically.

call

Call a smart contract on Starknet with the given parameters.

Required Common Arguments — Passed By CLI or Specified in snfoundry.toml

--contract-address, -a <CONTRACT_ADDRESS>

Required.

The address of the contract being called in hex (prefixed with '0x') or decimal representation.

--function, -f <FUNCTION_NAME>

Required.

The name of the function being called.

--calldata, -c <CALLDATA>

Optional.

Inputs to the function, represented by a list of space-delimited values, e.g. 0x1 2 0x3. Calldata arguments may be either 0x hex or decimal felts.

--block-id, -b <BLOCK_ID>

Optional.

Block identifier on which call should be performed. Possible values: pending, latest, block hash (0x prefixed string), and block number (u64). pending is used as a default value.

multicall

Provides utilities for performing multicalls on Starknet.

Multicall has the following subcommands:

new

Generates an empty template for the multicall .toml file that may be later used with the run subcommand. It writes it to a file provided as a required argument.

Usage

multicall new <OUTPUT-PATH> [OPTIONS]

Arguments

OUTPUT-PATH - a path to a file to write the generated .toml to.

Required Common Arguments — Passed By CLI or Specified in snfoundry.toml

--output-path, -p <PATH>

Optional.

Specifies a file path where the template should be saved. If omitted, the template contents will be printed out to the stdout.

--overwrite, -o <OVERWRITE>

Optional.

If the file specified by --output-path already exists, this parameter overwrites it.

run

Execute a single multicall transaction containing every call from passed file.

Required Common Arguments — Passed By CLI or Specified in snfoundry.toml

--path, -p <PATH>

Required.

Path to a TOML file with call declarations.

--max-fee, -m <MAX_FEE>

Optional.

Maximum fee for the invoke transaction in Fri or Wei depending on fee token or transaction version. When not used, defaults to auto-estimation.

--fee-token <FEE_TOKEN>

Optional. Required if --version is not provided.

Token used for fee payment. Possible values: ETH, STRK.

--max-gas <MAX_GAS>

Optional.

Maximum gas for the invoke transaction. When not used, defaults to auto-estimation. (Only for STRK fee payment)

--max-gas-unit-price <MAX_GAS_UNIT_PRICE>

Optional.

Maximum gas unit price for the invoke transaction paid in Fri. When not used, defaults to auto-estimation. (Only for STRK fee payment)

--version, -v <VERSION>

Optional. Required if --fee-token is not provided.

Version of the deployment transaction. Possible values: v1, v3.

File example:

[[call]]
call_type = "deploy"
class_hash = "0x076e94149fc55e7ad9c5fe3b9af570970ae2cf51205f8452f39753e9497fe849"
inputs = []
id = "map_contract"
unique = false

[[call]]
call_type = "invoke"
contract_address = "0x38b7b9507ccf73d79cb42c2cc4e58cf3af1248f342112879bfdf5aa4f606cc9"
function = "put"
inputs = ["0x123", "234"]

[[call]]
call_type = "invoke"
contract_address = "map_contract"
function = "put"
inputs = ["0x123", "234"]

[[call]]
call_type = "deploy"
class_hash = "0x2bb3d35dba2984b3d0cd0901b4e7de5411daff6bff5e072060bcfadbbd257b1"
inputs = ["0x123", "map_contract"]
unique = false

show_config

Prints the config currently being used

Required Common Arguments — Passed By CLI or Specified in snfoundry.toml

This doesn't take any arguments of its own.

script

Provides a set of commands to manage deployment scripts.

Script has the following subcommands:

init

Create a deployment script template.

The command creates the following file and directory structure:

.
└── scripts
    └── my_script
        ├── Scarb.toml
        └── src
            ├── lib.cairo
            └── my_script.cairo

<SCRIPT_NAME>

Required.

Name of a script to create.

run

Compile and run a cairo deployment script.

Required Common Arguments — Passed By CLI or Specified in snfoundry.toml

<MODULE_NAME>

Required.

Script module name that contains the 'main' function that will be executed.

--package <NAME>

Optional.

Name of the package that should be used.

If supplied, a script from this package will be used. Required if more than one package exists in a workspace.

--no-state-file

Optional.

Do not read/write state from/to the state file.

If set, a script will not read the state from the state file, and will not write a state to it.

tx-status

Get the status of a transaction

Required Common Arguments — Passed By CLI or Specified in snfoundry.toml

<TRANSACTION_HASH>

Required.

Hash of the transaction

verify

Verify Cairo contract on a chosen verification provider.

--contract-address, -a <CONTRACT_ADDRESS>

Required.

The address of the contract that is to be verified.

--contract-name <CONTRACT_NAME>

Required.

The name of the contract. The contract name is the part after the mod keyword in your contract file.

--verifier, -v <VERIFIER>

Optional.

The verification provider to use for the verification. Possible values are:

  • walnut

--network, -n <NETWORK>

Required.

The network on which block explorer will perform the verification. Possible values are:

  • mainnet
  • sepolia

--package <NAME>

Optional.

Name of the package that should be used.

If supplied, a contract from this package will be used. Required if more than one package exists in a workspace.

--confirm-verification

Optional.

If passed, assume "yes" as answer to confirmation prompt and run non-interactively.

Library Reference

  • declare - declares a 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
  • tx_status - gets the status of a transaction using its hash
  • 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, fee_settings: FeeSettings, nonce: Option<felt252>) -> Result<DeclareResult, ScriptCommandError>

Declares a contract and returns DeclareResult.

#[derive(Drop, Clone, Debug)]
pub struct DeclareResult {
    pub class_hash: ClassHash,
    pub transaction_hash: felt252,
}

#[derive(Drop, Clone, Debug, Serde, PartialEq)]
pub struct EthFeeSettings {
    pub max_fee: Option<felt252>,
}

#[derive(Drop, Clone, Debug, Serde, PartialEq)]
pub struct StrkFeeSettings {
    pub max_fee: Option<felt252>,
    pub max_gas: Option<u64>,
    pub max_gas_unit_price: Option<u128>,
}
  • contract_name - name of a contract as Cairo string. It is a name of the contract (part after mod keyword) e.g. "HelloStarknet".
  • fee_settings - fee settings for the transaction. Can be Eth or Strk. Read more about it here
  • nonce - nonce for declare transaction. If not provided, nonce will be set automatically.
use sncast_std::{declare, DeclareResult, FeeSettings, EthFeeSettings};

fn main() {
    let max_fee = 9999999;
    let declare_result = declare(
        "HelloStarknet",
        FeeSettings::Eth(EthFeeSettings { max_fee: Option::Some(max_fee) }),
        Option::None
    )
        .expect('declare failed');

    println!("declare_result: {}", declare_result);
    println!("debug declare_result: {:?}", declare_result);
}

deploy

pub fn deploy( class_hash: ClassHash, constructor_calldata: Array::<felt252>, salt: Option<felt252>, unique: bool, fee_settings: FeeSettings, nonce: Option<felt252> ) -> Result<DeployResult, ScriptCommandError>

Deploys a contract and returns DeployResult.

#[derive(Drop, Clone, Debug)]
pub struct DeployResult {
    pub contract_address: ContractAddress,
    pub transaction_hash: felt252,
}

#[derive(Drop, Clone, Debug, Serde, PartialEq)]
pub enum FeeSettings {
    Eth: EthFeeSettings,
    Strk: StrkFeeSettings
}

#[derive(Drop, Clone, Debug, Serde, PartialEq)]
pub struct EthFeeSettings {
    pub max_fee: Option<felt252>,
}

#[derive(Drop, Clone, Debug, Serde, PartialEq)]
pub struct StrkFeeSettings {
    pub max_fee: Option<felt252>,
    pub max_gas: Option<u64>,
    pub max_gas_unit_price: Option<u128>,
}
  • class_hash - class hash of a contract to deploy.
  • constructor_calldata - calldata for the contract constructor.
  • salt - salt for the contract address.
  • unique - determines if salt should be further modified with the account address.
  • fee_settings - fee settings for the transaction. Can be Eth or Strk. Read more about it here
  • nonce - nonce for declare transaction. If not provided, nonce will be set automatically.
use sncast_std::{deploy, DeployResult, FeeSettings, EthFeeSettings};

fn main() {
    let max_fee = 9999999;
    let salt = 0x1;
    let nonce = 0x1;
    let class_hash: ClassHash = 0x03a8b191831033ba48ee176d5dde7088e71c853002b02a1cfa5a760aa98be046
        .try_into()
        .expect('Invalid class hash value');

    let deploy_result = deploy(
        class_hash,
        ArrayTrait::new(),
        Option::Some(salt),
        true,
        FeeSettings::Eth(EthFeeSettings {max_fee: Option::Some(max_fee)}),
        Option::Some(deploy_nonce)
    ).expect('deploy failed');

    println!("deploy_result: {}", deploy_result);
    println!("debug deploy_result: {:?}", deploy_result);
}

invoke

pub fn invoke( contract_address: ContractAddress, entry_point_selector: felt252, calldata: Array::<felt252>, fee_settings: FeeSettings, nonce: Option<felt252> ) -> Result<InvokeResult, ScriptCommandError>

Invokes a contract and returns InvokeResult.

#[derive(Drop, Clone, Debug)]
pub struct InvokeResult {
    pub transaction_hash: felt252,
}

#[derive(Drop, Clone, Debug, Serde, PartialEq)]
pub struct EthFeeSettings {
    pub max_fee: Option<felt252>,
}

#[derive(Drop, Clone, Debug, Serde, PartialEq)]
pub struct StrkFeeSettings {
    pub max_fee: Option<felt252>,
    pub max_gas: Option<u64>,
    pub max_gas_unit_price: Option<u128>,
}
  • contract_address - address of the contract to invoke.
  • entry_point_selector - the selector of the function to invoke.
  • calldata - inputs to the function to be invoked.
  • fee_settings - fee settings for the transaction. Can be Eth or Strk. Read more about it here
  • nonce - nonce for declare transaction. If not provided, nonce will be set automatically.
use sncast_std::{invoke, InvokeResult, FeeSettings, EthFeeSettings};
use starknet::{ContractAddress};

fn main() {
    let contract_address: ContractAddress =
        0x1e52f6ebc3e594d2a6dc2a0d7d193cb50144cfdfb7fdd9519135c29b67e427
            .try_into()
            .expect('Invalid contract address value');

    let invoke_result = invoke(
        contract_address,
        selector!("put"),
        array![0x1, 0x2],
        FeeSettings::Eth(EthFeeSettings { max_fee: Option::None }),
        Option::None
    )
        .expect('invoke failed');

    println!("invoke_result: {}", invoke_result);
    println!("debug invoke_result: {:?}", invoke_result);
}

call

pub fn call( contract_address: ContractAddress, function_selector: felt252, calldata: Array::<felt252> ) -> Result<CallResult, ScriptCommandError>

Calls a contract and returns CallResult.

#[derive(Drop, Clone, Debug)]
pub struct CallResult {
    pub data: Array::<felt252>,
}
  • contract_address - address of the contract to call.
  • function_selector - the selector of the function to call.
  • calldata - inputs to the function to be called.
use sncast_std::{call, CallResult};
use starknet::{ContractAddress};

fn main() {
    let contract_address: ContractAddress = 0x1e52f6ebc3e594d2a6dc2a0d7d193cb50144cfdfb7fdd9519135c29b67e427
        .try_into()
        .expect('Invalid contract address value');

    let call_result = call(contract_address, selector!("get"), array![0x1]).expect('call failed');
    println!("call_result: {}", call_result);
    println!("debug call_result: {:?}", call_result);
}

get_nonce

pub fn get_nonce(block_tag: felt252) -> felt252

Gets nonce of an account for a given block tag (pending or latest) and returns nonce as felt252.

  • block_tag - block tag name, one of pending or latest.
use sncast_std::{get_nonce};

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

tx_status

pub fn tx_status(transaction_hash: felt252) -> Result<TxStatusResult, ScriptCommandError>

Gets the status of a transaction using its hash and returns TxStatusResult.

#[derive(Drop, Clone, Debug, Serde, PartialEq)]
pub enum FinalityStatus {
    Received,
    Rejected,
    AcceptedOnL2,
    AcceptedOnL1
}


#[derive(Drop, Copy, Debug, Serde, PartialEq)]
pub enum ExecutionStatus {
    Succeeded,
    Reverted,
}


#[derive(Drop, Clone, Debug, Serde, PartialEq)]
pub struct TxStatusResult {
    pub finality_status: FinalityStatus,
    pub execution_status: Option<ExecutionStatus>
}
  • transaction_hash - hash of the transaction
use sncast_std::{tx_status};

fn main() {
    let transaction_hash = 0x00ae35dacba17cde62b8ceb12e3b18f4ab6e103fa2d5e3d9821cb9dc59d59a3c;
    let status = tx_status(transaction_hash).expect("Failed to get transaction status");

    println!("transaction status: {:?}", status);
}

errors

#[derive(Drop, PartialEq, Serde, Debug)]
pub struct ErrorData {
    msg: ByteArray
}

#[derive(Drop, PartialEq, Serde, Debug)]
pub struct TransactionExecutionErrorData {
    transaction_index: felt252,
    execution_error: ByteArray,
}

#[derive(Drop, Serde, PartialEq, Debug)]
pub enum StarknetError {
    /// Failed to receive transaction
    FailedToReceiveTransaction,
    /// Contract not found
    ContractNotFound,
    /// Block not found
    BlockNotFound,
    /// Invalid transaction index in a block
    InvalidTransactionIndex,
    /// Class hash not found
    ClassHashNotFound,
    /// Transaction hash not found
    TransactionHashNotFound,
    /// Contract error
    ContractError: ErrorData,
    /// Transaction execution error
    TransactionExecutionError: TransactionExecutionErrorData,
    /// Class already declared
    ClassAlreadyDeclared,
    /// Invalid transaction nonce
    InvalidTransactionNonce,
    /// Max fee is smaller than the minimal transaction cost (validation plus fee transfer)
    InsufficientMaxFee,
    /// Account balance is smaller than the transaction's max_fee
    InsufficientAccountBalance,
    /// Account validation failed
    ValidationFailure: ErrorData,
    /// Compilation failed
    CompilationFailed,
    /// Contract class size it too large
    ContractClassSizeIsTooLarge,
    /// Sender address in not an account contract
    NonAccount,
    /// A transaction with the same hash already exists in the mempool
    DuplicateTx,
    /// the compiled class hash did not match the one supplied in the transaction
    CompiledClassHashMismatch,
    /// the transaction version is not supported
    UnsupportedTxVersion,
    /// the contract class version is not supported
    UnsupportedContractClassVersion,
    /// An unexpected error occurred
    UnexpectedError: ErrorData,
}

#[derive(Drop, Serde, PartialEq, Debug)]
pub enum ProviderError {
    StarknetError: StarknetError,
    RateLimited,
    UnknownError: ErrorData,
}

#[derive(Drop, Serde, PartialEq, Debug)]
pub enum TransactionError {
    Rejected,
    Reverted: ErrorData,
}

#[derive(Drop, Serde, PartialEq, Debug)]
pub enum WaitForTransactionError {
    TransactionError: TransactionError,
    TimedOut,
    ProviderError: ProviderError,
}

#[derive(Drop, Serde, PartialEq, Debug)]
pub enum ScriptCommandError {
    UnknownError: ErrorData,
    ContractArtifactsNotFound: ErrorData,
    WaitForTransactionError: WaitForTransactionError,
    ProviderError: ProviderError,
}