Starknet by Example

Starknet By Example is a collection of examples of how to use the Cairo programming language to create smart contracts on Starknet.

Starknet is a permissionless Validity-Rollup that supports general computation. It is currently used as an Ethereum layer-2. Starknet use the STARK cryptographic proof system to ensure high safety and scalability.

Starknet smart contracts are written in the Cairo language. Cairo is a Turing-complete programming language designed to write provable programs, abstracting the zk-STARK proof system away from the programmer.

⚠️ The examples have not been audited and are not intended for production use. The authors are not responsible for any damages caused by the use of the code provided in this book.

For whom is this for?

Starknet By Example is for anyone who wants to quickly learn how to write smart contracts on Starknet using Cairo with some technical background in programming and blockchain.

The first chapters will give you a basic understanding of the Cairo programming language and how to write, deploy and use smart contracts on Starknet. The later chapters will cover more advanced topics and show you how to write more complex smart contracts.

How to use this book?

Each chapter is a standalone example that demonstrates a specific feature or common use case of smart contracts on Starknet. If you are new to Starknet, it is recommended to read the chapters in order.

Most examples contains interfaces and tests that are hidden by default. You can hover over the code blocks and click on the "Show hidden lines" (eyes icon) to see the hidden code.

You can run each examples online by using the Starknet Remix Plugin.

Further reading

If you want to learn more about the Cairo programming language, you can read the Cairo Book. If you want to learn more about Starknet, you can read the Starknet documentation and the Starknet Book.

For more resources, check Awesome Starknet.

Versions

The current version of this book use:

cairo 2.6.3
edition = '2023_11'
scarb 2.6.4
Last change: 2024-04-12, commit: 93fab27

Basics of Smart Contracts in Cairo

The following chapters will introduce you to Starknet smart contracts and how to write them in Cairo.

Last change: 2023-10-12, commit: 90aa7c0

Storage

Here's the most minimal contract you can write in Cairo:

#[starknet::contract]
pub mod Contract {
    #[storage]
    struct Storage {}
}

#[cfg(test)]
mod test {
    use super::Contract;
    use starknet::{SyscallResultTrait, syscalls::deploy_syscall};

    #[test]
    fn test_can_deploy() {
        let (_contract_address, _) = deploy_syscall(
            Contract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
    // Not much to test
    }
}

Storage is a struct annotated with #[storage]. Every contract must have one and only one storage. It's a key-value store, where each key will be mapped to a storage address of the contract's storage space.

You can define storage variables in your contract, and then use them to store and retrieve data.

#[starknet::contract]
pub mod Contract {
    #[storage]
    struct Storage {
        a: u128,
        b: u8,
        c: u256
    }
}

#[cfg(test)]
mod test {
    use super::Contract;
    use starknet::{SyscallResultTrait, syscalls::deploy_syscall};
    use storage::contract::Contract::{
        aContractMemberStateTrait, bContractMemberStateTrait, cContractMemberStateTrait
    };

    #[test]
    fn test_can_deploy() {
        let (_contract_address, _) = deploy_syscall(
            Contract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
    }

    #[test]
    fn test_storage_members() {
        let state = Contract::contract_state_for_testing();
        assert_eq!(state.a.read(), 0_u128);
        assert_eq!(state.b.read(), 0_u8);
        assert_eq!(state.c.read(), 0_u256);
    }
}

Actually these two contracts have the same underlying sierra program. From the compiler's perspective, the storage variables don't exist until they are used.

You can also read about storing custom types

Last change: 2024-04-12, commit: 12bb167

Constructor

Constructors are a special type of function that runs only once when deploying a contract, and can be used to initialize the state of the contract. Your contract must not have more than one constructor, and that constructor function must be annotated with the #[constructor] attribute. Also, a good practice consists in naming that function constructor.

Here's a simple example that demonstrates how to initialize the state of a contract on deployment by defining logic inside a constructor.

#[starknet::contract]
pub mod ExampleConstructor {
    use starknet::ContractAddress;

    #[storage]
    struct Storage {
        names: LegacyMap::<ContractAddress, felt252>,
    }

    // The constructor is decorated with a `#[constructor]` attribute.
    // It is not inside an `impl` block.
    #[constructor]
    fn constructor(ref self: ContractState, name: felt252, address: ContractAddress) {
        self.names.write(address, name);
    }
}

#[cfg(test)]
mod tests {
    use super::{ExampleConstructor, ExampleConstructor::namesContractMemberStateTrait};
    use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};
    use starknet::{contract_address_const, testing::{set_contract_address}};

    #[test]
    fn should_deploy_with_constructor_init_value() {
        let name: felt252 = 'bob';
        let address: ContractAddress = contract_address_const::<'caller'>();

        let (contract_address, _) = deploy_syscall(
            ExampleConstructor::TEST_CLASS_HASH.try_into().unwrap(),
            0,
            array![name, address.into()].span(),
            false
        )
            .unwrap_syscall();

        let state = ExampleConstructor::contract_state_for_testing();
        set_contract_address(contract_address);

        let name = state.names.read(address);
        assert_eq!(name, 'bob');
    }
}
Last change: 2024-04-12, commit: a768477

Variables

There are 3 types of variables in Cairo contracts:

  • Local
    • declared inside a function
    • not stored on the blockchain
  • Storage
    • declared in the Storage of a contract
    • can be accessed from one execution to another
  • Global
    • provides information about the blockchain
    • accessed anywhere, even within library functions

Local Variables

Local variables are used and accessed within the scope of a specific function or block of code. They are temporary and exist only for the duration of that particular function or block execution.

Local variables are stored in memory and are not stored on the blockchain. This means they cannot be accessed from one execution to another. Local variables are useful for storing temporary data that is relevant only within a specific context. They also make the code more readable by giving names to intermediate values.

Here's a simple example of a contract with only local variables:

#[starknet::interface]
pub trait ILocalVariablesExample<TContractState> {
    fn do_something(self: @TContractState, value: u32) -> u32;
}

#[starknet::contract]
pub mod LocalVariablesExample {
    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl LocalVariablesExample of super::ILocalVariablesExample<ContractState> {
        fn do_something(self: @ContractState, value: u32) -> u32 {
            // This variable is local to the current block.
            // It can't be accessed once it goes out of scope.
            let increment = 10;

            {
                // The scope of a code block allows for local variable declaration
                // We can access variables defined in higher scopes.
                let sum = value + increment;
                sum
            }
        // We can't access the variable `sum` here, as it's out of scope.
        }
    }
}

#[cfg(test)]
mod test {
    use super::{
        LocalVariablesExample, ILocalVariablesExampleDispatcher,
        ILocalVariablesExampleDispatcherTrait
    };
    use starknet::{SyscallResultTrait, syscalls::deploy_syscall};

    #[test]
    fn test_can_deploy_and_do_something() {
        let (contract_address, _) = deploy_syscall(
            LocalVariablesExample::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();

        let contract = ILocalVariablesExampleDispatcher { contract_address };
        let value = 10;
        let res = contract.do_something(value);
        assert_eq!(res, value + 10);
    }
}

Storage Variables

Storage variables are persistent data stored on the blockchain. They can be accessed from one execution to another, allowing the contract to remember and update information over time. See Storage for more information.

To write or update a storage variable, you need to interact with the contract through an external entrypoint by sending a transaction.

On the other hand, you can read state variables, for free, without any transaction, simply by interacting with a node.

Here's a simple example of a contract with one storage variable:

#[starknet::interface]
pub trait IStorageVariableExample<TContractState> {
    fn set(ref self: TContractState, value: u32);
    fn get(self: @TContractState) -> u32;
}

#[starknet::contract]
pub mod StorageVariablesExample {
    // All storage variables are contained in a struct called Storage
    // annotated with the `#[storage]` attribute
    #[storage]
    struct Storage {
        // Storage variable holding a number
        value: u32
    }

    #[abi(embed_v0)]
    impl StorageVariablesExample of super::IStorageVariableExample<ContractState> {
        // Write to storage variables by sending a transaction
        // that calls an external function
        fn set(ref self: ContractState, value: u32) {
            self.value.write(value);
        }

        // Read from storage variables without sending transactions
        fn get(self: @ContractState) -> u32 {
            self.value.read()
        }
    }
}

#[cfg(test)]
mod test {
    use super::{
        StorageVariablesExample, StorageVariablesExample::valueContractMemberStateTrait,
        IStorageVariableExampleDispatcher, IStorageVariableExampleDispatcherTrait
    };
    use starknet::{SyscallResultTrait, syscalls::deploy_syscall};
    use starknet::testing::set_contract_address;

    #[test]
    fn test_can_deploy_and_mutate_storage() {
        let (contract_address, _) = deploy_syscall(
            StorageVariablesExample::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();

        let contract = IStorageVariableExampleDispatcher { contract_address };

        let initial_value = 10;

        contract.set(initial_value);
        assert_eq!(contract.get(), initial_value);

        // With contract state directly
        let state = StorageVariablesExample::contract_state_for_testing();
        set_contract_address(contract_address);
        assert_eq!(state.value.read(), initial_value);
    }
}

Global Variables

Global variables are predefined variables that provide information about the blockchain and the current execution environment. They can be accessed at any time and from anywhere!

In Starknet, you can access global variables by using specific functions from the starknet core library.

For example, the get_caller_address function returns the address of the caller of the current transaction, and the get_contract_address function returns the address of the current contract.

#[starknet::interface]
pub trait IGlobalExample<TContractState> {
    fn foo(ref self: TContractState);
}

#[starknet::contract]
pub mod GlobalExample {
    // import the required functions from the starknet core library
    use starknet::get_caller_address;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl GlobalExampleImpl of super::IGlobalExample<ContractState> {
        fn foo(ref self: ContractState) {
            // Call the get_caller_address function to get the sender address
            let _caller = get_caller_address();
        // ...
        }
    }
}

#[cfg(test)]
mod test {
    use super::GlobalExample;
    use starknet::{SyscallResultTrait, syscalls::deploy_syscall};

    #[test]
    fn test_can_deploy() {
        let (_contract_address, _) = deploy_syscall(
            GlobalExample::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
    // Not much to test
    }
}
Last change: 2024-04-12, commit: fcb534d

Visibility and Mutability

Visibility

There are two types of functions in Starknet contracts:

  • Functions that are accessible externally and can be called by anyone.
  • Functions that are only accessible internally and can only be called by other functions in the contract.

These functions are also typically divided into two different implementations blocks. The first impl block for externally accessible functions is explicitly annotated with an #[abi(embed_v0)] attribute. This indicates that all the functions inside this block can be called either as a transaction or as a view function. The second impl block for internally accessible functions is not annotated with any attribute, which means that all the functions inside this block are private by default.

State Mutability

Regardless of whether a function is internal or external, it can either modify the contract's state or not. When we declare functions that interact with storage variables inside a smart contract, we need to explicitly state that we are accessing the ContractState by adding it as the first parameter of the function. This can be done in two different ways:

  • If we want our function to be able to mutate the state of the contract, we pass it by reference like this: ref self: ContractState.
  • If we want our function to be read-only and not mutate the state of the contract, we pass it by snapshot like this: self: @ContractState.

Read-only functions, also called view functions, can be directly called without making a transaction. You can interact with them directly through a RPC node to read the contract's state, and they're free to call! External functions, that modify the contract's state, on the other side can only be called by making a transaction.

Internal functions can't be called externally, but the same principle applies regarding state mutability.

Let's take a look at a simple example contract to see these in action:

#[starknet::interface]
pub trait IExampleContract<TContractState> {
    fn set(ref self: TContractState, value: u32);
    fn get(self: @TContractState) -> u32;
}

#[starknet::contract]
pub mod ExampleContract {
    #[storage]
    struct Storage {
        value: u32
    }

    // The `abi(embed_v0)` attribute indicates that all
    // the functions in this implementation can be called externally.
    // Omitting this attribute would make all the functions internal.
    #[abi(embed_v0)]
    impl ExampleContract of super::IExampleContract<ContractState> {
        // The `set` function can be called externally
        // because it is written inside an implementation marked as `#[external]`.
        // It can modify the contract's state as it is passed as a reference.
        fn set(ref self: ContractState, value: u32) {
            self.value.write(value);
        }

        // The `get` function can be called externally
        // because it is written inside an implementation marked as `#[external]`.
        // However, it can't modify the contract's state is passed as a snapshot
        // -> It's only a "view" function.
        fn get(self: @ContractState) -> u32 {
            // We can call an internal function from any functions within the contract
            PrivateFunctionsTrait::_read_value(self)
        }
    }

    // The lack of the `external` attribute indicates that all the functions in
    // this implementation can only be called internally.
    // We name the trait `PrivateFunctionsTrait` to indicate that it is an
    // internal trait allowing us to call internal functions.
    #[generate_trait]
    pub impl PrivateFunctions of PrivateFunctionsTrait {
        // The `_read_value` function is outside the implementation that is
        // marked as `#[abi(embed_v0)]`, so it's an _internal_ function
        // and can only be called from within the contract.
        // However, it can't modify the contract's state is passed
        // as a snapshot: it is only a "view" function.
        fn _read_value(self: @ContractState) -> u32 {
            self.value.read()
        }
    }
}

#[cfg(test)]
mod test {
    use super::{ExampleContract, IExampleContractDispatcher, IExampleContractDispatcherTrait};
    use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};

    // These imports will allow us to directly access and set the contract state:
    // - for `value` storage variable access
    use super::ExampleContract::valueContractMemberStateTrait;
    // - for `PrivateFunctionsTrait` internal functions access
    //   implementation need to be public to be able to access it
    use super::ExampleContract::PrivateFunctionsTrait;
    // to set the contract address for the state
    // and also be able to use the dispatcher on the same contract
    use starknet::testing::set_contract_address;

    #[test]
    fn can_call_set_and_get() {
        let (contract_address, _) = deploy_syscall(
            ExampleContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();

        // You can interact with the external entrypoints of the contract using the dispatcher.
        let contract = IExampleContractDispatcher { contract_address };
        // But for internal functions, you need to use the contract state.
        let mut state = ExampleContract::contract_state_for_testing();
        set_contract_address(contract_address);

        // The contract dispatcher and state refer to the same contract.
        assert_eq!(contract.get(), state.value.read());

        // We can set from the dispatcher
        contract.set(42);
        assert_eq!(contract.get(), state.value.read());
        assert_eq!(42, state.value.read());
        assert_eq!(42, contract.get());

        // Or directly from the state for more complex operations
        state.value.write(24);
        assert_eq!(contract.get(), state.value.read());
        assert_eq!(24, state.value.read());
        assert_eq!(24, contract.get());

        // We can also acces internal functions from the state
        assert_eq!(state._read_value(), state.value.read());
        assert_eq!(state._read_value(), contract.get());
    }
}
Last change: 2024-04-12, commit: 8567119

Simple Counter

We now understand how to create a contract with state variables and functions. Let's create a simple counter contract that increments and decrements a counter.

Here's how it works:

  • The contract has a state variable counter that is initialized to 0.

  • When a user calls the increment entrypoint, the contract increments counter by 1.

  • When a user calls the decrement, the contract decrements counter by 1.

#[starknet::interface]
pub trait ISimpleCounter<TContractState> {
    fn get_current_count(self: @TContractState) -> u128;
    fn increment(ref self: TContractState);
    fn decrement(ref self: TContractState);
}

#[starknet::contract]
pub mod SimpleCounter {
    #[storage]
    struct Storage {
        // Counter variable
        counter: u128,
    }

    #[constructor]
    fn constructor(ref self: ContractState, init_value: u128) {
        // Store initial value
        self.counter.write(init_value);
    }

    #[abi(embed_v0)]
    impl SimpleCounter of super::ISimpleCounter<ContractState> {
        fn get_current_count(self: @ContractState) -> u128 {
            return self.counter.read();
        }

        fn increment(ref self: ContractState) {
            // Store counter value + 1
            let counter = self.counter.read() + 1;
            self.counter.write(counter);
        }

        fn decrement(ref self: ContractState) {
            // Store counter value - 1
            let counter = self.counter.read() - 1;
            self.counter.write(counter);
        }
    }
}

#[cfg(test)]
mod test {
    use super::{SimpleCounter, ISimpleCounterDispatcher, ISimpleCounterDispatcherTrait};
    use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};

    fn deploy(init_value: u128) -> ISimpleCounterDispatcher {
        let (contract_address, _) = deploy_syscall(
            SimpleCounter::TEST_CLASS_HASH.try_into().unwrap(),
            0,
            array![init_value.into()].span(),
            false
        )
            .unwrap_syscall();
        ISimpleCounterDispatcher { contract_address }
    }

    #[test]
    fn should_deploy() {
        let init_value = 10;
        let contract = deploy(init_value);

        let read_value = contract.get_current_count();
        assert_eq!(read_value, init_value);
    }

    #[test]
    fn should_increment() {
        let init_value = 10;
        let contract = deploy(init_value);

        contract.increment();
        assert_eq!(contract.get_current_count(), init_value + 1);

        contract.increment();
        contract.increment();
        assert_eq!(contract.get_current_count(), init_value + 3);
    }

    #[test]
    fn should_decrement() {
        let init_value = 10;
        let contract = deploy(init_value);

        contract.decrement();
        assert_eq!(contract.get_current_count(), init_value - 1);

        contract.decrement();
        contract.decrement();
        assert_eq!(contract.get_current_count(), init_value - 3);
    }

    #[test]
    fn should_increment_and_decrement() {
        let init_value = 10;
        let contract = deploy(init_value);

        contract.increment();
        contract.decrement();
        assert_eq!(contract.get_current_count(), init_value);

        contract.decrement();
        contract.increment();
        assert_eq!(contract.get_current_count(), init_value);
    }
}
Last change: 2024-04-12, commit: be52b01

Mappings

Maps are a key-value data structure used to store data within a smart contract. In Cairo they are implemented using the LegacyMap type. It's important to note that the LegacyMap type can only be used inside the Storage struct of a contract and that it can't be used elsewhere.

Here we demonstrate how to use the LegacyMap type within a Cairo contract, to map between a key of type ContractAddress and value of type felt252. The key-value types are specified within angular brackets <>. We write to the map by calling the write() method, passing in both the key and value. Similarly, we can read the value associated with a given key by calling the read() method and passing in the relevant key.

Some additional notes:

  • More complex key-value mappings are possible, for example we could use LegacyMap::<(ContractAddress, ContractAddress), felt252> to create an allowance on an ERC20 token contract.

  • In mappings, the address of the value at key k_1,...,k_n is h(...h(h(sn_keccak(variable_name),k_1),k_2),...,k_n) where is the Pedersen hash and the final value is taken mod2251−256. You can learn more about the contract storage layout in the Starknet Documentation

use starknet::ContractAddress;

#[starknet::interface]
pub trait IMapContract<TContractState> {
    fn set(ref self: TContractState, key: ContractAddress, value: felt252);
    fn get(self: @TContractState, key: ContractAddress) -> felt252;
}

#[starknet::contract]
pub mod MapContract {
    use starknet::ContractAddress;

    #[storage]
    struct Storage {
        // The `LegacyMap` type is only available inside the `Storage` struct.
        map: LegacyMap::<ContractAddress, felt252>,
    }

    #[abi(embed_v0)]
    impl MapContractImpl of super::IMapContract<ContractState> {
        fn set(ref self: ContractState, key: ContractAddress, value: felt252) {
            self.map.write(key, value);
        }

        fn get(self: @ContractState, key: ContractAddress) -> felt252 {
            self.map.read(key)
        }
    }
}

#[cfg(test)]
mod test {
    use super::{MapContract, IMapContractDispatcher, IMapContractDispatcherTrait};
    use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};

    #[test]
    fn test_deploy_and_set_get() {
        let (contract_address, _) = deploy_syscall(
            MapContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        let mut contract = IMapContractDispatcher { contract_address };

        // Write to map.
        let value: felt252 = 1;
        contract.set(key: contract_address, value: value);

        // Read from map.
        let read_value = contract.get(contract_address);
        assert(read_value == 1, 'wrong value read');
    }
}
Last change: 2024-04-12, commit: 864ccd5

Errors

Errors can be used to handle validation and other conditions that may occur during the execution of a smart contract. If an error is thrown during the execution of a smart contract call, the execution is stopped and any changes made during the transaction are reverted.

To throw an error, use the assert or panic functions:

  • assert is used to validate conditions. If the check fails, an error is thrown along with a specified value, often a message. It's similar to the require statement in Solidity.

  • panic immediately halt the execution with the given error value. It should be used when the condition to check is complex and for internal errors. It's similar to the revert statement in Solidity. (Use panic_with_felt252 to be able to directly pass a felt252 as the error value)

The assert_eq!, assert_ne!, assert_lt!, assert_le!, assert_gt! and assert_ge! macros can be used as an assert shorthand to compare two values, but only in tests. In contract, you should only use the assert function.

Here's a simple example that demonstrates the use of these functions:

#[starknet::interface]
pub trait IErrorsExample<TContractState> {
    fn test_assert(self: @TContractState, i: u256);
    fn test_panic(self: @TContractState, i: u256);
}

#[starknet::contract]
pub mod ErrorsExample {
    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl ErrorsExample of super::IErrorsExample<ContractState> {
        fn test_assert(self: @ContractState, i: u256) {
            // Assert used to validate a condition
            // and abort execution if the condition is not met
            assert(i > 0, 'i must be greater than 0');
        }

        fn test_panic(self: @ContractState, i: u256) {
            if (i == 0) {
                // Panic used to abort execution directly
                core::panic_with_felt252('i must not be 0');
            }
        }
    }
}

#[cfg(test)]
mod test {
    use super::{ErrorsExample, IErrorsExampleDispatcher, IErrorsExampleDispatcherTrait};
    use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};

    fn deploy() -> IErrorsExampleDispatcher {
        let (contract_address, _) = deploy_syscall(
            ErrorsExample::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        IErrorsExampleDispatcher { contract_address }
    }

    #[test]
    #[should_panic(expected: ('i must not be 0', 'ENTRYPOINT_FAILED'))]
    fn should_panic() {
        let contract = deploy();
        contract.test_panic(0);
    }

    #[test]
    #[should_panic(expected: ('i must be greater than 0', 'ENTRYPOINT_FAILED'))]
    fn should_assert() {
        let contract = deploy();
        contract.test_assert(0);
    }
}

Custom errors

You can make error handling easier by defining your error codes in a specific module.

#[starknet::interface]
pub trait ICustomErrorsExample<TContractState> {
    fn test_assert(self: @TContractState, i: u256);
    fn test_panic(self: @TContractState, i: u256);
}

pub mod Errors {
    pub const NOT_POSITIVE: felt252 = 'must be greater than 0';
    pub const NOT_NULL: felt252 = 'must not be null';
}

#[starknet::contract]
pub mod CustomErrorsExample {
    use super::Errors;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl CustomErrorsExample of super::ICustomErrorsExample<ContractState> {
        fn test_assert(self: @ContractState, i: u256) {
            assert(i > 0, Errors::NOT_POSITIVE);
        }

        fn test_panic(self: @ContractState, i: u256) {
            if (i == 0) {
                core::panic_with_felt252(Errors::NOT_NULL);
            }
        }
    }
}

#[cfg(test)]
mod test {
    use super::{
        CustomErrorsExample, ICustomErrorsExampleDispatcher, ICustomErrorsExampleDispatcherTrait
    };
    use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};

    fn deploy() -> ICustomErrorsExampleDispatcher {
        let (contract_address, _) = deploy_syscall(
            CustomErrorsExample::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        ICustomErrorsExampleDispatcher { contract_address }
    }

    #[test]
    #[should_panic(expected: ('must not be null', 'ENTRYPOINT_FAILED'))]
    fn should_panic() {
        let contract = deploy();
        contract.test_panic(0);
    }

    #[test]
    #[should_panic(expected: ('must be greater than 0', 'ENTRYPOINT_FAILED'))]
    fn should_assert() {
        let contract = deploy();
        contract.test_assert(0);
    }
}

Vault example

Here's another example that demonstrates the use of errors in a more complex contract:

#[starknet::interface]
pub trait IVaultErrorsExample<TContractState> {
    fn deposit(ref self: TContractState, amount: u256);
    fn withdraw(ref self: TContractState, amount: u256);
}

pub mod VaultErrors {
    pub const INSUFFICIENT_BALANCE: felt252 = 'insufficient_balance';
// you can define more errors here
}

#[starknet::contract]
pub mod VaultErrorsExample {
    use super::VaultErrors;

    #[storage]
    struct Storage {
        balance: u256,
    }

    #[abi(embed_v0)]
    impl VaultErrorsExample of super::IVaultErrorsExample<ContractState> {
        fn deposit(ref self: ContractState, amount: u256) {
            let mut balance = self.balance.read();
            balance = balance + amount;
            self.balance.write(balance);
        }

        fn withdraw(ref self: ContractState, amount: u256) {
            let mut balance = self.balance.read();

            assert(balance >= amount, VaultErrors::INSUFFICIENT_BALANCE);

            // Or using panic:
            if (balance < amount) {
                core::panic_with_felt252(VaultErrors::INSUFFICIENT_BALANCE);
            }

            let balance = balance - amount;

            self.balance.write(balance);
        }
    }
}

#[cfg(test)]
mod test {
    use super::{
        VaultErrorsExample, IVaultErrorsExampleDispatcher, IVaultErrorsExampleDispatcherTrait
    };
    use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};

    fn deploy() -> IVaultErrorsExampleDispatcher {
        let (contract_address, _) = deploy_syscall(
            VaultErrorsExample::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        IVaultErrorsExampleDispatcher { contract_address }
    }

    #[test]
    fn should_deposit_and_withdraw() {
        let mut contract = deploy();
        contract.deposit(10);
        contract.withdraw(5);
    }

    #[test]
    #[should_panic(expected: ('insufficient_balance', 'ENTRYPOINT_FAILED'))]
    fn should_panic_on_insufficient_balance() {
        let mut contract = deploy();
        contract.deposit(10);
        contract.withdraw(15);
    }
}
Last change: 2024-05-03, commit: dd8c9eb

Events

Events are a way to emit data from a contract. All events must be defined in the Event enum, which must be annotated with the #[event] attribute. An event is defined as a struct that derives the #[starknet::Event] trait. The fields of that struct correspond to the data that will be emitted. An event can be indexed for easy and fast access when querying the data at a later time, by adding a #[key] attribute to a field member.

Here's a simple example of a contract using events that emit an event each time a counter is incremented by the "increment" function:

#[starknet::interface]
pub trait IEventCounter<TContractState> {
    fn increment(ref self: TContractState, amount: u128);
}

#[starknet::contract]
pub mod EventCounter {
    use starknet::{get_caller_address, ContractAddress};

    #[storage]
    struct Storage {
        // Counter value
        counter: u128,
    }

    #[event]
    #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
    // The event enum must be annotated with the `#[event]` attribute.
    // It must also derive atleast `Drop` and `starknet::Event` traits.
    pub enum Event {
        CounterIncreased: CounterIncreased,
        UserIncreaseCounter: UserIncreaseCounter
    }

    // By deriving the `starknet::Event` trait, we indicate to the compiler that
    // this struct will be used when emitting events.
    #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
    pub struct CounterIncreased {
        pub amount: u128
    }

    #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
    pub struct UserIncreaseCounter {
        // The `#[key]` attribute indicates that this event will be indexed.
        // You can also use `#[flat]` for nested structs.
        #[key]
        pub user: ContractAddress,
        pub new_value: u128,
    }

    #[abi(embed_v0)]
    impl EventCounter of super::IEventCounter<ContractState> {
        fn increment(ref self: ContractState, amount: u128) {
            self.counter.write(self.counter.read() + amount);
            // Emit event
            self.emit(Event::CounterIncreased(CounterIncreased { amount }));
            self
                .emit(
                    Event::UserIncreaseCounter(
                        UserIncreaseCounter {
                            user: get_caller_address(), new_value: self.counter.read()
                        }
                    )
                );
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        EventCounter,
        EventCounter::{
            counterContractMemberStateTrait, Event, CounterIncreased, UserIncreaseCounter
        },
        IEventCounterDispatcherTrait, IEventCounterDispatcher
    };
    use starknet::{
        ContractAddress, contract_address_const, SyscallResultTrait, syscalls::deploy_syscall
    };
    use starknet::testing::{set_contract_address, set_account_contract_address};

    #[test]
    fn test_increment_events() {
        let (contract_address, _) = deploy_syscall(
            EventCounter::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        let mut contract = IEventCounterDispatcher { contract_address };
        let state = EventCounter::contract_state_for_testing();

        let amount = 10;
        let caller = contract_address_const::<'caller'>();

        // fake caller
        set_contract_address(caller);
        contract.increment(amount);
        // set back to the contract for reading state
        set_contract_address(contract_address);
        assert_eq!(state.counter.read(), amount);

        // Notice the order: the first event emitted is the first to be popped.
        assert_eq!(
            starknet::testing::pop_log(contract_address),
            Option::Some(Event::CounterIncreased(CounterIncreased { amount }))
        );
        assert_eq!(
            starknet::testing::pop_log(contract_address),
            Option::Some(
                Event::UserIncreaseCounter(UserIncreaseCounter { user: caller, new_value: amount })
            )
        );
    }
}
Last change: 2024-05-03, commit: a32e0d3

Syscalls

At the protocol level, the Starknet Operating System (OS) is the program that manages the whole Starknet network.

Some of the OS functionalities are exposed to smart contracts through the use of syscalls (system calls). Syscalls can be used to get information about the state of the Starknet network, to interact with/deploy contracts, emit events, send messages, and perform other low-level operations.

Syscalls return a SyscallResult which is either Sucess of Failure, allowing the contract to handle errors.

Here's the available syscalls:

get_block_hash

fn get_block_hash_syscall(block_number: u64) -> SyscallResult<felt252>

Get the hash of the block number block_number.

Only within the range [first_v0_12_0_block, current_block - 10].

get_execution_info

fn get_execution_info_syscall() -> SyscallResult<Box<starknet::info::ExecutionInfo>>

Get information about the current execution context. The returned ExecutionInfo is defined as :

#[derive(Copy, Drop, Debug)]
pub struct ExecutionInfo {
    pub block_info: Box<BlockInfo>,
    pub tx_info: Box<TxInfo>,
    pub caller_address: ContractAddress,
    pub contract_address: ContractAddress,
    pub entry_point_selector: felt252,
}

#[derive(Copy, Drop, Debug, Serde)]
pub struct BlockInfo {
    pub block_number: u64,
    pub block_timestamp: u64,
    pub sequencer_address: ContractAddress,
}

#[derive(Copy, Drop, Debug, Serde)]
pub struct TxInfo {
    // The version of the transaction. Always fixed (1)
    pub version: felt252,
    // The account contract from which this transaction originates.
    pub account_contract_address: ContractAddress,
    // The max_fee field of the transaction.
    pub max_fee: u128,
    // The signature of the transaction.
    pub signature: Span<felt252>,
    // The hash of the transaction.
    pub transaction_hash: felt252,
    // The identifier of the chain.
    // This field can be used to prevent replay of testnet transactions on mainnet.
    pub chain_id: felt252,
    // The transaction's nonce.
    pub nonce: felt252,
    // A span of ResourceBounds structs.
    pub resource_bounds: Span<ResourceBounds>,
    // The tip.
    pub tip: u128,
    // If specified, the paymaster should pay for the execution of the tx.
    // The data includes the address of the paymaster sponsoring the transaction, followed by
    // extra data to send to the paymaster.
    pub paymaster_data: Span<felt252>,
    // The data availability mode for the nonce.
    pub nonce_data_availability_mode: u32,
    // The data availability mode for the account balance from which fee will be taken.
    pub fee_data_availability_mode: u32,
    // If nonempty, will contain the required data for deploying and initializing an account
    // contract: its class hash, address salt and constructor calldata.
    pub account_deployment_data: Span<felt252>,
}

starknet::info provides helper functions to access the ExecutionInfo fields in a more convenient way:

  • get_execution_info() -> Box<ExecutionInfo>
  • get_caller_address() -> ContractAddress
  • get_contract_address() -> ContractAddress
  • get_block_info() -> Box<BlockInfo>
  • get_tx_info() -> Box<TxInfo>
  • get_block_timestamp() -> u64
  • get_block_number() -> u64

call_contract

fn call_contract_syscall(
    address: ContractAddress, entry_point_selector: felt252, calldata: Span<felt252>
) -> SyscallResult<Span<felt252>>

Call a contract at address with the given entry_point_selector and calldata. Failure can't be caught for this syscall, if the call fails, the whole transaction will revert.

This is not the recommended way to call a contract. Instead, use the dispatcher generated from the contract interface as shown in the Calling other contracts.

deploy

fn deploy_syscall(
    class_hash: ClassHash,
    contract_address_salt: felt252,
    calldata: Span<felt252>,
    deploy_from_zero: bool,
) -> SyscallResult<(ContractAddress, Span::<felt252>)>

Deploy a new contract of the predeclared class class_hash with calldata. The success result is a tuple containing the deployed contract address and the return value of the constructor.

contract_address_salt and deploy_from_zero are used to compute the contract address.

Example of the usage of the deploy syscall from the Factory pattern:

pub use starknet::{ContractAddress, ClassHash};

#[starknet::interface]
pub trait ICounterFactory<TContractState> {
    /// Create a new counter contract from stored arguments
    fn create_counter(ref self: TContractState) -> ContractAddress;

    /// Create a new counter contract from the given arguments
    fn create_counter_at(ref self: TContractState, init_value: u128) -> ContractAddress;

    /// Update the argument
    fn update_init_value(ref self: TContractState, init_value: u128);

    /// Update the class hash of the Counter contract to deploy when creating a new counter
    fn update_counter_class_hash(ref self: TContractState, counter_class_hash: ClassHash);
}

#[starknet::contract]
pub mod CounterFactory {
    use starknet::{ContractAddress, ClassHash, SyscallResultTrait, syscalls::deploy_syscall};

    #[storage]
    struct Storage {
        /// Store the constructor arguments of the contract to deploy
        init_value: u128,
        /// Store the class hash of the contract to deploy
        counter_class_hash: ClassHash,
    }

    #[constructor]
    fn constructor(ref self: ContractState, init_value: u128, class_hash: ClassHash) {
        self.init_value.write(init_value);
        self.counter_class_hash.write(class_hash);
    }

    #[abi(embed_v0)]
    impl Factory of super::ICounterFactory<ContractState> {
        fn create_counter_at(ref self: ContractState, init_value: u128) -> ContractAddress {
            // Contructor arguments
            let mut constructor_calldata: Array::<felt252> = array![init_value.into()];

            // Contract deployment
            let (deployed_address, _) = deploy_syscall(
                self.counter_class_hash.read(), 0, constructor_calldata.span(), false
            )
                .unwrap_syscall();

            deployed_address
        }

        fn create_counter(ref self: ContractState) -> ContractAddress {
            self.create_counter_at(self.init_value.read())
        }

        fn update_init_value(ref self: ContractState, init_value: u128) {
            self.init_value.write(init_value);
        }

        fn update_counter_class_hash(ref self: ContractState, counter_class_hash: ClassHash) {
            self.counter_class_hash.write(counter_class_hash);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{CounterFactory, ICounterFactoryDispatcher, ICounterFactoryDispatcherTrait};
    use starknet::{
        SyscallResultTrait, ContractAddress, ClassHash, contract_address_const,
        syscalls::deploy_syscall
    };

    // Define a target contract to deploy
    mod target {
        #[starknet::interface]
        pub trait ISimpleCounter<TContractState> {
            fn get_current_count(self: @TContractState) -> u128;
            fn increment(ref self: TContractState);
            fn decrement(ref self: TContractState);
        }

        #[starknet::contract]
        pub mod SimpleCounter {
            #[storage]
            struct Storage {
                // Counter variable
                counter: u128,
            }

            #[constructor]
            fn constructor(ref self: ContractState, init_value: u128) {
                // Store initial value
                self.counter.write(init_value);
            }

            #[abi(embed_v0)]
            impl SimpleCounter of super::ISimpleCounter<ContractState> {
                fn get_current_count(self: @ContractState) -> u128 {
                    self.counter.read()
                }

                fn increment(ref self: ContractState) {
                    // Store counter value + 1
                    let mut counter: u128 = self.counter.read() + 1;
                    self.counter.write(counter);
                }
                fn decrement(ref self: ContractState) {
                    // Store counter value - 1
                    let mut counter: u128 = self.counter.read() - 1;
                    self.counter.write(counter);
                }
            }
        }
    }
    use target::{ISimpleCounterDispatcher, ISimpleCounterDispatcherTrait};

    /// Deploy a counter factory contract
    fn deploy_factory(
        counter_class_hash: ClassHash, init_value: u128
    ) -> ICounterFactoryDispatcher {
        let mut constructor_calldata: Array::<felt252> = array![
            init_value.into(), counter_class_hash.into()
        ];

        let (contract_address, _) = deploy_syscall(
            CounterFactory::TEST_CLASS_HASH.try_into().unwrap(),
            0,
            constructor_calldata.span(),
            false
        )
            .unwrap_syscall();

        ICounterFactoryDispatcher { contract_address }
    }

    #[test]
    fn test_deploy_counter_constructor() {
        let init_value = 10;

        let counter_class_hash: ClassHash = target::SimpleCounter::TEST_CLASS_HASH
            .try_into()
            .unwrap();
        let factory = deploy_factory(counter_class_hash, init_value);

        let counter_address = factory.create_counter();
        let counter = target::ISimpleCounterDispatcher { contract_address: counter_address };

        assert_eq!(counter.get_current_count(), init_value);
    }

    #[test]
    fn test_deploy_counter_argument() {
        let init_value = 10;
        let argument_value = 20;

        let counter_class_hash: ClassHash = target::SimpleCounter::TEST_CLASS_HASH
            .try_into()
            .unwrap();
        let factory = deploy_factory(counter_class_hash, init_value);

        let counter_address = factory.create_counter_at(argument_value);
        let counter = target::ISimpleCounterDispatcher { contract_address: counter_address };

        assert_eq!(counter.get_current_count(), argument_value);
    }

    #[test]
    fn test_deploy_multiple() {
        let init_value = 10;
        let argument_value = 20;

        let counter_class_hash: ClassHash = target::SimpleCounter::TEST_CLASS_HASH
            .try_into()
            .unwrap();
        let factory = deploy_factory(counter_class_hash, init_value);

        let mut counter_address = factory.create_counter();
        let counter_1 = target::ISimpleCounterDispatcher { contract_address: counter_address };

        counter_address = factory.create_counter_at(argument_value);
        let counter_2 = target::ISimpleCounterDispatcher { contract_address: counter_address };

        assert_eq!(counter_1.get_current_count(), init_value);
        assert_eq!(counter_2.get_current_count(), argument_value);
    }
}

emit_event

fn emit_event_syscall(
    keys: Span<felt252>, data: Span<felt252>
) -> SyscallResult<()>

Emit an event with the given keys and data.

Example of the usage of the emit_event syscall from the Events chapter:

#[starknet::interface]
pub trait IEventCounter<TContractState> {
    fn increment(ref self: TContractState, amount: u128);
}

#[starknet::contract]
pub mod EventCounter {
    use starknet::{get_caller_address, ContractAddress};

    #[storage]
    struct Storage {
        // Counter value
        counter: u128,
    }

    #[event]
    #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
    // The event enum must be annotated with the `#[event]` attribute.
    // It must also derive atleast `Drop` and `starknet::Event` traits.
    pub enum Event {
        CounterIncreased: CounterIncreased,
        UserIncreaseCounter: UserIncreaseCounter
    }

    // By deriving the `starknet::Event` trait, we indicate to the compiler that
    // this struct will be used when emitting events.
    #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
    pub struct CounterIncreased {
        pub amount: u128
    }

    #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
    pub struct UserIncreaseCounter {
        // The `#[key]` attribute indicates that this event will be indexed.
        // You can also use `#[flat]` for nested structs.
        #[key]
        pub user: ContractAddress,
        pub new_value: u128,
    }

    #[abi(embed_v0)]
    impl EventCounter of super::IEventCounter<ContractState> {
        fn increment(ref self: ContractState, amount: u128) {
            self.counter.write(self.counter.read() + amount);
            // Emit event
            self.emit(Event::CounterIncreased(CounterIncreased { amount }));
            self
                .emit(
                    Event::UserIncreaseCounter(
                        UserIncreaseCounter {
                            user: get_caller_address(), new_value: self.counter.read()
                        }
                    )
                );
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        EventCounter,
        EventCounter::{
            counterContractMemberStateTrait, Event, CounterIncreased, UserIncreaseCounter
        },
        IEventCounterDispatcherTrait, IEventCounterDispatcher
    };
    use starknet::{
        ContractAddress, contract_address_const, SyscallResultTrait, syscalls::deploy_syscall
    };
    use starknet::testing::{set_contract_address, set_account_contract_address};

    #[test]
    fn test_increment_events() {
        let (contract_address, _) = deploy_syscall(
            EventCounter::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        let mut contract = IEventCounterDispatcher { contract_address };
        let state = EventCounter::contract_state_for_testing();

        let amount = 10;
        let caller = contract_address_const::<'caller'>();

        // fake caller
        set_contract_address(caller);
        contract.increment(amount);
        // set back to the contract for reading state
        set_contract_address(contract_address);
        assert_eq!(state.counter.read(), amount);

        // Notice the order: the first event emitted is the first to be popped.
        assert_eq!(
            starknet::testing::pop_log(contract_address),
            Option::Some(Event::CounterIncreased(CounterIncreased { amount }))
        );
        assert_eq!(
            starknet::testing::pop_log(contract_address),
            Option::Some(
                Event::UserIncreaseCounter(UserIncreaseCounter { user: caller, new_value: amount })
            )
        );
    }
}

library_call

fn library_call_syscall(
    class_hash: ClassHash, function_selector: felt252, calldata: Span<felt252>
) -> SyscallResult<Span<felt252>>

Call the function function_selector of the class class_hash with calldata. This is analogous to a delegate call in Ethereum, but only a single class is called.

send_message_to_L1

fn send_message_to_l1_syscall(
    to_address: felt252, payload: Span<felt252>
) -> SyscallResult<()>

Send a message to the L1 contract at to_address with the given payload.

replace_class

fn replace_class_syscall(
    class_hash: ClassHash
) -> SyscallResult<()>

Replace the class of the calling contract with the class class_hash.

This is used for contract upgrades. Here's an example from the Upgradeable Contract:

use starknet::class_hash::ClassHash;

#[starknet::interface]
pub trait IUpgradeableContract<TContractState> {
    fn upgrade(ref self: TContractState, impl_hash: ClassHash);
    fn version(self: @TContractState) -> u8;
}

#[starknet::contract]
pub mod UpgradeableContract_V0 {
    use starknet::class_hash::ClassHash;
    use starknet::SyscallResultTrait;
    use core::num::traits::Zero;

    #[storage]
    struct Storage {}


    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        Upgraded: Upgraded
    }

    #[derive(Drop, starknet::Event)]
    struct Upgraded {
        implementation: ClassHash
    }

    #[abi(embed_v0)]
    impl UpgradeableContract of super::IUpgradeableContract<ContractState> {
        fn upgrade(ref self: ContractState, impl_hash: ClassHash) {
            assert(!impl_hash.is_zero(), 'Class hash cannot be zero');
            starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall();
            self.emit(Event::Upgraded(Upgraded { implementation: impl_hash }))
        }

        fn version(self: @ContractState) -> u8 {
            0
        }
    }
}

The new class code will only be used for future calls to the contract. The current transaction containing the replace_class syscall will continue to use the old class code. (You can explicitly use the new class code by calling call_contract after the replace_class syscall in the same transaction)

storage_read

fn storage_read_syscall(
    address_domain: u32, address: StorageAddress,
) -> SyscallResult<felt252>

This low-level syscall is used to get the value in the storage of a specific key at address in the address_domain.

address_domain is used to distinguish between data availability modes. Currently, only mode ONCHAIN (0) is supported.

storage_write

fn storage_write_syscall(
    address_domain: u32, address: StorageAddress, value: felt252
) -> SyscallResult<()>

Similar to storage_read, this low-level syscall is used to write the value value in the storage of a specific key at address in the address_domain.

Documentation

Syscalls are defined in starknet::syscall

You can also read the official documentation page for more details.

Last change: 2024-02-17, commit: 32b8abc

Strings and ByteArrays

In Cairo, there's no native type for strings. Instead, you can use a single felt252 to store a short string or a ByteArray for strings of arbitrary length.

Short strings

Each character is encoded on 8 bits following the ASCII standard, so it's possible to store up to 31 characters in a single felt252.

Short strings are declared with single quotes, like this: 'Hello, World!'. See the Felt section for more information about short strings with the felt252 type.

Notice that any short string only use up to 31 bytes, so it's possible to represent any short string with the bytes31.

ByteArray (Long strings)

The ByteArray struct is used to store strings of arbitrary length. It contain a field data of type Array<bytes31> to store a sequence of short strings.

ByteArrays are declared with double quotes, like this: "Hello, World!".

They can be stored in the contract's storage and passed as arguments to entrypoints.

#[starknet::interface]
pub trait IMessage<TContractState> {
    fn append(ref self: TContractState, str: ByteArray);
    fn prepend(ref self: TContractState, str: ByteArray);
}

#[starknet::contract]
pub mod MessageContract {
    #[storage]
    struct Storage {
        message: ByteArray
    }

    #[constructor]
    fn constructor(ref self: ContractState) {
        self.message.write("World!");
    }

    #[abi(embed_v0)]
    impl MessageContract of super::IMessage<ContractState> {
        fn append(ref self: ContractState, str: ByteArray) {
            self.message.write(self.message.read() + str);
        }

        fn prepend(ref self: ContractState, str: ByteArray) {
            self.message.write(str + self.message.read());
        }
    }
}

#[cfg(test)]
mod tests {
    use bytearray::bytearray::{
        MessageContract::messageContractMemberStateTrait, MessageContract, IMessage
    };

    #[test]
    #[available_gas(2000000000)]
    fn message_contract_tests() {
        let mut state = MessageContract::contract_state_for_testing();
        state.message.write("World!");

        let message = state.message.read();
        assert(message == "World!", 'wrong message');

        state.append(" Good day, sir!");
        assert(state.message.read() == "World! Good day, sir!", 'wrong message (append)');

        state.prepend("Hello, ");
        assert(state.message.read() == "Hello, World! Good day, sir!", 'wrong message (prepend)');
    }
}

Operations

ByteArrays also provide a set of operations that facilitate the manipulation of strings. Here are the available operations on an instance of ByteArray:

  • append(mut other: @ByteArray) - Append another ByteArray to the current one.
  • append_word(word: felt252, len: usize) - Append a short string to the ByteArray. You need to ensure that len is at most 31 and that word can be converted to a bytes31 with maximum len bytes/characters.
  • append_byte(byte: felt252) - Append a single byte/character to the end of the ByteArray.
  • len() -> usize - Get the length of the ByteArray.
  • at(index: usize) -> Option<u8> - Access the character at the given index.
  • rev() -> ByteArray - Return a new ByteArray with the characters of the original one in reverse order.
  • append_word_rev(word: felt252, len: usize) - Append a short string to the ByteArray in reverse order. You need to ensure again that len is at most 31 and that word can be converted to a bytes31 with maximum len bytes/characters.

Additionally, there are some operations that can be called as static functions:

  • concat(left: @ByteArray, right: @ByteArray) - Concatenate two ByteArrays.

Concatenation of ByteArray (append(mut other: @ByteArray)) can also be done with the + and += operators directly, and access to a specific index can be done with the [] operator (with the maximum index being len() - 1).

Last change: 2024-02-16, commit: d569d57

Storing Custom Types

While native types can be stored in a contract's storage without any additional work, custom types require a bit more work. This is because at compile time, the compiler does not know how to store custom types in storage. To solve this, we need to implement the Store trait for our custom type. Hopefully, we can just derive this trait for our custom type - unless it contains arrays or dictionaries.

#[starknet::interface]
pub trait IStoringCustomType<TContractState> {
    fn set_person(ref self: TContractState, person: Person);
}

// Deriving the starknet::Store trait
// allows us to store the `Person` struct in the contract's storage.
#[derive(Drop, Serde, Copy, starknet::Store)]
pub struct Person {
    pub age: u8,
    pub name: felt252
}

#[starknet::contract]
pub mod StoringCustomType {
    use super::Person;

    #[storage]
    pub struct Storage {
        pub person: Person
    }

    #[abi(embed_v0)]
    impl StoringCustomType of super::IStoringCustomType<ContractState> {
        fn set_person(ref self: ContractState, person: Person) {
            self.person.write(person);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        IStoringCustomType, StoringCustomType, Person,
        StoringCustomType::personContractMemberStateTrait
    };

    #[test]
    fn can_call_set_person() {
        let mut state = StoringCustomType::contract_state_for_testing();

        let person = Person { age: 10, name: 'Joe' };

        state.set_person(person);
        let read_person = state.person.read();

        assert(person.age == read_person.age, 'wrong age');
        assert(person.name == read_person.name, 'wrong name');
    }
}
Last change: 2024-05-03, commit: cb31be2

Custom types in entrypoints

Using custom types in entrypoints requires our type to implement the Serde trait. This is because when calling an entrypoint, the input is sent as an array of felt252 to the entrypoint, and we need to be able to deserialize it into our custom type. Similarly, when returning a custom type from an entrypoint, we need to be able to serialize it into an array of felt252. Thankfully, we can just derive the Serde trait for our custom type.

The purpose is to only show the capability of using custom types as inputs and outputs in contract calls. We are not employing getters and setters for managing the contract's state in this example for simplicity.

#[starknet::interface]
pub trait ISerdeCustomType<TContractState> {
    fn person_input(ref self: TContractState, person: Person);
    fn person_output(self: @TContractState) -> Person;
}

// Deriving the `Serde` trait allows us to use
// the Person type as an entrypoint parameter and return value
#[derive(Drop, Serde)]
pub struct Person {
    pub age: u8,
    pub name: felt252
}

#[starknet::contract]
pub mod SerdeCustomType {
    use super::Person;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl SerdeCustomType of super::ISerdeCustomType<ContractState> {
        fn person_input(ref self: ContractState, person: Person) {}

        fn person_output(self: @ContractState) -> Person {
            Person { age: 10, name: 'Joe' }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        SerdeCustomType, Person, ISerdeCustomTypeDispatcher, ISerdeCustomTypeDispatcherTrait
    };
    use starknet::{ContractAddress, syscalls::deploy_syscall, SyscallResultTrait};

    fn deploy() -> ISerdeCustomTypeDispatcher {
        let (contract_address, _) = deploy_syscall(
            SerdeCustomType::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        ISerdeCustomTypeDispatcher { contract_address }
    }

    #[test]
    fn should_deploy() {
        deploy();
    }

    #[test]
    fn should_get_person_output() {
        let contract = deploy();
        let expected_person = Person { age: 10, name: 'Joe' };
        let received_person = contract.person_output();
        let age_received = received_person.age;
        let name_received = received_person.name;

        assert(age_received == expected_person.age, 'Wrong age value');
        assert(name_received == expected_person.name, 'Wrong name value');
    }

    #[test]
    #[available_gas(2000000000)]
    fn should_call_person_input() {
        let contract = deploy();
        let expected_person = Person { age: 10, name: 'Joe' };
        contract.person_input(expected_person);
    }
}
Last change: 2024-05-03, commit: 8d1b0b5

Documentation

It's important to take the time to document your code. It will helps developers and users to understand the contract and its functionalities.

In Cairo, you can add comments with //.

Best Practices:

Since Cairo 1, the community has adopted a Rust-like documentation style.

Contract Interface:

In smart contracts, you will often have a trait that defines the contract's interface (with #[starknet::interface]). This is the perfect place to include detailed documentation explaining the purpose and functionality of the contract entry points. You can follow this template:

#[starknet::interface]
trait IContract<TContractState> {
    /// High-level description of the function
    ///
    /// # Arguments
    ///
    /// * `arg_1` - Description of the argument
    /// * `arg_n` - ...
    ///
    /// # Returns
    ///
    /// High-level description of the return value
    fn do_something(ref self: TContractState, arg_1: T_arg_1) -> T_return;
}

Keep in mind that this should not describe the implementation details of the function, but rather the high-level purpose and functionality of the contract from the perspective of a user.

Implementation Details:

When writing the logic of the contract, you can add comments to describe the technical implementation details of the functions.

Avoid over-commenting: Comments should provide additional value and clarity.

Last change: 2023-12-05, commit: d8bdbed

Deploy and interact with contracts

In this chapter, we will see how to deploy and interact with contracts.

Last change: 2023-10-19, commit: dcadbd1

Contract interfaces and Traits generation

Contract interfaces define the structure and behavior of a contract, serving as the contract's public ABI. They list all the function signatures that a contract exposes. For a detailed explanation of interfaces, you can refer to the Cairo Book.

In cairo, to specify the interface you need to define a trait annotated with #[starknet::interface] and then implement that trait in the contract.

When a function needs to access the contract state, it must have a self parameter of type ContractState. This implies that the corresponding function signature in the interface trait must also take a TContractState type as a parameter. It's important to note that every function in the contract interface must have this self parameter of type TContractState.

You can use the #[generate_trait] attribute to implicitly generate the trait for a specific implementation block. This attribute automatically generates a trait with the same functions as the ones in the implemented block, replacing the self parameter with a generic TContractState parameter. However, you will need to annotate the block with the #[abi(per_item)] attribute, and each function with the appropriate attribute depending on whether it's an external function, a constructor or a l1 handler.

In summary, there's two ways to handle interfaces:

  • Explicitly, by defining a trait annoted with #[starknet::interface]
  • Implicitly, by using #[generate_trait] combined with the #[abi(per_item)] attributes, and annotating each function inside the implementation block with the appropriate attribute.

Explicit interface

#[starknet::interface]
pub trait IExplicitInterfaceContract<TContractState> {
    fn get_value(self: @TContractState) -> u32;
    fn set_value(ref self: TContractState, value: u32);
}

#[starknet::contract]
pub mod ExplicitInterfaceContract {
    #[storage]
    struct Storage {
        value: u32
    }

    #[abi(embed_v0)]
    impl ExplicitInterfaceContract of super::IExplicitInterfaceContract<ContractState> {
        fn get_value(self: @ContractState) -> u32 {
            self.value.read()
        }

        fn set_value(ref self: ContractState, value: u32) {
            self.value.write(value);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        IExplicitInterfaceContract, ExplicitInterfaceContract, IExplicitInterfaceContractDispatcher,
        IExplicitInterfaceContractDispatcherTrait
    };
    use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};

    #[test]
    fn test_interface() {
        let (contract_address, _) = deploy_syscall(
            ExplicitInterfaceContract::TEST_CLASS_HASH.try_into().unwrap(),
            0,
            array![].span(),
            false
        )
            .unwrap_syscall();
        let mut contract = IExplicitInterfaceContractDispatcher { contract_address };

        let value: u32 = 20;
        contract.set_value(value);

        let read_value = contract.get_value();

        assert_eq!(read_value, value);
    }
}

Implicit interface

#[starknet::contract]
pub mod ImplicitInterfaceContract {
    #[storage]
    struct Storage {
        value: u32
    }

    #[abi(per_item)]
    #[generate_trait]
    pub impl ImplicitInterfaceContract of IImplicitInterfaceContract {
        #[external(v0)]
        fn get_value(self: @ContractState) -> u32 {
            self.value.read()
        }

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

#[cfg(test)]
mod tests {
    use super::{
        ImplicitInterfaceContract, ImplicitInterfaceContract::valueContractMemberStateTrait,
        ImplicitInterfaceContract::IImplicitInterfaceContract
    };
    use starknet::{
        ContractAddress, SyscallResultTrait, syscalls::deploy_syscall, testing::set_contract_address
    };

    #[test]
    fn test_interface() {
        let (contract_address, _) = deploy_syscall(
            ImplicitInterfaceContract::TEST_CLASS_HASH.try_into().unwrap(),
            0,
            array![].span(),
            false
        )
            .unwrap_syscall();
        set_contract_address(contract_address);
        let mut state = ImplicitInterfaceContract::contract_state_for_testing();

        let value = 42;
        state.set_value(value);
        let read_value = state.get_value();

        assert_eq!(read_value, value);
    }
}

Note: You can import an implicitly generated contract interface with use contract::{GeneratedContractInterface}. However, the Dispatcher will not be generated automatically.

Internal functions

You can also use #[generate_trait] for your internal functions. Since this trait is generated in the context of the contract, you can define pure functions as well (functions without the self parameter).

#[starknet::interface]
pub trait IImplicitInternalContract<TContractState> {
    fn add(ref self: TContractState, nb: u32);
    fn get_value(self: @TContractState) -> u32;
    fn get_const(self: @TContractState) -> u32;
}

#[starknet::contract]
pub mod ImplicitInternalContract {
    #[storage]
    struct Storage {
        value: u32
    }

    #[generate_trait]
    impl InternalFunctions of InternalFunctionsTrait {
        fn set_value(ref self: ContractState, value: u32) {
            self.value.write(value);
        }

        fn get_const() -> u32 {
            42
        }
    }

    #[constructor]
    fn constructor(ref self: ContractState) {
        self.set_value(0);
    }

    #[abi(embed_v0)]
    impl ImplicitInternalContract of super::IImplicitInternalContract<ContractState> {
        fn add(ref self: ContractState, nb: u32) {
            self.set_value(self.value.read() + nb);
        }

        fn get_value(self: @ContractState) -> u32 {
            self.value.read()
        }

        fn get_const(self: @ContractState) -> u32 {
            self.get_const()
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        IImplicitInternalContract, ImplicitInternalContract, IImplicitInternalContractDispatcher,
        IImplicitInternalContractDispatcherTrait
    };
    use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};

    #[test]
    fn test_interface() {
        // Set up.
        let (contract_address, _) = deploy_syscall(
            ImplicitInternalContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        let mut contract = IImplicitInternalContractDispatcher { contract_address };

        let initial_value: u32 = 0;
        assert_eq!(contract.get_value(), initial_value);

        let add_value: u32 = 10;
        contract.add(add_value);

        assert_eq!(contract.get_value(), initial_value + add_value);
    }
}
Last change: 2024-05-03, commit: ee42efe

Calling other contracts

There are two different ways to call other contracts in Cairo.

The easiest way to call other contracts is by using the dispatcher of the contract you want to call. You can read more about Dispatchers in the Cairo Book

The other way is to use the starknet::call_contract_syscall syscall yourself. However, this method is not recommended and will not be covered in this example.

In order to call other contracts using dispatchers, you will need to define the called contract's interface as a trait annotated with the #[starknet::interface] attribute, and then import the IContractDispatcher and IContractDispatcherTrait items in your contract.

Here's the Callee contract interface and implementation:

// This will automatically generate ICalleeDispatcher and ICalleeDispatcherTrait
#[starknet::interface]
pub trait ICallee<TContractState> {
    fn set_value(ref self: TContractState, value: u128) -> u128;
}

#[starknet::contract]
pub mod Callee {
    #[storage]
    struct Storage {
        value: u128,
    }

    #[abi(embed_v0)]
    impl ICalleeImpl of super::ICallee<ContractState> {
        fn set_value(ref self: ContractState, value: u128) -> u128 {
            self.value.write(value);
            value
        }
    }
}

#[starknet::interface]
pub trait ICaller<TContractState> {
    fn set_value_from_address(
        ref self: TContractState, addr: starknet::ContractAddress, value: u128
    );
}

#[starknet::contract]
pub mod Caller {
    // We need to import the dispatcher of the callee contract
    // If you don't have a proper import, you can redefine the interface by yourself
    use super::{ICalleeDispatcher, ICalleeDispatcherTrait};
    use starknet::ContractAddress;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl ICallerImpl of super::ICaller<ContractState> {
        fn set_value_from_address(ref self: ContractState, addr: ContractAddress, value: u128) {
            ICalleeDispatcher { contract_address: addr }.set_value(value);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        Callee, ICalleeDispatcher, ICalleeDispatcherTrait, Callee::valueContractMemberStateTrait,
        Caller, ICallerDispatcher, ICallerDispatcherTrait
    };
    use starknet::{
        ContractAddress, contract_address_const, testing::set_contract_address,
        syscalls::deploy_syscall, SyscallResultTrait
    };

    fn deploy() -> (ICalleeDispatcher, ICallerDispatcher) {
        let (address_callee, _) = deploy_syscall(
            Callee::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        let (address_caller, _) = deploy_syscall(
            Caller::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        (
            ICalleeDispatcher { contract_address: address_callee },
            ICallerDispatcher { contract_address: address_caller }
        )
    }

    #[test]
    fn test_caller() {
        let init_value: u128 = 42;

        let (callee, caller) = deploy();
        caller.set_value_from_address(callee.contract_address, init_value);

        let state = Callee::contract_state_for_testing();
        set_contract_address(callee.contract_address);

        let value_read: u128 = state.value.read();

        assert_eq!(value_read, init_value);
    }
}

The following Caller contract use the Callee interface to call the Callee contract:

// This will automatically generate ICalleeDispatcher and ICalleeDispatcherTrait
#[starknet::interface]
pub trait ICallee<TContractState> {
    fn set_value(ref self: TContractState, value: u128) -> u128;
}

#[starknet::contract]
pub mod Callee {
    #[storage]
    struct Storage {
        value: u128,
    }

    #[abi(embed_v0)]
    impl ICalleeImpl of super::ICallee<ContractState> {
        fn set_value(ref self: ContractState, value: u128) -> u128 {
            self.value.write(value);
            value
        }
    }
}

#[starknet::interface]
pub trait ICaller<TContractState> {
    fn set_value_from_address(
        ref self: TContractState, addr: starknet::ContractAddress, value: u128
    );
}

#[starknet::contract]
pub mod Caller {
    // We need to import the dispatcher of the callee contract
    // If you don't have a proper import, you can redefine the interface by yourself
    use super::{ICalleeDispatcher, ICalleeDispatcherTrait};
    use starknet::ContractAddress;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl ICallerImpl of super::ICaller<ContractState> {
        fn set_value_from_address(ref self: ContractState, addr: ContractAddress, value: u128) {
            ICalleeDispatcher { contract_address: addr }.set_value(value);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        Callee, ICalleeDispatcher, ICalleeDispatcherTrait, Callee::valueContractMemberStateTrait,
        Caller, ICallerDispatcher, ICallerDispatcherTrait
    };
    use starknet::{
        ContractAddress, contract_address_const, testing::set_contract_address,
        syscalls::deploy_syscall, SyscallResultTrait
    };

    fn deploy() -> (ICalleeDispatcher, ICallerDispatcher) {
        let (address_callee, _) = deploy_syscall(
            Callee::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        let (address_caller, _) = deploy_syscall(
            Caller::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        (
            ICalleeDispatcher { contract_address: address_callee },
            ICallerDispatcher { contract_address: address_caller }
        )
    }

    #[test]
    fn test_caller() {
        let init_value: u128 = 42;

        let (callee, caller) = deploy();
        caller.set_value_from_address(callee.contract_address, init_value);

        let state = Callee::contract_state_for_testing();
        set_contract_address(callee.contract_address);

        let value_read: u128 = state.value.read();

        assert_eq!(value_read, init_value);
    }
}
Last change: 2024-05-03, commit: 1637154

Factory Pattern

The factory pattern is a well known pattern in object oriented programming. It provides an abstraction on how to instantiate a class.

In the case of smart contracts, we can use this pattern by defining a factory contract that have the sole responsibility of creating and managing other contracts.

Class hash and contract instance

In Starknet, there's a separation between contract's classes and instances. A contract class serves as a blueprint, defined by the underling Cairo bytecode, contract's entrypoints, ABI and Sierra program hash. The contract class is identified by a class hash. When you want to add a new class to the network, you first need to declare it.

When deploying a contract, you need to specify the class hash of the contract you want to deploy. Each instance of a contract has their own storage regardless of the class hash.

Using the factory pattern, we can deploy multiple instances of the same contract class and handle upgrades easily.

Minimal example

Here's a minimal example of a factory contract that deploy the SimpleCounter contract:

pub use starknet::{ContractAddress, ClassHash};

#[starknet::interface]
pub trait ICounterFactory<TContractState> {
    /// Create a new counter contract from stored arguments
    fn create_counter(ref self: TContractState) -> ContractAddress;

    /// Create a new counter contract from the given arguments
    fn create_counter_at(ref self: TContractState, init_value: u128) -> ContractAddress;

    /// Update the argument
    fn update_init_value(ref self: TContractState, init_value: u128);

    /// Update the class hash of the Counter contract to deploy when creating a new counter
    fn update_counter_class_hash(ref self: TContractState, counter_class_hash: ClassHash);
}

#[starknet::contract]
pub mod CounterFactory {
    use starknet::{ContractAddress, ClassHash, SyscallResultTrait, syscalls::deploy_syscall};

    #[storage]
    struct Storage {
        /// Store the constructor arguments of the contract to deploy
        init_value: u128,
        /// Store the class hash of the contract to deploy
        counter_class_hash: ClassHash,
    }

    #[constructor]
    fn constructor(ref self: ContractState, init_value: u128, class_hash: ClassHash) {
        self.init_value.write(init_value);
        self.counter_class_hash.write(class_hash);
    }

    #[abi(embed_v0)]
    impl Factory of super::ICounterFactory<ContractState> {
        fn create_counter_at(ref self: ContractState, init_value: u128) -> ContractAddress {
            // Contructor arguments
            let mut constructor_calldata: Array::<felt252> = array![init_value.into()];

            // Contract deployment
            let (deployed_address, _) = deploy_syscall(
                self.counter_class_hash.read(), 0, constructor_calldata.span(), false
            )
                .unwrap_syscall();

            deployed_address
        }

        fn create_counter(ref self: ContractState) -> ContractAddress {
            self.create_counter_at(self.init_value.read())
        }

        fn update_init_value(ref self: ContractState, init_value: u128) {
            self.init_value.write(init_value);
        }

        fn update_counter_class_hash(ref self: ContractState, counter_class_hash: ClassHash) {
            self.counter_class_hash.write(counter_class_hash);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{CounterFactory, ICounterFactoryDispatcher, ICounterFactoryDispatcherTrait};
    use starknet::{
        SyscallResultTrait, ContractAddress, ClassHash, contract_address_const,
        syscalls::deploy_syscall
    };

    // Define a target contract to deploy
    mod target {
        #[starknet::interface]
        pub trait ISimpleCounter<TContractState> {
            fn get_current_count(self: @TContractState) -> u128;
            fn increment(ref self: TContractState);
            fn decrement(ref self: TContractState);
        }

        #[starknet::contract]
        pub mod SimpleCounter {
            #[storage]
            struct Storage {
                // Counter variable
                counter: u128,
            }

            #[constructor]
            fn constructor(ref self: ContractState, init_value: u128) {
                // Store initial value
                self.counter.write(init_value);
            }

            #[abi(embed_v0)]
            impl SimpleCounter of super::ISimpleCounter<ContractState> {
                fn get_current_count(self: @ContractState) -> u128 {
                    self.counter.read()
                }

                fn increment(ref self: ContractState) {
                    // Store counter value + 1
                    let mut counter: u128 = self.counter.read() + 1;
                    self.counter.write(counter);
                }
                fn decrement(ref self: ContractState) {
                    // Store counter value - 1
                    let mut counter: u128 = self.counter.read() - 1;
                    self.counter.write(counter);
                }
            }
        }
    }
    use target::{ISimpleCounterDispatcher, ISimpleCounterDispatcherTrait};

    /// Deploy a counter factory contract
    fn deploy_factory(
        counter_class_hash: ClassHash, init_value: u128
    ) -> ICounterFactoryDispatcher {
        let mut constructor_calldata: Array::<felt252> = array![
            init_value.into(), counter_class_hash.into()
        ];

        let (contract_address, _) = deploy_syscall(
            CounterFactory::TEST_CLASS_HASH.try_into().unwrap(),
            0,
            constructor_calldata.span(),
            false
        )
            .unwrap_syscall();

        ICounterFactoryDispatcher { contract_address }
    }

    #[test]
    fn test_deploy_counter_constructor() {
        let init_value = 10;

        let counter_class_hash: ClassHash = target::SimpleCounter::TEST_CLASS_HASH
            .try_into()
            .unwrap();
        let factory = deploy_factory(counter_class_hash, init_value);

        let counter_address = factory.create_counter();
        let counter = target::ISimpleCounterDispatcher { contract_address: counter_address };

        assert_eq!(counter.get_current_count(), init_value);
    }

    #[test]
    fn test_deploy_counter_argument() {
        let init_value = 10;
        let argument_value = 20;

        let counter_class_hash: ClassHash = target::SimpleCounter::TEST_CLASS_HASH
            .try_into()
            .unwrap();
        let factory = deploy_factory(counter_class_hash, init_value);

        let counter_address = factory.create_counter_at(argument_value);
        let counter = target::ISimpleCounterDispatcher { contract_address: counter_address };

        assert_eq!(counter.get_current_count(), argument_value);
    }

    #[test]
    fn test_deploy_multiple() {
        let init_value = 10;
        let argument_value = 20;

        let counter_class_hash: ClassHash = target::SimpleCounter::TEST_CLASS_HASH
            .try_into()
            .unwrap();
        let factory = deploy_factory(counter_class_hash, init_value);

        let mut counter_address = factory.create_counter();
        let counter_1 = target::ISimpleCounterDispatcher { contract_address: counter_address };

        counter_address = factory.create_counter_at(argument_value);
        let counter_2 = target::ISimpleCounterDispatcher { contract_address: counter_address };

        assert_eq!(counter_1.get_current_count(), init_value);
        assert_eq!(counter_2.get_current_count(), argument_value);
    }
}

This factory can be used to deploy multiple instances of the SimpleCounter contract by calling the create_counter and create_counter_at functions.

The SimpleCounter class hash is stored inside the factory, and can be upgraded with the update_counter_class_hash function which allows to reuse the same factory contract when the SimpleCounter contract is upgraded.

This minimal example lacks several useful features such as access control, tracking of deployed contracts, events, ...

Last change: 2024-05-03, commit: 3364c20

Contract Testing

Testing plays a crucial role in software development, especially for smart contracts. In this section, we'll guide you through the basics of testing a smart contract on Starknet with scarb.

Let's start with a simple smart contract as an example:

#[starknet::interface]
pub trait ISimpleContract<TContractState> {
    fn get_value(self: @TContractState) -> u32;
    fn get_owner(self: @TContractState) -> starknet::ContractAddress;
    fn set_value(ref self: TContractState, value: u32);
}

#[starknet::contract]
pub mod SimpleContract {
    use starknet::{get_caller_address, ContractAddress};

    #[storage]
    struct Storage {
        value: u32,
        owner: ContractAddress
    }

    #[constructor]
    pub fn constructor(ref self: ContractState, initial_value: u32) {
        self.value.write(initial_value);
        self.owner.write(get_caller_address());
    }

    #[abi(embed_v0)]
    pub impl SimpleContractImpl of super::ISimpleContract<ContractState> {
        fn get_value(self: @ContractState) -> u32 {
            self.value.read()
        }

        fn get_owner(self: @ContractState) -> ContractAddress {
            self.owner.read()
        }

        fn set_value(ref self: ContractState, value: u32) {
            assert(self.owner.read() == get_caller_address(), 'Not owner');
            self.value.write(value);
        }
    }
}

Now, take a look at the tests for this contract:

#[cfg(test)]
mod tests {
    // Import the interface and dispatcher to be able to interact with the contract.
    use super::{SimpleContract, ISimpleContractDispatcher, ISimpleContractDispatcherTrait};

    // Import the deploy syscall to be able to deploy the contract.
    use starknet::{SyscallResultTrait, syscalls::deploy_syscall};
    use starknet::{
        ContractAddress, get_caller_address, get_contract_address, contract_address_const
    };

    // Use starknet test utils to fake the contract_address
    use starknet::testing::set_contract_address;

    // Deploy the contract and return its dispatcher.
    fn deploy(initial_value: u32) -> ISimpleContractDispatcher {
        // Declare and deploy
        let (contract_address, _) = deploy_syscall(
            SimpleContract::TEST_CLASS_HASH.try_into().unwrap(),
            0,
            array![initial_value.into()].span(),
            false
        )
            .unwrap_syscall();

        // Return the dispatcher.
        // The dispatcher allows to interact with the contract based on its interface.
        ISimpleContractDispatcher { contract_address }
    }

    #[test]
    fn test_deploy() {
        let initial_value: u32 = 10;
        let contract = deploy(initial_value);

        assert_eq!(contract.get_value(), initial_value);
        assert_eq!(contract.get_owner(), get_contract_address());
    }

    #[test]
    fn test_set_as_owner() {
        // Fake the contract address to owner
        let owner = contract_address_const::<'owner'>();
        set_contract_address(owner);

        // When deploying the contract, the owner is the caller.
        let contract = deploy(10);
        assert_eq!(contract.get_owner(), owner);

        // As the current caller is the owner, the value can be set.
        let new_value: u32 = 20;
        contract.set_value(new_value);

        assert_eq!(contract.get_value(), new_value);
    }

    #[test]
    #[should_panic]
    fn test_set_not_owner() {
        let owner = contract_address_const::<'owner'>();
        set_contract_address(owner);
        let contract = deploy(10);

        // Fake the contract address to another address
        let not_owner = contract_address_const::<'not owner'>();
        set_contract_address(not_owner);

        // As the current caller is not the owner, the value cannot be set.
        let new_value: u32 = 20;
        contract.set_value(new_value);
    // Panic expected
    }

    #[test]
    #[available_gas(150000)]
    fn test_deploy_gas() {
        deploy(10);
    }
}

To define our test, we use scarb, which allows us to create a separate module guarded with #[cfg(test)]. This ensures that the test module is only compiled when running tests using scarb test.

Each test is defined as a function with the #[test] attribute. You can also check if a test panics using the #[should_panic] attribute.

As we are in the context of a smart contract, you can also set up the gas limit for a test by using the #[available_gas(X)]. This is a great way to ensure that some of your contract's features stay under a certain gas limit!

Note: The term "gas" here refers to Sierra gas, not L1 gas

Now, let's move on to the testing process:

  • Use the deploy function logic to declare and deploy your contract.
  • Use assert to verify that the contract behaves as expected in the given context.
    • You can also use assertions macros: assert_eq, assert_ne, assert_gt, assert_ge, assert_lt, assert_le

If you didn't noticed yet, every examples in this book have hidden tests, you can see them by clicking on the "Show hidden lines" (eyes icon) on the top right of code blocks. You can also find a detailed explanation of testing in cairo in the Cairo book - Chapter 10.

Using the contract state

You can use the Contract::contract_state_for_testing function to access the contract state. This function is only available in the test environment and allows you to mutate and read the contract state directly.

This can be useful for testing internal functions, or specific state mutations that are not exposed to the contract's interface. You can either use it with a deployed contract or as a standalone state.

Here is an example of how to do the same previous test using the contract state:

#[cfg(test)]
mod tests_with_states {
    // Only import the contract
    use super::SimpleContract;

    // For accessing storage variables and entrypoints,
    // we must import the contract member state traits and implementation.
    use SimpleContract::{
        SimpleContractImpl, valueContractMemberStateTrait, ownerContractMemberStateTrait
    };

    use starknet::contract_address_const;
    use starknet::testing::set_caller_address;
    use core::num::traits::Zero;

    #[test]
    fn test_standalone_state() {
        let mut state = SimpleContract::contract_state_for_testing();

        // As no contract was deployed, the constructor was not called on the state
        // - with valueContractMemberStateTrait
        assert_eq!(state.value.read(), 0);
        // - with SimpleContractImpl 
        assert_eq!(state.get_value(), 0);
        assert_eq!(state.owner.read(), Zero::zero());

        // We can still directly call the constructor to initialize the state.
        let owner = contract_address_const::<'owner'>();
        // We are not setting the contract address but the caller address here,
        // as we are not deploying the contract but directly calling the constructor function.
        set_caller_address(owner);

        let initial_value: u32 = 10;
        SimpleContract::constructor(ref state, initial_value);
        assert_eq!(state.get_value(), initial_value);
        assert_eq!(state.get_owner(), owner);

        // As the current caller is the owner, the value can be set.
        let new_value: u32 = 20;
        state.set_value(new_value);
        assert_eq!(state.get_value(), new_value);
    }

    // But we can also deploy the contract and interact with it using the dispatcher
    // as shown in the previous tests, and still use the state for testing.
    use super::{ISimpleContractDispatcher, ISimpleContractDispatcherTrait};
    use starknet::{
        ContractAddress, SyscallResultTrait, syscalls::deploy_syscall, testing::set_contract_address
    };

    #[test]
    fn test_state_with_contract() {
        let owner = contract_address_const::<'owner'>();
        let not_owner = contract_address_const::<'not owner'>();

        // Deploy as owner
        let initial_value: u32 = 10;
        set_contract_address(owner);
        let (contract_address, _) = deploy_syscall(
            SimpleContract::TEST_CLASS_HASH.try_into().unwrap(),
            0,
            array![initial_value.into()].span(),
            false
        )
            .unwrap_syscall();
        let mut contract = ISimpleContractDispatcher { contract_address };

        // create the state
        // - Set back as not owner
        set_contract_address(not_owner);
        let mut state = SimpleContract::contract_state_for_testing();
        // - Currently, the state is not 'linked' to the contract
        assert_ne!(state.get_value(), initial_value);
        assert_ne!(state.get_owner(), owner);
        // - Link the state to the contract by setting the contract address
        set_contract_address(contract.contract_address);
        assert_eq!(state.get_value(), initial_value);
        assert_eq!(state.get_owner(), owner);

        // Mutating the state from the contract change the testing state
        set_contract_address(owner);
        let new_value: u32 = 20;
        contract.set_value(new_value);
        set_contract_address(contract.contract_address);
        assert_eq!(state.get_value(), new_value);

        // Mutating the state from the testing state change the contract state
        set_caller_address(owner);
        state.set_value(initial_value);
        assert_eq!(contract.get_value(), initial_value);

        // Directly mutating the state allows to change state
        // in ways that are not allowed by the contract, such as changing the owner.
        let new_owner = contract_address_const::<'new owner'>();
        state.owner.write(new_owner);
        assert_eq!(contract.get_owner(), new_owner);

        set_caller_address(new_owner);
        state.set_value(new_value);
        assert_eq!(contract.get_value(), new_value);
    }
}

Testing events

In order to test events, you need to use the starknet::pop_log function. If the contract did not emit any events, the function will return Option::None.

See the test for the Events section:

#[starknet::interface]
pub trait IEventCounter<TContractState> {
    fn increment(ref self: TContractState, amount: u128);
}

#[starknet::contract]
pub mod EventCounter {
    use starknet::{get_caller_address, ContractAddress};

    #[storage]
    struct Storage {
        // Counter value
        counter: u128,
    }

    #[event]
    #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
    // The event enum must be annotated with the `#[event]` attribute.
    // It must also derive atleast `Drop` and `starknet::Event` traits.
    pub enum Event {
        CounterIncreased: CounterIncreased,
        UserIncreaseCounter: UserIncreaseCounter
    }

    // By deriving the `starknet::Event` trait, we indicate to the compiler that
    // this struct will be used when emitting events.
    #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
    pub struct CounterIncreased {
        pub amount: u128
    }

    #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
    pub struct UserIncreaseCounter {
        // The `#[key]` attribute indicates that this event will be indexed.
        // You can also use `#[flat]` for nested structs.
        #[key]
        pub user: ContractAddress,
        pub new_value: u128,
    }

    #[abi(embed_v0)]
    impl EventCounter of super::IEventCounter<ContractState> {
        fn increment(ref self: ContractState, amount: u128) {
            self.counter.write(self.counter.read() + amount);
            // Emit event
            self.emit(Event::CounterIncreased(CounterIncreased { amount }));
            self
                .emit(
                    Event::UserIncreaseCounter(
                        UserIncreaseCounter {
                            user: get_caller_address(), new_value: self.counter.read()
                        }
                    )
                );
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        EventCounter,
        EventCounter::{
            counterContractMemberStateTrait, Event, CounterIncreased, UserIncreaseCounter
        },
        IEventCounterDispatcherTrait, IEventCounterDispatcher
    };
    use starknet::{
        ContractAddress, contract_address_const, SyscallResultTrait, syscalls::deploy_syscall
    };
    use starknet::testing::{set_contract_address, set_account_contract_address};

    #[test]
    fn test_increment_events() {
        let (contract_address, _) = deploy_syscall(
            EventCounter::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        let mut contract = IEventCounterDispatcher { contract_address };
        let state = EventCounter::contract_state_for_testing();

        let amount = 10;
        let caller = contract_address_const::<'caller'>();

        // fake caller
        set_contract_address(caller);
        contract.increment(amount);
        // set back to the contract for reading state
        set_contract_address(contract_address);
        assert_eq!(state.counter.read(), amount);

        // Notice the order: the first event emitted is the first to be popped.
        assert_eq!(
            starknet::testing::pop_log(contract_address),
            Option::Some(Event::CounterIncreased(CounterIncreased { amount }))
        );
        assert_eq!(
            starknet::testing::pop_log(contract_address),
            Option::Some(
                Event::UserIncreaseCounter(UserIncreaseCounter { user: caller, new_value: amount })
            )
        );
    }
}

Starknet Corelib Testing Module

To make testing more convenient, the testing module of the corelib provides some helpful functions:

  • set_caller_address(address: ContractAddress)
  • set_contract_address(address: ContractAddress)
  • set_block_number(block_number: u64)
  • set_block_timestamp(block_timestamp: u64)
  • set_account_contract_address(address: ContractAddress)
  • set_sequencer_address(address: ContractAddress)
  • set_version(version: felt252)
  • set_transaction_hash(hash: felt252)
  • set_chain_id(chain_id: felt252)
  • set_nonce(nonce: felt252)
  • set_signature(signature: felt252)
  • set_max_fee(fee: u128)
  • pop_log_raw(address: ContractAddress) -> Option<(Span<felt252>, Span<felt252>)>
  • pop_log<T, +starknet::Event<T>>(address: ContractAddress) -> Option<T>
  • pop_l2_to_l1_message(address: ContractAddress) -> Option<(felt252, Span<felt252>)>

You may also need the info module from the corelib, which allows you to access information about the current execution context (see syscalls):

  • get_caller_address() -> ContractAddress
  • get_contract_address() -> ContractAddress
  • get_block_info() -> Box<BlockInfo>
  • get_tx_info() -> Box<TxInfo>
  • get_block_timestamp() -> u64
  • get_block_number() -> u64

You can found the full list of functions in the Starknet Corelib repo.

Starknet Foundry

Starknet Foundry is a powerful toolkit for developing smart contracts on Starknet. It offers support for testing Starknet smart contracts on top of scarb with the Forge tool.

Testing with snforge is similar to the process we just described but simplified. Moreover, additional features are on the way, including cheatcodes or parallel tests execution. We highly recommend exploring Starknet Foundry and incorporating it into your projects.

For more detailed information about testing contracts with Starknet Foundry, check out the Starknet Foundry Book - Testing Contracts.

Last change: 2024-05-03, commit: 5fcef3e

Cairo Cheatsheet

This chapter aims to provide a quick reference for the most common Cairo constructs.

Last change: 2023-10-31, commit: d530b71

Felt252

Felt252 is a fundamental data type in Cairo from which all other data types are derived. Felt252 can also be used to store short string representations with a maximum length of 31 characters.

For example:

    let felt: felt252 = 100;
    let felt_as_str = 'Hello Starknet!';

    let _felt = felt + felt_as_str;
Last change: 2024-02-16, commit: d569d57

Mapping

The LegacyMap type can be used to represent a collection of key-value.

use starknet::ContractAddress;

#[starknet::interface]
trait IMappingExample<TContractState> {
    fn register_user(ref self: TContractState, student_add: ContractAddress, studentName: felt252);
    fn record_student_score(
        ref self: TContractState, student_add: ContractAddress, subject: felt252, score: u16
    );
    fn view_student_name(self: @TContractState, student_add: ContractAddress) -> felt252;
    fn view_student_score(
        self: @TContractState, student_add: ContractAddress, subject: felt252
    ) -> u16;
}

#[starknet::contract]
mod MappingContract {
    use starknet::ContractAddress;

    #[storage]
    struct Storage {
        students_name: LegacyMap::<ContractAddress, felt252>,
        students_result_record: LegacyMap::<(ContractAddress, felt252), u16>,
    }

    #[abi(embed_v0)]
    impl External of super::IMappingExample<ContractState> {
        fn register_user(
            ref self: ContractState, student_add: ContractAddress, studentName: felt252
        ) {
            self.students_name.write(student_add, studentName);
        }

        fn record_student_score(
            ref self: ContractState, student_add: ContractAddress, subject: felt252, score: u16
        ) {
            self.students_result_record.write((student_add, subject), score);
        }

        fn view_student_name(self: @ContractState, student_add: ContractAddress) -> felt252 {
            self.students_name.read(student_add)
        }

        fn view_student_score(
            self: @ContractState, student_add: ContractAddress, subject: felt252
        ) -> u16 {
            // for a 2D mapping its important to take note of the amount of brackets being used.
            self.students_result_record.read((student_add, subject))
        }
    }
}
Last change: 2023-12-07, commit: 286b664

Arrays

Arrays are collections of elements of the same type. The possible operations on arrays are defined with the array::ArrayTrait of the corelib:

trait ArrayTrait<T> {
    fn new() -> Array<T>;
    fn append(ref self: Array<T>, value: T);
    fn pop_front(ref self: Array<T>) -> Option<T> nopanic;
    fn pop_front_consume(self: Array<T>) -> Option<(Array<T>, T)> nopanic;
    fn get(self: @Array<T>, index: usize) -> Option<Box<@T>>;
    fn at(self: @Array<T>, index: usize) -> @T;
    fn len(self: @Array<T>) -> usize;
    fn is_empty(self: @Array<T>) -> bool;
    fn span(self: @Array<T>) -> Span<T>;
}

For example:

fn array() -> bool {
    let mut arr = array![];
    arr.append(10);
    arr.append(20);
    arr.append(30);

    assert(arr.len() == 3, 'array length should be 3');

    let first_value = arr.pop_front().unwrap();
    assert(first_value == 10, 'first value should match');

    let second_value = *arr.at(0);
    assert(second_value == 20, 'second value should match');

    // Returns true if an array is empty, then false if it isn't.
    arr.is_empty()
}
Last change: 2023-12-07, commit: 286b664

loop

A loop specifies a block of code that will run repetitively until a halting condition is encountered. For example:

    let mut arr = array![];

    // Same as ~ while (i < 10) arr.append(i++);
    let mut i: u32 = 0;
    let limit = 10;
    loop {
        if i == limit {
            break;
        };

        arr.append(i);

        i += 1;
    };

See also

while

Last change: 2024-04-10, commit: a0d03de

while

A while loop allows you to specify a condition that must be true for the loop to continue.

    let mut arr = array![];

    let mut i: u32 = 0;
    while (i < 10) {
        arr.append(i);
        i += 1;
    };

See also

loop

Last change: 2024-04-10, commit: a0d03de

if let

A if let statement is a combination of an if statement and a let statement. It allows you to execute the block only if the pattern matches. It's a cleaner way to handle a match statement with only one pattern that you want to handle.

#[derive(Drop)]
enum Foo {
    Bar,
    Baz,
    Qux: usize,
}

fn if_let() {
    let number = Option::Some(0_usize);
    let letter: Option<usize> = Option::None;

    // "if `let` destructures `number` into `Some(i)`:
    // evaluate the block (`{}`).
    if let Option::Some(i) = number {
        println!("Matched {}", i);
    }

    // If you need to specify a failure, use an else:
    if let Option::Some(i) = letter {
        println!("Matched {}", i);
    } else {
        // Destructure failed. Change to the failure case.
        println!("Didn't match a number.");
    }

    // Using `if let` with enum
    let a = Foo::Bar;
    let b = Foo::Baz;
    let c = Foo::Qux(100);

    // Variable a matches Foo::Bar
    if let Foo::Bar = a {
        println!("a is foobar");
    }

    // Variable b does not match Foo::Bar
    // So this will print nothing
    if let Foo::Bar = b {
        println!("b is foobar");
    }

    // Variable c matches Foo::Qux which has a value
    // Similar to Some() in the previous example
    if let Foo::Qux(value) = c {
        println!("c is {}", value);
    }
}

See also

while let

Last change: 2024-04-10, commit: a0d03de

while let

A while let loop is a combination of a while loop and a let statement. It allows you to execute the loop body only if the pattern matches.

    let mut option = Option::Some(0_usize);

    // "while `let` destructures `option` into `Some(i)`:
    // evaluate the block (`{}`), else `break`
    while let Option::Some(i) =
        option {
            if i > 0 {
                println!("Greater than 0, break...");
                option = Option::None;
            } else {
                println!("`i` is `{:?}`. Try again.", i);
                option = Option::Some(i + 1);
            }
        }

See also

while

Last change: 2024-04-10, commit: a0d03de

Match

The "match" expression in Cairo allows us to control the flow of our code by comparing a felt data type or an enum against various patterns and then running specific code based on the pattern that matches. For example:

#[derive(Drop, Serde)]
enum Colour {
    Red,
    Blue,
    Green,
    Orange,
    Black
}

#[derive(Drop, Serde)]
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> felt252 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn specified_colour(colour: Colour) -> felt252 {
    let mut response: felt252 = '';

    match colour {
        Colour::Red => { response = 'You passed in Red'; },
        Colour::Blue => { response = 'You passed in Blue'; },
        Colour::Green => { response = 'You passed in Green'; },
        Colour::Orange => { response = 'You passed in Orange'; },
        Colour::Black => { response = 'You passed in Black'; },
    };

    response
}

fn quiz(num: felt252) -> felt252 {
    let mut response: felt252 = '';

    match num {
        0 => { response = 'You failed' },
        _ => { response = 'You Passed' },
    };

    response
}
Last change: 2023-12-07, commit: 286b664

Tuples

Tuples is a data type to group a fixed number of items of potentially different types into a single compound structure. Unlike arrays, tuples have a set length and can contain elements of varying types. Once a tuple is created, its size cannot change. For example:

    let address = "0x000";
    let age = 20;
    let active = true;

    // Create tuple
    let user_tuple = (address, age, active);

    // Access tuple
    let (address, age, active) = stored_tuple;
Last change: 2023-12-07, commit: 286b664

Struct

A struct is a data type similar to tuple. Like tuples they can be used to hold data of different types. For example:

// With Store, you can store Data's structs in the storage part of contracts.
#[derive(Drop, starknet::Store)]
struct Data {
    address: starknet::ContractAddress,
    age: u8
}
Last change: 2023-12-07, commit: 286b664

Type casting

Cairo supports the conversion from one scalar types to another by using the into and try_into methods. traits::Into is used for conversion from a smaller data type to a larger data type, while traits::TryInto is used when converting from a larger to a smaller type that might not fit. For example:

    let a_number: u32 = 15;
    let my_felt252 = 15;

    // Since a u32 might not fit in a u8 and a u16, we need to use try_into,
    // then unwrap the Option<T> type thats returned.
    let _new_u8: u8 = a_number.try_into().unwrap();
    let new_u16: u16 = a_number.try_into().unwrap();

    // since new_u32 is the of the same type (u32) as rand_number, we can directly assign them,
    // or use the .into() method.
    let _new_u32: u32 = a_number;

    // When typecasting from a smaller size to an equal or larger size we use the .into() method.
    // Note: u64 and u128 are larger than u32, so a u32 type will always fit into them.
    let _new_u64: u64 = a_number.into();
    let _new_u128: u128 = a_number.into();

    // Since a felt252 is smaller than a u256, we can use the into() method
    let _new_u256: u256 = my_felt252.into();
    let _new_felt252: felt252 = new_u16.into();

    //note a usize is smaller than a felt so we use the try_into
    let _new_usize: usize = my_felt252.try_into().unwrap();
Last change: 2023-12-07, commit: 286b664

Components How-To

Components are like modular addons that can be snapped into contracts to add reusable logic, storage, and events. They are used to separate the core logic from common functionalities, simplifying the contract's code and making it easier to read and maintain. It also reduces the risk of bugs and vulnerabilities by using well-tested components.

Key characteristics:

  • Modularity: Easily pluggable into multiple contracts.
  • Reusable Logic: Encapsulates specific functionalities.
  • Not Standalone: Cannot be declared or deployed independently.

How to create a component

The following example shows a simple Switchable component that can be used to add a switch that can be either on or off. It contains a storage variable switchable_value, a function switch and an event Switch.

It is a good practice to prefix the component storage variables with the component name to avoid collisions.

#[starknet::interface]
pub trait ISwitchable<TContractState> {
    fn is_on(self: @TContractState) -> bool;
    fn switch(ref self: TContractState);
}

#[starknet::component]
pub mod switchable_component {
    #[storage]
    struct Storage {
        switchable_value: bool,
    }

    #[derive(Drop, starknet::Event)]
    struct SwitchEvent {}

    #[event]
    #[derive(Drop, starknet::Event)]
    pub enum Event {
        SwitchEvent: SwitchEvent,
    }

    #[embeddable_as(Switchable)]
    impl SwitchableImpl<
        TContractState, +HasComponent<TContractState>
    > of super::ISwitchable<ComponentState<TContractState>> {
        fn is_on(self: @ComponentState<TContractState>) -> bool {
            self.switchable_value.read()
        }

        fn switch(ref self: ComponentState<TContractState>) {
            self.switchable_value.write(!self.switchable_value.read());
            self.emit(Event::SwitchEvent(SwitchEvent {}));
        }
    }

    #[generate_trait]
    pub impl SwitchableInternalImpl<
        TContractState, +HasComponent<TContractState>
    > of SwitchableInternalTrait<TContractState> {
        fn _off(ref self: ComponentState<TContractState>) {
            self.switchable_value.write(false);
        }
    }
}

A component in itself is really similar to a contract, it can also have:

  • An interface defining entrypoints (ISwitchableComponent<TContractState>)
  • A Storage struct
  • Events
  • Internal functions

It don't have a constructor, but you can create a _init internal function and call it from the contract's constructor. In the previous example, the _off function is used this way.

It's currently not possible to use the same component multiple times in the same contract. This is a known limitation that may be lifted in the future.

For now, you can view components as an implementation of a specific interface/feature (Ownable, Upgradeable, ... ~able). This is why we called it Switchable and not Switch; The contract is switchable, not has a switch.

How to use a component

Now that we have a component, we can use it in a contract. The following contract incorporates the Switchable component:

#[starknet::contract]
pub mod SwitchContract {
    use components::switchable::switchable_component;

    component!(path: switchable_component, storage: switch, event: SwitchableEvent);

    #[abi(embed_v0)]
    impl SwitchableImpl = switchable_component::Switchable<ContractState>;
    impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        switch: switchable_component::Storage,
    }

    #[constructor]
    fn constructor(ref self: ContractState) {
        self.switch._off();
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        SwitchableEvent: switchable_component::Event,
    }
}

#[cfg(test)]
mod tests {
    use components::switchable::switchable_component::SwitchableInternalTrait;
    use components::switchable::ISwitchable;

    use starknet::storage::StorageMemberAccessTrait;
    use super::SwitchContract;

    fn STATE() -> SwitchContract::ContractState {
        SwitchContract::contract_state_for_testing()
    }

    #[test]
    #[available_gas(2000000)]
    fn test_init() {
        let state = STATE();
        assert(state.is_on() == false, 'The switch should be off');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_switch() {
        let mut state = STATE();

        state.switch();
        assert(state.is_on() == true, 'The switch should be on');

        state.switch();
        assert(state.is_on() == false, 'The switch should be off');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_value() {
        let mut state = STATE();
        assert(state.is_on() == state.switch.switchable_value.read(), 'Wrong value');

        state.switch.switch();
        assert(state.is_on() == state.switch.switchable_value.read(), 'Wrong value');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_internal_off() {
        let mut state = STATE();

        state.switch._off();
        assert(state.is_on() == false, 'The switch should be off');

        state.switch();
        state.switch._off();
        assert(state.is_on() == false, 'The switch should be off');
    }
}

Deep dive into components

You can find more in-depth information about components in the Cairo book - Components.

Last change: 2023-12-17, commit: 51a794a

Components Dependencies

A component with a dependency on a trait T can be used in a contract as long as the contract implements the trait T.

We will use a new Countable component as an example:

#[starknet::interface]
pub trait ICountable<TContractState> {
    fn get(self: @TContractState) -> u32;
    fn increment(ref self: TContractState);
}

#[starknet::component]
pub mod countable_component {
    #[storage]
    struct Storage {
        countable_value: u32,
    }

    #[embeddable_as(Countable)]
    impl CountableImpl<
        TContractState, +HasComponent<TContractState>
    > of super::ICountable<ComponentState<TContractState>> {
        fn get(self: @ComponentState<TContractState>) -> u32 {
            self.countable_value.read()
        }

        fn increment(ref self: ComponentState<TContractState>) {
            self.countable_value.write(self.countable_value.read() + 1);
        }
    }
}

We want to add a way to enable or disable the counter, in a way that calling increment on a disabled counter will not increment the counter. But we don't want to add this switch logic to the Countable component itself. We instead add the trait Switchable as a dependency to the Countable component.

Implementation of the trait in the contract

We first define the ISwitchable trait:

pub trait ISwitchable<TContractState> {
    fn is_on(self: @TContractState) -> bool;
    fn switch(ref self: TContractState);
}

Then we can modify the implementation of the Countable component to depend on the ISwitchable trait:

#[starknet::component]
pub mod countable_component {
    use components::countable::ICountable;
    use components::switchable::ISwitchable;

    #[storage]
    struct Storage {
        countable_value: u32,
    }

    #[embeddable_as(Countable)]
    impl CountableImpl<
        TContractState, +HasComponent<TContractState>, +ISwitchable<TContractState>
    > of ICountable<ComponentState<TContractState>> {
        fn get(self: @ComponentState<TContractState>) -> u32 {
            self.countable_value.read()
        }

        fn increment(ref self: ComponentState<TContractState>) {
            if (self.get_contract().is_on()) {
                self.countable_value.write(self.countable_value.read() + 1);
            }
        }
    }
}

A contract that uses the Countable component must implement the ISwitchable trait:

#[starknet::contract]
mod CountableContract {
    use components_dependencies::countable_dep_switch::countable_component;
    use components::switchable::ISwitchable;

    component!(path: countable_component, storage: counter, event: CountableEvent);

    #[abi(embed_v0)]
    impl CountableImpl = countable_component::Countable<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        counter: countable_component::Storage,
        switch: bool
    }

    // Implementation of the dependency:
    #[abi(embed_v0)]
    impl Switchable of ISwitchable<ContractState> {
        fn switch(ref self: ContractState) {
            self.switch.write(!self.switch.read());
        }

        fn is_on(self: @ContractState) -> bool {
            self.switch.read()
        }
    }

    #[constructor]
    fn constructor(ref self: ContractState) {
        self.switch.write(false);
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        CountableEvent: countable_component::Event,
    }
}

#[cfg(test)]
mod tests {
    use super::CountableContract;
    use components::countable::{ICountable, ICountableDispatcher, ICountableDispatcherTrait};
    use components::switchable::{ISwitchable, ISwitchableDispatcher, ISwitchableDispatcherTrait};

    use starknet::storage::StorageMemberAccessTrait;
    use starknet::SyscallResultTrait;
    use starknet::syscalls::deploy_syscall;

    fn deploy() -> (ICountableDispatcher, ISwitchableDispatcher) {
        let (contract_address, _) = deploy_syscall(
            CountableContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();

        (ICountableDispatcher { contract_address }, ISwitchableDispatcher { contract_address },)
    }

    #[test]
    #[available_gas(2000000)]
    fn test_init() {
        let (mut counter, mut switch) = deploy();

        assert(counter.get() == 0, 'Counter != 0');
        assert(switch.is_on() == false, 'Switch != false');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_increment_switch_off() {
        let (mut counter, mut switch) = deploy();

        counter.increment();
        assert(counter.get() == 0, 'Counter incremented');
        assert(switch.is_on() == false, 'Switch != false');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_increment_switch_on() {
        let (mut counter, mut switch) = deploy();

        switch.switch();
        assert(switch.is_on() == true, 'Switch != true');

        counter.increment();
        assert(counter.get() == 1, 'Counter did not increment');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_increment_multiple_switches() {
        let (mut counter, mut switch) = deploy();

        switch.switch();

        counter.increment();
        counter.increment();
        counter.increment();
        assert(counter.get() == 3, 'Counter did not increment');

        switch.switch();
        counter.increment();
        counter.increment();
        counter.increment();

        switch.switch();

        counter.increment();
        counter.increment();
        counter.increment();
        assert(counter.get() == 6, 'Counter did not increment');
    }
}

Implementation of the trait in another component

In the previous example, we implemented the ISwitchable trait in the contract.

We already implemented a Switchable component that provide an implementation of the ISwitchable trait. By using the Switchable component in a contract, we embed the implementation of the ISwitchable trait in the contract and resolve the dependency on the ISwitchable trait.

#[starknet::contract]
mod CountableContract {
    use components_dependencies::countable_dep_switch::countable_component;
    use components::switchable::switchable_component;

    component!(path: countable_component, storage: counter, event: CountableEvent);
    component!(path: switchable_component, storage: switch, event: SwitchableEvent);

    #[abi(embed_v0)]
    impl CountableImpl = countable_component::Countable<ContractState>;
    #[abi(embed_v0)]
    impl SwitchableImpl = switchable_component::Switchable<ContractState>;
    impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        counter: countable_component::Storage,
        #[substorage(v0)]
        switch: switchable_component::Storage
    }

    #[constructor]
    fn constructor(ref self: ContractState) {
        self.switch._off();
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        CountableEvent: countable_component::Event,
        SwitchableEvent: switchable_component::Event,
    }
}

#[cfg(test)]
mod tests {
    use super::CountableContract;
    use components::countable::{ICountable, ICountableDispatcher, ICountableDispatcherTrait};
    use components::switchable::{ISwitchable, ISwitchableDispatcher, ISwitchableDispatcherTrait};

    use starknet::storage::StorageMemberAccessTrait;
    use starknet::SyscallResultTrait;
    use starknet::syscalls::deploy_syscall;

    fn deploy() -> (ICountableDispatcher, ISwitchableDispatcher) {
        let (contract_address, _) = deploy_syscall(
            CountableContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();

        (ICountableDispatcher { contract_address }, ISwitchableDispatcher { contract_address },)
    }

    #[test]
    #[available_gas(2000000)]
    fn test_init() {
        let (mut counter, mut switch) = deploy();

        assert(counter.get() == 0, 'Counter != 0');
        assert(switch.is_on() == false, 'Switch != false');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_increment_switch_off() {
        let (mut counter, mut switch) = deploy();

        counter.increment();
        assert(counter.get() == 0, 'Counter incremented');
        assert(switch.is_on() == false, 'Switch != false');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_increment_switch_on() {
        let (mut counter, mut switch) = deploy();

        switch.switch();
        assert(switch.is_on() == true, 'Switch != true');

        counter.increment();
        assert(counter.get() == 1, 'Counter did not increment');
    }

    #[test]
    #[available_gas(3000000)]
    fn test_increment_multiple_switches() {
        let (mut counter, mut switch) = deploy();

        switch.switch();

        counter.increment();
        counter.increment();
        counter.increment();
        assert(counter.get() == 3, 'Counter did not increment');

        switch.switch();
        counter.increment();
        counter.increment();
        counter.increment();

        switch.switch();

        counter.increment();
        counter.increment();
        counter.increment();
        assert(counter.get() == 6, 'Counter did not increment');
    }
}

Dependency on other components internal functions

The previous example shows how to use the ISwitchable trait implementation from the Switchable component inside the Countable component by embedding the implementation in the contract. However, suppose we would like to turn off the switch after each increment. There's no set function in the ISwitchable trait, so we can't do it directly.

But the Switchable component implements the internal function _off from the SwitchableInternalTrait that set the switch to false. We can't embed SwitchableInternalImpl, but we can add switchable::HasComponent<TContractState> as a dependency inside CountableImpl.

We make the Countable component depend on the Switchable component. This will allow to do switchable::ComponentState<TContractState> -> TContractState -> countable::ComponentState<TcontractState> and access the internal functions of the Switchable component inside the Countable component:

#[starknet::component]
pub mod countable_component {
    use components::countable::ICountable;
    use components::switchable::ISwitchable;

    // Explicitly depends on a component and not a trait
    use components::switchable::switchable_component;
    use switchable_component::{SwitchableInternalImpl, SwitchableInternalTrait};

    #[storage]
    struct Storage {
        countable_value: u32,
    }

    #[generate_trait]
    impl GetSwitchable<
        TContractState,
        +HasComponent<TContractState>,
        +switchable_component::HasComponent<TContractState>,
        +Drop<TContractState>
    > of GetSwitchableTrait<TContractState> {
        fn get_switchable(
            self: @ComponentState<TContractState>
        ) -> @switchable_component::ComponentState<TContractState> {
            let contract = self.get_contract();
            switchable_component::HasComponent::<TContractState>::get_component(contract)
        }

        fn get_switchable_mut(
            ref self: ComponentState<TContractState>
        ) -> switchable_component::ComponentState<TContractState> {
            let mut contract = self.get_contract_mut();
            switchable_component::HasComponent::<TContractState>::get_component_mut(ref contract)
        }
    }

    #[embeddable_as(Countable)]
    impl CountableImpl<
        TContractState,
        +HasComponent<TContractState>,
        +ISwitchable<TContractState>,
        +switchable_component::HasComponent<TContractState>,
        +Drop<TContractState>
    > of ICountable<ComponentState<TContractState>> {
        fn get(self: @ComponentState<TContractState>) -> u32 {
            self.countable_value.read()
        }

        fn increment(ref self: ComponentState<TContractState>) {
            if (self.get_contract().is_on()) {
                self.countable_value.write(self.countable_value.read() + 1);

                // use the switchable component internal function
                let mut switch = self.get_switchable_mut();
                switch._off();
            }
        }
    }
}

The contract remains the same that the previous example, but the implementation of the Countable component is different:

#[starknet::contract]
pub mod CountableContract {
    use components_dependencies::countable_internal_dep_switch::countable_component;
    use components::switchable::switchable_component;

    component!(path: countable_component, storage: counter, event: CountableEvent);
    component!(path: switchable_component, storage: switch, event: SwitchableEvent);

    #[abi(embed_v0)]
    impl CountableImpl = countable_component::Countable<ContractState>;
    #[abi(embed_v0)]
    impl SwitchableImpl = switchable_component::Switchable<ContractState>;
    impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        counter: countable_component::Storage,
        #[substorage(v0)]
        switch: switchable_component::Storage
    }

    #[constructor]
    fn constructor(ref self: ContractState) {
        self.switch._off();
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        CountableEvent: countable_component::Event,
        SwitchableEvent: switchable_component::Event,
    }
}

#[cfg(test)]
mod tests {
    use super::CountableContract;
    use components::countable::{ICountable, ICountableDispatcher, ICountableDispatcherTrait};
    use components::switchable::{ISwitchable, ISwitchableDispatcher, ISwitchableDispatcherTrait};

    use starknet::storage::StorageMemberAccessTrait;
    use starknet::SyscallResultTrait;
    use starknet::syscalls::deploy_syscall;

    fn deploy() -> (ICountableDispatcher, ISwitchableDispatcher) {
        let (contract_address, _) = deploy_syscall(
            CountableContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();

        (ICountableDispatcher { contract_address }, ISwitchableDispatcher { contract_address },)
    }

    #[test]
    #[available_gas(2000000)]
    fn test_init() {
        let (mut counter, mut switch) = deploy();

        assert(counter.get() == 0, 'Counter != 0');
        assert(switch.is_on() == false, 'Switch != false');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_increment_switch_off() {
        let (mut counter, mut switch) = deploy();

        counter.increment();
        assert(counter.get() == 0, 'Counter incremented');
        assert(switch.is_on() == false, 'Switch != false');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_increment_switch_on() {
        let (mut counter, mut switch) = deploy();

        switch.switch();
        assert(switch.is_on() == true, 'Switch != true');

        counter.increment();
        assert(counter.get() == 1, 'Counter did not increment');

        // The counter turned the switch off.
        assert(switch.is_on() == false, 'Switch != false');
    }

    #[test]
    #[available_gas(3000000)]
    fn test_increment_multiple_switches() {
        let (mut counter, mut switch) = deploy();

        switch.switch();

        counter.increment();
        counter.increment(); // off
        counter.increment(); // off
        assert(counter.get() == 1, 'Counter did not increment');

        switch.switch();
        counter.increment();
        switch.switch();
        counter.increment();
        counter.increment();
        assert(counter.get() == 3, 'Counter did not increment');
    }
}
Last change: 2023-12-17, commit: 51a794a

Component-Contract Storage Collisions

Components can declare their own storage variables.

When a contract use a component, the component storage is merged with the contract storage. The storage layout is only determined by the variables names, so variables with the same name will collide.

In a future release, the #[substorage(v1)] will determine the storage layout based on the component as well, so collisions will be avoided.

A good practice is to prefix the component storage variables with the component name, as shown in the Switchable component example.

Example

Here's an example of a collision on the switchable_value storage variable of the Switchable component.

Interface:

#[starknet::interface]
pub trait ISwitchCollision<TContractState> {
    fn set(ref self: TContractState, value: bool);
    fn get(ref self: TContractState) -> bool;
}

Here's the storage of the contract (you can expand the code snippet to see the full contract):

#[starknet::interface]
pub trait ISwitchCollision<TContractState> {
    fn set(ref self: TContractState, value: bool);
    fn get(ref self: TContractState) -> bool;
}

#[starknet::contract]
pub mod SwitchCollisionContract {
    use components::switchable::switchable_component;

    component!(path: switchable_component, storage: switch, event: SwitchableEvent);

    #[abi(embed_v0)]
    impl SwitchableImpl = switchable_component::Switchable<ContractState>;
    impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl<ContractState>;

    #[storage]
    struct Storage {
        switchable_value: bool,
        #[substorage(v0)]
        switch: switchable_component::Storage,
    }

    #[constructor]
    fn constructor(ref self: ContractState) {
        self.switch._off();
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        SwitchableEvent: switchable_component::Event,
    }

    #[abi(embed_v0)]
    impl SwitchCollisionContract of super::ISwitchCollision<ContractState> {
        fn set(ref self: ContractState, value: bool) {
            self.switchable_value.write(value);
        }

        fn get(ref self: ContractState) -> bool {
            self.switchable_value.read()
        }
    }
}

Both the contract and the component have a switchable_value storage variable, so they collide:

mod switch_collision_tests {
    use components::switchable::switchable_component::SwitchableInternalTrait;
    use components::switchable::{ISwitchable, ISwitchableDispatcher, ISwitchableDispatcherTrait};

    use components::contracts::switch_collision::{
        SwitchCollisionContract, ISwitchCollisionDispatcher, ISwitchCollisionDispatcherTrait
    };

    use starknet::storage::StorageMemberAccessTrait;
    use starknet::SyscallResultTrait;
    use starknet::syscalls::deploy_syscall;

    fn deploy() -> (ISwitchCollisionDispatcher, ISwitchableDispatcher) {
        let (contract_address, _) = deploy_syscall(
            SwitchCollisionContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();

        (
            ISwitchCollisionDispatcher { contract_address },
            ISwitchableDispatcher { contract_address },
        )
    }

    #[test]
    #[available_gas(2000000)]
    fn test_collision() {
        let (mut contract, mut contract_iswitch) = deploy();

        assert(contract.get() == false, 'value !off');
        assert(contract_iswitch.is_on() == false, 'switch !off');

        contract_iswitch.switch();
        assert(contract_iswitch.is_on() == true, 'switch !on');
        assert(contract.get() == true, 'value !on');

        // `collision` between component storage 'value' and contract storage 'value'
        assert(contract.get() == contract_iswitch.is_on(), 'value != switch');

        contract.set(false);
        assert(contract.get() == contract_iswitch.is_on(), 'value != switch');
    }
}
Last change: 2023-12-17, commit: 51a794a

Ownable

The following Ownable component is a simple component that allows the contract to set an owner and provides a _assert_is_owner function that can be used to ensure that the caller is the owner.

It can also be used to renounce ownership of a contract, meaning that no one will be able to satisfy the _assert_is_owner function.

use starknet::ContractAddress;

#[starknet::interface]
pub trait IOwnable<TContractState> {
    fn owner(self: @TContractState) -> ContractAddress;
    fn transfer_ownership(ref self: TContractState, new: ContractAddress);
    fn renounce_ownership(ref self: TContractState);
}

mod Errors {
    pub const UNAUTHORIZED: felt252 = 'Not owner';
    pub const ZERO_ADDRESS_OWNER: felt252 = 'Owner cannot be zero';
    pub const ZERO_ADDRESS_CALLER: felt252 = 'Caller cannot be zero';
}

#[starknet::component]
pub mod ownable_component {
    use starknet::{ContractAddress, get_caller_address};
    use super::Errors;
    use core::num::traits::Zero;

    #[storage]
    struct Storage {
        ownable_owner: ContractAddress,
    }

    #[derive(Drop, starknet::Event)]
    struct OwnershipTransferredEvent {
        previous: ContractAddress,
        new: ContractAddress
    }

    #[derive(Drop, starknet::Event)]
    struct OwnershipRenouncedEvent {
        previous: ContractAddress
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    pub enum Event {
        OwnershipTransferredEvent: OwnershipTransferredEvent,
        OwnershipRenouncedEvent: OwnershipRenouncedEvent
    }

    #[embeddable_as(Ownable)]
    impl OwnableImpl<
        TContractState, +HasComponent<TContractState>
    > of super::IOwnable<ComponentState<TContractState>> {
        fn owner(self: @ComponentState<TContractState>) -> ContractAddress {
            self.ownable_owner.read()
        }

        fn transfer_ownership(ref self: ComponentState<TContractState>, new: ContractAddress) {
            self._assert_only_owner();
            self._transfer_ownership(new);
        }

        fn renounce_ownership(ref self: ComponentState<TContractState>) {
            self._assert_only_owner();
            self._renounce_ownership();
        }
    }

    #[generate_trait]
    pub impl OwnableInternalImpl<
        TContractState, +HasComponent<TContractState>
    > of OwnableInternalTrait<TContractState> {
        fn _assert_only_owner(self: @ComponentState<TContractState>) {
            let caller = get_caller_address();
            assert(!caller.is_zero(), Errors::ZERO_ADDRESS_CALLER);
            assert(caller == self.ownable_owner.read(), Errors::UNAUTHORIZED);
        }

        fn _init(ref self: ComponentState<TContractState>, owner: ContractAddress) {
            assert(!owner.is_zero(), Errors::ZERO_ADDRESS_OWNER);
            self.ownable_owner.write(owner);
        }

        fn _transfer_ownership(ref self: ComponentState<TContractState>, new: ContractAddress) {
            assert(!new.is_zero(), Errors::ZERO_ADDRESS_OWNER);
            let previous = self.ownable_owner.read();
            self.ownable_owner.write(new);
            self
                .emit(
                    Event::OwnershipTransferredEvent(OwnershipTransferredEvent { previous, new })
                );
        }

        fn _renounce_ownership(ref self: ComponentState<TContractState>) {
            let previous = self.ownable_owner.read();
            self.ownable_owner.write(Zero::zero());
            self.emit(Event::OwnershipRenouncedEvent(OwnershipRenouncedEvent { previous }));
        }
    }
}

A mock contract that uses the Ownable component:

#[starknet::interface]
pub trait IOwned<TContractState> {
    fn do_something(ref self: TContractState);
}

#[starknet::contract]
pub mod OwnedContract {
    use components::ownable::{IOwnable, ownable_component, ownable_component::OwnableInternalTrait};

    component!(path: ownable_component, storage: ownable, event: OwnableEvent);

    #[abi(embed_v0)]
    impl OwnableImpl = ownable_component::Ownable<ContractState>;
    impl OwnableInternalImpl = ownable_component::OwnableInternalImpl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        ownable: ownable_component::Storage,
    }

    #[constructor]
    fn constructor(ref self: ContractState) {
        self.ownable._init(starknet::get_caller_address());
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        OwnableEvent: ownable_component::Event,
    }

    #[abi(embed_v0)]
    impl Owned of super::IOwned<ContractState> {
        fn do_something(ref self: ContractState) {
            self.ownable._assert_only_owner();
        // ...
        }
    }
}

#[cfg(test)]
mod tests {
    use core::num::traits::Zero;
    use super::{OwnedContract, IOwnedDispatcher, IOwnedDispatcherTrait};
    use components::ownable::{IOwnable, IOwnableDispatcher, IOwnableDispatcherTrait};

    use starknet::{contract_address_const, ContractAddress};
    use starknet::testing::{set_caller_address, set_contract_address};
    use starknet::storage::StorageMemberAccessTrait;
    use starknet::SyscallResultTrait;
    use starknet::syscalls::deploy_syscall;

    fn deploy() -> (IOwnedDispatcher, IOwnableDispatcher) {
        let (contract_address, _) = deploy_syscall(
            OwnedContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();

        (IOwnedDispatcher { contract_address }, IOwnableDispatcher { contract_address },)
    }

    #[test]
    #[available_gas(2000000)]
    fn test_init() {
        let owner = contract_address_const::<'owner'>();
        set_contract_address(owner);
        let (_, ownable) = deploy();

        assert(ownable.owner() == owner, 'wrong_owner');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_wrong_owner() {
        set_contract_address(contract_address_const::<'owner'>());
        let (_, ownable) = deploy();

        let not_owner = contract_address_const::<'not_owner'>();
        assert(ownable.owner() != not_owner, 'wrong_owner');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_do_something() {
        set_contract_address(contract_address_const::<'owner'>());
        let (contract, _) = deploy();

        contract.do_something();
    // Should not panic
    }

    #[test]
    #[available_gas(2000000)]
    #[should_panic]
    fn test_do_something_not_owner() {
        set_contract_address(contract_address_const::<'owner'>());
        let (contract, _) = deploy();

        set_contract_address(contract_address_const::<'not_owner'>());
        contract.do_something();
    }

    #[test]
    #[available_gas(2000000)]
    fn test_transfer_ownership() {
        set_contract_address(contract_address_const::<'initial'>());
        let (contract, ownable) = deploy();

        let new_owner = contract_address_const::<'new_owner'>();
        ownable.transfer_ownership(new_owner);

        assert(ownable.owner() == new_owner, 'wrong_owner');

        set_contract_address(new_owner);
        contract.do_something();
    }

    #[test]
    #[available_gas(2000000)]
    #[should_panic]
    fn test_transfer_ownership_not_owner() {
        set_contract_address(contract_address_const::<'initial'>());
        let (_, ownable) = deploy();

        set_contract_address(contract_address_const::<'not_owner'>());
        ownable.transfer_ownership(contract_address_const::<'new_owner'>());
    }

    #[test]
    #[available_gas(2000000)]
    #[should_panic]
    fn test_transfer_ownership_zero_error() {
        set_contract_address(contract_address_const::<'initial'>());
        let (_, ownable) = deploy();

        ownable.transfer_ownership(Zero::zero());
    }

    #[test]
    #[available_gas(2000000)]
    fn test_renounce_ownership() {
        set_contract_address(contract_address_const::<'owner'>());
        let (_, ownable) = deploy();

        ownable.renounce_ownership();
        assert(ownable.owner() == Zero::zero(), 'not_zero_owner');
    }

    #[test]
    #[available_gas(2000000)]
    #[should_panic]
    fn test_renounce_ownership_not_owner() {
        set_contract_address(contract_address_const::<'owner'>());
        let (_, ownable) = deploy();

        set_contract_address(contract_address_const::<'not_owner'>());
        ownable.renounce_ownership();
    }

    #[test]
    #[available_gas(2000000)]
    #[should_panic]
    fn test_renounce_ownership_previous_owner() {
        set_contract_address(contract_address_const::<'owner'>());
        let (contract, ownable) = deploy();

        ownable.renounce_ownership();

        contract.do_something();
    }
}
Last change: 2024-01-05, commit: d1924c4

Upgradeable Contract

In Starknet, contracts are divided into two parts: contract classes and contract instances. This division follows a similar concept used in object-oriented programming languages, where we distinguish between the definition and implementation of objects.

A contract class is the definition of a contract: it specifies how the contract behaves. It contains essential information like the Cairo byte code, hint information, entry point names, and everything that defines its semantics unambiguously.

To identify different contract classes, Starknet assigns a unique identifier to each class: the class hash. A contract instance is a deployed contract that corresponds to a specific contract class. Think of it as an instance of an object in languages like Java.

Each class is identified by its class hash, which is analogous to a class name in an object-oriented programming language. A contract instance is a deployed contract corresponding to a class.

You can upgrade a deployed contract to a newer version by calling the replace_class_syscall function. By using this function, you can update the class hash associated with a deployed contract, effectively upgrading its implementation. However, this will not modify the contract's storage, so all the data stored in the contract will remain the same.

To illustrate this concept, let's consider an example with two contracts: UpgradeableContract_V0, and UpgradeableContract_V1. Start by deploying UpgradeableContract_V0 as the initial version. Next, send a transaction that invokes the upgrade function, with the class hash of UpgradeableContract_V1 as parameter to upgrade the class hash of the deployed contract to the UpgradeableContract_V1 one. Then, call the version method on the contract to see that the contract was upgraded to the V1 version.

use starknet::class_hash::ClassHash;

#[starknet::interface]
pub trait IUpgradeableContract<TContractState> {
    fn upgrade(ref self: TContractState, impl_hash: ClassHash);
    fn version(self: @TContractState) -> u8;
}

#[starknet::contract]
pub mod UpgradeableContract_V0 {
    use starknet::class_hash::ClassHash;
    use starknet::SyscallResultTrait;
    use core::num::traits::Zero;

    #[storage]
    struct Storage {}


    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        Upgraded: Upgraded
    }

    #[derive(Drop, starknet::Event)]
    struct Upgraded {
        implementation: ClassHash
    }

    #[abi(embed_v0)]
    impl UpgradeableContract of super::IUpgradeableContract<ContractState> {
        fn upgrade(ref self: ContractState, impl_hash: ClassHash) {
            assert(!impl_hash.is_zero(), 'Class hash cannot be zero');
            starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall();
            self.emit(Event::Upgraded(Upgraded { implementation: impl_hash }))
        }

        fn version(self: @ContractState) -> u8 {
            0
        }
    }
}
use starknet::class_hash::ClassHash;

#[starknet::interface]
pub trait IUpgradeableContract<TContractState> {
    fn upgrade(ref self: TContractState, impl_hash: ClassHash);
    fn version(self: @TContractState) -> u8;
}

#[starknet::contract]
pub mod UpgradeableContract_V1 {
    use starknet::class_hash::ClassHash;
    use starknet::SyscallResultTrait;
    use core::num::traits::Zero;

    #[storage]
    struct Storage {}

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        Upgraded: Upgraded
    }

    #[derive(Drop, starknet::Event)]
    struct Upgraded {
        implementation: ClassHash
    }

    #[abi(embed_v0)]
    impl UpgradeableContract of super::IUpgradeableContract<ContractState> {
        fn upgrade(ref self: ContractState, impl_hash: ClassHash) {
            assert(!impl_hash.is_zero(), 'Class hash cannot be zero');
            starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall();
            self.emit(Event::Upgraded(Upgraded { implementation: impl_hash }))
        }

        fn version(self: @ContractState) -> u8 {
            1
        }
    }
}
Last change: 2024-02-15, commit: 89037ca

Simple Defi Vault

This is the Cairo adaptation of the Solidity by example Vault. Here's how it works:

  • When a user deposits a token, the contract calculates the amount of shares to mint.

  • When a user withdraws, the contract burns their shares, calculates the yield, and withdraw both the yield and the initial amount of token deposited.

use starknet::ContractAddress;

// In order to make contract calls within our Vault,
// we need to have the interface of the remote ERC20 contract defined to import the Dispatcher.
#[starknet::interface]
pub trait IERC20<TContractState> {
    fn name(self: @TContractState) -> felt252;
    fn symbol(self: @TContractState) -> felt252;
    fn decimals(self: @TContractState) -> u8;
    fn total_supply(self: @TContractState) -> u256;
    fn balance_of(self: @TContractState, account: ContractAddress) -> u256;
    fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256;
    fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
    fn transfer_from(
        ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256
    ) -> bool;
    fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool;
}

#[starknet::interface]
pub trait ISimpleVault<TContractState> {
    fn deposit(ref self: TContractState, amount: u256);
    fn withdraw(ref self: TContractState, shares: u256);
}

#[starknet::contract]
pub mod SimpleVault {
    use super::{IERC20Dispatcher, IERC20DispatcherTrait};
    use starknet::{ContractAddress, get_caller_address, get_contract_address};

    #[storage]
    struct Storage {
        token: IERC20Dispatcher,
        total_supply: u256,
        balance_of: LegacyMap<ContractAddress, u256>
    }

    #[constructor]
    fn constructor(ref self: ContractState, token: ContractAddress) {
        self.token.write(IERC20Dispatcher { contract_address: token });
    }

    #[generate_trait]
    impl PrivateFunctions of PrivateFunctionsTrait {
        fn _mint(ref self: ContractState, to: ContractAddress, shares: u256) {
            self.total_supply.write(self.total_supply.read() + shares);
            self.balance_of.write(to, self.balance_of.read(to) + shares);
        }

        fn _burn(ref self: ContractState, from: ContractAddress, shares: u256) {
            self.total_supply.write(self.total_supply.read() - shares);
            self.balance_of.write(from, self.balance_of.read(from) - shares);
        }
    }

    #[abi(embed_v0)]
    impl SimpleVault of super::ISimpleVault<ContractState> {
        fn deposit(ref self: ContractState, amount: u256) {
            // a = amount
            // B = balance of token before deposit
            // T = total supply
            // s = shares to mint
            //
            // (T + s) / T = (a + B) / B 
            //
            // s = aT / B
            let caller = get_caller_address();
            let this = get_contract_address();

            let mut shares = 0;
            if self.total_supply.read() == 0 {
                shares = amount;
            } else {
                let balance = self.token.read().balance_of(this);
                shares = (amount * self.total_supply.read()) / balance;
            }

            PrivateFunctions::_mint(ref self, caller, shares);
            self.token.read().transfer_from(caller, this, amount);
        }

        fn withdraw(ref self: ContractState, shares: u256) {
            // a = amount
            // B = balance of token before withdraw
            // T = total supply
            // s = shares to burn
            //
            // (T - s) / T = (B - a) / B 
            //
            // a = sB / T
            let caller = get_caller_address();
            let this = get_contract_address();

            let balance = self.token.read().balance_of(this);
            let amount = (shares * balance) / self.total_supply.read();
            PrivateFunctions::_burn(ref self, caller, shares);
            self.token.read().transfer(caller, amount);
        }
    }
}
Last change: 2024-02-15, commit: 89037ca

ERC20 Token

Contracts that follow the ERC20 Standard are called ERC20 tokens. They are used to represent fungible assets.

To create an ERC20 conctract, it must implement the following interface:

#[starknet::interface]
trait IERC20<TContractState> {
    fn get_name(self: @TContractState) -> felt252;
    fn get_symbol(self: @TContractState) -> felt252;
    fn get_decimals(self: @TContractState) -> u8;
    fn get_total_supply(self: @TContractState) -> felt252;
    fn balance_of(self: @TContractState, account: ContractAddress) -> felt252;
    fn allowance(
        self: @TContractState, owner: ContractAddress, spender: ContractAddress
    ) -> felt252;
    fn transfer(ref self: TContractState, recipient: ContractAddress, amount: felt252);
    fn transfer_from(
        ref self: TContractState,
        sender: ContractAddress,
        recipient: ContractAddress,
        amount: felt252
    );
    fn approve(ref self: TContractState, spender: ContractAddress, amount: felt252);
    fn increase_allowance(ref self: TContractState, spender: ContractAddress, added_value: felt252);
    fn decrease_allowance(
        ref self: TContractState, spender: ContractAddress, subtracted_value: felt252
    );
}

In Starknet, function names should be written in snake_case. This is not the case in Solidity, where function names are written in camelCase. The Starknet ERC20 interface is therefore slightly different from the Solidity ERC20 interface.

Here's an implementation of the ERC20 interface in Cairo:

#[starknet::contract]
mod erc20 {
    use core::num::traits::Zero;
    use starknet::get_caller_address;
    use starknet::contract_address_const;
    use starknet::ContractAddress;

    #[storage]
    struct Storage {
        name: felt252,
        symbol: felt252,
        decimals: u8,
        total_supply: felt252,
        balances: LegacyMap::<ContractAddress, felt252>,
        allowances: LegacyMap::<(ContractAddress, ContractAddress), felt252>,
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        Transfer: Transfer,
        Approval: Approval,
    }
    #[derive(Drop, starknet::Event)]
    struct Transfer {
        from: ContractAddress,
        to: ContractAddress,
        value: felt252,
    }
    #[derive(Drop, starknet::Event)]
    struct Approval {
        owner: ContractAddress,
        spender: ContractAddress,
        value: felt252,
    }

    mod Errors {
        pub const APPROVE_FROM_ZERO: felt252 = 'ERC20: approve from 0';
        pub const APPROVE_TO_ZERO: felt252 = 'ERC20: approve to 0';
        pub const TRANSFER_FROM_ZERO: felt252 = 'ERC20: transfer from 0';
        pub const TRANSFER_TO_ZERO: felt252 = 'ERC20: transfer to 0';
        pub const BURN_FROM_ZERO: felt252 = 'ERC20: burn from 0';
        pub const MINT_TO_ZERO: felt252 = 'ERC20: mint to 0';
    }

    #[constructor]
    fn constructor(
        ref self: ContractState,
        recipient: ContractAddress,
        name: felt252,
        decimals: u8,
        initial_supply: felt252,
        symbol: felt252
    ) {
        self.name.write(name);
        self.symbol.write(symbol);
        self.decimals.write(decimals);
        self.mint(recipient, initial_supply);
    }

    #[abi(embed_v0)]
    impl IERC20Impl of super::IERC20<ContractState> {
        fn get_name(self: @ContractState) -> felt252 {
            self.name.read()
        }

        fn get_symbol(self: @ContractState) -> felt252 {
            self.symbol.read()
        }

        fn get_decimals(self: @ContractState) -> u8 {
            self.decimals.read()
        }

        fn get_total_supply(self: @ContractState) -> felt252 {
            self.total_supply.read()
        }

        fn balance_of(self: @ContractState, account: ContractAddress) -> felt252 {
            self.balances.read(account)
        }

        fn allowance(
            self: @ContractState, owner: ContractAddress, spender: ContractAddress
        ) -> felt252 {
            self.allowances.read((owner, spender))
        }

        fn transfer(ref self: ContractState, recipient: ContractAddress, amount: felt252) {
            let sender = get_caller_address();
            self._transfer(sender, recipient, amount);
        }

        fn transfer_from(
            ref self: ContractState,
            sender: ContractAddress,
            recipient: ContractAddress,
            amount: felt252
        ) {
            let caller = get_caller_address();
            self.spend_allowance(sender, caller, amount);
            self._transfer(sender, recipient, amount);
        }

        fn approve(ref self: ContractState, spender: ContractAddress, amount: felt252) {
            let caller = get_caller_address();
            self.approve_helper(caller, spender, amount);
        }

        fn increase_allowance(
            ref self: ContractState, spender: ContractAddress, added_value: felt252
        ) {
            let caller = get_caller_address();
            self
                .approve_helper(
                    caller, spender, self.allowances.read((caller, spender)) + added_value
                );
        }

        fn decrease_allowance(
            ref self: ContractState, spender: ContractAddress, subtracted_value: felt252
        ) {
            let caller = get_caller_address();
            self
                .approve_helper(
                    caller, spender, self.allowances.read((caller, spender)) - subtracted_value
                );
        }
    }

    #[generate_trait]
    impl InternalImpl of InternalTrait {
        fn _transfer(
            ref self: ContractState,
            sender: ContractAddress,
            recipient: ContractAddress,
            amount: felt252
        ) {
            assert(!sender.is_zero(), Errors::TRANSFER_FROM_ZERO);
            assert(!recipient.is_zero(), Errors::TRANSFER_TO_ZERO);
            self.balances.write(sender, self.balances.read(sender) - amount);
            self.balances.write(recipient, self.balances.read(recipient) + amount);
            self.emit(Transfer { from: sender, to: recipient, value: amount });
        }

        fn spend_allowance(
            ref self: ContractState,
            owner: ContractAddress,
            spender: ContractAddress,
            amount: felt252
        ) {
            let allowance = self.allowances.read((owner, spender));
            self.allowances.write((owner, spender), allowance - amount);
        }

        fn approve_helper(
            ref self: ContractState,
            owner: ContractAddress,
            spender: ContractAddress,
            amount: felt252
        ) {
            assert(!spender.is_zero(), Errors::APPROVE_TO_ZERO);
            self.allowances.write((owner, spender), amount);
            self.emit(Approval { owner, spender, value: amount });
        }

        fn mint(ref self: ContractState, recipient: ContractAddress, amount: felt252) {
            assert(!recipient.is_zero(), Errors::MINT_TO_ZERO);
            let supply = self.total_supply.read() + amount;
            self.total_supply.write(supply);
            let balance = self.balances.read(recipient) + amount;
            self.balances.write(recipient, balance);
            self
                .emit(
                    Event::Transfer(
                        Transfer {
                            from: contract_address_const::<0>(), to: recipient, value: amount
                        }
                    )
                );
        }
    }
}

There's several other implementations, such as the Open Zeppelin or the Cairo By Example ones.

Last change: 2024-02-15, commit: 89037ca

Constant Product AMM

This is the Cairo adaptation of the Solidity by example Constant Product AMM.

use starknet::ContractAddress;

#[starknet::interface]
pub trait IConstantProductAmm<TContractState> {
    fn swap(ref self: TContractState, token_in: ContractAddress, amount_in: u256) -> u256;
    fn add_liquidity(ref self: TContractState, amount0: u256, amount1: u256) -> u256;
    fn remove_liquidity(ref self: TContractState, shares: u256) -> (u256, u256);
}

#[starknet::contract]
pub mod ConstantProductAmm {
    use core::traits::Into;
    use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
    use starknet::{
        ContractAddress, get_caller_address, get_contract_address, contract_address_const
    };
    use core::integer::u256_sqrt;

    #[storage]
    struct Storage {
        token0: IERC20Dispatcher,
        token1: IERC20Dispatcher,
        reserve0: u256,
        reserve1: u256,
        total_supply: u256,
        balance_of: LegacyMap::<ContractAddress, u256>,
        // Fee 0 - 1000 (0% - 100%, 1 decimal places)
        // E.g. 3 = 0.3%
        fee: u16,
    }

    #[constructor]
    fn constructor(
        ref self: ContractState, token0: ContractAddress, token1: ContractAddress, fee: u16
    ) {
        // assert(fee <= 1000, 'fee > 1000');
        self.token0.write(IERC20Dispatcher { contract_address: token0 });
        self.token1.write(IERC20Dispatcher { contract_address: token1 });
        self.fee.write(fee);
    }

    #[generate_trait]
    impl PrivateFunctions of PrivateFunctionsTrait {
        fn _mint(ref self: ContractState, to: ContractAddress, amount: u256) {
            self.balance_of.write(to, self.balance_of.read(to) + amount);
            self.total_supply.write(self.total_supply.read() + amount);
        }

        fn _burn(ref self: ContractState, from: ContractAddress, amount: u256) {
            self.balance_of.write(from, self.balance_of.read(from) - amount);
            self.total_supply.write(self.total_supply.read() - amount);
        }

        fn _update(ref self: ContractState, reserve0: u256, reserve1: u256) {
            self.reserve0.write(reserve0);
            self.reserve1.write(reserve1);
        }

        #[inline(always)]
        fn select_token(self: @ContractState, token: ContractAddress) -> bool {
            assert(
                token == self.token0.read().contract_address
                    || token == self.token1.read().contract_address,
                'invalid token'
            );
            token == self.token0.read().contract_address
        }

        #[inline(always)]
        fn min(x: u256, y: u256) -> u256 {
            if (x <= y) {
                x
            } else {
                y
            }
        }
    }

    #[abi(embed_v0)]
    impl ConstantProductAmm of super::IConstantProductAmm<ContractState> {
        fn swap(ref self: ContractState, token_in: ContractAddress, amount_in: u256) -> u256 {
            assert(amount_in > 0, 'amount in = 0');
            let is_token0: bool = self.select_token(token_in);

            let (token0, token1): (IERC20Dispatcher, IERC20Dispatcher) = (
                self.token0.read(), self.token1.read()
            );
            let (reserve0, reserve1): (u256, u256) = (self.reserve0.read(), self.reserve1.read());
            let (
                token_in, token_out, reserve_in, reserve_out
            ): (IERC20Dispatcher, IERC20Dispatcher, u256, u256) =
                if (is_token0) {
                (token0, token1, reserve0, reserve1)
            } else {
                (token1, token0, reserve1, reserve0)
            };

            let caller = get_caller_address();
            let this = get_contract_address();
            token_in.transfer_from(caller, this, amount_in);

            // How much dy for dx?
            // xy = k
            // (x + dx)(y - dy) = k
            // y - dy = k / (x + dx)
            // y - k / (x + dx) = dy
            // y - xy / (x + dx) = dy
            // (yx + ydx - xy) / (x + dx) = dy
            // ydx / (x + dx) = dy

            let amount_in_with_fee = (amount_in * (1000 - self.fee.read().into()) / 1000);
            let amount_out = (reserve_out * amount_in_with_fee) / (reserve_in + amount_in_with_fee);

            token_out.transfer(caller, amount_out);

            self._update(self.token0.read().balance_of(this), self.token1.read().balance_of(this));
            amount_out
        }

        fn add_liquidity(ref self: ContractState, amount0: u256, amount1: u256) -> u256 {
            let caller = get_caller_address();
            let this = get_contract_address();
            let (token0, token1): (IERC20Dispatcher, IERC20Dispatcher) = (
                self.token0.read(), self.token1.read()
            );

            token0.transfer_from(caller, this, amount0);
            token1.transfer_from(caller, this, amount1);

            // How much dx, dy to add?
            //
            // xy = k
            // (x + dx)(y + dy) = k'
            //
            // No price change, before and after adding liquidity
            // x / y = (x + dx) / (y + dy)
            //
            // x(y + dy) = y(x + dx)
            // x * dy = y * dx
            //
            // x / y = dx / dy
            // dy = y / x * dx

            let (reserve0, reserve1): (u256, u256) = (self.reserve0.read(), self.reserve1.read());
            if (reserve0 > 0 || reserve1 > 0) {
                assert(reserve0 * amount1 == reserve1 * amount0, 'x / y != dx / dy');
            }

            // How much shares to mint?
            //
            // f(x, y) = value of liquidity
            // We will define f(x, y) = sqrt(xy)
            //
            // L0 = f(x, y)
            // L1 = f(x + dx, y + dy)
            // T = total shares
            // s = shares to mint
            //
            // Total shares should increase proportional to increase in liquidity
            // L1 / L0 = (T + s) / T
            //
            // L1 * T = L0 * (T + s)
            //
            // (L1 - L0) * T / L0 = s

            // Claim
            // (L1 - L0) / L0 = dx / x = dy / y
            //
            // Proof
            // --- Equation 1 ---
            // (L1 - L0) / L0 = (sqrt((x + dx)(y + dy)) - sqrt(xy)) / sqrt(xy)
            //
            // dx / dy = x / y so replace dy = dx * y / x
            //
            // --- Equation 2 ---
            // Equation 1 = (sqrt(xy + 2ydx + dx^2 * y / x) - sqrt(xy)) / sqrt(xy)
            //
            // Multiply by sqrt(x) / sqrt(x)
            // Equation 2 = (sqrt(x^2y + 2xydx + dx^2 * y) - sqrt(x^2y)) / sqrt(x^2y)
            //            = (sqrt(y)(sqrt(x^2 + 2xdx + dx^2) - sqrt(x^2)) / (sqrt(y)sqrt(x^2))
            // sqrt(y) on top and bottom cancels out
            //
            // --- Equation 3 ---
            // Equation 2 = (sqrt(x^2 + 2xdx + dx^2) - sqrt(x^2)) / (sqrt(x^2)
            // = (sqrt((x + dx)^2) - sqrt(x^2)) / sqrt(x^2)
            // = ((x + dx) - x) / x
            // = dx / x
            // Since dx / dy = x / y,
            // dx / x = dy / y
            //
            // Finally
            // (L1 - L0) / L0 = dx / x = dy / y

            let total_supply = self.total_supply.read();
            let shares = if (total_supply == 0) {
                u256_sqrt(amount0 * amount1).into()
            } else {
                PrivateFunctions::min(
                    amount0 * total_supply / reserve0, amount1 * total_supply / reserve1
                )
            };
            assert(shares > 0, 'shares = 0');
            self._mint(caller, shares);

            self._update(self.token0.read().balance_of(this), self.token1.read().balance_of(this));
            shares
        }

        fn remove_liquidity(ref self: ContractState, shares: u256) -> (u256, u256) {
            let caller = get_caller_address();
            let this = get_contract_address();
            let (token0, token1): (IERC20Dispatcher, IERC20Dispatcher) = (
                self.token0.read(), self.token1.read()
            );

            // Claim
            // dx, dy = amount of liquidity to remove
            // dx = s / T * x
            // dy = s / T * y
            //
            // Proof
            // Let's find dx, dy such that
            // v / L = s / T
            //
            // where
            // v = f(dx, dy) = sqrt(dxdy)
            // L = total liquidity = sqrt(xy)
            // s = shares
            // T = total supply
            //
            // --- Equation 1 ---
            // v = s / T * L
            // sqrt(dxdy) = s / T * sqrt(xy)
            //
            // Amount of liquidity to remove must not change price so
            // dx / dy = x / y
            //
            // replace dy = dx * y / x
            // sqrt(dxdy) = sqrt(dx * dx * y / x) = dx * sqrt(y / x)
            //
            // Divide both sides of Equation 1 with sqrt(y / x)
            // dx = s / T * sqrt(xy) / sqrt(y / x)
            // = s / T * sqrt(x^2) = s / T * x
            //
            // Likewise
            // dy = s / T * y

            // bal0 >= reserve0
            // bal1 >= reserve1
            let (bal0, bal1): (u256, u256) = (token0.balance_of(this), token1.balance_of(this));

            let total_supply = self.total_supply.read();
            let (amount0, amount1): (u256, u256) = (
                (shares * bal0) / total_supply, (shares * bal1) / total_supply
            );
            assert(amount0 > 0 && amount1 > 0, 'amount0 or amount1 = 0');

            self._burn(caller, shares);
            self._update(bal0 - amount0, bal1 - amount1);

            token0.transfer(caller, amount0);
            token1.transfer(caller, amount1);
            (amount0, amount1)
        }
    }
}
Last change: 2024-02-15, commit: 89037ca

Writing to any storage slot

On Starknet, a contract's storage is a map with 2^251 slots, where each slot is a felt which is initialized to 0. The address of storage variables is computed at compile time using the formula: storage variable address := pedersen(keccak(variable name), keys). Interactions with storage variables are commonly performed using the self.var.read() and self.var.write() functions.

Nevertheless, we can use the storage_write_syscall and storage_read_syscall syscalls, to write to and read from any storage slot. This is useful when writing to storage variables that are not known at compile time, or to ensure that even if the contract is upgraded and the computation method of storage variable addresses changes, they remain accessible.

In the following example, we use the Poseidon hash function to compute the address of a storage variable. Poseidon is a ZK-friendly hash function that is cheaper and faster than Pedersen, making it an excellent choice for onchain computations. Once the address is computed, we use the storage syscalls to interact with it.

#[starknet::interface]
pub trait IWriteToAnySlots<TContractState> {
    fn write_slot(ref self: TContractState, value: u32);
    fn read_slot(self: @TContractState) -> u32;
}

#[starknet::contract]
pub mod WriteToAnySlot {
    use starknet::syscalls::{storage_read_syscall, storage_write_syscall};
    use starknet::SyscallResultTrait;
    use core::poseidon::poseidon_hash_span;
    use starknet::StorageAddress;

    #[storage]
    struct Storage {}

    const SLOT_NAME: felt252 = 'test_slot';

    #[abi(embed_v0)]
    impl WriteToAnySlot of super::IWriteToAnySlots<ContractState> {
        fn write_slot(ref self: ContractState, value: u32) {
            storage_write_syscall(0, get_address_from_name(SLOT_NAME), value.into())
                .unwrap_syscall();
        }

        fn read_slot(self: @ContractState) -> u32 {
            storage_read_syscall(0, get_address_from_name(SLOT_NAME))
                .unwrap_syscall()
                .try_into()
                .unwrap()
        }
    }
    pub fn get_address_from_name(variable_name: felt252) -> StorageAddress {
        let mut data: Array<felt252> = array![];
        data.append(variable_name);
        let hashed_name: felt252 = poseidon_hash_span(data.span());
        let MASK_250: u256 = 0x03ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
        // By taking the 250 least significant bits of the hash output, we get a valid 250bits storage address.
        let result: felt252 = (hashed_name.into() & MASK_250).try_into().unwrap();
        let result: StorageAddress = result.try_into().unwrap();
        result
    }
}
Last change: 2024-02-15, commit: 89037ca

Storing Arrays

On Starknet, complex values (e.g., tuples or structs), are stored in a continuous segment starting from the address of the storage variable. There is a 256 field elements limitation to the maximal size of a complex storage value, meaning that to store arrays of more than 255 elements in storage, we would need to split it into segments of size n <= 255 and store these segments in multiple storage addresses. There is currently no native support for storing arrays in Cairo, so you will need to write your own implementation of the Store trait for the type of array you wish to store.

However, the ByteArray struct can be used to store Array<bytes31> in storage without additional implementation. Before implementing your own Store trait, consider wether the ByteArray struct can be used to store the data you need! See the ByteArray section for more information.

Note: While storing arrays in storage is possible, it is not always recommended, as the read and write operations can get very costly. For example, reading an array of size n requires n storage reads, and writing to an array of size n requires n storage writes. If you only need to access a single element of the array at a time, it is recommended to use a LegacyMap and store the length in another variable instead.

The following example demonstrates how to write a simple implementation of the StorageAccess trait for the Array<felt252> type, allowing us to store arrays of up to 255 felt252 elements.

impl StoreFelt252Array of Store<Array<felt252>> {
    fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult<Array<felt252>> {
        StoreFelt252Array::read_at_offset(address_domain, base, 0)
    }

    fn write(
        address_domain: u32, base: StorageBaseAddress, value: Array<felt252>
    ) -> SyscallResult<()> {
        StoreFelt252Array::write_at_offset(address_domain, base, 0, value)
    }

    fn read_at_offset(
        address_domain: u32, base: StorageBaseAddress, mut offset: u8
    ) -> SyscallResult<Array<felt252>> {
        let mut arr: Array<felt252> = array![];

        // Read the stored array's length. If the length is superior to 255, the read will fail.
        let len: u8 = Store::<u8>::read_at_offset(address_domain, base, offset)
            .expect('Storage Span too large');
        offset += 1;

        // Sequentially read all stored elements and append them to the array.
        let exit = len + offset;
        loop {
            if offset >= exit {
                break;
            }

            let value = Store::<felt252>::read_at_offset(address_domain, base, offset).unwrap();
            arr.append(value);
            offset += Store::<felt252>::size();
        };

        // Return the array.
        Result::Ok(arr)
    }

    fn write_at_offset(
        address_domain: u32, base: StorageBaseAddress, mut offset: u8, mut value: Array<felt252>
    ) -> SyscallResult<()> {
        // // Store the length of the array in the first storage slot.
        let len: u8 = value.len().try_into().expect('Storage - Span too large');
        Store::<u8>::write_at_offset(address_domain, base, offset, len).unwrap();
        offset += 1;

        // Store the array elements sequentially
        while let Option::Some(element) = value
            .pop_front() {
                Store::<felt252>::write_at_offset(address_domain, base, offset, element).unwrap();
                offset += Store::<felt252>::size();
            };

        Result::Ok(())
    }

    fn size() -> u8 {
        255 * Store::<felt252>::size()
    }
}

You can then import this implementation in your contract and use it to store arrays in storage:

#[starknet::interface]
pub trait IStoreArrayContract<TContractState> {
    fn store_array(ref self: TContractState, arr: Array<felt252>);
    fn read_array(self: @TContractState) -> Array<felt252>;
}

#[starknet::contract]
pub mod StoreArrayContract {
    use super::StoreFelt252Array;

    #[storage]
    struct Storage {
        arr: Array<felt252>
    }

    #[abi(embed_v0)]
    impl StoreArrayImpl of super::IStoreArrayContract<ContractState> {
        fn store_array(ref self: ContractState, arr: Array<felt252>) {
            self.arr.write(arr);
        }

        fn read_array(self: @ContractState) -> Array<felt252> {
            self.arr.read()
        }
    }
}
Last change: 2024-02-16, commit: d569d57

Structs as mapping keys

In order to use structs as mapping keys, you can use #[derive(Hash)] on the struct definition. This will automatically generate a hash function for the struct that can be used to represent the struct as a key in a LegacyMap.

Consider the following example in which we would like to use an object of type Pet as a key in a LegacyMap. The Pet struct has three fields: name, age and owner. We consider that the combination of these three fields uniquely identifies a pet.

#[derive(Copy, Drop, Serde, Hash)]
pub struct Pet {
    pub name: felt252,
    pub age: u8,
    pub owner: felt252,
}

#[starknet::interface]
pub trait IPetRegistry<TContractState> {
    fn register_pet(ref self: TContractState, key: Pet, timestamp: u64);
    fn get_registration_date(self: @TContractState, key: Pet) -> u64;
}

#[starknet::contract]
pub mod PetRegistry {
    use core::hash::{HashStateTrait, Hash};
    use super::Pet;

    #[storage]
    struct Storage {
        registration_time: LegacyMap::<Pet, u64>,
    }

    #[abi(embed_v0)]
    impl PetRegistry of super::IPetRegistry<ContractState> {
        fn register_pet(ref self: ContractState, key: Pet, timestamp: u64) {
            self.registration_time.write(key, timestamp);
        }

        fn get_registration_date(self: @ContractState, key: Pet) -> u64 {
            self.registration_time.read(key)
        }
    }
}
Last change: 2024-02-15, commit: 89037ca

Hashing

Hashing is a cryptographic technique that allows you to transform a variable length input into a fixed length output. The resulting output is called a hash and it's completely different from the input. Hash functions are deterministic, meaning that the same input will always produce the same output.

The two hash functions provided by the Cairo library are Poseidon and Pedersen. Pedersen hashes were used in the past (but still used in some scenario for backward compatibility) while Poseidon hashes are the standard nowadays since they were designed to be very efficient for Zero Knowledge proof systems.

In Cairo it's possible to hash all the types that can be converted to felt252 since they implement natively the Hash trait. It's also possible to hash more complex types like structs by deriving the Hash trait with the attribute #[derive(Hash)] but only if all the struct's fields are themselves hashable.

You first need to initialize a hash state with the new method of the HashStateTrait and then you can update it with the update method. You can accumulate multiple updates. Then, the finalize method returns the final hash value as a felt252.

#[starknet::interface]
pub trait IHashTrait<T> {
    fn save_user_with_poseidon(
        ref self: T, id: felt252, username: felt252, password: felt252
    ) -> felt252;
    fn save_user_with_pedersen(
        ref self: T, id: felt252, username: felt252, password: felt252
    ) -> felt252;
}

#[starknet::contract]
pub mod HashTraits {
    use core::hash::{HashStateTrait, HashStateExTrait};
    use core::{pedersen::PedersenTrait, poseidon::PoseidonTrait};

    #[storage]
    struct Storage {
        user_hash_poseidon: felt252,
        user_hash_pedersen: felt252,
    }

    #[derive(Drop, Hash)]
    struct LoginDetails {
        username: felt252,
        password: felt252,
    }

    #[derive(Drop, Hash)]
    struct UserDetails {
        id: felt252,
        login: LoginDetails,
    }

    #[abi(embed_v0)]
    impl HashTrait of super::IHashTrait<ContractState> {
        fn save_user_with_poseidon(
            ref self: ContractState, id: felt252, username: felt252, password: felt252
        ) -> felt252 {
            let login = LoginDetails { username, password };
            let user = UserDetails { id, login };

            let poseidon_hash = PoseidonTrait::new().update_with(user).finalize();

            self.user_hash_poseidon.write(poseidon_hash);
            poseidon_hash
        }

        fn save_user_with_pedersen(
            ref self: ContractState, id: felt252, username: felt252, password: felt252
        ) -> felt252 {
            let login = LoginDetails { username, password };
            let user = UserDetails { id, login };

            let pedersen_hash = PedersenTrait::new(0).update_with(user).finalize();

            self.user_hash_pedersen.write(pedersen_hash);
            pedersen_hash
        }
    }
}

#[cfg(test)]
mod tests {
    use starknet::SyscallResultTrait;
    use super::{HashTraits, IHashTraitDispatcher, IHashTraitDispatcherTrait};

    use core::hash::{HashStateTrait, HashStateExTrait};
    use core::{pedersen::PedersenTrait, poseidon::PoseidonTrait};
    use starknet::syscalls::deploy_syscall;

    fn deploy() -> IHashTraitDispatcher {
        let mut calldata = array![];
        let (address, _) = deploy_syscall(
            HashTraits::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false
        )
            .unwrap_syscall();
        IHashTraitDispatcher { contract_address: address }
    }


    #[test]
    #[available_gas(20000000)]
    fn test_pedersen_hash() {
        let mut contract = deploy();

        let id = 0x1;
        let username = 'A.stark';
        let password = 'password.stark';
        let test_hash = contract.save_user_with_pedersen(id, username, password);

        assert(
            test_hash == 0x6da4b4d0489989f5483d179643dafb3405b0e3b883a6c8efe5beb824ba9055a,
            'Incorrect hash output'
        );
    }

    #[test]
    #[available_gas(20000000)]
    fn test_poseidon_hash() {
        let mut contract = deploy();

        let id = 0x1;
        let username = 'A.stark';
        let password = 'password.stark';

        let test_hash = contract.save_user_with_poseidon(id, username, password);

        assert(
            test_hash == 0x4d165e1d398ae4864854518d3c58c3d7a21ed9c1f8f3618fbb0031d208aab7b,
            'Incorrect hash output'
        );
    }
}
Last change: 2024-01-05, commit: 28a2599

Optimisations

A collection of optimisation patterns to save gas and steps.

Last change: 2023-10-12, commit: 90aa7c0

Storage optimisation

A smart contract has a limited amount of storage slots. Each slot can store a single felt252 value. Writing to a storage slot has a cost, so we want to use as few storage slots as possible.

In Cairo, every type is derived from the felt252 type, which uses 252 bits to store a value. This design is quite simple, but it does have a drawback: it is not storage efficient. For example, if we want to store a u8 value, we need to use an entire slot, even though we only need 8 bits.

Packing

When storing multiple values, we can use a technique called packing. Packing is a technique that allows us to store multiple values in a single felt value. This is done by using the bits of the felt value to store multiple values.

For example, if we want to store two u8 values, we can use the first 8 bits of the felt value to store the first u8 value, and the last 8 bits to store the second u8 value. This way, we can store two u8 values in a single felt value.

Cairo provides a built-in store using packing that you can use with the StorePacking trait.

trait StorePacking<T, PackedT> {
    fn pack(value: T) -> PackedT;
    fn unpack(value: PackedT) -> T;
}

This allows to store the type T by first packing it into the type PackedT with the pack function, and then storing the PackedT value with it's Store implementation. When reading the value, we first retrieve the PackedT value, and then unpack it into the type T using the unpack function.

Here's an example of storing a Time struct with two u8 values using the StorePacking trait:

#[derive(Copy, Serde, Drop)]
pub struct Time {
    pub hour: u8,
    pub minute: u8
}

#[starknet::interface]
pub trait ITime<TContractState> {
    fn set(ref self: TContractState, value: Time);
    fn get(self: @TContractState) -> Time;
}

#[starknet::contract]
pub mod TimeContract {
    use super::Time;
    use starknet::storage_access::StorePacking;

    #[storage]
    struct Storage {
        time: Time
    }

    impl TimePackable of StorePacking<Time, felt252> {
        fn pack(value: Time) -> felt252 {
            let msb: felt252 = 256 * value.hour.into();
            let lsb: felt252 = value.minute.into();
            return msb + lsb;
        }
        fn unpack(value: felt252) -> Time {
            let value: u16 = value.try_into().unwrap();
            let (q, r) = DivRem::div_rem(value, 256_u16.try_into().unwrap());
            let hour: u8 = Into::<u16, felt252>::into(q).try_into().unwrap();
            let minute: u8 = Into::<u16, felt252>::into(r).try_into().unwrap();
            return Time { hour, minute };
        }
    }

    #[abi(embed_v0)]
    impl TimeContract of super::ITime<ContractState> {
        fn set(ref self: ContractState, value: Time) {
            // This will call the pack method of the TimePackable trait
            // and store the resulting felt252
            self.time.write(value);
        }
        fn get(self: @ContractState) -> Time {
            // This will read the felt252 value from storage
            // and return the result of the unpack method of the TimePackable trait
            return self.time.read();
        }
    }
}
Last change: 2024-02-15, commit: 89037ca

List

By default, there is no list type supported in Cairo, but you can use Alexandria. You can refer to the Alexandria documentation for more details.

What is List?

An ordered sequence of values that can be used in Starknet storage:

#[storage]
struct Storage {
  amounts: List<u128>
}

Interface

trait ListTrait<T> {
  fn len(self: @List<T>) -> u32;
  fn is_empty(self: @List<T>) -> bool;
  fn append(ref self: List<T>, value: T) -> u32;
  fn get(self: @List<T>, index: u32) -> Option<T>;
  fn set(ref self: List<T>, index: u32, value: T);
  fn pop_front(ref self: List<T>) -> Option<T>;
  fn array(self: @List<T>) -> Array<T>;
}

List also implements IndexView so you can use the familiar bracket notation to access its members:

let second = self.amounts.read()[1];

Note that unlike get, using this bracket notation panics when accessing an out of bounds index.

Support for custom types

List supports most of the corelib types out of the box. If you want to store a your own custom type in a List, it has to implement the Store trait. You can have the compiler derive it for you using the #[derive(starknet::Store)] attribute.

Caveats

There are two idiosyncacies you should be aware of when using List

  1. The append operation costs 2 storage writes - one for the value itself and another one for updating the List's length
  2. Due to a compiler limitation, it is not possible to use mutating operations with a single inline statement. For example, self.amounts.read().append(42); will not work. You have to do it in 2 steps:
let mut amounts = self.amounts.read();
amounts.append(42);

Dependencies

Update your project dependencies by in the Scarb.toml file:

[dependencies]
(...)
alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git" }

For example, let's use List to create a contract that tracks a list of amounts and tasks:

#[derive(Copy, Drop, Serde, starknet::Store)]
pub struct Task {
    pub description: felt252,
    pub status: felt252
}

#[starknet::interface]
pub trait IListExample<TContractState> {
    fn add_in_amount(ref self: TContractState, number: u128);
    fn add_in_task(ref self: TContractState, description: felt252, status: felt252);
    fn is_empty_list(self: @TContractState) -> bool;
    fn list_length(self: @TContractState) -> u32;
    fn get_from_index(self: @TContractState, index: u32) -> u128;
    fn set_from_index(ref self: TContractState, index: u32, number: u128);
    fn pop_front_list(ref self: TContractState);
    fn array_conversion(self: @TContractState) -> Array<u128>;
}

#[starknet::contract]
pub mod ListExample {
    use super::Task;
    use alexandria_storage::list::{List, ListTrait};

    #[storage]
    pub struct Storage {
        amount: List<u128>,
        tasks: List<Task>
    }

    #[abi(embed_v0)]
    impl ListExample of super::IListExample<ContractState> {
        fn add_in_amount(ref self: ContractState, number: u128) {
            let mut current_amount_list = self.amount.read();
            current_amount_list.append(number).unwrap();
        }

        fn add_in_task(ref self: ContractState, description: felt252, status: felt252) {
            let new_task = Task { description: description, status: status };
            let mut current_tasks_list = self.tasks.read();
            current_tasks_list.append(new_task).unwrap();
        }

        fn is_empty_list(self: @ContractState) -> bool {
            let mut current_amount_list = self.amount.read();
            current_amount_list.is_empty()
        }

        fn list_length(self: @ContractState) -> u32 {
            let mut current_amount_list = self.amount.read();
            current_amount_list.len()
        }

        fn get_from_index(self: @ContractState, index: u32) -> u128 {
            self.amount.read()[index]
        }

        fn set_from_index(ref self: ContractState, index: u32, number: u128) {
            let mut current_amount_list = self.amount.read();
            current_amount_list.set(index, number).unwrap();
        }

        fn pop_front_list(ref self: ContractState) {
            let mut current_amount_list = self.amount.read();
            current_amount_list.pop_front().unwrap().unwrap();
        }

        fn array_conversion(self: @ContractState) -> Array<u128> {
            let mut current_amount_list = self.amount.read();
            current_amount_list.array().unwrap()
        }
    }
}
Last change: 2024-01-17, commit: 1021875

Plugins

Compilers plugin in Cairo are a way to generate additional code for specific items during the compilation process.

There are already a set of core plugins that you may have already used, such as #[derive], #[cfg], #[generate_trait], etc.

It is possible to create your own plugins in rust. You can learn more about how to do that in the hello-cairo-plugin repository.

Last change: 2024-01-25, commit: 94363d0

ECDSA Verification

This is the Cairo adaptation of the Solidity by example Verifying Signature Messages can be signed off chain and then verified on chain using a smart contract.

// How to Sign and Verify
// # Signing
// 1. Create message to sign
// 2. Hash the message
// 3. Sign the hash (off chain, keep your private key secret)

use core::starknet::eth_address::EthAddress;
use starknet::secp256_trait::{Signature};
#[starknet::interface]
trait IVerifySignature<TContractState> {
    fn get_signature(self: @TContractState, r: u256, s: u256, v: u32,) -> Signature;
    fn verify_eth_signature(
        self: @TContractState, eth_address: EthAddress, msg_hash: u256, r: u256, s: u256, v: u32,
    );
    fn recover_public_key(
        self: @TContractState, eth_address: EthAddress, msg_hash: u256, r: u256, s: u256, v: u32
    );
}

#[starknet::contract]
mod verifySignature {
    use super::IVerifySignature;
    use core::starknet::eth_address::EthAddress;
    use starknet::get_caller_address;
    use starknet::secp256_trait;
    use starknet::secp256k1::{Secp256k1Point};
    use starknet::{SyscallResult, SyscallResultTrait};
    use starknet::secp256_trait::{
        Secp256Trait, Secp256PointTrait, Signature, signature_from_vrs, recover_public_key,
        is_signature_entry_valid
    };
    use core::traits::{TryInto, Into};
    use starknet::eth_signature::{verify_eth_signature, public_key_point_to_eth_address};

    #[storage]
    struct Storage {
        msg_hash: u256,
        signature: Signature,
        eth_address: EthAddress,
    }

    #[abi(embed_v0)]
    impl VerifySignature of IVerifySignature<ContractState> {
        /// This function returns the signature struct for the given parameters.
        ///
        /// # Arguments
        ///
        /// * `r` - The R component of the signature.
        /// * `s` - The S component of the signature.
        /// * `v` - The V component of the signature.
        ///
        /// # Returns
        ///
        /// * `Signature` - The signature struct.
        fn get_signature(self: @ContractState, r: u256, s: u256, v: u32,) -> Signature {
            // Create a Signature object from the given v, r, and s values.
            let signature: Signature = signature_from_vrs(v, r, s);
            signature
        }


        /// Verifies an Ethereum signature.
        ///
        /// # Arguments
        ///
        /// * `eth_address` - The Ethereum address to verify the signature against.
        /// * `msg_hash` - The hash of the message that was signed.
        /// * `r` - The R component of the signature.
        /// * `s` - The S component of the signature.
        /// * `v` - The V component of the signature.
        fn verify_eth_signature(
            self: @ContractState, eth_address: EthAddress, msg_hash: u256, r: u256, s: u256, v: u32
        ) {
            let signature = self.get_signature(r, s, v);
            verify_eth_signature(:msg_hash, :signature, :eth_address);
        }

        /// Recovers the public key from an Ethereum signature and verifies that it matches the given Ethereum address.
        ///
        /// # Arguments
        ///
        /// * `eth_address` - The Ethereum address to verify the signature against.
        /// * `msg_hash` - The hash of the message that was signed.
        /// * `r` - The R component of the signature.
        /// * `s` - The S component of the signature.
        /// * `v` - The V component of the signature.
        fn recover_public_key(
            self: @ContractState, eth_address: EthAddress, msg_hash: u256, r: u256, s: u256, v: u32
        ) {
            let signature = self.get_signature(r, s, v);
            let public_key_point = recover_public_key::<Secp256k1Point>(msg_hash, signature)
                .unwrap();
            let calculated_eth_address = public_key_point_to_eth_address(:public_key_point);
            assert(calculated_eth_address == eth_address, 'Invalid Address');
        }
    }
}

Click here to interact with the deployed contract on Voyager

Last change: 2024-02-21, commit: b2c8e07