Syntax changes introduced by the recently released version 2.0.0-rc0 of Cairo#
Channels for relevant information#
GitHub Cairo project task progress board: https://github.com/orgs/starkware-libs/projects/1/views/1
GitHub release information list: https://github.com/starkware-libs/cairo/releases
General changes#
(1). Integer literals no longer need to specify the variable type
// Before
let n:u8 = 8_u8;
// Now, the suffix can be omitted, but including it won't cause an error
let n:u8 = 8;
(2). Creating dictionaries
Before
use dict::Felt252DictTrait;
fn main(){
let mut dict = Felt252DictTrait::new();
}
Now, use the Default trait
use dict::Felt252DictTrait;
use traits::Default;
fn main(){
let mut map : Felt252Dict<felt252> = Default::default();
}
Syntax changes for contract writing#
First, here is the contract code for the old version:
#[abi]
trait IOtherContract {
fn decrease_allowed() -> bool;
}
#[contract]
mod CounterContract {
use starknet::ContractAddress;
use super::{
IOtherContractDispatcher,
IOtherContractDispatcherTrait,
IOtherContractLibraryDispatcher
};
struct Storage {
counter: u128,
other_contract: IOtherContractDispatcher
}
#[event]
fn counter_increased(amount: u128) {}
#[event]
fn counter_decreased(amount: u128) {}
#[constructor]
fn constructor(initial_counter: u128, other_contract_addr: ContractAddress) {
counter::write(initial_counter);
other_contract::write(IOtherContractDispatcher { contract_address: other_contract_addr });
}
#[external]
fn increase_counter(amount: u128) {
let current = counter::read();
counter::write(current + amount);
counter_increased(amount);
}
#[external]
fn decrease_counter(amount: u128) {
let allowed = other_contract::read().decrease_allowed();
if allowed {
let current = counter::read();
counter::write(current - amount);
counter_decreased(amount);
}
}
#[view]
fn get_counter() -> u128 {
counter::read()
}
}
The following code is for the new syntax of the contract#
(1). External functions are centralized in a specific trait and its corresponding impl
The above contract has three public functions: increase_counter
, decrease_counter
, and get_counter
.
- First, these public functions will be defined in a trait marked with
#[starknet::interface]
, which specifies the function signature (or function selector). - This trait contains a generic variable
TContractState
, which represents the contract's storage struct. - These public functions are methods of
TContractState
. - In the methods, the first parameter is
self
. If it is a view method,self
is the snapshot ofTContractState
(self: @TContractState
). If it is a state-changing method,self
is the reference ofTContractState
(ref self: TContractState
). Other parameters follow. - The logic of the public functions is written in an impl marked with
#[external(v0)]
.
The syntax changes regarding public functions are quite significant. The comments in the code provide more details:
/// @notice Defines the external interface of the current contract, all external functions will be defined in the impl of this trait
#[starknet::interface]
trait ICounterContract<TContractState> {
fn increase_counter(ref self: TContractState, amount: u128);
fn decrease_counter(ref self: TContractState, amount: u128);
fn get_counter(self: @TContractState) -> u128;
}
#[starknet::contract]
mod CounterContract {
...
/// @notice Define all external functions here, ContractState represents the contract's storage state
/// @dev Distinguish between view functions and non-view functions by passing snapshot or reference as the first parameter
/// @dev The contract syntax is still being updated, v0 is used to ensure compatibility of current contracts with future versions of the compiler
#[external(v0)]
impl CounterContract of super::ICounterContract<ContractState> {
// Takes a snapshot, so it is a view function
fn get_counter(self: @ContractState) -> u128 {
self.counter.read()
}
// Takes a reference, so it modifies the contract's storage state
fn increase_counter(ref self: ContractState, amount: u128) {
let current = self.counter.read();
self.counter.write(current + amount);
self.emit(Event::CounterIncreased(CounterIncreased { amount }));
}
fn decrease_counter(ref self: ContractState, amount: u128) {
let allowed = self.other_contract.read().decrease_allowed();
if allowed {
let current = self.counter.read();
self.counter.write(current - amount);
self.emit(Event::CounterDecreased(CounterDecreased { amount }));
}
}
}
...
}
(2). External contract calls
Since the syntax for public functions has changed, external contract calls have naturally changed as well.
- The part previously marked with
#[abi]
is now marked with#[starknet::interface]
. - The trait uses a generic trait, which is used in the same way as described above.
/// @notice Definition of the external contract interface
/// @dev Replace #[abi] with #[starknet::interface]
/// @dev Use a generic trait, where TContractState is the generic name representing the contract state
#[starknet::interface]
trait IOtherContract<TContractState> {
fn decrease_allowed(self: @TContractState) -> bool;
}
(3). Changes to events
There are significant changes to events as well, now represented using enums and structs.
- All events are defined in an enum marked with
#[event]
and#[derive(Drop, starknet::Event)]
. - Each event is represented by a separate struct that holds the fields and types of the event, also marked with
#[derive(Drop, starknet::Event)]
. - Event calls use
ContractState
:self.emit(Event::CounterDecreased(CounterDecreased { amount }));
/// @notice Events in the contract have also undergone significant changes
/// @dev Define all events in an enum named Event, marked with #[event]
/// @dev Each event is defined with a structure named event_type to hold the parameters of the event, also marked with #[derive(Drop, starknet::Event)]
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
CounterIncreased: CounterIncreased,
CounterDecreased: CounterDecreased
}
#[derive(Drop, starknet::Event)]
struct CounterIncreased {
amount: u128
}
#[derive(Drop, starknet::Event)]
struct CounterDecreased {
amount: u128
}