Skip to main content

Uncached storage modification

Description

When a storage value is read into a local variable, mutated, and the same key is read again without writing the updated value back, the second read returns stale data. This can lead to incorrect state transitions and logic errors.

Why is this bad?

A stale re-read can cause logic that depends on the updated value to use outdated data. This is a cache coherence bug that can silently corrupt state.

Issue example

let mut value: i32 = env
.storage()
.persistent()
.get(&DataKey::Value(key.clone()))
.unwrap_or(0);

value += 1;

let reread: i32 = env
.storage()
.persistent()
.get(&DataKey::Value(key))
.unwrap_or(0);

In this example, the value is modified in memory but never written back to storage before a second read.

Remediated example

let mut value: i32 = env
.storage()
.persistent()
.get(&DataKey::Value(key.clone()))
.unwrap_or(0);

value += 1;

env.storage()
.persistent()
.set(&DataKey::Value(key.clone()), &value);

let reread: i32 = env
.storage()
.persistent()
.get(&DataKey::Value(key))
.unwrap_or(0);

The modified value is written back to storage before the key is read again.

How is it detected?

The detector tracks storage reads into locals, marks them dirty on mutation, and reports when the same key is read again without a write in between.