Using Cairo's Storage Trait for Persistent State in Starknet Contracts
Learn how to define persistent state in Cairo contracts using the storage trait, avoid common pitfalls, and verify state changes with a simple counter example.
09 Aug 2026, 14:09 UTC

Use the #[storage] attribute to define a struct that holds persistent mappings; this is the only way to retain data across transactions in Cairo.
How Cairo Storage Works
Cairo separates transient memory from persistent storage. Memory exists only for the duration of a single function call and is discarded when the call ends. Storage, by contrast, is recorded in the Starknet state and verified by the Zero‑Knowledge (ZK) proof that proves each state transition. The storage trait lets you declare a struct whose fields are StorageMap or StorageRef objects, each mapping a 252‑bit key (usually a Poseidon hash) to a value type. Because every load and store operation adds to the complexity of the ZK‑proof, developers must minimize direct storage interactions.
Worked Example: Persistent Counter
The following contract defines a storage struct with a single counter field. The increment function reads the current value, adds one in memory, and writes the result back to storage.
use starknet::storage::refs::StorageRef;
use starknet::storage::map::StorageMap;
#[storage]
struct CounterStorage {
pub count: StorageRef,
}
impl CounterStorage of SuperContract {
#[external(v0)]
pub fn increment(self: @mut CounterStorage) {
let current = self.count.read();
self.count.write(current + 1);
}
#[external(v0)]
pub fn get_count(self: @CounterStorage) -> u256 {
self.count.read()
}
}
To compile and test the contract:
- Save the code in
src/lib.cairo of a Scarb project.
- Run
scarb build to compile the contract (no special permissions required).
- Run
scarb test to execute the built‑in test suite; a simple test can call increment twice and assert that get_count returns 2.
This example shows the pattern of loading a value into transient memory, performing the computation there, and storing the final result once.
Limits and Common Mistakes
- Avoid storage inside loops. Each
readorwriteinside a loop forces the prover to generate a separate state transition for every iteration, dramatically increasing proof size and gas cost. Load the needed data into a memory array, process it, then write back once. - Key collisions. Storage keys are 252‑bit values. If you derive keys from user input without a cryptographic hash, two different inputs may map to the same key, overwriting data. Use the built‑in Poseidon hash function (
poseidon_hash_many) to create unique keys. - Confusing memory and storage. Declaring a variable as
StorageRefwhen it is only needed for the current function wastes proof resources. Use plain local variables for temporary values and reserveStorageReffor fields declared in the#[storage]struct. - Uninitialized reads. Reading a storage cell that has never been written returns the default value for its type (zero for numeric types). Do not assume the cell contains a meaningful value without an explicit initialization step.
- Storage layout changes. Modifying the
#[storage]struct after deployment (e.g., adding or removing fields) breaks compatibility with existing state because the storage slots are fixed at compile time. Treat the storage struct as immutable once the contract is deployed.
Verifying State Persistence
Use the Cairo testing framework to assert that storage updates survive between calls. A typical test pattern:
- Deploy the contract in a test context.
- Call a function that writes to storage (e.g.,
increment). - Call a read‑only function (e.g.,
get_count) and assert the expected value. - Repeat the write‑read cycle to confirm accumulation.
If the test passes, the storage writes have been correctly incorporated into the ZK‑proof and the state transition is valid on Starknet.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.