cairo by example

contracts

Contracts

Contracts can be deployed to Starknet.

The contract modules are defined by adding a #[starknet::contract] attribute above the module definition:

#[starknet::contract]
mod Contract {
}

Storage

Inside a contract definition, you can define a storage, using the #[storage] attribute:

#[storage]
struct Storage {
    token_supply: felt252,
    decimals: u8
}

Interfaces

In order to interact with your contract, you’ll want to define an interface, which gathers external (functions that mutate the storage) and view (functions that don’t mutate the storage) function signatures under a trait. Traits use a generic argument, generally referring to the contract state. Static functions that do not use the storage or emit events do not require an additional argument. Interfaces are defined by adding a #[starknet::interface] attribute:

#[starknet::interface]
trait IContract<TContractState> {
   fn increase_token_supply(ref self: TContractState, amount: felt252);
   fn increase_decimals(ref self: TContractState, amount: u8);
   fn get_token_supply(self: @TContractState) -> felt252;
}

Implementations

Functions that affect the contract state use a mutable reference to the ContractState: ref self: TContractState ; while the functions that don’t affect the state use: self: @TContractState.

And to implement this trait inside the contract, we use the impl keyword, with a #[external(v0)] attribute:

mod Contract {
    #[storage]
    struct Storage {
        token_supply: felt252,
        decimals: u8
    }

    #[external(v0)]
    impl Contract of super::IContract<ContractState> {
        fn increase_token_supply(ref self: ContractState, amount: felt252) { ... }
        fn increase_decimals(ref self: ContractState, amount: u8) { ... }
        fn get_token_supply(self: @ContractState) -> felt252 { ... }
    }
}

Storage Access

In order to interact with the contract state, you can use read and write functions:

let current_balance = self.balance.read();
self.balance.write(current_balance + amount)

That can be used inside functions:

fn increase_token_supply(ref self: ContractState, amount: felt252) { 
    let current_token_supply = self.token_supply.read();
    self.token_supply.write(current_token_supply + amount)
}

Constructors

When you deploy a contract, you can set its initial values using a constructor:

mod Contract {
    #[constructor]
    fn constructor(ref self: ContractState, initial_token_supply: felt252, initial_decimals: u8) { 
        self.token_supply.write(initial_token_supply);
        self.decimals.write(initial_decimals);
    }
}

Events definition

In Cairo2 all the contract events are unified under the Event enum:

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

#[derive(Drop, starknet::Event)]
struct TokenSupplyIncreased {
    amount: felt252
}

#[derive(Drop, starknet::Event)]
struct DecimalsIncreased {
    amount: u8
}

Emitting Events

In order to emit events, you can do the following:

fn increase_token_supply(ref self: ContractState, amount: felt252) {
    ...
    self.emit(TokenSupplyIncreased { amount });
}


Try it out!
  1. Install the toolchain:
    • For macOS and Linux, run our script:
    • curl -sL https://raw.githubusercontent.com/lambdaclass/cairo-by-example/main/build/installer.sh | bash -s 2.2.0
    • For Windows and others, please see the official guide
  2. Run the example:
    1. Copy the example into a contracts.cairo file and run with:
    2. %!s(<nil>) contracts.cairo

prev (spans) next (system calls)