Optimizing Rust Test Architecture for Complex Smart Contracts

Hello everyone!

We’ve built a complex system consisting of five smart contracts, and we’re currently in the testing phase. We’ve created 11 different scenarios to thoroughly test our system. However, we’ve hit a roadblock. Our current approach, where we write all the scenarios in a single main file, is causing a stack overflow error in Rust. We’re seeking guidance on a better test architecture to prevent this error.

One key challenge we face is that our scenarios must be executed step-by-step. This is because the logic in each new scenario depends on the state of the contracts, which may have changed in previous steps. Consequently, we need to run up to 20 calls in a row to ensure comprehensive testing.

Here’s a glimpse of our folder structure to provide more context:

  • abis (folder containing ABIs of all smart contracts)
  • contract1
  • contract2
  • contract3
  • contract4
  • contract5
  • token
  • tests
    • local-tests
    • utils (folder with function wrappers that optimize smart contract calls)

Within the utils folder, we’ve created function wrappers that allow us to efficiently manage smart contract instances and invoke their methods.

To give you a better idea of the issue, here’s a code snippet from our testing environment along with the encountered error. We’re eagerly awaiting suggestions for architectural solutions to overcome this challenge. Your insights and recommendations are greatly appreciated! Thank you.

utils file example

use fuels::{
    prelude::{abigen, Contract, LoadConfiguration, TxParameters, WalletUnlocked},
    programs::{call_response::FuelCallResponse, contract::SettableContract},
    types::{bech32::Bech32ContractId, Address, AssetId},
};
use rand::Rng;

abigen!(Contract(
    name = "AccountBalanceContract",
    abi = "account-balance/out/debug/account-balance-abi.json"
));

/*

class AccountBalanceInstance{

    pub instance: AccountBalanceContract<WalletUnlocked>,
    pub wallet: WalletUnlocked,

    constructor(this){
    }

}
*/

pub struct AccountBalanceInstance {
    pub instance: AccountBalanceContract<WalletUnlocked>,
    pub wallet: WalletUnlocked,
}

impl AccountBalanceInstance {
    // ## Pnl block

    /// Modifies the owed and realized PNL (Profit and Loss) for a trader.
    ///
    /// # Arguments
    ///
    /// * `trader`: [Address] - The address of the trader.
    /// * `amount`: [I64] - The amount to modify the owed and realized PNL.
    async fn modify_owed_realized_pnl(
        &self,
        trader: Address,
        amount: I64,
    ) -> Result<FuelCallResponse<()>, fuels::types::errors::Error> {
        let tx_params = TxParameters::default().with_gas_price(1);
        self.instance
            .methods()
            .modify_owed_realized_pnl(trader, amount)
            .tx_params(tx_params)
            // .call_params(call_params)?
            // .with_contracts(contract_ids)
            // .append_variable_outputs(1)
            .call()
            .await
    }

    /// Settles a specified amount of quote currency to the trader's owed and realized PNL (Profit and Loss).
    ///
    /// # Arguments
    ///
    /// * `trader`: [Address] - The address of the trader.
    /// * `base_token`: [AssetId] - The AssetId of the base token.
    /// * `amount`: [I64] - The amount to settle from quote currency to owed and realized PNL.
    async fn settle_quote_to_owed_realized_pnl(
        &self,
        trader: Address,
        base_token: AssetId,
        amount: I64,
    ) -> Result<FuelCallResponse<()>, fuels::types::errors::Error> {
        let tx_params = TxParameters::default().with_gas_price(1);
        self.instance
            .methods()
            .settle_quote_to_owed_realized_pnl(trader, base_token, amount)
            .tx_params(tx_params)
            // .call_params(call_params)?
            // .with_contracts(contract_ids)
            // .append_variable_outputs(1)
            .call()
            .await
    }

    /// Settles the owed and realized PNL (Profit and Loss) for a trader, returning the settled amount.
    ///
    /// # Arguments
    ///
    /// * `trader`: [Address] - The address of the trader.
    ///
    /// # Returns
    ///
    /// * [I64] - The settled owed and realized PNL amount.
    async fn settle_owed_realized_pnl(
        &self,
        trader: Address,
    ) -> Result<FuelCallResponse<I64>, fuels::types::errors::Error> {
        let tx_params = TxParameters::default().with_gas_price(1);
        self.instance
            .methods()
            .settle_owed_realized_pnl(trader)
            .tx_params(tx_params)
            // .call_params(call_params)?
            // .with_contracts(contract_ids)
            // .append_variable_outputs(1)
            .call()
            .await
    }

    /// Retrieves the PNL (Profit and Loss) information for a trader, including owed and unrealized PNL.
    ///
    /// # Arguments
    ///
    /// * `trader`: [Address] - The address of the trader.
    ///
    /// # Returns
    ///
    /// * [(I64, I64)] - A tuple containing the owed PNL and unrealized PNL for the trader.
    pub async fn get_pnl(
        &self,
        trader: Address,
        contracts: &[&dyn SettableContract],
    ) -> Result<FuelCallResponse<(I64, I64)>, fuels::types::errors::Error> {
        let tx_params = TxParameters::default().with_gas_price(1);
        self.instance
            .methods()
            .get_pnl(trader)
            .tx_params(tx_params)
            // .call_params(call_params)?
            .with_contracts(contracts)
            // .append_variable_outputs(1)
            .simulate()
            .await
    }

    // ## Balance block

    /// Settles the balance for a trader, deregisters the trader from the specified market, and records realized PNL (Profit and Loss).
    ///
    /// # Arguments
    ///
    /// * `trader`: [Address] - The address of the trader.
    /// * `base_token`: [AssetId] - The AssetId of the base token.
    /// * `taker_base`: [I64] - The base token amount to settle.
    /// * `taker_quote`: [I64] - The quote token amount to settle.
    /// * `realized_pnl`: [I64] - The realized Profit and Loss amount.
    async fn settle_balance_and_deregister(
        &self,
        trader: Address,
        base_token: AssetId,
        taker_base: I64,
        taker_quote: I64,
        realized_pnl: I64,
    ) -> Result<FuelCallResponse<()>, fuels::types::errors::Error> {
        let tx_params = TxParameters::default().with_gas_price(1);
        self.instance
            .methods()
            .settle_balance_and_deregister(
                trader,
                base_token,
                taker_base,
                taker_quote,
                realized_pnl,
            )
            .tx_params(tx_params)
            // .call_params(call_params)?
            // .with_contracts(contract_ids)
            // .append_variable_outputs(1)
            .call()
            .await
    }

    /// Retrieves the account balance for a trader in a specific market.
    ///
    /// # Arguments
    ///
    /// * `trader`: [Address] - The address of the trader.
    /// * `base_token`: [AssetId] - The AssetId of the base token.
    ///
    /// # Returns
    ///
    /// * [AccountBalance] - The account balance information.
    pub async fn get_account_balance(
        &self,
        trader: Address,
        base_token: AssetId,
    ) -> Result<FuelCallResponse<AccountBalance>, fuels::types::errors::Error> {
        let tx_params = TxParameters::default().with_gas_price(1);
        self.instance
            .methods()
            .get_account_balance(trader, base_token)
            .tx_params(tx_params)
            // .call_params(call_params)?
            // .with_contracts(contract_ids)
            // .append_variable_outputs(1)
            .simulate()
            .await
    }

    /// Modifies the taker's balance in a specific market.
    ///
    /// # Arguments
    ///
    /// * `trader`: [Address] - The address of the trader.
    /// * `base_token`: [AssetId] - The AssetId of the base token.
    /// * `base`: [I64] - The base token amount to modify.
    /// * `quote`: [I64] - The quote token amount to modify.
    ///
    /// # Returns
    ///
    /// * [(I64, I64)] - A tuple containing the modified base and quote token amounts.
    async fn modify_taker_balance(
        &self,
        trader: Address,
        base_token: AssetId,
        base: I64,
        quote: I64,
    ) -> Result<FuelCallResponse<(I64, I64)>, fuels::types::errors::Error> {
        let tx_params = TxParameters::default().with_gas_price(1);
        self.instance
            .methods()
            .modify_taker_balance(trader, base_token, base, quote)
            .tx_params(tx_params)
            // .call_params(call_params)?
            // .with_contracts(contract_ids)
            // .append_variable_outputs(1)
            .call()
            .await
    }

    // ## Base token block

    /// Registers a base token for a trader.
    ///
    /// # Arguments
    ///
    /// * `trader`: [Address] - The address of the trader.
    /// * `base_token`: [AssetId] - The AssetId of the base token to register.
    async fn register_base_token(
        &self,
        trader: Address,
        base_token: AssetId,
    ) -> Result<FuelCallResponse<()>, fuels::types::errors::Error> {
        let tx_params = TxParameters::default().with_gas_price(1);
        self.instance
            .methods()
            .register_base_token(trader, base_token)
            .tx_params(tx_params)
            // .call_params(call_params)?
            // .with_contracts(contract_ids)
            // .append_variable_outputs(1)
            .call()
            .await
    }

    /// Deregisters a base token for a trader.
    ///
    /// # Arguments
    ///
    /// * `trader`: [Address] - The address of the trader.
    /// * `base_token`: [AssetId] - The AssetId of the base token to deregister.
    async fn deregister_base_token(
        &self,
        trader: Address,
        base_token: AssetId,
    ) -> Result<FuelCallResponse<()>, fuels::types::errors::Error> {
        let tx_params = TxParameters::default().with_gas_price(1);
        self.instance
            .methods()
            .deregister_base_token(trader, base_token)
            .tx_params(tx_params)
            // .call_params(call_params)?
            // .with_contracts(contract_ids)
            // .append_variable_outputs(1)
            .call()
            .await
    }

    /// Retrieves a list of base tokens registered by a trader.
    ///
    /// # Arguments
    ///
    /// * `trader`: [Address] - The address of the trader.
    ///
    /// # Returns
    ///
    /// * [Vec<AssetId>] - A vector containing the AssetIds of the registered base tokens.
    async fn get_base_tokens(
        &self,
        trader: Address,
    ) -> Result<FuelCallResponse<Vec<AssetId>>, fuels::types::errors::Error> {
        let tx_params = TxParameters::default().with_gas_price(1);
        self.instance
            .methods()
            .get_base_tokens(trader)
            .tx_params(tx_params)
            // .call_params(call_params)?
            // .with_contracts(contract_ids)
            // .append_variable_outputs(1)
            .simulate()
            .await
    }

    // ## Position block

    /// Settles a trader's position in a closed market for a specific base token.
    ///
    /// # Arguments
    ///
    /// * `trader`: [Address] - The address of the trader.
    /// * `base_token`: [AssetId] - The AssetId of the base token for which the position is settled.
    ///
    /// # Returns
    ///
    /// * [(I64, I64, I64, u64)] - A tuple containing the position notional, open notional, realized P&L, and the closed price.
    async fn settle_position_in_closed_market(
        &self,
        trader: Address,
        base_token: AssetId,
    ) -> Result<FuelCallResponse<(I64, I64, I64, u64)>, fuels::types::errors::Error> {
        // -> (positionNotional, openNotional, realizedPnl, closedPrice
        let tx_params = TxParameters::default().with_gas_price(1);
        self.instance
            .methods()
            .settle_position_in_closed_market(trader, base_token)
            .tx_params(tx_params)
            // .call_params(call_params)?
            // .with_contracts(contract_ids)
            // .append_variable_outputs(1)
            .call()
            .await
    }

    /// Calculates the size of a liquidatable position for a trader in a specified base token based on the trader's account value.
    ///
    /// # Arguments
    ///
    /// * `trader`: [Address] - The address of the trader.
    /// * `base_token`: [AssetId] - The AssetId of the base token.
    /// * `account_value`: [I64] - The trader's account value.
    ///
    /// # Returns
    ///
    /// * [I64] - The size of the liquidatable position
    pub async fn get_liquidatable_position_size(
        &self,
        trader: Address,
        base_token: AssetId,
        account_value: I64,
    ) -> Result<FuelCallResponse<I64>, fuels::types::errors::Error> {
        let tx_params = TxParameters::default().with_gas_price(1);
        self.instance
            .methods()
            .get_liquidatable_position_size(trader, base_token, account_value)
            .tx_params(tx_params)
            // .call_params(call_params)?
            // .with_contracts(contract_ids)
            // .append_variable_outputs(1)
            .simulate()
            .await
    }

    /// Retrieves the taker position size for a trader in a specified base token.
    ///
    /// # Arguments
    ///
    /// * `trader`: [Address] - The address of the trader.
    /// * `base_token`: [AssetId] - The AssetId of the base token.
    ///
    /// # Returns
    ///
    /// * [I64] - The taker position size.
    async fn get_taker_position_size(
        &self,
        trader: Address,
        base_token: AssetId,
    ) -> Result<FuelCallResponse<I64>, fuels::types::errors::Error> {
        let tx_params = TxParameters::default().with_gas_price(1);
        self.instance
            .methods()
            .get_taker_position_size(trader, base_token)
            .tx_params(tx_params)
            // .call_params(call_params)?
            // .with_contracts(contract_ids)
            // .append_variable_outputs(1)
            .simulate()
            .await
    }

    /// Retrieves the total position value for a trader in a specified base token.
    ///
    /// # Arguments
    ///
    /// * `trader`: [Address] - The address of the trader.
    /// * `base_token`: [AssetId] - The AssetId of the base token.
    ///
    /// # Returns
    ///
    /// * [I64] - The total position value.
    pub async fn get_total_position_value(
        &self,
        trader: Address,
        base_token: AssetId,
        contracts: &[&dyn SettableContract],
    ) -> Result<FuelCallResponse<I64>, fuels::types::errors::Error> {
        let tx_params = TxParameters::default().with_gas_price(1);
        self.instance
            .methods()
            .get_total_position_value(trader, base_token)
            .tx_params(tx_params)
            // .call_params(call_params)?
            .with_contracts(contracts)
            // .append_variable_outputs(1)
            .simulate()
            .await
    }

    /// Retrieves the total absolute position value for a trader.
    ///
    /// # Arguments
    ///
    /// * `trader`: [Address] - The address of the trader.
    ///
    /// # Returns
    ///
    /// * [u64] - The total absolute position value.
    pub async fn get_total_abs_position_value(
        &self,
        trader: Address,
        contracts: &[&dyn SettableContract],
    ) -> Result<FuelCallResponse<u64>, fuels::types::errors::Error> {
        let tx_params = TxParameters::default().with_gas_price(1);
        self.instance
            .methods()
            .get_total_abs_position_value(trader)
            .tx_params(tx_params)
            // .call_params(call_params)?
            .with_contracts(contracts)
            // .append_variable_outputs(1)
            .simulate()
            .await
    }

    // ## TW Premium

    /// Updates the global twap premium growth for a trader's position in a specified base token.
    ///
    /// # Arguments
    ///
    /// * `trader`: [Address] - The address of the trader.
    /// * `base_token`: [AssetId] - The AssetId of the base token.
    /// * `last_tw_premium_growth_global`: [I64] - The last twap premium growth global value.
    async fn update_tw_premium_growth_global(
        &self,
        trader: Address,
        base_token: AssetId,
        last_tw_premium_growth_global: I64,
    ) -> Result<FuelCallResponse<()>, fuels::types::errors::Error> {
        let tx_params = TxParameters::default().with_gas_price(1);
        self.instance
            .methods()
            .update_tw_premium_growth_global(trader, base_token, last_tw_premium_growth_global)
            .tx_params(tx_params)
            // .call_params(call_params)?
            // .with_contracts(contract_ids)
            // .append_variable_outputs(1)
            .call()
            .await
    }

    // ## Open notional

    /// Retrieves the taker's open notional value for a specified base token.
    ///
    /// # Arguments
    ///
    /// * `trader`: [Address] - The address of the trader.
    /// * `base_token`: [AssetId] - The AssetId of the base token.
    ///
    /// # Returns
    ///
    /// * [I64] - The taker's open notional value.
    async fn get_taker_open_notional(
        &self,
        trader: Address,
        base_token: AssetId,
    ) -> Result<FuelCallResponse<I64>, fuels::types::errors::Error> {
        let tx_params = TxParameters::default().with_gas_price(1);
        self.instance
            .methods()
            .get_taker_open_notional(trader, base_token)
            .tx_params(tx_params)
            // .call_params(call_params)?
            // .with_contracts(contract_ids)
            // .append_variable_outputs(1)
            .simulate()
            .await
    }

    /// Retrieves the margin requirement for a specified trader.
    ///
    /// # Arguments
    ///
    /// * `trader`: [Address] - The address of the trader.
    ///
    /// # Returns
    ///
    /// * [u64] - The margin requirement.
    pub async fn get_margin_requirement(
        &self,
        trader: Address,
        contracts: &[&dyn SettableContract],
    ) -> Result<FuelCallResponse<u64>, fuels::types::errors::Error> {
        let tx_params = TxParameters::default().with_gas_price(1);
        self.instance
            .methods()
            .get_margin_requirement(trader)
            .tx_params(tx_params)
            // .call_params(call_params)?
            .with_contracts(contracts)
            // .append_variable_outputs(1)
            .simulate()
            .await
    }

    pub fn new(wallet: &WalletUnlocked, id: &Bech32ContractId) -> Self {
        Self {
            instance: AccountBalanceContract::new(id, wallet.clone()),
            wallet: wallet.clone(),
        }
    }

    pub async fn deploy(wallet: &WalletUnlocked, proxy: Address) -> Self {
        let mut rng = rand::thread_rng();
        let salt = rng.gen::<[u8; 32]>();

        let configurables =
            AccountBalanceContractConfigurables::default().with_PROXY_ADDRESS(proxy);
        let config = LoadConfiguration::default().with_configurables(configurables);

        let id = Contract::load_from("account-balance/out/debug/account-balance.bin", config)
            .unwrap()
            .with_salt(salt)
            .deploy(wallet, TxParameters::default().with_gas_price(1))
            .await
            .unwrap();

        Self {
            instance: AccountBalanceContract::new(id, wallet.clone()),
            wallet: wallet.clone(),
        }
    }
}

3 Likes

This is quite strange, my protocol has around 13 smart contracts, and we also have robust testing where all contracts are deployed and interacting with each other. Some tests simulate 25+ users interacting. I’ve never run into this error hmm.

But from what I’m reading online, it’s likely related to a big vector loop you’re running or some other logic specific to that test, maybe recursion unintentionally. I don’t think this is architectural in nature.

Hard to say without seeing the specific test that it’s breaking on

Hi there!

As said by @diyahir, the first step might be to isolate the functions that are causing the overflow.

Then, my recommendation is to read carefully your code and try to avoid duplications, accidental recursions, etc.

Example: using a borrowed reference to let tx_params = TxParameters::default().with_gas_price(1); might be better than creating a new variable each time.

Beyond that, it is hard to tell where the problem might be without going through the whole test suite.

Please consider:

  • Using stacker, a crate that helps to grow the stack on demand.
  • Run production builds (that include the LLVM optimizations).
  • Relying more on the heap by using structs like Vec.

However, the best approach is just to isolate the overflowing function, then, we can take a deeper look.

Let me know if this does work for you :palm_tree: