diff --git a/apps/contracts/vault/src/deposit.rs b/apps/contracts/vault/src/deposit.rs index 8cc872f..4092e45 100644 --- a/apps/contracts/vault/src/deposit.rs +++ b/apps/contracts/vault/src/deposit.rs @@ -57,7 +57,7 @@ pub fn process_deposit( } // Mint shares - mint_shares(e, total_supply, shares_to_mint, from.clone())?; + mint_shares(e, &total_supply, shares_to_mint, from.clone())?; Ok((amounts, shares_to_mint)) } @@ -88,11 +88,11 @@ fn calculate_single_asset_shares( /// Mint vault shares. fn mint_shares( e: &Env, - total_supply: i128, + total_supply: &i128, shares_to_mint: i128, from: Address, ) -> Result<(), ContractError> { - if total_supply == 0 { + if *total_supply == 0 { if shares_to_mint < MINIMUM_LIQUIDITY { panic_with_error!(&e, ContractError::InsufficientAmount); } diff --git a/apps/contracts/vault/src/funds.rs b/apps/contracts/vault/src/funds.rs index 5ef9e21..ca05edc 100644 --- a/apps/contracts/vault/src/funds.rs +++ b/apps/contracts/vault/src/funds.rs @@ -47,7 +47,7 @@ pub fn fetch_invested_funds_for_asset(e: &Env, asset: &AssetStrategySet) -> (i12 }); } (invested_funds, strategy_allocations) -} +} /// Fetches the current idle funds for all assets managed by the contract. diff --git a/apps/contracts/vault/src/lib.rs b/apps/contracts/vault/src/lib.rs index 987ed0b..68a7d93 100755 --- a/apps/contracts/vault/src/lib.rs +++ b/apps/contracts/vault/src/lib.rs @@ -2,7 +2,7 @@ use deposit::{generate_and_execute_investments, process_deposit}; use soroban_sdk::{ contract, contractimpl, panic_with_error, - token::{TokenClient, TokenInterface}, + token::{TokenClient}, Address, Env, Map, String, Vec, }; use soroban_token_sdk::metadata::TokenMetadata; @@ -42,9 +42,9 @@ use storage::{ use strategies::{ get_strategy_asset, get_strategy_client, get_strategy_struct, invest_in_strategy, pause_strategy, unpause_strategy, - withdraw_from_strategy, + unwind_from_strategy, }; -use token::{internal_burn, write_metadata, VaultToken}; +use token::{internal_burn, write_metadata}; use utils::{ calculate_asset_amounts_per_vault_shares, check_initialized, @@ -224,19 +224,41 @@ impl VaultTrait for DeFindexVault { Ok((amounts, shares_to_mint)) } - /// Withdraws assets from the DeFindex Vault by burning Vault Share tokens. - /// - /// This function calculates the amount of assets to withdraw based on the number of Vault Share tokens being burned, - /// then transfers the appropriate assets back to the user, pulling from both idle funds and strategies - /// as needed. - /// - /// # Arguments: - /// * `e` - The environment. - /// * `shares_amount` - The amount of Vault Share tokens to burn for the withdrawal. - /// * `from` - The address of the user requesting the withdrawal. - /// - /// # Returns: - /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. + /// Handles the withdrawal process for a specified number of vault shares. + /// + /// This function performs the following steps: + /// 1. Validates the environment and the inputs: + /// - Ensures the contract is initialized. + /// - Checks that the withdrawal amount (`withdraw_shares`) is non-negative. + /// - Verifies the authorization of the `from` address. + /// 2. Collects applicable fees. + /// 3. Calculates the proportionate withdrawal amounts for each asset based on the number of shares. + /// 4. Burns the specified shares from the user's account. + /// 5. Processes the withdrawal for each asset: + /// - First attempts to cover the withdrawal amount using idle funds. + /// - If idle funds are insufficient, unwinds investments from the associated strategies + /// to cover the remaining amount, accounting for rounding errors in the last strategy. + /// 6. Transfers the withdrawn funds to the user's address (`from`). + /// 7. Emits an event to record the withdrawal details. + /// + /// ## Parameters: + /// - `e`: The contract environment (`Env`). + /// - `withdraw_shares`: The number of vault shares to withdraw. + /// - `from`: The address initiating the withdrawal. + /// + /// ## Returns: + /// - A `Result` containing a vector of withdrawn amounts for each asset (`Vec`), + /// or a `ContractError` if the withdrawal fails. + /// + /// ## Errors: + /// - `ContractError::AmountOverTotalSupply`: If the specified shares exceed the total supply. + /// - `ContractError::ArithmeticError`: If any arithmetic operation fails during calculations. + /// - `ContractError::WrongAmountsLength`: If there is a mismatch in asset allocation data. + /// + /// ## TODOs: + /// - Implement minimum amounts for withdrawals to ensure compliance with potential restrictions. + /// - Replace the returned vector with the original `asset_withdrawal_amounts` map for better structure. + /// - avoid the usage of a Map, choose between using map or vector fn withdraw( e: Env, withdraw_shares: i128, @@ -260,68 +282,71 @@ impl VaultTrait for DeFindexVault { )?; // Burn the shares after calculating the withdrawal amounts - // this will panic with error if the user does not have enough balance + // This will panic with error if the user does not have enough balance internal_burn(e.clone(), from.clone(), withdraw_shares); + let assets = get_assets(&e); // Use assets for iteration order // Loop through each asset to handle the withdrawal let mut withdrawn_amounts: Vec = Vec::new(&e); - for (asset_address, requested_withdrawal_amount) in asset_withdrawal_amounts.iter() { - - let asset_allocation = total_managed_funds - .get(asset_address.clone()) - .unwrap_or_else(|| panic_with_error!(&e, ContractError::WrongAmountsLength)); - // Check idle funds for this asset - let idle_funds = asset_allocation.idle_amount; - - // Withdraw from idle funds first - if idle_funds >= requested_withdrawal_amount { - // Idle funds cover the full amount - TokenClient::new(&e, &asset_address).transfer( - &e.current_contract_address(), - &from, - &requested_withdrawal_amount, - ); - withdrawn_amounts.push_back(requested_withdrawal_amount); - continue; - } else { - let mut total_withdrawn_for_asset = 0; - // Partial withdrawal from idle funds - total_withdrawn_for_asset += idle_funds; - let remaining_withdrawal_amount = requested_withdrawal_amount - idle_funds; - - // Withdraw the remaining amount from strategies - let total_invested_amount = asset_allocation.invested_amount; - - for strategy_allocation in asset_allocation.strategy_allocations.iter() { - // TODO: If strategy is paused, should we skip it? Otherwise, the calculation will go wrong. - // if strategy.paused { - // continue; - // } - - // Amount to unwind from strategy - let strategy_withdrawal_share = - (remaining_withdrawal_amount * strategy_allocation.amount) / total_invested_amount; - - if strategy_withdrawal_share > 0 { - withdraw_from_strategy(&e, &strategy_allocation.strategy_address, &strategy_withdrawal_share)?; - TokenClient::new(&e, &asset_address).transfer( - &e.current_contract_address(), - &from, - &strategy_withdrawal_share, - ); - total_withdrawn_for_asset += strategy_withdrawal_share; + for asset in assets.iter() { // Use assets instead of asset_withdrawal_amounts + let asset_address = &asset.address; + + if let Some(requested_withdrawal_amount) = asset_withdrawal_amounts.get(asset_address.clone()) { + let asset_allocation = total_managed_funds + .get(asset_address.clone()) + .unwrap_or_else(|| panic_with_error!(&e, ContractError::WrongAmountsLength)); + + let idle_funds = asset_allocation.idle_amount; + + if idle_funds >= requested_withdrawal_amount { + TokenClient::new(&e, asset_address).transfer( + &e.current_contract_address(), + &from, + &requested_withdrawal_amount, + ); + withdrawn_amounts.push_back(requested_withdrawal_amount); + } else { + let mut cumulative_amount_for_asset = idle_funds; + let remaining_amount_to_unwind = requested_withdrawal_amount + .checked_sub(idle_funds) + .unwrap(); + + let total_invested_amount = asset_allocation.invested_amount; + + for (i, strategy_allocation) in asset_allocation.strategy_allocations.iter().enumerate() { + let strategy_amount_to_unwind: i128 = if i == (asset_allocation.strategy_allocations.len() as usize) - 1 { + requested_withdrawal_amount + .checked_sub(cumulative_amount_for_asset) + .unwrap() + } else { + remaining_amount_to_unwind + .checked_mul(strategy_allocation.amount) + .and_then(|result| result.checked_div(total_invested_amount)) + .unwrap_or(0) + }; + + if strategy_amount_to_unwind > 0 { + unwind_from_strategy(&e, &strategy_allocation.strategy_address, &strategy_amount_to_unwind)?; + cumulative_amount_for_asset += strategy_amount_to_unwind; + } } + + TokenClient::new(&e, asset_address).transfer( + &e.current_contract_address(), + &from, + &cumulative_amount_for_asset, + ); + withdrawn_amounts.push_back(cumulative_amount_for_asset); } - TokenClient::new(&e, &asset_address).transfer( - &e.current_contract_address(), - &from, - &total_withdrawn_for_asset, - ); - withdrawn_amounts.push_back(total_withdrawn_for_asset); + } else { + withdrawn_amounts.push_back(0); // No withdrawal for this asset } } - + + + // TODO: Add minimuim amounts for withdrawn_amounts + // TODO: Return the asset_withdrawal_amounts Map instead of a vec events::emit_withdraw_event(&e, from, withdraw_shares, withdrawn_amounts.clone()); Ok(withdrawn_amounts) @@ -700,7 +725,7 @@ impl VaultManagementTrait for DeFindexVault { match instruction.action { ActionType::Withdraw => match (&instruction.strategy, &instruction.amount) { (Some(strategy_address), Some(amount)) => { - withdraw_from_strategy(&e, strategy_address, amount)?; + unwind_from_strategy(&e, strategy_address, amount)?; } _ => return Err(ContractError::MissingInstructionData), }, diff --git a/apps/contracts/vault/src/strategies.rs b/apps/contracts/vault/src/strategies.rs index 340896b..885c9fb 100644 --- a/apps/contracts/vault/src/strategies.rs +++ b/apps/contracts/vault/src/strategies.rs @@ -124,7 +124,7 @@ pub fn unpause_strategy(e: &Env, strategy_address: Address) -> Result<(), Contra Err(ContractError::StrategyNotFound) } -pub fn withdraw_from_strategy( +pub fn unwind_from_strategy( e: &Env, strategy_address: &Address, amount: &i128, diff --git a/apps/contracts/vault/src/test.rs b/apps/contracts/vault/src/test.rs index 8f9f222..7e717d2 100755 --- a/apps/contracts/vault/src/test.rs +++ b/apps/contracts/vault/src/test.rs @@ -70,7 +70,7 @@ pub(crate) fn get_token_admin_client<'a>( pub(crate) fn create_strategy_params_token0(test: &DeFindexVaultTest) -> Vec { sorobanvec![ - &test.env, + &test.env, Strategy { name: String::from_str(&test.env, "Strategy 1"), address: test.strategy_client_token0.address.clone(), diff --git a/apps/contracts/vault/src/test/vault/deposit_and_invest.rs b/apps/contracts/vault/src/test/vault/deposit_and_invest.rs index ba7d90e..dfbcd10 100644 --- a/apps/contracts/vault/src/test/vault/deposit_and_invest.rs +++ b/apps/contracts/vault/src/test/vault/deposit_and_invest.rs @@ -398,6 +398,15 @@ fn several_assets_success() { } +#[test] +fn one_asset_several_strategies() { + /* + What happens when no previous investment has been done? + + */ + todo!(); +} + #[test] diff --git a/apps/contracts/vault/src/test/vault/initialize.rs b/apps/contracts/vault/src/test/vault/initialize.rs index 376f2ce..c3db071 100644 --- a/apps/contracts/vault/src/test/vault/initialize.rs +++ b/apps/contracts/vault/src/test/vault/initialize.rs @@ -178,3 +178,9 @@ fn emergency_withdraw_not_yet_initialized() { .try_emergency_withdraw(&strategy_params_token1.first().unwrap().address, &users[0]); assert_eq!(result, Err(Ok(ContractError::NotInitialized))); } + +// test initialzie with one asset and several strategies for the same asset +#[test] +fn with_one_asset_and_several_strategies() { + todo!(); +} diff --git a/apps/contracts/vault/src/test/vault/withdraw.rs b/apps/contracts/vault/src/test/vault/withdraw.rs index ede44e6..ec8d71a 100644 --- a/apps/contracts/vault/src/test/vault/withdraw.rs +++ b/apps/contracts/vault/src/test/vault/withdraw.rs @@ -1,10 +1,15 @@ -use soroban_sdk::{vec as sorobanvec, String, Vec}; +use soroban_sdk::{vec as sorobanvec, String, Vec, Map}; // use super::hodl_strategy::StrategyError; use crate::test::{ create_strategy_params_token0, + create_strategy_params_token1, defindex_vault::{ - AssetInvestmentAllocation, AssetStrategySet, ContractError, StrategyAllocation, + AssetStrategySet, + AssetInvestmentAllocation, + StrategyAllocation, + Strategy, + ContractError, CurrentAssetInvestmentAllocation }, DeFindexVaultTest, }; @@ -50,7 +55,8 @@ fn negative_amount() { assert_eq!(result, Err(Ok(ContractError::NegativeNotAllowed))); } -// check that withdraw without balance after initialized returns error InsufficientBalance + +// check that withdraw without balance after initialized returns error AmountOverTotalSupply #[test] fn zero_total_supply() { let test = DeFindexVaultTest::setup(); @@ -82,8 +88,71 @@ fn zero_total_supply() { assert_eq!(result, Err(Ok(ContractError::AmountOverTotalSupply))); } +// check that withdraw with not enough balance returns error InsufficientBalance +#[test] +fn not_enough_balance() { + let test = DeFindexVaultTest::setup(); + test.env.mock_all_auths(); + let strategy_params_token0 = create_strategy_params_token0(&test); + let assets: Vec = sorobanvec![ + &test.env, + AssetStrategySet { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + } + ]; + + test.defindex_contract.initialize( + &assets, + &test.manager, + &test.emergency_manager, + &test.vault_fee_receiver, + &2000u32, + &test.defindex_protocol_receiver, + &test.defindex_factory, + &String::from_str(&test.env, "dfToken"), + &String::from_str(&test.env, "DFT"), + ); + + // We need to generate 2 users, to have more total supply than the amount to withdraw + let users = DeFindexVaultTest::generate_random_users(&test.env, 2); + + let amount_to_deposit = 567890i128; + test.token0_admin_client.mint(&users[0], &amount_to_deposit); + test.token0_admin_client.mint(&users[1], &amount_to_deposit); + + assert_eq!(test.token0.balance(&users[0]), amount_to_deposit); + assert_eq!(test.token0.balance(&users[1]), amount_to_deposit); + + // first the user deposits + test.defindex_contract.deposit( + &sorobanvec![&test.env, amount_to_deposit], + &sorobanvec![&test.env, amount_to_deposit], + &users[0], + &false + ); + + test.defindex_contract.deposit( + &sorobanvec![&test.env, amount_to_deposit], + &sorobanvec![&test.env, amount_to_deposit], + &users[1], + &false + ); + + // check that the every user has vault shares + assert_eq!(test.defindex_contract.balance(&users[0]), amount_to_deposit - 1000); + assert_eq!(test.defindex_contract.balance(&users[1]), amount_to_deposit); + // check that total supply of vault shares is indeed amount_to_deposit*2 + assert_eq!(test.defindex_contract.total_supply(), amount_to_deposit*2); + + // now user 0 tries to withdraw amount_to_deposit - 1000 +1 (more that it has) + + let result = test.defindex_contract.try_withdraw(&(amount_to_deposit - 1000 +1), &users[0]); + assert_eq!(result, Err(Ok(ContractError::InsufficientBalance))); +} + #[test] -fn withdraw_from_idle_success() { +fn from_idle_one_asset_one_strategy_success() { let test = DeFindexVaultTest::setup(); test.env.mock_all_auths(); let strategy_params_token0 = create_strategy_params_token0(&test); @@ -143,10 +212,30 @@ fn withdraw_from_idle_success() { let strategy_balance = test.token0.balance(&test.strategy_client_token0.address); assert_eq!(strategy_balance, 0); - // Df balance of user should be equal to deposited amount + // Df balance of user should be equal to deposited amount - 1000 let df_balance = test.defindex_contract.balance(&users[0]); assert_eq!(df_balance, amount_to_deposit - 1000); // 1000 gets locked in the vault forever + // check total manage funds + let mut total_managed_funds_expected = Map::new(&test.env); + let strategy_investments_expected = sorobanvec![&test.env, StrategyAllocation { + strategy_address: test.strategy_client_token0.address.clone(), + amount: 0, //funds has not been invested yet! + }]; + total_managed_funds_expected.set(test.token0.address.clone(), + CurrentAssetInvestmentAllocation { + asset: test.token0.address.clone(), + total_amount: amount_to_deposit, + idle_amount: amount_to_deposit, + invested_amount: 0i128, + strategy_allocations: strategy_investments_expected, + } + ); + + let total_managed_funds = test.defindex_contract.fetch_total_managed_funds(); + assert_eq!(total_managed_funds, total_managed_funds_expected); + + // user decides to withdraw a portion of deposited amount let amount_to_withdraw = 123456i128; test.defindex_contract @@ -173,6 +262,26 @@ fn withdraw_from_idle_success() { let df_balance = test.defindex_contract.balance(&users[0]); assert_eq!(df_balance, amount_to_deposit - amount_to_withdraw - 1000); + // check total manage funds + let mut total_managed_funds_expected = Map::new(&test.env); + let strategy_investments_expected = sorobanvec![&test.env, StrategyAllocation { + strategy_address: test.strategy_client_token0.address.clone(), + amount: 0, //funds has not been invested yet! + }]; + total_managed_funds_expected.set(test.token0.address.clone(), + CurrentAssetInvestmentAllocation { + asset: test.token0.address.clone(), + total_amount: amount_to_deposit - amount_to_withdraw, + idle_amount: amount_to_deposit - amount_to_withdraw, + invested_amount: 0i128, + strategy_allocations: strategy_investments_expected, + } + ); + + let total_managed_funds = test.defindex_contract.fetch_total_managed_funds(); + assert_eq!(total_managed_funds, total_managed_funds_expected); + + // user tries to withdraw more than deposited amount let amount_to_withdraw_more = amount_to_deposit + 1; let result = test @@ -196,10 +305,227 @@ fn withdraw_from_idle_success() { let user_balance = test.token0.balance(&users[0]); assert_eq!(user_balance, amount - 1000); + + // check total manage funds + let mut total_managed_funds_expected = Map::new(&test.env); + let strategy_investments_expected = sorobanvec![&test.env, StrategyAllocation { + strategy_address: test.strategy_client_token0.address.clone(), + amount: 0, //funds has not been invested yet! + }]; + total_managed_funds_expected.set(test.token0.address.clone(), + CurrentAssetInvestmentAllocation { + asset: test.token0.address.clone(), + total_amount: 1000, + idle_amount: 1000, + invested_amount: 0i128, + strategy_allocations: strategy_investments_expected, + } + ); + + let total_managed_funds = test.defindex_contract.fetch_total_managed_funds(); + assert_eq!(total_managed_funds, total_managed_funds_expected); + } #[test] -fn withdraw_from_strategy_success() { +fn from_idle_two_assets_success() { + let test = DeFindexVaultTest::setup(); + test.env.mock_all_auths(); + let strategy_params_token0 = create_strategy_params_token0(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); + let assets: Vec = sorobanvec![ + &test.env, + AssetStrategySet { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + }, + AssetStrategySet { + address: test.token1.address.clone(), + strategies: strategy_params_token1.clone() + } + ]; + + test.defindex_contract.initialize( + &assets, + &test.manager, + &test.emergency_manager, + &test.vault_fee_receiver, + &2000u32, + &test.defindex_protocol_receiver, + &test.defindex_factory, + &String::from_str(&test.env, "dfToken"), + &String::from_str(&test.env, "DFT"), + ); + let amount = 1234567890i128; + + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); + + test.token0_admin_client.mint(&users[0], &amount); + test.token1_admin_client.mint(&users[0], &amount); + assert_eq!(test.token0.balance(&users[0]), amount); + assert_eq!(test.token0.balance(&users[0]), amount); + + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, 0i128); + + // Deposit + let amount_to_deposit_0 = 567890i128; + let amount_to_deposit_1 = 987654i128; + test.defindex_contract.deposit( + &sorobanvec![&test.env, amount_to_deposit_0, amount_to_deposit_1], + &sorobanvec![&test.env, amount_to_deposit_0, amount_to_deposit_1], + &users[0], + &false + ); + + // Check Balances after deposit + + // Token balance of user + assert_eq!(test.token0.balance(&users[0]), amount - amount_to_deposit_0); + assert_eq!(test.token1.balance(&users[0]), amount - amount_to_deposit_1); + + // Token balance of vault should be amount_to_deposit + // Because balances are still in indle, balances are not in strategy, but in idle + + assert_eq!(test.token0.balance(&test.defindex_contract.address), amount_to_deposit_0); + assert_eq!(test.token1.balance(&test.defindex_contract.address), amount_to_deposit_1); + + // Token balance of hodl strategy should be 0 (all in idle) + assert_eq!(test.token0.balance(&test.strategy_client_token0.address), 0); + assert_eq!(test.token1.balance(&test.strategy_client_token1.address), 0); + + // Df balance of user should be equal to amount_to_deposit_0+amount_to_deposit_1 - 1000 + // 567890+987654-1000 = 1554544 + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, 1554544 ); + + // check total manage funds + let mut total_managed_funds_expected = Map::new(&test.env); + total_managed_funds_expected.set(test.token0.address.clone(), + CurrentAssetInvestmentAllocation { + asset: test.token0.address.clone(), + total_amount: 567890i128, + idle_amount: 567890i128, + invested_amount: 0i128, + strategy_allocations: sorobanvec![&test.env, + StrategyAllocation { + strategy_address: test.strategy_client_token0.address.clone(), + amount: 0, //funds has not been invested yet! + }], + } + ); + + total_managed_funds_expected.set(test.token1.address.clone(), + CurrentAssetInvestmentAllocation { + asset: test.token1.address.clone(), + total_amount: 987654i128, + idle_amount: 987654i128, + invested_amount: 0i128, + strategy_allocations: sorobanvec![&test.env, + StrategyAllocation { + strategy_address: test.strategy_client_token1.address.clone(), + amount: 0, //funds has not been invested yet! + }], + } + ); + let total_managed_funds = test.defindex_contract.fetch_total_managed_funds(); + assert_eq!(total_managed_funds, total_managed_funds_expected); + + // user decides to withdraw a portion of their vault shares + // from 1554544 it will withdraw 123456. + // total shares = 567890+987654 = 1555544 + // asset 0 = withdaw_shares*total_asset_0/total_shares = 123456*567890/1555544 = 45070.681279347 = 45070 + // asset 1 = withdaw_shares*total_asset_1/total_shares = 123456*987654/1555544 = 78385.318720653 = 78385 + + let amount_to_withdraw = 123456i128; + let result = test.defindex_contract + .withdraw(&amount_to_withdraw, &users[0]); + + // expected asset vec Vec + // pub struct AssetStrategySet { + // pub address: Address, + // pub strategies: Vec, + // } + // pub struct Strategy { + // pub address: Address, + // pub name: String, + // pub paused: bool, + // } + let expected_asset_vec = sorobanvec![&test.env, AssetStrategySet { + address: test.token0.address.clone(), + strategies: sorobanvec![&test.env, Strategy { + address: test.strategy_client_token0.address.clone(), + name: String::from_str(&test.env, "Strategy 1"), + paused: false, + }], + }, AssetStrategySet { + address: test.token1.address.clone(), + strategies: sorobanvec![&test.env, Strategy { + address: test.strategy_client_token1.address.clone(), + name: String::from_str(&test.env, "Strategy 1"), + paused: false, + }], + }]; + assert_eq!(test.defindex_contract.get_assets(), expected_asset_vec); + let expected_result = sorobanvec![&test.env, 45070, 78385]; + assert_eq!(result, expected_result); + + // Token balance of user + assert_eq!(test.token0.balance(&users[0]), amount - amount_to_deposit_0 + 45070); + assert_eq!(test.token1.balance(&users[0]), amount - amount_to_deposit_1 + 78385); + + // Token balance of vault (still idle) + + assert_eq!(test.token0.balance(&test.defindex_contract.address), amount_to_deposit_0 - 45070); + assert_eq!(test.token1.balance(&test.defindex_contract.address), amount_to_deposit_1 - 78385); + + // Token balance of hodl strategy should be 0 (all in idle) + assert_eq!(test.token0.balance(&test.strategy_client_token0.address), 0); + assert_eq!(test.token1.balance(&test.strategy_client_token1.address), 0); + + // Df balance of user should be equal to amount_to_deposit_0+amount_to_deposit_1 - 1000 - 123456 + // 567890+987654-1000 -123456 = 1434088 + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, 1431088 ); + + // check total manage funds + let mut total_managed_funds_expected = Map::new(&test.env); + total_managed_funds_expected.set(test.token0.address.clone(), + CurrentAssetInvestmentAllocation { + asset: test.token0.address.clone(), + total_amount: 567890i128 - 45070, + idle_amount: 567890i128 - 45070, + invested_amount: 0i128, + strategy_allocations: sorobanvec![&test.env, + StrategyAllocation { + strategy_address: test.strategy_client_token0.address.clone(), + amount: 0, //funds has not been invested yet! + }], + } + ); + + total_managed_funds_expected.set(test.token1.address.clone(), + CurrentAssetInvestmentAllocation { + asset: test.token1.address.clone(), + total_amount: 987654i128 - 78385, + idle_amount: 987654i128 - 78385, + invested_amount: 0i128, + strategy_allocations: sorobanvec![&test.env, + StrategyAllocation { + strategy_address: test.strategy_client_token1.address.clone(), + amount: 0, //funds has not been invested yet! + }], + } + ); + + let total_managed_funds = test.defindex_contract.fetch_total_managed_funds(); + assert_eq!(total_managed_funds, total_managed_funds_expected); + +} + + +#[test] +fn from_strategy_one_asset_one_strategy_success() { let test = DeFindexVaultTest::setup(); test.env.mock_all_auths(); let strategy_params_token0 = create_strategy_params_token0(&test); @@ -272,6 +598,336 @@ fn withdraw_from_strategy_success() { assert_eq!(user_balance, amount - 1000); } +#[test] +fn from_strategies_one_asset_two_strategies_success() { + todo!(); + // let test = DeFindexVaultTest::setup(); + // test.env.mock_all_auths(); + // let assets: Vec = sorobanvec![ + // &test.env, + // AssetStrategySet { + // address: test.token0.address.clone(), + // strategies: sorobanvec![ + // &test.env, + // Strategy { + // name: String::from_str(&test.env, "Strategy 1"), + // address: test.strategy_client_token0.address.clone(), + // paused: false, + // } + // ] + // } + // ]; + + // test.defindex_contract.initialize( + // &assets, + // &test.manager, + // &test.emergency_manager, + // &test.vault_fee_receiver, + // &2000u32, + // &test.defindex_protocol_receiver, + // &test.defindex_factory, + // &String::from_str(&test.env, "dfToken"), + // &String::from_str(&test.env, "DFT"), + // ); + // let amount = 1234567890000000i128; + + // let users = DeFindexVaultTest::generate_random_users(&test.env, 1); + + // test.token0_admin_client.mint(&users[0], &amount); + // assert_eq!(test.token0.balance(&users[0]), amount); + + // let df_balance = test.defindex_contract.balance(&users[0]); + // assert_eq!(df_balance, 0i128); + + // // Deposit + // let amount_to_deposit = 987654321i128; + + // test.defindex_contract.deposit( + // &sorobanvec![&test.env, amount_to_deposit], + // &sorobanvec![&test.env, amount_to_deposit], + // &users[0], + // &false + // ); + + // FIX invest in 2 stretegies for the same asset + +} + + +#[test] +fn from_strategies_two_asset_each_one_strategy_success() { + // We will have two assets, each asset with one strategy + let test = DeFindexVaultTest::setup(); + test.env.mock_all_auths(); + let strategy_params_token0 = create_strategy_params_token0(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); + let assets: Vec = sorobanvec![ + &test.env, + AssetStrategySet { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + }, + AssetStrategySet { + address: test.token1.address.clone(), + strategies: strategy_params_token1.clone() + } + ]; + // initialize + test.defindex_contract.initialize( + &assets, + &test.manager, + &test.emergency_manager, + &test.vault_fee_receiver, + &2000u32, + &test.defindex_protocol_receiver, + &test.defindex_factory, + &String::from_str(&test.env, "dfToken"), + &String::from_str(&test.env, "DFT"), + ); + // mint + let amount = 987654321i128; + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); + test.token0_admin_client.mint(&users[0], &amount); + test.token1_admin_client.mint(&users[0], &amount); + assert_eq!(test.token0.balance(&users[0]), amount); + assert_eq!(test.token1.balance(&users[0]), amount); + + // deposit + let amount_to_deposit_0 = 123456789i128; + let amount_to_deposit_1 = 234567890i128; + + let deposit_result = test.defindex_contract.deposit( + &sorobanvec![&test.env, amount_to_deposit_0, amount_to_deposit_1], + &sorobanvec![&test.env, amount_to_deposit_0, amount_to_deposit_1], + &users[0], + &false + ); + + // check deposit result. Ok((amounts, shares_to_mint)) + // shares to mint = 123456789 + 234567890 = 358024679 + assert_eq!(test.defindex_contract.total_supply(), 358024679); + + assert_eq!(deposit_result, (sorobanvec![&test.env, amount_to_deposit_0, amount_to_deposit_1], 358024679)); + + + // check balances + assert_eq!(test.token0.balance(&users[0]), amount - amount_to_deposit_0); + assert_eq!(test.token1.balance(&users[0]), amount - amount_to_deposit_1); + + // check vault balances + assert_eq!(test.token0.balance(&test.defindex_contract.address), amount_to_deposit_0); + assert_eq!(test.token1.balance(&test.defindex_contract.address), amount_to_deposit_1); + + // check strategy balances + assert_eq!(test.token0.balance(&test.strategy_client_token0.address), 0); + assert_eq!(test.token1.balance(&test.strategy_client_token1.address), 0); + + // invest in strategies. We will invest 100% of the idle funds + let investments = sorobanvec![ + &test.env, + Some(AssetInvestmentAllocation { + asset: test.token0.address.clone(), + strategy_allocations: sorobanvec![ + &test.env, + Some(StrategyAllocation { + strategy_address: test.strategy_client_token0.address.clone(), + amount: amount_to_deposit_0, + }), + ], + }), + Some(AssetInvestmentAllocation { + asset: test.token1.address.clone(), + strategy_allocations: sorobanvec![ + &test.env, + Some(StrategyAllocation { + strategy_address: test.strategy_client_token1.address.clone(), + amount: amount_to_deposit_1, + }), + ], + }), + ]; + + test.defindex_contract.invest(&investments); + + // check vault balances + assert_eq!(test.token0.balance(&test.defindex_contract.address), 0); + assert_eq!(test.token1.balance(&test.defindex_contract.address), 0); + + // check strategy balances + assert_eq!(test.token0.balance(&test.strategy_client_token0.address), amount_to_deposit_0); + assert_eq!(test.token1.balance(&test.strategy_client_token1.address), amount_to_deposit_1); + + //check user vault shares + let df_balance = test.defindex_contract.balance(&users[0]); + // vault shares should be amount_to_deposit_0 + amount_to_deposit_1 - 1000 + // 123456789 + 234567890 - 1000 = 358023679 + // but total vault shares are 358023679 + 1000 = 358024679 + assert_eq!(df_balance, 358023679); + + // User wants to withdraw 35353535 shares + // from asset 0: total_funds_0 * withdraw_shares / total_shares + // from asset 1: total_funds_1 * withdraw_shares / total_shares + // user will get asset 0: 123456789 * 35353535 / 358024679 = 12190874.447789436 = 12190874 + // user will get asset 1: 234567890 * 35353535 / 358024679 = 23162660.552210564 = 23162660 + + let amount_to_withdraw = 35353535i128; + let result = test.defindex_contract.withdraw(&amount_to_withdraw, &users[0]); + + assert_eq!(test.defindex_contract.total_supply(), 322671144); //358024679- 35353535 + + // check expected result + let expected_result = sorobanvec![&test.env, 12190874, 23162660]; + assert_eq!(result, expected_result); + + // check user balances + assert_eq!(test.token0.balance(&users[0]), amount - amount_to_deposit_0 + 12190874); + assert_eq!(test.token1.balance(&users[0]), amount - amount_to_deposit_1 + 23162660); + + // check vault balances + assert_eq!(test.token0.balance(&test.defindex_contract.address), 0); + assert_eq!(test.token1.balance(&test.defindex_contract.address), 0); + + // check strategy balances + assert_eq!(test.token0.balance(&test.strategy_client_token0.address), amount_to_deposit_0 - 12190874); + assert_eq!(test.token1.balance(&test.strategy_client_token1.address), amount_to_deposit_1 - 23162660); + + // check user vault shares // should be 358023679−35353535 = 322670144 + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, 322670144); + + // now we deposit again to have a lot in idle funds + // because the vault has 123456789 - 12190874 = 111,265,915 of token 0 + // and 234567890 - 23162660 = 211,405,230 of token 1 + // this proportion should be maintained + + // if user wants to deposit again 2,222,222 of token 0, it should invest from token 1: + // 2222222 * 211405230 / 111265915 = 4222221.630236537 = 4222221 + + let amount_to_deposit_0_new = 2222222i128; + let amount_to_deposit_1_new = 4222221i128; + + let (amounts, shares_minted) = test.defindex_contract.deposit( + &sorobanvec![&test.env, amount_to_deposit_0_new, amount_to_deposit_1_new+100], + &sorobanvec![&test.env, amount_to_deposit_0_new, amount_to_deposit_1_new-100], + &users[0], + &false + ); + + // expected amounts + let expected_amounts = sorobanvec![&test.env, 2222222, 4222221]; + assert_eq!(amounts, expected_amounts); + + // expected shares minted + // total supply was 123456789+234567890 = 358024679 + // then we withdaw 35353535 + // total supply is 358024679 - 35353535 = 322671144 + // new shares to mint = total_supplly * amount_desired_target / reserve_target + // 322671144 * 2222222 / 111265915 = 6444443.610264365 = 6444443 + assert_eq!(shares_minted, 6444443); + + assert_eq!(test.defindex_contract.total_supply(), 329115587); //358024679- 35353535 + 6444443 + + + // check user balances + assert_eq!(test.token0.balance(&users[0]), amount - amount_to_deposit_0 + 12190874 - 2222222); + assert_eq!(test.token1.balance(&users[0]), amount - amount_to_deposit_1 + 23162660 - 4222221); + + // check vault balance + assert_eq!(test.token0.balance(&test.defindex_contract.address), 2222222); + assert_eq!(test.token1.balance(&test.defindex_contract.address), 4222221); + + // check strategies balance + assert_eq!(test.token0.balance(&test.strategy_client_token0.address), amount_to_deposit_0 - 12190874); + assert_eq!(test.token1.balance(&test.strategy_client_token1.address), amount_to_deposit_1 - 23162660); + + // user withdraws only from idle funds 644444 (10% of what just deposited) + // this should only affect idle funds + + let amount_to_withdraw = 644444i128; + let result = test.defindex_contract.withdraw(&amount_to_withdraw, &users[0]); + + assert_eq!(test.defindex_contract.total_supply(), 328471143); //358024679- 35353535 + 6444443 - 644444 + + + // the new totqal supply was 322671144+6444443 = 329115587 + // the total managed funds for asset 0 was 2222222 (idle) + amount_to_deposit_0 - 12190874 + // = 2222222 + 123456789 - 12190874 = 113488137 + + // the total managed funds for asset 1 was 4222221 (idle) + amount_to_deposit_1 - 23162660 + // = 4222221 + 234567890 - 23162660 = 215627451 + + // the expected amount to withdraw for asset 0 was total_funds_0 * withdraw_shares / total_shares + // = 113488137 * 644444 / 329115587 = 222222.075920178 = 222222 + + // the expected amount to withdraw for asset 1 was total_funds_1 * withdraw_shares / total_shares + // = 215627451 * 644444 / 329115587 = 422221.92603793 = 422221 + + let expected_result = sorobanvec![&test.env, 222222, 422221]; + assert_eq!(result, expected_result); + + // check balances + assert_eq!(test.token0.balance(&users[0]), amount - amount_to_deposit_0 + 12190874 - 2222222 + 222222); + assert_eq!(test.token1.balance(&users[0]), amount - amount_to_deposit_1 + 23162660 - 4222221 + 422221); + + // check vault balance + assert_eq!(test.token0.balance(&test.defindex_contract.address), 2222222 - 222222); + assert_eq!(test.token1.balance(&test.defindex_contract.address), 4222221 - 422221); + + // check strategies balance + + assert_eq!(test.token0.balance(&test.strategy_client_token0.address), amount_to_deposit_0 - 12190874); + assert_eq!(test.token1.balance(&test.strategy_client_token1.address), amount_to_deposit_1 - 23162660); + + assert_eq!(test.defindex_contract.total_supply(), 328471143); //358024679- 35353535 + 6444443 - 644444 + + // check df tokens balance of user + assert_eq!(test.defindex_contract.balance(&users[0]), 328470143); + + // // Now we will wihdraw the total remineder amount of vault shares of the user + // // 328471143 - 1000 = 328470143 + let result = test.defindex_contract.withdraw(&328470143, &users[0]); + + + // from the total supply 328471143, the user will take 328470143 (almost all) + // for asset 0 this means + // 2222222 - 222222 (idle) + amount_to_deposit_0 - 12190874 + // 2000000 + 123456789 - 12190874 = 113265915 + + // for asset 1 this means + // 4222221 - 422221 (idle) + amount_to_deposit_1 - 23162660 + // 3800000 + 234567890 - 23162660 = 215205230 + + + // amounts to withdraw + // for asset 0: total_funds_0 * withdraw_shares / total_shares + // 113265915 * 328470143 / 328471143 = 113265570.17240277 = 113265570 + + // for asset 1: total_funds_1 * withdraw_shares / total_shares + // 215205230 * 328470143 / 328471143 = 215204574.827591141 = 215204574 + + let expected_result = sorobanvec![&test.env, 113265570, 215204574]; + assert_eq!(result, expected_result); + + assert_eq!(test.defindex_contract.balance(&users[0]), 0); + assert_eq!(test.defindex_contract.balance(&test.defindex_contract.address), 1000); + + // CHECK IDLE BALANCES + // check vault balance + assert_eq!(test.token0.balance(&test.defindex_contract.address), 0); + assert_eq!(test.token1.balance(&test.defindex_contract.address), 0); + + + // check strategies balance, they will hold the rest + // for asset 0: total_funds_0 * 1000 / total_shares + // 113265915 - 113265570 = 345 + + // for asset 1: total_funds_1 * withdraw_shares / total_shares + // 215205230- 215204574 = 656 + assert_eq!(test.token0.balance(&test.strategy_client_token0.address), 345); + assert_eq!(test.token1.balance(&test.strategy_client_token1.address), 656); +} + + // test withdraw without mock all auths #[test] fn from_strategy_success_no_mock_all_auths() { diff --git a/apps/contracts/vault/src/token/contract.rs b/apps/contracts/vault/src/token/contract.rs index 7641250..eac13b2 100644 --- a/apps/contracts/vault/src/token/contract.rs +++ b/apps/contracts/vault/src/token/contract.rs @@ -6,12 +6,11 @@ use crate::token::metadata::{read_decimal, read_name, read_symbol}; use crate::token::total_supply::{decrease_total_supply, increase_total_supply, read_total_supply}; #[cfg(test)] -use crate::token::storage_types::{AllowanceDataKey, AllowanceValue, DataKey}; +use crate::token::storage_types::{AllowanceDataKey}; use crate::token::storage_types::{INSTANCE_BUMP_AMOUNT, INSTANCE_LIFETIME_THRESHOLD}; use soroban_sdk::token::{self, Interface as _}; use soroban_sdk::{contract, contractimpl, Address, Env, String}; use soroban_token_sdk::TokenUtils; -use crate::ContractError; fn check_nonnegative_amount(amount: i128) { if amount < 0 {