Storage
When developing a smart contract, you will typically need some sort of persistent storage. In this case, persistent storage, often just called storage in this context, is a place where you can store values that are persisted inside the contract itself. This is in contrast to a regular value in memory, which disappears after the contract exits.
Put in conventional programming terms, contract storage is like saving data to a hard drive. That data is saved even after the program which saved it exits. That data is persistent. Using memory is like declaring a variable in a program: it exists for the duration of the program and is non-persistent.
Some basic use cases of storage include declaring an owner address for a contract and saving balances in a wallet.
Storage Accesses Via the storage
Keyword
Declaring variables in storage requires a storage
declaration that contains a list of all your variables and their types as follows:
storage {
var1: Type1,
var2: Type2,
...
}
To write into a storage variable, you need to use the storage
keyword as follows:
storage.var1 = v;
To read a storage variable, you also need to use the storage
keyword as follows:
let v = storage.var1;
Notes:
- The only types currently supported by the syntax above are integers, Booleans, and structs.
- Storage, in general, is still work-in-progress and so, its use model may change in the future.
Storage Maps
Generic storage maps are available in the standard library as StorageMap<K, V>
which have to be defined inside a storage
block and allow you to call insert()
and get()
to insert values at specific keys and get those values respectively. For example:
contract;
use std::storage::StorageMap;
struct Data {
x: b256,
y: str[4],
}
storage {
map1: StorageMap<u64,
u64>, map2: StorageMap<(b256,
bool), Data>,
}
abi StorageMapExample {
#[storage(write)]fn insert_into_map1(key: u64, value: u64);
#[storage(read)]fn get_from_map1(key: u64, value: u64) -> u64;
#[storage(write)]fn insert_into_map2(key: (b256, bool), value: Data);
#[storage(read)]fn get_from_map2(key: (b256, bool), value: Data) -> Data;
}
impl StorageMapExample for Contract {
#[storage(write)]fn insert_into_map1(key: u64, value: u64) {
storage.map1.insert(key, value);
}
#[storage(read)]fn get_from_map1(key: u64, value: u64) -> u64 {
storage.map1.get(key)
}
#[storage(write)]fn insert_into_map2(key: (b256, bool), value: Data) {
storage.map2.insert(key, value);
}
#[storage(read)]fn get_from_map2(key: (b256, bool), value: Data) -> Data {
storage.map2.get(key)
}
}
Because storage maps have to be defined inside a storage
block, the storage
keyword is required to access the map itself and then access the appropriate method.
Note: Calling
get(k)
for some keyk
that does not exist in the map (i.e.insert()
hasn't been called with keyk
yet) returns zero. This is because the FuelVM initializes all storage slots to zero.
Manual Storage Management
It is possible to leverage FuelVM storage operations directly using the std::storage::store
and std::storage::get
functions provided in the standard library. With this approach you will have to manually assign the internal key used for storage. An example is as follows:
contract;
use std::storage::{get, store};
abi StorageExample {
#[storage(write)]fn store_something(amount: u64);
#[storage(read)]fn get_something() -> u64;
}
const STORAGE_KEY: b256 = 0x0000000000000000000000000000000000000000000000000000000000000000;
impl StorageExample for Contract {
#[storage(write)]fn store_something(amount: u64) {
store(STORAGE_KEY, amount);
}
#[storage(read)]fn get_something() -> u64 {
let value = get::<u64>(STORAGE_KEY);
value
}
}
Note: Though these functions can be used for any data type, they should mostly be used for arrays because arrays are not yet supported in
storage
blocks. Note, however, that all data types can be used as types for keys and/or values inStorageMap<K, V>
without any restrictions.