Storage optimisation

A smart contract has a limited amount of storage slots. Each slot can store a single felt252 value. Writing to a storage slot has a cost, so we want to use as few storage slots as possible.

In Cairo, every type is derived from the felt252 type, which uses 252 bits to store a value. This design is quite simple, but it does have a drawback: it is not storage efficient. For example, if we want to store a u8 value, we need to use an entire slot, even though we only need 8 bits.

Packing

When storing multiple values, we can use a technique called packing. Packing is a technique that allows us to store multiple values in a single felt value. This is done by using the bits of the felt value to store multiple values.

For example, if we want to store two u8 values, we can use the first 8 bits of the felt value to store the first u8 value, and the last 8 bits to store the second u8 value. This way, we can store two u8 values in a single felt value.

Cairo provides a built-in store using packing that you can use with the StorePacking trait.

trait StorePacking<T, PackedT> {
    fn pack(value: T) -> PackedT;
    fn unpack(value: PackedT) -> T;
}

This allows to store the type T by first packing it into the type PackedT with the pack function, and then storing the PackedT value with it's Store implementation. When reading the value, we first retrieve the PackedT value, and then unpack it into the type T using the unpack function.

Here's an example of storing a Time struct with two u8 values using the StorePacking trait:

#[derive(Copy, Serde, Drop)]
pub struct Time {
    pub hour: u8,
    pub minute: u8
}

#[starknet::interface]
pub trait ITime<TContractState> {
    fn set(ref self: TContractState, value: Time);
    fn get(self: @TContractState) -> Time;
}

#[starknet::contract]
pub mod TimeContract {
    use super::Time;
    use starknet::storage_access::StorePacking;

    #[storage]
    struct Storage {
        time: Time
    }

    impl TimePackable of StorePacking<Time, felt252> {
        fn pack(value: Time) -> felt252 {
            let msb: felt252 = 256 * value.hour.into();
            let lsb: felt252 = value.minute.into();
            return msb + lsb;
        }
        fn unpack(value: felt252) -> Time {
            let value: u16 = value.try_into().unwrap();
            let (q, r) = DivRem::div_rem(value, 256_u16.try_into().unwrap());
            let hour: u8 = Into::<u16, felt252>::into(q).try_into().unwrap();
            let minute: u8 = Into::<u16, felt252>::into(r).try_into().unwrap();
            return Time { hour, minute };
        }
    }

    #[abi(embed_v0)]
    impl TimeContract of super::ITime<ContractState> {
        fn set(ref self: ContractState, value: Time) {
            // This will call the pack method of the TimePackable trait
            // and store the resulting felt252
            self.time.write(value);
        }
        fn get(self: @ContractState) -> Time {
            // This will read the felt252 value from storage
            // and return the result of the unpack method of the TimePackable trait
            return self.time.read();
        }
    }
}
Last change: 2024-02-15, commit: 89037ca