Contract fails to deliver promised returns, but doesn't lose value
Description
Thunder Exchange
Faulty Index out of Bounds
Description
An issue has been identified where the index out-of-bounds check is incorrectly implemented, potentially leading to incorrect error handling.
Root Cause
The problem originates from the current implementation of the getter functions for vector types in both the Asset Manager and Execution Manager modules.
Specifically, the checks for valid indices do not correctly account for the last index of vectors, leading to potential out-of-bound access. The problematic code is as follows:
// execution managerfnget_whitelisted_strategy(index:u64) ->Option<ContractId> {let len = storage.strategies.len();require(len !=0, ExecutionManagerErrors::ZeroLengthVec);require(index <= len, ExecutionManagerErrors::IndexOutOfBound); // here is the issue storage.strategies.get(index).unwrap().try_read() }// asset managerfnget_supported_asset(index:u64) ->Option<AssetId> {let len = storage.assets.len();require(len !=0, AssetManagerErrors::ZeroLengthVec);require(index <= len, AssetManagerErrors::IndexOutOfBound); // here is the issue storage.assets.get(index).unwrap().try_read() }
The current check allows indices up to and including the vector length. However, since vector indices are zero-based, the valid range is from 0 to len - 1. This error could result in an out-of-bounds access attempt.
Impact
Luckily, the Fuel Storage Vector implementation includes a safeguard that verifies the index is within bounds during access. Therefore, the primary impact is the incorrect error log being generated, rather than any actual out-of-bound access or program failure, which is somewhere between an Insight and Low issue.
Proposed fix
Check the index is index < len
Proof of concept
Proof of Concept
There are some steps to follow:
Create forc.toml in contracts-v1 and add the below in the forc.toml:
contract;use standards::{src20::SRC20, src3::SRC3};use std::{ asset::{ burn, mint_to, }, call_frames::msg_asset_id, context::msg_amount, hash::Hash, storage::storage_string::*, string::String,};// In this example, all assets minted from this contract have the same decimals, name, and symbolconfigurable {/// The decimals of every asset minted by this contract. DECIMALS:u8=9u8,/// The name of every asset minted by this contract. NAME:str[12] =__to_str_array("ExampleAsset"),/// The symbol of every asset minted by this contract. SYMBOL:str[2] =__to_str_array("EA"),}storage {/// The total number of distinguishable assets this contract has minted. total_assets:u64=0,/// The total supply of a particular asset. total_supply:StorageMap<AssetId, u64> =StorageMap {},}impl SRC3 forContract {/// Unconditionally mints new assets using the `sub_id` sub-identifier.////// # Arguments////// * `recipient`: [Identity] - The user to which the newly minted asset is transferred to./// * `sub_id`: [SubId] - The sub-identifier of the newly minted asset./// * `amount`: [u64] - The quantity of coins to mint.////// # Number of Storage Accesses////// * Reads: `2`/// * Writes: `2`////// # Examples////// ```sway/// use src3::SRC3;/// use std::constants::DEFAULT_SUB_ID;////// fn foo(contract_id: ContractId) {/// let contract_abi = abi(SRC3, contract_id);/// contract_abi.mint(Identity::ContractId(contract_id), DEFAULT_SUB_ID, 100);/// }/// ``` #[storage(read, write)]fnmint(recipient:Identity, sub_id:SubId, amount:u64) {let asset_id =AssetId::new(ContractId::this(), sub_id);// If this SubId is new, increment the total number of distinguishable assets this contract has minted.let asset_supply = storage.total_supply.get(asset_id).try_read();match asset_supply {None=> { storage.total_assets.write(storage.total_assets.read() +1) }, _ => {}, }// Increment total supply of the asset and mint to the recipient. storage.total_supply.insert(asset_id, amount + asset_supply.unwrap_or(0));mint_to(recipient, sub_id, amount); }/// Unconditionally burns assets sent with the `sub_id` sub-identifier.////// # Arguments////// * `sub_id`: [SubId] - The sub-identifier of the asset to burn./// * `amount`: [u64] - The quantity of coins to burn.////// # Number of Storage Accesses////// * Reads: `1`/// * Writes: `1`////// # Reverts////// * When the transaction did not include at least `amount` coins./// * When the asset included in the transaction does not have the SubId `sub_id`.////// # Examples////// ```sway/// use src3::SRC3;/// use std::constants::DEFAULT_SUB_ID;////// fn foo(contract_id: ContractId, asset_id: AssetId) {/// let contract_abi = abi(SRC3, contract_id);/// contract_abi {/// gas: 10000,/// coins: 100,/// asset_id: asset_id,/// }.burn(DEFAULT_SUB_ID, 100);/// }/// ``` #[payable] #[storage(read, write)]fnburn(sub_id:SubId, amount:u64) {let asset_id =AssetId::new(ContractId::this(), sub_id);require(msg_amount() == amount, "Incorrect amount provided");require(msg_asset_id() == asset_id, "Incorrect asset provided");// Decrement total supply of the asset and burn. storage.total_supply.insert(asset_id, storage.total_supply.get(asset_id).read() - amount);burn(sub_id, amount); }}// SRC3 extends SRC20, so this must be includedimpl SRC20 forContract { #[storage(read)]fntotal_assets() ->u64 { storage.total_assets.read() } #[storage(read)]fntotal_supply(asset:AssetId) ->Option<u64> { storage.total_supply.get(asset).try_read() } #[storage(read)]fnname(asset:AssetId) ->Option<String> {match storage.total_supply.get(asset).try_read() {Some(_) =>Some(String::from_ascii_str(from_str_array(NAME))),None=>None, } } #[storage(read)]fnsymbol(asset:AssetId) ->Option<String> {match storage.total_supply.get(asset).try_read() {Some(_) =>Some(String::from_ascii_str(from_str_array(SYMBOL))),None=>None, } } #[storage(read)]fndecimals(asset:AssetId) ->Option<u8> {match storage.total_supply.get(asset).try_read() {Some(_) =>Some(DECIMALS),None=>None, } }}
Run it all!
Simply run forc test in smart-contracts/contracts-v1.