Starknet v0.12 Quantum Leap 👉👈
Quantum Leap is a game-changer for Starknet, offering reported improvements that are seven times better than previous versions, with an initial estimated throughput of 40 TPS and a latency of 30 seconds. Furthermore, the Cairo syntax has recently undergone a facelift and received further enhancements. It is already available on the testnet and will soon be deployed on the mainnet in the upcoming weeks.
In this article, we will explore some of the changes in the Cairo syntax.
Let’s delve in.
Changes to the Cairo Syntax
The new changes were initially posted on June 11th by @FeedTheFed in the Starknet community forum. You can find the full version of the post here.
Two main important things are being addressed, and those are safety and extendibility. This article will focus on the safety side, while we will cover the extendibility in another article.
On the safety side, there are three major changes that we will delve into: contract interface, storage, and events.
But first, let’s examine an example contract with the new syntax:
#[starknet::interface]
trait IExternalContract<TContractState> {
fn subtraction_allowed(self: @TContractState) -> bool;
}
#[starknet::interface]
trait iMathContract<TContractState> {
fn get_total(self: @TContractState) -> u128
fn addition(ref self: TContractState, amount: u128);
fn subtraction(ref self: TContractState, amount: u128);
}
#[starknet::contract]
mod math_contract{
use starknet::ContractAddress;
use super:: {
IExternalContractDispatcher,
IExternalContractDispatcherTrait,
IExternalContractLibraryDispatcher
};
#[storage]
struct Storage {
total: u128,
external_contract : IExternalContractDispatcher
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Addition: Addition,
Subtraction: Subtraction
}
#[derive(Drop, starknet::Event)]
struct Addition {
amount: u128
}
#[derive(Drop, starknet::Event)]
struct Subtraction {
amount: u128
}
#[constructor]
fn constructor(ref self: ContractState, _initial_value: u128, _external_contract: ContractAddress){
self.total.write(_initial_value);
self.external_contract.write(IExternalContractDispatcher {contract_address: _external_contract})
}
#[external(v0)]
impl MathContract of super::iMathContract<ContractState>{
fn get_total(self: @ContractState) -> u128 {
self.total.read()
}
fn addition(ref self: ContractState, amount: u128){
let current_total = self.total.read();
self.total.write(current_total + amount);
self.emit(Addition { amount });
}
fn subtraction(ref self: ContractState, amount: u128){
let allowed = self.external_contract.read().subtraction_allowed();
if allowed {
let current_total = self.total.read();
self.total.write(current_total - amount);
self.emit(Subtraction { amount });
}
}
}
}
Let’s analyse the contract and examine each individual component more closely.
Contract Interfaces
By leveraging traits
and impls
, contract interfaces can become more readable, with improved clarity and safety ensured when interacting with the contract.
One noticeable change is that all the Starknet related attributes that are outside the contract module will be defined with the #[starknet::]
prefix from now on.
For example in the previous version of Cairo (v.1.0.0 for example) we would define an external interface such as:
#[abi]
trait IExternalContract {
fn subtraction_allowed() -> bool;
}
In the new syntax, this is done as:
#[starknet::interface]
trait IExternalContract<TContractState> {
fn subtraction_allowed(self: @TContractState) -> bool;
}
Storage now will require the #[storage]
attribute when defining the contract’s storage.
#[storage]
struct Storage {
total: u128,
external_contract : IExternalContractDispatcher
}
Other changes include introduction of new generic types such as TContractState
and self
. These new generic types aim to introduce storage safety, better readability and overall avoidance of any side effects when interacting with the contract state and storage variables.
#[starknet::interface]
trait iMathContract<TContractState> {
fn get_total(self: @TContractState) -> u128
fn addition(ref self: TContractState, amount: u128);
fn subtraction(ref self: TContractState, amount: u128);
In the example above, we define a trait called iMathContract
, and pass the contract state as TContractState
. In this trait we defined a view function get_total()
and two external functions namely addition()
and subtraction()
. The self
parameter in each of these functions refers to contract state.
In the previous version of Cairo the view function was denoted by the #[view]
attribute, however in the new syntax this has been removed. The reason why is that we define the get_counter
function “indirectly” by passing a snapshot of the @TContractState
, denominated by the argument @
. This infers that the function has read-only access to the state and cannot be changed.
fn get_total(self: @TContractState) -> u128
For #[external]
functions, we no longer need to explicitly define the attribute, as our external function will be part of the implementation of the IMathContract
trait. The signature of the function makes it clear that external functions modify the state by using the ref
argument, implying that self
may be modified and at the end of the function, ownership of self
is implicitly returned to the caller.
fn addition(ref self: TContractState, amount: u128);
#[external(v0)]
The reason why we need the (v0)
argument in the #[external(v0)]
attribute is that future compilers will support multiple impls
under the same name. Currently, the compiler does not accept multiple impls
, but this will be possible in the near future. The (v0)
is added to ensure that current contracts remain compatible with future compilers that allow multiple impls
even when compiling with a new compiler.
#[external(v0)]
impl MathContract of super::iMathContract<ContractState>{
}
Multiple impls
With the new syntax, it will be possible in future versions to define two impls
with the same name, allowing us to define different behaviours for the contract. For example, we can have two impls
with the same name but using different integer types such as u128
and u256
:
#[starknet::interface]
trait iMathContract<TContractState> {
fn get_total(self: @TContractState) -> u128
fn addition(ref self: TContractState, amount: u128);
fn subtraction(ref self: TContractState, amount: u128);
#[starknet::interface]
trait iMathContract<TContractState> {
fn get_total(self: @TContractState) -> u256
fn addition(ref self: TContractState, amount: u256);
fn subtraction(ref self: TContractState, amount: u256);
Events have undergone changes in their syntax as well. Now, we need to use the #[event] attribute to define events and unify them under enum Event{} . Each event must be defined as a struct and derived from starknet::Event.
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Addition: Addition,
Subtraction: Subtraction
}
#[derive(Drop, starknet::Event)]
struct Addition {
amount: u128
}
#[derive(Drop, starknet::Event)]
struct Subtraction {
amount: u128
}
To emit an event, you need to call it within a function that has access to the ContractState
. You can emit the event by calling self.emit()
followed by the event name and the corresponding data.
fn addition(ref self: ContractState, amount: u128){
self.emit(Addition { amount });
}
New features added:
For a full release note check out the full report here
- introduction of the
||
and&&
operators - possibility of short circuiting with logical operators
#[generate_trait]
attributereturn;
andbreak;
support- support for
pop_log
in testing
For more information on the differences between the old syntax and the new syntax check out Starknet Documentation.