Starknet by Example
Starknet By Example is a collection of examples of how to use the Cairo programming language to create smart contracts on Starknet.
Starknet is a permissionless Validity-Rollup that supports general computation. It is currently used as an Ethereum layer-2. Starknet uses the STARK cryptographic proof system to ensure high safety and scalability.
Starknet smart contracts are written in the Cairo language. Cairo is a Turing-complete programming language designed to write provable programs, abstracting the zk-STARK proof system away from the programmer.
⚠️ The examples have not been audited and are not intended for production use. The authors are not responsible for any damages caused by the use of the code provided in this book.
For whom is this for?
Starknet By Example is for anyone who wants to quickly learn how to write smart contracts on Starknet using Cairo with some technical background in programming and blockchain.
The first chapters will give you a basic understanding of the Cairo programming language and how to write, deploy and use smart contracts on Starknet. The later chapters will cover more advanced topics and show you how to write more complex smart contracts.
How to use this book?
Each chapter is a standalone example that demonstrates a specific feature or common use case of smart contracts on Starknet. If you are new to Starknet, it is recommended to read the chapters in order.
Most examples contain interfaces and tests that are hidden by default. You can hover over the code blocks and click on the "Show hidden lines" (eyes icon) to see the hidden code.
You can run each example online by using the Starknet Remix Plugin.
Further reading
If you want to learn more about the Cairo programming language, you can read the Cairo Book. If you want to learn more about Starknet, you can read the Starknet documentation and the Starknet Book.
For more resources, check Awesome Starknet.
Versions
The current version this book uses:
cairo 2.8.2
edition = "2024_07"
sierra: 1.6.0
scarb 2.8.2
starknet-foundry 0.30.0
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 {
pub a: u128,
pub b: u8,
pub c: u256
}
}
#[cfg(test)]
mod test {
use super::Contract;
use starknet::{SyscallResultTrait, syscalls::deploy_syscall};
use starknet::storage::StoragePointerReadAccess;
#[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;
use starknet::storage::{Map, StorageMapWriteAccess};
#[storage]
struct Storage {
pub names: Map::<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;
use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};
use starknet::{contract_address_const, testing::{set_contract_address}};
use starknet::storage::StorageMapReadAccess;
#[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 {
// You need to import these storage functions to read and write to storage variables
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
// All storage variables are contained in a struct called Storage
// annotated with the `#[storage]` attribute
#[storage]
struct Storage {
// Storage variable holding a number
pub 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, IStorageVariableExampleDispatcher,
IStorageVariableExampleDispatcherTrait
};
use starknet::{SyscallResultTrait, syscalls::deploy_syscall};
use starknet::testing::set_contract_address;
use starknet::storage::StoragePointerReadAccess;
#[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 implementation blocks. The first impl
block for externally accessible functions is explicitly annotated with an #[abi(embed_v0)]
attribute. This indicates that all the functions inside this block can be called either as a transaction or as a view function. The second impl
block for internally accessible functions is not annotated with any attribute, which means that all the functions inside this block are private by default.
State Mutability
Regardless of whether a function is internal or external, it can either modify the contract's state or not. When we declare functions that interact with storage variables inside a smart contract,
we need to explicitly state that we are accessing the ContractState
by adding it as the first parameter of the function. This can be done in two different ways:
- If we want our function to be able to mutate the state of the contract, we pass it by reference like this:
ref self: ContractState
- If we want our function to be read-only and not mutate the state of the contract, we pass it by snapshot like this:
self: @ContractState
Read-only functions, also called view functions, can be directly called without making a transaction. You can interact with them directly through an RPC node to read the contract's state, and they're free to call! External functions, that modify the contract's state, on the other hand, can only be called by making a transaction.
Internal functions can't be called externally, but the same principle applies regarding state mutability.
Let's take a look at a simple example contract to see these in action:
#[starknet::interface]
pub trait IExampleContract<TContractState> {
fn set(ref self: TContractState, value: u32);
fn get(self: @TContractState) -> u32;
}
#[starknet::contract]
pub mod ExampleContract {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
pub value: u32
}
// The `#[abi(embed_v0)]` attribute indicates that all
// the functions in this implementation can be called externally.
// Omitting this attribute would make all the functions internal.
#[abi(embed_v0)]
impl ExampleContract of super::IExampleContract<ContractState> {
// The `set` function can be called externally
// because it is written inside an implementation marked as `#[abi(embed_v0)]`.
// It can modify the contract's state as it is passed as a reference.
fn set(ref self: ContractState, value: u32) {
self.value.write(value);
}
// The `get` function can be called externally
// because it is written inside an implementation marked as `#[abi(embed_v0)]`.
// However, it can't modify the contract's state, as it is passed as a snapshot
// -> It's only a "view" function.
fn get(self: @ContractState) -> u32 {
// We can call an internal function from any functions within the contract
PrivateFunctionsTrait::_read_value(self)
}
}
// The lack of the `#[abi(embed_v0)]` attribute indicates that all the functions in
// this implementation can only be called internally.
// We name the trait `PrivateFunctionsTrait` to indicate that it is an
// internal trait allowing us to call internal functions.
#[generate_trait]
pub impl PrivateFunctions of PrivateFunctionsTrait {
// The `_read_value` function is outside the implementation that is
// marked as `#[abi(embed_v0)]`, so it's an _internal_ function
// and can only be called from within the contract.
// However, it can't modify the contract's state, as it is passed
// as a snapshot: it is only a "view" function.
fn _read_value(self: @ContractState) -> u32 {
self.value.read()
}
}
}
#[cfg(test)]
mod test {
use super::{ExampleContract, IExampleContractDispatcher, IExampleContractDispatcherTrait};
use starknet::{SyscallResultTrait, syscalls::deploy_syscall};
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
// These imports will allow us to directly access and set the contract state:
// - 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 access 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 {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
// Counter variable
pub 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::{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 Map
type. It's important to note that the Map
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 Map
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
Map::<(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 taken \( \bmod {2^{251}} - 256 \). You can learn more about the contract storage layout in the Starknet Documentation.
use starknet::ContractAddress;
#[starknet::interface]
pub trait IMapContract<TContractState> {
fn set(ref self: TContractState, key: ContractAddress, value: felt252);
fn get(self: @TContractState, key: ContractAddress) -> felt252;
}
#[starknet::contract]
pub mod MapContract {
use starknet::ContractAddress;
use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess};
#[storage]
struct Storage {
map: Map::<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::{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 halts the execution with the given error value. It should be used for complex condition checks and for internal errors. It's similar to therevert
statement in Solidity. You can usepanic_with_felt252
to directly pass afelt252
as the error value.
The assert_eq!
, assert_ne!
, assert_lt!
, assert_le!
, assert_gt!
and assert_ge!
macros can be used as an assert
shorthand to compare two values, but only in tests. In contracts, you should only use the assert
function.
Here's a simple example that demonstrates the use of these functions:
#[starknet::interface]
pub trait IErrorsExample<TContractState> {
fn test_assert(self: @TContractState, i: u256);
fn test_panic(self: @TContractState, i: u256);
}
#[starknet::contract]
pub mod ErrorsExample {
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl ErrorsExample of super::IErrorsExample<ContractState> {
fn test_assert(self: @ContractState, i: u256) {
// Assert used to validate a condition
// and abort execution if the condition is not met
assert(i > 0, 'i must be greater than 0');
}
fn test_panic(self: @ContractState, i: u256) {
if (i == 0) {
// Panic used to abort execution directly
core::panic_with_felt252('i must not be 0');
}
}
}
}
#[cfg(test)]
mod test {
use super::{ErrorsExample, IErrorsExampleDispatcher, IErrorsExampleDispatcherTrait};
use starknet::{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::{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 starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
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::{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 that emits an event each time a counter is incremented by the increment
function:
#[starknet::interface]
pub trait IEventCounter<TContractState> {
fn increment(ref self: TContractState, amount: u128);
}
#[starknet::contract]
pub mod EventCounter {
use starknet::{get_caller_address, ContractAddress};
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
// Counter value
pub counter: u128,
}
#[event]
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
// The event enum must be annotated with the `#[event]` attribute.
// It must also derive at least the `Drop` and `starknet::Event` traits.
pub enum Event {
CounterIncreased: CounterIncreased,
UserIncreaseCounter: UserIncreaseCounter
}
// By deriving the `starknet::Event` trait, we indicate to the compiler that
// this struct will be used when emitting events.
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct CounterIncreased {
pub amount: u128
}
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct UserIncreaseCounter {
// The `#[key]` attribute indicates that this event will be indexed.
// You can also use `#[flat]` for nested structs.
#[key]
pub user: ContractAddress,
pub new_value: u128,
}
#[abi(embed_v0)]
impl EventCounter of super::IEventCounter<ContractState> {
fn increment(ref self: ContractState, amount: u128) {
self.counter.write(self.counter.read() + amount);
// Emit event
self.emit(Event::CounterIncreased(CounterIncreased { amount }));
self
.emit(
Event::UserIncreaseCounter(
UserIncreaseCounter {
user: get_caller_address(), new_value: self.counter.read()
}
)
);
}
}
}
#[cfg(test)]
mod tests {
use super::{
EventCounter, EventCounter::{Event, CounterIncreased, UserIncreaseCounter},
IEventCounterDispatcherTrait, IEventCounterDispatcher
};
use starknet::{contract_address_const, SyscallResultTrait, syscalls::deploy_syscall};
use starknet::testing::set_contract_address;
use starknet::storage::StoragePointerReadAccess;
#[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 Success
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, and if the call fails, the whole transaction will revert.
This is not the recommended way to call a contract. Instead, use the dispatcher generated from the contract interface as shown in the Calling other contracts chapter.
deploy
fn deploy_syscall(
class_hash: ClassHash,
contract_address_salt: felt252,
calldata: Span<felt252>,
deploy_from_zero: bool,
) -> SyscallResult<(ContractAddress, Span::<felt252>)>
Deploy a new contract of the predeclared class class_hash
with calldata
.
The success result is a tuple containing the deployed contract address and the return value of the constructor.
contract_address_salt
and deploy_from_zero
are used to compute the contract address.
Example of the usage of the deploy
syscall from the Factory pattern:
pub use starknet::{ContractAddress, ClassHash};
#[starknet::interface]
pub trait ICounterFactory<TContractState> {
/// Create a new counter contract from stored arguments
fn create_counter(ref self: TContractState) -> ContractAddress;
/// Create a new counter contract from the given arguments
fn create_counter_at(ref self: TContractState, init_value: u128) -> ContractAddress;
/// Update the argument
fn update_init_value(ref self: TContractState, init_value: u128);
/// Update the class hash of the Counter contract to deploy when creating a new counter
fn update_counter_class_hash(ref self: TContractState, counter_class_hash: ClassHash);
}
#[starknet::contract]
pub mod CounterFactory {
use starknet::{ContractAddress, ClassHash, SyscallResultTrait, syscalls::deploy_syscall};
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[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 {
// Constructor 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, ClassHash, 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 {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[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::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};
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
// Counter value
pub counter: u128,
}
#[event]
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
// The event enum must be annotated with the `#[event]` attribute.
// It must also derive at least the `Drop` and `starknet::Event` traits.
pub enum Event {
CounterIncreased: CounterIncreased,
UserIncreaseCounter: UserIncreaseCounter
}
// By deriving the `starknet::Event` trait, we indicate to the compiler that
// this struct will be used when emitting events.
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct CounterIncreased {
pub amount: u128
}
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct UserIncreaseCounter {
// The `#[key]` attribute indicates that this event will be indexed.
// You can also use `#[flat]` for nested structs.
#[key]
pub user: ContractAddress,
pub new_value: u128,
}
#[abi(embed_v0)]
impl EventCounter of super::IEventCounter<ContractState> {
fn increment(ref self: ContractState, amount: u128) {
self.counter.write(self.counter.read() + amount);
// Emit event
self.emit(Event::CounterIncreased(CounterIncreased { amount }));
self
.emit(
Event::UserIncreaseCounter(
UserIncreaseCounter {
user: get_caller_address(), new_value: self.counter.read()
}
)
);
}
}
}
#[cfg(test)]
mod tests {
use super::{
EventCounter, EventCounter::{Event, CounterIncreased, UserIncreaseCounter},
IEventCounterDispatcherTrait, IEventCounterDispatcher
};
use starknet::{contract_address_const, SyscallResultTrait, syscalls::deploy_syscall};
use starknet::testing::set_contract_address;
use starknet::storage::StoragePointerReadAccess;
#[test]
fn test_increment_events() {
let (contract_address, _) = deploy_syscall(
EventCounter::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
let mut contract = IEventCounterDispatcher { contract_address };
let state = @EventCounter::contract_state_for_testing();
let amount = 10;
let caller = contract_address_const::<'caller'>();
// fake caller
set_contract_address(caller);
contract.increment(amount);
// set back to the contract for reading state
set_contract_address(contract_address);
assert_eq!(state.counter.read(), amount);
// Notice the order: the first event emitted is the first to be popped.
assert_eq!(
starknet::testing::pop_log(contract_address),
Option::Some(Event::CounterIncreased(CounterIncreased { amount }))
);
assert_eq!(
starknet::testing::pop_log(contract_address),
Option::Some(
Event::UserIncreaseCounter(UserIncreaseCounter { user: caller, new_value: amount })
)
);
}
}
library_call
fn library_call_syscall(
class_hash: ClassHash, function_selector: felt252, calldata: Span<felt252>
) -> SyscallResult<Span<felt252>>
Call the function function_selector
of the class class_hash
with calldata
.
This is analogous to a delegate call in Ethereum, but only a single class is called.
send_message_to_L1
fn send_message_to_l1_syscall(
to_address: felt252, payload: Span<felt252>
) -> SyscallResult<()>
Send a message to the L1 contract at to_address
with the given payload
.
replace_class
fn replace_class_syscall(
class_hash: ClassHash
) -> SyscallResult<()>
Replace the class of the calling contract with the class class_hash
.
This is used for contract upgrades. Here's an example from the Upgradeable Contract:
use starknet::class_hash::ClassHash;
#[starknet::interface]
pub trait IUpgradeableContract<TContractState> {
fn upgrade(ref self: TContractState, impl_hash: ClassHash);
fn version(self: @TContractState) -> u8;
}
#[starknet::contract]
pub mod UpgradeableContract_V0 {
use starknet::class_hash::ClassHash;
use starknet::SyscallResultTrait;
use core::num::traits::Zero;
#[storage]
struct Storage {}
#[event]
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub enum Event {
Upgraded: Upgraded,
}
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct Upgraded {
pub implementation: ClassHash
}
#[abi(embed_v0)]
impl UpgradeableContract of super::IUpgradeableContract<ContractState> {
fn upgrade(ref self: ContractState, impl_hash: ClassHash) {
assert(impl_hash.is_non_zero(), 'Class hash cannot be zero');
starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall();
self.emit(Event::Upgraded(Upgraded { implementation: impl_hash }))
}
fn version(self: @ContractState) -> u8 {
0
}
}
}
The new class code will only be used for future calls to the contract.
The current transaction containing the replace_class
syscall will continue to use the old class code. Note that you can explicitly use the new class code in the same transaction by calling call_contract
after the replace_class
syscall.
storage_read
fn storage_read_syscall(
address_domain: u32, address: StorageAddress,
) -> SyscallResult<felt252>
This low-level syscall is used to get the value in the storage of a specific key at address
in the address_domain
.
address_domain
is used to distinguish between data availability modes.
Currently, only mode ONCHAIN
(0
) is supported.
storage_write
fn storage_write_syscall(
address_domain: u32, address: StorageAddress, value: felt252
) -> SyscallResult<()>
Similar to storage_read
, this low-level syscall is used to write the value value
in the storage of a specific key at address
in the address_domain
.
Documentation
Syscalls are defined in starknet::syscall
.
You can also read the official documentation page for more details.
Strings and ByteArrays
In Cairo, there's no native type for strings. Instead, you can use a single felt252
to store a short string
or a ByteArray
for strings of arbitrary length.
Short strings
Each character is encoded on 8 bits following the ASCII standard, so it's possible to store up to 31 characters in a single felt252
.
Short strings are declared with single quotes, like this: 'Hello, World!'
.
See the Felt section for more information about short strings with the felt252
type.
Notice that any short string only use up to 31 bytes, so it's possible to represent any short string with
bytes31
.
ByteArray (Long strings)
The ByteArray
struct is used to store strings of arbitrary length. It contains a field data
of type Array<bytes31>
to store a sequence of short strings.
ByteArrays are declared with double quotes, like this: "Hello, World!"
.
They can be stored in the contract's storage and passed as arguments to entrypoints.
#[starknet::interface]
pub trait IMessage<TContractState> {
fn append(ref self: TContractState, str: ByteArray);
fn prepend(ref self: TContractState, str: ByteArray);
}
#[starknet::contract]
pub mod MessageContract {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
pub 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 starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use bytearray::bytearray::{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. It is enough to just derive this trait, unless our custom type contains arrays or dictionaries.
#[starknet::interface]
pub trait IStoringCustomType<TContractState> {
fn set_person(ref self: TContractState, person: Person);
fn set_name(ref self: TContractState, name: felt252);
}
// 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 starknet::storage::StoragePointerWriteAccess;
use super::Person;
#[storage]
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);
}
fn set_name(ref self: ContractState, name: felt252) {
self.person.name.write(name);
}
}
}
#[cfg(test)]
mod tests {
use super::{IStoringCustomType, StoringCustomType, Person,};
use starknet::storage::StoragePointerReadAccess;
#[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_eq!(person.age, read_person.age);
assert_eq!(person.name, read_person.name);
}
#[test]
fn can_call_set_name() {
let mut state = StoringCustomType::contract_state_for_testing();
state.set_name('John');
let read_person = state.person.read();
assert_eq!(read_person.name, 'John');
}
}
Note that it is also possible to individually access the members of the stored struct.
This is possible because deriving the Store
trait also generates the corresponding StoragePointer
for each member.
#[starknet::interface]
pub trait IStoringCustomType<TContractState> {
fn set_person(ref self: TContractState, person: Person);
fn set_name(ref self: TContractState, name: felt252);
}
// 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 starknet::storage::StoragePointerWriteAccess;
use super::Person;
#[storage]
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);
}
fn set_name(ref self: ContractState, name: felt252) {
self.person.name.write(name);
}
}
}
#[cfg(test)]
mod tests {
use super::{IStoringCustomType, StoringCustomType, Person,};
use starknet::storage::StoragePointerReadAccess;
#[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_eq!(person.age, read_person.age);
assert_eq!(person.name, read_person.name);
}
#[test]
fn can_call_set_name() {
let mut state = StoringCustomType::contract_state_for_testing();
state.set_name('John');
let read_person = state.person.read();
assert_eq!(read_person.name, 'John');
}
}
Custom types in entrypoints
Using custom types in entrypoints requires our type to implement the Serde
trait. This is because when calling an entrypoint, the input is sent as an array of felt252
to the entrypoint, and we need to be able to deserialize it into our custom type. Similarly, when returning a custom type from an entrypoint, we need to be able to serialize it into an array of felt252
.
Thankfully, we can just derive the Serde
trait for our custom type.
#[starknet::interface]
pub trait ISerdeCustomType<TContractState> {
fn person_input(ref self: TContractState, person: Person);
fn person_output(self: @TContractState) -> Person;
}
// Deriving the `Serde` trait allows us to use
// the `Person` type as an entrypoint parameter and as a return value
#[derive(Drop, Serde)]
pub struct Person {
pub age: u8,
pub name: felt252
}
#[starknet::contract]
pub mod SerdeCustomType {
use super::Person;
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl SerdeCustomType of super::ISerdeCustomType<ContractState> {
fn person_input(ref self: ContractState, person: Person) {}
fn person_output(self: @ContractState) -> Person {
Person { age: 10, name: 'Joe' }
}
}
}
#[cfg(test)]
mod tests {
use super::{
SerdeCustomType, Person, ISerdeCustomTypeDispatcher, ISerdeCustomTypeDispatcherTrait
};
use starknet::{syscalls::deploy_syscall, SyscallResultTrait};
fn deploy() -> ISerdeCustomTypeDispatcher {
let (contract_address, _) = deploy_syscall(
SerdeCustomType::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
ISerdeCustomTypeDispatcher { contract_address }
}
#[test]
fn should_deploy() {
deploy();
}
#[test]
fn should_get_person_output() {
let contract = deploy();
let expected_person = Person { age: 10, name: 'Joe' };
let received_person = contract.person_output();
let age_received = received_person.age;
let name_received = received_person.name;
assert(age_received == expected_person.age, 'Wrong age value');
assert(name_received == expected_person.name, 'Wrong name value');
}
#[test]
#[available_gas(2000000000)]
fn should_call_person_input() {
let contract = deploy();
let expected_person = Person { age: 10, name: 'Joe' };
contract.person_input(expected_person);
}
}
Note: The purpose of this example is to demonstrate the ability to use custom types as inputs and outputs in contract calls. For simplicity, we are not using getters and setters to manage the contract's state.
Documentation
It's important to take the time to document your code. It will help developers and users to understand the contract and its functionalities.
In Cairo, you can add comments with //
.
Best Practices
Since Cairo 1, the community has adopted a Rust-like documentation style.
Contract Interface
In smart contracts, you will often have a trait that defines the contract's interface (with #[starknet::interface]
).
This is the perfect place to include detailed documentation explaining the purpose and functionality of the contract entry points. You can follow this template:
#[starknet::interface]
trait IContract<TContractState> {
/// High-level description of the function
///
/// # Arguments
///
/// * `arg_1` - Description of the argument
/// * `arg_n` - ...
///
/// # Returns
///
/// High-level description of the return value
fn do_something(ref self: TContractState, arg_1: T_arg_1) -> T_return;
}
Keep in mind that this should not describe the implementation details of the function, but rather the high-level purpose and functionality of the contract from the perspective of a user.
Implementation Details
When writing the contract logic, you can add comments to describe the technical implementation details of the functions.
Avoid over-commenting: Comments should provide additional value and clarity.
Deploy and interact with contracts
In this chapter, we will see how to deploy and interact with contracts.
Declaring and Deploying Your Contract
We will use Starkli to declare and deploy a smart contract on Starknet. Make sure that Starkli is installed on your device. You can check out the starkli book for more information.
We will need an account, so first we will create one. If you already have one, you can skip this step and move directly to the part where we declare our contract.
Creating a new account:
You should move to the directory where you want to access your account keystores, and then create a new folder for the wallet.
$ mkdir ./starkli-wallet
Create a new signer. You will be instructed to enter a password to encrypt your private key:
$ starkli signer keystore new ./starkli-wallet/keystore.json
After this command, the path of the encrypted keystore file is shown which will be needed during the declaration and deployment of the contract.
Export the keystore path in order not to call --keystore
in every command:
$ export STARKNET_KEYSTORE="./starkli-wallet/keystore.json"
Initialize the account with the following command using OpenZeppelin's class deployed on Starknet.
$ starkli account oz init ./starkli-wallet/account.json
After this command, the address of the account is shown once it is deployed along with the deploy command. Deploy the account:
$ starkli account deploy ./starkli-wallet/account.json
This command wants you to fund the address (given in the instructions below the command) in order to deploy the account on the Starknet Sepolia Testnet. We need Starknet Sepolia testnet ethers which could be obtained from this faucet.
Once the transaction is confirmed on the faucet page, press ENTER, and the account will be deployed on Starknet Sepolia! Try to find your account on Voyager Sepolia!
Declaring & Deploying your Contract:
Firstly, you need to declare your contract which will create a class on Starknet Sepolia. Note that we will use the Sierra program in ./target/ProjectName_ContractName.contract_class.json
in your Scarb folder.
If you are deploying a contract code that is already used, you can skip the declaration step because the class hash is already declared on the network. One example of this is when you are deploying common contract instances such as ERC20 or ERC721 contracts.
Note: The command below is written to run in the directory of the Scarb folder.
$ starkli declare \
--keystore /path/to/starkli-wallet/keystore.json \
--account /path/to/starkli-wallet/account.json \
--watch ./target/dev/simple_storage_SimpleStorage.contract_class.json
After this command, the class hash for your contract is declared. You should be able to find the hash under the command:
Class hash declared:
0x05c8c21062a74e3c8f2015311d7431e820a08a6b0a9571422b607429112d2eb4
Check the Voyager Class Page.
Now, it's time to deploy the contract. Add the class hash given above after --watch
:
$ starkli deploy \
--keystore /path/to/starkli-wallet/keystore.json \
--account /path/to/starkli-wallet/account.json \
--watch 0x05c8c21062a74e3c8f2015311d7431e820a08a6b0a9571422b607429112d2eb4
You should now see the address of the deployed contract. Congratulations, you have deployed your contract on Starknet Sepolia Testnet! Check the Voyager Contract Page to see your contract! Additionally, you can also find all contract instances of a given class on the Voyager Class Page as well, for example, this page.
Contract interfaces and Traits generation
Contract interfaces define the structure and behavior of a contract, serving as the contract's public ABI. They list all the function signatures that a contract exposes. For a detailed explanation of interfaces, you can refer to the Cairo Book.
In Cairo, to specify the interface you need to define a trait annotated with #[starknet::interface]
and then implement that trait in the contract.
When a function needs to access the contract state, it must have a self
parameter of type ContractState
. This implies that the corresponding function signature in the interface trait must also take a TContractState
type as a parameter. It's important to note that every function in the contract interface must have this self
parameter of type TContractState
.
You can use the #[generate_trait]
attribute to implicitly generate the trait for a specific implementation block. This attribute automatically generates a trait with the same functions as the ones in the implemented block, replacing the self
parameter with a generic TContractState
parameter. However, you will need to annotate the block with the #[abi(per_item)]
attribute, and each function with the appropriate attribute depending on whether it's an external function, a constructor or an L1 handler.
In summary, there's two ways to handle interfaces:
- Explicitly, by defining a trait annotated with
#[starknet::interface]
- Implicitly, by using
#[generate_trait]
combined with the#[abi(per_item)]
attributes, and annotating each function inside the implementation block with the appropriate attribute.
Explicit interface
#[starknet::interface]
pub trait IExplicitInterfaceContract<TContractState> {
fn get_value(self: @TContractState) -> u32;
fn set_value(ref self: TContractState, value: u32);
}
#[starknet::contract]
pub mod ExplicitInterfaceContract {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[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::{
ExplicitInterfaceContract, IExplicitInterfaceContractDispatcher,
IExplicitInterfaceContractDispatcherTrait
};
use starknet::{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 {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[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::IImplicitInterfaceContract};
use starknet::{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 {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[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::{
ImplicitInternalContract, IImplicitInternalContractDispatcher,
IImplicitInternalContractDispatcherTrait
};
use starknet::{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 chapter.
In order to call other contracts using dispatchers, you will need to define the called contract's interface as a trait annotated with the #[starknet::interface]
attribute, and then import the IContractDispatcher
and IContractDispatcherTrait
items in your contract.
Here's the Callee
contract interface and implementation:
// This will automatically generate ICalleeDispatcher and ICalleeDispatcherTrait
#[starknet::interface]
pub trait ICallee<TContractState> {
fn set_value(ref self: TContractState, value: u128) -> u128;
}
#[starknet::contract]
pub mod Callee {
use starknet::storage::StoragePointerWriteAccess;
#[storage]
struct Storage {
pub 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, Caller, ICallerDispatcher, ICallerDispatcherTrait};
use starknet::{testing::set_contract_address, syscalls::deploy_syscall, SyscallResultTrait};
use starknet::storage::StoragePointerReadAccess;
fn deploy() -> (ICalleeDispatcher, ICallerDispatcher) {
let (address_callee, _) = deploy_syscall(
Callee::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
let (address_caller, _) = deploy_syscall(
Caller::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
(
ICalleeDispatcher { contract_address: address_callee },
ICallerDispatcher { contract_address: address_caller }
)
}
#[test]
fn test_caller() {
let init_value: u128 = 42;
let (callee, caller) = deploy();
caller.set_value_from_address(callee.contract_address, init_value);
let state = @Callee::contract_state_for_testing();
set_contract_address(callee.contract_address);
let value_read: u128 = state.value.read();
assert_eq!(value_read, init_value);
}
}
The following Caller
contract uses the Callee
dispatcher to call the Callee
contract:
// This will automatically generate ICalleeDispatcher and ICalleeDispatcherTrait
#[starknet::interface]
pub trait ICallee<TContractState> {
fn set_value(ref self: TContractState, value: u128) -> u128;
}
#[starknet::contract]
pub mod Callee {
use starknet::storage::StoragePointerWriteAccess;
#[storage]
struct Storage {
pub 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, Caller, ICallerDispatcher, ICallerDispatcherTrait};
use starknet::{testing::set_contract_address, syscalls::deploy_syscall, SyscallResultTrait};
use starknet::storage::StoragePointerReadAccess;
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 has the sole responsibility of creating and managing other contracts.
Class hash and contract instance
In Starknet, there's a separation between contract's classes and instances. A contract class serves as a blueprint, defined by the underlying Cairo bytecode, contract's entrypoints, ABI and Sierra program hash. The contract class is identified by a class hash. When you want to add a new class to the network, you first need to declare it.
When deploying a contract, you need to specify the class hash of the contract you want to deploy. Each instance of a contract has their own storage regardless of the class hash.
Using the factory pattern, we can deploy multiple instances of the same contract class and handle upgrades easily.
Minimal example
Here's a minimal example of a factory contract that deploys the SimpleCounter
contract:
pub use starknet::{ContractAddress, ClassHash};
#[starknet::interface]
pub trait ICounterFactory<TContractState> {
/// Create a new counter contract from stored arguments
fn create_counter(ref self: TContractState) -> ContractAddress;
/// Create a new counter contract from the given arguments
fn create_counter_at(ref self: TContractState, init_value: u128) -> ContractAddress;
/// Update the argument
fn update_init_value(ref self: TContractState, init_value: u128);
/// Update the class hash of the Counter contract to deploy when creating a new counter
fn update_counter_class_hash(ref self: TContractState, counter_class_hash: ClassHash);
}
#[starknet::contract]
pub mod CounterFactory {
use starknet::{ContractAddress, ClassHash, SyscallResultTrait, syscalls::deploy_syscall};
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[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 {
// Constructor 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, ClassHash, 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 {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[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::ISimpleCounterDispatcherTrait;
/// Deploy a counter factory contract
fn deploy_factory(
counter_class_hash: ClassHash, init_value: u128
) -> ICounterFactoryDispatcher {
let mut constructor_calldata: Array::<felt252> = array![
init_value.into(), counter_class_hash.into()
];
let (contract_address, _) = deploy_syscall(
CounterFactory::TEST_CLASS_HASH.try_into().unwrap(),
0,
constructor_calldata.span(),
false
)
.unwrap_syscall();
ICounterFactoryDispatcher { contract_address }
}
#[test]
fn test_deploy_counter_constructor() {
let init_value = 10;
let counter_class_hash: ClassHash = target::SimpleCounter::TEST_CLASS_HASH
.try_into()
.unwrap();
let factory = deploy_factory(counter_class_hash, init_value);
let counter_address = factory.create_counter();
let counter = target::ISimpleCounterDispatcher { contract_address: counter_address };
assert_eq!(counter.get_current_count(), init_value);
}
#[test]
fn test_deploy_counter_argument() {
let init_value = 10;
let argument_value = 20;
let counter_class_hash: ClassHash = target::SimpleCounter::TEST_CLASS_HASH
.try_into()
.unwrap();
let factory = deploy_factory(counter_class_hash, init_value);
let counter_address = factory.create_counter_at(argument_value);
let counter = target::ISimpleCounterDispatcher { contract_address: counter_address };
assert_eq!(counter.get_current_count(), argument_value);
}
#[test]
fn test_deploy_multiple() {
let init_value = 10;
let argument_value = 20;
let counter_class_hash: ClassHash = target::SimpleCounter::TEST_CLASS_HASH
.try_into()
.unwrap();
let factory = deploy_factory(counter_class_hash, init_value);
let mut counter_address = factory.create_counter();
let counter_1 = target::ISimpleCounterDispatcher { contract_address: counter_address };
counter_address = factory.create_counter_at(argument_value);
let counter_2 = target::ISimpleCounterDispatcher { contract_address: counter_address };
assert_eq!(counter_1.get_current_count(), init_value);
assert_eq!(counter_2.get_current_count(), argument_value);
}
}
This factory can be used to deploy multiple instances of the SimpleCounter
contract by calling the create_counter
and create_counter_at
functions.
The SimpleCounter
class hash is stored inside the factory, and can be upgraded with the update_counter_class_hash
function which allows to reuse the same factory contract when the SimpleCounter
contract is upgraded.
Note: This minimal example lacks several useful features such as access control, tracking of deployed contracts, events etc.
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};
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
pub value: u32,
pub 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::{get_contract_address, contract_address_const};
// Use starknet test utils to fake the contract_address
use starknet::testing::set_contract_address;
// Deploy the contract and return its dispatcher.
fn deploy(initial_value: u32) -> ISimpleContractDispatcher {
// Declare and deploy
let (contract_address, _) = deploy_syscall(
SimpleContract::TEST_CLASS_HASH.try_into().unwrap(),
0,
array![initial_value.into()].span(),
false
)
.unwrap_syscall();
// Return the dispatcher.
// The dispatcher allows to interact with the contract based on its interface.
ISimpleContractDispatcher { contract_address }
}
#[test]
fn test_deploy() {
let initial_value: u32 = 10;
let contract = deploy(initial_value);
assert_eq!(contract.get_value(), initial_value);
assert_eq!(contract.get_owner(), get_contract_address());
}
#[test]
fn test_set_as_owner() {
// Fake the contract address to owner
let owner = contract_address_const::<'owner'>();
set_contract_address(owner);
// When deploying the contract, the owner is the caller.
let contract = deploy(10);
assert_eq!(contract.get_owner(), owner);
// As the current caller is the owner, the value can be set.
let new_value: u32 = 20;
contract.set_value(new_value);
assert_eq!(contract.get_value(), new_value);
}
#[test]
#[should_panic]
fn test_set_not_owner() {
let owner = contract_address_const::<'owner'>();
set_contract_address(owner);
let contract = deploy(10);
// Fake the contract address to another address
let not_owner = contract_address_const::<'not owner'>();
set_contract_address(not_owner);
// As the current caller is not the owner, the value cannot be set.
let new_value: u32 = 20;
contract.set_value(new_value);
// Panic expected
}
#[test]
#[available_gas(150000)]
fn test_deploy_gas() {
deploy(10);
}
}
To define our test, we use scarb, which allows us to create a separate module guarded with #[cfg(test)]
. This ensures that the test module is only compiled when running tests using scarb test
.
Each test is defined as a function with the #[test]
attribute. You can also check if a test panics using the #[should_panic]
attribute.
As we are in the context of a smart contract, you can also set up the gas limit for a test by using the #[available_gas(X)]
. This is a great way to ensure that some of your contract's features stay under a certain gas limit!
Note: The term "gas" here refers to Sierra gas, not L1 gas.
Now, let's move on to the testing process:
- Use the
deploy
function logic to declare and deploy your contract - Use
assert
to verify that the contract behaves as expected in the given context- You can also use assertion macros:
assert_eq!
,assert_ne!
,assert_gt!
,assert_ge!
,assert_lt!
,assert_le!
- You can also use assertion macros:
If you haven't noticed yet, every example in this book has hidden tests, you can see them by clicking the "Show hidden lines" (eyes icon) on the top right of code blocks. You can also find a detailed explanation of testing in Cairo in The Cairo Book.
Using the contract state
You can use the Contract::contract_state_for_testing
function to access the contract state. This function is only available in the test environment and allows you to mutate and read the contract state directly.
This can be useful for testing internal functions, or specific state mutations that are not exposed to the contract's interface. You can either use it with a deployed contract or as a standalone state.
Here is an example of how to do the same previous test using the contract state:
#[cfg(test)]
mod tests_with_states {
// Only import the contract and implementation
use super::SimpleContract;
use SimpleContract::SimpleContractImpl;
use starknet::contract_address_const;
use starknet::testing::set_caller_address;
use core::num::traits::Zero;
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[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::{SyscallResultTrait, syscalls::deploy_syscall, testing::set_contract_address};
#[test]
fn test_state_with_contract() {
let owner = contract_address_const::<'owner'>();
let not_owner = contract_address_const::<'not owner'>();
// Deploy as owner
let initial_value: u32 = 10;
set_contract_address(owner);
let (contract_address, _) = deploy_syscall(
SimpleContract::TEST_CLASS_HASH.try_into().unwrap(),
0,
array![initial_value.into()].span(),
false
)
.unwrap_syscall();
let mut contract = ISimpleContractDispatcher { contract_address };
// create the state
// - Set back as not owner
set_contract_address(not_owner);
let mut state = SimpleContract::contract_state_for_testing();
// - Currently, the state is not 'linked' to the contract
assert_ne!(state.get_value(), initial_value);
assert_ne!(state.get_owner(), owner);
// - Link the state to the contract by setting the contract address
set_contract_address(contract.contract_address);
assert_eq!(state.get_value(), initial_value);
assert_eq!(state.get_owner(), owner);
// Mutating the state from the contract changes the testing state
set_contract_address(owner);
let new_value: u32 = 20;
contract.set_value(new_value);
set_contract_address(contract.contract_address);
assert_eq!(state.get_value(), new_value);
// Mutating the state from the testing state changes the contract state
set_caller_address(owner);
state.set_value(initial_value);
assert_eq!(contract.get_value(), initial_value);
// Directly mutating the state allows to change state
// in ways that are not allowed by the contract, such as changing the owner.
let new_owner = contract_address_const::<'new owner'>();
state.owner.write(new_owner);
assert_eq!(contract.get_owner(), new_owner);
set_caller_address(new_owner);
state.set_value(new_value);
assert_eq!(contract.get_value(), new_value);
}
}
Testing events
In order to test events, you need to use the starknet::pop_log
function. If the contract did not emit any events, the function will return Option::None
.
See the test for the Events section:
#[starknet::interface]
pub trait IEventCounter<TContractState> {
fn increment(ref self: TContractState, amount: u128);
}
#[starknet::contract]
pub mod EventCounter {
use starknet::{get_caller_address, ContractAddress};
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
// Counter value
pub counter: u128,
}
#[event]
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
// The event enum must be annotated with the `#[event]` attribute.
// It must also derive at least the `Drop` and `starknet::Event` traits.
pub enum Event {
CounterIncreased: CounterIncreased,
UserIncreaseCounter: UserIncreaseCounter
}
// By deriving the `starknet::Event` trait, we indicate to the compiler that
// this struct will be used when emitting events.
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct CounterIncreased {
pub amount: u128
}
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct UserIncreaseCounter {
// The `#[key]` attribute indicates that this event will be indexed.
// You can also use `#[flat]` for nested structs.
#[key]
pub user: ContractAddress,
pub new_value: u128,
}
#[abi(embed_v0)]
impl EventCounter of super::IEventCounter<ContractState> {
fn increment(ref self: ContractState, amount: u128) {
self.counter.write(self.counter.read() + amount);
// Emit event
self.emit(Event::CounterIncreased(CounterIncreased { amount }));
self
.emit(
Event::UserIncreaseCounter(
UserIncreaseCounter {
user: get_caller_address(), new_value: self.counter.read()
}
)
);
}
}
}
#[cfg(test)]
mod tests {
use super::{
EventCounter, EventCounter::{Event, CounterIncreased, UserIncreaseCounter},
IEventCounterDispatcherTrait, IEventCounterDispatcher
};
use starknet::{contract_address_const, SyscallResultTrait, syscalls::deploy_syscall};
use starknet::testing::set_contract_address;
use starknet::storage::StoragePointerReadAccess;
#[test]
fn test_increment_events() {
let (contract_address, _) = deploy_syscall(
EventCounter::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
let mut contract = IEventCounterDispatcher { contract_address };
let state = @EventCounter::contract_state_for_testing();
let amount = 10;
let caller = contract_address_const::<'caller'>();
// fake caller
set_contract_address(caller);
contract.increment(amount);
// set back to the contract for reading state
set_contract_address(contract_address);
assert_eq!(state.counter.read(), amount);
// Notice the order: the first event emitted is the first to be popped.
assert_eq!(
starknet::testing::pop_log(contract_address),
Option::Some(Event::CounterIncreased(CounterIncreased { amount }))
);
assert_eq!(
starknet::testing::pop_log(contract_address),
Option::Some(
Event::UserIncreaseCounter(UserIncreaseCounter { user: caller, new_value: amount })
)
);
}
}
Starknet Corelib Testing Module
To make testing more convenient, the testing
module of the corelib provides some helpful functions:
set_caller_address(address: ContractAddress)
set_contract_address(address: ContractAddress)
set_block_number(block_number: u64)
set_block_timestamp(block_timestamp: u64)
set_account_contract_address(address: ContractAddress)
set_sequencer_address(address: ContractAddress)
set_version(version: felt252)
set_transaction_hash(hash: felt252)
set_chain_id(chain_id: felt252)
set_nonce(nonce: felt252)
set_signature(signature: felt252)
set_max_fee(fee: u128)
pop_log_raw(address: ContractAddress) -> Option<(Span<felt252>, Span<felt252>)>
pop_log<T, +starknet::Event<T>>(address: ContractAddress) -> Option<T>
pop_l2_to_l1_message(address: ContractAddress) -> Option<(felt252, Span<felt252>)>
You may also need the info
module from the corelib, which allows you to access information about the current execution context (see syscalls):
get_caller_address() -> ContractAddress
get_contract_address() -> ContractAddress
get_block_info() -> Box<BlockInfo>
get_tx_info() -> Box<TxInfo>
get_block_timestamp() -> u64
get_block_number() -> u64
You can find the full list of functions in the Starknet Corelib repo.
Starknet Foundry
Starknet Foundry is a powerful toolkit for developing smart contracts on Starknet. It offers support for testing Starknet smart contracts on top of scarb
with the Forge
tool.
Testing with snforge
is similar to the process we just described, but simplified. Moreover, additional features are on the way, including cheatcodes and parallel test execution. We highly recommend exploring Starknet Foundry and incorporating it into your projects.
For more detailed information about testing contracts with Starknet Foundry, check out the Starknet Foundry Book - Testing Contracts.
Cairo Cheatsheet
This chapter aims to provide a quick reference for the most common Cairo constructs.
Felt
felt252
is a fundamental data type in Cairo from which all other data types are derived.
felt252
can also be used to store short string representations with a maximum length of 31 characters.
For example:
let felt: felt252 = 100;
let felt_as_str = 'Hello Starknet!';
let _felt = felt + felt_as_str;
Map
The Map
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;
use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess};
#[storage]
struct Storage {
students_name: Map::<ContractAddress, felt252>,
students_result_record: Map::<(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, and 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
Enums
Just like other programming languages, enums (enumerations) are used in cairo to define variables that can only hold a set of predefined variants (= enum options), enhancing code readability and safety. They facilitate strong type checking and are ideal for organizing related options and supporting structured logic through pattern matching for example, which is also described in the next chapter.
In cairo, enum variants
can hold different data types (the unit type, structs, other enums, tuples, default core library types, arrays, dictionaries, ...), as shown in the code snippet below. Furthermore, as a quick reminder, enums are expressions, meaning they can return values.
#[derive(Drop, Serde, Copy, starknet::Store)]
struct Position {
x: u32,
y: u32,
}
#[derive(Drop, Serde, Copy, starknet::Store)]
enum UserCommand {
Login,
UpdateProfile,
Logout,
}
#[derive(Drop, Serde, Copy, starknet::Store)]
enum Action {
Quit,
Move: Position,
SendMessage: felt252,
ChangeAvatarColor: (u8, u8, u8),
ProfileState: UserCommand
}
Enums can be declared both inside and outside a contract. If declared outside, they need to be imported inside using the use
keyword, just like other imports.
-
Storing enums in contract
-
It is possible to store
enums
in the contract storage. But unlike most of the core library types which implement theStore
trait, enums are custom types and therefore do not automatically implement theStore
trait. The enum as well as all of its variants have to explicitly implement theStore
trait in order for it to be stored inside a contract storage. -
If all of its variants implement the
Store
trait, implementing theStore
trait on the enum is as simple as deriving it, using#[derive(starknet::Store)]
(as shown in example above). If not, theStore
trait has to be manually implemented -- see an example of manually implementing theStore
trait for a complex type in chapter Storing Arrays.
-
-
Enums as parameters and return values to entrypoints
- It is possible to pass
enums
to contract entrypoints as parameters, as well as return them from entrypoints. For that purpose, the enum needs to be serializable and droppable, hence the derivation of traitsSerde
andDrop
in the above code snippet.
- It is possible to pass
Here is an example of a contract illustrating the above statements :
#[starknet::interface]
trait IEnumContract<TContractState> {
fn register_action(ref self: TContractState, action: Action);
fn generate_default_actions_list(self: @TContractState) -> Array<Action>;
}
#[starknet::contract]
mod EnumContract {
use starknet::storage::StoragePointerWriteAccess;
use super::IEnumContract;
use super::{Action, Position, UserCommand};
#[storage]
struct Storage {
most_recent_action: Action,
}
#[abi(embed_v0)]
impl IEnumContractImpl of IEnumContract<ContractState> {
fn register_action(ref self: ContractState, action: Action) {
// quick note: match takes ownership of variable (but enum Action implements Copy trait)
match action {
Action::Quit => { println!("Quit"); },
Action::Move(value) => { println!("Move with x: {} and y: {}", value.x, value.y); },
Action::SendMessage(msg) => { println!("Write with message: {}", msg); },
Action::ChangeAvatarColor((
r, g, b
)) => { println!("Change color to r: {}, g: {}, b: {}", r, g, b); },
Action::ProfileState(state) => {
let profile_state = match state {
UserCommand::Login => 1,
UserCommand::UpdateProfile => 2,
UserCommand::Logout => 3,
};
println!("profile_state: {}", profile_state);
}
};
self.most_recent_action.write(action);
}
fn generate_default_actions_list(self: @ContractState) -> Array<Action> {
let actions = array![
Action::Quit,
Action::Move(Position { x: 1, y: 2 }),
Action::SendMessage('here is my message'),
Action::ChangeAvatarColor((1, 2, 3)),
Action::ProfileState(UserCommand::Login),
];
actions
}
}
}
Match
The match
expression in Cairo allows us to control the flow of our code by comparing a felt252
data type or an enum against various patterns and then running specific code based on the pattern that matches.
For example:
#[derive(Drop, Serde)]
enum Colour {
Red,
Blue,
Green,
Orange,
Black
}
#[derive(Drop, Serde)]
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> felt252 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn specified_colour(colour: Colour) -> felt252 {
let mut response: felt252 = '';
match colour {
Colour::Red => { response = 'You passed in Red'; },
Colour::Blue => { response = 'You passed in Blue'; },
Colour::Green => { response = 'You passed in Green'; },
Colour::Orange => { response = 'You passed in Orange'; },
Colour::Black => { response = 'You passed in Black'; },
};
response
}
fn quiz(num: felt252) -> felt252 {
let mut response: felt252 = '';
match num {
0 => { response = 'You failed' },
_ => { response = 'You Passed' },
};
response
}
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 a tuple. Like tuples, they can be used to hold data of different types.
For example:
// With Store, you can store Data's structs in the storage part of contracts.
#[derive(Drop, starknet::Store)]
struct Data {
address: starknet::ContractAddress,
age: u8
}
Type casting
Cairo supports the conversion from one scalar type to another by using the into
and try_into
methods.
The into
method is used for conversion from a smaller data type to a larger data type, while try_into
is used when converting from a larger to a smaller type that might not fit.
For example:
let a_number: u32 = 15;
let my_felt252 = 15;
// Since a u32 might not fit in a u8 and a u16, we need to use try_into,
// then unwrap the Option<T> type thats returned.
let _new_u8: u8 = a_number.try_into().unwrap();
let new_u16: u16 = a_number.try_into().unwrap();
// since new_u32 is the of the same type (u32) as rand_number, we can directly assign them,
// or use the .into() method.
let _new_u32: u32 = a_number;
// When typecasting from a smaller size to an equal or larger size we use the .into() method.
// Note: u64 and u128 are larger than u32, so a u32 type will always fit into them.
let _new_u64: u64 = a_number.into();
let _new_u128: u128 = a_number.into();
// Since a felt252 is smaller than a u256, we can use the into() method
let _new_u256: u256 = my_felt252.into();
let _new_felt252: felt252 = new_u16.into();
// Note: usize is smaller than felt252, so we use try_into
let _new_usize: usize = my_felt252.try_into().unwrap();
Dictionary
A dictionary is a data structure used to store key-value pairs, enabling efficient data retrieval. The keys and values in a Cairo dictionary can be of various types, including Felt252. Dictionaries provide fast access to data, as they allow for quick lookups, insertions, and deletions based on the keys.The core functionality of a Felt252Dict<T>
is implemented in the trait Felt252DictTrait
, which includes all basic operations. Among them, we can find:
insert(felt252, T) -> ()
to write values to a dictionary instance.get(felt252) -> T
to read values from it.
For example:
use core::dict::Felt252Dict;
fn dict() {
let mut Auctions: Felt252Dict<u64> = Default::default();
Auctions.insert('Bola', 30);
Auctions.insert('Maria', 40);
let bola_balance = Auctions.get('Bola');
assert!(bola_balance == 30, "Bola balance should be 30");
let maria_balance = Auctions.get('Maria');
assert!(maria_balance == 40, "Maria balance should be 40");
}
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 {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
pub struct Storage {
switchable_value: bool,
}
#[derive(Drop, Debug, PartialEq, starknet::Event)]
pub struct SwitchEvent {}
#[event]
#[derive(Drop, Debug, PartialEq, starknet::Event)]
pub enum Event {
SwitchEvent: SwitchEvent,
}
#[embeddable_as(Switchable)]
impl SwitchableImpl<
TContractState, +HasComponent<TContractState>
> of super::ISwitchable<ComponentState<TContractState>> {
fn is_on(self: @ComponentState<TContractState>) -> bool {
self.switchable_value.read()
}
fn switch(ref self: ComponentState<TContractState>) {
self.switchable_value.write(!self.switchable_value.read());
self.emit(Event::SwitchEvent(SwitchEvent {}));
}
}
#[generate_trait]
pub impl SwitchableInternalImpl<
TContractState, +HasComponent<TContractState>
> of SwitchableInternalTrait<TContractState> {
fn _off(ref self: ComponentState<TContractState>) {
self.switchable_value.write(false);
}
}
}
A component is really similar to a contract and can also have:
- An interface defining entrypoints (
ISwitchableComponent<TContractState>
) - A Storage struct
- Events
- Internal functions
It doesn't have a constructor, but you can create an _init
internal function and call it from the contract's constructor. In the previous example, the _off
function will be used this way.
It's currently not possible to use the same component multiple times in the same contract. This is a known limitation that may be lifted in the future.
For now, you can view components as implementations of specific interfaces or features (
Ownable
,Upgradeable
, ...~able
). This is why we called the component in the above exampleSwitchable
, and notSwitch
; the contract is switchable, it does not have a switch.
How to use a component
Now that we have a component, we can use it in a contract.
The following contract incorporates the Switchable
component:
#[starknet::contract]
pub mod SwitchContract {
use super::switchable_component;
component!(path: switchable_component, storage: switch, event: SwitchableEvent);
#[abi(embed_v0)]
impl SwitchableImpl = switchable_component::Switchable<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
switch: switchable_component::Storage,
}
#[event]
#[derive(Drop, Debug, PartialEq, starknet::Event)]
pub enum Event {
SwitchableEvent: switchable_component::Event,
}
// You can optionally use the internal implementation of the component as well
impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl<ContractState>;
#[constructor]
fn constructor(ref self: ContractState) {
// Internal function call
self.switch._off();
}
}
How to test a component
In order to effectively test a component, you need to test it in the context of a contract.
A common practice is to declare a Mock
contract that has the only purpose of testing the component.
To test the Switchable
component, we can use the previous SwitchableContract
:
#[cfg(test)]
mod test {
use super::SwitchContract; // Used as a mock contract
use super::switchable_component::SwitchEvent;
use super::{ISwitchableDispatcher, ISwitchableDispatcherTrait};
use starknet::{syscalls::deploy_syscall, ContractAddress};
use starknet::SyscallResultTrait;
fn deploy() -> (ISwitchableDispatcher, ContractAddress) {
let (address, _) = deploy_syscall(
SwitchContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
(ISwitchableDispatcher { contract_address: address }, address)
}
#[test]
fn test_constructor() {
let (switchable, _) = deploy();
assert_eq!(switchable.is_on(), false);
}
#[test]
fn test_switch() {
let (switchable, contract_address) = deploy();
switchable.switch();
assert_eq!(switchable.is_on(), true);
assert_eq!(
starknet::testing::pop_log(contract_address),
Option::Some(SwitchContract::Event::SwitchableEvent(SwitchEvent {}.into()))
);
}
#[test]
fn test_multiple_switches() {
let (switchable, _) = deploy();
switchable.switch();
assert_eq!(switchable.is_on(), true);
switchable.switch();
assert_eq!(switchable.is_on(), false);
switchable.switch();
assert_eq!(switchable.is_on(), true);
}
}
Deep dive into components
You can find more in-depth information about components in The Cairo book - Components.
Component Dependencies
A component with a dependency on a trait T
can be used in a contract as long as the contract implements the trait T
.
We will use a new Countable
component as an example:
#[starknet::interface]
pub trait ICountable<TContractState> {
fn get(self: @TContractState) -> u32;
fn increment(ref self: TContractState);
}
#[starknet::component]
pub mod countable_component {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
pub struct Storage {
countable_value: u32,
}
#[embeddable_as(Countable)]
impl CountableImpl<
TContractState, +HasComponent<TContractState>
> of super::ICountable<ComponentState<TContractState>> {
fn get(self: @ComponentState<TContractState>) -> u32 {
self.countable_value.read()
}
fn increment(ref self: ComponentState<TContractState>) {
self.countable_value.write(self.countable_value.read() + 1);
}
}
}
#[starknet::contract]
mod CountableContract {
use super::countable_component;
component!(path: countable_component, storage: countable, event: CountableEvent);
#[storage]
struct Storage {
#[substorage(v0)]
countable: countable_component::Storage,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
CountableEvent: countable_component::Event
}
#[abi(embed_v0)]
impl CountableImpl = countable_component::Countable<ContractState>;
}
#[cfg(test)]
mod test {
use super::CountableContract;
use super::{ICountableDispatcher, ICountableDispatcherTrait};
use starknet::syscalls::deploy_syscall;
use starknet::SyscallResultTrait;
fn deploy_countable() -> ICountableDispatcher {
let (address, _) = deploy_syscall(
CountableContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
ICountableDispatcher { contract_address: address }
}
#[test]
fn test_constructor() {
let counter = deploy_countable();
assert_eq!(counter.get(), 0);
}
#[test]
fn test_increment() {
let counter = deploy_countable();
counter.increment();
assert_eq!(counter.get(), 1);
}
#[test]
fn test_multiple_increments() {
let counter = deploy_countable();
counter.increment();
counter.increment();
counter.increment();
assert_eq!(counter.get(), 3);
}
}
We want to add a way to enable or disable the counter, in a way that calling increment
on a disabled counter will not increment it.
But we don't want to add this switch logic to the Countable
component itself.
Instead, we add the trait Switchable
as a dependency to the Countable
component.
Implementation of the trait in the contract
First, we import the ISwitchable
trait defined in chapter "Components How-To":
pub trait ISwitchable<TContractState> {
fn is_on(self: @TContractState) -> bool;
fn switch(ref self: TContractState);
}
Then we can modify the implementation of the Countable
component to depend on the ISwitchable
trait:
#[starknet::component]
pub mod countable_component {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use components::countable::ICountable;
use components::switchable::ISwitchable;
#[storage]
pub struct Storage {
countable_value: u32,
}
#[embeddable_as(Countable)]
impl CountableImpl<
TContractState, +HasComponent<TContractState>, +ISwitchable<TContractState>
> of ICountable<ComponentState<TContractState>> {
fn get(self: @ComponentState<TContractState>) -> u32 {
self.countable_value.read()
}
fn increment(ref self: ComponentState<TContractState>) {
if (self.get_contract().is_on()) {
self.countable_value.write(self.countable_value.read() + 1);
}
}
}
}
#[starknet::contract]
mod MockContract {
use super::countable_component;
use components::switchable::ISwitchable;
component!(path: countable_component, storage: counter, event: CountableEvent);
#[storage]
struct Storage {
#[substorage(v0)]
counter: countable_component::Storage,
switch: bool,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
CountableEvent: countable_component::Event,
}
#[abi(embed_v0)]
impl CountableImpl = countable_component::Countable<ContractState>;
#[abi(embed_v0)]
impl Switchable of ISwitchable<ContractState> {
fn switch(ref self: ContractState) {}
fn is_on(self: @ContractState) -> bool {
true
}
}
}
#[cfg(test)]
mod test {
use super::MockContract;
use components::countable::{ICountableDispatcher, ICountableDispatcherTrait};
use starknet::syscalls::deploy_syscall;
use starknet::SyscallResultTrait;
fn deploy_countable() -> ICountableDispatcher {
let (contract_address, _) = deploy_syscall(
MockContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
ICountableDispatcher { contract_address: contract_address }
}
#[test]
fn test_get() {
let countable = deploy_countable();
assert_eq!(countable.get(), 0);
}
#[test]
fn test_increment() {
let countable = deploy_countable();
countable.increment();
assert_eq!(countable.get(), 1);
}
}
A contract that uses the Countable
component must implement the ISwitchable
trait:
#[starknet::contract]
mod CountableContract {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
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::{ICountableDispatcher, ICountableDispatcherTrait};
use components::switchable::{ISwitchableDispatcher, ISwitchableDispatcherTrait};
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]
fn test_init() {
let (mut counter, mut switch) = deploy();
assert_eq!(counter.get(), 0);
assert_eq!(switch.is_on(), false);
}
#[test]
fn test_increment_switch_off() {
let (mut counter, mut switch) = deploy();
counter.increment();
assert_eq!(counter.get(), 0);
assert_eq!(switch.is_on(), false);
}
#[test]
fn test_increment_switch_on() {
let (mut counter, mut switch) = deploy();
switch.switch();
assert_eq!(switch.is_on(), true);
counter.increment();
assert_eq!(counter.get(), 1);
}
#[test]
fn test_increment_multiple_switches() {
let (mut counter, mut switch) = deploy();
switch.switch();
counter.increment();
counter.increment();
counter.increment();
assert_eq!(counter.get(), 3);
switch.switch();
counter.increment();
counter.increment();
counter.increment();
switch.switch();
counter.increment();
counter.increment();
counter.increment();
assert_eq!(counter.get(), 6);
}
}
Implementation of the trait in another component
In the previous example, we implemented the ISwitchable
trait in the contract.
We already implemented a Switchable
component that provides an implementation of the ISwitchable
trait.
By using the Switchable
component in a contract, we can embed the implementation of the ISwitchable
trait in the contract and resolve the dependency on the ISwitchable
trait.
#[starknet::contract]
mod CountableContract {
use components_dependencies::countable_dep_switch::countable_component;
use components::switchable::switchable_component;
component!(path: countable_component, storage: counter, event: CountableEvent);
component!(path: switchable_component, storage: switch, event: SwitchableEvent);
#[abi(embed_v0)]
impl CountableImpl = countable_component::Countable<ContractState>;
#[abi(embed_v0)]
impl SwitchableImpl = switchable_component::Switchable<ContractState>;
impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
counter: countable_component::Storage,
#[substorage(v0)]
switch: switchable_component::Storage
}
#[constructor]
fn constructor(ref self: ContractState) {
self.switch._off();
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
CountableEvent: countable_component::Event,
SwitchableEvent: switchable_component::Event,
}
}
#[cfg(test)]
mod tests {
use super::CountableContract;
use components::countable::{ICountableDispatcher, ICountableDispatcherTrait};
use components::switchable::{ISwitchableDispatcher, ISwitchableDispatcherTrait};
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]
fn test_init() {
let (mut counter, mut switch) = deploy();
assert_eq!(counter.get(), 0);
assert_eq!(switch.is_on(), false);
}
#[test]
fn test_increment_switch_off() {
let (mut counter, mut switch) = deploy();
counter.increment();
assert_eq!(counter.get(), 0);
assert_eq!(switch.is_on(), false);
}
#[test]
fn test_increment_switch_on() {
let (mut counter, mut switch) = deploy();
switch.switch();
assert_eq!(switch.is_on(), true);
counter.increment();
assert_eq!(counter.get(), 1);
}
#[test]
fn test_increment_multiple_switches() {
let (mut counter, mut switch) = deploy();
switch.switch();
counter.increment();
counter.increment();
counter.increment();
assert_eq!(counter.get(), 3);
switch.switch();
counter.increment();
counter.increment();
counter.increment();
switch.switch();
counter.increment();
counter.increment();
counter.increment();
assert_eq!(counter.get(), 6);
}
}
Dependency on other component's internal functions
The previous example shows how to use the ISwitchable
trait implementation from the Switchable
component inside the Countable
component by embedding the implementation in the contract.
However, suppose we would like to turn off the switch after each increment. There's no set
function in the ISwitchable
trait, so we can't do it directly.
But the Switchable
component implements the internal function _off
from the SwitchableInternalTrait
that set the switch to false
.
We can't embed SwitchableInternalImpl
, but we can add switchable::HasComponent<TContractState>
as a dependency inside CountableImpl
.
We make the Countable
component depend on the Switchable
component.
This will allow to do switchable::ComponentState<TContractState>
-> TContractState
-> countable::ComponentState<TcontractState>
and access the internal functions of the Switchable
component inside the Countable
component:
#[starknet::component]
pub mod countable_component {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
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]
pub struct Storage {
countable_value: u32,
}
#[generate_trait]
impl GetSwitchable<
TContractState,
+HasComponent<TContractState>,
+switchable_component::HasComponent<TContractState>,
+Drop<TContractState>
> of GetSwitchableTrait<TContractState> {
fn get_switchable(
self: @ComponentState<TContractState>
) -> @switchable_component::ComponentState<TContractState> {
let contract = self.get_contract();
switchable_component::HasComponent::<TContractState>::get_component(contract)
}
fn get_switchable_mut(
ref self: ComponentState<TContractState>
) -> switchable_component::ComponentState<TContractState> {
let mut contract = self.get_contract_mut();
switchable_component::HasComponent::<TContractState>::get_component_mut(ref contract)
}
}
#[embeddable_as(Countable)]
impl CountableImpl<
TContractState,
+HasComponent<TContractState>,
+ISwitchable<TContractState>,
+switchable_component::HasComponent<TContractState>,
+Drop<TContractState>
> of ICountable<ComponentState<TContractState>> {
fn get(self: @ComponentState<TContractState>) -> u32 {
self.countable_value.read()
}
fn increment(ref self: ComponentState<TContractState>) {
if (self.get_contract().is_on()) {
self.countable_value.write(self.countable_value.read() + 1);
// use the switchable component internal function
let mut switch = self.get_switchable_mut();
switch._off();
}
}
}
}
The CountableContract
contract remains the same as in the previous example, only the implementation of the Countable
component is different.
Component-Contract Storage Collisions
Components can declare their own storage variables.
When a contract uses a component, the component storage is merged with the contract storage. The storage layout is only determined by the variables names, so variables with the same name will collide.
In a future release, the
#[substorage(v1)]
will determine the storage layout based on the component as well, so collisions will be avoided.
A good practice is to prefix the component storage variables with the component name, as shown in the Switchable component example.
Example
Here's an example of a collision on the switchable_value
storage variable of the Switchable
component.
Interface:
#[starknet::interface]
pub trait ISwitchCollision<TContractState> {
fn set(ref self: TContractState, value: bool);
fn get(ref self: TContractState) -> bool;
}
Here's the storage of the contract (you can expand the code snippet to see the full contract and tests):
#[starknet::interface]
pub trait ISwitchCollision<TContractState> {
fn set(ref self: TContractState, value: bool);
fn get(ref self: TContractState) -> bool;
}
#[starknet::contract]
pub mod SwitchCollisionContract {
use components::switchable::switchable_component;
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
component!(path: switchable_component, storage: switch, event: SwitchableEvent);
#[abi(embed_v0)]
impl SwitchableImpl = switchable_component::Switchable<ContractState>;
impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl<ContractState>;
#[storage]
struct Storage {
switchable_value: bool,
#[substorage(v0)]
switch: switchable_component::Storage,
}
#[constructor]
fn constructor(ref self: ContractState) {
self.switch._off();
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
SwitchableEvent: switchable_component::Event,
}
#[abi(embed_v0)]
impl SwitchCollisionContract of super::ISwitchCollision<ContractState> {
fn set(ref self: ContractState, value: bool) {
self.switchable_value.write(value);
}
fn get(ref self: ContractState) -> bool {
self.switchable_value.read()
}
}
}
#[cfg(test)]
mod switch_collision_tests {
use components::switchable::{ISwitchableDispatcher, ISwitchableDispatcherTrait};
use super::{
SwitchCollisionContract, ISwitchCollisionDispatcher, ISwitchCollisionDispatcherTrait
};
use starknet::SyscallResultTrait;
use starknet::syscalls::deploy_syscall;
fn deploy() -> (ISwitchCollisionDispatcher, ISwitchableDispatcher) {
let (contract_address, _) = deploy_syscall(
SwitchCollisionContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
(
ISwitchCollisionDispatcher { contract_address },
ISwitchableDispatcher { contract_address },
)
}
#[test]
fn test_collision() {
let (mut contract, mut contract_iswitch) = deploy();
assert_eq!(contract.get(), false);
assert_eq!(contract_iswitch.is_on(), false);
contract_iswitch.switch();
assert_eq!(contract_iswitch.is_on(), true);
assert_eq!(contract.get(), true);
// `collision` between component storage 'value' and contract storage 'value'
assert_eq!(contract.get(), contract_iswitch.is_on());
contract.set(false);
assert_eq!(contract.get(), contract_iswitch.is_on());
}
}
Both the contract and the component have a switchable_value
storage variable, so they collide:
fn test_collision() {
let (mut contract, mut contract_iswitch) = deploy();
assert_eq!(contract.get(), false);
assert_eq!(contract_iswitch.is_on(), false);
contract_iswitch.switch();
assert_eq!(contract_iswitch.is_on(), true);
assert_eq!(contract.get(), true);
// `collision` between component storage 'value' and contract storage 'value'
assert_eq!(contract.get(), contract_iswitch.is_on());
contract.set(false);
assert_eq!(contract.get(), contract_iswitch.is_on());
}
Ownable
The following Ownable
component is a simple component that allows the contract to set an owner and provides an _assert_is_owner
function that can be used to ensure that the caller is the owner.
It can also be used to renounce ownership of a contract, meaning that no one will be able to satisfy the _assert_is_owner
function.
use starknet::ContractAddress;
#[starknet::interface]
pub trait IOwnable<TContractState> {
fn owner(self: @TContractState) -> ContractAddress;
fn transfer_ownership(ref self: TContractState, new: ContractAddress);
fn renounce_ownership(ref self: TContractState);
}
pub mod Errors {
pub const UNAUTHORIZED: felt252 = 'Not owner';
pub const ZERO_ADDRESS_OWNER: felt252 = 'Owner cannot be zero';
pub const ZERO_ADDRESS_CALLER: felt252 = 'Caller cannot be zero';
}
#[starknet::component]
pub mod ownable_component {
use super::Errors;
use starknet::{ContractAddress, get_caller_address};
use core::num::traits::Zero;
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
pub struct Storage {
ownable_owner: ContractAddress,
}
#[derive(Drop, Debug, PartialEq, starknet::Event)]
pub struct OwnershipTransferredEvent {
pub previous: ContractAddress,
pub new: ContractAddress
}
#[derive(Drop, Debug, PartialEq, starknet::Event)]
pub struct OwnershipRenouncedEvent {
pub previous: ContractAddress
}
#[event]
#[derive(Drop, Debug, PartialEq, starknet::Event)]
pub enum Event {
OwnershipTransferredEvent: OwnershipTransferredEvent,
OwnershipRenouncedEvent: OwnershipRenouncedEvent
}
#[embeddable_as(Ownable)]
pub impl OwnableImpl<
TContractState, +HasComponent<TContractState>
> of super::IOwnable<ComponentState<TContractState>> {
fn owner(self: @ComponentState<TContractState>) -> ContractAddress {
self.ownable_owner.read()
}
fn transfer_ownership(ref self: ComponentState<TContractState>, new: ContractAddress) {
self._assert_only_owner();
self._transfer_ownership(new);
}
fn renounce_ownership(ref self: ComponentState<TContractState>) {
self._assert_only_owner();
self._renounce_ownership();
}
}
#[generate_trait]
pub impl OwnableInternalImpl<
TContractState, +HasComponent<TContractState>
> of OwnableInternalTrait<TContractState> {
fn _assert_only_owner(self: @ComponentState<TContractState>) {
let caller = get_caller_address();
assert(caller.is_non_zero(), Errors::ZERO_ADDRESS_CALLER);
assert(caller == self.ownable_owner.read(), Errors::UNAUTHORIZED);
}
fn _init(ref self: ComponentState<TContractState>, owner: ContractAddress) {
assert(owner.is_non_zero(), Errors::ZERO_ADDRESS_OWNER);
self.ownable_owner.write(owner);
}
fn _transfer_ownership(ref self: ComponentState<TContractState>, new: ContractAddress) {
assert(new.is_non_zero(), Errors::ZERO_ADDRESS_OWNER);
let previous = self.ownable_owner.read();
self.ownable_owner.write(new);
self
.emit(
Event::OwnershipTransferredEvent(OwnershipTransferredEvent { previous, new })
);
}
fn _renounce_ownership(ref self: ComponentState<TContractState>) {
let previous = self.ownable_owner.read();
self.ownable_owner.write(Zero::zero());
self.emit(Event::OwnershipRenouncedEvent(OwnershipRenouncedEvent { previous }));
}
}
}
A mock contract that uses the Ownable
component:
use starknet::ContractAddress;
#[starknet::interface]
pub trait IOwnable<TContractState> {
fn owner(self: @TContractState) -> ContractAddress;
fn transfer_ownership(ref self: TContractState, new: ContractAddress);
fn renounce_ownership(ref self: TContractState);
}
pub mod Errors {
pub const UNAUTHORIZED: felt252 = 'Not owner';
pub const ZERO_ADDRESS_OWNER: felt252 = 'Owner cannot be zero';
pub const ZERO_ADDRESS_CALLER: felt252 = 'Caller cannot be zero';
}
#[starknet::component]
pub mod ownable_component {
use super::Errors;
use starknet::{ContractAddress, get_caller_address};
use core::num::traits::Zero;
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
pub struct Storage {
ownable_owner: ContractAddress,
}
#[derive(Drop, Debug, PartialEq, starknet::Event)]
pub struct OwnershipTransferredEvent {
pub previous: ContractAddress,
pub new: ContractAddress
}
#[derive(Drop, Debug, PartialEq, starknet::Event)]
pub struct OwnershipRenouncedEvent {
pub previous: ContractAddress
}
#[event]
#[derive(Drop, Debug, PartialEq, starknet::Event)]
pub enum Event {
OwnershipTransferredEvent: OwnershipTransferredEvent,
OwnershipRenouncedEvent: OwnershipRenouncedEvent
}
#[embeddable_as(Ownable)]
pub impl OwnableImpl<
TContractState, +HasComponent<TContractState>
> of super::IOwnable<ComponentState<TContractState>> {
fn owner(self: @ComponentState<TContractState>) -> ContractAddress {
self.ownable_owner.read()
}
fn transfer_ownership(ref self: ComponentState<TContractState>, new: ContractAddress) {
self._assert_only_owner();
self._transfer_ownership(new);
}
fn renounce_ownership(ref self: ComponentState<TContractState>) {
self._assert_only_owner();
self._renounce_ownership();
}
}
#[generate_trait]
pub impl OwnableInternalImpl<
TContractState, +HasComponent<TContractState>
> of OwnableInternalTrait<TContractState> {
fn _assert_only_owner(self: @ComponentState<TContractState>) {
let caller = get_caller_address();
assert(caller.is_non_zero(), Errors::ZERO_ADDRESS_CALLER);
assert(caller == self.ownable_owner.read(), Errors::UNAUTHORIZED);
}
fn _init(ref self: ComponentState<TContractState>, owner: ContractAddress) {
assert(owner.is_non_zero(), Errors::ZERO_ADDRESS_OWNER);
self.ownable_owner.write(owner);
}
fn _transfer_ownership(ref self: ComponentState<TContractState>, new: ContractAddress) {
assert(new.is_non_zero(), Errors::ZERO_ADDRESS_OWNER);
let previous = self.ownable_owner.read();
self.ownable_owner.write(new);
self
.emit(
Event::OwnershipTransferredEvent(OwnershipTransferredEvent { previous, new })
);
}
fn _renounce_ownership(ref self: ComponentState<TContractState>) {
let previous = self.ownable_owner.read();
self.ownable_owner.write(Zero::zero());
self.emit(Event::OwnershipRenouncedEvent(OwnershipRenouncedEvent { previous }));
}
}
}
#[starknet::contract]
pub mod OwnedContract {
use super::{ownable_component, ownable_component::OwnableInternalTrait};
component!(path: ownable_component, storage: ownable, event: OwnableEvent);
#[abi(embed_v0)]
impl OwnableImpl = ownable_component::Ownable<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
ownable: ownable_component::Storage,
}
#[constructor]
fn constructor(ref self: ContractState) {
self.ownable._init(starknet::get_caller_address());
}
#[event]
#[derive(Drop, Debug, PartialEq, starknet::Event)]
pub enum Event {
OwnableEvent: ownable_component::Event,
}
}
#[cfg(test)]
mod test {
use super::OwnedContract;
use super::ownable_component::{OwnershipRenouncedEvent, OwnershipTransferredEvent};
use super::{IOwnableDispatcher, IOwnableDispatcherTrait};
use starknet::ContractAddress;
use starknet::{syscalls::deploy_syscall, SyscallResultTrait, contract_address_const};
use starknet::testing::{set_contract_address};
use core::num::traits::Zero;
fn deploy() -> (IOwnableDispatcher, ContractAddress) {
let (contract_address, _) = deploy_syscall(
OwnedContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
(IOwnableDispatcher { contract_address }, contract_address)
}
#[test]
fn test_initial_state() {
let owner = contract_address_const::<'owner'>();
set_contract_address(owner);
let (ownable, _) = deploy();
assert_eq!(ownable.owner(), owner);
}
#[test]
fn test_transfer_ownership() {
let contract_address = contract_address_const::<'owner'>();
set_contract_address(contract_address);
let (ownable, address) = deploy();
let new_owner = contract_address_const::<'new_owner'>();
ownable.transfer_ownership(new_owner);
assert_eq!(ownable.owner(), new_owner);
assert_eq!(
starknet::testing::pop_log(address),
Option::Some(
OwnedContract::Event::OwnableEvent(
OwnershipTransferredEvent { previous: contract_address, new: new_owner }.into()
)
)
);
}
#[test]
#[should_panic]
fn test_transfer_ownership_not_owner() {
set_contract_address(contract_address_const::<'initial'>());
let (ownable, _) = deploy();
set_contract_address(contract_address_const::<'not_owner'>());
ownable.transfer_ownership(contract_address_const::<'new_owner'>());
}
#[test]
#[should_panic]
fn test_transfer_ownership_zero_error() {
set_contract_address(contract_address_const::<'initial'>());
let (ownable, _) = deploy();
ownable.transfer_ownership(Zero::zero());
}
#[test]
fn test_renounce_ownership() {
let contract_address = contract_address_const::<'owner'>();
set_contract_address(contract_address);
let (ownable, address) = deploy();
ownable.renounce_ownership();
assert_eq!(ownable.owner(), Zero::zero());
assert_eq!(
starknet::testing::pop_log(address),
Option::Some(
OwnedContract::Event::OwnableEvent(
OwnershipRenouncedEvent { previous: contract_address }.into()
)
)
);
}
#[test]
#[should_panic]
fn test_renounce_ownership_not_owner() {
set_contract_address(contract_address_const::<'owner'>());
let (ownable, _) = deploy();
set_contract_address(contract_address_const::<'not_owner'>());
ownable.renounce_ownership();
}
#[test]
#[should_panic]
fn test_renounce_ownership_previous_owner() {
set_contract_address(contract_address_const::<'owner'>());
let (ownable, _) = deploy();
ownable.renounce_ownership();
ownable.transfer_ownership(contract_address_const::<'new_owner'>());
}
}
Upgradeable Contract
In Starknet, contracts are divided into two parts: contract classes and contract instances. This division follows a similar concept used in object-oriented programming languages, where we distinguish between the definition and implementation of objects.
A contract class is the definition of a contract: it specifies how the contract behaves. It contains essential information like the Cairo byte code, hint information, entry point names, and everything that defines its semantics unambiguously.
To identify different contract classes, Starknet assigns a unique identifier to each class: the class hash. A contract instance is a deployed contract that corresponds to a specific contract class. Think of it as an instance of an object in languages like Java.
Each class is identified by its class hash, which is analogous to a class name in an object-oriented programming language. A contract instance is a deployed contract corresponding to a class.
You can upgrade a deployed contract to a newer version by calling the replace_class_syscall
function. By using this function, you can update the class hash associated with a deployed contract, effectively upgrading its implementation. However, this will not modify the contract's storage, so all the data stored in the contract will remain the same.
To illustrate this concept, let's consider an example with two contracts: UpgradeableContract_V0
, and UpgradeableContract_V1
.
Start by deploying UpgradeableContract_V0
as the initial version. Next, send a transaction that invokes the upgrade
function, with the class hash of UpgradeableContract_V1
as parameter to upgrade the class hash of the deployed contract to the UpgradeableContract_V1
one. Then, call the version
method on the contract to see that the contract was upgraded to the V1 version.
use starknet::class_hash::ClassHash;
#[starknet::interface]
pub trait IUpgradeableContract<TContractState> {
fn upgrade(ref self: TContractState, impl_hash: ClassHash);
fn version(self: @TContractState) -> u8;
}
#[starknet::contract]
pub mod UpgradeableContract_V0 {
use starknet::class_hash::ClassHash;
use starknet::SyscallResultTrait;
use core::num::traits::Zero;
#[storage]
struct Storage {}
#[event]
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub enum Event {
Upgraded: Upgraded,
}
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct Upgraded {
pub implementation: ClassHash
}
#[abi(embed_v0)]
impl UpgradeableContract of super::IUpgradeableContract<ContractState> {
fn upgrade(ref self: ContractState, impl_hash: ClassHash) {
assert(impl_hash.is_non_zero(), 'Class hash cannot be zero');
starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall();
self.emit(Event::Upgraded(Upgraded { implementation: impl_hash }))
}
fn version(self: @ContractState) -> u8 {
0
}
}
}
use starknet::class_hash::ClassHash;
#[starknet::interface]
pub trait IUpgradeableContract<TContractState> {
fn upgrade(ref self: TContractState, impl_hash: ClassHash);
fn version(self: @TContractState) -> u8;
}
#[starknet::contract]
pub mod UpgradeableContract_V1 {
use starknet::class_hash::ClassHash;
use starknet::SyscallResultTrait;
use core::num::traits::Zero;
#[storage]
struct Storage {}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Upgraded: Upgraded
}
#[derive(Drop, starknet::Event)]
struct Upgraded {
implementation: ClassHash
}
#[abi(embed_v0)]
impl UpgradeableContract of super::IUpgradeableContract<ContractState> {
fn upgrade(ref self: ContractState, impl_hash: ClassHash) {
assert(impl_hash.is_non_zero(), 'Class hash cannot be zero');
starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall();
self.emit(Event::Upgraded(Upgraded { implementation: impl_hash }))
}
fn version(self: @ContractState) -> u8 {
1
}
}
}
Simple Defi Vault
This is the Cairo adaptation of the Solidity by Example - Vault. Here's how it works:
-
When a user deposits a token, the contract calculates the amount of shares to mint.
-
When a user withdraws, the contract burns their shares, calculates the yield, and withdraws both the yield and the initial amount of tokens deposited.
// ANCHOR: contract
use starknet::ContractAddress;
// In order to make contract calls within our Vault,
// we need to have the interface of the remote ERC20 contract defined to import the Dispatcher.
#[starknet::interface]
pub trait IERC20<TContractState> {
fn get_name(self: @TContractState) -> felt252;
fn get_symbol(self: @TContractState) -> felt252;
fn get_decimals(self: @TContractState) -> u8;
fn get_total_supply(self: @TContractState) -> felt252;
fn balance_of(self: @TContractState, account: ContractAddress) -> felt252;
fn allowance(
self: @TContractState, owner: ContractAddress, spender: ContractAddress
) -> felt252;
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: felt252);
fn transfer_from(
ref self: TContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: felt252
);
fn approve(ref self: TContractState, spender: ContractAddress, amount: felt252);
fn increase_allowance(ref self: TContractState, spender: ContractAddress, added_value: felt252);
fn decrease_allowance(
ref self: TContractState, spender: ContractAddress, subtracted_value: felt252
);
}
#[starknet::interface]
pub trait ISimpleVault<TContractState> {
fn deposit(ref self: TContractState, amount: u256);
fn withdraw(ref self: TContractState, shares: u256);
fn user_balance_of(ref self: TContractState, account: ContractAddress) -> u256;
fn contract_total_supply(ref self: TContractState) -> u256;
}
#[starknet::contract]
pub mod SimpleVault {
use super::{IERC20Dispatcher, IERC20DispatcherTrait};
use starknet::{ContractAddress, get_caller_address, get_contract_address};
use starknet::storage::{
Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess,
StoragePointerWriteAccess
};
#[storage]
struct Storage {
token: IERC20Dispatcher,
total_supply: u256,
balance_of: Map<ContractAddress, u256>
}
#[constructor]
fn constructor(ref self: ContractState, token: ContractAddress) {
self.token.write(IERC20Dispatcher { contract_address: token });
}
#[generate_trait]
impl PrivateFunctions of PrivateFunctionsTrait {
fn _mint(ref self: ContractState, to: ContractAddress, shares: u256) {
self.total_supply.write(self.total_supply.read() + shares);
self.balance_of.write(to, self.balance_of.read(to) + shares);
}
fn _burn(ref self: ContractState, from: ContractAddress, shares: u256) {
self.total_supply.write(self.total_supply.read() - shares);
self.balance_of.write(from, self.balance_of.read(from) - shares);
}
}
#[abi(embed_v0)]
impl SimpleVault of super::ISimpleVault<ContractState> {
fn user_balance_of(ref self: ContractState, account: ContractAddress) -> u256 {
self.balance_of.read(account)
}
fn contract_total_supply(ref self: ContractState) -> u256 {
self.total_supply.read()
}
fn deposit(ref self: ContractState, amount: u256) {
// a = amount
// B = balance of token before deposit
// T = total supply
// s = shares to mint
//
// (T + s) / T = (a + B) / B
//
// s = aT / B
let caller = get_caller_address();
let this = get_contract_address();
let mut shares = 0;
if self.total_supply.read() == 0 {
shares = amount;
} else {
let balance: u256 = self.token.read().balance_of(this).try_into().unwrap();
shares = (amount * self.total_supply.read()) / balance;
}
PrivateFunctions::_mint(ref self, caller, shares);
let amount_felt252: felt252 = amount.low.into();
self.token.read().transfer_from(caller, this, amount_felt252);
}
fn withdraw(ref self: ContractState, shares: u256) {
// a = amount
// B = balance of token before withdraw
// T = total supply
// s = shares to burn
//
// (T - s) / T = (B - a) / B
//
// a = sB / T
let caller = get_caller_address();
let this = get_contract_address();
let balance = self.user_balance_of(this);
let amount = (shares * balance) / self.total_supply.read();
PrivateFunctions::_burn(ref self, caller, shares);
let amount_felt252: felt252 = amount.low.into();
self.token.read().transfer(caller, amount_felt252);
}
}
}
// ANCHOR_END: contract
#[cfg(test)]
mod tests {
use super::{SimpleVault, ISimpleVaultDispatcher, ISimpleVaultDispatcherTrait,};
use erc20::token::{
IERC20DispatcherTrait as IERC20DispatcherTrait_token,
IERC20Dispatcher as IERC20Dispatcher_token
};
use starknet::testing::{set_contract_address, set_account_contract_address};
use starknet::{
ContractAddress, SyscallResultTrait, syscalls::deploy_syscall, contract_address_const
};
const token_name: felt252 = 'myToken';
const decimals: u8 = 18;
const initial_supply: felt252 = 100000;
const symbols: felt252 = 'mtk';
fn deploy() -> (ISimpleVaultDispatcher, ContractAddress, IERC20Dispatcher_token) {
let _token_address: ContractAddress = contract_address_const::<'token_address'>();
let caller = contract_address_const::<'caller'>();
let (token_contract_address, _) = deploy_syscall(
erc20::token::erc20::TEST_CLASS_HASH.try_into().unwrap(),
caller.into(),
array![caller.into(), 'myToken', '8', '1000'.into(), 'MYT'].span(),
false
)
.unwrap_syscall();
let (contract_address, _) = deploy_syscall(
SimpleVault::TEST_CLASS_HASH.try_into().unwrap(),
0,
array![token_contract_address.into()].span(),
false
)
.unwrap_syscall();
(
ISimpleVaultDispatcher { contract_address },
contract_address,
IERC20Dispatcher_token { contract_address: token_contract_address }
)
}
#[test]
fn test_deposit() {
let caller = contract_address_const::<'caller'>();
let (dispatcher, vault_address, token_dispatcher) = deploy();
// Approve the vault to transfer tokens on behalf of the caller
let amount: felt252 = 10.into();
token_dispatcher.approve(vault_address.into(), amount);
set_contract_address(caller);
// Deposit tokens into the vault
let amount: u256 = 10.into();
let _deposit = dispatcher.deposit(amount);
println!("deposit :{:?}", _deposit);
// Check balances and total supply
let balance_of_caller = dispatcher.user_balance_of(caller);
let total_supply = dispatcher.contract_total_supply();
assert_eq!(balance_of_caller, amount);
assert_eq!(total_supply, amount);
}
#[test]
fn test_deposit_withdraw() {
let caller = contract_address_const::<'caller'>();
let (dispatcher, vault_address, token_dispatcher) = deploy();
// Approve the vault to transfer tokens on behalf of the caller
let amount: felt252 = 10.into();
token_dispatcher.approve(vault_address.into(), amount);
set_contract_address(caller);
set_account_contract_address(vault_address);
// Deposit tokens into the vault
let amount: u256 = 10.into();
dispatcher.deposit(amount);
dispatcher.withdraw(amount);
// Check balances of user in the vault after withdraw
let balance_of_caller = dispatcher.user_balance_of(caller);
assert_eq!(balance_of_caller, 0.into());
}
}
ERC20 Token
Contracts that follow the ERC20 Standard are called ERC20 tokens. They are used to represent fungible assets.
To create an ERC20 contract, it must implement the following interface:
#[starknet::interface]
pub trait IERC20<TContractState> {
fn get_name(self: @TContractState) -> felt252;
fn get_symbol(self: @TContractState) -> felt252;
fn get_decimals(self: @TContractState) -> u8;
fn get_total_supply(self: @TContractState) -> felt252;
fn balance_of(self: @TContractState, account: ContractAddress) -> felt252;
fn allowance(
self: @TContractState, owner: ContractAddress, spender: ContractAddress
) -> felt252;
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: felt252);
fn transfer_from(
ref self: TContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: felt252
);
fn approve(ref self: TContractState, spender: ContractAddress, amount: felt252);
fn increase_allowance(ref self: TContractState, spender: ContractAddress, added_value: felt252);
fn decrease_allowance(
ref self: TContractState, spender: ContractAddress, subtracted_value: felt252
);
}
In Starknet, function names should be written in snake_case. This is not the case in Solidity, where function names are written in camelCase. The Starknet ERC20 interface is therefore slightly different from the Solidity ERC20 interface.
Here's an implementation of the ERC20 interface in Cairo:
#[starknet::contract]
pub mod erc20 {
use core::num::traits::Zero;
use starknet::get_caller_address;
use starknet::contract_address_const;
use starknet::ContractAddress;
use starknet::storage::{
Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess,
StoragePointerWriteAccess
};
#[storage]
struct Storage {
name: felt252,
symbol: felt252,
decimals: u8,
total_supply: felt252,
balances: Map::<ContractAddress, felt252>,
allowances: Map::<(ContractAddress, ContractAddress), felt252>,
}
#[event]
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub enum Event {
Transfer: Transfer,
Approval: Approval,
}
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct Transfer {
pub from: ContractAddress,
pub to: ContractAddress,
pub value: felt252,
}
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct Approval {
pub owner: ContractAddress,
pub spender: ContractAddress,
pub value: felt252,
}
mod Errors {
pub const APPROVE_FROM_ZERO: felt252 = 'ERC20: approve from 0';
pub const APPROVE_TO_ZERO: felt252 = 'ERC20: approve to 0';
pub const TRANSFER_FROM_ZERO: felt252 = 'ERC20: transfer from 0';
pub const TRANSFER_TO_ZERO: felt252 = 'ERC20: transfer to 0';
pub const BURN_FROM_ZERO: felt252 = 'ERC20: burn from 0';
pub const MINT_TO_ZERO: felt252 = 'ERC20: mint to 0';
}
#[constructor]
fn constructor(
ref self: ContractState,
recipient: ContractAddress,
name: felt252,
decimals: u8,
initial_supply: felt252,
symbol: felt252
) {
self.name.write(name);
self.symbol.write(symbol);
self.decimals.write(decimals);
self.mint(recipient, initial_supply);
}
#[abi(embed_v0)]
impl IERC20Impl of super::IERC20<ContractState> {
fn get_name(self: @ContractState) -> felt252 {
self.name.read()
}
fn get_symbol(self: @ContractState) -> felt252 {
self.symbol.read()
}
fn get_decimals(self: @ContractState) -> u8 {
self.decimals.read()
}
fn get_total_supply(self: @ContractState) -> felt252 {
self.total_supply.read()
}
fn balance_of(self: @ContractState, account: ContractAddress) -> felt252 {
self.balances.read(account)
}
fn allowance(
self: @ContractState, owner: ContractAddress, spender: ContractAddress
) -> felt252 {
self.allowances.read((owner, spender))
}
fn transfer(ref self: ContractState, recipient: ContractAddress, amount: felt252) {
let sender = get_caller_address();
self._transfer(sender, recipient, amount);
}
fn transfer_from(
ref self: ContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: felt252
) {
let caller = get_caller_address();
self.spend_allowance(sender, caller, amount);
self._transfer(sender, recipient, amount);
}
fn approve(ref self: ContractState, spender: ContractAddress, amount: felt252) {
let caller = get_caller_address();
self.approve_helper(caller, spender, amount);
}
fn increase_allowance(
ref self: ContractState, spender: ContractAddress, added_value: felt252
) {
let caller = get_caller_address();
self
.approve_helper(
caller, spender, self.allowances.read((caller, spender)) + added_value
);
}
fn decrease_allowance(
ref self: ContractState, spender: ContractAddress, subtracted_value: felt252
) {
let caller = get_caller_address();
self
.approve_helper(
caller, spender, self.allowances.read((caller, spender)) - subtracted_value
);
}
}
#[generate_trait]
impl InternalImpl of InternalTrait {
fn _transfer(
ref self: ContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: felt252
) {
assert(sender.is_non_zero(), Errors::TRANSFER_FROM_ZERO);
assert(recipient.is_non_zero(), Errors::TRANSFER_TO_ZERO);
self.balances.write(sender, self.balances.read(sender) - amount);
self.balances.write(recipient, self.balances.read(recipient) + amount);
self.emit(Transfer { from: sender, to: recipient, value: amount });
}
fn spend_allowance(
ref self: ContractState,
owner: ContractAddress,
spender: ContractAddress,
amount: felt252
) {
let allowance = self.allowances.read((owner, spender));
self.allowances.write((owner, spender), allowance - amount);
}
fn approve_helper(
ref self: ContractState,
owner: ContractAddress,
spender: ContractAddress,
amount: felt252
) {
assert(spender.is_non_zero(), Errors::APPROVE_TO_ZERO);
self.allowances.write((owner, spender), amount);
self.emit(Approval { owner, spender, value: amount });
}
fn mint(ref self: ContractState, recipient: ContractAddress, amount: felt252) {
assert(recipient.is_non_zero(), Errors::MINT_TO_ZERO);
let supply = self.total_supply.read() + amount;
self.total_supply.write(supply);
let balance = self.balances.read(recipient) + amount;
self.balances.write(recipient, balance);
self
.emit(
Event::Transfer(
Transfer {
from: contract_address_const::<0>(), to: recipient, value: amount
}
)
);
}
}
}
There's several other implementations, such as the Open Zeppelin or the Cairo By Example ones.
NFT Dutch Auction
This is the Cairo adaptation (with some modifications) of the Solidity by example NFT Dutch Auction.
Here's how it works:
- The seller of the NFT deploys this contract with a startingPrice.
- The auction lasts for a specified duration.
- The price decreases over time.
- Participants can purchase NFTs at any time as long as the totalSupply has not been reached.
- The auction ends when either the totalSupply is reached or the duration has elapsed.
use starknet::ContractAddress;
#[starknet::interface]
pub trait IERC20<TContractState> {
fn get_name(self: @TContractState) -> felt252;
fn get_symbol(self: @TContractState) -> felt252;
fn get_decimals(self: @TContractState) -> u8;
fn get_total_supply(self: @TContractState) -> felt252;
fn balance_of(self: @TContractState, account: ContractAddress) -> felt252;
fn allowance(
self: @TContractState, owner: ContractAddress, spender: ContractAddress
) -> felt252;
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: felt252);
fn transfer_from(
ref self: TContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: felt252
);
fn approve(ref self: TContractState, spender: ContractAddress, amount: felt252);
fn increase_allowance(ref self: TContractState, spender: ContractAddress, added_value: felt252);
fn decrease_allowance(
ref self: TContractState, spender: ContractAddress, subtracted_value: felt252
);
}
#[starknet::interface]
trait IERC721<TContractState> {
fn get_name(self: @TContractState) -> felt252;
fn get_symbol(self: @TContractState) -> felt252;
fn get_token_uri(self: @TContractState, token_id: u256) -> felt252;
fn balance_of(self: @TContractState, account: ContractAddress) -> u256;
fn owner_of(self: @TContractState, token_id: u256) -> ContractAddress;
fn get_approved(self: @TContractState, token_id: u256) -> ContractAddress;
fn is_approved_for_all(
self: @TContractState, owner: ContractAddress, operator: ContractAddress
) -> bool;
fn approve(ref self: TContractState, to: ContractAddress, token_id: u256);
fn set_approval_for_all(ref self: TContractState, operator: ContractAddress, approved: bool);
fn transfer_from(
ref self: TContractState, from: ContractAddress, to: ContractAddress, token_id: u256
);
fn mint(ref self: TContractState, to: ContractAddress, token_id: u256);
}
#[starknet::interface]
pub trait INFTDutchAuction<TContractState> {
fn buy(ref self: TContractState, token_id: u256);
fn get_price(self: @TContractState) -> u64;
}
#[starknet::contract]
pub mod NFTDutchAuction {
use super::{IERC20Dispatcher, IERC20DispatcherTrait, IERC721Dispatcher, IERC721DispatcherTrait};
use starknet::{ContractAddress, get_caller_address, get_block_timestamp};
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
erc20_token: ContractAddress,
erc721_token: ContractAddress,
starting_price: u64,
seller: ContractAddress,
duration: u64,
discount_rate: u64,
start_at: u64,
expires_at: u64,
purchase_count: u128,
total_supply: u128
}
mod Errors {
pub const AUCTION_ENDED: felt252 = 'auction has ended';
pub const LOW_STARTING_PRICE: felt252 = 'low starting price';
pub const INSUFFICIENT_BALANCE: felt252 = 'insufficient balance';
}
#[constructor]
fn constructor(
ref self: ContractState,
erc20_token: ContractAddress,
erc721_token: ContractAddress,
starting_price: u64,
seller: ContractAddress,
duration: u64,
discount_rate: u64,
total_supply: u128
) {
assert(starting_price >= discount_rate * duration, Errors::LOW_STARTING_PRICE);
self.erc20_token.write(erc20_token);
self.erc721_token.write(erc721_token);
self.starting_price.write(starting_price);
self.seller.write(seller);
self.duration.write(duration);
self.discount_rate.write(discount_rate);
self.start_at.write(get_block_timestamp());
self.expires_at.write(get_block_timestamp() + duration * 1000);
self.total_supply.write(total_supply);
}
#[abi(embed_v0)]
impl NFTDutchAuction of super::INFTDutchAuction<ContractState> {
fn get_price(self: @ContractState) -> u64 {
let time_elapsed = (get_block_timestamp() - self.start_at.read())
/ 1000; // Ignore milliseconds
let discount = self.discount_rate.read() * time_elapsed;
self.starting_price.read() - discount
}
fn buy(ref self: ContractState, token_id: u256) {
// Check duration
assert(get_block_timestamp() < self.expires_at.read(), Errors::AUCTION_ENDED);
// Check total supply
assert(self.purchase_count.read() < self.total_supply.read(), Errors::AUCTION_ENDED);
let erc20_dispatcher = IERC20Dispatcher { contract_address: self.erc20_token.read() };
let erc721_dispatcher = IERC721Dispatcher {
contract_address: self.erc721_token.read()
};
let caller = get_caller_address();
// Get NFT price
let price: u256 = self.get_price().into();
let buyer_balance: u256 = erc20_dispatcher.balance_of(caller).into();
// Ensure buyer has enough token for payment
assert(buyer_balance >= price, Errors::INSUFFICIENT_BALANCE);
// Transfer payment token from buyer to seller
erc20_dispatcher.transfer_from(caller, self.seller.read(), price.try_into().unwrap());
// Mint token to buyer's address
erc721_dispatcher.mint(caller, token_id);
// Increase purchase count
self.purchase_count.write(self.purchase_count.read() + 1);
}
}
}
#[cfg(test)]
mod tests {
use starknet::ContractAddress;
use snforge_std::{
declare, DeclareResultTrait, ContractClassTrait, cheat_caller_address, CheatSpan,
cheat_block_timestamp
};
use nft_dutch_auction::erc721::{IERC721Dispatcher, IERC721DispatcherTrait};
use super::{INFTDutchAuctionDispatcher, INFTDutchAuctionDispatcherTrait};
use erc20::token::{IERC20Dispatcher, IERC20DispatcherTrait};
// ERC721 token
pub const erc721_name: felt252 = 'My NFT';
pub const erc721_symbol: felt252 = 'MNFT';
// ERC20 token
pub const erc20_name: felt252 = 'My Token';
pub const erc20_symbol: felt252 = 'MTKN';
pub const erc20_recipient: felt252 = 'admin';
pub const erc20_decimals: u8 = 1_u8;
pub const erc20_initial_supply: u128 = 10000_u128;
// NFT Auction
pub const starting_price: felt252 = 500;
pub const seller: felt252 = 'seller';
pub const duration: felt252 = 60; // in seconds
pub const discount_rate: felt252 = 5;
pub const total_supply: felt252 = 2;
fn get_contract_addresses() -> (ContractAddress, ContractAddress, ContractAddress) {
let erc721 = declare("ERC721").unwrap().contract_class();
let erc721_constructor_calldata = array![erc721_name, erc721_symbol];
let (erc721_address, _) = erc721.deploy(@erc721_constructor_calldata).unwrap();
let erc20 = declare("erc20").unwrap().contract_class();
let erc20_constructor_calldata = array![
erc20_recipient,
erc20_name,
erc20_decimals.into(),
erc20_initial_supply.into(),
erc20_symbol
];
let (erc20_address, _) = erc20.deploy(@erc20_constructor_calldata).unwrap();
let nft_auction = declare("NFTDutchAuction").unwrap().contract_class();
let nft_auction_constructor_calldata = array![
erc20_address.into(),
erc721_address.into(),
starting_price,
seller,
duration,
discount_rate,
total_supply
];
let (nft_auction_address, _) = nft_auction
.deploy(@nft_auction_constructor_calldata)
.unwrap();
(erc721_address, erc20_address, nft_auction_address)
}
#[test]
fn test_buy() {
let (erc721_address, erc20_address, nft_auction_address) = get_contract_addresses();
let erc721_dispatcher = IERC721Dispatcher { contract_address: erc721_address };
let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address };
let nft_auction_dispatcher = INFTDutchAuctionDispatcher {
contract_address: nft_auction_address
};
let erc20_admin: ContractAddress = 'admin'.try_into().unwrap();
let seller: ContractAddress = 'seller'.try_into().unwrap();
let buyer: ContractAddress = 'buyer'.try_into().unwrap();
// Transfer erc20 tokens to buyer
assert_eq!(erc20_dispatcher.balance_of(buyer), 0.into());
cheat_caller_address(erc20_address, erc20_admin, CheatSpan::TargetCalls(1));
let transfer_amt = 5000;
erc20_dispatcher.transfer(buyer, transfer_amt.into());
assert_eq!(erc20_dispatcher.balance_of(buyer), transfer_amt.into());
// Buy token
cheat_caller_address(nft_auction_address, buyer, CheatSpan::TargetCalls(3));
cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(2));
let nft_id_1 = 1;
let seller_bal_before_buy = erc20_dispatcher.balance_of(seller);
let buyer_bal_before_buy = erc20_dispatcher.balance_of(buyer);
let nft_price = nft_auction_dispatcher.get_price().into();
// buyer approves nft auction contract to spend own erc20 token
erc20_dispatcher.approve(nft_auction_address, nft_price);
nft_auction_dispatcher.buy(nft_id_1);
let seller_bal_after_buy = erc20_dispatcher.balance_of(seller);
let buyer_bal_after_buy = erc20_dispatcher.balance_of(buyer);
assert_eq!(seller_bal_after_buy, seller_bal_before_buy + nft_price);
assert_eq!(buyer_bal_after_buy, buyer_bal_before_buy - nft_price);
assert_eq!(erc721_dispatcher.owner_of(nft_id_1), buyer);
// Forward block timestamp in order for a reduced nft price
let forward_blocktime_by = 4000; // milliseconds
cheat_block_timestamp(nft_auction_address, forward_blocktime_by, CheatSpan::TargetCalls(1));
// Buy token again after some time
let nft_id_2 = 2;
// buyer approves nft auction contract to spend own erc20 token
erc20_dispatcher.approve(nft_auction_address, nft_price);
assert_ne!(erc721_dispatcher.owner_of(nft_id_2), buyer);
nft_auction_dispatcher.buy(nft_id_2);
assert_eq!(erc721_dispatcher.owner_of(nft_id_2), buyer);
}
#[test]
#[should_panic(expected: 'auction has ended')]
fn test_buy_should_panic_when_total_supply_reached() {
let (_, erc20_address, nft_auction_address) = get_contract_addresses();
let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address };
let nft_auction_dispatcher = INFTDutchAuctionDispatcher {
contract_address: nft_auction_address
};
let erc20_admin: ContractAddress = 'admin'.try_into().unwrap();
let buyer: ContractAddress = 'buyer'.try_into().unwrap();
// Transfer erc20 tokens to buyer
cheat_caller_address(erc20_address, erc20_admin, CheatSpan::TargetCalls(1));
let transfer_amt = 5000;
erc20_dispatcher.transfer(buyer, transfer_amt.into());
// Buy token
cheat_caller_address(nft_auction_address, buyer, CheatSpan::TargetCalls(4));
cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(3));
let nft_id_1 = 1;
let nft_price = nft_auction_dispatcher.get_price().into();
// buyer approves nft auction contract to spend own erc20 token
erc20_dispatcher.approve(nft_auction_address, nft_price);
nft_auction_dispatcher.buy(nft_id_1);
// Forward block timestamp in order for a reduced nft price
let forward_blocktime_by = 4000; // 4 seconds (in milliseconds)
cheat_block_timestamp(nft_auction_address, forward_blocktime_by, CheatSpan::TargetCalls(1));
// Buy token again after some time
let nft_id_2 = 2;
// buyer approves nft auction contract to spend own erc20 token
erc20_dispatcher.approve(nft_auction_address, nft_price);
nft_auction_dispatcher.buy(nft_id_2);
// Buy token again after the total supply has reached
let nft_id_3 = 3;
// buyer approves nft auction contract to spend own erc20 token
erc20_dispatcher.approve(nft_auction_address, nft_price);
nft_auction_dispatcher.buy(nft_id_3);
}
#[test]
#[should_panic(expected: 'auction has ended')]
fn test_buy_should_panic_when_duration_ended() {
let (_, erc20_address, nft_auction_address) = get_contract_addresses();
let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address };
let nft_auction_dispatcher = INFTDutchAuctionDispatcher {
contract_address: nft_auction_address
};
let erc20_admin: ContractAddress = 'admin'.try_into().unwrap();
let buyer: ContractAddress = 'buyer'.try_into().unwrap();
// Transfer erc20 tokens to buyer
cheat_caller_address(erc20_address, erc20_admin, CheatSpan::TargetCalls(1));
let transfer_amt = 5000;
erc20_dispatcher.transfer(buyer, transfer_amt.into());
// Buy token
cheat_caller_address(nft_auction_address, buyer, CheatSpan::TargetCalls(4));
cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(3));
let nft_id_1 = 1;
let nft_price = nft_auction_dispatcher.get_price().into();
// buyer approves nft auction contract to spend own erc20 token
erc20_dispatcher.approve(nft_auction_address, nft_price);
nft_auction_dispatcher.buy(nft_id_1);
// Forward block timestamp to a time after duration has ended
// During deployment, duration was set to 60 seconds
let forward_blocktime_by = 61000; // 61 seconds (in milliseconds)
cheat_block_timestamp(nft_auction_address, forward_blocktime_by, CheatSpan::TargetCalls(1));
// Buy token again after some time
let nft_id_2 = 2;
// buyer approves nft auction contract to spend own erc20 token
erc20_dispatcher.approve(nft_auction_address, nft_price);
nft_auction_dispatcher.buy(nft_id_2);
}
#[test]
fn test_price_decreases_after_some_time() {
let (_, _, nft_auction_address) = get_contract_addresses();
let nft_auction_dispatcher = INFTDutchAuctionDispatcher {
contract_address: nft_auction_address
};
let nft_price_before_time_travel = nft_auction_dispatcher.get_price();
// Forward time
let forward_blocktime_by = 10000; // 10 seconds (in milliseconds)
cheat_block_timestamp(nft_auction_address, forward_blocktime_by, CheatSpan::TargetCalls(1));
let nft_price_after_time_travel = nft_auction_dispatcher.get_price();
println!("price: {:?}", nft_price_after_time_travel);
assert_gt!(nft_price_before_time_travel, nft_price_after_time_travel);
}
}
Constant Product AMM
This is the Cairo adaptation of the Solidity by Example - Constant Product AMM.
In this contract, we implement a simple Automated Market Maker (AMM) following
the constant product formula: \( x \cdot y = k \). This formula ensures
that the product of the two token reserves (x
and y
representing the tokens
being swapper) remains constant, regardless of trades. Here, we provide
liquidity pools that allow users to trade between two tokens or add and remove
liquidity from the pool.
Key Concepts
-
approve() before swap or adding liquidity: Before interacting with the AMM (whether through swaps or adding liquidity), the user must approve the contract to spend their tokens. This is done by calling the
approve()
function on the ERC20 token contracts, allowing the AMM to transfer the required tokens on behalf of the user. -
Constant Product Formula for Swaps: The swap function operates based on the constant product formula \( x \cdot y = k \), where
x
andy
are the token reserves. When a user swaps one token for another, the product of the reserves remains constant, which determines how much of the other token the user will receive. -
Shares and Token Ratios for Liquidity: When adding liquidity, users provide both tokens in the ratio of the current reserves. The number of shares (liquidity tokens) the user receives represents their contribution to the pool. Similarly, when removing liquidity, users receive back tokens proportional to the number of shares they burn.
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 starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
use starknet::{ContractAddress, get_caller_address, get_contract_address};
use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess};
use core::num::traits::Sqrt;
#[storage]
struct Storage {
token0: IERC20Dispatcher,
token1: IERC20Dispatcher,
reserve0: u256,
reserve1: u256,
total_supply: u256,
balance_of: Map::<ContractAddress, u256>,
// Fee 0 - 1000 (0% - 100%, 1 decimal places)
// E.g. 3 = 0.3%
fee: u16,
}
#[constructor]
fn constructor(
ref self: ContractState, token0: ContractAddress, token1: ContractAddress, fee: u16
) {
// assert(fee <= 1000, 'fee > 1000');
self.token0.write(IERC20Dispatcher { contract_address: token0 });
self.token1.write(IERC20Dispatcher { contract_address: token1 });
self.fee.write(fee);
}
#[generate_trait]
impl PrivateFunctions of PrivateFunctionsTrait {
fn _mint(ref self: ContractState, to: ContractAddress, amount: u256) {
self.balance_of.write(to, self.balance_of.read(to) + amount);
self.total_supply.write(self.total_supply.read() + amount);
}
fn _burn(ref self: ContractState, from: ContractAddress, amount: u256) {
self.balance_of.write(from, self.balance_of.read(from) - amount);
self.total_supply.write(self.total_supply.read() - amount);
}
fn _update(ref self: ContractState, reserve0: u256, reserve1: u256) {
self.reserve0.write(reserve0);
self.reserve1.write(reserve1);
}
#[inline(always)]
fn select_token(self: @ContractState, token: ContractAddress) -> bool {
assert(
token == self.token0.read().contract_address
|| token == self.token1.read().contract_address,
'invalid token'
);
token == self.token0.read().contract_address
}
#[inline(always)]
fn min(x: u256, y: u256) -> u256 {
if (x <= y) {
x
} else {
y
}
}
}
#[abi(embed_v0)]
impl ConstantProductAmm of super::IConstantProductAmm<ContractState> {
fn swap(ref self: ContractState, token_in: ContractAddress, amount_in: u256) -> u256 {
assert(amount_in > 0, 'amount in = 0');
let is_token0: bool = self.select_token(token_in);
let (token0, token1): (IERC20Dispatcher, IERC20Dispatcher) = (
self.token0.read(), self.token1.read()
);
let (reserve0, reserve1): (u256, u256) = (self.reserve0.read(), self.reserve1.read());
let (
token_in, token_out, reserve_in, reserve_out
): (IERC20Dispatcher, IERC20Dispatcher, u256, u256) =
if (is_token0) {
(token0, token1, reserve0, reserve1)
} else {
(token1, token0, reserve1, reserve0)
};
let caller = get_caller_address();
let this = get_contract_address();
token_in.transfer_from(caller, this, amount_in);
// How much dy for dx?
// xy = k
// (x + dx)(y - dy) = k
// y - dy = k / (x + dx)
// y - k / (x + dx) = dy
// y - xy / (x + dx) = dy
// (yx + ydx - xy) / (x + dx) = dy
// ydx / (x + dx) = dy
let amount_in_with_fee = (amount_in * (1000 - self.fee.read().into()) / 1000);
let amount_out = (reserve_out * amount_in_with_fee) / (reserve_in + amount_in_with_fee);
token_out.transfer(caller, amount_out);
self._update(self.token0.read().balance_of(this), self.token1.read().balance_of(this));
amount_out
}
fn add_liquidity(ref self: ContractState, amount0: u256, amount1: u256) -> u256 {
let caller = get_caller_address();
let this = get_contract_address();
let (token0, token1): (IERC20Dispatcher, IERC20Dispatcher) = (
self.token0.read(), self.token1.read()
);
token0.transfer_from(caller, this, amount0);
token1.transfer_from(caller, this, amount1);
// How much dx, dy to add?
//
// xy = k
// (x + dx)(y + dy) = k'
//
// No price change, before and after adding liquidity
// x / y = (x + dx) / (y + dy)
//
// x(y + dy) = y(x + dx)
// x * dy = y * dx
//
// x / y = dx / dy
// dy = y / x * dx
let (reserve0, reserve1): (u256, u256) = (self.reserve0.read(), self.reserve1.read());
if (reserve0 > 0 || reserve1 > 0) {
assert(reserve0 * amount1 == reserve1 * amount0, 'x / y != dx / dy');
}
// How many shares to mint?
//
// f(x, y) = value of liquidity
// We will define f(x, y) = sqrt(xy)
//
// L0 = f(x, y)
// L1 = f(x + dx, y + dy)
// T = total shares
// s = shares to mint
//
// Total shares should increase proportional to increase in liquidity
// L1 / L0 = (T + s) / T
//
// L1 * T = L0 * (T + s)
//
// (L1 - L0) * T / L0 = s
// Claim
// (L1 - L0) / L0 = dx / x = dy / y
//
// Proof
// --- Equation 1 ---
// (L1 - L0) / L0 = (sqrt((x + dx)(y + dy)) - sqrt(xy)) / sqrt(xy)
//
// dx / dy = x / y so replace dy = dx * y / x
//
// --- Equation 2 ---
// Equation 1 = (sqrt(xy + 2ydx + dx^2 * y / x) - sqrt(xy)) / sqrt(xy)
//
// Multiply by sqrt(x) / sqrt(x)
// Equation 2 = (sqrt(x^2y + 2xydx + dx^2 * y) - sqrt(x^2y)) / sqrt(x^2y)
// = (sqrt(y)(sqrt(x^2 + 2xdx + dx^2) - sqrt(x^2))) / (sqrt(y)sqrt(x^2))
// sqrt(y) on top and bottom cancels out
//
// --- Equation 3 ---
// Equation 2 = (sqrt(x^2 + 2xdx + dx^2) - sqrt(x^2)) / sqrt(x^2)
// = (sqrt((x + dx)^2) - sqrt(x^2)) / sqrt(x^2)
// = ((x + dx) - x) / x
// = dx / x
// Since dx / dy = x / y,
// dx / x = dy / y
//
// Finally
// (L1 - L0) / L0 = dx / x = dy / y
let total_supply = self.total_supply.read();
let shares = if (total_supply == 0) {
(amount0 * amount1).sqrt().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)
}
}
}
TimeLock
This is the Cairo adaptation of the Solidity by example TimeLock.
use starknet::account::Call;
#[starknet::interface]
pub trait ITimeLock<TState> {
fn get_tx_id(self: @TState, call: Call, timestamp: u64) -> felt252;
fn queue(ref self: TState, call: Call, timestamp: u64) -> felt252;
fn execute(ref self: TState, call: Call, timestamp: u64) -> Span<felt252>;
fn cancel(ref self: TState, tx_id: felt252);
}
#[starknet::contract]
pub mod TimeLock {
use core::poseidon::{PoseidonTrait, poseidon_hash_span};
use core::hash::HashStateTrait;
use starknet::{get_caller_address, get_block_timestamp, SyscallResultTrait, syscalls};
use starknet::account::Call;
use components::ownable::ownable_component;
use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess};
component!(path: ownable_component, storage: ownable, event: OwnableEvent);
// Ownable
#[abi(embed_v0)]
impl OwnableImpl = ownable_component::Ownable<ContractState>;
impl OwnableInternalImpl = ownable_component::OwnableInternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
ownable: ownable_component::Storage,
queued: Map::<felt252, bool>,
}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
#[flat]
OwnableEvent: ownable_component::Event,
Queue: Queue,
Execute: Execute,
Cancel: Cancel
}
#[derive(Drop, starknet::Event)]
pub struct Queue {
#[key]
pub tx_id: felt252,
pub call: Call,
pub timestamp: u64
}
#[derive(Drop, starknet::Event)]
pub struct Execute {
#[key]
pub tx_id: felt252,
pub call: Call,
pub timestamp: u64
}
#[derive(Drop, starknet::Event)]
pub struct Cancel {
#[key]
pub tx_id: felt252
}
pub const MIN_DELAY: u64 = 10; // seconds
pub const MAX_DELAY: u64 = 1000; // seconds
pub const GRACE_PERIOD: u64 = 1000; // seconds
pub mod Errors {
pub const ALREADY_QUEUED: felt252 = 'TimeLock: already queued';
pub const TIMESTAMP_NOT_IN_RANGE: felt252 = 'TimeLock: timestamp range';
pub const NOT_QUEUED: felt252 = 'TimeLock: not queued';
pub const TIMESTAMP_NOT_PASSED: felt252 = 'TimeLock: timestamp not passed';
pub const TIMESTAMP_EXPIRED: felt252 = 'TimeLock: timestamp expired';
}
#[constructor]
fn constructor(ref self: ContractState) {
self.ownable._init(get_caller_address());
}
#[abi(embed_v0)]
impl TimeLockImpl of super::ITimeLock<ContractState> {
fn get_tx_id(self: @ContractState, call: Call, timestamp: u64) -> felt252 {
PoseidonTrait::new()
.update(call.to.into())
.update(call.selector.into())
.update(poseidon_hash_span(call.calldata))
.update(timestamp.into())
.finalize()
}
fn queue(ref self: ContractState, call: Call, timestamp: u64) -> felt252 {
self.ownable._assert_only_owner();
let tx_id = self.get_tx_id(self._copy_call(@call), timestamp);
assert(!self.queued.read(tx_id), Errors::ALREADY_QUEUED);
// ---|------------|---------------|-------
// block block + min block + max
let block_timestamp = get_block_timestamp();
assert(
timestamp >= block_timestamp
+ MIN_DELAY && timestamp <= block_timestamp
+ MAX_DELAY,
Errors::TIMESTAMP_NOT_IN_RANGE
);
self.queued.write(tx_id, true);
self.emit(Queue { tx_id, call: self._copy_call(@call), timestamp });
tx_id
}
fn execute(ref self: ContractState, call: Call, timestamp: u64) -> Span<felt252> {
self.ownable._assert_only_owner();
let tx_id = self.get_tx_id(self._copy_call(@call), timestamp);
assert(self.queued.read(tx_id), Errors::NOT_QUEUED);
// ----|-------------------|-------
// timestamp timestamp + grace period
let block_timestamp = get_block_timestamp();
assert(block_timestamp >= timestamp, Errors::TIMESTAMP_NOT_PASSED);
assert(block_timestamp <= timestamp + GRACE_PERIOD, Errors::TIMESTAMP_EXPIRED);
self.queued.write(tx_id, false);
let result = syscalls::call_contract_syscall(call.to, call.selector, call.calldata)
.unwrap_syscall();
self.emit(Execute { tx_id, call: self._copy_call(@call), timestamp });
result
}
fn cancel(ref self: ContractState, tx_id: felt252) {
self.ownable._assert_only_owner();
assert(self.queued.read(tx_id), Errors::NOT_QUEUED);
self.queued.write(tx_id, false);
self.emit(Cancel { tx_id });
}
}
#[generate_trait]
impl InternalImpl of InternalTrait {
fn _copy_call(self: @ContractState, call: @Call) -> Call {
Call { to: *call.to, selector: *call.selector, calldata: *call.calldata }
}
}
}
Staking contract
The following staking contract is designed to allow users to stake tokens in exchange for reward tokens over a specified duration. Here's a quick summary of how it operates and what functionalities it supports:
Key Features:
-
Token staking and unstaking:
- Users can stake an ERC20 token, specified at deployment.
- Users can withdraw their staked tokens at any time.
-
Reward calculation and distribution:
- The rewards are distributed as an ERC20, also specified at deployment (can be different from the staking token).
- Rewards are calculated based on the duration of staking and the amount the user staked relative to the total staked amount by all users.
- A user’s reward accumulates over time up until the reward period's end and can be claimed anytime by the user.
-
Dynamic reward rates:
- The reward rate is determined by the total amount of reward tokens over a set period (duration).
- The reward rate can be adjusted during the rewards period if new rewards are added before the current reward period finishes.
- Even after a reward period finishes, a new reward duration and new rewards can be set up if desired.
-
Ownership and administration:
- Only the owner of the contract can set the rewards amount and duration.
The reward mechanism ensures that rewards are distributed fairly based on the amount and duration of tokens staked by each user.
The following implementation is the Cairo adaptation of the Solidity by Example - Staking Rewards contract. It includes a small adaptation to keep track of the amount of total distributed reward tokens and emit an event when the remaining reward token amount reaches 0.
use starknet::ContractAddress;
#[starknet::interface]
pub trait IStakingContract<TContractState> {
fn set_reward_amount(ref self: TContractState, amount: u256);
fn set_reward_duration(ref self: TContractState, duration: u256);
fn stake(ref self: TContractState, amount: u256);
fn withdraw(ref self: TContractState, amount: u256);
fn get_rewards(self: @TContractState, account: ContractAddress) -> u256;
fn claim_rewards(ref self: TContractState);
}
mod Errors {
pub const NULL_REWARDS: felt252 = 'Reward amount must be > 0';
pub const NOT_ENOUGH_REWARDS: felt252 = 'Reward amount must be > balance';
pub const NULL_AMOUNT: felt252 = 'Amount must be > 0';
pub const NULL_DURATION: felt252 = 'Duration must be > 0';
pub const UNFINISHED_DURATION: felt252 = 'Reward duration not finished';
pub const NOT_OWNER: felt252 = 'Caller is not the owner';
pub const NOT_ENOUGH_BALANCE: felt252 = 'Balance too low';
}
#[starknet::contract]
pub mod StakingContract {
use core::starknet::event::EventEmitter;
use core::num::traits::Zero;
use starknet::{ContractAddress, get_caller_address, get_block_timestamp, get_contract_address};
use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
use starknet::storage::{
Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess,
StoragePointerWriteAccess
};
#[storage]
struct Storage {
pub staking_token: IERC20Dispatcher,
pub reward_token: IERC20Dispatcher,
pub owner: ContractAddress,
pub reward_rate: u256,
pub duration: u256,
pub current_reward_per_staked_token: u256,
pub finish_at: u256,
// last time an operation (staking / withdrawal / rewards claimed) was registered
pub last_updated_at: u256,
pub last_user_reward_per_staked_token: Map::<ContractAddress, u256>,
pub unclaimed_rewards: Map::<ContractAddress, u256>,
pub total_distributed_rewards: u256,
// total amount of staked tokens
pub total_supply: u256,
// amount of staked tokens per user
pub balance_of: Map::<ContractAddress, u256>,
}
#[event]
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub enum Event {
Deposit: Deposit,
Withdrawal: Withdrawal,
RewardsFinished: RewardsFinished,
}
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct Deposit {
pub user: ContractAddress,
pub amount: u256,
}
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct Withdrawal {
pub user: ContractAddress,
pub amount: u256,
}
#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)]
pub struct RewardsFinished {
pub msg: felt252,
}
#[constructor]
fn constructor(
ref self: ContractState,
staking_token_address: ContractAddress,
reward_token_address: ContractAddress,
) {
self.staking_token.write(IERC20Dispatcher { contract_address: staking_token_address });
self.reward_token.write(IERC20Dispatcher { contract_address: reward_token_address });
self.owner.write(get_caller_address());
}
#[abi(embed_v0)]
impl StakingContract of super::IStakingContract<ContractState> {
fn set_reward_duration(ref self: ContractState, duration: u256) {
self.only_owner();
assert(duration > 0, super::Errors::NULL_DURATION);
// can only set duration if the previous duration has already finished
assert(
self.finish_at.read() < get_block_timestamp().into(),
super::Errors::UNFINISHED_DURATION
);
self.duration.write(duration);
}
fn set_reward_amount(ref self: ContractState, amount: u256) {
self.only_owner();
self.update_rewards(Zero::zero());
assert(amount > 0, super::Errors::NULL_REWARDS);
assert(self.duration.read() > 0, super::Errors::NULL_DURATION);
let block_timestamp: u256 = get_block_timestamp().into();
let rate = if self.finish_at.read() < block_timestamp {
amount / self.duration.read()
} else {
let remaining_rewards = self.reward_rate.read()
* (self.finish_at.read() - block_timestamp);
(remaining_rewards + amount) / self.duration.read()
};
assert(
self.reward_token.read().balance_of(get_contract_address()) >= rate
* self.duration.read(),
super::Errors::NOT_ENOUGH_REWARDS
);
self.reward_rate.write(rate);
// even if the previous reward duration has not finished, we reset the finish_at
// variable
self.finish_at.write(block_timestamp + self.duration.read());
self.last_updated_at.write(block_timestamp);
// reset total distributed rewards
self.total_distributed_rewards.write(0);
}
fn stake(ref self: ContractState, amount: u256) {
assert(amount > 0, super::Errors::NULL_AMOUNT);
let user = get_caller_address();
self.update_rewards(user);
self.balance_of.write(user, self.balance_of.read(user) + amount);
self.total_supply.write(self.total_supply.read() + amount);
self.staking_token.read().transfer_from(user, get_contract_address(), amount);
self.emit(Deposit { user, amount });
}
fn withdraw(ref self: ContractState, amount: u256) {
assert(amount > 0, super::Errors::NULL_AMOUNT);
let user = get_caller_address();
assert(
self.staking_token.read().balance_of(user) >= amount,
super::Errors::NOT_ENOUGH_BALANCE
);
self.update_rewards(user);
self.balance_of.write(user, self.balance_of.read(user) - amount);
self.total_supply.write(self.total_supply.read() - amount);
self.staking_token.read().transfer(user, amount);
self.emit(Withdrawal { user, amount });
}
fn get_rewards(self: @ContractState, account: ContractAddress) -> u256 {
self.unclaimed_rewards.read(account) + self.compute_new_rewards(account)
}
fn claim_rewards(ref self: ContractState) {
let user = get_caller_address();
self.update_rewards(user);
let rewards = self.unclaimed_rewards.read(user);
if rewards > 0 {
self.unclaimed_rewards.write(user, 0);
self.reward_token.read().transfer(user, rewards);
}
}
}
#[generate_trait]
impl PrivateFunctions of PrivateFunctionsTrait {
// call this function every time a user (including owner) performs a state-modifying action
fn update_rewards(ref self: ContractState, account: ContractAddress) {
self
.current_reward_per_staked_token
.write(self.compute_current_reward_per_staked_token());
self.last_updated_at.write(self.last_time_applicable());
if account.is_non_zero() {
self.distribute_user_rewards(account);
self
.last_user_reward_per_staked_token
.write(account, self.current_reward_per_staked_token.read());
self.send_rewards_finished_event();
}
}
fn distribute_user_rewards(ref self: ContractState, account: ContractAddress) {
// compute earned rewards since last update for the user `account`
let user_rewards = self.get_rewards(account);
self.unclaimed_rewards.write(account, user_rewards);
// track amount of total rewards distributed
self
.total_distributed_rewards
.write(self.total_distributed_rewards.read() + user_rewards);
}
fn send_rewards_finished_event(ref self: ContractState) {
// check whether we should send a RewardsFinished event
if self.last_updated_at.read() == self.finish_at.read() {
let total_rewards = self.reward_rate.read() * self.duration.read();
if total_rewards != 0 && self.total_distributed_rewards.read() == total_rewards {
// owner should set up NEW rewards into the contract
self.emit(RewardsFinished { msg: 'Rewards all distributed' });
} else {
// owner should set up rewards into the contract (or add duration by setting up
// rewards)
self.emit(RewardsFinished { msg: 'Rewards not active yet' });
}
}
}
fn compute_current_reward_per_staked_token(self: @ContractState) -> u256 {
if self.total_supply.read() == 0 {
self.current_reward_per_staked_token.read()
} else {
self.current_reward_per_staked_token.read()
+ self.reward_rate.read()
* (self.last_time_applicable() - self.last_updated_at.read())
/ self.total_supply.read()
}
}
fn compute_new_rewards(self: @ContractState, account: ContractAddress) -> u256 {
self.balance_of.read(account)
* (self.current_reward_per_staked_token.read()
- self.last_user_reward_per_staked_token.read(account))
}
#[inline(always)]
fn last_time_applicable(self: @ContractState) -> u256 {
Self::min(self.finish_at.read(), get_block_timestamp().into())
}
#[inline(always)]
fn min(x: u256, y: u256) -> u256 {
if (x <= y) {
x
} else {
y
}
}
fn only_owner(self: @ContractState) {
let caller = get_caller_address();
assert(caller == self.owner.read(), super::Errors::NOT_OWNER);
}
}
}
Merkle Tree contract
A Merkle tree, also known as a hash tree, is a data structure used in cryptography and computer science to verify data integrity and consistency. It is a binary tree where each leaf node represents the cryptographic hash of some data (a transaction for example), and each non-leaf node represents the cryptographic hash of its child nodes. This hierarchical structure allows efficient and secure verification of the data integrity.
Here's a quick summary of how it operates and what functionalities it supports:
How it works:
- Leaves Creation:
- Some data is hashed to create a leaf node.
- Intermediate Nodes Creation:
- Pairwise hashes of the leaf nodes are combined and hashed again to create parent nodes.
- This process continues until only one hash remains, known as the Merkle root.
- Merkle Root:
- The final hash at the top of the tree, representing the entire dataset.
- Changing any single data block will change its corresponding leaf node, which will propagate up the tree, altering the Merkle root.
Key Features:
-
Efficient Verification:
- Only a small subset of the tree (the Merkle proof) is needed to verify the inclusion of a particular data block, reducing the amount of data that must be processed.
-
Data Integrity:
- The Merkle root ensures the integrity of all the underlying data blocks.
- Any alteration in the data will result in a different root hash.
Examples of use cases:
-
Fundamental use case: Ethereum blockchain integrity
- Cryptocurrencies like Ethereum use Merkle trees to efficiently verify and maintain transaction integrity within blocks.
- Each transaction in a block is hashed to form leaf nodes, and these hashes are recursively combined to form a single Merkle root, summarizing all transactions.
- The Merkle root is stored in the block header, which is hashed to generate the block's unique identifier.
- Guaranteed Integrity: Any change to a transaction alters the Merkle root, block header, and block hash, making it easy for nodes to detect tampering.
- Transaction verification: Nodes can verify specific transactions via Merkle proofs without downloading the entire block.
-
Whitelist inclusion
- Merkle trees allow efficient whitelist verification without storing the full list on-chain, reducing storage costs.
- The Merkle root of the whitelist is stored on-chain, while the full list remains off-chain.
- To verify if an address is on the whitelist, a user provides a Merkle proof and the address. The Merkle root is recalculated using the provided data and compared to the stored on-chain root. If they match, the address is included; if not, it's excluded.
-
Decentralized Identity Verification
- Merkle trees can be used in decentralized identity systems to verify credentials.
- Off-chain data: a user's credentials.
- On-chain data: the Merkle root representing the credentials.
Visual example
The above diagram represents a merkle tree.
Each leaf node is the hash of some data.
Each other node is the hash of the combination of both children nodes.
If we were to verify
the hash 6
, the merkle proof would need to contain the hash 5
, hash 12
and hash 13
:
- The
hash 5
would be combined with thehash 6
to re-compute thehash 11
. - The newly computed
hash 11
in step 1 would be combined withhash 12
to re-computehash 14
. - The
hash 13
would be combined with the newly computedhash 14
in step 2 to re-compute the merkle root. - We can then compare the computed resultant merkle root with the one provided to the
verify
function.
Code
The following implementation is the Cairo adaptation of the Solidity by Example - Merkle Tree contract.
#[generate_trait]
pub impl ByteArrayHashTraitImpl of ByteArrayHashTrait {
fn hash(self: @ByteArray) -> felt252 {
let mut serialized_byte_arr: Array<felt252> = ArrayTrait::new();
self.serialize(ref serialized_byte_arr);
core::poseidon::poseidon_hash_span(serialized_byte_arr.span())
}
}
#[starknet::interface]
pub trait IMerkleTree<TContractState> {
fn build_tree(ref self: TContractState, data: Array<ByteArray>) -> Array<felt252>;
fn get_root(self: @TContractState) -> felt252;
// function to verify if leaf node exists in the merkle tree
fn verify(
self: @TContractState, proof: Array<felt252>, root: felt252, leaf: felt252, index: usize
) -> bool;
}
mod errors {
pub const NOT_POW_2: felt252 = 'Data length is not a power of 2';
pub const NOT_PRESENT: felt252 = 'No element in merkle tree';
}
#[starknet::contract]
pub mod MerkleTree {
use core::poseidon::PoseidonTrait;
use core::hash::{HashStateTrait, HashStateExTrait};
use starknet::storage::{
StoragePointerWriteAccess, StoragePointerReadAccess, Vec, MutableVecTrait, VecTrait
};
use super::ByteArrayHashTrait;
#[storage]
struct Storage {
pub hashes: Vec<felt252>
}
#[derive(Drop, Serde, Copy)]
struct Vec2 {
x: u32,
y: u32
}
#[abi(embed_v0)]
impl IMerkleTreeImpl of super::IMerkleTree<ContractState> {
fn build_tree(ref self: ContractState, mut data: Array<ByteArray>) -> Array<felt252> {
let data_len = data.len();
assert(data_len > 0 && (data_len & (data_len - 1)) == 0, super::errors::NOT_POW_2);
let mut _hashes: Array<felt252> = ArrayTrait::new();
// first, hash every leaf
for value in data {
_hashes.append(value.hash());
};
// then, hash all levels above leaves
let mut current_nodes_lvl_len = data_len;
let mut hashes_offset = 0;
while current_nodes_lvl_len > 0 {
let mut i = 0;
while i < current_nodes_lvl_len - 1 {
let left_elem = *_hashes.at(hashes_offset + i);
let right_elem = *_hashes.at(hashes_offset + i + 1);
let hash = PoseidonTrait::new().update_with((left_elem, right_elem)).finalize();
_hashes.append(hash);
i += 2;
};
hashes_offset += current_nodes_lvl_len;
current_nodes_lvl_len /= 2;
};
// write to the contract state (useful for the get_root function)
for hash in _hashes.span() {
self.hashes.append().write(*hash);
};
_hashes
}
fn get_root(self: @ContractState) -> felt252 {
let merkle_tree_length = self.hashes.len();
assert(merkle_tree_length > 0, super::errors::NOT_PRESENT);
self.hashes.at(merkle_tree_length - 1).read()
}
fn verify(
self: @ContractState,
mut proof: Array<felt252>,
root: felt252,
leaf: felt252,
mut index: usize
) -> bool {
let mut current_hash = leaf;
while let Option::Some(value) = proof.pop_front() {
current_hash =
if index % 2 == 0 {
PoseidonTrait::new().update_with((current_hash, value)).finalize()
} else {
PoseidonTrait::new().update_with((value, current_hash)).finalize()
};
index /= 2;
};
current_hash == root
}
}
}
Simple Storage (Starknet-js + Cairo)
In this example, we will use a SimpleStorage Cairo contract deployed on Starknet Sepolia Testnet and show how you can interact with the contract using Starknet-js.
Writing SimpleStorage contract in Cairo
The SimpleStorage contract has only one purpose: storing a number. We want the users to interact with the stored number by writing to the currently stored number and reading the number in the contract.
We will use the following SimpleStorage contract. In the Storage Variables page, you can find explanations for each component of the contract:
#[starknet::interface]
trait ISimpleStorage<T> {
fn set(ref self: T, x: u128);
fn get(self: @T) -> u128;
}
#[starknet::contract]
mod SimpleStorage {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
stored_data: u128
}
#[abi(embed_v0)]
impl SimpleStorage of super::ISimpleStorage<ContractState> {
fn set(ref self: ContractState, x: u128) {
self.stored_data.write(x);
}
fn get(self: @ContractState) -> u128 {
self.stored_data.read()
}
}
}
#
#
Because we want to interact with the get and set functions of the SimpleStorage contract using Starknet-js, we define the function signatures in #[starknet::interface]
. The functions are defined under the macro #[abi(embed_v0)]
where external functions are written.
Only deployed instances of the contract can be interacted with. You can refer to the How to Deploy page. Note down the address of your contract, as it is needed for the following part.
Interacting with SimpleStorage contract
We will interact with the SimpleStorage contract using Starknet-js. Firstly, create a new folder and inside the directory of the new folder, initialize the npm package (click Enter several items, you can skip adding the package info):
$ npm init
Now, package.json
file is created. Change the type of the package to a module.
"type": "module"
Let's add Starknet-js as a dependency:
$ npm install starknet@next
Create a file named index.js
where we will write JavaScript code to interact with our contract. Let's start our code by importing from Starknet-js, and from other libraries we will need:
import { Account, RpcProvider, json, Contract } from "starknet";
import fs from "fs";
import * as dotenv from "dotenv";
dotenv.config();
Let's create our provider object, and add our account address as a constant variable. We need the provider in order to send our queries and transactions to a Starknet node that is connected to the Starknet network:
const provider = new RpcProvider({
nodeUrl: "https://free-rpc.nethermind.io/sepolia-juno",
});
const accountAddress = // 'PASTE_ACCOUNT_ADDRESS_HERE';
The next step is creating an Account
object that will be used to sign transactions, so we need to import the account private key. You can access it directly from your keystore with the following command using Starkli:
$ starkli signer keystore inspect-private /path/to/starkli-wallet/keystore.json --raw
Create a .env
file in your project folder, and paste your private key as shown in the following line:
PRIVATE_KEY = "PASTE_PRIVATE_KEY_HERE"
Warning: Using
.env
files is not recommended for production environments, please use.env
files only for development purposes! It is HIGHLY recommended to add.gitignore
, and include your .env file there if you will be pushing your project to GitHub.
Now, import your private key from the environment variables and create your Account object.
const accountAddress = // 'PASTE_ACCOUNT_PUBLIC_ADDRESS_HERE';
const privateKey = process.env.PRIVATE_KEY;
// "1" is added to show that our account is deployed using Cairo 1.0.
const account = new Account(provider, accountAddress, privateKey, "1");
Now, let's create a Contract object in order to interact with our contract. In order to create the Contract object, we need the ABI and the address of our contract. The ABI contains information about what kind of data structures and functions there are in our contract so that we can interact with them using SDKs like Starknet-js.
We will copy ./target/simple_storage_SimpleStorage.contract_class.json
to abi.json
in the Scarb project folder. The beginning of the content of the ABI file should look like this:
{"sierra_program":["0x1","0x5","0x0","0x2","0x6","0x3","0x98","0x68","0x18", //...
We can then create the Account object and the Contract object in our index.js
file:
const contractAddress = 'PASTE_CONTRACT_ADDRESS_HERE';
const compiledContractAbi = json.parse(
fs.readFileSync("./abi.json").toString("ascii")
);
const storageContract = new Contract(
compiledContractAbi.abi,
contractAddress,
provider
);
The setup is finished! By calling the fn get(self: @ContractState) -> u128
function, we will be able to read the stored_data
variable from the contract:
let getData = await storageContract.get();
console.log("Stored_data:", getData.toString());
In order to run your code, run the command node index.js
in your project directory. After a short amount of time, you should see a "0" as the stored data.
Now, we will set a new number to the stored_data
variable by calling the fn set(self: @mut ContractState, new_data: u128)
function. This is an INVOKE
transaction, so we need to sign the transaction with our account's private key and pass along the calldata.
The transaction is signed and broadcasted to the network and it can takes a few seconds for the transaction to be confirmed.
storageContract.connect(account);
const myCall = storageContract.populate("set", [59]);
const res = await storageContract.set(myCall.calldata);
await provider.waitForTransaction(res.transaction_hash);
// Get the stored data after setting it
getData = await storageContract.get();
console.log("Stored_data after set():", getData.toString());
Crowdfunding Campaign
Crowdfunding is a method of raising capital through the collective effort of many individuals. It allows project creators to raise funds from a large number of people, usually through small contributions.
- Contract admin creates a campaign in some user's name (i.e. creator).
- Users can pledge, transferring their token to a campaign.
- Users can "unpledge", retrieving their tokens.
- The creator can at any point refund any of the users.
- Once the total amount pledged is more than the campaign goal, the campaign funds are "locked" in the contract, meaning the users can no longer unpledge; they can still pledge though.
- After the campaign ends, the campaign creator can claim the funds if the campaign goal is reached.
- Otherwise, campaign did not reach it's goal, pledgers can retrieve their funds.
- The creator can at any point cancel the campaign for whatever reason and refund all of the pledgers.
- The contract admin can upgrade the contract implementation, refunding all of the users and resetting the campaign state (we will use this in the Advanced Factory chapter).
Because contract upgrades need to be able to refund all of the pledges, we need to be able to iterate over all of the pledgers and their amounts. Since iteration is not supported by Map
, we need to create a custom storage type that will encompass pledge management. We use a component for this purpose.
use starknet::ContractAddress;
#[starknet::interface]
pub trait IPledgeable<TContractState> {
fn add(ref self: TContractState, pledger: ContractAddress, amount: u256);
fn get(self: @TContractState, pledger: ContractAddress) -> u256;
fn get_pledger_count(self: @TContractState) -> u32;
fn array(self: @TContractState) -> Array<ContractAddress>;
fn get_total(self: @TContractState) -> u256;
fn remove(ref self: TContractState, pledger: ContractAddress) -> u256;
}
#[starknet::component]
pub mod pledgeable_component {
use core::array::ArrayTrait;
use core::num::traits::Zero;
use starknet::ContractAddress;
use starknet::storage::{
Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess,
StoragePointerWriteAccess
};
#[storage]
pub struct Storage {
index_to_pledger: Map<u32, ContractAddress>,
pledger_to_amount: Map<ContractAddress, u256>,
pledger_count: u32,
total_amount: u256,
}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {}
mod Errors {
pub const INCONSISTENT_STATE: felt252 = 'Non-indexed pledger found';
}
#[embeddable_as(Pledgeable)]
pub impl PledgeableImpl<
TContractState, +HasComponent<TContractState>
> of super::IPledgeable<ComponentState<TContractState>> {
fn add(ref self: ComponentState<TContractState>, pledger: ContractAddress, amount: u256) {
let old_amount: u256 = self.pledger_to_amount.read(pledger);
if old_amount == 0 {
let index = self.pledger_count.read();
self.index_to_pledger.write(index, pledger);
self.pledger_count.write(index + 1);
}
self.pledger_to_amount.write(pledger, old_amount + amount);
self.total_amount.write(self.total_amount.read() + amount);
}
fn get(self: @ComponentState<TContractState>, pledger: ContractAddress) -> u256 {
self.pledger_to_amount.read(pledger)
}
fn get_pledger_count(self: @ComponentState<TContractState>) -> u32 {
self.pledger_count.read()
}
fn array(self: @ComponentState<TContractState>) -> Array<ContractAddress> {
let mut result = array![];
let mut index = self.pledger_count.read();
while index != 0 {
index -= 1;
let pledger = self.index_to_pledger.read(index);
result.append(pledger);
};
result
}
fn get_total(self: @ComponentState<TContractState>) -> u256 {
self.total_amount.read()
}
fn remove(ref self: ComponentState<TContractState>, pledger: ContractAddress) -> u256 {
let amount: u256 = self.pledger_to_amount.read(pledger);
// check if the pledge even exists
if amount == 0 {
return 0;
}
let last_index = self.pledger_count.read() - 1;
// if there are other pledgers, we need to update our indices
if last_index != 0 {
let mut pledger_index = last_index;
loop {
if self.index_to_pledger.read(pledger_index) == pledger {
break;
}
// if pledger_to_amount contains a pledger, then so does index_to_pledger
// thus this will never underflow
pledger_index -= 1;
};
self.index_to_pledger.write(pledger_index, self.index_to_pledger.read(last_index));
}
// last_index == new pledger count
self.pledger_count.write(last_index);
self.pledger_to_amount.write(pledger, 0);
self.index_to_pledger.write(last_index, Zero::zero());
self.total_amount.write(self.total_amount.read() - amount);
amount
}
}
}
Now we can create the Campaign
contract.
use starknet::{ClassHash, ContractAddress};
#[derive(Drop, Serde)]
pub struct Details {
pub canceled: bool,
pub claimed: bool,
pub creator: ContractAddress,
pub description: ByteArray,
pub end_time: u64,
pub goal: u256,
pub start_time: u64,
pub title: ByteArray,
pub token: ContractAddress,
pub total_pledges: u256,
}
#[starknet::interface]
pub trait ICampaign<TContractState> {
fn claim(ref self: TContractState);
fn cancel(ref self: TContractState, reason: ByteArray);
fn pledge(ref self: TContractState, amount: u256);
fn get_pledge(self: @TContractState, pledger: ContractAddress) -> u256;
fn get_pledgers(self: @TContractState) -> Array<ContractAddress>;
fn get_details(self: @TContractState) -> Details;
fn refund(ref self: TContractState, pledger: ContractAddress, reason: ByteArray);
fn upgrade(ref self: TContractState, impl_hash: ClassHash, new_end_time: Option<u64>);
fn unpledge(ref self: TContractState, reason: ByteArray);
}
#[starknet::contract]
pub mod Campaign {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use components::ownable::ownable_component::OwnableInternalTrait;
use core::num::traits::Zero;
use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
use starknet::{
ClassHash, ContractAddress, SyscallResultTrait, get_block_timestamp, get_caller_address,
get_contract_address
};
use components::ownable::ownable_component;
use super::pledgeable::pledgeable_component;
use super::Details;
component!(path: ownable_component, storage: ownable, event: OwnableEvent);
component!(path: pledgeable_component, storage: pledges, event: PledgeableEvent);
#[abi(embed_v0)]
pub impl OwnableImpl = ownable_component::Ownable<ContractState>;
impl OwnableInternalImpl = ownable_component::OwnableInternalImpl<ContractState>;
#[abi(embed_v0)]
impl PledgeableImpl = pledgeable_component::Pledgeable<ContractState>;
#[storage]
struct Storage {
canceled: bool,
claimed: bool,
creator: ContractAddress,
description: ByteArray,
end_time: u64,
goal: u256,
#[substorage(v0)]
ownable: ownable_component::Storage,
#[substorage(v0)]
pledges: pledgeable_component::Storage,
start_time: u64,
title: ByteArray,
token: IERC20Dispatcher,
}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
Claimed: Claimed,
Canceled: Canceled,
#[flat]
OwnableEvent: ownable_component::Event,
PledgeableEvent: pledgeable_component::Event,
PledgeMade: PledgeMade,
Refunded: Refunded,
RefundedAll: RefundedAll,
Unpledged: Unpledged,
Upgraded: Upgraded,
}
#[derive(Drop, starknet::Event)]
pub struct Canceled {
pub reason: ByteArray,
}
#[derive(Drop, starknet::Event)]
pub struct Claimed {
pub amount: u256,
}
#[derive(Drop, starknet::Event)]
pub struct PledgeMade {
#[key]
pub pledger: ContractAddress,
pub amount: u256,
}
#[derive(Drop, starknet::Event)]
pub struct Refunded {
#[key]
pub pledger: ContractAddress,
pub amount: u256,
pub reason: ByteArray,
}
#[derive(Drop, starknet::Event)]
pub struct RefundedAll {
pub reason: ByteArray,
}
#[derive(Drop, starknet::Event)]
pub struct Unpledged {
#[key]
pub pledger: ContractAddress,
pub amount: u256,
pub reason: ByteArray,
}
#[derive(Drop, starknet::Event)]
pub struct Upgraded {
pub implementation: ClassHash
}
pub mod Errors {
pub const CANCELED: felt252 = 'Campaign canceled';
pub const CLAIMED: felt252 = 'Campaign already claimed';
pub const CLASS_HASH_ZERO: felt252 = 'Class hash zero';
pub const CREATOR_ZERO: felt252 = 'Creator address zero';
pub const ENDED: felt252 = 'Campaign already ended';
pub const END_BEFORE_NOW: felt252 = 'End time < now';
pub const END_BEFORE_START: felt252 = 'End time < start time';
pub const END_BIGGER_THAN_MAX: felt252 = 'End time > max duration';
pub const NOTHING_TO_REFUND: felt252 = 'Nothing to refund';
pub const NOTHING_TO_UNPLEDGE: felt252 = 'Nothing to unpledge';
pub const NOT_CREATOR: felt252 = 'Not creator';
pub const NOT_STARTED: felt252 = 'Campaign not started';
pub const PLEDGES_LOCKED: felt252 = 'Goal reached, pledges locked';
pub const START_TIME_IN_PAST: felt252 = 'Start time < now';
pub const STILL_ACTIVE: felt252 = 'Campaign not ended';
pub const GOAL_NOT_REACHED: felt252 = 'Goal not reached';
pub const TITLE_EMPTY: felt252 = 'Title empty';
pub const TRANSFER_FAILED: felt252 = 'Transfer failed';
pub const ZERO_ADDRESS_CALLER: felt252 = 'Caller address zero';
pub const ZERO_ADDRESS_PLEDGER: felt252 = 'Pledger address zero';
pub const ZERO_ADDRESS_TOKEN: felt252 = 'Token address zero';
pub const ZERO_DONATION: felt252 = 'Donation must be > 0';
pub const ZERO_GOAL: felt252 = 'Goal must be > 0';
pub const ZERO_PLEDGES: felt252 = 'No pledges to claim';
}
const NINETY_DAYS: u64 = 90 * 24 * 60 * 60;
#[constructor]
fn constructor(
ref self: ContractState,
creator: ContractAddress,
title: ByteArray,
description: ByteArray,
goal: u256,
start_time: u64,
end_time: u64,
token_address: ContractAddress,
) {
assert(creator.is_non_zero(), Errors::CREATOR_ZERO);
assert(title.len() > 0, Errors::TITLE_EMPTY);
assert(goal > 0, Errors::ZERO_GOAL);
assert(start_time >= get_block_timestamp(), Errors::START_TIME_IN_PAST);
assert(end_time >= start_time, Errors::END_BEFORE_START);
assert(end_time <= get_block_timestamp() + NINETY_DAYS, Errors::END_BIGGER_THAN_MAX);
assert(token_address.is_non_zero(), Errors::ZERO_ADDRESS_TOKEN);
self.creator.write(creator);
self.title.write(title);
self.goal.write(goal);
self.description.write(description);
self.start_time.write(start_time);
self.end_time.write(end_time);
self.token.write(IERC20Dispatcher { contract_address: token_address });
self.ownable._init(get_caller_address());
}
#[abi(embed_v0)]
impl Campaign of super::ICampaign<ContractState> {
fn cancel(ref self: ContractState, reason: ByteArray) {
self._assert_only_creator();
assert(!self.canceled.read(), Errors::CANCELED);
assert(!self.claimed.read(), Errors::CLAIMED);
self.canceled.write(true);
self._refund_all(reason.clone());
self.emit(Event::Canceled(Canceled { reason }));
}
/// Sends the funds to the campaign creator.
/// It leaves the pledge data intact as a testament to campaign success
fn claim(ref self: ContractState) {
self._assert_only_creator();
assert(self._is_started(), Errors::NOT_STARTED);
assert(self._is_ended(), Errors::STILL_ACTIVE);
assert(!self.claimed.read(), Errors::CLAIMED);
assert(self._is_goal_reached(), Errors::GOAL_NOT_REACHED);
// no need to check if canceled; if it was, then the goal wouldn't have been reached
let this = get_contract_address();
let token = self.token.read();
let amount = token.balance_of(this);
assert(amount > 0, Errors::ZERO_PLEDGES);
self.claimed.write(true);
let owner = get_caller_address();
let success = token.transfer(owner, amount);
assert(success, Errors::TRANSFER_FAILED);
self.emit(Event::Claimed(Claimed { amount }));
}
fn get_details(self: @ContractState) -> Details {
Details {
canceled: self.canceled.read(),
claimed: self.claimed.read(),
creator: self.creator.read(),
description: self.description.read(),
end_time: self.end_time.read(),
goal: self.goal.read(),
start_time: self.start_time.read(),
title: self.title.read(),
token: self.token.read().contract_address,
total_pledges: self.pledges.get_total(),
}
}
fn get_pledge(self: @ContractState, pledger: ContractAddress) -> u256 {
self.pledges.get(pledger)
}
fn get_pledgers(self: @ContractState) -> Array<ContractAddress> {
self.pledges.array()
}
fn pledge(ref self: ContractState, amount: u256) {
assert(self._is_started(), Errors::NOT_STARTED);
assert(!self._is_ended(), Errors::ENDED);
assert(!self.canceled.read(), Errors::CANCELED);
assert(amount > 0, Errors::ZERO_DONATION);
let pledger = get_caller_address();
let this = get_contract_address();
let success = self.token.read().transfer_from(pledger, this, amount);
assert(success, Errors::TRANSFER_FAILED);
self.pledges.add(pledger, amount);
self.emit(Event::PledgeMade(PledgeMade { pledger, amount }));
}
fn refund(ref self: ContractState, pledger: ContractAddress, reason: ByteArray) {
self._assert_only_creator();
assert(self._is_started(), Errors::NOT_STARTED);
assert(!self.claimed.read(), Errors::CLAIMED);
assert(!self.canceled.read(), Errors::CANCELED);
assert(pledger.is_non_zero(), Errors::ZERO_ADDRESS_PLEDGER);
assert(self.pledges.get(pledger) != 0, Errors::NOTHING_TO_REFUND);
let amount = self._refund(pledger);
self.emit(Event::Refunded(Refunded { pledger, amount, reason }))
}
fn unpledge(ref self: ContractState, reason: ByteArray) {
assert(self._is_started(), Errors::NOT_STARTED);
assert(!self._is_goal_reached(), Errors::PLEDGES_LOCKED);
assert(self.pledges.get(get_caller_address()) != 0, Errors::NOTHING_TO_UNPLEDGE);
let pledger = get_caller_address();
let amount = self._refund(pledger);
self.emit(Event::Unpledged(Unpledged { pledger, amount, reason }));
}
fn upgrade(ref self: ContractState, impl_hash: ClassHash, new_end_time: Option<u64>) {
self.ownable._assert_only_owner();
assert(impl_hash.is_non_zero(), Errors::CLASS_HASH_ZERO);
// only active campaigns have pledges to refund and an end time to update
if self._is_started() {
if let Option::Some(end_time) = new_end_time {
assert(end_time >= get_block_timestamp(), Errors::END_BEFORE_NOW);
assert(
end_time <= get_block_timestamp() + NINETY_DAYS, Errors::END_BIGGER_THAN_MAX
);
self.end_time.write(end_time);
};
self._refund_all("contract upgraded");
}
starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall();
self.emit(Event::Upgraded(Upgraded { implementation: impl_hash }));
}
}
#[generate_trait]
impl CampaignInternalImpl of CampaignInternalTrait {
fn _assert_only_creator(self: @ContractState) {
let caller = get_caller_address();
assert(caller.is_non_zero(), Errors::ZERO_ADDRESS_CALLER);
assert(caller == self.creator.read(), Errors::NOT_CREATOR);
}
fn _is_ended(self: @ContractState) -> bool {
get_block_timestamp() >= self.end_time.read()
}
fn _is_goal_reached(self: @ContractState) -> bool {
self.pledges.get_total() >= self.goal.read()
}
fn _is_started(self: @ContractState) -> bool {
get_block_timestamp() >= self.start_time.read()
}
#[inline(always)]
fn _refund(ref self: ContractState, pledger: ContractAddress) -> u256 {
let amount = self.pledges.remove(pledger);
let success = self.token.read().transfer(pledger, amount);
assert(success, Errors::TRANSFER_FAILED);
amount
}
fn _refund_all(ref self: ContractState, reason: ByteArray) {
let mut pledges = self.pledges.array();
while let Option::Some(pledger) = pledges.pop_front() {
self._refund(pledger);
};
self.emit(Event::RefundedAll(RefundedAll { reason }));
}
}
}
AdvancedFactory: Crowdfunding
This is an example of an advanced factory contract that manages crowdfunding Campaign contracts created in the "Crowdfunding" chapter. The advanced factory allows for a centralized creation and management of Campaign
contracts on the Starknet blockchain, ensuring that they adhere to a standard interface and can be easily upgraded.
Key Features
- Campaign Creation: Users can create new crowdfunding campaigns with specific details such as title, description, goal, and duration.
- Campaign Management: The factory contract stores and manages the campaigns, allowing for upgrades and tracking.
- Upgrade Mechanism: The factory owner can update the implementation of the campaign contract, ensuring that all campaigns benefit from improvements and bug fixes.
- the factory only updates it's
Campaign
class hash and emits an event to notify any listeners, but theCampaign
creators are in the end responsible for actually upgrading their contracts.
- the factory only updates it's
pub use starknet::{ContractAddress, ClassHash};
#[starknet::interface]
pub trait ICampaignFactory<TContractState> {
fn create_campaign(
ref self: TContractState,
title: ByteArray,
description: ByteArray,
goal: u256,
start_time: u64,
end_time: u64,
token_address: ContractAddress
) -> ContractAddress;
fn get_campaign_class_hash(self: @TContractState) -> ClassHash;
fn update_campaign_class_hash(ref self: TContractState, new_class_hash: ClassHash);
fn upgrade_campaign(
ref self: TContractState, campaign_address: ContractAddress, new_end_time: Option<u64>
);
}
#[starknet::contract]
pub mod CampaignFactory {
use core::num::traits::Zero;
use starknet::{
ContractAddress, ClassHash, SyscallResultTrait, syscalls::deploy_syscall, get_caller_address
};
use crowdfunding::campaign::{ICampaignDispatcher, ICampaignDispatcherTrait};
use components::ownable::ownable_component;
use starknet::storage::{
Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess,
StoragePointerWriteAccess
};
component!(path: ownable_component, storage: ownable, event: OwnableEvent);
#[abi(embed_v0)]
impl OwnableImpl = ownable_component::Ownable<ContractState>;
impl OwnableInternalImpl = ownable_component::OwnableInternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
ownable: ownable_component::Storage,
/// Store all of the created campaign instances' addresses and their class hashes
campaigns: Map<(ContractAddress, ContractAddress), ClassHash>,
/// Store the class hash of the contract to deploy
campaign_class_hash: ClassHash,
}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
#[flat]
OwnableEvent: ownable_component::Event,
CampaignClassHashUpgraded: CampaignClassHashUpgraded,
CampaignCreated: CampaignCreated,
ClassHashUpdated: ClassHashUpdated,
}
#[derive(Drop, starknet::Event)]
pub struct ClassHashUpdated {
pub new_class_hash: ClassHash,
}
#[derive(Drop, starknet::Event)]
pub struct CampaignClassHashUpgraded {
pub campaign: ContractAddress,
}
#[derive(Drop, starknet::Event)]
pub struct CampaignCreated {
pub creator: ContractAddress,
pub contract_address: ContractAddress
}
pub mod Errors {
pub const CLASS_HASH_ZERO: felt252 = 'Class hash cannot be zero';
pub const ZERO_ADDRESS: felt252 = 'Zero address';
pub const SAME_IMPLEMENTATION: felt252 = 'Implementation is unchanged';
pub const CAMPAIGN_NOT_FOUND: felt252 = 'Campaign not found';
}
#[constructor]
fn constructor(ref self: ContractState, class_hash: ClassHash) {
assert(class_hash.is_non_zero(), Errors::CLASS_HASH_ZERO);
self.campaign_class_hash.write(class_hash);
self.ownable._init(get_caller_address());
}
#[abi(embed_v0)]
impl CampaignFactory of super::ICampaignFactory<ContractState> {
fn create_campaign(
ref self: ContractState,
title: ByteArray,
description: ByteArray,
goal: u256,
start_time: u64,
end_time: u64,
token_address: ContractAddress,
) -> ContractAddress {
let creator = get_caller_address();
// Create constructor arguments
let mut constructor_calldata: Array::<felt252> = array![];
((creator, title, description, goal), start_time, end_time, token_address)
.serialize(ref constructor_calldata);
// Contract deployment
let (contract_address, _) = deploy_syscall(
self.campaign_class_hash.read(), 0, constructor_calldata.span(), false
)
.unwrap_syscall();
// track new campaign instance
self.campaigns.write((creator, contract_address), self.campaign_class_hash.read());
self.emit(Event::CampaignCreated(CampaignCreated { creator, contract_address }));
contract_address
}
fn get_campaign_class_hash(self: @ContractState) -> ClassHash {
self.campaign_class_hash.read()
}
fn update_campaign_class_hash(ref self: ContractState, new_class_hash: ClassHash) {
self.ownable._assert_only_owner();
assert(new_class_hash.is_non_zero(), Errors::CLASS_HASH_ZERO);
self.campaign_class_hash.write(new_class_hash);
self.emit(Event::ClassHashUpdated(ClassHashUpdated { new_class_hash }));
}
fn upgrade_campaign(
ref self: ContractState, campaign_address: ContractAddress, new_end_time: Option<u64>
) {
assert(campaign_address.is_non_zero(), Errors::ZERO_ADDRESS);
let creator = get_caller_address();
let old_class_hash = self.campaigns.read((creator, campaign_address));
assert(old_class_hash.is_non_zero(), Errors::CAMPAIGN_NOT_FOUND);
assert(old_class_hash != self.campaign_class_hash.read(), Errors::SAME_IMPLEMENTATION);
let campaign = ICampaignDispatcher { contract_address: campaign_address };
campaign.upgrade(self.campaign_class_hash.read(), new_end_time);
}
}
}
Random Number Generator
Randomness plays a crucial role in blockchain and smart contract development. In the context of blockchain, randomness is about generating unpredictable values using some source of entropy that is fair and resistant to manipulation.
In blockchain and smart contracts, randomness is needed for:
- Gaming: Ensuring fair outcomes in games of chance.
- Lotteries: Selecting winners in a verifiable and unbiased manner.
- Security: Generating cryptographic keys and nonces that are hard to predict.
- Consensus Protocols: Selecting validators or block producers in some proof-of-stake systems.
However, achieving true randomness on a decentralized platform poses significant challenges. There are numerous sources of entropy, each with its strengths and weaknesses.
Sources of Entropy
1. Block Properties
- Description: Using properties of the blockchain itself, like the hash of a block, or a block timestamp, as a source of randomness.
- Example: A common approach is to use the hash of a recent block as a seed for random number generation.
- Risks:
- Predictability: Miners can influence future block hashes by controlling the nonce they use during mining.
- Manipulation: Many of the blockchain properties (block hash, timestamp etc.) can be manipulated by some entities, especially if they stand to gain from a specific random outcome.
2. User-Provided Inputs
- Description: Allowing users to provide entropy directly, often combined with other sources to generate a random number.
- Example: Users submitting their own random values which are then hashed together with other inputs.
- Risks:
- Collusion: Users may collude to provide inputs that skew the randomness in their favor.
- Front-Running: Other participants might observe a user's input and act on it before it gets included in the block, affecting the outcome.
3. External Oracles
- Description: Using a trusted third-party service to supply randomness. Oracles are off-chain services that provide data to smart contracts.
- Example: Pragma VRF (Verifiable Random Function) is a service that provides cryptographically secure randomness.
- Risks:
- Trust: Reliance on a third party undermines the trustless nature of blockchain.
- Centralization: If the oracle service is compromised or shut down, so is the randomness it provides.
- Cost: Using an oracle often involves additional transaction fees.
4. Commit-Reveal Schemes
- Description: A multi-phase protocol where participants commit to a value in the first phase and reveal it in the second.
- Example: Participants submit a hash of their random value (commitment) first and reveal the actual value later. The final random number is derived from all revealed values.
- Risks:
- Dishonest Behavior: Participants may choose not to reveal their values if the outcome is unfavorable.
- Coordination: Requires honest participation from multiple parties, which can be hard to guarantee.
There are other ways to generate randomness on-chain, for more information read the "Public Randomness and Randomness Beacons" article.
CoinFlip using Pragma VRF
Below is an implementation of a CoinFlip
contract that utilizes a Pragma Verifiable Random Function (VRF) to generate random numbers on-chain.
- Players can flip a virtual coin and receive a random outcome of
Heads
orTails
- The contract needs to be funded with enough ETH to perform the necessary operations, including paying fees to Pragma's Randomness Oracle which returns a random value
- When the coin is "flipped", the contract makes a call to the Randomness Oracle to request a random value and the
Flipped
event is emitted - Randomness is generated off-chain, and then submitted to the contract using the
receive_random_words
callback - Based on this random value, the contract determines whether the coin "landed" on
Heads
or onTails
, and theLanded
event is emitted
use starknet::ContractAddress;
#[starknet::interface]
pub trait ICoinFlip<TContractState> {
fn flip(ref self: TContractState);
}
// declares just the pragma_lib::abi::IRandomness.receive_random_words function
#[starknet::interface]
pub trait IPragmaVRF<TContractState> {
fn receive_random_words(
ref self: TContractState,
requestor_address: ContractAddress,
request_id: u64,
random_words: Span<felt252>,
calldata: Array<felt252>
);
}
#[starknet::contract]
pub mod CoinFlip {
use core::num::traits::zero::Zero;
use starknet::{ContractAddress, get_caller_address, get_contract_address,};
use starknet::storage::{
Map, StoragePointerReadAccess, StoragePathEntry, StoragePointerWriteAccess
};
use pragma_lib::abi::{IRandomnessDispatcher, IRandomnessDispatcherTrait};
use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
#[storage]
struct Storage {
eth_dispatcher: IERC20Dispatcher,
flips: Map<u64, ContractAddress>,
nonce: u64,
randomness_contract_address: ContractAddress,
}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
Flipped: Flipped,
Landed: Landed,
}
#[derive(Drop, starknet::Event)]
pub struct Flipped {
pub flip_id: u64,
pub flipper: ContractAddress,
}
#[derive(Drop, starknet::Event)]
pub struct Landed {
pub flip_id: u64,
pub flipper: ContractAddress,
pub side: Side
}
#[derive(Drop, Debug, PartialEq, Serde)]
pub enum Side {
Heads,
Tails,
}
pub mod Errors {
pub const CALLER_NOT_RANDOMNESS: felt252 = 'Caller not randomness contract';
pub const INVALID_ADDRESS: felt252 = 'Invalid address';
pub const INVALID_FLIP_ID: felt252 = 'No flip with the given ID';
pub const REQUESTOR_NOT_SELF: felt252 = 'Requestor is not self';
pub const TRANSFER_FAILED: felt252 = 'Transfer failed';
}
pub const PUBLISH_DELAY: u64 = 1; // return the random value asap
pub const NUM_OF_WORDS: u64 = 1; // one random value is sufficient
pub const CALLBACK_FEE_LIMIT: u128 = 100_000_000_000_000; // 0.0001 ETH
pub const MAX_CALLBACK_FEE_DEPOSIT: u256 =
500_000_000_000_000; // CALLBACK_FEE_LIMIT * 5; needs to cover the Premium fee
#[constructor]
fn constructor(
ref self: ContractState,
randomness_contract_address: ContractAddress,
eth_address: ContractAddress
) {
assert(randomness_contract_address.is_non_zero(), Errors::INVALID_ADDRESS);
assert(eth_address.is_non_zero(), Errors::INVALID_ADDRESS);
self.randomness_contract_address.write(randomness_contract_address);
self.eth_dispatcher.write(IERC20Dispatcher { contract_address: eth_address });
}
#[abi(embed_v0)]
impl CoinFlip of super::ICoinFlip<ContractState> {
/// The contract needs to be funded with some ETH in order for this function
/// to be callable. For simplicity, anyone can fund the contract.
fn flip(ref self: ContractState) {
let flip_id = self._request_my_randomness();
let flipper = get_caller_address();
self.flips.entry(flip_id).write(flipper);
self.emit(Event::Flipped(Flipped { flip_id, flipper }));
}
}
#[abi(embed_v0)]
impl PragmaVRF of super::IPragmaVRF<ContractState> {
fn receive_random_words(
ref self: ContractState,
requestor_address: ContractAddress,
request_id: u64,
random_words: Span<felt252>,
calldata: Array<felt252>
) {
let caller = get_caller_address();
assert(
caller == self.randomness_contract_address.read(), Errors::CALLER_NOT_RANDOMNESS
);
let this = get_contract_address();
assert(requestor_address == this, Errors::REQUESTOR_NOT_SELF);
self._process_coin_flip(request_id, random_words.at(0));
}
}
#[generate_trait]
impl Private of PrivateTrait {
fn _request_my_randomness(ref self: ContractState) -> u64 {
let randomness_contract_address = self.randomness_contract_address.read();
let randomness_dispatcher = IRandomnessDispatcher {
contract_address: randomness_contract_address
};
let this = get_contract_address();
// Approve the randomness contract to transfer the callback deposit/fee
let eth_dispatcher = self.eth_dispatcher.read();
eth_dispatcher.approve(randomness_contract_address, MAX_CALLBACK_FEE_DEPOSIT);
let nonce = self.nonce.read();
// Request the randomness to be used to construct the winning combination
let request_id = randomness_dispatcher
.request_random(
nonce, this, CALLBACK_FEE_LIMIT, PUBLISH_DELAY, NUM_OF_WORDS, array![]
);
self.nonce.write(nonce + 1);
request_id
}
fn _process_coin_flip(ref self: ContractState, flip_id: u64, random_value: @felt252) {
let flipper = self.flips.entry(flip_id).read();
assert(flipper.is_non_zero(), Errors::INVALID_FLIP_ID);
let random_value: u256 = (*random_value).into();
let side = if random_value % 2 == 0 {
Side::Heads
} else {
Side::Tails
};
self.emit(Event::Landed(Landed { flip_id, flipper, side }));
}
}
}
Writing to any storage slot
On Starknet, a contract's storage is a map with \( 2^{251} \) slots, where each slot is a felt252
which is initialized to 0.
The address of storage variables is computed at compile time using the formula: storage variable address := pedersen(keccak(variable name), keys)
. Interactions with storage variables are commonly performed using the self.var.read()
and self.var.write()
functions.
Nevertheless, we can use the storage_write_syscall
and storage_read_syscall
syscalls, to write to and read from any storage slot.
This is useful when writing to storage variables that are not known at compile time, or to ensure that even if the contract is upgraded and the computation method of storage variable addresses changes, they remain accessible.
In the following example, we use the Poseidon hash function to compute the address of a storage variable. Poseidon is a ZK-friendly hash function that is cheaper and faster than Pedersen, making it an excellent choice for onchain computations. Once the address is computed, we use the storage syscalls to interact with it.
#[starknet::interface]
pub trait IWriteToAnySlots<TContractState> {
fn write_slot(ref self: TContractState, value: u32);
fn read_slot(self: @TContractState) -> u32;
}
#[starknet::contract]
pub mod WriteToAnySlot {
use starknet::syscalls::{storage_read_syscall, storage_write_syscall};
use starknet::SyscallResultTrait;
use core::poseidon::poseidon_hash_span;
use starknet::StorageAddress;
#[storage]
struct Storage {}
const SLOT_NAME: felt252 = 'test_slot';
#[abi(embed_v0)]
impl WriteToAnySlot of super::IWriteToAnySlots<ContractState> {
fn write_slot(ref self: ContractState, value: u32) {
storage_write_syscall(0, get_address_from_name(SLOT_NAME), value.into())
.unwrap_syscall();
}
fn read_slot(self: @ContractState) -> u32 {
storage_read_syscall(0, get_address_from_name(SLOT_NAME))
.unwrap_syscall()
.try_into()
.unwrap()
}
}
pub fn get_address_from_name(variable_name: felt252) -> StorageAddress {
let mut data: Array<felt252> = array![];
data.append(variable_name);
let hashed_name: felt252 = poseidon_hash_span(data.span());
let MASK_250: u256 = 0x03ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
// By taking the 250 least significant bits of the hash output, we get a valid 250bits
// storage address.
let result: felt252 = (hashed_name.into() & MASK_250).try_into().unwrap();
let result: StorageAddress = result.try_into().unwrap();
result
}
}
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 Map
.
Consider the following example in which we would like to use an object of
type Pet
as a key in a Map
. 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 super::Pet;
use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess};
#[storage]
struct Storage {
registration_time: Map::<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 are still used in some scenarios for backward compatibility), while Poseidon hashes are the standard nowadays since they were designed to be very efficient for Zero Knowledge proof systems.
In Cairo, it's possible to hash all types that can be converted to felt252
since they natively implement the Hash
trait. It's also possible to hash more complex types, like structs, by deriving the Hash trait with the #[derive(Hash)]
attribute, but only if all the struct's fields are themselves hashable.
To hash a value, you first need to initialize a hash state with the new
method of the HashStateTrait
. Then, you can update the hash state with the update
method. You can accumulate multiple updates if necessary. Finally, the finalize
method returns the final hash value as a felt252
.
#[starknet::interface]
pub trait IHashTrait<T> {
fn save_user_with_poseidon(
ref self: T, id: felt252, username: felt252, password: felt252
) -> felt252;
fn save_user_with_pedersen(
ref self: T, id: felt252, username: felt252, password: felt252
) -> felt252;
}
#[starknet::contract]
pub mod HashTraits {
use starknet::storage::StoragePointerWriteAccess;
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 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]
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_eq!(test_hash, 0x6da4b4d0489989f5483d179643dafb3405b0e3b883a6c8efe5beb824ba9055a);
}
#[test]
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_eq!(test_hash, 0x4d165e1d398ae4864854518d3c58c3d7a21ed9c1f8f3618fbb0031d208aab7b);
}
}
Commit-Reveal
The Commit-Reveal pattern is a fundamental blockchain pattern that enables to:
- Commit to a value without revealing it (commit phase)
- Reveal the value later to prove they knew it in advance (reveal phase)
Some use cases:
- Blind Auctions: Bidders commit to their bids first, then reveal them after the bidding period
- Voting Systems: Voters commit their votes early, revealing them only after voting ends
- Knowledge Proofs/Attestations: Proving you knew information at a specific time without revealing it immediately
- Fair Random Number Generation: Players commit to random numbers that get combined later, making it harder to manipulate the outcome
How It Works
-
Commit Phase:
- User generates a value (
secret
) - User creates a hash of this value
- User submits only the hash on-chain (
commit
)
- User generates a value (
-
Reveal Phase:
- User submits the original value (
reveal
) - Contract verifies that the hash of the submitted value matches the previously committed hash
- If it matches then it proves that the user knew the value at the commitment time
- User submits the original value (
Minimal commit-reveal contract:
#[starknet::interface]
pub trait ICommitmentRevealTrait<T> {
fn commit(ref self: T, commitment: felt252);
fn reveal(self: @T, secret: felt252) -> bool;
}
#[starknet::contract]
pub mod CommitmentRevealTraits {
use starknet::storage::{StoragePointerWriteAccess, StoragePointerReadAccess};
use core::hash::HashStateTrait;
use core::pedersen::PedersenTrait;
#[storage]
struct Storage {
commitment: felt252,
}
#[abi(embed_v0)]
impl CommitmentRevealTrait of super::ICommitmentRevealTrait<ContractState> {
fn commit(ref self: ContractState, commitment: felt252) {
self.commitment.write(commitment);
}
fn reveal(self: @ContractState, secret: felt252) -> bool {
let hash = PedersenTrait::new(secret).finalize();
self.commitment.read() == hash
}
}
}
#[cfg(test)]
mod tests {
use starknet::SyscallResultTrait;
use super::{
CommitmentRevealTraits, ICommitmentRevealTraitDispatcher,
ICommitmentRevealTraitDispatcherTrait
};
use core::hash::HashStateTrait;
use core::pedersen::PedersenTrait;
use starknet::syscalls::deploy_syscall;
fn deploy() -> ICommitmentRevealTraitDispatcher {
let (contract_address, _) = deploy_syscall(
CommitmentRevealTraits::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
ICommitmentRevealTraitDispatcher { contract_address }
}
#[test]
fn commit_and_reveal() {
let mut contract = deploy();
// Off-chain, compute the commitment hash for secret
let secret = 'My secret';
let offchain_commitment = PedersenTrait::new(secret).finalize();
// Commit on-chain
contract.commit(offchain_commitment);
// Reveal on-chain and assert the result
let reveal_result = contract.reveal(secret);
assert_eq!(reveal_result, true);
}
}
Usage example:
// Off-chain, compute the commitment hash for secret
let secret = 'My secret';
let offchain_commitment = PedersenTrait::new(secret).finalize();
// Commit on-chain
contract.commit(offchain_commitment);
// Reveal on-chain and assert the result
let reveal_result = contract.reveal(secret);
Some considerations:
- The commit phase must complete before any reveals can start
- Users might choose not to reveal if the outcome is unfavorable (consider adding stake/slashing mechanics to ensure reveals)
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 felt252
value. This is done by using the bits of the felt252
value to store multiple values.
For example, if we want to store two u8
values, we can use the first 8 bits of the felt252
value to store the first u8
value, and the last 8 bits to store the second u8
value. This way, we can store two u8
values in a single felt252
value.
Cairo provides a built-in store using packing that you can use with the StorePacking
trait.
trait StorePacking<T, PackedT> {
fn pack(value: T) -> PackedT;
fn unpack(value: PackedT) -> T;
}
This allows us to store the type T
by first packing it into the type PackedT
with the pack
function, and then storing the PackedT
value with it's Store
implementation. When reading the value, we first retrieve the PackedT
value, and then unpack it into the type T
using the unpack
function.
Here's an example of storing a Time
struct with two u8
values using the StorePacking
trait:
#[derive(Copy, Serde, Drop)]
pub struct Time {
pub hour: u8,
pub minute: u8
}
#[starknet::interface]
pub trait ITime<TContractState> {
fn set(ref self: TContractState, value: Time);
fn get(self: @TContractState) -> Time;
}
#[starknet::contract]
pub mod TimeContract {
use starknet::storage::{StoragePointerWriteAccess, StoragePointerReadAccess};
use super::Time;
use starknet::storage_access::StorePacking;
#[storage]
struct Storage {
time: Time
}
impl TimePackable of StorePacking<Time, felt252> {
fn pack(value: Time) -> felt252 {
let msb: felt252 = 256 * value.hour.into();
let lsb: felt252 = value.minute.into();
msb + lsb
}
fn unpack(value: felt252) -> Time {
let value: u16 = value.try_into().unwrap();
let (q, r) = DivRem::div_rem(value, 256_u16.try_into().unwrap());
let hour: u8 = Into::<u16, felt252>::into(q).try_into().unwrap();
let minute: u8 = Into::<u16, felt252>::into(r).try_into().unwrap();
Time { hour, minute }
}
}
#[abi(embed_v0)]
impl TimeContract of super::ITime<ContractState> {
fn set(ref self: ContractState, value: Time) {
// This will call the pack method of the TimePackable trait
// and store the resulting felt252
self.time.write(value);
}
fn get(self: @ContractState) -> Time {
// This will read the felt252 value from storage
// and return the result of the unpack method of the TimePackable trait
return self.time.read();
}
}
}
Account Abstraction
An account is an unique entity that can send transactions, users usually use wallets to manage their accounts.
Historically, in Ethereum, all accounts were Externally Owned Accounts (EOA) and were controlled by private keys. This is a simple and secure way to manage accounts, but it has limitations as the account logic is hardcoded in the protocol.
Account Abstraction (AA) is the concept behind abstracting parts of the account logic to allow for a more flexible account system. This replaces EOA with Account Contracts, which are smart contracts that implement the account logic. This opens up a lot of possibilities that can significantly improve the user experience when dealing with accounts.
On Starknet, Account Abstraction is natively supported, and all accounts are Account Contracts.
In this section we will how to implement an Account.
Account Contract
A smart contract must follow the Standard Account Interface specification defined in the SNIP-6.
In practice, this means that the contract must implement the SRC6
and SRC5
interfaces to be considered an account contract.
SNIP-6: SRC6 + SRC5
/// @title Represents a call to a target contract
/// @param to The target contract address
/// @param selector The target function selector
/// @param calldata The serialized function parameters
struct Call {
to: ContractAddress,
selector: felt252,
calldata: Array<felt252>
}
The Call
struct is used to represent a call to a function (selector
) in a target contract (to
) with parameters (calldata
). It is available under the starknet::account
module.
/// @title SRC-6 Standard Account
trait ISRC6 {
/// @notice Execute a transaction through the account
/// @param calls The list of calls to execute
/// @return The list of each call's serialized return value
fn __execute__(calls: Array<Call>) -> Array<Span<felt252>>;
/// @notice Assert whether the transaction is valid to be executed
/// @param calls The list of calls to execute
/// @return The string 'VALID' represented as felt when is valid
fn __validate__(calls: Array<Call>) -> felt252;
/// @notice Assert whether a given signature for a given hash is valid
/// @param hash The hash of the data
/// @param signature The signature to validate
/// @return The string 'VALID' represented as felt when the signature is valid
fn is_valid_signature(hash: felt252, signature: Array<felt252>) -> felt252;
}
A transaction can be represented as a list of calls Array<Call>
to other contracts, with atleast one call.
-
__execute__
: Executes a transaction after the validation phase. Returns an array of the serialized return of value (Span<felt252>
) of each call. -
__validate__
: Validates a transaction by verifying some predefined rules, such as the signature of the transaction. Returns theVALID
short string (as a felt252) if the transaction is valid. -
is_valid_signature
: Verify that a given signature is valid. This is mainly used by applications for authentication purposes.
Both __execute__
and __validate__
functions are exclusively called by the Starknet protocol.
/// @title SRC-5 Standard Interface Detection
trait ISRC5 {
/// @notice Query if a contract implements an interface
/// @param interface_id The interface identifier, as specified in SRC-5
/// @return `true` if the contract implements `interface_id`, `false` otherwise
fn supports_interface(interface_id: felt252) -> bool;
}
The interface identifiers of both SRC5
and SRC6
must be published with supports_interface
.
Minimal account contract Executing Transactions
In this example, we will implement a minimal account contract that can validate and execute transactions.
use starknet::account::Call;
#[starknet::interface]
trait ISRC6<TContractState> {
fn execute_calls(self: @TContractState, calls: Array<Call>) -> Array<Span<felt252>>;
fn validate_calls(self: @TContractState, calls: Array<Call>) -> felt252;
fn is_valid_signature(
self: @TContractState, hash: felt252, signature: Array<felt252>
) -> felt252;
}
#[starknet::contract]
mod simpleAccount {
use super::ISRC6;
use starknet::account::Call;
use core::num::traits::Zero;
use core::ecdsa::check_ecdsa_signature;
use starknet::storage::{StoragePointerWriteAccess, StoragePointerReadAccess};
// Implement SRC5 with openzeppelin
use openzeppelin::account::interface;
use openzeppelin::introspection::src5::SRC5Component;
component!(path: SRC5Component, storage: src5, event: SRC5Event);
#[abi(embed_v0)]
impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;
impl SRC5InternalImpl = SRC5Component::InternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
src5: SRC5Component::Storage,
public_key: felt252
}
#[constructor]
fn constructor(ref self: ContractState, public_key: felt252) {
self.src5.register_interface(interface::ISRC6_ID);
self.public_key.write(public_key);
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
SRC5Event: SRC5Component::Event
}
#[abi(embed_v0)]
impl SRC6 of ISRC6<ContractState> {
fn execute_calls(self: @ContractState, calls: Array<Call>) -> Array<Span<felt252>> {
assert(starknet::get_caller_address().is_zero(), 'Not Starknet Protocol');
let Call { to, selector, calldata } = calls.at(0);
let res = starknet::syscalls::call_contract_syscall(*to, *selector, *calldata).unwrap();
array![res]
}
fn validate_calls(self: @ContractState, calls: Array<Call>) -> felt252 {
assert(starknet::get_caller_address().is_zero(), 'Not Starknet Protocol');
let tx_info = starknet::get_tx_info().unbox();
let tx_hash = tx_info.transaction_hash;
let signature = tx_info.signature;
if self._is_valid_signature(tx_hash, signature) {
starknet::VALIDATED
} else {
0
}
}
fn is_valid_signature(
self: @ContractState, hash: felt252, signature: Array<felt252>
) -> felt252 {
if self._is_valid_signature(hash, signature.span()) {
starknet::VALIDATED
} else {
0
}
}
}
#[generate_trait]
impl SignatureVerificationImpl of SignatureVerification {
fn _is_valid_signature(
self: @ContractState, hash: felt252, signature: Span<felt252>
) -> bool {
check_ecdsa_signature(
hash, self.public_key.read(), *signature.at(0_u32), *signature.at(1_u32)
)
}
}
}
Library Calls
External calls can be made on Starknet by two means: Contract dispatchers or Library dispatchers. Dispatchers are automatically created and exported by the compiler when a contract interface is defined.
With Contract dispatcher we are calling an already deployed contract (with associated state), therefore contract address is passed to the dispatcher to make the call. However, with library dispatcher we are simply making function calls to declared contract classes (stateless).
Contract dispatcher call is synonymous to external calls in Solidity, while library dispatcher call is synonymous to delegate call.
For further reading: Cairo book
#[starknet::interface]
pub trait IMathUtils<T> {
fn add(ref self: T, x: u32, y: u32) -> u32;
fn set_class_hash(ref self: T, class_hash: starknet::ClassHash);
}
// contract A
#[starknet::contract]
pub mod MathUtils {
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl ImathUtilsImpl of super::IMathUtils<ContractState> {
fn add(ref self: ContractState, x: u32, y: u32) -> u32 {
x + y
}
fn set_class_hash(ref self: ContractState, class_hash: starknet::ClassHash) {}
}
}
// contract B to make library call to the class of contract A
#[starknet::contract]
pub mod MathUtilsLibraryCall {
use super::{IMathUtilsDispatcherTrait, IMathUtilsLibraryDispatcher};
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
value: u32,
lib_class_hash: starknet::ClassHash,
}
#[abi(embed_v0)]
impl MathUtils of super::IMathUtils<ContractState> {
fn add(ref self: ContractState, x: u32, y: u32) -> u32 {
IMathUtilsLibraryDispatcher { class_hash: self.lib_class_hash.read() }.add(x, y)
}
#[abi(embed_v0)]
fn set_class_hash(ref self: ContractState, class_hash: starknet::ClassHash) {
self.lib_class_hash.write(class_hash);
}
}
}
Plugins
Compiler plugins in Cairo are a way to generate additional code for specific items during the compilation process.
There are already a set of core plugins that you may have already used, such as #[derive]
, #[cfg]
, #[generate_trait]
, etc.
It is possible to create your own plugins in Cairo. You can learn more about how to do that in the hello-cairo-plugin repository.
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.
use core::starknet::eth_address::EthAddress;
use starknet::secp256_trait::{Signature};
// 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)
#[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::secp256k1::Secp256k1Point;
use starknet::secp256_trait::{Signature, signature_from_vrs, recover_public_key,};
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');
}
}
}
#[cfg(test)]
mod tests {
use starknet::secp256_trait::{Signature, signature_from_vrs, recover_public_key,};
use starknet::EthAddress;
use starknet::secp256k1::{Secp256k1Point};
use starknet::eth_signature::{verify_eth_signature, public_key_point_to_eth_address};
fn get_message_and_signature() -> (u256, Signature, EthAddress) {
let msg_hash = 0x546ec3fa4f7d3308931816fafd47fa297afe9ac9a09651f77acc13c05a84734f;
let r = 0xc0f30bcef72974dedaf165cf7848a83b0b9eb6a65167a14643df96698d753efb;
let s = 0x7f189e3cb5eb992d8cd26e287a13e900326b87f58da2b7fb48fbd3977e3cab1c;
let v = 27;
let eth_address = 0x5F04693482cfC121FF244cB3c3733aF712F9df02_u256.into();
let signature: Signature = signature_from_vrs(v, r, s);
(msg_hash, signature, eth_address)
}
#[test]
fn test_verify_eth_signature() {
let (msg_hash, signature, eth_address) = get_message_and_signature();
verify_eth_signature(msg_hash, signature, eth_address);
}
#[test]
fn test_secp256k1_recover_public_key() {
let (msg_hash, signature, eth_address) = get_message_and_signature();
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_eq!(calculated_eth_address, eth_address);
}
}