Starknet by Example
Starknet By Example is a collection of examples of how to use the Cairo programming language to create smart contracts on Starknet.
Starknet is a permissionless Validity-Rollup that supports general computation. It is currently used as an Ethereum layer-2. Starknet use the STARK cryptographic proof system to ensure high safety and scalability.
Starknet smart contracts are written in the Cairo language. Cairo is a Turing-complete programming language designed to write provable programs, abstracting the zk-STARK proof system away from the programmer.
⚠️ The examples have not been audited and are not intended for production use. The authors are not responsible for any damages caused by the use of the code provided in this book.
For whom is this for?
Starknet By Example is for anyone who wants to quickly learn how to write smart contracts on Starknet using Cairo with some technical background in programming and blockchain.
The first chapters will give you a basic understanding of the Cairo programming language and how to write, deploy and use smart contracts on Starknet. The later chapters will cover more advanced topics and show you how to write more complex smart contracts.
How to use this book?
Each chapter is a standalone example that demonstrates a specific feature or common use case of smart contracts on Starknet. If you are new to Starknet, it is recommended to read the chapters in order.
Most examples contains interfaces and tests that are hidden by default. You can hover over the code blocks and click on the "Show hidden lines" (eyes icon) to see the hidden code.
You can run each examples online by using the Starknet Remix Plugin.
Further reading
If you want to learn more about the Cairo programming language, you can read the Cairo Book. If you want to learn more about Starknet, you can read the Starknet documentation and the Starknet Book.
For more resources, check Awesome Starknet.
Versions
The current version of this book use:
cairo 2.6.3
edition = '2023_11'
scarb 2.6.4
Basics of Smart Contracts in Cairo
The following chapters will introduce you to Starknet smart contracts and how to write them in Cairo.
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
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');
}
}
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
}
}
Visibility and Mutability
Visibility
There are two types of functions in Starknet contracts:
- Functions that are accessible externally and can be called by anyone.
- Functions that are only accessible internally and can only be called by other functions in the contract.
These functions are also typically divided into two different implementations blocks. The first impl
block for externally accessible functions is explicitly annotated with an #[abi(embed_v0)]
attribute. This indicates that all the functions inside this block can be called either as a transaction or as a view function. The second impl
block for internally accessible functions is not annotated with any attribute, which means that all the functions inside this block are private by default.
State Mutability
Regardless of whether a function is internal or external, it can either modify the contract's state or not. When we declare functions that interact with storage variables inside a smart contract,
we need to explicitly state that we are accessing the ContractState
by adding it as the first parameter of the function. This can be done in two different ways:
- If we want our function to be able to mutate the state of the contract, we pass it by reference like this:
ref self: ContractState
. - If we want our function to be read-only and not mutate the state of the contract, we pass it by snapshot like this:
self: @ContractState
.
Read-only functions, also called view functions, can be directly called without making a transaction. You can interact with them directly through a RPC node to read the contract's state, and they're free to call! External functions, that modify the contract's state, on the other side can only be called by making a transaction.
Internal functions can't be called externally, but the same principle applies regarding state mutability.
Let's take a look at a simple example contract to see these in action:
#[starknet::interface]
pub trait IExampleContract<TContractState> {
fn set(ref self: TContractState, value: u32);
fn get(self: @TContractState) -> u32;
}
#[starknet::contract]
pub mod ExampleContract {
#[storage]
struct Storage {
value: u32
}
// The `abi(embed_v0)` attribute indicates that all
// the functions in this implementation can be called externally.
// Omitting this attribute would make all the functions internal.
#[abi(embed_v0)]
impl ExampleContract of super::IExampleContract<ContractState> {
// The `set` function can be called externally
// because it is written inside an implementation marked as `#[external]`.
// It can modify the contract's state as it is passed as a reference.
fn set(ref self: ContractState, value: u32) {
self.value.write(value);
}
// The `get` function can be called externally
// because it is written inside an implementation marked as `#[external]`.
// However, it can't modify the contract's state is passed as a snapshot
// -> It's only a "view" function.
fn get(self: @ContractState) -> u32 {
// We can call an internal function from any functions within the contract
PrivateFunctionsTrait::_read_value(self)
}
}
// The lack of the `external` attribute indicates that all the functions in
// this implementation can only be called internally.
// We name the trait `PrivateFunctionsTrait` to indicate that it is an
// internal trait allowing us to call internal functions.
#[generate_trait]
pub impl PrivateFunctions of PrivateFunctionsTrait {
// The `_read_value` function is outside the implementation that is
// marked as `#[abi(embed_v0)]`, so it's an _internal_ function
// and can only be called from within the contract.
// However, it can't modify the contract's state is passed
// as a snapshot: it is only a "view" function.
fn _read_value(self: @ContractState) -> u32 {
self.value.read()
}
}
}
#[cfg(test)]
mod test {
use super::{ExampleContract, IExampleContractDispatcher, IExampleContractDispatcherTrait};
use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};
// These imports will allow us to directly access and set the contract state:
// - for `value` storage variable access
use super::ExampleContract::valueContractMemberStateTrait;
// - for `PrivateFunctionsTrait` internal functions access
// implementation need to be public to be able to access it
use super::ExampleContract::PrivateFunctionsTrait;
// to set the contract address for the state
// and also be able to use the dispatcher on the same contract
use starknet::testing::set_contract_address;
#[test]
fn can_call_set_and_get() {
let (contract_address, _) = deploy_syscall(
ExampleContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
// You can interact with the external entrypoints of the contract using the dispatcher.
let contract = IExampleContractDispatcher { contract_address };
// But for internal functions, you need to use the contract state.
let mut state = ExampleContract::contract_state_for_testing();
set_contract_address(contract_address);
// The contract dispatcher and state refer to the same contract.
assert_eq!(contract.get(), state.value.read());
// We can set from the dispatcher
contract.set(42);
assert_eq!(contract.get(), state.value.read());
assert_eq!(42, state.value.read());
assert_eq!(42, contract.get());
// Or directly from the state for more complex operations
state.value.write(24);
assert_eq!(contract.get(), state.value.read());
assert_eq!(24, state.value.read());
assert_eq!(24, contract.get());
// We can also acces internal functions from the state
assert_eq!(state._read_value(), state.value.read());
assert_eq!(state._read_value(), contract.get());
}
}
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 to0
. -
When a user calls the
increment
entrypoint, the contract incrementscounter
by1
. -
When a user calls the
decrement
, the contract decrementscounter
by1
.
#[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);
}
}
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
ish(...h(h(sn_keccak(variable_name),k_1),k_2),...,k_n)
whereℎ
is the Pedersen hash and the final value is takenmod2251−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');
}
}
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 therequire
statement in Solidity. -
panic
immediately halt the execution with the given error value. It should be used when the condition to check is complex and for internal errors. It's similar to therevert
statement in Solidity. (Usepanic_with_felt252
to be able to directly pass a felt252 as the error value)
The assert_eq!
, assert_ne!
, assert_lt!
, assert_le!
, assert_gt!
and assert_ge!
macros can be used as an assert
shorthand to compare two values, but only in tests. In contract, you should only use the assert
function.
Here's a simple example that demonstrates the use of these functions:
#[starknet::interface]
pub trait IErrorsExample<TContractState> {
fn test_assert(self: @TContractState, i: u256);
fn test_panic(self: @TContractState, i: u256);
}
#[starknet::contract]
pub mod ErrorsExample {
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl ErrorsExample of super::IErrorsExample<ContractState> {
fn test_assert(self: @ContractState, i: u256) {
// Assert used to validate a condition
// and abort execution if the condition is not met
assert(i > 0, 'i must be greater than 0');
}
fn test_panic(self: @ContractState, i: u256) {
if (i == 0) {
// Panic used to abort execution directly
core::panic_with_felt252('i must not be 0');
}
}
}
}
#[cfg(test)]
mod test {
use super::{ErrorsExample, IErrorsExampleDispatcher, IErrorsExampleDispatcherTrait};
use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};
fn deploy() -> IErrorsExampleDispatcher {
let (contract_address, _) = deploy_syscall(
ErrorsExample::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
IErrorsExampleDispatcher { contract_address }
}
#[test]
#[should_panic(expected: ('i must not be 0', 'ENTRYPOINT_FAILED'))]
fn should_panic() {
let contract = deploy();
contract.test_panic(0);
}
#[test]
#[should_panic(expected: ('i must be greater than 0', 'ENTRYPOINT_FAILED'))]
fn should_assert() {
let contract = deploy();
contract.test_assert(0);
}
}
Custom errors
You can make error handling easier by defining your error codes in a specific module.
#[starknet::interface]
pub trait ICustomErrorsExample<TContractState> {
fn test_assert(self: @TContractState, i: u256);
fn test_panic(self: @TContractState, i: u256);
}
pub mod Errors {
pub const NOT_POSITIVE: felt252 = 'must be greater than 0';
pub const NOT_NULL: felt252 = 'must not be null';
}
#[starknet::contract]
pub mod CustomErrorsExample {
use super::Errors;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl CustomErrorsExample of super::ICustomErrorsExample<ContractState> {
fn test_assert(self: @ContractState, i: u256) {
assert(i > 0, Errors::NOT_POSITIVE);
}
fn test_panic(self: @ContractState, i: u256) {
if (i == 0) {
core::panic_with_felt252(Errors::NOT_NULL);
}
}
}
}
#[cfg(test)]
mod test {
use super::{
CustomErrorsExample, ICustomErrorsExampleDispatcher, ICustomErrorsExampleDispatcherTrait
};
use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};
fn deploy() -> ICustomErrorsExampleDispatcher {
let (contract_address, _) = deploy_syscall(
CustomErrorsExample::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
ICustomErrorsExampleDispatcher { contract_address }
}
#[test]
#[should_panic(expected: ('must not be null', 'ENTRYPOINT_FAILED'))]
fn should_panic() {
let contract = deploy();
contract.test_panic(0);
}
#[test]
#[should_panic(expected: ('must be greater than 0', 'ENTRYPOINT_FAILED'))]
fn should_assert() {
let contract = deploy();
contract.test_assert(0);
}
}
Vault example
Here's another example that demonstrates the use of errors in a more complex contract:
#[starknet::interface]
pub trait IVaultErrorsExample<TContractState> {
fn deposit(ref self: TContractState, amount: u256);
fn withdraw(ref self: TContractState, amount: u256);
}
pub mod VaultErrors {
pub const INSUFFICIENT_BALANCE: felt252 = 'insufficient_balance';
// you can define more errors here
}
#[starknet::contract]
pub mod VaultErrorsExample {
use super::VaultErrors;
#[storage]
struct Storage {
balance: u256,
}
#[abi(embed_v0)]
impl VaultErrorsExample of super::IVaultErrorsExample<ContractState> {
fn deposit(ref self: ContractState, amount: u256) {
let mut balance = self.balance.read();
balance = balance + amount;
self.balance.write(balance);
}
fn withdraw(ref self: ContractState, amount: u256) {
let mut balance = self.balance.read();
assert(balance >= amount, VaultErrors::INSUFFICIENT_BALANCE);
// Or using panic:
if (balance < amount) {
core::panic_with_felt252(VaultErrors::INSUFFICIENT_BALANCE);
}
let balance = balance - amount;
self.balance.write(balance);
}
}
}
#[cfg(test)]
mod test {
use super::{
VaultErrorsExample, IVaultErrorsExampleDispatcher, IVaultErrorsExampleDispatcherTrait
};
use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};
fn deploy() -> IVaultErrorsExampleDispatcher {
let (contract_address, _) = deploy_syscall(
VaultErrorsExample::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
IVaultErrorsExampleDispatcher { contract_address }
}
#[test]
fn should_deposit_and_withdraw() {
let mut contract = deploy();
contract.deposit(10);
contract.withdraw(5);
}
#[test]
#[should_panic(expected: ('insufficient_balance', 'ENTRYPOINT_FAILED'))]
fn should_panic_on_insufficient_balance() {
let mut contract = deploy();
contract.deposit(10);
contract.withdraw(15);
}
}
Events
Events are a way to emit data from a contract. All events must be defined in the Event
enum, which must be annotated with the #[event]
attribute.
An event is defined as a struct that derives the #[starknet::Event]
trait. The fields of that struct correspond to the data that will be emitted. An event can be indexed for easy and fast access when querying the data at a later time, by adding a #[key]
attribute to a field member.
Here's a simple example of a contract using events that emit an event each time a counter is incremented by the "increment" function:
#[starknet::interface]
pub trait IEventCounter<TContractState> {
fn increment(ref self: TContractState, amount: u128);
}
#[starknet::contract]
pub mod EventCounter {
use starknet::{get_caller_address, ContractAddress};
#[storage]
struct Storage {
// Counter value
counter: u128,
}
#[event]
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
// The event enum must be annotated with the `#[event]` attribute.
// It must also derive atleast `Drop` and `starknet::Event` traits.
pub enum Event {
CounterIncreased: CounterIncreased,
UserIncreaseCounter: UserIncreaseCounter
}
// By deriving the `starknet::Event` trait, we indicate to the compiler that
// this struct will be used when emitting events.
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct CounterIncreased {
pub amount: u128
}
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct UserIncreaseCounter {
// The `#[key]` attribute indicates that this event will be indexed.
// You can also use `#[flat]` for nested structs.
#[key]
pub user: ContractAddress,
pub new_value: u128,
}
#[abi(embed_v0)]
impl EventCounter of super::IEventCounter<ContractState> {
fn increment(ref self: ContractState, amount: u128) {
self.counter.write(self.counter.read() + amount);
// Emit event
self.emit(Event::CounterIncreased(CounterIncreased { amount }));
self
.emit(
Event::UserIncreaseCounter(
UserIncreaseCounter {
user: get_caller_address(), new_value: self.counter.read()
}
)
);
}
}
}
#[cfg(test)]
mod tests {
use super::{
EventCounter,
EventCounter::{
counterContractMemberStateTrait, Event, CounterIncreased, UserIncreaseCounter
},
IEventCounterDispatcherTrait, IEventCounterDispatcher
};
use starknet::{
ContractAddress, contract_address_const, SyscallResultTrait, syscalls::deploy_syscall
};
use starknet::testing::{set_contract_address, set_account_contract_address};
#[test]
fn test_increment_events() {
let (contract_address, _) = deploy_syscall(
EventCounter::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
let mut contract = IEventCounterDispatcher { contract_address };
let state = EventCounter::contract_state_for_testing();
let amount = 10;
let caller = contract_address_const::<'caller'>();
// fake caller
set_contract_address(caller);
contract.increment(amount);
// set back to the contract for reading state
set_contract_address(contract_address);
assert_eq!(state.counter.read(), amount);
// Notice the order: the first event emitted is the first to be popped.
assert_eq!(
starknet::testing::pop_log(contract_address),
Option::Some(Event::CounterIncreased(CounterIncreased { amount }))
);
assert_eq!(
starknet::testing::pop_log(contract_address),
Option::Some(
Event::UserIncreaseCounter(UserIncreaseCounter { user: caller, new_value: amount })
)
);
}
}
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
- get_execution_info
- call_contract
- deploy
- emit_event
- library_call
- send_message_to_L1
- replace_class
- storage_read
- storage_write
get_block_hash
fn get_block_hash_syscall(block_number: u64) -> SyscallResult<felt252>
Get the hash of the block number block_number
.
Only within the range [first_v0_12_0_block, current_block - 10]
.
get_execution_info
fn get_execution_info_syscall() -> SyscallResult<Box<starknet::info::ExecutionInfo>>
Get information about the current execution context.
The returned ExecutionInfo
is defined as :
#[derive(Copy, Drop, Debug)]
pub struct ExecutionInfo {
pub block_info: Box<BlockInfo>,
pub tx_info: Box<TxInfo>,
pub caller_address: ContractAddress,
pub contract_address: ContractAddress,
pub entry_point_selector: felt252,
}
#[derive(Copy, Drop, Debug, Serde)]
pub struct BlockInfo {
pub block_number: u64,
pub block_timestamp: u64,
pub sequencer_address: ContractAddress,
}
#[derive(Copy, Drop, Debug, Serde)]
pub struct TxInfo {
// The version of the transaction. Always fixed (1)
pub version: felt252,
// The account contract from which this transaction originates.
pub account_contract_address: ContractAddress,
// The max_fee field of the transaction.
pub max_fee: u128,
// The signature of the transaction.
pub signature: Span<felt252>,
// The hash of the transaction.
pub transaction_hash: felt252,
// The identifier of the chain.
// This field can be used to prevent replay of testnet transactions on mainnet.
pub chain_id: felt252,
// The transaction's nonce.
pub nonce: felt252,
// A span of ResourceBounds structs.
pub resource_bounds: Span<ResourceBounds>,
// The tip.
pub tip: u128,
// If specified, the paymaster should pay for the execution of the tx.
// The data includes the address of the paymaster sponsoring the transaction, followed by
// extra data to send to the paymaster.
pub paymaster_data: Span<felt252>,
// The data availability mode for the nonce.
pub nonce_data_availability_mode: u32,
// The data availability mode for the account balance from which fee will be taken.
pub fee_data_availability_mode: u32,
// If nonempty, will contain the required data for deploying and initializing an account
// contract: its class hash, address salt and constructor calldata.
pub account_deployment_data: Span<felt252>,
}
starknet::info
provides helper functions to access the ExecutionInfo
fields in a more convenient way:
get_execution_info() -> Box<ExecutionInfo>
get_caller_address() -> ContractAddress
get_contract_address() -> ContractAddress
get_block_info() -> Box<BlockInfo>
get_tx_info() -> Box<TxInfo>
get_block_timestamp() -> u64
get_block_number() -> u64
call_contract
fn call_contract_syscall(
address: ContractAddress, entry_point_selector: felt252, calldata: Span<felt252>
) -> SyscallResult<Span<felt252>>
Call a contract at address
with the given entry_point_selector
and calldata
.
Failure can't be caught for this syscall, if the call fails, the whole transaction will revert.
This is not the recommended way to call a contract. Instead, use the dispatcher generated from the contract interface as shown in the Calling other contracts.
deploy
fn deploy_syscall(
class_hash: ClassHash,
contract_address_salt: felt252,
calldata: Span<felt252>,
deploy_from_zero: bool,
) -> SyscallResult<(ContractAddress, Span::<felt252>)>
Deploy a new contract of the predeclared class class_hash
with calldata
.
The success result is a tuple containing the deployed contract address and the return value of the constructor.
contract_address_salt
and deploy_from_zero
are used to compute the contract address.
Example of the usage of the deploy
syscall from the Factory pattern:
pub use starknet::{ContractAddress, ClassHash};
#[starknet::interface]
pub trait ICounterFactory<TContractState> {
/// Create a new counter contract from stored arguments
fn create_counter(ref self: TContractState) -> ContractAddress;
/// Create a new counter contract from the given arguments
fn create_counter_at(ref self: TContractState, init_value: u128) -> ContractAddress;
/// Update the argument
fn update_init_value(ref self: TContractState, init_value: u128);
/// Update the class hash of the Counter contract to deploy when creating a new counter
fn update_counter_class_hash(ref self: TContractState, counter_class_hash: ClassHash);
}
#[starknet::contract]
pub mod CounterFactory {
use starknet::{ContractAddress, ClassHash, SyscallResultTrait, syscalls::deploy_syscall};
#[storage]
struct Storage {
/// Store the constructor arguments of the contract to deploy
init_value: u128,
/// Store the class hash of the contract to deploy
counter_class_hash: ClassHash,
}
#[constructor]
fn constructor(ref self: ContractState, init_value: u128, class_hash: ClassHash) {
self.init_value.write(init_value);
self.counter_class_hash.write(class_hash);
}
#[abi(embed_v0)]
impl Factory of super::ICounterFactory<ContractState> {
fn create_counter_at(ref self: ContractState, init_value: u128) -> ContractAddress {
// Contructor arguments
let mut constructor_calldata: Array::<felt252> = array![init_value.into()];
// Contract deployment
let (deployed_address, _) = deploy_syscall(
self.counter_class_hash.read(), 0, constructor_calldata.span(), false
)
.unwrap_syscall();
deployed_address
}
fn create_counter(ref self: ContractState) -> ContractAddress {
self.create_counter_at(self.init_value.read())
}
fn update_init_value(ref self: ContractState, init_value: u128) {
self.init_value.write(init_value);
}
fn update_counter_class_hash(ref self: ContractState, counter_class_hash: ClassHash) {
self.counter_class_hash.write(counter_class_hash);
}
}
}
#[cfg(test)]
mod tests {
use super::{CounterFactory, ICounterFactoryDispatcher, ICounterFactoryDispatcherTrait};
use starknet::{
SyscallResultTrait, ContractAddress, ClassHash, contract_address_const,
syscalls::deploy_syscall
};
// Define a target contract to deploy
mod target {
#[starknet::interface]
pub trait ISimpleCounter<TContractState> {
fn get_current_count(self: @TContractState) -> u128;
fn increment(ref self: TContractState);
fn decrement(ref self: TContractState);
}
#[starknet::contract]
pub mod SimpleCounter {
#[storage]
struct Storage {
// Counter variable
counter: u128,
}
#[constructor]
fn constructor(ref self: ContractState, init_value: u128) {
// Store initial value
self.counter.write(init_value);
}
#[abi(embed_v0)]
impl SimpleCounter of super::ISimpleCounter<ContractState> {
fn get_current_count(self: @ContractState) -> u128 {
self.counter.read()
}
fn increment(ref self: ContractState) {
// Store counter value + 1
let mut counter: u128 = self.counter.read() + 1;
self.counter.write(counter);
}
fn decrement(ref self: ContractState) {
// Store counter value - 1
let mut counter: u128 = self.counter.read() - 1;
self.counter.write(counter);
}
}
}
}
use target::{ISimpleCounterDispatcher, ISimpleCounterDispatcherTrait};
/// Deploy a counter factory contract
fn deploy_factory(
counter_class_hash: ClassHash, init_value: u128
) -> ICounterFactoryDispatcher {
let mut constructor_calldata: Array::<felt252> = array![
init_value.into(), counter_class_hash.into()
];
let (contract_address, _) = deploy_syscall(
CounterFactory::TEST_CLASS_HASH.try_into().unwrap(),
0,
constructor_calldata.span(),
false
)
.unwrap_syscall();
ICounterFactoryDispatcher { contract_address }
}
#[test]
fn test_deploy_counter_constructor() {
let init_value = 10;
let counter_class_hash: ClassHash = target::SimpleCounter::TEST_CLASS_HASH
.try_into()
.unwrap();
let factory = deploy_factory(counter_class_hash, init_value);
let counter_address = factory.create_counter();
let counter = target::ISimpleCounterDispatcher { contract_address: counter_address };
assert_eq!(counter.get_current_count(), init_value);
}
#[test]
fn test_deploy_counter_argument() {
let init_value = 10;
let argument_value = 20;
let counter_class_hash: ClassHash = target::SimpleCounter::TEST_CLASS_HASH
.try_into()
.unwrap();
let factory = deploy_factory(counter_class_hash, init_value);
let counter_address = factory.create_counter_at(argument_value);
let counter = target::ISimpleCounterDispatcher { contract_address: counter_address };
assert_eq!(counter.get_current_count(), argument_value);
}
#[test]
fn test_deploy_multiple() {
let init_value = 10;
let argument_value = 20;
let counter_class_hash: ClassHash = target::SimpleCounter::TEST_CLASS_HASH
.try_into()
.unwrap();
let factory = deploy_factory(counter_class_hash, init_value);
let mut counter_address = factory.create_counter();
let counter_1 = target::ISimpleCounterDispatcher { contract_address: counter_address };
counter_address = factory.create_counter_at(argument_value);
let counter_2 = target::ISimpleCounterDispatcher { contract_address: counter_address };
assert_eq!(counter_1.get_current_count(), init_value);
assert_eq!(counter_2.get_current_count(), argument_value);
}
}
emit_event
fn emit_event_syscall(
keys: Span<felt252>, data: Span<felt252>
) -> SyscallResult<()>
Emit an event with the given keys
and data
.
Example of the usage of the emit_event
syscall from the Events chapter:
#[starknet::interface]
pub trait IEventCounter<TContractState> {
fn increment(ref self: TContractState, amount: u128);
}
#[starknet::contract]
pub mod EventCounter {
use starknet::{get_caller_address, ContractAddress};
#[storage]
struct Storage {
// Counter value
counter: u128,
}
#[event]
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
// The event enum must be annotated with the `#[event]` attribute.
// It must also derive atleast `Drop` and `starknet::Event` traits.
pub enum Event {
CounterIncreased: CounterIncreased,
UserIncreaseCounter: UserIncreaseCounter
}
// By deriving the `starknet::Event` trait, we indicate to the compiler that
// this struct will be used when emitting events.
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct CounterIncreased {
pub amount: u128
}
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct UserIncreaseCounter {
// The `#[key]` attribute indicates that this event will be indexed.
// You can also use `#[flat]` for nested structs.
#[key]
pub user: ContractAddress,
pub new_value: u128,
}
#[abi(embed_v0)]
impl EventCounter of super::IEventCounter<ContractState> {
fn increment(ref self: ContractState, amount: u128) {
self.counter.write(self.counter.read() + amount);
// Emit event
self.emit(Event::CounterIncreased(CounterIncreased { amount }));
self
.emit(
Event::UserIncreaseCounter(
UserIncreaseCounter {
user: get_caller_address(), new_value: self.counter.read()
}
)
);
}
}
}
#[cfg(test)]
mod tests {
use super::{
EventCounter,
EventCounter::{
counterContractMemberStateTrait, Event, CounterIncreased, UserIncreaseCounter
},
IEventCounterDispatcherTrait, IEventCounterDispatcher
};
use starknet::{
ContractAddress, contract_address_const, SyscallResultTrait, syscalls::deploy_syscall
};
use starknet::testing::{set_contract_address, set_account_contract_address};
#[test]
fn test_increment_events() {
let (contract_address, _) = deploy_syscall(
EventCounter::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
let mut contract = IEventCounterDispatcher { contract_address };
let state = EventCounter::contract_state_for_testing();
let amount = 10;
let caller = contract_address_const::<'caller'>();
// fake caller
set_contract_address(caller);
contract.increment(amount);
// set back to the contract for reading state
set_contract_address(contract_address);
assert_eq!(state.counter.read(), amount);
// Notice the order: the first event emitted is the first to be popped.
assert_eq!(
starknet::testing::pop_log(contract_address),
Option::Some(Event::CounterIncreased(CounterIncreased { amount }))
);
assert_eq!(
starknet::testing::pop_log(contract_address),
Option::Some(
Event::UserIncreaseCounter(UserIncreaseCounter { user: caller, new_value: amount })
)
);
}
}
library_call
fn library_call_syscall(
class_hash: ClassHash, function_selector: felt252, calldata: Span<felt252>
) -> SyscallResult<Span<felt252>>
Call the function function_selector
of the class class_hash
with calldata
.
This is analogous to a delegate call in Ethereum, but only a single class is called.
send_message_to_L1
fn send_message_to_l1_syscall(
to_address: felt252, payload: Span<felt252>
) -> SyscallResult<()>
Send a message to the L1 contract at to_address
with the given payload
.
replace_class
fn replace_class_syscall(
class_hash: ClassHash
) -> SyscallResult<()>
Replace the class of the calling contract with the class class_hash
.
This is used for contract upgrades. Here's an example from the Upgradeable Contract:
use starknet::class_hash::ClassHash;
#[starknet::interface]
pub trait IUpgradeableContract<TContractState> {
fn upgrade(ref self: TContractState, impl_hash: ClassHash);
fn version(self: @TContractState) -> u8;
}
#[starknet::contract]
pub mod UpgradeableContract_V0 {
use starknet::class_hash::ClassHash;
use starknet::SyscallResultTrait;
use core::num::traits::Zero;
#[storage]
struct Storage {}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Upgraded: Upgraded
}
#[derive(Drop, starknet::Event)]
struct Upgraded {
implementation: ClassHash
}
#[abi(embed_v0)]
impl UpgradeableContract of super::IUpgradeableContract<ContractState> {
fn upgrade(ref self: ContractState, impl_hash: ClassHash) {
assert(!impl_hash.is_zero(), 'Class hash cannot be zero');
starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall();
self.emit(Event::Upgraded(Upgraded { implementation: impl_hash }))
}
fn version(self: @ContractState) -> u8 {
0
}
}
}
The new class code will only be used for future calls to the contract.
The current transaction containing the replace_class
syscall will continue to use the old class code. (You can explicitly use the new class code by calling call_contract
after the replace_class
syscall in the same transaction)
storage_read
fn storage_read_syscall(
address_domain: u32, address: StorageAddress,
) -> SyscallResult<felt252>
This low-level syscall is used to get the value in the storage of a specific key at address
in the address_domain
.
address_domain
is used to distinguish between data availability modes.
Currently, only mode ONCHAIN
(0
) is supported.
storage_write
fn storage_write_syscall(
address_domain: u32, address: StorageAddress, value: felt252
) -> SyscallResult<()>
Similar to storage_read
, this low-level syscall is used to write the value value
in the storage of a specific key at address
in the address_domain
.
Documentation
Syscalls are defined in starknet::syscall
You can also read the official documentation page for more details.
Strings and ByteArrays
In Cairo, there's no native type for strings. Instead, you can use a single felt252
to store a short string
or a ByteArray
for strings of arbitrary length.
Short strings
Each character is encoded on 8 bits following the ASCII standard, so it's possible to store up to 31 characters in a single felt252
.
Short strings are declared with single quotes, like this: 'Hello, World!'
.
See the Felt section for more information about short strings with the felt252
type.
Notice that any short string only use up to 31 bytes, so it's possible to represent any short string with the
bytes31
.
ByteArray (Long strings)
The ByteArray
struct is used to store strings of arbitrary length. It contain a field data
of type Array<bytes31>
to store a sequence of short strings.
ByteArrays are declared with double quotes, like this: "Hello, World!"
.
They can be stored in the contract's storage and passed as arguments to entrypoints.
#[starknet::interface]
pub trait IMessage<TContractState> {
fn append(ref self: TContractState, str: ByteArray);
fn prepend(ref self: TContractState, str: ByteArray);
}
#[starknet::contract]
pub mod MessageContract {
#[storage]
struct Storage {
message: ByteArray
}
#[constructor]
fn constructor(ref self: ContractState) {
self.message.write("World!");
}
#[abi(embed_v0)]
impl MessageContract of super::IMessage<ContractState> {
fn append(ref self: ContractState, str: ByteArray) {
self.message.write(self.message.read() + str);
}
fn prepend(ref self: ContractState, str: ByteArray) {
self.message.write(str + self.message.read());
}
}
}
#[cfg(test)]
mod tests {
use bytearray::bytearray::{
MessageContract::messageContractMemberStateTrait, MessageContract, IMessage
};
#[test]
#[available_gas(2000000000)]
fn message_contract_tests() {
let mut state = MessageContract::contract_state_for_testing();
state.message.write("World!");
let message = state.message.read();
assert(message == "World!", 'wrong message');
state.append(" Good day, sir!");
assert(state.message.read() == "World! Good day, sir!", 'wrong message (append)');
state.prepend("Hello, ");
assert(state.message.read() == "Hello, World! Good day, sir!", 'wrong message (prepend)');
}
}
Operations
ByteArrays also provide a set of operations that facilitate the manipulation of strings.
Here are the available operations on an instance of ByteArray
:
append(mut other: @ByteArray)
- Append another ByteArray to the current one.append_word(word: felt252, len: usize)
- Append a short string to the ByteArray. You need to ensure thatlen
is at most 31 and thatword
can be converted to abytes31
with maximumlen
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 thatlen
is at most 31 and thatword
can be converted to abytes31
with maximumlen
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
).
Storing Custom Types
While native types can be stored in a contract's storage without any additional work, custom types require a bit more work. This is because at compile time, the compiler does not know how to store custom types in storage. To solve this, we need to implement the Store
trait for our custom type. Hopefully, we can just derive this trait for our custom type - unless it contains arrays or dictionaries.
#[starknet::interface]
pub trait IStoringCustomType<TContractState> {
fn set_person(ref self: TContractState, person: Person);
}
// Deriving the starknet::Store trait
// allows us to store the `Person` struct in the contract's storage.
#[derive(Drop, Serde, Copy, starknet::Store)]
pub struct Person {
pub age: u8,
pub name: felt252
}
#[starknet::contract]
pub mod StoringCustomType {
use super::Person;
#[storage]
pub struct Storage {
pub person: Person
}
#[abi(embed_v0)]
impl StoringCustomType of super::IStoringCustomType<ContractState> {
fn set_person(ref self: ContractState, person: Person) {
self.person.write(person);
}
}
}
#[cfg(test)]
mod tests {
use super::{
IStoringCustomType, StoringCustomType, Person,
StoringCustomType::personContractMemberStateTrait
};
#[test]
fn can_call_set_person() {
let mut state = StoringCustomType::contract_state_for_testing();
let person = Person { age: 10, name: 'Joe' };
state.set_person(person);
let read_person = state.person.read();
assert(person.age == read_person.age, 'wrong age');
assert(person.name == read_person.name, 'wrong name');
}
}
Custom types in entrypoints
Using custom types in entrypoints requires our type to implement the Serde
trait. This is because when calling an entrypoint, the input is sent as an array of felt252
to the entrypoint, and we need to be able to deserialize it into our custom type. Similarly, when returning a custom type from an entrypoint, we need to be able to serialize it into an array of felt252
.
Thankfully, we can just derive the Serde
trait for our custom type.
The purpose is to only show the capability of using custom types as inputs and outputs in contract calls. We are not employing getters and setters for managing the contract's state in this example for simplicity.
#[starknet::interface]
pub trait ISerdeCustomType<TContractState> {
fn person_input(ref self: TContractState, person: Person);
fn person_output(self: @TContractState) -> Person;
}
// Deriving the `Serde` trait allows us to use
// the Person type as an entrypoint parameter and return value
#[derive(Drop, Serde)]
pub struct Person {
pub age: u8,
pub name: felt252
}
#[starknet::contract]
pub mod SerdeCustomType {
use super::Person;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl SerdeCustomType of super::ISerdeCustomType<ContractState> {
fn person_input(ref self: ContractState, person: Person) {}
fn person_output(self: @ContractState) -> Person {
Person { age: 10, name: 'Joe' }
}
}
}
#[cfg(test)]
mod tests {
use super::{
SerdeCustomType, Person, ISerdeCustomTypeDispatcher, ISerdeCustomTypeDispatcherTrait
};
use starknet::{ContractAddress, syscalls::deploy_syscall, SyscallResultTrait};
fn deploy() -> ISerdeCustomTypeDispatcher {
let (contract_address, _) = deploy_syscall(
SerdeCustomType::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
ISerdeCustomTypeDispatcher { contract_address }
}
#[test]
fn should_deploy() {
deploy();
}
#[test]
fn should_get_person_output() {
let contract = deploy();
let expected_person = Person { age: 10, name: 'Joe' };
let received_person = contract.person_output();
let age_received = received_person.age;
let name_received = received_person.name;
assert(age_received == expected_person.age, 'Wrong age value');
assert(name_received == expected_person.name, 'Wrong name value');
}
#[test]
#[available_gas(2000000000)]
fn should_call_person_input() {
let contract = deploy();
let expected_person = Person { age: 10, name: 'Joe' };
contract.person_input(expected_person);
}
}
Documentation
It's important to take the time to document your code. It will helps developers and users to understand the contract and its functionalities.
In Cairo, you can add comments with //
.
Best Practices:
Since Cairo 1, the community has adopted a Rust-like documentation style.
Contract Interface:
In smart contracts, you will often have a trait that defines the contract's interface (with #[starknet::interface]
).
This is the perfect place to include detailed documentation explaining the purpose and functionality of the contract entry points. You can follow this template:
#[starknet::interface]
trait IContract<TContractState> {
/// High-level description of the function
///
/// # Arguments
///
/// * `arg_1` - Description of the argument
/// * `arg_n` - ...
///
/// # Returns
///
/// High-level description of the return value
fn do_something(ref self: TContractState, arg_1: T_arg_1) -> T_return;
}
Keep in mind that this should not describe the implementation details of the function, but rather the high-level purpose and functionality of the contract from the perspective of a user.
Implementation Details:
When writing the logic of the contract, you can add comments to describe the technical implementation details of the functions.
Avoid over-commenting: Comments should provide additional value and clarity.
Deploy and interact with contracts
In this chapter, we will see how to deploy and interact with contracts.
Contract interfaces and Traits generation
Contract interfaces define the structure and behavior of a contract, serving as the contract's public ABI. They list all the function signatures that a contract exposes. For a detailed explanation of interfaces, you can refer to the Cairo Book.
In cairo, to specify the interface you need to define a trait annotated with #[starknet::interface]
and then implement that trait in the contract.
When a function needs to access the contract state, it must have a self
parameter of type ContractState
. This implies that the corresponding function signature in the interface trait must also take a TContractState
type as a parameter. It's important to note that every function in the contract interface must have this self
parameter of type TContractState
.
You can use the #[generate_trait]
attribute to implicitly generate the trait for a specific implementation block. This attribute automatically generates a trait with the same functions as the ones in the implemented block, replacing the self
parameter with a generic TContractState
parameter. However, you will need to annotate the block with the #[abi(per_item)]
attribute, and each function with the appropriate attribute depending on whether it's an external function, a constructor or a l1 handler.
In summary, there's two ways to handle interfaces:
- Explicitly, by defining a trait annoted with
#[starknet::interface]
- Implicitly, by using
#[generate_trait]
combined with the#[abi(per_item)]
attributes, and annotating each function inside the implementation block with the appropriate attribute.
Explicit interface
#[starknet::interface]
pub trait IExplicitInterfaceContract<TContractState> {
fn get_value(self: @TContractState) -> u32;
fn set_value(ref self: TContractState, value: u32);
}
#[starknet::contract]
pub mod ExplicitInterfaceContract {
#[storage]
struct Storage {
value: u32
}
#[abi(embed_v0)]
impl ExplicitInterfaceContract of super::IExplicitInterfaceContract<ContractState> {
fn get_value(self: @ContractState) -> u32 {
self.value.read()
}
fn set_value(ref self: ContractState, value: u32) {
self.value.write(value);
}
}
}
#[cfg(test)]
mod tests {
use super::{
IExplicitInterfaceContract, ExplicitInterfaceContract, IExplicitInterfaceContractDispatcher,
IExplicitInterfaceContractDispatcherTrait
};
use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};
#[test]
fn test_interface() {
let (contract_address, _) = deploy_syscall(
ExplicitInterfaceContract::TEST_CLASS_HASH.try_into().unwrap(),
0,
array![].span(),
false
)
.unwrap_syscall();
let mut contract = IExplicitInterfaceContractDispatcher { contract_address };
let value: u32 = 20;
contract.set_value(value);
let read_value = contract.get_value();
assert_eq!(read_value, value);
}
}
Implicit interface
#[starknet::contract]
pub mod ImplicitInterfaceContract {
#[storage]
struct Storage {
value: u32
}
#[abi(per_item)]
#[generate_trait]
pub impl ImplicitInterfaceContract of IImplicitInterfaceContract {
#[external(v0)]
fn get_value(self: @ContractState) -> u32 {
self.value.read()
}
#[external(v0)]
fn set_value(ref self: ContractState, value: u32) {
self.value.write(value);
}
}
}
#[cfg(test)]
mod tests {
use super::{
ImplicitInterfaceContract, ImplicitInterfaceContract::valueContractMemberStateTrait,
ImplicitInterfaceContract::IImplicitInterfaceContract
};
use starknet::{
ContractAddress, SyscallResultTrait, syscalls::deploy_syscall, testing::set_contract_address
};
#[test]
fn test_interface() {
let (contract_address, _) = deploy_syscall(
ImplicitInterfaceContract::TEST_CLASS_HASH.try_into().unwrap(),
0,
array![].span(),
false
)
.unwrap_syscall();
set_contract_address(contract_address);
let mut state = ImplicitInterfaceContract::contract_state_for_testing();
let value = 42;
state.set_value(value);
let read_value = state.get_value();
assert_eq!(read_value, value);
}
}
Note: You can import an implicitly generated contract interface with
use contract::{GeneratedContractInterface}
. However, theDispatcher
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);
}
}
Calling other contracts
There are two different ways to call other contracts in Cairo.
The easiest way to call other contracts is by using the dispatcher of the contract you want to call. You can read more about Dispatchers in the Cairo Book
The other way is to use the starknet::call_contract_syscall
syscall yourself. However, this method is not recommended and will not be covered in this example.
In order to call other contracts using dispatchers, you will need to define the called contract's interface as a trait annotated with the #[starknet::interface]
attribute, and then import the IContractDispatcher
and IContractDispatcherTrait
items in your contract.
Here's the Callee
contract interface and implementation:
// This will automatically generate ICalleeDispatcher and ICalleeDispatcherTrait
#[starknet::interface]
pub trait ICallee<TContractState> {
fn set_value(ref self: TContractState, value: u128) -> u128;
}
#[starknet::contract]
pub mod Callee {
#[storage]
struct Storage {
value: u128,
}
#[abi(embed_v0)]
impl ICalleeImpl of super::ICallee<ContractState> {
fn set_value(ref self: ContractState, value: u128) -> u128 {
self.value.write(value);
value
}
}
}
#[starknet::interface]
pub trait ICaller<TContractState> {
fn set_value_from_address(
ref self: TContractState, addr: starknet::ContractAddress, value: u128
);
}
#[starknet::contract]
pub mod Caller {
// We need to import the dispatcher of the callee contract
// If you don't have a proper import, you can redefine the interface by yourself
use super::{ICalleeDispatcher, ICalleeDispatcherTrait};
use starknet::ContractAddress;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl ICallerImpl of super::ICaller<ContractState> {
fn set_value_from_address(ref self: ContractState, addr: ContractAddress, value: u128) {
ICalleeDispatcher { contract_address: addr }.set_value(value);
}
}
}
#[cfg(test)]
mod tests {
use super::{
Callee, ICalleeDispatcher, ICalleeDispatcherTrait, Callee::valueContractMemberStateTrait,
Caller, ICallerDispatcher, ICallerDispatcherTrait
};
use starknet::{
ContractAddress, contract_address_const, testing::set_contract_address,
syscalls::deploy_syscall, SyscallResultTrait
};
fn deploy() -> (ICalleeDispatcher, ICallerDispatcher) {
let (address_callee, _) = deploy_syscall(
Callee::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
let (address_caller, _) = deploy_syscall(
Caller::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
(
ICalleeDispatcher { contract_address: address_callee },
ICallerDispatcher { contract_address: address_caller }
)
}
#[test]
fn test_caller() {
let init_value: u128 = 42;
let (callee, caller) = deploy();
caller.set_value_from_address(callee.contract_address, init_value);
let state = Callee::contract_state_for_testing();
set_contract_address(callee.contract_address);
let value_read: u128 = state.value.read();
assert_eq!(value_read, init_value);
}
}
The following Caller
contract use the Callee
interface to call the Callee
contract:
// This will automatically generate ICalleeDispatcher and ICalleeDispatcherTrait
#[starknet::interface]
pub trait ICallee<TContractState> {
fn set_value(ref self: TContractState, value: u128) -> u128;
}
#[starknet::contract]
pub mod Callee {
#[storage]
struct Storage {
value: u128,
}
#[abi(embed_v0)]
impl ICalleeImpl of super::ICallee<ContractState> {
fn set_value(ref self: ContractState, value: u128) -> u128 {
self.value.write(value);
value
}
}
}
#[starknet::interface]
pub trait ICaller<TContractState> {
fn set_value_from_address(
ref self: TContractState, addr: starknet::ContractAddress, value: u128
);
}
#[starknet::contract]
pub mod Caller {
// We need to import the dispatcher of the callee contract
// If you don't have a proper import, you can redefine the interface by yourself
use super::{ICalleeDispatcher, ICalleeDispatcherTrait};
use starknet::ContractAddress;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl ICallerImpl of super::ICaller<ContractState> {
fn set_value_from_address(ref self: ContractState, addr: ContractAddress, value: u128) {
ICalleeDispatcher { contract_address: addr }.set_value(value);
}
}
}
#[cfg(test)]
mod tests {
use super::{
Callee, ICalleeDispatcher, ICalleeDispatcherTrait, Callee::valueContractMemberStateTrait,
Caller, ICallerDispatcher, ICallerDispatcherTrait
};
use starknet::{
ContractAddress, contract_address_const, testing::set_contract_address,
syscalls::deploy_syscall, SyscallResultTrait
};
fn deploy() -> (ICalleeDispatcher, ICallerDispatcher) {
let (address_callee, _) = deploy_syscall(
Callee::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
let (address_caller, _) = deploy_syscall(
Caller::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
(
ICalleeDispatcher { contract_address: address_callee },
ICallerDispatcher { contract_address: address_caller }
)
}
#[test]
fn test_caller() {
let init_value: u128 = 42;
let (callee, caller) = deploy();
caller.set_value_from_address(callee.contract_address, init_value);
let state = Callee::contract_state_for_testing();
set_contract_address(callee.contract_address);
let value_read: u128 = state.value.read();
assert_eq!(value_read, init_value);
}
}
Factory Pattern
The factory pattern is a well known pattern in object oriented programming. It provides an abstraction on how to instantiate a class.
In the case of smart contracts, we can use this pattern by defining a factory contract that have the sole responsibility of creating and managing other contracts.
Class hash and contract instance
In Starknet, there's a separation between contract's classes and instances. A contract class serves as a blueprint, defined by the underling Cairo bytecode, contract's entrypoints, ABI and Sierra program hash. The contract class is identified by a class hash. When you want to add a new class to the network, you first need to declare it.
When deploying a contract, you need to specify the class hash of the contract you want to deploy. Each instance of a contract has their own storage regardless of the class hash.
Using the factory pattern, we can deploy multiple instances of the same contract class and handle upgrades easily.
Minimal example
Here's a minimal example of a factory contract that deploy the SimpleCounter
contract:
pub use starknet::{ContractAddress, ClassHash};
#[starknet::interface]
pub trait ICounterFactory<TContractState> {
/// Create a new counter contract from stored arguments
fn create_counter(ref self: TContractState) -> ContractAddress;
/// Create a new counter contract from the given arguments
fn create_counter_at(ref self: TContractState, init_value: u128) -> ContractAddress;
/// Update the argument
fn update_init_value(ref self: TContractState, init_value: u128);
/// Update the class hash of the Counter contract to deploy when creating a new counter
fn update_counter_class_hash(ref self: TContractState, counter_class_hash: ClassHash);
}
#[starknet::contract]
pub mod CounterFactory {
use starknet::{ContractAddress, ClassHash, SyscallResultTrait, syscalls::deploy_syscall};
#[storage]
struct Storage {
/// Store the constructor arguments of the contract to deploy
init_value: u128,
/// Store the class hash of the contract to deploy
counter_class_hash: ClassHash,
}
#[constructor]
fn constructor(ref self: ContractState, init_value: u128, class_hash: ClassHash) {
self.init_value.write(init_value);
self.counter_class_hash.write(class_hash);
}
#[abi(embed_v0)]
impl Factory of super::ICounterFactory<ContractState> {
fn create_counter_at(ref self: ContractState, init_value: u128) -> ContractAddress {
// Contructor arguments
let mut constructor_calldata: Array::<felt252> = array![init_value.into()];
// Contract deployment
let (deployed_address, _) = deploy_syscall(
self.counter_class_hash.read(), 0, constructor_calldata.span(), false
)
.unwrap_syscall();
deployed_address
}
fn create_counter(ref self: ContractState) -> ContractAddress {
self.create_counter_at(self.init_value.read())
}
fn update_init_value(ref self: ContractState, init_value: u128) {
self.init_value.write(init_value);
}
fn update_counter_class_hash(ref self: ContractState, counter_class_hash: ClassHash) {
self.counter_class_hash.write(counter_class_hash);
}
}
}
#[cfg(test)]
mod tests {
use super::{CounterFactory, ICounterFactoryDispatcher, ICounterFactoryDispatcherTrait};
use starknet::{
SyscallResultTrait, ContractAddress, ClassHash, contract_address_const,
syscalls::deploy_syscall
};
// Define a target contract to deploy
mod target {
#[starknet::interface]
pub trait ISimpleCounter<TContractState> {
fn get_current_count(self: @TContractState) -> u128;
fn increment(ref self: TContractState);
fn decrement(ref self: TContractState);
}
#[starknet::contract]
pub mod SimpleCounter {
#[storage]
struct Storage {
// Counter variable
counter: u128,
}
#[constructor]
fn constructor(ref self: ContractState, init_value: u128) {
// Store initial value
self.counter.write(init_value);
}
#[abi(embed_v0)]
impl SimpleCounter of super::ISimpleCounter<ContractState> {
fn get_current_count(self: @ContractState) -> u128 {
self.counter.read()
}
fn increment(ref self: ContractState) {
// Store counter value + 1
let mut counter: u128 = self.counter.read() + 1;
self.counter.write(counter);
}
fn decrement(ref self: ContractState) {
// Store counter value - 1
let mut counter: u128 = self.counter.read() - 1;
self.counter.write(counter);
}
}
}
}
use target::{ISimpleCounterDispatcher, ISimpleCounterDispatcherTrait};
/// Deploy a counter factory contract
fn deploy_factory(
counter_class_hash: ClassHash, init_value: u128
) -> ICounterFactoryDispatcher {
let mut constructor_calldata: Array::<felt252> = array![
init_value.into(), counter_class_hash.into()
];
let (contract_address, _) = deploy_syscall(
CounterFactory::TEST_CLASS_HASH.try_into().unwrap(),
0,
constructor_calldata.span(),
false
)
.unwrap_syscall();
ICounterFactoryDispatcher { contract_address }
}
#[test]
fn test_deploy_counter_constructor() {
let init_value = 10;
let counter_class_hash: ClassHash = target::SimpleCounter::TEST_CLASS_HASH
.try_into()
.unwrap();
let factory = deploy_factory(counter_class_hash, init_value);
let counter_address = factory.create_counter();
let counter = target::ISimpleCounterDispatcher { contract_address: counter_address };
assert_eq!(counter.get_current_count(), init_value);
}
#[test]
fn test_deploy_counter_argument() {
let init_value = 10;
let argument_value = 20;
let counter_class_hash: ClassHash = target::SimpleCounter::TEST_CLASS_HASH
.try_into()
.unwrap();
let factory = deploy_factory(counter_class_hash, init_value);
let counter_address = factory.create_counter_at(argument_value);
let counter = target::ISimpleCounterDispatcher { contract_address: counter_address };
assert_eq!(counter.get_current_count(), argument_value);
}
#[test]
fn test_deploy_multiple() {
let init_value = 10;
let argument_value = 20;
let counter_class_hash: ClassHash = target::SimpleCounter::TEST_CLASS_HASH
.try_into()
.unwrap();
let factory = deploy_factory(counter_class_hash, init_value);
let mut counter_address = factory.create_counter();
let counter_1 = target::ISimpleCounterDispatcher { contract_address: counter_address };
counter_address = factory.create_counter_at(argument_value);
let counter_2 = target::ISimpleCounterDispatcher { contract_address: counter_address };
assert_eq!(counter_1.get_current_count(), init_value);
assert_eq!(counter_2.get_current_count(), argument_value);
}
}
This factory can be used to deploy multiple instances of the SimpleCounter
contract by calling the create_counter
and create_counter_at
functions.
The SimpleCounter
class hash is stored inside the factory, and can be upgraded with the update_counter_class_hash
function which allows to reuse the same factory contract when the SimpleCounter
contract is upgraded.
This minimal example lacks several useful features such as access control, tracking of deployed contracts, events, ...
Contract Testing
Testing plays a crucial role in software development, especially for smart contracts. In this section, we'll guide you through the basics of testing a smart contract on Starknet with scarb
.
Let's start with a simple smart contract as an example:
#[starknet::interface]
pub trait ISimpleContract<TContractState> {
fn get_value(self: @TContractState) -> u32;
fn get_owner(self: @TContractState) -> starknet::ContractAddress;
fn set_value(ref self: TContractState, value: u32);
}
#[starknet::contract]
pub mod SimpleContract {
use starknet::{get_caller_address, ContractAddress};
#[storage]
struct Storage {
value: u32,
owner: ContractAddress
}
#[constructor]
pub fn constructor(ref self: ContractState, initial_value: u32) {
self.value.write(initial_value);
self.owner.write(get_caller_address());
}
#[abi(embed_v0)]
pub impl SimpleContractImpl of super::ISimpleContract<ContractState> {
fn get_value(self: @ContractState) -> u32 {
self.value.read()
}
fn get_owner(self: @ContractState) -> ContractAddress {
self.owner.read()
}
fn set_value(ref self: ContractState, value: u32) {
assert(self.owner.read() == get_caller_address(), 'Not owner');
self.value.write(value);
}
}
}
Now, take a look at the tests for this contract:
#[cfg(test)]
mod tests {
// Import the interface and dispatcher to be able to interact with the contract.
use super::{SimpleContract, ISimpleContractDispatcher, ISimpleContractDispatcherTrait};
// Import the deploy syscall to be able to deploy the contract.
use starknet::{SyscallResultTrait, syscalls::deploy_syscall};
use starknet::{
ContractAddress, get_caller_address, get_contract_address, contract_address_const
};
// Use starknet test utils to fake the contract_address
use starknet::testing::set_contract_address;
// Deploy the contract and return its dispatcher.
fn deploy(initial_value: u32) -> ISimpleContractDispatcher {
// Declare and deploy
let (contract_address, _) = deploy_syscall(
SimpleContract::TEST_CLASS_HASH.try_into().unwrap(),
0,
array![initial_value.into()].span(),
false
)
.unwrap_syscall();
// Return the dispatcher.
// The dispatcher allows to interact with the contract based on its interface.
ISimpleContractDispatcher { contract_address }
}
#[test]
fn test_deploy() {
let initial_value: u32 = 10;
let contract = deploy(initial_value);
assert_eq!(contract.get_value(), initial_value);
assert_eq!(contract.get_owner(), get_contract_address());
}
#[test]
fn test_set_as_owner() {
// Fake the contract address to owner
let owner = contract_address_const::<'owner'>();
set_contract_address(owner);
// When deploying the contract, the owner is the caller.
let contract = deploy(10);
assert_eq!(contract.get_owner(), owner);
// As the current caller is the owner, the value can be set.
let new_value: u32 = 20;
contract.set_value(new_value);
assert_eq!(contract.get_value(), new_value);
}
#[test]
#[should_panic]
fn test_set_not_owner() {
let owner = contract_address_const::<'owner'>();
set_contract_address(owner);
let contract = deploy(10);
// Fake the contract address to another address
let not_owner = contract_address_const::<'not owner'>();
set_contract_address(not_owner);
// As the current caller is not the owner, the value cannot be set.
let new_value: u32 = 20;
contract.set_value(new_value);
// Panic expected
}
#[test]
#[available_gas(150000)]
fn test_deploy_gas() {
deploy(10);
}
}
To define our test, we use scarb, which allows us to create a separate module guarded with #[cfg(test)]
. This ensures that the test module is only compiled when running tests using scarb test
.
Each test is defined as a function with the #[test]
attribute. You can also check if a test panics using the #[should_panic]
attribute.
As we are in the context of a smart contract, you can also set up the gas limit for a test by using the #[available_gas(X)]
. This is a great way to ensure that some of your contract's features stay under a certain gas limit!
Note: The term "gas" here refers to Sierra gas, not L1 gas
Now, let's move on to the testing process:
- Use the
deploy
function logic to declare and deploy your contract. - Use
assert
to verify that the contract behaves as expected in the given context.- You can also use assertions macros:
assert_eq
,assert_ne
,assert_gt
,assert_ge
,assert_lt
,assert_le
- You can also use assertions macros:
If you didn't noticed yet, every examples in this book have hidden tests, you can see them by clicking on the "Show hidden lines" (eyes icon) on the top right of code blocks. You can also find a detailed explanation of testing in cairo in the Cairo book - Chapter 10.
Using the contract state
You can use the Contract::contract_state_for_testing
function to access the contract state. This function is only available in the test environment and allows you to mutate and read the contract state directly.
This can be useful for testing internal functions, or specific state mutations that are not exposed to the contract's interface. You can either use it with a deployed contract or as a standalone state.
Here is an example of how to do the same previous test using the contract state:
#[cfg(test)]
mod tests_with_states {
// Only import the contract
use super::SimpleContract;
// For accessing storage variables and entrypoints,
// we must import the contract member state traits and implementation.
use SimpleContract::{
SimpleContractImpl, valueContractMemberStateTrait, ownerContractMemberStateTrait
};
use starknet::contract_address_const;
use starknet::testing::set_caller_address;
use core::num::traits::Zero;
#[test]
fn test_standalone_state() {
let mut state = SimpleContract::contract_state_for_testing();
// As no contract was deployed, the constructor was not called on the state
// - with valueContractMemberStateTrait
assert_eq!(state.value.read(), 0);
// - with SimpleContractImpl
assert_eq!(state.get_value(), 0);
assert_eq!(state.owner.read(), Zero::zero());
// We can still directly call the constructor to initialize the state.
let owner = contract_address_const::<'owner'>();
// We are not setting the contract address but the caller address here,
// as we are not deploying the contract but directly calling the constructor function.
set_caller_address(owner);
let initial_value: u32 = 10;
SimpleContract::constructor(ref state, initial_value);
assert_eq!(state.get_value(), initial_value);
assert_eq!(state.get_owner(), owner);
// As the current caller is the owner, the value can be set.
let new_value: u32 = 20;
state.set_value(new_value);
assert_eq!(state.get_value(), new_value);
}
// But we can also deploy the contract and interact with it using the dispatcher
// as shown in the previous tests, and still use the state for testing.
use super::{ISimpleContractDispatcher, ISimpleContractDispatcherTrait};
use starknet::{
ContractAddress, SyscallResultTrait, syscalls::deploy_syscall, testing::set_contract_address
};
#[test]
fn test_state_with_contract() {
let owner = contract_address_const::<'owner'>();
let not_owner = contract_address_const::<'not owner'>();
// Deploy as owner
let initial_value: u32 = 10;
set_contract_address(owner);
let (contract_address, _) = deploy_syscall(
SimpleContract::TEST_CLASS_HASH.try_into().unwrap(),
0,
array![initial_value.into()].span(),
false
)
.unwrap_syscall();
let mut contract = ISimpleContractDispatcher { contract_address };
// create the state
// - Set back as not owner
set_contract_address(not_owner);
let mut state = SimpleContract::contract_state_for_testing();
// - Currently, the state is not 'linked' to the contract
assert_ne!(state.get_value(), initial_value);
assert_ne!(state.get_owner(), owner);
// - Link the state to the contract by setting the contract address
set_contract_address(contract.contract_address);
assert_eq!(state.get_value(), initial_value);
assert_eq!(state.get_owner(), owner);
// Mutating the state from the contract change the testing state
set_contract_address(owner);
let new_value: u32 = 20;
contract.set_value(new_value);
set_contract_address(contract.contract_address);
assert_eq!(state.get_value(), new_value);
// Mutating the state from the testing state change the contract state
set_caller_address(owner);
state.set_value(initial_value);
assert_eq!(contract.get_value(), initial_value);
// Directly mutating the state allows to change state
// in ways that are not allowed by the contract, such as changing the owner.
let new_owner = contract_address_const::<'new owner'>();
state.owner.write(new_owner);
assert_eq!(contract.get_owner(), new_owner);
set_caller_address(new_owner);
state.set_value(new_value);
assert_eq!(contract.get_value(), new_value);
}
}
Testing events
In order to test events, you need to use the starknet::pop_log
function. If the contract did not emit any events, the function will return Option::None
.
See the test for the Events section:
#[starknet::interface]
pub trait IEventCounter<TContractState> {
fn increment(ref self: TContractState, amount: u128);
}
#[starknet::contract]
pub mod EventCounter {
use starknet::{get_caller_address, ContractAddress};
#[storage]
struct Storage {
// Counter value
counter: u128,
}
#[event]
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
// The event enum must be annotated with the `#[event]` attribute.
// It must also derive atleast `Drop` and `starknet::Event` traits.
pub enum Event {
CounterIncreased: CounterIncreased,
UserIncreaseCounter: UserIncreaseCounter
}
// By deriving the `starknet::Event` trait, we indicate to the compiler that
// this struct will be used when emitting events.
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct CounterIncreased {
pub amount: u128
}
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct UserIncreaseCounter {
// The `#[key]` attribute indicates that this event will be indexed.
// You can also use `#[flat]` for nested structs.
#[key]
pub user: ContractAddress,
pub new_value: u128,
}
#[abi(embed_v0)]
impl EventCounter of super::IEventCounter<ContractState> {
fn increment(ref self: ContractState, amount: u128) {
self.counter.write(self.counter.read() + amount);
// Emit event
self.emit(Event::CounterIncreased(CounterIncreased { amount }));
self
.emit(
Event::UserIncreaseCounter(
UserIncreaseCounter {
user: get_caller_address(), new_value: self.counter.read()
}
)
);
}
}
}
#[cfg(test)]
mod tests {
use super::{
EventCounter,
EventCounter::{
counterContractMemberStateTrait, Event, CounterIncreased, UserIncreaseCounter
},
IEventCounterDispatcherTrait, IEventCounterDispatcher
};
use starknet::{
ContractAddress, contract_address_const, SyscallResultTrait, syscalls::deploy_syscall
};
use starknet::testing::{set_contract_address, set_account_contract_address};
#[test]
fn test_increment_events() {
let (contract_address, _) = deploy_syscall(
EventCounter::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
let mut contract = IEventCounterDispatcher { contract_address };
let state = EventCounter::contract_state_for_testing();
let amount = 10;
let caller = contract_address_const::<'caller'>();
// fake caller
set_contract_address(caller);
contract.increment(amount);
// set back to the contract for reading state
set_contract_address(contract_address);
assert_eq!(state.counter.read(), amount);
// Notice the order: the first event emitted is the first to be popped.
assert_eq!(
starknet::testing::pop_log(contract_address),
Option::Some(Event::CounterIncreased(CounterIncreased { amount }))
);
assert_eq!(
starknet::testing::pop_log(contract_address),
Option::Some(
Event::UserIncreaseCounter(UserIncreaseCounter { user: caller, new_value: amount })
)
);
}
}
Starknet Corelib Testing Module
To make testing more convenient, the testing
module of the corelib provides some helpful functions:
set_caller_address(address: ContractAddress)
set_contract_address(address: ContractAddress)
set_block_number(block_number: u64)
set_block_timestamp(block_timestamp: u64)
set_account_contract_address(address: ContractAddress)
set_sequencer_address(address: ContractAddress)
set_version(version: felt252)
set_transaction_hash(hash: felt252)
set_chain_id(chain_id: felt252)
set_nonce(nonce: felt252)
set_signature(signature: felt252)
set_max_fee(fee: u128)
pop_log_raw(address: ContractAddress) -> Option<(Span<felt252>, Span<felt252>)>
pop_log<T, +starknet::Event<T>>(address: ContractAddress) -> Option<T>
pop_l2_to_l1_message(address: ContractAddress) -> Option<(felt252, Span<felt252>)>
You may also need the info
module from the corelib, which allows you to access information about the current execution context (see syscalls):
get_caller_address() -> ContractAddress
get_contract_address() -> ContractAddress
get_block_info() -> Box<BlockInfo>
get_tx_info() -> Box<TxInfo>
get_block_timestamp() -> u64
get_block_number() -> u64
You can found the full list of functions in the Starknet Corelib repo.
Starknet Foundry
Starknet Foundry is a powerful toolkit for developing smart contracts on Starknet. It offers support for testing Starknet smart contracts on top of scarb
with the Forge
tool.
Testing with snforge
is similar to the process we just described but simplified. Moreover, additional features are on the way, including cheatcodes or parallel tests execution. We highly recommend exploring Starknet Foundry and incorporating it into your projects.
For more detailed information about testing contracts with Starknet Foundry, check out the Starknet Foundry Book - Testing Contracts.
Cairo Cheatsheet
This chapter aims to provide a quick reference for the most common Cairo constructs.
Felt252
Felt252 is a fundamental data type in Cairo from which all other data types are derived. Felt252 can also be used to store short string representations with a maximum length of 31 characters.
For example:
let felt: felt252 = 100;
let felt_as_str = 'Hello Starknet!';
let _felt = felt + felt_as_str;
Mapping
The LegacyMap
type can be used to represent a collection of key-value.
use starknet::ContractAddress;
#[starknet::interface]
trait IMappingExample<TContractState> {
fn register_user(ref self: TContractState, student_add: ContractAddress, studentName: felt252);
fn record_student_score(
ref self: TContractState, student_add: ContractAddress, subject: felt252, score: u16
);
fn view_student_name(self: @TContractState, student_add: ContractAddress) -> felt252;
fn view_student_score(
self: @TContractState, student_add: ContractAddress, subject: felt252
) -> u16;
}
#[starknet::contract]
mod MappingContract {
use starknet::ContractAddress;
#[storage]
struct Storage {
students_name: LegacyMap::<ContractAddress, felt252>,
students_result_record: LegacyMap::<(ContractAddress, felt252), u16>,
}
#[abi(embed_v0)]
impl External of super::IMappingExample<ContractState> {
fn register_user(
ref self: ContractState, student_add: ContractAddress, studentName: felt252
) {
self.students_name.write(student_add, studentName);
}
fn record_student_score(
ref self: ContractState, student_add: ContractAddress, subject: felt252, score: u16
) {
self.students_result_record.write((student_add, subject), score);
}
fn view_student_name(self: @ContractState, student_add: ContractAddress) -> felt252 {
self.students_name.read(student_add)
}
fn view_student_score(
self: @ContractState, student_add: ContractAddress, subject: felt252
) -> u16 {
// for a 2D mapping its important to take note of the amount of brackets being used.
self.students_result_record.read((student_add, subject))
}
}
}
Arrays
Arrays are collections of elements of the same type.
The possible operations on arrays are defined with the array::ArrayTrait
of the corelib:
trait ArrayTrait<T> {
fn new() -> Array<T>;
fn append(ref self: Array<T>, value: T);
fn pop_front(ref self: Array<T>) -> Option<T> nopanic;
fn pop_front_consume(self: Array<T>) -> Option<(Array<T>, T)> nopanic;
fn get(self: @Array<T>, index: usize) -> Option<Box<@T>>;
fn at(self: @Array<T>, index: usize) -> @T;
fn len(self: @Array<T>) -> usize;
fn is_empty(self: @Array<T>) -> bool;
fn span(self: @Array<T>) -> Span<T>;
}
For example:
fn array() -> bool {
let mut arr = array![];
arr.append(10);
arr.append(20);
arr.append(30);
assert(arr.len() == 3, 'array length should be 3');
let first_value = arr.pop_front().unwrap();
assert(first_value == 10, 'first value should match');
let second_value = *arr.at(0);
assert(second_value == 20, 'second value should match');
// Returns true if an array is empty, then false if it isn't.
arr.is_empty()
}
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
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
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
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
Match
The "match" expression in Cairo allows us to control the flow of our code by comparing a felt data type or an enum against various patterns and then running specific code based on the pattern that matches. For example:
#[derive(Drop, Serde)]
enum Colour {
Red,
Blue,
Green,
Orange,
Black
}
#[derive(Drop, Serde)]
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> felt252 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn specified_colour(colour: Colour) -> felt252 {
let mut response: felt252 = '';
match colour {
Colour::Red => { response = 'You passed in Red'; },
Colour::Blue => { response = 'You passed in Blue'; },
Colour::Green => { response = 'You passed in Green'; },
Colour::Orange => { response = 'You passed in Orange'; },
Colour::Black => { response = 'You passed in Black'; },
};
response
}
fn quiz(num: felt252) -> felt252 {
let mut response: felt252 = '';
match num {
0 => { response = 'You failed' },
_ => { response = 'You Passed' },
};
response
}
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;
Struct
A struct is a data type similar to tuple. Like tuples they can be used to hold data of different types. For example:
// With Store, you can store Data's structs in the storage part of contracts.
#[derive(Drop, starknet::Store)]
struct Data {
address: starknet::ContractAddress,
age: u8
}
Type casting
Cairo supports the conversion from one scalar types to another by using the into and try_into methods.
traits::Into
is used for conversion from a smaller data type to a larger data type, while traits::TryInto
is used when converting from a larger to a smaller type that might not fit.
For example:
let a_number: u32 = 15;
let my_felt252 = 15;
// Since a u32 might not fit in a u8 and a u16, we need to use try_into,
// then unwrap the Option<T> type thats returned.
let _new_u8: u8 = a_number.try_into().unwrap();
let new_u16: u16 = a_number.try_into().unwrap();
// since new_u32 is the of the same type (u32) as rand_number, we can directly assign them,
// or use the .into() method.
let _new_u32: u32 = a_number;
// When typecasting from a smaller size to an equal or larger size we use the .into() method.
// Note: u64 and u128 are larger than u32, so a u32 type will always fit into them.
let _new_u64: u64 = a_number.into();
let _new_u128: u128 = a_number.into();
// Since a felt252 is smaller than a u256, we can use the into() method
let _new_u256: u256 = my_felt252.into();
let _new_felt252: felt252 = new_u16.into();
//note a usize is smaller than a felt so we use the try_into
let _new_usize: usize = my_felt252.try_into().unwrap();
Components How-To
Components are like modular addons that can be snapped into contracts to add reusable logic, storage, and events. They are used to separate the core logic from common functionalities, simplifying the contract's code and making it easier to read and maintain. It also reduces the risk of bugs and vulnerabilities by using well-tested components.
Key characteristics:
- Modularity: Easily pluggable into multiple contracts.
- Reusable Logic: Encapsulates specific functionalities.
- Not Standalone: Cannot be declared or deployed independently.
How to create a component
The following example shows a simple Switchable
component that can be used to add a switch that can be either on or off.
It contains a storage variable switchable_value
, a function switch
and an event Switch
.
It is a good practice to prefix the component storage variables with the component name to avoid collisions.
#[starknet::interface]
pub trait ISwitchable<TContractState> {
fn is_on(self: @TContractState) -> bool;
fn switch(ref self: TContractState);
}
#[starknet::component]
pub mod switchable_component {
#[storage]
struct Storage {
switchable_value: bool,
}
#[derive(Drop, starknet::Event)]
struct SwitchEvent {}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
SwitchEvent: SwitchEvent,
}
#[embeddable_as(Switchable)]
impl SwitchableImpl<
TContractState, +HasComponent<TContractState>
> of super::ISwitchable<ComponentState<TContractState>> {
fn is_on(self: @ComponentState<TContractState>) -> bool {
self.switchable_value.read()
}
fn switch(ref self: ComponentState<TContractState>) {
self.switchable_value.write(!self.switchable_value.read());
self.emit(Event::SwitchEvent(SwitchEvent {}));
}
}
#[generate_trait]
pub impl SwitchableInternalImpl<
TContractState, +HasComponent<TContractState>
> of SwitchableInternalTrait<TContractState> {
fn _off(ref self: ComponentState<TContractState>) {
self.switchable_value.write(false);
}
}
}
A component in itself is really similar to a contract, it can also have:
- An interface defining entrypoints (
ISwitchableComponent<TContractState>
) - A Storage struct
- Events
- Internal functions
It don't have a constructor, but you can create a _init
internal function and call it from the contract's constructor. In the previous example, the _off
function is used this way.
It's currently not possible to use the same component multiple times in the same contract. This is a known limitation that may be lifted in the future.
For now, you can view components as an implementation of a specific interface/feature (
Ownable
,Upgradeable
, ...~able
). This is why we called itSwitchable
and notSwitch
; The contract is switchable, not has a switch.
How to use a component
Now that we have a component, we can use it in a contract.
The following contract incorporates the Switchable
component:
#[starknet::contract]
pub mod SwitchContract {
use components::switchable::switchable_component;
component!(path: switchable_component, storage: switch, event: SwitchableEvent);
#[abi(embed_v0)]
impl SwitchableImpl = switchable_component::Switchable<ContractState>;
impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
switch: switchable_component::Storage,
}
#[constructor]
fn constructor(ref self: ContractState) {
self.switch._off();
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
SwitchableEvent: switchable_component::Event,
}
}
#[cfg(test)]
mod tests {
use components::switchable::switchable_component::SwitchableInternalTrait;
use components::switchable::ISwitchable;
use starknet::storage::StorageMemberAccessTrait;
use super::SwitchContract;
fn STATE() -> SwitchContract::ContractState {
SwitchContract::contract_state_for_testing()
}
#[test]
#[available_gas(2000000)]
fn test_init() {
let state = STATE();
assert(state.is_on() == false, 'The switch should be off');
}
#[test]
#[available_gas(2000000)]
fn test_switch() {
let mut state = STATE();
state.switch();
assert(state.is_on() == true, 'The switch should be on');
state.switch();
assert(state.is_on() == false, 'The switch should be off');
}
#[test]
#[available_gas(2000000)]
fn test_value() {
let mut state = STATE();
assert(state.is_on() == state.switch.switchable_value.read(), 'Wrong value');
state.switch.switch();
assert(state.is_on() == state.switch.switchable_value.read(), 'Wrong value');
}
#[test]
#[available_gas(2000000)]
fn test_internal_off() {
let mut state = STATE();
state.switch._off();
assert(state.is_on() == false, 'The switch should be off');
state.switch();
state.switch._off();
assert(state.is_on() == false, 'The switch should be off');
}
}
Deep dive into components
You can find more in-depth information about components in the Cairo book - Components.
Components Dependencies
A component with a dependency on a trait T can be used in a contract as long as the contract implements the trait T.
We will use a new Countable
component as an example:
#[starknet::interface]
pub trait ICountable<TContractState> {
fn get(self: @TContractState) -> u32;
fn increment(ref self: TContractState);
}
#[starknet::component]
pub mod countable_component {
#[storage]
struct Storage {
countable_value: u32,
}
#[embeddable_as(Countable)]
impl CountableImpl<
TContractState, +HasComponent<TContractState>
> of super::ICountable<ComponentState<TContractState>> {
fn get(self: @ComponentState<TContractState>) -> u32 {
self.countable_value.read()
}
fn increment(ref self: ComponentState<TContractState>) {
self.countable_value.write(self.countable_value.read() + 1);
}
}
}
We want to add a way to enable or disable the counter, in a way that calling increment
on a disabled counter will not increment the counter.
But we don't want to add this switch logic to the Countable
component itself.
We instead add the trait Switchable
as a dependency to the Countable
component.
Implementation of the trait in the contract
We first define the ISwitchable
trait:
pub trait ISwitchable<TContractState> {
fn is_on(self: @TContractState) -> bool;
fn switch(ref self: TContractState);
}
Then we can modify the implementation of the Countable
component to depend on the ISwitchable
trait:
#[starknet::component]
pub mod countable_component {
use components::countable::ICountable;
use components::switchable::ISwitchable;
#[storage]
struct Storage {
countable_value: u32,
}
#[embeddable_as(Countable)]
impl CountableImpl<
TContractState, +HasComponent<TContractState>, +ISwitchable<TContractState>
> of ICountable<ComponentState<TContractState>> {
fn get(self: @ComponentState<TContractState>) -> u32 {
self.countable_value.read()
}
fn increment(ref self: ComponentState<TContractState>) {
if (self.get_contract().is_on()) {
self.countable_value.write(self.countable_value.read() + 1);
}
}
}
}
A contract that uses the Countable
component must implement the ISwitchable
trait:
#[starknet::contract]
mod CountableContract {
use components_dependencies::countable_dep_switch::countable_component;
use components::switchable::ISwitchable;
component!(path: countable_component, storage: counter, event: CountableEvent);
#[abi(embed_v0)]
impl CountableImpl = countable_component::Countable<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
counter: countable_component::Storage,
switch: bool
}
// Implementation of the dependency:
#[abi(embed_v0)]
impl Switchable of ISwitchable<ContractState> {
fn switch(ref self: ContractState) {
self.switch.write(!self.switch.read());
}
fn is_on(self: @ContractState) -> bool {
self.switch.read()
}
}
#[constructor]
fn constructor(ref self: ContractState) {
self.switch.write(false);
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
CountableEvent: countable_component::Event,
}
}
#[cfg(test)]
mod tests {
use super::CountableContract;
use components::countable::{ICountable, ICountableDispatcher, ICountableDispatcherTrait};
use components::switchable::{ISwitchable, ISwitchableDispatcher, ISwitchableDispatcherTrait};
use starknet::storage::StorageMemberAccessTrait;
use starknet::SyscallResultTrait;
use starknet::syscalls::deploy_syscall;
fn deploy() -> (ICountableDispatcher, ISwitchableDispatcher) {
let (contract_address, _) = deploy_syscall(
CountableContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
(ICountableDispatcher { contract_address }, ISwitchableDispatcher { contract_address },)
}
#[test]
#[available_gas(2000000)]
fn test_init() {
let (mut counter, mut switch) = deploy();
assert(counter.get() == 0, 'Counter != 0');
assert(switch.is_on() == false, 'Switch != false');
}
#[test]
#[available_gas(2000000)]
fn test_increment_switch_off() {
let (mut counter, mut switch) = deploy();
counter.increment();
assert(counter.get() == 0, 'Counter incremented');
assert(switch.is_on() == false, 'Switch != false');
}
#[test]
#[available_gas(2000000)]
fn test_increment_switch_on() {
let (mut counter, mut switch) = deploy();
switch.switch();
assert(switch.is_on() == true, 'Switch != true');
counter.increment();
assert(counter.get() == 1, 'Counter did not increment');
}
#[test]
#[available_gas(2000000)]
fn test_increment_multiple_switches() {
let (mut counter, mut switch) = deploy();
switch.switch();
counter.increment();
counter.increment();
counter.increment();
assert(counter.get() == 3, 'Counter did not increment');
switch.switch();
counter.increment();
counter.increment();
counter.increment();
switch.switch();
counter.increment();
counter.increment();
counter.increment();
assert(counter.get() == 6, 'Counter did not increment');
}
}
Implementation of the trait in another component
In the previous example, we implemented the ISwitchable
trait in the contract.
We already implemented a Switchable
component that provide an implementation of the ISwitchable
trait.
By using the Switchable
component in a contract, we embed the implementation of the ISwitchable
trait in the contract and resolve the dependency on the ISwitchable
trait.
#[starknet::contract]
mod CountableContract {
use components_dependencies::countable_dep_switch::countable_component;
use components::switchable::switchable_component;
component!(path: countable_component, storage: counter, event: CountableEvent);
component!(path: switchable_component, storage: switch, event: SwitchableEvent);
#[abi(embed_v0)]
impl CountableImpl = countable_component::Countable<ContractState>;
#[abi(embed_v0)]
impl SwitchableImpl = switchable_component::Switchable<ContractState>;
impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
counter: countable_component::Storage,
#[substorage(v0)]
switch: switchable_component::Storage
}
#[constructor]
fn constructor(ref self: ContractState) {
self.switch._off();
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
CountableEvent: countable_component::Event,
SwitchableEvent: switchable_component::Event,
}
}
#[cfg(test)]
mod tests {
use super::CountableContract;
use components::countable::{ICountable, ICountableDispatcher, ICountableDispatcherTrait};
use components::switchable::{ISwitchable, ISwitchableDispatcher, ISwitchableDispatcherTrait};
use starknet::storage::StorageMemberAccessTrait;
use starknet::SyscallResultTrait;
use starknet::syscalls::deploy_syscall;
fn deploy() -> (ICountableDispatcher, ISwitchableDispatcher) {
let (contract_address, _) = deploy_syscall(
CountableContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
(ICountableDispatcher { contract_address }, ISwitchableDispatcher { contract_address },)
}
#[test]
#[available_gas(2000000)]
fn test_init() {
let (mut counter, mut switch) = deploy();
assert(counter.get() == 0, 'Counter != 0');
assert(switch.is_on() == false, 'Switch != false');
}
#[test]
#[available_gas(2000000)]
fn test_increment_switch_off() {
let (mut counter, mut switch) = deploy();
counter.increment();
assert(counter.get() == 0, 'Counter incremented');
assert(switch.is_on() == false, 'Switch != false');
}
#[test]
#[available_gas(2000000)]
fn test_increment_switch_on() {
let (mut counter, mut switch) = deploy();
switch.switch();
assert(switch.is_on() == true, 'Switch != true');
counter.increment();
assert(counter.get() == 1, 'Counter did not increment');
}
#[test]
#[available_gas(3000000)]
fn test_increment_multiple_switches() {
let (mut counter, mut switch) = deploy();
switch.switch();
counter.increment();
counter.increment();
counter.increment();
assert(counter.get() == 3, 'Counter did not increment');
switch.switch();
counter.increment();
counter.increment();
counter.increment();
switch.switch();
counter.increment();
counter.increment();
counter.increment();
assert(counter.get() == 6, 'Counter did not increment');
}
}
Dependency on other components internal functions
The previous example shows how to use the ISwitchable
trait implementation from the Switchable
component inside the Countable
component by embedding the implementation in the contract.
However, suppose we would like to turn off the switch after each increment. There's no set
function in the ISwitchable
trait, so we can't do it directly.
But the Switchable component implements the internal function _off
from the SwitchableInternalTrait
that set the switch to false
.
We can't embed SwitchableInternalImpl
, but we can add switchable::HasComponent<TContractState>
as a dependency inside CountableImpl
.
We make the Countable
component depend on the Switchable
component.
This will allow to do switchable::ComponentState<TContractState>
-> TContractState
-> countable::ComponentState<TcontractState>
and access the internal functions of the Switchable
component inside the Countable
component:
#[starknet::component]
pub mod countable_component {
use components::countable::ICountable;
use components::switchable::ISwitchable;
// Explicitly depends on a component and not a trait
use components::switchable::switchable_component;
use switchable_component::{SwitchableInternalImpl, SwitchableInternalTrait};
#[storage]
struct Storage {
countable_value: u32,
}
#[generate_trait]
impl GetSwitchable<
TContractState,
+HasComponent<TContractState>,
+switchable_component::HasComponent<TContractState>,
+Drop<TContractState>
> of GetSwitchableTrait<TContractState> {
fn get_switchable(
self: @ComponentState<TContractState>
) -> @switchable_component::ComponentState<TContractState> {
let contract = self.get_contract();
switchable_component::HasComponent::<TContractState>::get_component(contract)
}
fn get_switchable_mut(
ref self: ComponentState<TContractState>
) -> switchable_component::ComponentState<TContractState> {
let mut contract = self.get_contract_mut();
switchable_component::HasComponent::<TContractState>::get_component_mut(ref contract)
}
}
#[embeddable_as(Countable)]
impl CountableImpl<
TContractState,
+HasComponent<TContractState>,
+ISwitchable<TContractState>,
+switchable_component::HasComponent<TContractState>,
+Drop<TContractState>
> of ICountable<ComponentState<TContractState>> {
fn get(self: @ComponentState<TContractState>) -> u32 {
self.countable_value.read()
}
fn increment(ref self: ComponentState<TContractState>) {
if (self.get_contract().is_on()) {
self.countable_value.write(self.countable_value.read() + 1);
// use the switchable component internal function
let mut switch = self.get_switchable_mut();
switch._off();
}
}
}
}
The contract remains the same that the previous example, but the implementation of the Countable
component is different:
#[starknet::contract]
pub mod CountableContract {
use components_dependencies::countable_internal_dep_switch::countable_component;
use components::switchable::switchable_component;
component!(path: countable_component, storage: counter, event: CountableEvent);
component!(path: switchable_component, storage: switch, event: SwitchableEvent);
#[abi(embed_v0)]
impl CountableImpl = countable_component::Countable<ContractState>;
#[abi(embed_v0)]
impl SwitchableImpl = switchable_component::Switchable<ContractState>;
impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
counter: countable_component::Storage,
#[substorage(v0)]
switch: switchable_component::Storage
}
#[constructor]
fn constructor(ref self: ContractState) {
self.switch._off();
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
CountableEvent: countable_component::Event,
SwitchableEvent: switchable_component::Event,
}
}
#[cfg(test)]
mod tests {
use super::CountableContract;
use components::countable::{ICountable, ICountableDispatcher, ICountableDispatcherTrait};
use components::switchable::{ISwitchable, ISwitchableDispatcher, ISwitchableDispatcherTrait};
use starknet::storage::StorageMemberAccessTrait;
use starknet::SyscallResultTrait;
use starknet::syscalls::deploy_syscall;
fn deploy() -> (ICountableDispatcher, ISwitchableDispatcher) {
let (contract_address, _) = deploy_syscall(
CountableContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
(ICountableDispatcher { contract_address }, ISwitchableDispatcher { contract_address },)
}
#[test]
#[available_gas(2000000)]
fn test_init() {
let (mut counter, mut switch) = deploy();
assert(counter.get() == 0, 'Counter != 0');
assert(switch.is_on() == false, 'Switch != false');
}
#[test]
#[available_gas(2000000)]
fn test_increment_switch_off() {
let (mut counter, mut switch) = deploy();
counter.increment();
assert(counter.get() == 0, 'Counter incremented');
assert(switch.is_on() == false, 'Switch != false');
}
#[test]
#[available_gas(2000000)]
fn test_increment_switch_on() {
let (mut counter, mut switch) = deploy();
switch.switch();
assert(switch.is_on() == true, 'Switch != true');
counter.increment();
assert(counter.get() == 1, 'Counter did not increment');
// The counter turned the switch off.
assert(switch.is_on() == false, 'Switch != false');
}
#[test]
#[available_gas(3000000)]
fn test_increment_multiple_switches() {
let (mut counter, mut switch) = deploy();
switch.switch();
counter.increment();
counter.increment(); // off
counter.increment(); // off
assert(counter.get() == 1, 'Counter did not increment');
switch.switch();
counter.increment();
switch.switch();
counter.increment();
counter.increment();
assert(counter.get() == 3, 'Counter did not increment');
}
}
Component-Contract Storage Collisions
Components can declare their own storage variables.
When a contract use a component, the component storage is merged with the contract storage. The storage layout is only determined by the variables names, so variables with the same name will collide.
In a future release, the
#[substorage(v1)]
will determine the storage layout based on the component as well, so collisions will be avoided.
A good practice is to prefix the component storage variables with the component name, as shown in the Switchable component example.
Example
Here's an example of a collision on the switchable_value
storage variable of the Switchable
component.
Interface:
#[starknet::interface]
pub trait ISwitchCollision<TContractState> {
fn set(ref self: TContractState, value: bool);
fn get(ref self: TContractState) -> bool;
}
Here's the storage of the contract (you can expand the code snippet to see the full contract):
#[starknet::interface]
pub trait ISwitchCollision<TContractState> {
fn set(ref self: TContractState, value: bool);
fn get(ref self: TContractState) -> bool;
}
#[starknet::contract]
pub mod SwitchCollisionContract {
use components::switchable::switchable_component;
component!(path: switchable_component, storage: switch, event: SwitchableEvent);
#[abi(embed_v0)]
impl SwitchableImpl = switchable_component::Switchable<ContractState>;
impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl<ContractState>;
#[storage]
struct Storage {
switchable_value: bool,
#[substorage(v0)]
switch: switchable_component::Storage,
}
#[constructor]
fn constructor(ref self: ContractState) {
self.switch._off();
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
SwitchableEvent: switchable_component::Event,
}
#[abi(embed_v0)]
impl SwitchCollisionContract of super::ISwitchCollision<ContractState> {
fn set(ref self: ContractState, value: bool) {
self.switchable_value.write(value);
}
fn get(ref self: ContractState) -> bool {
self.switchable_value.read()
}
}
}
Both the contract and the component have a switchable_value
storage variable, so they collide:
mod switch_collision_tests {
use components::switchable::switchable_component::SwitchableInternalTrait;
use components::switchable::{ISwitchable, ISwitchableDispatcher, ISwitchableDispatcherTrait};
use components::contracts::switch_collision::{
SwitchCollisionContract, ISwitchCollisionDispatcher, ISwitchCollisionDispatcherTrait
};
use starknet::storage::StorageMemberAccessTrait;
use starknet::SyscallResultTrait;
use starknet::syscalls::deploy_syscall;
fn deploy() -> (ISwitchCollisionDispatcher, ISwitchableDispatcher) {
let (contract_address, _) = deploy_syscall(
SwitchCollisionContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
(
ISwitchCollisionDispatcher { contract_address },
ISwitchableDispatcher { contract_address },
)
}
#[test]
#[available_gas(2000000)]
fn test_collision() {
let (mut contract, mut contract_iswitch) = deploy();
assert(contract.get() == false, 'value !off');
assert(contract_iswitch.is_on() == false, 'switch !off');
contract_iswitch.switch();
assert(contract_iswitch.is_on() == true, 'switch !on');
assert(contract.get() == true, 'value !on');
// `collision` between component storage 'value' and contract storage 'value'
assert(contract.get() == contract_iswitch.is_on(), 'value != switch');
contract.set(false);
assert(contract.get() == contract_iswitch.is_on(), 'value != switch');
}
}
Ownable
The following Ownable
component is a simple component that allows the contract to set an owner and provides a _assert_is_owner
function that can be used to ensure that the caller is the owner.
It can also be used to renounce ownership of a contract, meaning that no one will be able to satisfy the _assert_is_owner
function.
use starknet::ContractAddress;
#[starknet::interface]
pub trait IOwnable<TContractState> {
fn owner(self: @TContractState) -> ContractAddress;
fn transfer_ownership(ref self: TContractState, new: ContractAddress);
fn renounce_ownership(ref self: TContractState);
}
mod Errors {
pub const UNAUTHORIZED: felt252 = 'Not owner';
pub const ZERO_ADDRESS_OWNER: felt252 = 'Owner cannot be zero';
pub const ZERO_ADDRESS_CALLER: felt252 = 'Caller cannot be zero';
}
#[starknet::component]
pub mod ownable_component {
use starknet::{ContractAddress, get_caller_address};
use super::Errors;
use core::num::traits::Zero;
#[storage]
struct Storage {
ownable_owner: ContractAddress,
}
#[derive(Drop, starknet::Event)]
struct OwnershipTransferredEvent {
previous: ContractAddress,
new: ContractAddress
}
#[derive(Drop, starknet::Event)]
struct OwnershipRenouncedEvent {
previous: ContractAddress
}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
OwnershipTransferredEvent: OwnershipTransferredEvent,
OwnershipRenouncedEvent: OwnershipRenouncedEvent
}
#[embeddable_as(Ownable)]
impl OwnableImpl<
TContractState, +HasComponent<TContractState>
> of super::IOwnable<ComponentState<TContractState>> {
fn owner(self: @ComponentState<TContractState>) -> ContractAddress {
self.ownable_owner.read()
}
fn transfer_ownership(ref self: ComponentState<TContractState>, new: ContractAddress) {
self._assert_only_owner();
self._transfer_ownership(new);
}
fn renounce_ownership(ref self: ComponentState<TContractState>) {
self._assert_only_owner();
self._renounce_ownership();
}
}
#[generate_trait]
pub impl OwnableInternalImpl<
TContractState, +HasComponent<TContractState>
> of OwnableInternalTrait<TContractState> {
fn _assert_only_owner(self: @ComponentState<TContractState>) {
let caller = get_caller_address();
assert(!caller.is_zero(), Errors::ZERO_ADDRESS_CALLER);
assert(caller == self.ownable_owner.read(), Errors::UNAUTHORIZED);
}
fn _init(ref self: ComponentState<TContractState>, owner: ContractAddress) {
assert(!owner.is_zero(), Errors::ZERO_ADDRESS_OWNER);
self.ownable_owner.write(owner);
}
fn _transfer_ownership(ref self: ComponentState<TContractState>, new: ContractAddress) {
assert(!new.is_zero(), Errors::ZERO_ADDRESS_OWNER);
let previous = self.ownable_owner.read();
self.ownable_owner.write(new);
self
.emit(
Event::OwnershipTransferredEvent(OwnershipTransferredEvent { previous, new })
);
}
fn _renounce_ownership(ref self: ComponentState<TContractState>) {
let previous = self.ownable_owner.read();
self.ownable_owner.write(Zero::zero());
self.emit(Event::OwnershipRenouncedEvent(OwnershipRenouncedEvent { previous }));
}
}
}
A mock contract that uses the Ownable
component:
#[starknet::interface]
pub trait IOwned<TContractState> {
fn do_something(ref self: TContractState);
}
#[starknet::contract]
pub mod OwnedContract {
use components::ownable::{IOwnable, ownable_component, ownable_component::OwnableInternalTrait};
component!(path: ownable_component, storage: ownable, event: OwnableEvent);
#[abi(embed_v0)]
impl OwnableImpl = ownable_component::Ownable<ContractState>;
impl OwnableInternalImpl = ownable_component::OwnableInternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
ownable: ownable_component::Storage,
}
#[constructor]
fn constructor(ref self: ContractState) {
self.ownable._init(starknet::get_caller_address());
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
OwnableEvent: ownable_component::Event,
}
#[abi(embed_v0)]
impl Owned of super::IOwned<ContractState> {
fn do_something(ref self: ContractState) {
self.ownable._assert_only_owner();
// ...
}
}
}
#[cfg(test)]
mod tests {
use core::num::traits::Zero;
use super::{OwnedContract, IOwnedDispatcher, IOwnedDispatcherTrait};
use components::ownable::{IOwnable, IOwnableDispatcher, IOwnableDispatcherTrait};
use starknet::{contract_address_const, ContractAddress};
use starknet::testing::{set_caller_address, set_contract_address};
use starknet::storage::StorageMemberAccessTrait;
use starknet::SyscallResultTrait;
use starknet::syscalls::deploy_syscall;
fn deploy() -> (IOwnedDispatcher, IOwnableDispatcher) {
let (contract_address, _) = deploy_syscall(
OwnedContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
(IOwnedDispatcher { contract_address }, IOwnableDispatcher { contract_address },)
}
#[test]
#[available_gas(2000000)]
fn test_init() {
let owner = contract_address_const::<'owner'>();
set_contract_address(owner);
let (_, ownable) = deploy();
assert(ownable.owner() == owner, 'wrong_owner');
}
#[test]
#[available_gas(2000000)]
fn test_wrong_owner() {
set_contract_address(contract_address_const::<'owner'>());
let (_, ownable) = deploy();
let not_owner = contract_address_const::<'not_owner'>();
assert(ownable.owner() != not_owner, 'wrong_owner');
}
#[test]
#[available_gas(2000000)]
fn test_do_something() {
set_contract_address(contract_address_const::<'owner'>());
let (contract, _) = deploy();
contract.do_something();
// Should not panic
}
#[test]
#[available_gas(2000000)]
#[should_panic]
fn test_do_something_not_owner() {
set_contract_address(contract_address_const::<'owner'>());
let (contract, _) = deploy();
set_contract_address(contract_address_const::<'not_owner'>());
contract.do_something();
}
#[test]
#[available_gas(2000000)]
fn test_transfer_ownership() {
set_contract_address(contract_address_const::<'initial'>());
let (contract, ownable) = deploy();
let new_owner = contract_address_const::<'new_owner'>();
ownable.transfer_ownership(new_owner);
assert(ownable.owner() == new_owner, 'wrong_owner');
set_contract_address(new_owner);
contract.do_something();
}
#[test]
#[available_gas(2000000)]
#[should_panic]
fn test_transfer_ownership_not_owner() {
set_contract_address(contract_address_const::<'initial'>());
let (_, ownable) = deploy();
set_contract_address(contract_address_const::<'not_owner'>());
ownable.transfer_ownership(contract_address_const::<'new_owner'>());
}
#[test]
#[available_gas(2000000)]
#[should_panic]
fn test_transfer_ownership_zero_error() {
set_contract_address(contract_address_const::<'initial'>());
let (_, ownable) = deploy();
ownable.transfer_ownership(Zero::zero());
}
#[test]
#[available_gas(2000000)]
fn test_renounce_ownership() {
set_contract_address(contract_address_const::<'owner'>());
let (_, ownable) = deploy();
ownable.renounce_ownership();
assert(ownable.owner() == Zero::zero(), 'not_zero_owner');
}
#[test]
#[available_gas(2000000)]
#[should_panic]
fn test_renounce_ownership_not_owner() {
set_contract_address(contract_address_const::<'owner'>());
let (_, ownable) = deploy();
set_contract_address(contract_address_const::<'not_owner'>());
ownable.renounce_ownership();
}
#[test]
#[available_gas(2000000)]
#[should_panic]
fn test_renounce_ownership_previous_owner() {
set_contract_address(contract_address_const::<'owner'>());
let (contract, ownable) = deploy();
ownable.renounce_ownership();
contract.do_something();
}
}
Upgradeable Contract
In Starknet, contracts are divided into two parts: contract classes and contract instances. This division follows a similar concept used in object-oriented programming languages, where we distinguish between the definition and implementation of objects.
A contract class is the definition of a contract: it specifies how the contract behaves. It contains essential information like the Cairo byte code, hint information, entry point names, and everything that defines its semantics unambiguously.
To identify different contract classes, Starknet assigns a unique identifier to each class: the class hash. A contract instance is a deployed contract that corresponds to a specific contract class. Think of it as an instance of an object in languages like Java.
Each class is identified by its class hash, which is analogous to a class name in an object-oriented programming language. A contract instance is a deployed contract corresponding to a class.
You can upgrade a deployed contract to a newer version by calling the replace_class_syscall
function. By using this function, you can update the class hash associated with a deployed contract, effectively upgrading its implementation. However, this will not modify the contract's storage, so all the data stored in the contract will remain the same.
To illustrate this concept, let's consider an example with two contracts: UpgradeableContract_V0
, and UpgradeableContract_V1
.
Start by deploying UpgradeableContract_V0
as the initial version. Next, send a transaction that invokes the upgrade
function, with the class hash of UpgradeableContract_V1
as parameter to upgrade the class hash of the deployed contract to the UpgradeableContract_V1
one. Then, call the version
method on the contract to see that the contract was upgraded to the V1 version.
use starknet::class_hash::ClassHash;
#[starknet::interface]
pub trait IUpgradeableContract<TContractState> {
fn upgrade(ref self: TContractState, impl_hash: ClassHash);
fn version(self: @TContractState) -> u8;
}
#[starknet::contract]
pub mod UpgradeableContract_V0 {
use starknet::class_hash::ClassHash;
use starknet::SyscallResultTrait;
use core::num::traits::Zero;
#[storage]
struct Storage {}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Upgraded: Upgraded
}
#[derive(Drop, starknet::Event)]
struct Upgraded {
implementation: ClassHash
}
#[abi(embed_v0)]
impl UpgradeableContract of super::IUpgradeableContract<ContractState> {
fn upgrade(ref self: ContractState, impl_hash: ClassHash) {
assert(!impl_hash.is_zero(), 'Class hash cannot be zero');
starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall();
self.emit(Event::Upgraded(Upgraded { implementation: impl_hash }))
}
fn version(self: @ContractState) -> u8 {
0
}
}
}
use starknet::class_hash::ClassHash;
#[starknet::interface]
pub trait IUpgradeableContract<TContractState> {
fn upgrade(ref self: TContractState, impl_hash: ClassHash);
fn version(self: @TContractState) -> u8;
}
#[starknet::contract]
pub mod UpgradeableContract_V1 {
use starknet::class_hash::ClassHash;
use starknet::SyscallResultTrait;
use core::num::traits::Zero;
#[storage]
struct Storage {}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Upgraded: Upgraded
}
#[derive(Drop, starknet::Event)]
struct Upgraded {
implementation: ClassHash
}
#[abi(embed_v0)]
impl UpgradeableContract of super::IUpgradeableContract<ContractState> {
fn upgrade(ref self: ContractState, impl_hash: ClassHash) {
assert(!impl_hash.is_zero(), 'Class hash cannot be zero');
starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall();
self.emit(Event::Upgraded(Upgraded { implementation: impl_hash }))
}
fn version(self: @ContractState) -> u8 {
1
}
}
}
Simple Defi Vault
This is the Cairo adaptation of the Solidity by example Vault. Here's how it works:
-
When a user deposits a token, the contract calculates the amount of shares to mint.
-
When a user withdraws, the contract burns their shares, calculates the yield, and withdraw both the yield and the initial amount of token deposited.
use starknet::ContractAddress;
// In order to make contract calls within our Vault,
// we need to have the interface of the remote ERC20 contract defined to import the Dispatcher.
#[starknet::interface]
pub trait IERC20<TContractState> {
fn name(self: @TContractState) -> felt252;
fn symbol(self: @TContractState) -> felt252;
fn decimals(self: @TContractState) -> u8;
fn total_supply(self: @TContractState) -> u256;
fn balance_of(self: @TContractState, account: ContractAddress) -> u256;
fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256;
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
fn transfer_from(
ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256
) -> bool;
fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool;
}
#[starknet::interface]
pub trait ISimpleVault<TContractState> {
fn deposit(ref self: TContractState, amount: u256);
fn withdraw(ref self: TContractState, shares: u256);
}
#[starknet::contract]
pub mod SimpleVault {
use super::{IERC20Dispatcher, IERC20DispatcherTrait};
use starknet::{ContractAddress, get_caller_address, get_contract_address};
#[storage]
struct Storage {
token: IERC20Dispatcher,
total_supply: u256,
balance_of: LegacyMap<ContractAddress, u256>
}
#[constructor]
fn constructor(ref self: ContractState, token: ContractAddress) {
self.token.write(IERC20Dispatcher { contract_address: token });
}
#[generate_trait]
impl PrivateFunctions of PrivateFunctionsTrait {
fn _mint(ref self: ContractState, to: ContractAddress, shares: u256) {
self.total_supply.write(self.total_supply.read() + shares);
self.balance_of.write(to, self.balance_of.read(to) + shares);
}
fn _burn(ref self: ContractState, from: ContractAddress, shares: u256) {
self.total_supply.write(self.total_supply.read() - shares);
self.balance_of.write(from, self.balance_of.read(from) - shares);
}
}
#[abi(embed_v0)]
impl SimpleVault of super::ISimpleVault<ContractState> {
fn deposit(ref self: ContractState, amount: u256) {
// a = amount
// B = balance of token before deposit
// T = total supply
// s = shares to mint
//
// (T + s) / T = (a + B) / B
//
// s = aT / B
let caller = get_caller_address();
let this = get_contract_address();
let mut shares = 0;
if self.total_supply.read() == 0 {
shares = amount;
} else {
let balance = self.token.read().balance_of(this);
shares = (amount * self.total_supply.read()) / balance;
}
PrivateFunctions::_mint(ref self, caller, shares);
self.token.read().transfer_from(caller, this, amount);
}
fn withdraw(ref self: ContractState, shares: u256) {
// a = amount
// B = balance of token before withdraw
// T = total supply
// s = shares to burn
//
// (T - s) / T = (B - a) / B
//
// a = sB / T
let caller = get_caller_address();
let this = get_contract_address();
let balance = self.token.read().balance_of(this);
let amount = (shares * balance) / self.total_supply.read();
PrivateFunctions::_burn(ref self, caller, shares);
self.token.read().transfer(caller, amount);
}
}
}
ERC20 Token
Contracts that follow the ERC20 Standard are called ERC20 tokens. They are used to represent fungible assets.
To create an ERC20 conctract, it must implement the following interface:
#[starknet::interface]
trait IERC20<TContractState> {
fn get_name(self: @TContractState) -> felt252;
fn get_symbol(self: @TContractState) -> felt252;
fn get_decimals(self: @TContractState) -> u8;
fn get_total_supply(self: @TContractState) -> felt252;
fn balance_of(self: @TContractState, account: ContractAddress) -> felt252;
fn allowance(
self: @TContractState, owner: ContractAddress, spender: ContractAddress
) -> felt252;
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: felt252);
fn transfer_from(
ref self: TContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: felt252
);
fn approve(ref self: TContractState, spender: ContractAddress, amount: felt252);
fn increase_allowance(ref self: TContractState, spender: ContractAddress, added_value: felt252);
fn decrease_allowance(
ref self: TContractState, spender: ContractAddress, subtracted_value: felt252
);
}
In Starknet, function names should be written in snake_case. This is not the case in Solidity, where function names are written in camelCase. The Starknet ERC20 interface is therefore slightly different from the Solidity ERC20 interface.
Here's an implementation of the ERC20 interface in Cairo:
#[starknet::contract]
mod erc20 {
use core::num::traits::Zero;
use starknet::get_caller_address;
use starknet::contract_address_const;
use starknet::ContractAddress;
#[storage]
struct Storage {
name: felt252,
symbol: felt252,
decimals: u8,
total_supply: felt252,
balances: LegacyMap::<ContractAddress, felt252>,
allowances: LegacyMap::<(ContractAddress, ContractAddress), felt252>,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Transfer: Transfer,
Approval: Approval,
}
#[derive(Drop, starknet::Event)]
struct Transfer {
from: ContractAddress,
to: ContractAddress,
value: felt252,
}
#[derive(Drop, starknet::Event)]
struct Approval {
owner: ContractAddress,
spender: ContractAddress,
value: felt252,
}
mod Errors {
pub const APPROVE_FROM_ZERO: felt252 = 'ERC20: approve from 0';
pub const APPROVE_TO_ZERO: felt252 = 'ERC20: approve to 0';
pub const TRANSFER_FROM_ZERO: felt252 = 'ERC20: transfer from 0';
pub const TRANSFER_TO_ZERO: felt252 = 'ERC20: transfer to 0';
pub const BURN_FROM_ZERO: felt252 = 'ERC20: burn from 0';
pub const MINT_TO_ZERO: felt252 = 'ERC20: mint to 0';
}
#[constructor]
fn constructor(
ref self: ContractState,
recipient: ContractAddress,
name: felt252,
decimals: u8,
initial_supply: felt252,
symbol: felt252
) {
self.name.write(name);
self.symbol.write(symbol);
self.decimals.write(decimals);
self.mint(recipient, initial_supply);
}
#[abi(embed_v0)]
impl IERC20Impl of super::IERC20<ContractState> {
fn get_name(self: @ContractState) -> felt252 {
self.name.read()
}
fn get_symbol(self: @ContractState) -> felt252 {
self.symbol.read()
}
fn get_decimals(self: @ContractState) -> u8 {
self.decimals.read()
}
fn get_total_supply(self: @ContractState) -> felt252 {
self.total_supply.read()
}
fn balance_of(self: @ContractState, account: ContractAddress) -> felt252 {
self.balances.read(account)
}
fn allowance(
self: @ContractState, owner: ContractAddress, spender: ContractAddress
) -> felt252 {
self.allowances.read((owner, spender))
}
fn transfer(ref self: ContractState, recipient: ContractAddress, amount: felt252) {
let sender = get_caller_address();
self._transfer(sender, recipient, amount);
}
fn transfer_from(
ref self: ContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: felt252
) {
let caller = get_caller_address();
self.spend_allowance(sender, caller, amount);
self._transfer(sender, recipient, amount);
}
fn approve(ref self: ContractState, spender: ContractAddress, amount: felt252) {
let caller = get_caller_address();
self.approve_helper(caller, spender, amount);
}
fn increase_allowance(
ref self: ContractState, spender: ContractAddress, added_value: felt252
) {
let caller = get_caller_address();
self
.approve_helper(
caller, spender, self.allowances.read((caller, spender)) + added_value
);
}
fn decrease_allowance(
ref self: ContractState, spender: ContractAddress, subtracted_value: felt252
) {
let caller = get_caller_address();
self
.approve_helper(
caller, spender, self.allowances.read((caller, spender)) - subtracted_value
);
}
}
#[generate_trait]
impl InternalImpl of InternalTrait {
fn _transfer(
ref self: ContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: felt252
) {
assert(!sender.is_zero(), Errors::TRANSFER_FROM_ZERO);
assert(!recipient.is_zero(), Errors::TRANSFER_TO_ZERO);
self.balances.write(sender, self.balances.read(sender) - amount);
self.balances.write(recipient, self.balances.read(recipient) + amount);
self.emit(Transfer { from: sender, to: recipient, value: amount });
}
fn spend_allowance(
ref self: ContractState,
owner: ContractAddress,
spender: ContractAddress,
amount: felt252
) {
let allowance = self.allowances.read((owner, spender));
self.allowances.write((owner, spender), allowance - amount);
}
fn approve_helper(
ref self: ContractState,
owner: ContractAddress,
spender: ContractAddress,
amount: felt252
) {
assert(!spender.is_zero(), Errors::APPROVE_TO_ZERO);
self.allowances.write((owner, spender), amount);
self.emit(Approval { owner, spender, value: amount });
}
fn mint(ref self: ContractState, recipient: ContractAddress, amount: felt252) {
assert(!recipient.is_zero(), Errors::MINT_TO_ZERO);
let supply = self.total_supply.read() + amount;
self.total_supply.write(supply);
let balance = self.balances.read(recipient) + amount;
self.balances.write(recipient, balance);
self
.emit(
Event::Transfer(
Transfer {
from: contract_address_const::<0>(), to: recipient, value: amount
}
)
);
}
}
}
There's several other implementations, such as the Open Zeppelin or the Cairo By Example ones.
Constant Product AMM
This is the Cairo adaptation of the Solidity by example Constant Product AMM.
use starknet::ContractAddress;
#[starknet::interface]
pub trait IConstantProductAmm<TContractState> {
fn swap(ref self: TContractState, token_in: ContractAddress, amount_in: u256) -> u256;
fn add_liquidity(ref self: TContractState, amount0: u256, amount1: u256) -> u256;
fn remove_liquidity(ref self: TContractState, shares: u256) -> (u256, u256);
}
#[starknet::contract]
pub mod ConstantProductAmm {
use core::traits::Into;
use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
use starknet::{
ContractAddress, get_caller_address, get_contract_address, contract_address_const
};
use core::integer::u256_sqrt;
#[storage]
struct Storage {
token0: IERC20Dispatcher,
token1: IERC20Dispatcher,
reserve0: u256,
reserve1: u256,
total_supply: u256,
balance_of: LegacyMap::<ContractAddress, u256>,
// Fee 0 - 1000 (0% - 100%, 1 decimal places)
// E.g. 3 = 0.3%
fee: u16,
}
#[constructor]
fn constructor(
ref self: ContractState, token0: ContractAddress, token1: ContractAddress, fee: u16
) {
// assert(fee <= 1000, 'fee > 1000');
self.token0.write(IERC20Dispatcher { contract_address: token0 });
self.token1.write(IERC20Dispatcher { contract_address: token1 });
self.fee.write(fee);
}
#[generate_trait]
impl PrivateFunctions of PrivateFunctionsTrait {
fn _mint(ref self: ContractState, to: ContractAddress, amount: u256) {
self.balance_of.write(to, self.balance_of.read(to) + amount);
self.total_supply.write(self.total_supply.read() + amount);
}
fn _burn(ref self: ContractState, from: ContractAddress, amount: u256) {
self.balance_of.write(from, self.balance_of.read(from) - amount);
self.total_supply.write(self.total_supply.read() - amount);
}
fn _update(ref self: ContractState, reserve0: u256, reserve1: u256) {
self.reserve0.write(reserve0);
self.reserve1.write(reserve1);
}
#[inline(always)]
fn select_token(self: @ContractState, token: ContractAddress) -> bool {
assert(
token == self.token0.read().contract_address
|| token == self.token1.read().contract_address,
'invalid token'
);
token == self.token0.read().contract_address
}
#[inline(always)]
fn min(x: u256, y: u256) -> u256 {
if (x <= y) {
x
} else {
y
}
}
}
#[abi(embed_v0)]
impl ConstantProductAmm of super::IConstantProductAmm<ContractState> {
fn swap(ref self: ContractState, token_in: ContractAddress, amount_in: u256) -> u256 {
assert(amount_in > 0, 'amount in = 0');
let is_token0: bool = self.select_token(token_in);
let (token0, token1): (IERC20Dispatcher, IERC20Dispatcher) = (
self.token0.read(), self.token1.read()
);
let (reserve0, reserve1): (u256, u256) = (self.reserve0.read(), self.reserve1.read());
let (
token_in, token_out, reserve_in, reserve_out
): (IERC20Dispatcher, IERC20Dispatcher, u256, u256) =
if (is_token0) {
(token0, token1, reserve0, reserve1)
} else {
(token1, token0, reserve1, reserve0)
};
let caller = get_caller_address();
let this = get_contract_address();
token_in.transfer_from(caller, this, amount_in);
// How much dy for dx?
// xy = k
// (x + dx)(y - dy) = k
// y - dy = k / (x + dx)
// y - k / (x + dx) = dy
// y - xy / (x + dx) = dy
// (yx + ydx - xy) / (x + dx) = dy
// ydx / (x + dx) = dy
let amount_in_with_fee = (amount_in * (1000 - self.fee.read().into()) / 1000);
let amount_out = (reserve_out * amount_in_with_fee) / (reserve_in + amount_in_with_fee);
token_out.transfer(caller, amount_out);
self._update(self.token0.read().balance_of(this), self.token1.read().balance_of(this));
amount_out
}
fn add_liquidity(ref self: ContractState, amount0: u256, amount1: u256) -> u256 {
let caller = get_caller_address();
let this = get_contract_address();
let (token0, token1): (IERC20Dispatcher, IERC20Dispatcher) = (
self.token0.read(), self.token1.read()
);
token0.transfer_from(caller, this, amount0);
token1.transfer_from(caller, this, amount1);
// How much dx, dy to add?
//
// xy = k
// (x + dx)(y + dy) = k'
//
// No price change, before and after adding liquidity
// x / y = (x + dx) / (y + dy)
//
// x(y + dy) = y(x + dx)
// x * dy = y * dx
//
// x / y = dx / dy
// dy = y / x * dx
let (reserve0, reserve1): (u256, u256) = (self.reserve0.read(), self.reserve1.read());
if (reserve0 > 0 || reserve1 > 0) {
assert(reserve0 * amount1 == reserve1 * amount0, 'x / y != dx / dy');
}
// How much shares to mint?
//
// f(x, y) = value of liquidity
// We will define f(x, y) = sqrt(xy)
//
// L0 = f(x, y)
// L1 = f(x + dx, y + dy)
// T = total shares
// s = shares to mint
//
// Total shares should increase proportional to increase in liquidity
// L1 / L0 = (T + s) / T
//
// L1 * T = L0 * (T + s)
//
// (L1 - L0) * T / L0 = s
// Claim
// (L1 - L0) / L0 = dx / x = dy / y
//
// Proof
// --- Equation 1 ---
// (L1 - L0) / L0 = (sqrt((x + dx)(y + dy)) - sqrt(xy)) / sqrt(xy)
//
// dx / dy = x / y so replace dy = dx * y / x
//
// --- Equation 2 ---
// Equation 1 = (sqrt(xy + 2ydx + dx^2 * y / x) - sqrt(xy)) / sqrt(xy)
//
// Multiply by sqrt(x) / sqrt(x)
// Equation 2 = (sqrt(x^2y + 2xydx + dx^2 * y) - sqrt(x^2y)) / sqrt(x^2y)
// = (sqrt(y)(sqrt(x^2 + 2xdx + dx^2) - sqrt(x^2)) / (sqrt(y)sqrt(x^2))
// sqrt(y) on top and bottom cancels out
//
// --- Equation 3 ---
// Equation 2 = (sqrt(x^2 + 2xdx + dx^2) - sqrt(x^2)) / (sqrt(x^2)
// = (sqrt((x + dx)^2) - sqrt(x^2)) / sqrt(x^2)
// = ((x + dx) - x) / x
// = dx / x
// Since dx / dy = x / y,
// dx / x = dy / y
//
// Finally
// (L1 - L0) / L0 = dx / x = dy / y
let total_supply = self.total_supply.read();
let shares = if (total_supply == 0) {
u256_sqrt(amount0 * amount1).into()
} else {
PrivateFunctions::min(
amount0 * total_supply / reserve0, amount1 * total_supply / reserve1
)
};
assert(shares > 0, 'shares = 0');
self._mint(caller, shares);
self._update(self.token0.read().balance_of(this), self.token1.read().balance_of(this));
shares
}
fn remove_liquidity(ref self: ContractState, shares: u256) -> (u256, u256) {
let caller = get_caller_address();
let this = get_contract_address();
let (token0, token1): (IERC20Dispatcher, IERC20Dispatcher) = (
self.token0.read(), self.token1.read()
);
// Claim
// dx, dy = amount of liquidity to remove
// dx = s / T * x
// dy = s / T * y
//
// Proof
// Let's find dx, dy such that
// v / L = s / T
//
// where
// v = f(dx, dy) = sqrt(dxdy)
// L = total liquidity = sqrt(xy)
// s = shares
// T = total supply
//
// --- Equation 1 ---
// v = s / T * L
// sqrt(dxdy) = s / T * sqrt(xy)
//
// Amount of liquidity to remove must not change price so
// dx / dy = x / y
//
// replace dy = dx * y / x
// sqrt(dxdy) = sqrt(dx * dx * y / x) = dx * sqrt(y / x)
//
// Divide both sides of Equation 1 with sqrt(y / x)
// dx = s / T * sqrt(xy) / sqrt(y / x)
// = s / T * sqrt(x^2) = s / T * x
//
// Likewise
// dy = s / T * y
// bal0 >= reserve0
// bal1 >= reserve1
let (bal0, bal1): (u256, u256) = (token0.balance_of(this), token1.balance_of(this));
let total_supply = self.total_supply.read();
let (amount0, amount1): (u256, u256) = (
(shares * bal0) / total_supply, (shares * bal1) / total_supply
);
assert(amount0 > 0 && amount1 > 0, 'amount0 or amount1 = 0');
self._burn(caller, shares);
self._update(bal0 - amount0, bal1 - amount1);
token0.transfer(caller, amount0);
token1.transfer(caller, amount1);
(amount0, amount1)
}
}
}
Writing to any storage slot
On Starknet, a contract's storage is a map with 2^251 slots, where each slot is a felt which is initialized to 0.
The address of storage variables is computed at compile time using the formula: storage variable address := pedersen(keccak(variable name), keys)
. Interactions with storage variables are commonly performed using the self.var.read()
and self.var.write()
functions.
Nevertheless, we can use the storage_write_syscall
and storage_read_syscall
syscalls, to write to and read from any storage slot.
This is useful when writing to storage variables that are not known at compile time, or to ensure that even if the contract is upgraded and the computation method of storage variable addresses changes, they remain accessible.
In the following example, we use the Poseidon hash function to compute the address of a storage variable. Poseidon is a ZK-friendly hash function that is cheaper and faster than Pedersen, making it an excellent choice for onchain computations. Once the address is computed, we use the storage syscalls to interact with it.
#[starknet::interface]
pub trait IWriteToAnySlots<TContractState> {
fn write_slot(ref self: TContractState, value: u32);
fn read_slot(self: @TContractState) -> u32;
}
#[starknet::contract]
pub mod WriteToAnySlot {
use starknet::syscalls::{storage_read_syscall, storage_write_syscall};
use starknet::SyscallResultTrait;
use core::poseidon::poseidon_hash_span;
use starknet::StorageAddress;
#[storage]
struct Storage {}
const SLOT_NAME: felt252 = 'test_slot';
#[abi(embed_v0)]
impl WriteToAnySlot of super::IWriteToAnySlots<ContractState> {
fn write_slot(ref self: ContractState, value: u32) {
storage_write_syscall(0, get_address_from_name(SLOT_NAME), value.into())
.unwrap_syscall();
}
fn read_slot(self: @ContractState) -> u32 {
storage_read_syscall(0, get_address_from_name(SLOT_NAME))
.unwrap_syscall()
.try_into()
.unwrap()
}
}
pub fn get_address_from_name(variable_name: felt252) -> StorageAddress {
let mut data: Array<felt252> = array![];
data.append(variable_name);
let hashed_name: felt252 = poseidon_hash_span(data.span());
let MASK_250: u256 = 0x03ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
// By taking the 250 least significant bits of the hash output, we get a valid 250bits storage address.
let result: felt252 = (hashed_name.into() & MASK_250).try_into().unwrap();
let result: StorageAddress = result.try_into().unwrap();
result
}
}
Storing Arrays
On Starknet, complex values (e.g., tuples or structs), are stored in a continuous segment starting from the address of the storage variable. There is a 256 field elements limitation to the maximal size of a complex storage value, meaning that to store arrays of more than 255 elements in storage, we would need to split it into segments of size n <= 255
and store these segments in multiple storage addresses. There is currently no native support for storing arrays in Cairo, so you will need to write your own implementation of the Store
trait for the type of array you wish to store.
However, the ByteArray
struct can be used to store Array<bytes31>
in storage without additional implementation. Before implementing your own Store
trait, consider wether the ByteArray
struct can be used to store the data you need! See the ByteArray section for more information.
Note: While storing arrays in storage is possible, it is not always recommended, as the read and write operations can get very costly. For example, reading an array of size
n
requiresn
storage reads, and writing to an array of sizen
requiresn
storage writes. If you only need to access a single element of the array at a time, it is recommended to use aLegacyMap
and store the length in another variable instead.
The following example demonstrates how to write a simple implementation of the StorageAccess
trait for the Array<felt252>
type, allowing us to store arrays of up to 255 felt252
elements.
impl StoreFelt252Array of Store<Array<felt252>> {
fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult<Array<felt252>> {
StoreFelt252Array::read_at_offset(address_domain, base, 0)
}
fn write(
address_domain: u32, base: StorageBaseAddress, value: Array<felt252>
) -> SyscallResult<()> {
StoreFelt252Array::write_at_offset(address_domain, base, 0, value)
}
fn read_at_offset(
address_domain: u32, base: StorageBaseAddress, mut offset: u8
) -> SyscallResult<Array<felt252>> {
let mut arr: Array<felt252> = array![];
// Read the stored array's length. If the length is superior to 255, the read will fail.
let len: u8 = Store::<u8>::read_at_offset(address_domain, base, offset)
.expect('Storage Span too large');
offset += 1;
// Sequentially read all stored elements and append them to the array.
let exit = len + offset;
loop {
if offset >= exit {
break;
}
let value = Store::<felt252>::read_at_offset(address_domain, base, offset).unwrap();
arr.append(value);
offset += Store::<felt252>::size();
};
// Return the array.
Result::Ok(arr)
}
fn write_at_offset(
address_domain: u32, base: StorageBaseAddress, mut offset: u8, mut value: Array<felt252>
) -> SyscallResult<()> {
// // Store the length of the array in the first storage slot.
let len: u8 = value.len().try_into().expect('Storage - Span too large');
Store::<u8>::write_at_offset(address_domain, base, offset, len).unwrap();
offset += 1;
// Store the array elements sequentially
while let Option::Some(element) = value
.pop_front() {
Store::<felt252>::write_at_offset(address_domain, base, offset, element).unwrap();
offset += Store::<felt252>::size();
};
Result::Ok(())
}
fn size() -> u8 {
255 * Store::<felt252>::size()
}
}
You can then import this implementation in your contract and use it to store arrays in storage:
#[starknet::interface]
pub trait IStoreArrayContract<TContractState> {
fn store_array(ref self: TContractState, arr: Array<felt252>);
fn read_array(self: @TContractState) -> Array<felt252>;
}
#[starknet::contract]
pub mod StoreArrayContract {
use super::StoreFelt252Array;
#[storage]
struct Storage {
arr: Array<felt252>
}
#[abi(embed_v0)]
impl StoreArrayImpl of super::IStoreArrayContract<ContractState> {
fn store_array(ref self: ContractState, arr: Array<felt252>) {
self.arr.write(arr);
}
fn read_array(self: @ContractState) -> Array<felt252> {
self.arr.read()
}
}
}
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)
}
}
}
Hashing
Hashing is a cryptographic technique that allows you to transform a variable length input into a fixed length output. The resulting output is called a hash and it's completely different from the input. Hash functions are deterministic, meaning that the same input will always produce the same output.
The two hash functions provided by the Cairo library are Poseidon
and Pedersen
.
Pedersen hashes were used in the past (but still used in some scenario for backward compatibility) while Poseidon hashes are the standard nowadays since they were designed to be very efficient for Zero Knowledge proof systems.
In Cairo it's possible to hash all the types that can be converted to felt252
since they implement natively the Hash
trait. It's also possible to hash more complex types like structs by deriving the Hash trait with the attribute #[derive(Hash)]
but only if all the struct's fields are themselves hashable.
You first need to initialize a hash state with the new
method of the HashStateTrait
and then you can update it with the update
method. You can accumulate multiple updates. Then, the finalize
method returns the final hash value as a felt252
.
#[starknet::interface]
pub trait IHashTrait<T> {
fn save_user_with_poseidon(
ref self: T, id: felt252, username: felt252, password: felt252
) -> felt252;
fn save_user_with_pedersen(
ref self: T, id: felt252, username: felt252, password: felt252
) -> felt252;
}
#[starknet::contract]
pub mod HashTraits {
use core::hash::{HashStateTrait, HashStateExTrait};
use core::{pedersen::PedersenTrait, poseidon::PoseidonTrait};
#[storage]
struct Storage {
user_hash_poseidon: felt252,
user_hash_pedersen: felt252,
}
#[derive(Drop, Hash)]
struct LoginDetails {
username: felt252,
password: felt252,
}
#[derive(Drop, Hash)]
struct UserDetails {
id: felt252,
login: LoginDetails,
}
#[abi(embed_v0)]
impl HashTrait of super::IHashTrait<ContractState> {
fn save_user_with_poseidon(
ref self: ContractState, id: felt252, username: felt252, password: felt252
) -> felt252 {
let login = LoginDetails { username, password };
let user = UserDetails { id, login };
let poseidon_hash = PoseidonTrait::new().update_with(user).finalize();
self.user_hash_poseidon.write(poseidon_hash);
poseidon_hash
}
fn save_user_with_pedersen(
ref self: ContractState, id: felt252, username: felt252, password: felt252
) -> felt252 {
let login = LoginDetails { username, password };
let user = UserDetails { id, login };
let pedersen_hash = PedersenTrait::new(0).update_with(user).finalize();
self.user_hash_pedersen.write(pedersen_hash);
pedersen_hash
}
}
}
#[cfg(test)]
mod tests {
use starknet::SyscallResultTrait;
use super::{HashTraits, IHashTraitDispatcher, IHashTraitDispatcherTrait};
use core::hash::{HashStateTrait, HashStateExTrait};
use core::{pedersen::PedersenTrait, poseidon::PoseidonTrait};
use starknet::syscalls::deploy_syscall;
fn deploy() -> IHashTraitDispatcher {
let mut calldata = array![];
let (address, _) = deploy_syscall(
HashTraits::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false
)
.unwrap_syscall();
IHashTraitDispatcher { contract_address: address }
}
#[test]
#[available_gas(20000000)]
fn test_pedersen_hash() {
let mut contract = deploy();
let id = 0x1;
let username = 'A.stark';
let password = 'password.stark';
let test_hash = contract.save_user_with_pedersen(id, username, password);
assert(
test_hash == 0x6da4b4d0489989f5483d179643dafb3405b0e3b883a6c8efe5beb824ba9055a,
'Incorrect hash output'
);
}
#[test]
#[available_gas(20000000)]
fn test_poseidon_hash() {
let mut contract = deploy();
let id = 0x1;
let username = 'A.stark';
let password = 'password.stark';
let test_hash = contract.save_user_with_poseidon(id, username, password);
assert(
test_hash == 0x4d165e1d398ae4864854518d3c58c3d7a21ed9c1f8f3618fbb0031d208aab7b,
'Incorrect hash output'
);
}
}
Optimisations
A collection of optimisation patterns to save gas and steps.
Storage optimisation
A smart contract has a limited amount of storage slots. Each slot can store a single felt252
value.
Writing to a storage slot has a cost, so we want to use as few storage slots as possible.
In Cairo, every type is derived from the felt252
type, which uses 252 bits to store a value.
This design is quite simple, but it does have a drawback: it is not storage efficient. For example, if we want to store a u8
value, we need to use an entire slot, even though we only need 8 bits.
Packing
When storing multiple values, we can use a technique called packing. Packing is a technique that allows us to store multiple values in a single felt value. This is done by using the bits of the felt value to store multiple values.
For example, if we want to store two u8
values, we can use the first 8 bits of the felt value to store the first u8
value, and the last 8 bits to store the second u8
value. This way, we can store two u8
values in a single felt value.
Cairo provides a built-in store using packing that you can use with the StorePacking
trait.
trait StorePacking<T, PackedT> {
fn pack(value: T) -> PackedT;
fn unpack(value: PackedT) -> T;
}
This allows to store the type T
by first packing it into the type PackedT
with the pack
function, and then storing the PackedT
value with it's Store
implementation. When reading the value, we first retrieve the PackedT
value, and then unpack it into the type T
using the unpack
function.
Here's an example of storing a Time
struct with two u8
values using the StorePacking
trait:
#[derive(Copy, Serde, Drop)]
pub struct Time {
pub hour: u8,
pub minute: u8
}
#[starknet::interface]
pub trait ITime<TContractState> {
fn set(ref self: TContractState, value: Time);
fn get(self: @TContractState) -> Time;
}
#[starknet::contract]
pub mod TimeContract {
use super::Time;
use starknet::storage_access::StorePacking;
#[storage]
struct Storage {
time: Time
}
impl TimePackable of StorePacking<Time, felt252> {
fn pack(value: Time) -> felt252 {
let msb: felt252 = 256 * value.hour.into();
let lsb: felt252 = value.minute.into();
return msb + lsb;
}
fn unpack(value: felt252) -> Time {
let value: u16 = value.try_into().unwrap();
let (q, r) = DivRem::div_rem(value, 256_u16.try_into().unwrap());
let hour: u8 = Into::<u16, felt252>::into(q).try_into().unwrap();
let minute: u8 = Into::<u16, felt252>::into(r).try_into().unwrap();
return Time { hour, minute };
}
}
#[abi(embed_v0)]
impl TimeContract of super::ITime<ContractState> {
fn set(ref self: ContractState, value: Time) {
// This will call the pack method of the TimePackable trait
// and store the resulting felt252
self.time.write(value);
}
fn get(self: @ContractState) -> Time {
// This will read the felt252 value from storage
// and return the result of the unpack method of the TimePackable trait
return self.time.read();
}
}
}
List
By default, there is no list type supported in Cairo, but you can use Alexandria. You can refer to the Alexandria documentation for more details.
What is List
?
An ordered sequence of values that can be used in Starknet storage:
#[storage]
struct Storage {
amounts: List<u128>
}
Interface
trait ListTrait<T> {
fn len(self: @List<T>) -> u32;
fn is_empty(self: @List<T>) -> bool;
fn append(ref self: List<T>, value: T) -> u32;
fn get(self: @List<T>, index: u32) -> Option<T>;
fn set(ref self: List<T>, index: u32, value: T);
fn pop_front(ref self: List<T>) -> Option<T>;
fn array(self: @List<T>) -> Array<T>;
}
List
also implements IndexView
so you can use the familiar bracket notation to access its members:
let second = self.amounts.read()[1];
Note that unlike get
, using this bracket notation panics when accessing an out of bounds index.
Support for custom types
List
supports most of the corelib types out of the box. If you want to store a your own custom type in a List
, it has to implement the Store
trait. You can have the compiler derive it for you using the #[derive(starknet::Store)]
attribute.
Caveats
There are two idiosyncacies you should be aware of when using List
- The
append
operation costs 2 storage writes - one for the value itself and another one for updating the List's length - Due to a compiler limitation, it is not possible to use mutating operations with a single inline statement. For example,
self.amounts.read().append(42);
will not work. You have to do it in 2 steps:
let mut amounts = self.amounts.read();
amounts.append(42);
Dependencies
Update your project dependencies by in the Scarb.toml
file:
[dependencies]
(...)
alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git" }
For example, let's use List
to create a contract that tracks a list of amounts and tasks:
#[derive(Copy, Drop, Serde, starknet::Store)]
pub struct Task {
pub description: felt252,
pub status: felt252
}
#[starknet::interface]
pub trait IListExample<TContractState> {
fn add_in_amount(ref self: TContractState, number: u128);
fn add_in_task(ref self: TContractState, description: felt252, status: felt252);
fn is_empty_list(self: @TContractState) -> bool;
fn list_length(self: @TContractState) -> u32;
fn get_from_index(self: @TContractState, index: u32) -> u128;
fn set_from_index(ref self: TContractState, index: u32, number: u128);
fn pop_front_list(ref self: TContractState);
fn array_conversion(self: @TContractState) -> Array<u128>;
}
#[starknet::contract]
pub mod ListExample {
use super::Task;
use alexandria_storage::list::{List, ListTrait};
#[storage]
pub struct Storage {
amount: List<u128>,
tasks: List<Task>
}
#[abi(embed_v0)]
impl ListExample of super::IListExample<ContractState> {
fn add_in_amount(ref self: ContractState, number: u128) {
let mut current_amount_list = self.amount.read();
current_amount_list.append(number).unwrap();
}
fn add_in_task(ref self: ContractState, description: felt252, status: felt252) {
let new_task = Task { description: description, status: status };
let mut current_tasks_list = self.tasks.read();
current_tasks_list.append(new_task).unwrap();
}
fn is_empty_list(self: @ContractState) -> bool {
let mut current_amount_list = self.amount.read();
current_amount_list.is_empty()
}
fn list_length(self: @ContractState) -> u32 {
let mut current_amount_list = self.amount.read();
current_amount_list.len()
}
fn get_from_index(self: @ContractState, index: u32) -> u128 {
self.amount.read()[index]
}
fn set_from_index(ref self: ContractState, index: u32, number: u128) {
let mut current_amount_list = self.amount.read();
current_amount_list.set(index, number).unwrap();
}
fn pop_front_list(ref self: ContractState) {
let mut current_amount_list = self.amount.read();
current_amount_list.pop_front().unwrap().unwrap();
}
fn array_conversion(self: @ContractState) -> Array<u128> {
let mut current_amount_list = self.amount.read();
current_amount_list.array().unwrap()
}
}
}
Plugins
Compilers plugin in Cairo are a way to generate additional code for specific items during the compilation process.
There are already a set of core plugins that you may have already used, such as #[derive]
, #[cfg]
, #[generate_trait]
, etc.
It is possible to create your own plugins in rust. You can learn more about how to do that in the hello-cairo-plugin repository.
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