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 uses 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 contain 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 example 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 this book uses:

cairo 2.6.3
edition = '2023_11'
scarb 2.6.5
starknet-foundry 0.25.0
Last change: 2024-06-03, commit: bc223b4

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: 2024-06-09, commit: 3fbfb60

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-06-09, commit: 3fbfb60

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-06-09, commit: 3fbfb60

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-06-09, commit: 3fbfb60

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 implementation 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 an 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 hand, 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 `#[abi(embed_v0)]`.
        // 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 `#[abi(embed_v0)]`.
        // However, it can't modify the contract's state, as it 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 `#[abi(embed_v0)]` 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, as it 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-06-09, commit: 3fbfb60

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-06-09, commit: 3fbfb60

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 \( \bmod {2^{251}} - 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-06-09, commit: 3fbfb60

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 halts the execution with the given error value. It should be used for complex condition checks and for internal errors. It's similar to the revert statement in Solidity. You can use panic_with_felt252 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 contracts, 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-06-09, commit: 3fbfb60

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 that emits 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 at least the `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-06-09, commit: 3fbfb60

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, and 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 chapter.

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 at least the `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(Copy, Drop, Debug, PartialEq, starknet::Event)]
    pub enum Event {
        Upgraded: Upgraded,
    }

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

    #[abi(embed_v0)]
    impl UpgradeableContract of super::IUpgradeableContract<ContractState> {
        fn upgrade(ref self: ContractState, impl_hash: ClassHash) {
            assert(impl_hash.is_non_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. Note that you can explicitly use the new class code in the same transaction by calling call_contract after the replace_class syscall.

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-06-09, commit: 3fbfb60

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 bytes31.

ByteArray (Long strings)

The ByteArray struct is used to store strings of arbitrary length. It contains 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-06-09, commit: 3fbfb60

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. It is enough to just derive this trait, unless our custom type 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-06-09, commit: 3fbfb60

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.

#[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 as a 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);
    }
}

Note: The purpose of this example is to demonstrate the ability to use custom types as inputs and outputs in contract calls. For simplicity, we are not using getters and setters to manage the contract's state.

Last change: 2024-06-09, commit: 3fbfb60

Documentation

It's important to take the time to document your code. It will help 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 contract logic, 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: 2024-06-09, commit: 3fbfb60

Deploy and interact with contracts

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

Last change: 2024-06-09, commit: 3fbfb60

Declaring and Deploying Your Contract

We will use Starkli to declare and deploy a smart contract on Starknet. Make sure that Starkli is installed on your device. You can check out the starkli book for more information.

We will need an account, so first we will create one. If you already have one, you can skip this step and move directly to the part where we declare our contract.

Creating a new account:

You should move to the directory where you want to access your account keystores, and then create a new folder for the wallet.

$ mkdir ./starkli-wallet

Create a new signer. You will be instructed to enter a password to encrypt your private key:

$ starkli signer keystore new ./starkli-wallet/keystore.json

After this command, the path of the encrypted keystore file is shown which will be needed during the declaration and deployment of the contract.

Export the keystore path in order not to call --keystore in every command:

$ export STARKNET_KEYSTORE="./starkli-wallet/keystore.json"

Initialize the account with the following command using OpenZeppelin's class deployed on Starknet.

$ starkli account oz init ./starkli-wallet/account.json

After this command, the address of the account is shown once it is deployed along with the deploy command. Deploy the account:

$ starkli account deploy ./starkli-wallet/account.json

This command wants you to fund the address (given in the instructions below the command) in order to deploy the account on the Starknet Sepolia Testnet. We need Starknet Sepolia testnet ethers which could be obtained from this faucet.

Once the transaction is confirmed on the faucet page, press ENTER, and the account will be deployed on Starknet Sepolia! Try to find your account on Voyager Sepolia!

Declaring & Deploying your Contract:

Firstly, you need to declare your contract which will create a class on Starknet Sepolia. Note that we will use the Sierra program in ./target/ProjectName_ContractName.contract_class.json in your Scarb folder.

If you are deploying a contract code that is already used, you can skip the declaration step because the class hash is already declared on the network. One example of this is when you are deploying common contract instances such as ERC20 or ERC721 contracts.

Note: The command below is written to run in the directory of the Scarb folder.

$ starkli declare \
  --keystore /path/to/starkli-wallet/keystore.json \
  --account /path/to/starkli-wallet/account.json \
  --watch ./target/dev/simple_storage_SimpleStorage.contract_class.json

After this command, the class hash for your contract is declared. You should be able to find the hash under the command:

Class hash declared:
0x05c8c21062a74e3c8f2015311d7431e820a08a6b0a9571422b607429112d2eb4

Check the Voyager Class Page. Now, it's time to deploy the contract. Add the clash hash given above after --watch:

$ starkli deploy \
  --keystore /path/to/starkli-wallet/keystore.json \
  --account /path/to/starkli-wallet/account.json \
  --watch 0x05c8c21062a74e3c8f2015311d7431e820a08a6b0a9571422b607429112d2eb4

You should now see the address of the deployed contract. Congratulations, you have deployed your contract on Starknet Sepolia Testnet! Check the Voyager Contract Page to see your contract! Additionally, you can also find all contract instances of a given class on the Voyager Class Page as well, for example, this page.

Last change: 2024-06-25, commit: 0d9f473

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 an L1 handler.

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

  • Explicitly, by defining a trait annotated 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-06-09, commit: 3fbfb60

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 chapter.

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 uses the Callee dispatcher 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-06-09, commit: 3fbfb60

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 has 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 deploys 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.

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

Last change: 2024-06-09, commit: 3fbfb60

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 assertion macros: assert_eq!, assert_ne!, assert_gt!, assert_ge!, assert_lt!, assert_le!

If you haven't noticed yet, every example in this book has hidden tests, you can see them by clicking 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.

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 changes 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 changes 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 at least the `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 find 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 and parallel test 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-06-09, commit: 3fbfb60

Cairo Cheatsheet

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

Last change: 2024-06-09, commit: 3fbfb60

Felt

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-06-09, commit: 3fbfb60

LegacyMap

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: 2024-06-09, commit: 3fbfb60

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, and false if it isn't.
    arr.is_empty()
}
Last change: 2024-06-09, commit: 3fbfb60

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-06-09, commit: 3fbfb60

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

Last change: 2024-06-09, commit: 3fbfb60

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-06-09, commit: 3fbfb60

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

Last change: 2024-06-09, commit: 3fbfb60

Enums

Just like other programming languages, enums (enumerations) are used in cairo to define variables that can only hold a set of predefined variants (= enum options), enhancing code readability and safety. They facilitate strong type checking and are ideal for organizing related options and supporting structured logic through pattern matching for example, which is also described in the next chapter.

In cairo, enum variants can hold different data types (the unit type, structs, other enums, tuples, default core library types, arrays, dictionaries, ...), as shown in the code snippet below. Furthermore, as a quick reminder, enums are expressions, meaning they can return values.

#[derive(Drop, Serde, Copy, starknet::Store)]
struct Position {
    x: u32,
    y: u32,
}

#[derive(Drop, Serde, Copy, starknet::Store)]
enum UserCommand {
    Login,
    UpdateProfile,
    Logout,
}

#[derive(Drop, Serde, Copy, starknet::Store)]
enum Action {
    Quit,
    Move: Position,
    SendMessage: felt252,
    ChangeAvatarColor: (u8, u8, u8),
    ProfileState: UserCommand
}

Enums can be declared both inside and outside a contract. If declared outside, they need to be imported inside using the use keyword, just like other imports.

  1. Storing enums in contract

    • It is possible to store enums in the contract storage. But unlike most of the core library types which implement the Store trait, enums are custom types and therefore do not automatically implement the Store trait. The enum as well as all of its variants have to explicitly implement the Store trait in order for it to be stored inside a contract storage.

    • If all of its variants implement the Store trait, implementing the Store trait on the enum is as simple as deriving it, using #[derive(starknet::Store)] (as shown in example above). If not, the Store trait has to be manually implemented -- see an example of manually implementing the Store trait for a complex type in chapter Storing Arrays.

  2. Enums as parameters and return values to entrypoints

    • It is possible to pass enums to contract entrypoints as parameters, as well as return them from entrypoints. For that purpose, the enum needs to be serializable and dropable, hence the derivation of traits Serde and Drop in the above code snippet.

Here is an example of a contract illustrating the above statements :

#[starknet::interface]
trait IEnumContract<TContractState> {
    fn register_action(ref self: TContractState, action: Action);
    fn generate_default_actions_list(self: @TContractState) -> Array<Action>;
}

#[starknet::contract]
mod EnumContract {
    use core::clone::Clone;
    use core::traits::Into;
    use super::IEnumContract;
    use super::{Action, Position, UserCommand};

    #[storage]
    struct Storage {
        most_recent_action: Action,
    }

    #[abi(embed_v0)]
    impl IEnumContractImpl of IEnumContract<ContractState> {
        fn register_action(ref self: ContractState, action: Action) {
            // quick note: match takes ownership of variable (but enum Action implements Copy trait)
            match action {
                Action::Quit => { println!("Quit"); },
                Action::Move(value) => { println!("Move with x: {} and y: {}", value.x, value.y); },
                Action::SendMessage(msg) => { println!("Write with message: {}", msg); },
                Action::ChangeAvatarColor((
                    r, g, b
                )) => { println!("Change color to r: {}, g: {}, b: {}", r, g, b); },
                Action::ProfileState(state) => {
                    let profile_state = match state {
                        UserCommand::Login => 1,
                        UserCommand::UpdateProfile => 2,
                        UserCommand::Logout => 3,
                    };
                    println!("profile_state: {}", profile_state);
                }
            };

            self.most_recent_action.write(action);
        }

        fn generate_default_actions_list(self: @ContractState) -> Array<Action> {
            let actions = array![
                Action::Quit,
                Action::Move(Position { x: 1, y: 2 }),
                Action::SendMessage('here is my message'),
                Action::ChangeAvatarColor((1, 2, 3)),
                Action::ProfileState(UserCommand::Login),
            ];

            actions
        }
    }
}
Last change: 2024-06-09, commit: 3fbfb60

Match

The match expression in Cairo allows us to control the flow of our code by comparing a felt252 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: 2024-06-09, commit: 3fbfb60

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: 2024-06-09, commit: 3fbfb60

Struct

A struct is a data type similar to a 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: 2024-06-09, commit: 3fbfb60

Type casting

Cairo supports the conversion from one scalar type to another by using the into and try_into methods. The into method is used for conversion from a smaller data type to a larger data type, while try_into 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: usize is smaller than felt252, so we use try_into
    let _new_usize: usize = my_felt252.try_into().unwrap();
Last change: 2024-06-09, commit: 3fbfb60

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, Debug, PartialEq, starknet::Event)]
    pub struct SwitchEvent {}

    #[event]
    #[derive(Drop, Debug, PartialEq, 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 is really similar to a contract and can also have:

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

It doesn't have a constructor, but you can create an _init internal function and call it from the contract's constructor. In the previous example, the _off function will be 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 implementations of specific interfaces or features (Ownable, Upgradeable, ... ~able). This is why we called the component in the above example Switchable, and not Switch; the contract is switchable, it does not have 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 super::switchable_component;

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

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

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

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

    // You can optionally use the internal implementation of the component as well
    impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl<ContractState>;

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

How to test a component

In order to effectively test a component, you need to test it in the context of a contract. A common practice is to declare a Mock contract that has the only purpose of testing the component.

To test the Switchable component, we can use the previous SwitchableContract:

#[cfg(test)]
mod test {
    use super::SwitchContract; // Used as a mock contract
    use super::switchable_component::{Event, SwitchEvent};
    use super::{ISwitchableDispatcher, ISwitchableDispatcherTrait};
    use starknet::{syscalls::deploy_syscall, contract_address_const, ContractAddress};
    use starknet::SyscallResultTrait;

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

    #[test]
    fn test_constructor() {
        let (switchable, _) = deploy();
        assert_eq!(switchable.is_on(), false);
    }

    #[test]
    fn test_switch() {
        let (switchable, contract_address) = deploy();
        switchable.switch();
        assert_eq!(switchable.is_on(), true);
        assert_eq!(
            starknet::testing::pop_log(contract_address),
            Option::Some(SwitchContract::Event::SwitchableEvent(SwitchEvent {}.into()))
        );
    }

    #[test]
    fn test_multiple_switches() {
        let (switchable, _) = deploy();
        switchable.switch();
        assert_eq!(switchable.is_on(), true);
        switchable.switch();
        assert_eq!(switchable.is_on(), false);
        switchable.switch();
        assert_eq!(switchable.is_on(), true);
    }
}

Deep dive into components

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

Last change: 2024-07-01, commit: 6f4d055

Component 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);
        }
    }
}

#[starknet::contract]
mod CountableContract {
    use super::countable_component;

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

    #[storage]
    struct Storage {
        #[substorage(v0)]
        countable: countable_component::Storage,
    }

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

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


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

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

    #[test]
    fn test_constructor() {
        let counter = deploy_countable();
        assert_eq!(counter.get(), 0);
    }

    #[test]
    fn test_increment() {
        let counter = deploy_countable();
        counter.increment();
        assert_eq!(counter.get(), 1);
    }

    #[test]
    fn test_multiple_increments() {
        let counter = deploy_countable();
        counter.increment();
        counter.increment();
        counter.increment();
        assert_eq!(counter.get(), 3);
    }
}

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 it. But we don't want to add this switch logic to the Countable component itself. Instead, we add the trait Switchable as a dependency to the Countable component.

Implementation of the trait in the contract

First, we import the ISwitchable trait defined in chapter "Components How-To":

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);
            }
        }
    }
}


#[starknet::contract]
mod MockContract {
    use super::countable_component;
    use components::switchable::ISwitchable;

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

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

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

    #[abi(embed_v0)]
    impl CountableImpl = countable_component::Countable<ContractState>;
    #[abi(embed_v0)]
    impl Switchable of ISwitchable<ContractState> {
        fn switch(ref self: ContractState) {}

        fn is_on(self: @ContractState) -> bool {
            true
        }
    }
}


#[cfg(test)]
mod test {
    use super::MockContract;
    use components::countable::{ICountableDispatcher, ICountableDispatcherTrait};
    use starknet::syscalls::deploy_syscall;
    use starknet::SyscallResultTrait;

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

    #[test]
    fn test_get() {
        let countable = deploy_countable();
        assert_eq!(countable.get(), 0);
    }

    #[test]
    fn test_increment() {
        let countable = deploy_countable();
        countable.increment();
        assert_eq!(countable.get(), 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 provides an implementation of the ISwitchable trait. By using the Switchable component in a contract, we can 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 component's 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 CountableContract contract remains the same as in the previous example, only the implementation of the Countable component is different.

Last change: 2024-07-01, commit: 6f4d055

Component-Contract Storage Collisions

Components can declare their own storage variables.

When a contract uses 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 and tests):

#[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()
        }
    }
}

#[cfg(test)]
mod switch_collision_tests {
    use components::switchable::switchable_component::SwitchableInternalTrait;
    use components::switchable::{ISwitchable, ISwitchableDispatcher, ISwitchableDispatcherTrait};
    use super::{
        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]
    fn test_collision() {
        let (mut contract, mut contract_iswitch) = deploy();

        assert_eq!(contract.get(), false);
        assert_eq!(contract_iswitch.is_on(), false);

        contract_iswitch.switch();
        assert_eq!(contract_iswitch.is_on(), true);
        assert_eq!(contract.get(), true);

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

        contract.set(false);
        assert_eq!(contract.get(), contract_iswitch.is_on());
    }
}

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

    fn test_collision() {
        let (mut contract, mut contract_iswitch) = deploy();

        assert_eq!(contract.get(), false);
        assert_eq!(contract_iswitch.is_on(), false);

        contract_iswitch.switch();
        assert_eq!(contract_iswitch.is_on(), true);
        assert_eq!(contract.get(), true);

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

        contract.set(false);
        assert_eq!(contract.get(), contract_iswitch.is_on());
    }
Last change: 2024-07-01, commit: 6f4d055

Ownable

The following Ownable component is a simple component that allows the contract to set an owner and provides an _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);
}

pub 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 super::Errors;
    use starknet::{ContractAddress, get_caller_address};
    use core::num::traits::Zero;

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

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

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

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

    #[embeddable_as(Ownable)]
    pub 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_non_zero(), Errors::ZERO_ADDRESS_CALLER);
            assert(caller == self.ownable_owner.read(), Errors::UNAUTHORIZED);
        }

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

        fn _transfer_ownership(ref self: ComponentState<TContractState>, new: ContractAddress) {
            assert(new.is_non_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:

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);
}

pub 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 super::Errors;
    use starknet::{ContractAddress, get_caller_address};
    use core::num::traits::Zero;

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

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

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

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

    #[embeddable_as(Ownable)]
    pub 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_non_zero(), Errors::ZERO_ADDRESS_CALLER);
            assert(caller == self.ownable_owner.read(), Errors::UNAUTHORIZED);
        }

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

        fn _transfer_ownership(ref self: ComponentState<TContractState>, new: ContractAddress) {
            assert(new.is_non_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 }));
        }
    }
}

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

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

    #[abi(embed_v0)]
    impl OwnableImpl = ownable_component::Ownable<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, Debug, PartialEq, starknet::Event)]
    pub enum Event {
        OwnableEvent: ownable_component::Event,
    }
}

#[cfg(test)]
mod test {
    use super::OwnedContract;
    use super::ownable_component::{Event, OwnershipRenouncedEvent, OwnershipTransferredEvent};
    use super::{IOwnableDispatcher, IOwnableDispatcherTrait};
    use super::Errors;
    use starknet::ContractAddress;
    use starknet::{syscalls::deploy_syscall, SyscallResultTrait, contract_address_const};
    use starknet::testing::{set_caller_address, set_contract_address};
    use core::traits::TryInto;
    use core::num::traits::Zero;

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

        (IOwnableDispatcher { contract_address }, contract_address)
    }

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

        assert_eq!(ownable.owner(), owner);
    }

    #[test]
    fn test_transfer_ownership() {
        let contract_address = contract_address_const::<'owner'>();
        set_contract_address(contract_address);
        let (ownable, address) = deploy();
        let new_owner = contract_address_const::<'new_owner'>();

        ownable.transfer_ownership(new_owner);
        assert_eq!(ownable.owner(), new_owner);
        assert_eq!(
            starknet::testing::pop_log(address),
            Option::Some(
                OwnedContract::Event::OwnableEvent(
                    OwnershipTransferredEvent { previous: contract_address, new: new_owner }.into()
                )
            )
        );
    }

    #[test]
    #[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]
    #[should_panic]
    fn test_transfer_ownership_zero_error() {
        set_contract_address(contract_address_const::<'initial'>());
        let (ownable, _) = deploy();

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

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

        ownable.renounce_ownership();
        assert_eq!(ownable.owner(), Zero::zero());
        assert_eq!(
            starknet::testing::pop_log(address),
            Option::Some(
                OwnedContract::Event::OwnableEvent(
                    OwnershipRenouncedEvent { previous: contract_address }.into()
                )
            )
        );
    }

    #[test]
    #[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]
    #[should_panic]
    fn test_renounce_ownership_previous_owner() {
        set_contract_address(contract_address_const::<'owner'>());
        let (ownable, _) = deploy();

        ownable.renounce_ownership();
        ownable.transfer_ownership(contract_address_const::<'new_owner'>());
    }
}
Last change: 2024-07-01, commit: 6f4d055

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(Copy, Drop, Debug, PartialEq, starknet::Event)]
    pub enum Event {
        Upgraded: Upgraded,
    }

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

    #[abi(embed_v0)]
    impl UpgradeableContract of super::IUpgradeableContract<ContractState> {
        fn upgrade(ref self: ContractState, impl_hash: ClassHash) {
            assert(impl_hash.is_non_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_non_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-06-09, commit: 3fbfb60

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 withdraws both the yield and the initial amount of tokens 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 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
    );
}

#[starknet::interface]
pub trait ISimpleVault<TContractState> {
    fn deposit(ref self: TContractState, amount: u256);
    fn withdraw(ref self: TContractState, shares: u256);
    fn user_balance_of(ref self: TContractState, account: ContractAddress) -> u256;
    fn contract_total_supply(ref self: TContractState) -> 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 user_balance_of(ref self: ContractState, account: ContractAddress) -> u256 {
            self.balance_of.read(account)
        }

        fn contract_total_supply(ref self: ContractState) -> u256 {
            self.total_supply.read()
        }


        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: u256 = self.token.read().balance_of(this).try_into()
                .unwrap();
                shares = (amount * self.total_supply.read()) / balance;
            }
            
           PrivateFunctions::_mint(ref self, caller, shares);
           
            let amount_felt252: felt252 = amount.low.into();
            self.token.read().transfer_from(caller, this, amount_felt252);
        }

        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.user_balance_of(this);
            let amount = (shares * balance) / self.total_supply.read();
            PrivateFunctions::_burn(ref self, caller, shares);
            let amount_felt252: felt252 = amount.low.into();
            self.token.read().transfer(caller, amount_felt252);
        }
    }
}
// ANCHOR_END: simple_vault

#[cfg(test)]
mod tests {
    use core::traits::Into;
    use super::{
        SimpleVault, ISimpleVaultDispatcher, ISimpleVaultDispatcherTrait, IERC20Dispatcher,
        IERC20DispatcherTrait
    };

    // use erc20::token::IERC20;
    use erc20::token::{IERC20DispatcherTrait as IERC20DispatcherTrait_token,
        IERC20Dispatcher as IERC20Dispatcher_token
    };

    use core::num::traits::Zero;

    use starknet::testing::{set_contract_address, set_account_contract_address};
    use starknet::{
        ContractAddress, SyscallResultTrait, syscalls::deploy_syscall, get_caller_address,
        contract_address_const
    };

    const token_name: felt252 = 'myToken';
    const decimals: u8 = 18;
    const initial_supply: felt252 = 100000;
    const symbols: felt252 = 'mtk';

    fn deploy() -> (ISimpleVaultDispatcher, ContractAddress, IERC20Dispatcher_token) {
        let _token_address: ContractAddress = contract_address_const::<'token_address'>();
        let caller = contract_address_const::<'caller'>();

        let (token_contract_address, _) = deploy_syscall(
            erc20::token::erc20::TEST_CLASS_HASH.try_into().unwrap(),
            caller.into(),
            array![caller.into(), 'myToken', '8', '1000'.into(), 'MYT'].span(),
            false
        )
            .unwrap_syscall();

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

        (
            ISimpleVaultDispatcher { contract_address },
            contract_address,
            IERC20Dispatcher_token { contract_address: token_contract_address }
        )
    }

    #[test]
    fn test_deposit() {
        let caller = contract_address_const::<'caller'>();
        let (dispatcher, vault_address, token_dispatcher) = deploy();

        // Approve the vault to transfer tokens on behalf of the caller
        let amount: felt252 = 10.into();
        token_dispatcher.approve(vault_address.into(), amount);
        set_contract_address(caller);

        // Deposit tokens into the vault
        let amount: u256 = 10.into();
        let _deposit = dispatcher.deposit(amount);
        println!("deposit :{:?}", _deposit);

        // Check balances and total supply
        let balance_of_caller = dispatcher.user_balance_of(caller);
        let total_supply = dispatcher.contract_total_supply();

        assert(balance_of_caller == amount, 'Deposit failed');
        assert(total_supply == amount, 'total supply mismatch');

    }

    #[test]
    fn test_deposit_withdraw() {
        let caller = contract_address_const::<'caller'>();
        let (dispatcher, vault_address, token_dispatcher) = deploy();

        // Approve the vault to transfer tokens on behalf of the caller
        let amount: felt252 = 10.into();
        token_dispatcher.approve(vault_address.into(), amount);
        set_contract_address(caller);
        set_account_contract_address(vault_address);

        // Deposit tokens into the vault
        let amount: u256 = 10.into();
        dispatcher.deposit(amount);
        dispatcher.withdraw(amount);

        // Check balances of user in the vault after withdraw
        let balance_of_caller = dispatcher.user_balance_of(caller);

        assert(balance_of_caller == 0.into(), 'withdraw failed');
    }
   
    
}
Last change: 2024-06-09, commit: 3fbfb60

ERC20 Token

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

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

#[starknet::interface]
pub 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]
pub 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(Copy, Drop, Debug, PartialEq, starknet::Event)]
    pub enum Event {
        Transfer: Transfer,
        Approval: Approval,
    }
    #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
    pub struct Transfer {
        pub from: ContractAddress,
        pub to: ContractAddress,
        pub value: felt252,
    }
    #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
    pub struct Approval {
        pub owner: ContractAddress,
        pub spender: ContractAddress,
        pub 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_non_zero(), Errors::TRANSFER_FROM_ZERO);
            assert(recipient.is_non_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_non_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_non_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-06-09, commit: 3fbfb60

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 many 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-06-09, commit: 3fbfb60

TimeLock

This is the Cairo adaptation of the Solidity by example TimeLock.

use starknet::ContractAddress;
use starknet::account::Call;

#[starknet::interface]
pub trait ITimeLock<TState> {
    fn get_tx_id(self: @TState, call: Call, timestamp: u64) -> felt252;
    fn queue(ref self: TState, call: Call, timestamp: u64) -> felt252;
    fn execute(ref self: TState, call: Call, timestamp: u64) -> Span<felt252>;
    fn cancel(ref self: TState, tx_id: felt252);
}

#[starknet::contract]
pub mod TimeLock {
    use core::poseidon::{PoseidonTrait, poseidon_hash_span};
    use core::hash::{HashStateTrait, HashStateExTrait};
    use starknet::{
        ContractAddress, get_caller_address, get_block_timestamp, SyscallResultTrait, syscalls
    };
    use starknet::account::Call;
    use components::ownable::ownable_component;

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

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

    #[storage]
    struct Storage {
        #[substorage(v0)]
        ownable: ownable_component::Storage,
        queued: LegacyMap::<felt252, bool>,
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    pub enum Event {
        #[flat]
        OwnableEvent: ownable_component::Event,
        Queue: Queue,
        Execute: Execute,
        Cancel: Cancel
    }

    #[derive(Drop, starknet::Event)]
    pub struct Queue {
        #[key]
        pub tx_id: felt252,
        pub call: Call,
        pub timestamp: u64
    }

    #[derive(Drop, starknet::Event)]
    pub struct Execute {
        #[key]
        pub tx_id: felt252,
        pub call: Call,
        pub timestamp: u64
    }

    #[derive(Drop, starknet::Event)]
    pub struct Cancel {
        #[key]
        pub tx_id: felt252
    }

    pub const MIN_DELAY: u64 = 10; // seconds
    pub const MAX_DELAY: u64 = 1000; // seconds
    pub const GRACE_PERIOD: u64 = 1000; // seconds

    pub mod Errors {
        pub const ALREADY_QUEUED: felt252 = 'TimeLock: already queued';
        pub const TIMESTAMP_NOT_IN_RANGE: felt252 = 'TimeLock: timestamp range';
        pub const NOT_QUEUED: felt252 = 'TimeLock: not queued';
        pub const TIMESTAMP_NOT_PASSED: felt252 = 'TimeLock: timestamp not passed';
        pub const TIMESTAMP_EXPIRED: felt252 = 'TimeLock: timestamp expired';
    }

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

    #[abi(embed_v0)]
    impl TimeLockImpl of super::ITimeLock<ContractState> {
        fn get_tx_id(self: @ContractState, call: Call, timestamp: u64) -> felt252 {
            PoseidonTrait::new()
                .update(call.to.into())
                .update(call.selector.into())
                .update(poseidon_hash_span(call.calldata))
                .update(timestamp.into())
                .finalize()
        }

        fn queue(ref self: ContractState, call: Call, timestamp: u64) -> felt252 {
            self.ownable._assert_only_owner();

            let tx_id = self.get_tx_id(self._copy_call(@call), timestamp);
            assert(!self.queued.read(tx_id), Errors::ALREADY_QUEUED);
            // ---|------------|---------------|-------
            //  block    block + min     block + max
            let block_timestamp = get_block_timestamp();
            assert(
                timestamp >= block_timestamp
                    + MIN_DELAY && timestamp <= block_timestamp
                    + MAX_DELAY,
                Errors::TIMESTAMP_NOT_IN_RANGE
            );

            self.queued.write(tx_id, true);
            self.emit(Queue { tx_id, call: self._copy_call(@call), timestamp });

            tx_id
        }

        fn execute(ref self: ContractState, call: Call, timestamp: u64) -> Span<felt252> {
            self.ownable._assert_only_owner();

            let tx_id = self.get_tx_id(self._copy_call(@call), timestamp);
            assert(self.queued.read(tx_id), Errors::NOT_QUEUED);
            // ----|-------------------|-------
            //  timestamp    timestamp + grace period
            let block_timestamp = get_block_timestamp();
            assert(block_timestamp >= timestamp, Errors::TIMESTAMP_NOT_PASSED);
            assert(block_timestamp <= timestamp + GRACE_PERIOD, Errors::TIMESTAMP_EXPIRED);

            self.queued.write(tx_id, false);

            let result = syscalls::call_contract_syscall(call.to, call.selector, call.calldata)
                .unwrap_syscall();

            self.emit(Execute { tx_id, call: self._copy_call(@call), timestamp });

            result
        }

        fn cancel(ref self: ContractState, tx_id: felt252) {
            self.ownable._assert_only_owner();

            assert(self.queued.read(tx_id), Errors::NOT_QUEUED);

            self.queued.write(tx_id, false);

            self.emit(Cancel { tx_id });
        }
    }

    #[generate_trait]
    impl InternalImpl of InternalTrait {
        fn _copy_call(self: @ContractState, call: @Call) -> Call {
            Call { to: *call.to, selector: *call.selector, calldata: *call.calldata }
        }
    }
}
Last change: 2024-06-09, commit: 3fbfb60

Staking contract

The following staking contract is designed to allow users to stake tokens in exchange for reward tokens over a specified duration. Here's a quick summary of how it operates and what functionalities it supports:

Key Features:

  1. Token staking and unstaking:

    • Users can stake an ERC20 token, specified at deployment.
    • Users can withdraw their staked tokens at any time.
  2. Reward calculation and distribution:

    • The rewards are distributed as an ERC20, also specified at deployment (can be different from the staking token).
    • Rewards are calculated based on the duration of staking and the amount the user staked relative to the total staked amount by all users.
    • A user’s reward accumulates over time up until the reward period's end and can be claimed anytime by the user.
  3. Dynamic reward rates:

    • The reward rate is determined by the total amount of reward tokens over a set period (duration).
    • The reward rate can be adjusted during the rewards period if new rewards are added before the current reward period finishes.
    • Even after a reward period finishes, a new reward duration and new rewards can be set up if desired.
  4. Ownership and administration:

    • Only the owner of the contract can set the rewards amount and duration.

The reward mechanism ensures that rewards are distributed fairly based on the amount and duration of tokens staked by each user.

The following implementation is the Cairo adaptation of the Solidity by Example - Staking Rewards contract. It includes a small adaptation to keep track of the amount of total distributed reward tokens and emit an event when the remaining reward token amount reaches 0.

use starknet::ContractAddress;

#[starknet::interface]
pub trait IStakingContract<TContractState> {
    fn set_reward_amount(ref self: TContractState, amount: u256);
    fn set_reward_duration(ref self: TContractState, duration: u256);
    fn stake(ref self: TContractState, amount: u256);
    fn withdraw(ref self: TContractState, amount: u256);
    fn get_rewards(self: @TContractState, account: ContractAddress) -> u256;
    fn claim_rewards(ref self: TContractState);
}

mod Errors {
    pub const NULL_REWARDS: felt252 = 'Reward amount must be > 0';
    pub const NOT_ENOUGH_REWARDS: felt252 = 'Reward amount must be > balance';
    pub const NULL_AMOUNT: felt252 = 'Amount must be > 0';
    pub const NULL_DURATION: felt252 = 'Duration must be > 0';
    pub const UNFINISHED_DURATION: felt252 = 'Reward duration not finished';
    pub const NOT_OWNER: felt252 = 'Caller is not the owner';
    pub const NOT_ENOUGH_BALANCE: felt252 = 'Balance too low';
}

#[starknet::contract]
pub mod StakingContract {
    use core::starknet::event::EventEmitter;
    use core::traits::Into;
    use core::num::traits::Zero;
    use starknet::{ContractAddress, get_caller_address, get_block_timestamp, get_contract_address};
    use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};

    #[storage]
    struct Storage {
        staking_token: IERC20Dispatcher,
        reward_token: IERC20Dispatcher,
        owner: ContractAddress,
        reward_rate: u256,
        duration: u256,
        current_reward_per_staked_token: u256,
        finish_at: u256,
        // last time an operation (staking / withdrawal / rewards claimed) was registered
        last_updated_at: u256,
        last_user_reward_per_staked_token: LegacyMap::<ContractAddress, u256>,
        unclaimed_rewards: LegacyMap::<ContractAddress, u256>,
        total_distributed_rewards: u256,
        // total amount of staked tokens
        total_supply: u256,
        // amount of staked tokens per user
        balance_of: LegacyMap::<ContractAddress, u256>,
    }

    #[event]
    #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
    pub enum Event {
        Deposit: Deposit,
        Withdrawal: Withdrawal,
        RewardsFinished: RewardsFinished,
    }

    #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
    pub struct Deposit {
        pub user: ContractAddress,
        pub amount: u256,
    }

    #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
    pub struct Withdrawal {
        pub user: ContractAddress,
        pub amount: u256,
    }

    #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
    pub struct RewardsFinished {
        pub msg: felt252,
    }

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

        self.owner.write(get_caller_address());
    }

    #[abi(embed_v0)]
    impl StakingContract of super::IStakingContract<ContractState> {
        fn set_reward_duration(ref self: ContractState, duration: u256) {
            self.only_owner();

            assert(duration > 0, super::Errors::NULL_DURATION);

            // can only set duration if the previous duration has already finished
            assert(
                self.finish_at.read() < get_block_timestamp().into(),
                super::Errors::UNFINISHED_DURATION
            );

            self.duration.write(duration);
        }

        fn set_reward_amount(ref self: ContractState, amount: u256) {
            self.only_owner();
            self.update_rewards(Zero::zero());

            assert(amount > 0, super::Errors::NULL_REWARDS);
            assert(self.duration.read() > 0, super::Errors::NULL_DURATION);

            let block_timestamp: u256 = get_block_timestamp().into();

            let rate = if self.finish_at.read() < block_timestamp {
                amount / self.duration.read()
            } else {
                let remaining_rewards = self.reward_rate.read()
                    * (self.finish_at.read() - block_timestamp);
                (remaining_rewards + amount) / self.duration.read()
            };

            assert(
                self.reward_token.read().balance_of(get_contract_address()) >= rate
                    * self.duration.read(),
                super::Errors::NOT_ENOUGH_REWARDS
            );

            self.reward_rate.write(rate);

            // even if the previous reward duration has not finished, we reset the finish_at variable
            self.finish_at.write(block_timestamp + self.duration.read());
            self.last_updated_at.write(block_timestamp);

            // reset total distributed rewards
            self.total_distributed_rewards.write(0);
        }

        fn stake(ref self: ContractState, amount: u256) {
            assert(amount > 0, super::Errors::NULL_AMOUNT);

            let user = get_caller_address();
            self.update_rewards(user);

            self.balance_of.write(user, self.balance_of.read(user) + amount);
            self.total_supply.write(self.total_supply.read() + amount);
            self.staking_token.read().transfer_from(user, get_contract_address(), amount);

            self.emit(Deposit { user, amount });
        }

        fn withdraw(ref self: ContractState, amount: u256) {
            assert(amount > 0, super::Errors::NULL_AMOUNT);

            let user = get_caller_address();

            assert(
                self.staking_token.read().balance_of(user) >= amount,
                super::Errors::NOT_ENOUGH_BALANCE
            );

            self.update_rewards(user);

            self.balance_of.write(user, self.balance_of.read(user) - amount);
            self.total_supply.write(self.total_supply.read() - amount);
            self.staking_token.read().transfer(user, amount);

            self.emit(Withdrawal { user, amount });
        }

        fn get_rewards(self: @ContractState, account: ContractAddress) -> u256 {
            self.unclaimed_rewards.read(account) + self.compute_new_rewards(account)
        }

        fn claim_rewards(ref self: ContractState) {
            let user = get_caller_address();
            self.update_rewards(user);

            let rewards = self.unclaimed_rewards.read(user);

            if rewards > 0 {
                self.unclaimed_rewards.write(user, 0);
                self.reward_token.read().transfer(user, rewards);
            }
        }
    }

    #[generate_trait]
    impl PrivateFunctions of PrivateFunctionsTrait {
        // call this function every time a user (including owner) performs a state-modifying action
        fn update_rewards(ref self: ContractState, account: ContractAddress) {
            self
                .current_reward_per_staked_token
                .write(self.compute_current_reward_per_staked_token());

            self.last_updated_at.write(self.last_time_applicable());

            if account.is_non_zero() {
                self.distribute_user_rewards(account);

                self
                    .last_user_reward_per_staked_token
                    .write(account, self.current_reward_per_staked_token.read());

                self.send_rewards_finished_event();
            }
        }

        fn distribute_user_rewards(ref self: ContractState, account: ContractAddress) {
            // compute earned rewards since last update for the user `account`
            let user_rewards = self.get_rewards(account);
            self.unclaimed_rewards.write(account, user_rewards);

            // track amount of total rewards distributed
            self
                .total_distributed_rewards
                .write(self.total_distributed_rewards.read() + user_rewards);
        }

        fn send_rewards_finished_event(ref self: ContractState) {
            // check whether we should send a RewardsFinished event
            if self.last_updated_at.read() == self.finish_at.read() {
                let total_rewards = self.reward_rate.read() * self.duration.read();

                if total_rewards != 0 && self.total_distributed_rewards.read() == total_rewards {
                    // owner should set up NEW rewards into the contract
                    self.emit(RewardsFinished { msg: 'Rewards all distributed' });
                } else {
                    // owner should set up rewards into the contract (or add duration by setting up rewards)
                    self.emit(RewardsFinished { msg: 'Rewards not active yet' });
                }
            }
        }

        fn compute_current_reward_per_staked_token(self: @ContractState) -> u256 {
            if self.total_supply.read() == 0 {
                self.current_reward_per_staked_token.read()
            } else {
                self.current_reward_per_staked_token.read()
                    + self.reward_rate.read()
                        * (self.last_time_applicable() - self.last_updated_at.read())
                        / self.total_supply.read()
            }
        }

        fn compute_new_rewards(self: @ContractState, account: ContractAddress) -> u256 {
            self.balance_of.read(account)
                * (self.current_reward_per_staked_token.read()
                    - self.last_user_reward_per_staked_token.read(account))
        }

        #[inline(always)]
        fn last_time_applicable(self: @ContractState) -> u256 {
            PrivateFunctions::min(self.finish_at.read(), get_block_timestamp().into())
        }

        #[inline(always)]
        fn min(x: u256, y: u256) -> u256 {
            if (x <= y) {
                x
            } else {
                y
            }
        }

        fn only_owner(self: @ContractState) {
            let caller = get_caller_address();
            assert(caller == self.owner.read(), super::Errors::NOT_OWNER);
        }
    }
}
Last change: 2024-06-09, commit: 3fbfb60

Simple Storage (Starknet-js + Cairo)

In this example, we will use a SimpleStorage Cairo contract deployed on Starknet Sepolia Testnet and show how you can interact with the contract using Starknet-js.

Writing SimpleStorage contract in Cairo

The SimpleStorage contract has only one purpose: storing a number. We want the users to interact with the stored number by writing to the currently stored number and reading the number in the contract.

We will use the following SimpleStorage contract. In the Storage Variables page, you can find explanations for each component of the contract:

#[starknet::interface]
trait ISimpleStorage<T> {
    fn set(ref self: T, x: u128);
    fn get(self: @T) -> u128;
}

#[starknet::contract]
mod SimpleStorage {
    #[storage]
    struct Storage {
        stored_data: u128
    }

    #[abi(embed_v0)]
    impl SimpleStorage of super::ISimpleStorage<ContractState> {
        fn set(ref self: ContractState, x: u128) {
            self.stored_data.write(x);
        }

        fn get(self: @ContractState) -> u128 {
            self.stored_data.read()
        }
    }
}
# 
# 

Because we want to interact with the get and set functions of the SimpleStorage contract using Starknet-js, we define the function signatures in #[starknet::interface]. The functions are defined under the macro #[abi(embed_v0)] where external functions are written.

Only deployed instances of the contract can be interacted with. You can refer to the How to Deploy page. Note down the address of your contract, as it is needed for the following part.

Interacting with SimpleStorage contract

We will interact with the SimpleStorage contract using Starknet-js. Firstly, create a new folder and inside the directory of the new folder, initialize the npm package (click Enter several items, you can skip adding the package info):

$ npm init

Now, package.json file is created. Change the type of the package to a module.

"type": "module"

Let's add Starknet-js as a dependency:

$ npm install starknet@next

Create a file named index.js where we will write JavaScript code to interact with our contract. Let's start our code by importing from Starknet-js, and from other libraries we will need:

import { Account, RpcProvider, json, Contract } from "starknet";
import fs from "fs";
import * as dotenv from "dotenv";
dotenv.config();

Let's create our provider object, and add our account address as a constant variable. We need the provider in order to send our queries and transactions to a Starknet node that is connected to the Starknet network:

const provider = new RpcProvider({
  nodeUrl: "https://free-rpc.nethermind.io/sepolia-juno",
});
const accountAddress = // 'PASTE_ACCOUNT_ADDRESS_HERE';

The next step is creating an Account object that will be used to sign transactions, so we need to import the account private key. You can access it directly from your keystore with the following command using Starkli:

$ starkli signer keystore inspect-private /path/to/starkli-wallet/keystore.json --raw

Create a .env file in your project folder, and paste your private key as shown in the following line:

PRIVATE_KEY = "PASTE_PRIVATE_KEY_HERE"

Warning: Using .env files is not recommended for production environments, please use .env files only for development purposes! It is HIGHLY recommended to add .gitignore, and include your .env file there if you will be pushing your project to GitHub.

Now, import your private key from the environment variables and create your Account object.

const accountAddress = // 'PASTE_ACCOUNT_PUBLIC_ADDRESS_HERE';
const privateKey = process.env.PRIVATE_KEY;
// "1" is added to show that our account is deployed using Cairo 1.0.
const account = new Account(provider, accountAddress, privateKey, "1");

Now, let's create a Contract object in order to interact with our contract. In order to create the Contract object, we need the ABI and the address of our contract. The ABI contains information about what kind of data structures and functions there are in our contract so that we can interact with them using SDKs like Starknet-js.

We will copy ./target/simple_storage_SimpleStorage.contract_class.json to abi.json in the Scarb project folder. The beginning of the content of the ABI file should look like this:

{"sierra_program":["0x1","0x5","0x0","0x2","0x6","0x3","0x98","0x68","0x18", //...

We can then create the Account object and the Contract object in our index.js file:

const contractAddress = 'PASTE_CONTRACT_ADDRESS_HERE';
const compiledContractAbi = json.parse(
  fs.readFileSync("./abi.json").toString("ascii")
);
const storageContract = new Contract(
  compiledContractAbi.abi,
  contractAddress,
  provider
);

The setup is finished! By calling the fn get(self: @ContractState) -> u128 function, we will be able to read the stored_data variable from the contract:

let getData = await storageContract.get();
console.log("Stored_data:", getData.toString());

In order to run your code, run the command node index.js in your project directory. After a short amount of time, you should see a "0" as the stored data.

Now, we will set a new number to the stored_data variable by calling the fn set(self: @mut ContractState, new_data: u128) function. This is an INVOKE transaction, so we need to sign the transaction with our account's private key and pass along the calldata.

The transaction is signed and broadcasted to the network and it can takes a few seconds for the transaction to be confirmed.

storageContract.connect(account);
const myCall = storageContract.populate("set", [59]);
const res = await storageContract.set(myCall.calldata);
await provider.waitForTransaction(res.transaction_hash);

// Get the stored data after setting it
getData = await storageContract.get();
console.log("Stored_data after set():", getData.toString());
Last change: 2024-06-25, commit: 0d9f473

Crowdfunding Campaign

Crowdfunding is a method of raising capital through the collective effort of many individuals. It allows project creators to raise funds from a large number of people, usually through small contributions.

  1. Contract admin creates a campaign in some user's name (i.e. creator).
  2. Users can pledge, transferring their token to a campaign.
  3. Users can "unpledge", retrieving their tokens.
  4. The creator can at any point refund any of the users.
  5. Once the total amount pledged is more than the campaign goal, the campaign funds are "locked" in the contract, meaning the users can no longer unpledge; they can still pledge though.
  6. After the campaign ends, the campaign creator can claim the funds if the campaign goal is reached.
  7. Otherwise, campaign did not reach it's goal, pledgers can retrieve their funds.
  8. The creator can at any point cancel the campaign for whatever reason and refund all of the pledgers.
  9. The contract admin can upgrade the contract implementation, refunding all of the users and reseting the campaign state (we will use this in the Advanced Factory chapter).

Because contract upgrades need to be able to refund all of the pledges, we need to be able to iterate over all of the pledgers and their amounts. Since iteration is not supported by LegacyMap, we need to create a custom storage type that will encompass pledge management. We use a component for this purpose.

use starknet::ContractAddress;

#[starknet::interface]
pub trait IPledgeable<TContractState> {
    fn add(ref self: TContractState, pledger: ContractAddress, amount: u256);
    fn get(self: @TContractState, pledger: ContractAddress) -> u256;
    fn get_pledger_count(self: @TContractState) -> u32;
    fn array(self: @TContractState) -> Array<ContractAddress>;
    fn get_total(self: @TContractState) -> u256;
    fn remove(ref self: TContractState, pledger: ContractAddress) -> u256;
}

#[starknet::component]
pub mod pledgeable_component {
    use core::array::ArrayTrait;
    use starknet::{ContractAddress};
    use core::num::traits::Zero;

    #[storage]
    struct Storage {
        index_to_pledger: LegacyMap<u32, ContractAddress>,
        pledger_to_amount: LegacyMap<ContractAddress, u256>,
        pledger_count: u32,
        total_amount: u256,
    }

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

    mod Errors {
        pub const INCONSISTENT_STATE: felt252 = 'Non-indexed pledger found';
    }

    #[embeddable_as(Pledgeable)]
    pub impl PledgeableImpl<
        TContractState, +HasComponent<TContractState>
    > of super::IPledgeable<ComponentState<TContractState>> {
        fn add(ref self: ComponentState<TContractState>, pledger: ContractAddress, amount: u256) {
            let old_amount: u256 = self.pledger_to_amount.read(pledger);

            if old_amount == 0 {
                let index = self.pledger_count.read();
                self.index_to_pledger.write(index, pledger);
                self.pledger_count.write(index + 1);
            }

            self.pledger_to_amount.write(pledger, old_amount + amount);
            self.total_amount.write(self.total_amount.read() + amount);
        }

        fn get(self: @ComponentState<TContractState>, pledger: ContractAddress) -> u256 {
            self.pledger_to_amount.read(pledger)
        }

        fn get_pledger_count(self: @ComponentState<TContractState>) -> u32 {
            self.pledger_count.read()
        }

        fn array(self: @ComponentState<TContractState>) -> Array<ContractAddress> {
            let mut result = array![];

            let mut index = self.pledger_count.read();
            while index != 0 {
                index -= 1;
                let pledger = self.index_to_pledger.read(index);
                result.append(pledger);
            };

            result
        }

        fn get_total(self: @ComponentState<TContractState>) -> u256 {
            self.total_amount.read()
        }

        fn remove(ref self: ComponentState<TContractState>, pledger: ContractAddress) -> u256 {
            let amount: u256 = self.pledger_to_amount.read(pledger);

            // check if the pledge even exists
            if amount == 0 {
                return 0;
            }

            let last_index = self.pledger_count.read() - 1;

            // if there are other pledgers, we need to update our indices
            if last_index != 0 {
                let mut pledger_index = last_index;
                loop {
                    if self.index_to_pledger.read(pledger_index) == pledger {
                        break;
                    }
                    // if pledger_to_amount contains a pledger, then so does index_to_pledger
                    // thus this will never underflow
                    pledger_index -= 1;
                };

                self.index_to_pledger.write(pledger_index, self.index_to_pledger.read(last_index));
            }

            // last_index == new pledger count
            self.pledger_count.write(last_index);
            self.pledger_to_amount.write(pledger, 0);
            self.index_to_pledger.write(last_index, Zero::zero());

            self.total_amount.write(self.total_amount.read() - amount);

            amount
        }
    }
}

Now we can create the Campaign contract.

use starknet::{ClassHash, ContractAddress};

#[derive(Drop, Serde)]
pub struct Details {
    pub canceled: bool,
    pub claimed: bool,
    pub creator: ContractAddress,
    pub description: ByteArray,
    pub end_time: u64,
    pub goal: u256,
    pub start_time: u64,
    pub title: ByteArray,
    pub token: ContractAddress,
    pub total_pledges: u256,
}

#[starknet::interface]
pub trait ICampaign<TContractState> {
    fn claim(ref self: TContractState);
    fn cancel(ref self: TContractState, reason: ByteArray);
    fn pledge(ref self: TContractState, amount: u256);
    fn get_pledge(self: @TContractState, pledger: ContractAddress) -> u256;
    fn get_pledgers(self: @TContractState) -> Array<ContractAddress>;
    fn get_details(self: @TContractState) -> Details;
    fn refund(ref self: TContractState, pledger: ContractAddress, reason: ByteArray);
    fn upgrade(ref self: TContractState, impl_hash: ClassHash, new_end_time: Option<u64>);
    fn unpledge(ref self: TContractState, reason: ByteArray);
}

#[starknet::contract]
pub mod Campaign {
    use components::ownable::ownable_component::OwnableInternalTrait;
    use core::num::traits::zero::Zero;
    use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
    use starknet::{
        ClassHash, ContractAddress, SyscallResultTrait, get_block_timestamp, contract_address_const,
        get_caller_address, get_contract_address, class_hash::class_hash_const
    };
    use components::ownable::ownable_component;
    use super::pledgeable::pledgeable_component;
    use super::Details;

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

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

    #[storage]
    struct Storage {
        canceled: bool,
        claimed: bool,
        creator: ContractAddress,
        description: ByteArray,
        end_time: u64,
        goal: u256,
        #[substorage(v0)]
        ownable: ownable_component::Storage,
        #[substorage(v0)]
        pledges: pledgeable_component::Storage,
        start_time: u64,
        title: ByteArray,
        token: IERC20Dispatcher,
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    pub enum Event {
        Claimed: Claimed,
        Canceled: Canceled,
        #[flat]
        OwnableEvent: ownable_component::Event,
        PledgeableEvent: pledgeable_component::Event,
        PledgeMade: PledgeMade,
        Refunded: Refunded,
        RefundedAll: RefundedAll,
        Unpledged: Unpledged,
        Upgraded: Upgraded,
    }

    #[derive(Drop, starknet::Event)]
    pub struct Canceled {
        pub reason: ByteArray,
    }

    #[derive(Drop, starknet::Event)]
    pub struct Claimed {
        pub amount: u256,
    }

    #[derive(Drop, starknet::Event)]
    pub struct PledgeMade {
        #[key]
        pub pledger: ContractAddress,
        pub amount: u256,
    }

    #[derive(Drop, starknet::Event)]
    pub struct Refunded {
        #[key]
        pub pledger: ContractAddress,
        pub amount: u256,
        pub reason: ByteArray,
    }

    #[derive(Drop, starknet::Event)]
    pub struct RefundedAll {
        pub reason: ByteArray,
    }

    #[derive(Drop, starknet::Event)]
    pub struct Unpledged {
        #[key]
        pub pledger: ContractAddress,
        pub amount: u256,
        pub reason: ByteArray,
    }

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

    pub mod Errors {
        pub const CANCELED: felt252 = 'Campaign canceled';
        pub const CLAIMED: felt252 = 'Campaign already claimed';
        pub const CLASS_HASH_ZERO: felt252 = 'Class hash zero';
        pub const CREATOR_ZERO: felt252 = 'Creator address zero';
        pub const ENDED: felt252 = 'Campaign already ended';
        pub const END_BEFORE_NOW: felt252 = 'End time < now';
        pub const END_BEFORE_START: felt252 = 'End time < start time';
        pub const END_BIGGER_THAN_MAX: felt252 = 'End time > max duration';
        pub const NOTHING_TO_REFUND: felt252 = 'Nothing to refund';
        pub const NOTHING_TO_UNPLEDGE: felt252 = 'Nothing to unpledge';
        pub const NOT_CREATOR: felt252 = 'Not creator';
        pub const NOT_STARTED: felt252 = 'Campaign not started';
        pub const PLEDGES_LOCKED: felt252 = 'Goal reached, pledges locked';
        pub const START_TIME_IN_PAST: felt252 = 'Start time < now';
        pub const STILL_ACTIVE: felt252 = 'Campaign not ended';
        pub const GOAL_NOT_REACHED: felt252 = 'Goal not reached';
        pub const TITLE_EMPTY: felt252 = 'Title empty';
        pub const TRANSFER_FAILED: felt252 = 'Transfer failed';
        pub const ZERO_ADDRESS_CALLER: felt252 = 'Caller address zero';
        pub const ZERO_ADDRESS_PLEDGER: felt252 = 'Pledger address zero';
        pub const ZERO_ADDRESS_TOKEN: felt252 = 'Token address zerp';
        pub const ZERO_DONATION: felt252 = 'Donation must be > 0';
        pub const ZERO_GOAL: felt252 = 'Goal must be > 0';
        pub const ZERO_PLEDGES: felt252 = 'No pledges to claim';
    }

    const NINETY_DAYS: u64 = consteval_int!(90 * 24 * 60 * 60);

    #[constructor]
    fn constructor(
        ref self: ContractState,
        creator: ContractAddress,
        title: ByteArray,
        description: ByteArray,
        goal: u256,
        start_time: u64,
        end_time: u64,
        token_address: ContractAddress,
    ) {
        assert(creator.is_non_zero(), Errors::CREATOR_ZERO);
        assert(title.len() > 0, Errors::TITLE_EMPTY);
        assert(goal > 0, Errors::ZERO_GOAL);
        assert(start_time >= get_block_timestamp(), Errors::START_TIME_IN_PAST);
        assert(end_time >= start_time, Errors::END_BEFORE_START);
        assert(end_time <= get_block_timestamp() + NINETY_DAYS, Errors::END_BIGGER_THAN_MAX);
        assert(token_address.is_non_zero(), Errors::ZERO_ADDRESS_TOKEN);

        self.creator.write(creator);
        self.title.write(title);
        self.goal.write(goal);
        self.description.write(description);
        self.start_time.write(start_time);
        self.end_time.write(end_time);
        self.token.write(IERC20Dispatcher { contract_address: token_address });
        self.ownable._init(get_caller_address());
    }

    #[abi(embed_v0)]
    impl Campaign of super::ICampaign<ContractState> {
        fn cancel(ref self: ContractState, reason: ByteArray) {
            self._assert_only_creator();
            assert(!self.canceled.read(), Errors::CANCELED);
            assert(!self.claimed.read(), Errors::CLAIMED);

            self.canceled.write(true);

            self._refund_all(reason.clone());

            self.emit(Event::Canceled(Canceled { reason }));
        }

        /// Sends the funds to the campaign creator.
        /// It leaves the pledge data intact as a testament to campaign success
        fn claim(ref self: ContractState) {
            self._assert_only_creator();
            assert(self._is_started(), Errors::NOT_STARTED);
            assert(self._is_ended(), Errors::STILL_ACTIVE);
            assert(!self.claimed.read(), Errors::CLAIMED);
            assert(self._is_goal_reached(), Errors::GOAL_NOT_REACHED);
            // no need to check if canceled; if it was, then the goal wouldn't have been reached

            let this = get_contract_address();
            let token = self.token.read();
            let amount = token.balance_of(this);
            assert(amount > 0, Errors::ZERO_PLEDGES);

            self.claimed.write(true);

            let owner = get_caller_address();
            let success = token.transfer(owner, amount);
            assert(success, Errors::TRANSFER_FAILED);

            self.emit(Event::Claimed(Claimed { amount }));
        }

        fn get_details(self: @ContractState) -> Details {
            Details {
                canceled: self.canceled.read(),
                claimed: self.claimed.read(),
                creator: self.creator.read(),
                description: self.description.read(),
                end_time: self.end_time.read(),
                goal: self.goal.read(),
                start_time: self.start_time.read(),
                title: self.title.read(),
                token: self.token.read().contract_address,
                total_pledges: self.pledges.get_total(),
            }
        }

        fn get_pledge(self: @ContractState, pledger: ContractAddress) -> u256 {
            self.pledges.get(pledger)
        }

        fn get_pledgers(self: @ContractState) -> Array<ContractAddress> {
            self.pledges.array()
        }

        fn pledge(ref self: ContractState, amount: u256) {
            assert(self._is_started(), Errors::NOT_STARTED);
            assert(!self._is_ended(), Errors::ENDED);
            assert(!self.canceled.read(), Errors::CANCELED);
            assert(amount > 0, Errors::ZERO_DONATION);

            let pledger = get_caller_address();
            let this = get_contract_address();
            let success = self.token.read().transfer_from(pledger, this, amount);
            assert(success, Errors::TRANSFER_FAILED);

            self.pledges.add(pledger, amount);

            self.emit(Event::PledgeMade(PledgeMade { pledger, amount }));
        }

        fn refund(ref self: ContractState, pledger: ContractAddress, reason: ByteArray) {
            self._assert_only_creator();
            assert(self._is_started(), Errors::NOT_STARTED);
            assert(!self.claimed.read(), Errors::CLAIMED);
            assert(!self.canceled.read(), Errors::CANCELED);
            assert(pledger.is_non_zero(), Errors::ZERO_ADDRESS_PLEDGER);
            assert(self.pledges.get(pledger) != 0, Errors::NOTHING_TO_REFUND);

            let amount = self._refund(pledger);

            self.emit(Event::Refunded(Refunded { pledger, amount, reason }))
        }

        fn unpledge(ref self: ContractState, reason: ByteArray) {
            assert(self._is_started(), Errors::NOT_STARTED);
            assert(!self._is_goal_reached(), Errors::PLEDGES_LOCKED);
            assert(self.pledges.get(get_caller_address()) != 0, Errors::NOTHING_TO_UNPLEDGE);

            let pledger = get_caller_address();
            let amount = self._refund(pledger);

            self.emit(Event::Unpledged(Unpledged { pledger, amount, reason }));
        }

        fn upgrade(ref self: ContractState, impl_hash: ClassHash, new_end_time: Option<u64>) {
            self.ownable._assert_only_owner();
            assert(impl_hash.is_non_zero(), Errors::CLASS_HASH_ZERO);

            // only active campaigns have pledges to refund and an end time to update
            if self._is_started() {
                if let Option::Some(end_time) = new_end_time {
                    assert(end_time >= get_block_timestamp(), Errors::END_BEFORE_NOW);
                    assert(
                        end_time <= get_block_timestamp() + NINETY_DAYS, Errors::END_BIGGER_THAN_MAX
                    );
                    self.end_time.write(end_time);
                };
                self._refund_all("contract upgraded");
            }

            starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall();

            self.emit(Event::Upgraded(Upgraded { implementation: impl_hash }));
        }
    }

    #[generate_trait]
    impl CampaignInternalImpl of CampaignInternalTrait {
        fn _assert_only_creator(self: @ContractState) {
            let caller = get_caller_address();
            assert(caller.is_non_zero(), Errors::ZERO_ADDRESS_CALLER);
            assert(caller == self.creator.read(), Errors::NOT_CREATOR);
        }

        fn _is_ended(self: @ContractState) -> bool {
            get_block_timestamp() >= self.end_time.read()
        }

        fn _is_goal_reached(self: @ContractState) -> bool {
            self.pledges.get_total() >= self.goal.read()
        }

        fn _is_started(self: @ContractState) -> bool {
            get_block_timestamp() >= self.start_time.read()
        }

        #[inline(always)]
        fn _refund(ref self: ContractState, pledger: ContractAddress) -> u256 {
            let amount = self.pledges.remove(pledger);

            let success = self.token.read().transfer(pledger, amount);
            assert(success, Errors::TRANSFER_FAILED);

            amount
        }

        fn _refund_all(ref self: ContractState, reason: ByteArray) {
            let mut pledges = self.pledges.array();
            while let Option::Some(pledger) = pledges.pop_front() {
                self._refund(pledger);
            };
            self.emit(Event::RefundedAll(RefundedAll { reason }));
        }
    }
}
Last change: 2024-06-27, commit: 52fbc5c

AdvancedFactory: Crowdfunding

This is an example of an advanced factory contract that manages crowdfunding Campaign contracts created in the "Crowdfunding" chapter. The advanced factory allows for a centralized creation and management of Campaign contracts on the Starknet blockchain, ensuring that they adhere to a standard interface and can be easily upgraded.

Key Features

  1. Campaign Creation: Users can create new crowdfunding campaigns with specific details such as title, description, goal, and duration.
  2. Campaign Management: The factory contract stores and manages the campaigns, allowing for upgrades and tracking.
  3. Upgrade Mechanism: The factory owner can update the implementation of the campaign contract, ensuring that all campaigns benefit from improvements and bug fixes.
    • the factory only updates it's Campaign class hash and emits an event to notify any listeners, but the Campaign creators are in the end responsible for actually upgrading their contracts.
pub use starknet::{ContractAddress, ClassHash};

#[starknet::interface]
pub trait ICampaignFactory<TContractState> {
    fn create_campaign(
        ref self: TContractState,
        title: ByteArray,
        description: ByteArray,
        goal: u256,
        start_time: u64,
        end_time: u64,
        token_address: ContractAddress
    ) -> ContractAddress;
    fn get_campaign_class_hash(self: @TContractState) -> ClassHash;
    fn update_campaign_class_hash(ref self: TContractState, new_class_hash: ClassHash);
    fn upgrade_campaign(
        ref self: TContractState, campaign_address: ContractAddress, new_end_time: Option<u64>
    );
}

#[starknet::contract]
pub mod CampaignFactory {
    use core::num::traits::zero::Zero;
    use starknet::{
        ContractAddress, ClassHash, SyscallResultTrait, syscalls::deploy_syscall,
        get_caller_address, get_contract_address
    };
    use alexandria_storage::list::{List, ListTrait};
    use crowdfunding::campaign::{ICampaignDispatcher, ICampaignDispatcherTrait};
    use components::ownable::ownable_component;

    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,
        /// Store all of the created campaign instances' addresses and thei class hashes
        campaigns: LegacyMap<(ContractAddress, ContractAddress), ClassHash>,
        /// Store the class hash of the contract to deploy
        campaign_class_hash: ClassHash,
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    pub enum Event {
        #[flat]
        OwnableEvent: ownable_component::Event,
        CampaignClassHashUpgraded: CampaignClassHashUpgraded,
        CampaignCreated: CampaignCreated,
        ClassHashUpdated: ClassHashUpdated,
    }

    #[derive(Drop, starknet::Event)]
    pub struct ClassHashUpdated {
        pub new_class_hash: ClassHash,
    }

    #[derive(Drop, starknet::Event)]
    pub struct CampaignClassHashUpgraded {
        pub campaign: ContractAddress,
    }

    #[derive(Drop, starknet::Event)]
    pub struct CampaignCreated {
        pub creator: ContractAddress,
        pub contract_address: ContractAddress
    }

    pub mod Errors {
        pub const CLASS_HASH_ZERO: felt252 = 'Class hash cannot be zero';
        pub const ZERO_ADDRESS: felt252 = 'Zero address';
        pub const SAME_IMPLEMENTATION: felt252 = 'Implementation is unchanged';
        pub const CAMPAIGN_NOT_FOUND: felt252 = 'Campaign not found';
    }

    #[constructor]
    fn constructor(ref self: ContractState, class_hash: ClassHash) {
        assert(class_hash.is_non_zero(), Errors::CLASS_HASH_ZERO);
        self.campaign_class_hash.write(class_hash);
        self.ownable._init(get_caller_address());
    }


    #[abi(embed_v0)]
    impl CampaignFactory of super::ICampaignFactory<ContractState> {
        fn create_campaign(
            ref self: ContractState,
            title: ByteArray,
            description: ByteArray,
            goal: u256,
            start_time: u64,
            end_time: u64,
            token_address: ContractAddress,
        ) -> ContractAddress {
            let creator = get_caller_address();

            // Create contructor arguments
            let mut constructor_calldata: Array::<felt252> = array![];
            ((creator, title, description, goal), start_time, end_time, token_address)
                .serialize(ref constructor_calldata);

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

            // track new campaign instance
            self.campaigns.write((creator, contract_address), self.campaign_class_hash.read());

            self.emit(Event::CampaignCreated(CampaignCreated { creator, contract_address }));

            contract_address
        }

        fn get_campaign_class_hash(self: @ContractState) -> ClassHash {
            self.campaign_class_hash.read()
        }

        fn update_campaign_class_hash(ref self: ContractState, new_class_hash: ClassHash) {
            self.ownable._assert_only_owner();
            assert(new_class_hash.is_non_zero(), Errors::CLASS_HASH_ZERO);

            self.campaign_class_hash.write(new_class_hash);

            self.emit(Event::ClassHashUpdated(ClassHashUpdated { new_class_hash }));
        }

        fn upgrade_campaign(
            ref self: ContractState, campaign_address: ContractAddress, new_end_time: Option<u64>
        ) {
            assert(campaign_address.is_non_zero(), Errors::ZERO_ADDRESS);

            let creator = get_caller_address();
            let old_class_hash = self.campaigns.read((creator, campaign_address));
            assert(old_class_hash.is_non_zero(), Errors::CAMPAIGN_NOT_FOUND);
            assert(old_class_hash != self.campaign_class_hash.read(), Errors::SAME_IMPLEMENTATION);

            let campaign = ICampaignDispatcher { contract_address: campaign_address };
            campaign.upgrade(self.campaign_class_hash.read(), new_end_time);
        }
    }
}
Last change: 2024-06-27, commit: 52fbc5c

Writing to any storage slot

On Starknet, a contract's storage is a map with \( 2^{251} \) slots, where each slot is a felt252 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-06-09, commit: 3fbfb60

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 limitation in Cairo that restricts complex storage values to a maximum of 256 field elements. This means that to store arrays with more than 255 elements, you would have to split them into segments of size n <= 255 and store these segments at multiple storage addresses. There is currently no native support for storing arrays in Cairo, so you would need to write your own implementation of the Store trait for the array type 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 greater than 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-06-09, commit: 3fbfb60

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-06-09, commit: 3fbfb60

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 are still used in some scenarios 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 types that can be converted to felt252 since they natively implement the Hash trait. It's also possible to hash more complex types, like structs, by deriving the Hash trait with the #[derive(Hash)] attribute, but only if all the struct's fields are themselves hashable.

To hash a value, you first need to initialize a hash state with the new method of the HashStateTrait. Then, you can update the hash state with the update method. You can accumulate multiple updates if necessary. Finally, 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-06-09, commit: 3fbfb60

Optimisations

A collection of optimisation patterns to save gas and steps.

Last change: 2024-06-09, commit: 3fbfb60

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 felt252 value. This is done by using the bits of the felt252 value to store multiple values.

For example, if we want to store two u8 values, we can use the first 8 bits of the felt252 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 felt252 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 us 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();
            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();
            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-06-09, commit: 3fbfb60

Account Abstraction

An account is an unique entity that can send transactions, users usually use wallets to manage their accounts.

Historically, in Ethereum, all accounts were Externally Owned Accounts (EOA) and were controlled by private keys. This is a simple and secure way to manage accounts, but it has limitations as the account logic is hardcoded in the protocol.

Account Abstraction (AA) is the concept behind abstracting parts of the account logic to allow for a more flexible account system. This replaces EOA with Account Contracts, which are smart contracts that implement the account logic. This opens up a lot of possibilities that can significantly improve the user experience when dealing with accounts.

On Starknet, Account Abstraction is natively supported, and all accounts are Account Contracts.

In this section we will how to implement an Account.

Last change: 2024-07-01, commit: 630092f

Account Contract

A smart contract must follow the Standard Account Interface specification defined in the SNIP-6. In practice, this means that the contract must implement the SRC6 and SRC5 interfaces to be considered an account contract.

SNIP-6: SRC6 + SRC5

/// @title Represents a call to a target contract
/// @param to The target contract address
/// @param selector The target function selector
/// @param calldata The serialized function parameters
struct Call {
    to: ContractAddress,
    selector: felt252,
    calldata: Array<felt252>
}

The Call struct is used to represent a call to a function (selector) in a target contract (to) with parameters (calldata). It is available under the starknet::account module.

/// @title SRC-6 Standard Account
trait ISRC6 {
    /// @notice Execute a transaction through the account
    /// @param calls The list of calls to execute
    /// @return The list of each call's serialized return value
    fn __execute__(calls: Array<Call>) -> Array<Span<felt252>>;

    /// @notice Assert whether the transaction is valid to be executed
    /// @param calls The list of calls to execute
    /// @return The string 'VALID' represented as felt when is valid
    fn __validate__(calls: Array<Call>) -> felt252;

    /// @notice Assert whether a given signature for a given hash is valid
    /// @param hash The hash of the data
    /// @param signature The signature to validate
    /// @return The string 'VALID' represented as felt when the signature is valid
    fn is_valid_signature(hash: felt252, signature: Array<felt252>) -> felt252;
}

A transaction can be represented as a list of calls Array<Call> to other contracts, with atleast one call.

  • __execute__: Executes a transaction after the validation phase. Returns an array of the serialized return of value (Span<felt252>) of each call.

  • __validate__: Validates a transaction by verifying some predefined rules, such as the signature of the transaction. Returns the VALID short string (as a felt252) if the transaction is valid.

  • is_valid_signature: Verify that a given signature is valid. This is mainly used by applications for authentication purposes.

Both __execute__ and __validate__ functions are exclusively called by the Starknet protocol.

/// @title SRC-5 Standard Interface Detection
trait ISRC5 {
    /// @notice Query if a contract implements an interface
    /// @param interface_id The interface identifier, as specified in SRC-5
    /// @return `true` if the contract implements `interface_id`, `false` otherwise
    fn supports_interface(interface_id: felt252) -> bool;
}

The interface identifiers of both SRC5 and SRC6 must be published with supports_interface.

Minimal account contract Executing Transactions

In this example, we will implement a minimal account contract that can validate and execute transactions.

use starknet::account::Call;

#[starknet::interface]
trait ISRC6<TContractState> {
    fn execute_calls(self: @TContractState, calls: Array<Call>) -> Array<Span<felt252>>;
    fn validate_calls(self: @TContractState, calls: Array<Call>) -> felt252;
    fn is_valid_signature(
        self: @TContractState, hash: felt252, signature: Array<felt252>
    ) -> felt252;
}

#[starknet::contract]
mod simpleAccount {
    use super::ISRC6;
    use starknet::account::Call;
    use core::num::traits::Zero;
    use core::ecdsa::check_ecdsa_signature;

    // Implement SRC5 with openzeppelin
    use openzeppelin::account::interface;
    use openzeppelin::introspection::src5::SRC5Component;
    component!(path: SRC5Component, storage: src5, event: SRC5Event);

    #[abi(embed_v0)]
    impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;
    impl SRC5InternalImpl = SRC5Component::InternalImpl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        src5: SRC5Component::Storage,
        public_key: felt252
    }

    #[constructor]
    fn constructor(ref self: ContractState, public_key: felt252) {
        self.src5.register_interface(interface::ISRC6_ID);
        self.public_key.write(public_key);
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        #[flat]
        SRC5Event: SRC5Component::Event
    }

    #[abi(embed_v0)]
    impl SRC6 of ISRC6<ContractState> {
        fn execute_calls(self: @ContractState, calls: Array<Call>) -> Array<Span<felt252>> {
            assert(starknet::get_caller_address().is_zero(), 'Not Starknet Protocol');
            let Call { to, selector, calldata } = calls.at(0);
            let res = starknet::syscalls::call_contract_syscall(*to, *selector, *calldata).unwrap();
            array![res]
        }

        fn validate_calls(self: @ContractState, calls: Array<Call>) -> felt252 {
            assert(starknet::get_caller_address().is_zero(), 'Not Starknet Protocol');
            let tx_info = starknet::get_tx_info().unbox();
            let tx_hash = tx_info.transaction_hash;
            let signature = tx_info.signature;
            if self._is_valid_signature(tx_hash, signature) {
                starknet::VALIDATED
            } else {
                0
            }
        }

        fn is_valid_signature(
            self: @ContractState, hash: felt252, signature: Array<felt252>
        ) -> felt252 {
            if self._is_valid_signature(hash, signature.span()) {
                starknet::VALIDATED
            } else {
                0
            }
        }
    }

    #[generate_trait]
    impl SignatureVerificationImpl of SignatureVerification {
        fn _is_valid_signature(
            self: @ContractState, hash: felt252, signature: Span<felt252>
        ) -> bool {
            check_ecdsa_signature(
                hash, self.public_key.read(), *signature.at(0_u32), *signature.at(1_u32)
            )
        }
    }
}
Last change: 2024-07-01, commit: 630092f

List

By default, there is no list type supported in Cairo, but you can use the Alexandria standard library. 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 your own custom type in a List, you have to implement the Store trait for it. You can have the compiler derive it for you using the #[derive(starknet::Store)] attribute.

Caveats

There are two idiosyncrasies 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 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 {
        amounts: 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.amounts.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.amounts.read();
            current_amount_list.is_empty()
        }

        fn list_length(self: @ContractState) -> u32 {
            let mut current_amount_list = self.amounts.read();
            current_amount_list.len()
        }

        fn get_from_index(self: @ContractState, index: u32) -> u128 {
            self.amounts.read()[index]
        }

        fn set_from_index(ref self: ContractState, index: u32, number: u128) {
            let mut current_amount_list = self.amounts.read();
            current_amount_list.set(index, number).unwrap();
        }

        fn pop_front_list(ref self: ContractState) {
            let mut current_amount_list = self.amounts.read();
            current_amount_list.pop_front().unwrap().unwrap();
        }

        fn array_conversion(self: @ContractState) -> Array<u128> {
            let mut current_amount_list = self.amounts.read();
            current_amount_list.array().unwrap()
        }
    }
}
Last change: 2024-06-09, commit: 3fbfb60

Library Calls

External calls can be made on Starknet by two means: Contract dispatchers or Library dispatchers. Dispatchers are automatically created and exported by the compiler when a contract interface is defined.

With Contract dispatcher we are calling an already deployed contract (with associated state), therefore contract address is passed to the dispatcher to make the call. However, with library dispatcher we are simply making function calls to declared contract classes (stateless).

Contract dispatcher call is synonymous to external calls in Solidity, while library dispatcher call is synonymous to delegate call.

For further reading: Cairo book.

#[starknet::interface]
pub trait IMathUtils<T> {
    fn add(ref self: T, x: u32, y: u32) -> u32;
    fn set_class_hash(ref self: T, class_hash: starknet::ClassHash);
}

// contract A
#[starknet::contract]
pub mod MathUtils {
    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl ImathUtilsImpl of super::IMathUtils<ContractState> {
        fn add(ref self: ContractState, x: u32, y: u32) -> u32 {
            x + y
        }

        fn set_class_hash(ref self: ContractState, class_hash: starknet::ClassHash) {}
    }
}


// contract B to make library call to the class of contract A
#[starknet::contract]
pub mod MathUtilsLibraryCall {
    use starknet::{class_hash::class_hash_const, ContractAddress};
    use super::{IMathUtilsDispatcherTrait, IMathUtilsLibraryDispatcher};

    #[storage]
    struct Storage {
        value: u32,
        lib_class_hash: starknet::ClassHash,
    }

    #[abi(embed_v0)]
    impl MathUtils of super::IMathUtils<ContractState> {
        fn add(ref self: ContractState, x: u32, y: u32) -> u32 {
            IMathUtilsLibraryDispatcher { class_hash: self.lib_class_hash.read() }.add(x, y)
        }

        #[abi(embed_v0)]
        fn set_class_hash(ref self: ContractState, class_hash: starknet::ClassHash) {
            self.lib_class_hash.write(class_hash);
        }
    }
}

Last change: 2024-06-09, commit: 3fbfb60

Plugins

Compiler plugins 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 Cairo. You can learn more about how to do that in the hello-cairo-plugin repository.

Last change: 2024-06-09, commit: 3fbfb60

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-06-09, commit: 3fbfb60