Journey through Cairo IV — A deep dive into Cairo’s Storage with Starklings

Darlington Nnam
Coinmonks
Published in
7 min readSep 7, 2022

--

welcome to the fourth episode in our series, “Journey through Cairo”. In our last article which you can find here, we took a good look at the felt data type, and short strings in Cairo.

As always, if you are just joining mid-flight, i’d advise you checkout the previous articles, as we’d be kicking off from our last checkpoint.

P.S: All code snippets used in this tutorial, makes use of the old Cairo syntaxes from Cairo-lang v0.9.0, since its what was used in our Starklings exercises as at the time of this writing.

Storage In Cairo

In the second part of this series, we differentiated between Cairo programs and Cairo contracts, noting that while programs are stateless, contracts run on Starknet VM, and as such have persistent state (storage).

A contract storage is a persistent storage space, where you could write, read and alter data. According to the official docs, it is a map with 2^251 slots, where each slot is a felt which is initialized to 0.

A storage variable in Cairo looks like this:

@storage_varfunc id() -> (number: felt):end

where @storage_var is called a decorator, and is used for specifying storage variables.

Decorators

Unlike solidity, all function executions in Cairo are specified by the func keyword, and are hardly distinguishable. To solve this issue, Cairo uses what we call decorators, to differentiate these functions. All decorators begins with an @.

Here are the most common decorators you’d encounter in Cairo:

  1. @storage_var — used for specifying state variables.
  2. @constructor — used for specifying constructors.
  3. @external — used for specifying functions that write to a state variable.
  4. @view — used for specifying functions that reads from a state variable.
  5. @event — used for specifying events
  6. @l1_handler — used for specifying functions that processes message sent from an L1 contract in a messaging bridge.

How to read and write from Contract Storage

writing to storage

Just as stated earlier, functions that writes to a state variable, must be specified with the @external decorator. Here is an example of a Cairo function that updates the storage variable from earlier:

PS: Don’t worry if you don’t understand everything here, as we haven’t treated Cairo functions yet

@externalfunc update_id{      syscall_ptr : felt*,      pedersen_ptr : HashBuiltin*,      range_check_ptr    }(_number: felt):    id.write(_number)end

The main focus here, is id.write(_number).

To write or update a state variable, we use variablename.write().

Reading from Storage

Reading the value of a state variable is super easy. As stated earlier, functions that reads from a state variable must be specified with @view decorator. Here is an example function that reads from the id state variable:

@viewfunc read_id{      syscall_ptr : felt*,      pedersen_ptr : HashBuiltin*,      range_check_ptr    }(_number: felt):    id.read()end

With the key focus on id.read(), we could see that similar to writing to storage, to read you just need to use variablename.read().

Storage Mapping

Unlike Solidity, where mapping has its own special keyword, Cairo also uses storage variables for mapping.

In our earlier example with the id, we had a state variable storing just a value, but we could also create more complex state variables, that have key -> value pairs. Here’s an example:

@storage_varfunc balance(address: felt) -> (amount: felt):end

In here, the state variable balance, is a mapping of addresses to the amount they hold.

To write to this type of state variable:

we’d need to provide both the key(address), and the value(amount), like this:

@externalfunc update_balance{      syscall_ptr : felt*,      pedersen_ptr : HashBuiltin*,      range_check_ptr    }(_address: felt, _amount: felt):    balance.write(_address, _amount)end

As you can see, we provide both the key and the value inside the write parentheses, because we need to specify the key for which its value is being updated.

And to read from this state variable:

We’d need just the address like this:

@viewfunc read_balance{      syscall_ptr : felt*,      pedersen_ptr : HashBuiltin*,      range_check_ptr    }(_address: felt, _amount: felt):    balance.read(_address)
end

We used just the key (address) here, because we only want to get the value for that particular key, not update or alter it. So using balance.read(_address), returns to us, the amount or balance of that particular address.

Having understood all that, let’s dive straight into some practical work with Starklings!

storage01.cairo

In here, we are expected, to create a storage named bool, that stores a single felt.

This is very easy, as it resembles our first, storage example with the id. So here’s how we’d want to create the bool state variable:

@storage_varfunc bool () -> (value: felt):end

Now let’s check if we pass the challenge..

Yeap! we did. Over to the next challenge

storage02.cairo

Do you know that we could also store Structs too? That’s what we are about to explore in this exercise.

So we have 3 TODOs.

  1. We are expected to create a storage named wallet, that maps one felt to another.

The storage variable, required for this challenge is very similar to what we did with our balance state variable (a key to value mapping):

@storage_varfunc wallet (id: felt) -> (amount: felt):end

2. We are expected to create a storage named height_map, mapping two felts to another.

This is also similar to what we did with the balance state variable, but this time, we are mapping 2 keys to 1 value:

@storage_varfunc height_map (length: felt, width: felt)  -> (height: felt):end

3. This is the last and the most powerful challenge! We are expected to map a felt, to an Id(struct). As you can see, the user value is a struct of type Id.

@storage_varfunc id (address: felt) -> (user: Id):end

Fingers crossed, lets check if we passed the test.

yesss! we did.

storage03.cairo

In here, we are expected to implement external and view functions to read and write to the bool state variable.

Just like we stated earlier, when a function writes to a storage variable, it is an external function and should be specified with the @external decorator, while if it reads from the storage variable, it is a view function, and should be specified with the @view decorator.

Our first function to fix, is the toggle function, which should update the bool state variable when called, but there is a catch here..

A boolean value can only be 0 or 1, so we are to implement this in such a way that each time the toggle function is called, if the bool value is 0, we’d update it to 1, and if its 1, we’d update it to 0. To do this, we’d implore the use of conditionals:

@externalfunc toggle{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}():  let (value) = bool.read()  if value == 0:    bool.write(1)  else:    bool.write(0)  end  return () end

Next, we’d need to fix the view_bool function to return the value of the bool state variable when called.

@viewfunc view_bool{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() -> (bool : felt):  let (value) = bool.read()  return (value)end

Here’s the complete code.

Now let’s check to see if what we did, is enough to pass the challenge.

Voila! we did.

Conclusion

Congratulations! You just mastered how storage works in Cairo.

Remember if you ever got stuck with anything, i have the solutions to these exercises in my repo here.

In our next article, we’d study implicit arguments in details.

If you got value from this article, do well to share with others.

You could also connect with me on the following socials, especially on Twitter, where i share my little findings on Cairo!

Twitter: https://twitter.com/0xdarlington

LinkedIn: https://www.linkedin.com/in/nnamdarlington

Github: https://github.com/Darlington02

New to trading? Try crypto trading bots or copy trading

--

--