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, their types, and their initial values as follows:

struct Type1 {
    x: u64,
    y: u64,
}

struct Type2 {
    w: b256,
    z: bool,
}

storage {
    var1: Type1 = Type1 {
        x: 0,
        y: 0,
    },
    var2: Type2 = Type2 {
        w: 0x0000000000000000000000000000000000000000000000000000000000000000,
        z: false,
    },
}

To write into a storage variable, you need to use the storage keyword as follows:

    #[storage(write)]fn store_something() {
        storage.var1.x = 42;
        storage.var1.y = 77;
        storage.var2.w = 0x1111111111111111111111111111111111111111111111111111111111111111;
        storage.var2.z = true;
    }

To read a storage variable, you also need to use the storage keyword as follows:

    #[storage(read)]fn get_something() -> (u64, u64, b256, bool) {
        (storage.var1.x, storage.var1.y, storage.var2.w, storage.var2.z)
    }

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> = StorageMap {
    },
    map2: StorageMap<(b256,
    bool), Data> = StorageMap {
    },
}

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 key k that does not exist in the map (i.e. insert() hasn't been called with key k 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 in StorageMap<K, V> without any restrictions.