Cairo v2.11.0 is out!

Cairo 2.11.0 was just released. This version only involves high-level compiler (Cairo→Sierra) changes and thus can be used to deploy contracts on Starknet v0.13.4. This means that Cairo 2.11.0 is usable on Testnet without delay and will be usable on Mainnet later this month.

We proceed to discuss a few of the noticeable updates of Cairo v2.10.0 (for an exhaustive list of changes, see the release notes):

In addition to changes in Cairo, the language server continues to improve and create a smoother dev experience, you can read about the recent and upcoming changes in this tweet thread.

Another notable change is Scarb’s support for procedural macros. While already in v2.11.0, the compiler<>macro interface is expected to change in the next version. Given the scope of the feature, procedural macros will get their own post in the coming weeks alongside the new interface.

Breaking changes

While not a breaking change, we’d like to use this opportunity to remind developers that Cairo ≥ 2.10.0 requires starknet-foundry ≥ 0.38.0 to function properly.

The new change introduced with Cairo 2.11.0 is that dependency on snforge_std < 0.38.0 (note that we distinguish snforge_std the library, from foundry the testing engine, as one can use new foundry versions with older snforge_std libraries), then for tests to compile you need to add the following to your dev dependencies section in your Scarb.toml:

snforge_std = "x.y.z"
snforge_scarb_plugin = "x.y.z"

That is, whenever you depend on snforge_std , add a dependency on snforge_scarb_plugin with the same version.

Note that this is NOT required when using snforge_std ≥ 0.38.0.

Corelib updates

Iterator trait

In Cairo v2.11.0 the iterator trait is much richer, and includes the following functions:

  • count
  • last
  • advance_by
  • nth
  • map
  • enumerate
  • fold
  • any
  • all
  • find
  • filter
  • zip
  • collect
  • peekable
  • take
  • sum
  • product
  • chain

You can find a few usage examples below, for more examples, see iter_test.cairo in the corelib:

let mut iter = array![1, 2, 3].into_iter().map(|x| 2 * x);

assert_eq!(iter.next(), Some(2));
assert_eq!(iter.next(), Some(4));
assert_eq!(iter.next(), Some(6));
assert_eq!(iter.next(), None);
for (index, elem) in arr.into_iter().enumerate() {
    ...
}
let mut iter = array![0_u32, 1, 2].into_iter().filter(|x| *x > 0);
assert_eq!(iter.next(), Option::Some(1));
assert_eq!(iter.next(), Option::Some(2));
assert_eq!(iter.next(), Option::None);
let mut iter = array![1, 2, 3].into_iter();
let sum = iter.fold(0, |acc, x| acc + x);
assert_eq!(sum, 6);

Option trait

The following functions are added to OptionTrait:

  • ok_or_else
  • and
  • and_then
  • or
  • or_else
  • xor
  • is_some_and
  • is_none_or
  • unwrap_or_else
  • map
  • map_or
  • map_or_else
  • take
  • filter
  • flatten

You can find a few usage examples below, for more examples, see option_test.cairo in the corelib:

let maybe_some_string: Option<ByteArray> = Some("Hello, World!");
let maybe_some_len = maybe_some_string.map(|s| s.len());
assert!(maybe_some_len == Some(13));
let k = 21;
let mut x = Some("foo");
assert_eq!(x.map_or_else(|| 2 * k, |v: ByteArray| v.len()), 3);
x = None;
assert_eq!(x.map_or_else(|| 2 * k, |v: ByteArray| v.len()), 42);
let option: Option<felt252> = None;
assert_eq!(option.ok_or_else(|| 0), Err(0));
let x: Option<Option<u32>> = Some(Some(6));
assert_eq!(Some(6), x.flatten());

Using enum variants

You can now import concrete enum variants, and refer to them without the full path:

mod definitions {
  enum MyEnum {
     Var1,
     Var2
  }
}

use definitions::MyEnum::{Var1, Var2}

let my_enum = Var1;
match my_enum {
  Var1 => 1,
  Var2 => 2
}

In particular, the prelude now includes Option::Some and Option::None, so you can write the following without using the Option:: prefix:

let something = Some(3);
let nothing = None;

Consts updates

Const ContractAddress and ClassHash

You can now instantiate constants of the ContractAddress and ClassHash types, the generic functions contract_address_const and class_hash_const are no longer needed:

use starknet::{ContractAddress, ClassHash};

const class_hash: ClassHash = 0x123.try_into().unwrap();
const contract_address: ContractAddress = 0x123.try_into().unwrap();

Note that we don’t have dedicated literals for those types, hence conversions are necessary.

Const functions

Functions that can be evaluated at compile time can now be marked as const via the const fn syntax, similarly to Rust:

use core::num::traits::Pow;

const mask: u32 = 2_u32.pow(20)

Several functions in the corelib are now marked by const, in fact this was used in the previous section where we applied try_into and unwrap in a const expression.

Storage scrubbing

The Store trait now the additional scrub function:

fn scrub(
    address_domain: u32, base: StorageBaseAddress, offset: u8,
) -> SyscallResult<()>

Since this function has a defsiault implementation, this is a non-breaking change. With scrubbing, you can “remove” from storage by writing zeros instead of the existing value.

:light_bulb:Note that scrubbing storage is an expensive operation: scrubbing will cost 32*TYPE_SIZE L1 blob gas. For example, a struct with 2 u256 members will cost 32*4=128 blob gas to scrub.

New Vec interface

The MutableVecTrait now includes the following functions:

fn allocate(self: T) -> StoragePath<Mutable<Self::ElementType>>;

fn push<+Drop<Self::ElementType>, +starknet::Store<Self::ElementType>>(
    self: T, value: Self::ElementType,
);

fn pop<+Drop<Self::ElementType>, +starknet::Store<Self::ElementType>>(
    self: T,
) -> Option<Self::ElementType>;

Following community feedback, we’re adding the more intuitive push and pop interface to Vec, which replaces the append and write flow. For backward compatibility purposes, the old functions still exist in the Vec trait, but are marked as deprecated.

Below you can find an example of interaction with the new Vec interface:

use starknet::storage::{
   StoragePointerReadAccess,
   StoragePointerWriteAccess,
   Vec,
   MutableVecTrait
};

#[storage]
struct Storage {
    my_vec: Vec<u8>
}

...

self.my_vec.push(1);
self.my_vec.push(2);
self.my_vec.push(3);

assert_eq!(self.my_vec[0].read(), 1);
assert_eq!(self.my_vec[1].read(), 2);
assert_eq!(self.my_vec[2].read(), 3);

// pop
assert_eq!(self.my_vec.pop(), Some(3));
assert_eq!(self.my_vec.len(), 2);

:light_bulb:Note that the pop function is costly as it involves a call to scrub, which involves multiple storage writes (depending on the underlying type’s size).

The append function is now deprecated, and using it will emit a warning (which can be canceled by using the starknet-storage-deprecation feature). When you have a Vec of types that do not implement the Store trait, e.g. a vector of storage nodes, then you’ll need to use the new allocate function, which is similar to the deprecated append (in fact, they have exactly the same implementation):

use starknet::storage::{
    Vec,
    MutableVecTrait,
    Map,
    StorageMapReadAccess,
    StorageMapWriteAccess,
    StoragePointerReadAccess,
    StoragePointerWriteAccess
};

#[starknet::storage_node]
struct node {
    map: Map<u8,u8>
}

#[storage]
struct Storage {
    my_vec: Vec<node>
}

...

self.my_vec.allocate().map.write(1, 1);

To add a new node to my_vec, we must use the allocate function since node itself cannot be instantiated (it only serves to indicate storage paths via its members) and hence can never be an argument for push.

Early return and error propagation in loops

We can now have return statements and use the ? operator inside loops:

fn foo() -> Result<u8, ByteArray> {
    // error propagation
    for i in 1..10_u64 {
        let _converted: u8 = i.try_into().ok_or("fail to convert u64 into u8")?;
    }

    // early return
    for i in 1..100_u64 {
        if (i == 42) {
            return Ok(42);
        }
    }

    Ok(42)
}

No more ; after curly brackets

Semicolon is no longer required after a loop block, i.e. the following is out:

for i in 1..10_u8 {
  println!("{}", i);
};

This is in:

for i in 1..10_u8 {
  println!("{}", i)
}

Deref is extended to methods

In Cairo v2.7.0 we introduced the Deref and DerefMut trait, which allowed transparent access to the members of type Dest from an instance of type Target when there is an impl of Deref<Target, Dest> in the current context. Now, this mechanism is extended to methods that operate on the Dest type, as demonstrated by the code below:

use core::ops::Deref;

struct MySource {
    pub data: u8
}

struct MyTarget {
    pub data: u8
}

#[generate_trait]
impl TargetMutImpl of TargetTrait {
    fn foo(ref self: MyTarget) -> u8 {
        self.data
    }
}

impl SourceDeref of Deref<MySource> {
    type Target = MyTarget;
    fn deref(self: MySource) -> MyTarget {
        MyTarget { 
            data: self.data 
        }
    }
}

Thanks to the Deref impl, we can call foo on MySource:

let mut source = MySource { data: 5 };

let res = source.foo(5);