Storing Collections with Vectors

The Vec type provides a way to store collections of values in the contract's storage. In this section, we will explore how to declare, add elements to and retrieve elements from a Vec, as well as how the storage addresses for Vec variables are computed.

The Vec type is provided by the Cairo core library, inside the starknet::storage module. Its associated methods are defined in the VecTrait and MutableVecTrait traits that you will also need to import for read and write operations on the Vec type.

The Array<T> type is a memory type and cannot be directly stored in contract storage. For storage, use the Vec<T> type, which is a [phantom type][phantom types] designed specifically for contract storage. However, Vec<T> has limitations: it can't be instantiated as a regular variable, used as a function parameter, or included as a member in regular structs. To work with the full contents of a Vec<T>, you'll need to copy its elements to and from a memory Array<T>.

Declaring and Using Storage Vectors

To declare a Storage Vector, use the Vec type enclosed in angle brackets <>, specifying the type of elements it will store. In Listing 15-3, we create a simple contract that registers all the addresses that call it and stores them in a Vec. We can then retrieve the n-th registered address, or all registered addresses.

use starknet::ContractAddress;

#[starknet::interface]
pub trait IAddressList<TState> {
    fn register_caller(ref self: TState);
    fn get_n_th_registered_address(self: @TState, index: u64) -> Option<ContractAddress>;
    fn get_all_addresses(self: @TState) -> Array<ContractAddress>;
    fn modify_nth_address(ref self: TState, index: u64, new_address: ContractAddress);
    fn pop_last_registered_address(ref self: TState) -> Option<ContractAddress>;
}

#[starknet::contract]
pub mod AddressList {
    use starknet::storage::{
        MutableVecTrait, StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait,
    };
    use starknet::{ContractAddress, get_caller_address};

    #[storage]
    struct Storage {
        addresses: Vec<ContractAddress>,
    }

    #[abi(embed_v0)]
    impl AddressListImpl of super::IAddressList<ContractState> {
        fn register_caller(ref self: ContractState) {
            let caller = get_caller_address();
            self.addresses.push(caller);
        }

        fn get_n_th_registered_address(
            self: @ContractState, index: u64,
        ) -> Option<ContractAddress> {
            self.addresses.get(index).map(|ptr| ptr.read())
        }

        fn get_all_addresses(self: @ContractState) -> Array<ContractAddress> {
            let mut addresses = array![];
            for i in 0..self.addresses.len() {
                addresses.append(self.addresses[i].read());
            }
            addresses
        }

        fn modify_nth_address(ref self: ContractState, index: u64, new_address: ContractAddress) {
            self.addresses[index].write(new_address);
        }

        fn pop_last_registered_address(ref self: ContractState) -> Option<ContractAddress> {
            self.addresses.pop()
        }
    }
}

#[cfg(test)]
mod tests {
    use snforge_std::{
        ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address,
        stop_cheat_caller_address,
    };
    use starknet::ContractAddress;
    use super::{IAddressListDispatcher, IAddressListDispatcherTrait};

    fn deploy_contract() -> IAddressListDispatcher {
        let contract = declare("AddressList").unwrap().contract_class();
        let (contract_address, _) = contract.deploy(@array![]).unwrap();
        IAddressListDispatcher { contract_address }
    }

    #[test]
    fn test_get_out_of_bounds_returns_none() {
        let dispatcher = deploy_contract();
        assert!(dispatcher.get_n_th_registered_address(0).is_none());
    }

    #[test]
    fn test_register_and_get_single() {
        let dispatcher = deploy_contract();

        let user: ContractAddress = 0x111.try_into().unwrap();
        start_cheat_caller_address(dispatcher.contract_address, user);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        let first = dispatcher.get_n_th_registered_address(0).unwrap();
        assert_eq!(first, user);
    }

    #[test]
    fn test_register_multiple_and_get_all_in_order() {
        let dispatcher = deploy_contract();

        let a1: ContractAddress = 0xaaa.try_into().unwrap();
        let a2: ContractAddress = 0xbbb.try_into().unwrap();
        let a3: ContractAddress = 0xccc.try_into().unwrap();

        start_cheat_caller_address(dispatcher.contract_address, a1);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a2);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a3);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        let mut all = dispatcher.get_all_addresses();
        assert_eq!(all.pop_front().unwrap(), a1);
        assert_eq!(all.pop_front().unwrap(), a2);
        assert_eq!(all.pop_front().unwrap(), a3);
        assert!(all.pop_front().is_none());
    }

    #[test]
    fn test_modify_nth_address() {
        let dispatcher = deploy_contract();

        let a1: ContractAddress = 0x101.try_into().unwrap();
        let a2: ContractAddress = 0x202.try_into().unwrap();
        let a3: ContractAddress = 0x303.try_into().unwrap();
        let new_mid: ContractAddress = 0x404.try_into().unwrap();

        start_cheat_caller_address(dispatcher.contract_address, a1);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a2);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a3);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        // Modify the second entry (index 1)
        dispatcher.modify_nth_address(1, new_mid);

        assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);
        assert_eq!(dispatcher.get_n_th_registered_address(1).unwrap(), new_mid);
        assert_eq!(dispatcher.get_n_th_registered_address(2).unwrap(), a3);
    }

    #[test]
    fn test_pop_empty_returns_none() {
        let dispatcher = deploy_contract();
        assert!(dispatcher.pop_last_registered_address().is_none());
    }

    #[test]
    fn test_pop_removes_last_in_lifo_order() {
        let dispatcher = deploy_contract();

        let a1: ContractAddress = 0x111.try_into().unwrap();
        let a2: ContractAddress = 0x222.try_into().unwrap();

        start_cheat_caller_address(dispatcher.contract_address, a1);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a2);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        // First pop returns last pushed (a2)
        assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a2);
        // Index 1 should now be out of bounds
        assert!(dispatcher.get_n_th_registered_address(1).is_none());
        // Index 0 remains a1
        assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);

        // Second pop returns a1 and empties the list
        assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a1);
        assert!(dispatcher.get_n_th_registered_address(0).is_none());
        // Further pops return None
        assert!(dispatcher.pop_last_registered_address().is_none());
    }
}

Declaring a storage Vec in the Storage struct

To add an element to a Vec, you can use the push method to add an element to the end of the Vec.

use starknet::ContractAddress;

#[starknet::interface]
pub trait IAddressList<TState> {
    fn register_caller(ref self: TState);
    fn get_n_th_registered_address(self: @TState, index: u64) -> Option<ContractAddress>;
    fn get_all_addresses(self: @TState) -> Array<ContractAddress>;
    fn modify_nth_address(ref self: TState, index: u64, new_address: ContractAddress);
    fn pop_last_registered_address(ref self: TState) -> Option<ContractAddress>;
}

#[starknet::contract]
pub mod AddressList {
    use starknet::storage::{
        MutableVecTrait, StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait,
    };
    use starknet::{ContractAddress, get_caller_address};

    #[storage]
    struct Storage {
        addresses: Vec<ContractAddress>,
    }

    #[abi(embed_v0)]
    impl AddressListImpl of super::IAddressList<ContractState> {
        fn register_caller(ref self: ContractState) {
            let caller = get_caller_address();
            self.addresses.push(caller);
        }

        fn get_n_th_registered_address(
            self: @ContractState, index: u64,
        ) -> Option<ContractAddress> {
            self.addresses.get(index).map(|ptr| ptr.read())
        }

        fn get_all_addresses(self: @ContractState) -> Array<ContractAddress> {
            let mut addresses = array![];
            for i in 0..self.addresses.len() {
                addresses.append(self.addresses[i].read());
            }
            addresses
        }

        fn modify_nth_address(ref self: ContractState, index: u64, new_address: ContractAddress) {
            self.addresses[index].write(new_address);
        }

        fn pop_last_registered_address(ref self: ContractState) -> Option<ContractAddress> {
            self.addresses.pop()
        }
    }
}

#[cfg(test)]
mod tests {
    use snforge_std::{
        ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address,
        stop_cheat_caller_address,
    };
    use starknet::ContractAddress;
    use super::{IAddressListDispatcher, IAddressListDispatcherTrait};

    fn deploy_contract() -> IAddressListDispatcher {
        let contract = declare("AddressList").unwrap().contract_class();
        let (contract_address, _) = contract.deploy(@array![]).unwrap();
        IAddressListDispatcher { contract_address }
    }

    #[test]
    fn test_get_out_of_bounds_returns_none() {
        let dispatcher = deploy_contract();
        assert!(dispatcher.get_n_th_registered_address(0).is_none());
    }

    #[test]
    fn test_register_and_get_single() {
        let dispatcher = deploy_contract();

        let user: ContractAddress = 0x111.try_into().unwrap();
        start_cheat_caller_address(dispatcher.contract_address, user);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        let first = dispatcher.get_n_th_registered_address(0).unwrap();
        assert_eq!(first, user);
    }

    #[test]
    fn test_register_multiple_and_get_all_in_order() {
        let dispatcher = deploy_contract();

        let a1: ContractAddress = 0xaaa.try_into().unwrap();
        let a2: ContractAddress = 0xbbb.try_into().unwrap();
        let a3: ContractAddress = 0xccc.try_into().unwrap();

        start_cheat_caller_address(dispatcher.contract_address, a1);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a2);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a3);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        let mut all = dispatcher.get_all_addresses();
        assert_eq!(all.pop_front().unwrap(), a1);
        assert_eq!(all.pop_front().unwrap(), a2);
        assert_eq!(all.pop_front().unwrap(), a3);
        assert!(all.pop_front().is_none());
    }

    #[test]
    fn test_modify_nth_address() {
        let dispatcher = deploy_contract();

        let a1: ContractAddress = 0x101.try_into().unwrap();
        let a2: ContractAddress = 0x202.try_into().unwrap();
        let a3: ContractAddress = 0x303.try_into().unwrap();
        let new_mid: ContractAddress = 0x404.try_into().unwrap();

        start_cheat_caller_address(dispatcher.contract_address, a1);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a2);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a3);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        // Modify the second entry (index 1)
        dispatcher.modify_nth_address(1, new_mid);

        assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);
        assert_eq!(dispatcher.get_n_th_registered_address(1).unwrap(), new_mid);
        assert_eq!(dispatcher.get_n_th_registered_address(2).unwrap(), a3);
    }

    #[test]
    fn test_pop_empty_returns_none() {
        let dispatcher = deploy_contract();
        assert!(dispatcher.pop_last_registered_address().is_none());
    }

    #[test]
    fn test_pop_removes_last_in_lifo_order() {
        let dispatcher = deploy_contract();

        let a1: ContractAddress = 0x111.try_into().unwrap();
        let a2: ContractAddress = 0x222.try_into().unwrap();

        start_cheat_caller_address(dispatcher.contract_address, a1);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a2);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        // First pop returns last pushed (a2)
        assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a2);
        // Index 1 should now be out of bounds
        assert!(dispatcher.get_n_th_registered_address(1).is_none());
        // Index 0 remains a1
        assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);

        // Second pop returns a1 and empties the list
        assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a1);
        assert!(dispatcher.get_n_th_registered_address(0).is_none());
        // Further pops return None
        assert!(dispatcher.pop_last_registered_address().is_none());
    }
}

To retrieve an element, you can use the indexing syntax (vec[index]) or the at/get methods to obtain a storage pointer to the element at the specified index, and then call read() to get the value. If the index is out of bounds, at (and indexing) panics, while get returns None.

use starknet::ContractAddress;

#[starknet::interface]
pub trait IAddressList<TState> {
    fn register_caller(ref self: TState);
    fn get_n_th_registered_address(self: @TState, index: u64) -> Option<ContractAddress>;
    fn get_all_addresses(self: @TState) -> Array<ContractAddress>;
    fn modify_nth_address(ref self: TState, index: u64, new_address: ContractAddress);
    fn pop_last_registered_address(ref self: TState) -> Option<ContractAddress>;
}

#[starknet::contract]
pub mod AddressList {
    use starknet::storage::{
        MutableVecTrait, StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait,
    };
    use starknet::{ContractAddress, get_caller_address};

    #[storage]
    struct Storage {
        addresses: Vec<ContractAddress>,
    }

    #[abi(embed_v0)]
    impl AddressListImpl of super::IAddressList<ContractState> {
        fn register_caller(ref self: ContractState) {
            let caller = get_caller_address();
            self.addresses.push(caller);
        }

        fn get_n_th_registered_address(
            self: @ContractState, index: u64,
        ) -> Option<ContractAddress> {
            self.addresses.get(index).map(|ptr| ptr.read())
        }

        fn get_all_addresses(self: @ContractState) -> Array<ContractAddress> {
            let mut addresses = array![];
            for i in 0..self.addresses.len() {
                addresses.append(self.addresses[i].read());
            }
            addresses
        }

        fn modify_nth_address(ref self: ContractState, index: u64, new_address: ContractAddress) {
            self.addresses[index].write(new_address);
        }

        fn pop_last_registered_address(ref self: ContractState) -> Option<ContractAddress> {
            self.addresses.pop()
        }
    }
}

#[cfg(test)]
mod tests {
    use snforge_std::{
        ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address,
        stop_cheat_caller_address,
    };
    use starknet::ContractAddress;
    use super::{IAddressListDispatcher, IAddressListDispatcherTrait};

    fn deploy_contract() -> IAddressListDispatcher {
        let contract = declare("AddressList").unwrap().contract_class();
        let (contract_address, _) = contract.deploy(@array![]).unwrap();
        IAddressListDispatcher { contract_address }
    }

    #[test]
    fn test_get_out_of_bounds_returns_none() {
        let dispatcher = deploy_contract();
        assert!(dispatcher.get_n_th_registered_address(0).is_none());
    }

    #[test]
    fn test_register_and_get_single() {
        let dispatcher = deploy_contract();

        let user: ContractAddress = 0x111.try_into().unwrap();
        start_cheat_caller_address(dispatcher.contract_address, user);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        let first = dispatcher.get_n_th_registered_address(0).unwrap();
        assert_eq!(first, user);
    }

    #[test]
    fn test_register_multiple_and_get_all_in_order() {
        let dispatcher = deploy_contract();

        let a1: ContractAddress = 0xaaa.try_into().unwrap();
        let a2: ContractAddress = 0xbbb.try_into().unwrap();
        let a3: ContractAddress = 0xccc.try_into().unwrap();

        start_cheat_caller_address(dispatcher.contract_address, a1);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a2);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a3);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        let mut all = dispatcher.get_all_addresses();
        assert_eq!(all.pop_front().unwrap(), a1);
        assert_eq!(all.pop_front().unwrap(), a2);
        assert_eq!(all.pop_front().unwrap(), a3);
        assert!(all.pop_front().is_none());
    }

    #[test]
    fn test_modify_nth_address() {
        let dispatcher = deploy_contract();

        let a1: ContractAddress = 0x101.try_into().unwrap();
        let a2: ContractAddress = 0x202.try_into().unwrap();
        let a3: ContractAddress = 0x303.try_into().unwrap();
        let new_mid: ContractAddress = 0x404.try_into().unwrap();

        start_cheat_caller_address(dispatcher.contract_address, a1);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a2);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a3);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        // Modify the second entry (index 1)
        dispatcher.modify_nth_address(1, new_mid);

        assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);
        assert_eq!(dispatcher.get_n_th_registered_address(1).unwrap(), new_mid);
        assert_eq!(dispatcher.get_n_th_registered_address(2).unwrap(), a3);
    }

    #[test]
    fn test_pop_empty_returns_none() {
        let dispatcher = deploy_contract();
        assert!(dispatcher.pop_last_registered_address().is_none());
    }

    #[test]
    fn test_pop_removes_last_in_lifo_order() {
        let dispatcher = deploy_contract();

        let a1: ContractAddress = 0x111.try_into().unwrap();
        let a2: ContractAddress = 0x222.try_into().unwrap();

        start_cheat_caller_address(dispatcher.contract_address, a1);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a2);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        // First pop returns last pushed (a2)
        assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a2);
        // Index 1 should now be out of bounds
        assert!(dispatcher.get_n_th_registered_address(1).is_none());
        // Index 0 remains a1
        assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);

        // Second pop returns a1 and empties the list
        assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a1);
        assert!(dispatcher.get_n_th_registered_address(0).is_none());
        // Further pops return None
        assert!(dispatcher.pop_last_registered_address().is_none());
    }
}

If you want to retrieve all the elements of the Vec, you can iterate over the indices of the storage Vec, read the value at each index, and append it to a memory Array<T>. Similarly, you can't store an Array<T> in storage: you would need to iterate over the elements of the array and append them to a storage Vec<T>.

At this point, you should be familiar with the concept of storage pointers and storage paths introduced in the "Contract Storage" section and how they are used to access storage variables through a pointer-based model. Thus how would you modify the address stored at a specific index of a Vec?

use starknet::ContractAddress;

#[starknet::interface]
pub trait IAddressList<TState> {
    fn register_caller(ref self: TState);
    fn get_n_th_registered_address(self: @TState, index: u64) -> Option<ContractAddress>;
    fn get_all_addresses(self: @TState) -> Array<ContractAddress>;
    fn modify_nth_address(ref self: TState, index: u64, new_address: ContractAddress);
    fn pop_last_registered_address(ref self: TState) -> Option<ContractAddress>;
}

#[starknet::contract]
pub mod AddressList {
    use starknet::storage::{
        MutableVecTrait, StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait,
    };
    use starknet::{ContractAddress, get_caller_address};

    #[storage]
    struct Storage {
        addresses: Vec<ContractAddress>,
    }

    #[abi(embed_v0)]
    impl AddressListImpl of super::IAddressList<ContractState> {
        fn register_caller(ref self: ContractState) {
            let caller = get_caller_address();
            self.addresses.push(caller);
        }

        fn get_n_th_registered_address(
            self: @ContractState, index: u64,
        ) -> Option<ContractAddress> {
            self.addresses.get(index).map(|ptr| ptr.read())
        }

        fn get_all_addresses(self: @ContractState) -> Array<ContractAddress> {
            let mut addresses = array![];
            for i in 0..self.addresses.len() {
                addresses.append(self.addresses[i].read());
            }
            addresses
        }

        fn modify_nth_address(ref self: ContractState, index: u64, new_address: ContractAddress) {
            self.addresses[index].write(new_address);
        }

        fn pop_last_registered_address(ref self: ContractState) -> Option<ContractAddress> {
            self.addresses.pop()
        }
    }
}

#[cfg(test)]
mod tests {
    use snforge_std::{
        ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address,
        stop_cheat_caller_address,
    };
    use starknet::ContractAddress;
    use super::{IAddressListDispatcher, IAddressListDispatcherTrait};

    fn deploy_contract() -> IAddressListDispatcher {
        let contract = declare("AddressList").unwrap().contract_class();
        let (contract_address, _) = contract.deploy(@array![]).unwrap();
        IAddressListDispatcher { contract_address }
    }

    #[test]
    fn test_get_out_of_bounds_returns_none() {
        let dispatcher = deploy_contract();
        assert!(dispatcher.get_n_th_registered_address(0).is_none());
    }

    #[test]
    fn test_register_and_get_single() {
        let dispatcher = deploy_contract();

        let user: ContractAddress = 0x111.try_into().unwrap();
        start_cheat_caller_address(dispatcher.contract_address, user);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        let first = dispatcher.get_n_th_registered_address(0).unwrap();
        assert_eq!(first, user);
    }

    #[test]
    fn test_register_multiple_and_get_all_in_order() {
        let dispatcher = deploy_contract();

        let a1: ContractAddress = 0xaaa.try_into().unwrap();
        let a2: ContractAddress = 0xbbb.try_into().unwrap();
        let a3: ContractAddress = 0xccc.try_into().unwrap();

        start_cheat_caller_address(dispatcher.contract_address, a1);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a2);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a3);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        let mut all = dispatcher.get_all_addresses();
        assert_eq!(all.pop_front().unwrap(), a1);
        assert_eq!(all.pop_front().unwrap(), a2);
        assert_eq!(all.pop_front().unwrap(), a3);
        assert!(all.pop_front().is_none());
    }

    #[test]
    fn test_modify_nth_address() {
        let dispatcher = deploy_contract();

        let a1: ContractAddress = 0x101.try_into().unwrap();
        let a2: ContractAddress = 0x202.try_into().unwrap();
        let a3: ContractAddress = 0x303.try_into().unwrap();
        let new_mid: ContractAddress = 0x404.try_into().unwrap();

        start_cheat_caller_address(dispatcher.contract_address, a1);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a2);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a3);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        // Modify the second entry (index 1)
        dispatcher.modify_nth_address(1, new_mid);

        assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);
        assert_eq!(dispatcher.get_n_th_registered_address(1).unwrap(), new_mid);
        assert_eq!(dispatcher.get_n_th_registered_address(2).unwrap(), a3);
    }

    #[test]
    fn test_pop_empty_returns_none() {
        let dispatcher = deploy_contract();
        assert!(dispatcher.pop_last_registered_address().is_none());
    }

    #[test]
    fn test_pop_removes_last_in_lifo_order() {
        let dispatcher = deploy_contract();

        let a1: ContractAddress = 0x111.try_into().unwrap();
        let a2: ContractAddress = 0x222.try_into().unwrap();

        start_cheat_caller_address(dispatcher.contract_address, a1);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a2);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        // First pop returns last pushed (a2)
        assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a2);
        // Index 1 should now be out of bounds
        assert!(dispatcher.get_n_th_registered_address(1).is_none());
        // Index 0 remains a1
        assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);

        // Second pop returns a1 and empties the list
        assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a1);
        assert!(dispatcher.get_n_th_registered_address(0).is_none());
        // Further pops return None
        assert!(dispatcher.pop_last_registered_address().is_none());
    }
}

The answer is fairly simple: get a mutable pointer to the storage pointer at the desired index, and use the write method to modify the value at that index.

You can also remove the last element of a storage Vec using the pop method. It returns Some(value) if the vector is non-empty and None otherwise, and updates the stored length accordingly.

use starknet::ContractAddress;

#[starknet::interface]
pub trait IAddressList<TState> {
    fn register_caller(ref self: TState);
    fn get_n_th_registered_address(self: @TState, index: u64) -> Option<ContractAddress>;
    fn get_all_addresses(self: @TState) -> Array<ContractAddress>;
    fn modify_nth_address(ref self: TState, index: u64, new_address: ContractAddress);
    fn pop_last_registered_address(ref self: TState) -> Option<ContractAddress>;
}

#[starknet::contract]
pub mod AddressList {
    use starknet::storage::{
        MutableVecTrait, StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait,
    };
    use starknet::{ContractAddress, get_caller_address};

    #[storage]
    struct Storage {
        addresses: Vec<ContractAddress>,
    }

    #[abi(embed_v0)]
    impl AddressListImpl of super::IAddressList<ContractState> {
        fn register_caller(ref self: ContractState) {
            let caller = get_caller_address();
            self.addresses.push(caller);
        }

        fn get_n_th_registered_address(
            self: @ContractState, index: u64,
        ) -> Option<ContractAddress> {
            self.addresses.get(index).map(|ptr| ptr.read())
        }

        fn get_all_addresses(self: @ContractState) -> Array<ContractAddress> {
            let mut addresses = array![];
            for i in 0..self.addresses.len() {
                addresses.append(self.addresses[i].read());
            }
            addresses
        }

        fn modify_nth_address(ref self: ContractState, index: u64, new_address: ContractAddress) {
            self.addresses[index].write(new_address);
        }

        fn pop_last_registered_address(ref self: ContractState) -> Option<ContractAddress> {
            self.addresses.pop()
        }
    }
}

#[cfg(test)]
mod tests {
    use snforge_std::{
        ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address,
        stop_cheat_caller_address,
    };
    use starknet::ContractAddress;
    use super::{IAddressListDispatcher, IAddressListDispatcherTrait};

    fn deploy_contract() -> IAddressListDispatcher {
        let contract = declare("AddressList").unwrap().contract_class();
        let (contract_address, _) = contract.deploy(@array![]).unwrap();
        IAddressListDispatcher { contract_address }
    }

    #[test]
    fn test_get_out_of_bounds_returns_none() {
        let dispatcher = deploy_contract();
        assert!(dispatcher.get_n_th_registered_address(0).is_none());
    }

    #[test]
    fn test_register_and_get_single() {
        let dispatcher = deploy_contract();

        let user: ContractAddress = 0x111.try_into().unwrap();
        start_cheat_caller_address(dispatcher.contract_address, user);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        let first = dispatcher.get_n_th_registered_address(0).unwrap();
        assert_eq!(first, user);
    }

    #[test]
    fn test_register_multiple_and_get_all_in_order() {
        let dispatcher = deploy_contract();

        let a1: ContractAddress = 0xaaa.try_into().unwrap();
        let a2: ContractAddress = 0xbbb.try_into().unwrap();
        let a3: ContractAddress = 0xccc.try_into().unwrap();

        start_cheat_caller_address(dispatcher.contract_address, a1);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a2);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a3);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        let mut all = dispatcher.get_all_addresses();
        assert_eq!(all.pop_front().unwrap(), a1);
        assert_eq!(all.pop_front().unwrap(), a2);
        assert_eq!(all.pop_front().unwrap(), a3);
        assert!(all.pop_front().is_none());
    }

    #[test]
    fn test_modify_nth_address() {
        let dispatcher = deploy_contract();

        let a1: ContractAddress = 0x101.try_into().unwrap();
        let a2: ContractAddress = 0x202.try_into().unwrap();
        let a3: ContractAddress = 0x303.try_into().unwrap();
        let new_mid: ContractAddress = 0x404.try_into().unwrap();

        start_cheat_caller_address(dispatcher.contract_address, a1);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a2);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a3);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        // Modify the second entry (index 1)
        dispatcher.modify_nth_address(1, new_mid);

        assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);
        assert_eq!(dispatcher.get_n_th_registered_address(1).unwrap(), new_mid);
        assert_eq!(dispatcher.get_n_th_registered_address(2).unwrap(), a3);
    }

    #[test]
    fn test_pop_empty_returns_none() {
        let dispatcher = deploy_contract();
        assert!(dispatcher.pop_last_registered_address().is_none());
    }

    #[test]
    fn test_pop_removes_last_in_lifo_order() {
        let dispatcher = deploy_contract();

        let a1: ContractAddress = 0x111.try_into().unwrap();
        let a2: ContractAddress = 0x222.try_into().unwrap();

        start_cheat_caller_address(dispatcher.contract_address, a1);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        start_cheat_caller_address(dispatcher.contract_address, a2);
        dispatcher.register_caller();
        stop_cheat_caller_address(dispatcher.contract_address);

        // First pop returns last pushed (a2)
        assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a2);
        // Index 1 should now be out of bounds
        assert!(dispatcher.get_n_th_registered_address(1).is_none());
        // Index 0 remains a1
        assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);

        // Second pop returns a1 and empties the list
        assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a1);
        assert!(dispatcher.get_n_th_registered_address(0).is_none());
        // Further pops return None
        assert!(dispatcher.pop_last_registered_address().is_none());
    }
}

Storage Address Computation for Vecs

The address in storage of a variable stored in a Vec is computed according to the following rules:

  • The length of the Vec is stored at the base address, computed as sn_keccak(variable_name).
  • The elements of the Vec are stored in addresses computed as h(base_address, i), where i is the index of the element in the Vec and h is the Pedersen hash function.

Summary

  • Use the Vec type to store collections of values in contract storage
  • Access Vecs using the push method to add elements, the pop method to remove the last element, and the at/indexing or get methods to read elements
  • The address of a Vec variable is computed using the sn_keccak and the Pedersen hash functions

This wraps up our tour of the Contract Storage! In the next section, we'll start looking at the different kind of functions defined in a contract. You already know most of them, as we used them in the previous chapters, but we'll explain them in more detail.