diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 00000000..7d17046f --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,57 @@ +name: Run dapp tests + +on: + pull_request: + workflow_dispatch: +permissions: + contents: write + pull-requests: write + issues: read + packages: none + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + - uses: actions/cache@v3 + with: + path: | + node_modules + key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} + + - name: Set up Node.js + uses: actions/setup-node@v4.0.2 + with: + node-version: '20.18.0' + + - name: Install dependencies + run: yarn install + + - name: Build app + run: yarn build --filter dapp + + run-tests: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4.1.1 + - uses: actions/cache@v3 + with: + path: | + node_modules + key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} + + - name: Set up Node.js + uses: actions/setup-node@v4.0.2 + with: + node-version: '20.18.0' + + - name: Run jest tests in dapp directory + run: | + cd apps/dapp + yarn test diff --git a/README.md b/README.md index 9307fd02..b7883fb5 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ To deploy the factory contract run: ```sh bash run.sh cd apps/contracts -yarn deploy-factory +yarn deploy-factory ``` ### Publish addresses Once you have deployed an instance of the factory contract. You can publish the addresses to be used by anyone on the network. diff --git a/apps/contracts/Cargo.lock b/apps/contracts/Cargo.lock index 889ab1d3..c4c169b0 100644 --- a/apps/contracts/Cargo.lock +++ b/apps/contracts/Cargo.lock @@ -439,6 +439,14 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "fixed_apr_strategy" +version = "1.0.0" +dependencies = [ + "defindex-strategy-core", + "soroban-sdk", +] + [[package]] name = "fnv" version = "1.0.7" diff --git a/apps/contracts/Cargo.toml b/apps/contracts/Cargo.toml index dd4a992d..9dee20c8 100644 --- a/apps/contracts/Cargo.toml +++ b/apps/contracts/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["strategies/*", "defindex", "factory"] +members = ["strategies/*", "vault", "factory"] exclude = [ "strategies/external_wasms", ] diff --git a/apps/contracts/Makefile b/apps/contracts/Makefile index 9c88fe8c..30807186 100644 --- a/apps/contracts/Makefile +++ b/apps/contracts/Makefile @@ -1,4 +1,4 @@ -SUBDIRS = strategies defindex factory +SUBDIRS = strategies vault factory default: build diff --git a/apps/contracts/defindex/src/constants.rs b/apps/contracts/defindex/src/constants.rs deleted file mode 100644 index 772d4f54..00000000 --- a/apps/contracts/defindex/src/constants.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) const MAX_BPS: i128 = 10_000; -pub(crate) const SECONDS_PER_YEAR: i128 = 31_536_000; \ No newline at end of file diff --git a/apps/contracts/defindex/src/fee.rs b/apps/contracts/defindex/src/fee.rs deleted file mode 100644 index 3d2e765d..00000000 --- a/apps/contracts/defindex/src/fee.rs +++ /dev/null @@ -1,82 +0,0 @@ -use soroban_sdk::{Address, Env, Map, Symbol, Vec}; - -use crate::{access::AccessControl, constants::{MAX_BPS, SECONDS_PER_YEAR}, events, funds::fetch_total_managed_funds, storage::{get_defindex_receiver, get_factory, get_last_fee_assesment, get_vault_share, set_last_fee_assesment}, token::internal_mint, utils::calculate_dftokens_from_asset_amounts, ContractError}; - -/// Fetches the current fee rate from the factory contract. -/// The fee rate is expressed in basis points (BPS). -fn fetch_fee_rate(e: &Env) -> u32 { - let factory_address = get_factory(e); - // Interacts with the factory contract to get the fee rate. - e.invoke_contract( - &factory_address, - &Symbol::new(&e, "fee_rate"), - Vec::new(&e) - ) -} - -fn calculate_fees(e: &Env, time_elapsed: u64, fee_rate: u32) -> Result { - - let total_managed_funds = fetch_total_managed_funds(e); // Get total managed funds per asset - - let seconds_per_year = SECONDS_PER_YEAR; // 365 days in seconds - - let mut total_fees_per_asset: Map = Map::new(&e); - - // Iterate over each asset in the vault - for (asset_address, amount) in total_managed_funds.iter() { - // Fetch current managed funds for each asset - let current_asset_value = amount; - - // Calculate the fee for this asset based on the fee rate and time elapsed - let asset_fee = (current_asset_value * fee_rate as i128 * time_elapsed as i128) / (seconds_per_year * MAX_BPS); - - total_fees_per_asset.set(asset_address.clone(), asset_fee); - - } - - let total_fees_in_dftokens = calculate_dftokens_from_asset_amounts(e, total_fees_per_asset)?; - - Ok(total_fees_in_dftokens) -} - -pub fn collect_fees(e: &Env) -> Result<(), ContractError> { - let current_timestamp = e.ledger().timestamp(); - let last_fee_assessment = get_last_fee_assesment(e); - - let time_elapsed = current_timestamp.checked_sub(last_fee_assessment).unwrap(); - - if time_elapsed == 0 { - return Ok(()); - } - - let fee_rate = fetch_fee_rate(e); - - let total_fees = calculate_fees(e, time_elapsed, fee_rate)?; - - // Mint the total fees as dfTokens - mint_fees(e, total_fees)?; - - // Update the last fee assessment timestamp - set_last_fee_assesment(e, ¤t_timestamp); - - Ok(()) -} - -fn mint_fees(e: &Env, total_fees: i128) -> Result<(), ContractError> { - let access_control = AccessControl::new(&e); - - let vault_fee_receiver = access_control.get_fee_receiver()?; - let defindex_receiver = get_defindex_receiver(e); - - let vault_share_bps = get_vault_share(e); - - let vault_shares = (total_fees * vault_share_bps as i128) / MAX_BPS; - - let defindex_shares = total_fees - vault_shares; - - internal_mint(e.clone(), vault_fee_receiver.clone(), vault_shares); - internal_mint(e.clone(), defindex_receiver.clone(), defindex_shares); - - events::emit_fees_minted_event(e, defindex_receiver, defindex_shares, vault_fee_receiver, vault_shares); - Ok(()) -} \ No newline at end of file diff --git a/apps/contracts/defindex/src/investment.rs b/apps/contracts/defindex/src/investment.rs deleted file mode 100644 index adeb3930..00000000 --- a/apps/contracts/defindex/src/investment.rs +++ /dev/null @@ -1,43 +0,0 @@ -use soroban_sdk::{Address, Env, Map, Vec}; - -use crate::{models::Investment, strategies::{get_strategy_asset, invest_in_strategy}, utils::check_nonnegative_amount, ContractError}; - -pub fn prepare_investment(e: &Env, investments: Vec, idle_funds: Map) -> Result, ContractError> { - let mut total_investment_per_asset: Map = Map::new(e); - - for investment in investments.iter() { - let strategy_address = &investment.strategy; - let amount_to_invest = investment.amount; - check_nonnegative_amount(amount_to_invest.clone())?; - - // Find the corresponding asset for the strategy - let asset = get_strategy_asset(&e, strategy_address)?; - - // Track investment per asset - let current_investment = total_investment_per_asset - .get(asset.address.clone()) - .unwrap_or(0); - let updated_investment = current_investment.checked_add(amount_to_invest).ok_or(ContractError::Overflow)?; - - total_investment_per_asset.set(asset.address.clone(), updated_investment); - - // Check if total investment exceeds idle funds - let idle_balance = idle_funds.get(asset.address.clone()).unwrap_or(0); - if updated_investment > idle_balance { - return Err(ContractError::NotEnoughIdleFunds); - } - } - - Ok(total_investment_per_asset) -} - -pub fn execute_investment(e: &Env, investments: Vec) -> Result<(), ContractError> { - for investment in investments.iter() { - let strategy_address = &investment.strategy; - let amount_to_invest = &investment.amount; - - invest_in_strategy(e, strategy_address, amount_to_invest)? - } - - Ok(()) -} \ No newline at end of file diff --git a/apps/contracts/defindex/src/test/deposit.rs b/apps/contracts/defindex/src/test/deposit.rs deleted file mode 100644 index b765e1d3..00000000 --- a/apps/contracts/defindex/src/test/deposit.rs +++ /dev/null @@ -1,123 +0,0 @@ -use soroban_sdk::{vec as sorobanvec, String, Vec}; - -use crate::test::{create_strategy_params, DeFindexVaultTest}; -use crate::test::defindex_vault::{AssetAllocation, ContractError}; - -#[test] -fn deposit_amounts_desired_wrong_length() { - - let test = DeFindexVaultTest::setup(); - test.env.mock_all_auths(); - let strategy_params = create_strategy_params(&test); - - // initialize with 2 assets - let assets: Vec = sorobanvec![ - &test.env, - AssetAllocation { - address: test.token0.address.clone(), - strategies: strategy_params.clone() - }, - AssetAllocation { - address: test.token1.address.clone(), - strategies: strategy_params.clone() - } - ]; - - test.defindex_contract.initialize( - &assets, - &test.manager, - &test.emergency_manager, - &test.fee_receiver, - &2000u32, - &test.defindex_receiver, - &test.defindex_factory, - &String::from_str(&test.env, "dfToken"), - &String::from_str(&test.env, "DFT"), - ); - let amount = 1000i128; - - let users = DeFindexVaultTest::generate_random_users(&test.env, 1); - - - let response = test.defindex_contract.try_deposit( - &sorobanvec![&test.env, amount], // wrong amount desired - &sorobanvec![&test.env, amount, amount], - &users[0]); - - assert_eq!(response, Err(Ok(ContractError::WrongAmuntsLength))); - -} - - -#[test] -fn deposit_amounts_min_wrong_length() { - todo!(); -} - - -#[test] -fn deposit_amounts_desired_negative() { - todo!(); -} - -#[test] -fn deposit_one_asset() { - todo!(); -} - - -#[test] -fn deposit_several_assets() { - todo!(); -} - -// #[test] -// fn test_deposit_success() { -// todo!(); -// } -// #[test] -// fn test_withdraw_success() { -// let test = DeFindexVaultTest::setup(); -// test.env.mock_all_auths(); -// let strategy_params = create_strategy_params(&test); -// let assets: Vec = sorobanvec![ -// &test.env, -// AssetAllocation { -// address: test.token0.address.clone(), -// -// strategies: strategy_params.clone() -// } -// ]; - -// test.defindex_contract.initialize( -// &assets, -// &test.manager, -// &test.emergency_manager, -// &test.fee_receiver, -// &test.defindex_receiver, -// ); -// let amount = 1000i128; - -// let users = DeFindexVaultTest::generate_random_users(&test.env, 1); - -// test.token0_admin_client.mint(&users[0], &amount); -// let user_balance = test.token0.balance(&users[0]); -// assert_eq!(user_balance, amount); -// // here youll need to create a client for a token with the same address - -// let df_balance = test.defindex_contract.balance(&users[0]); -// assert_eq!(df_balance, 0i128); - -// test.defindex_contract.deposit(&sorobanvec![&test.env, amount], &sorobanvec![&test.env, amount], &users[0]); - -// let df_balance = test.defindex_contract.balance(&users[0]); -// assert_eq!(df_balance, amount); - -// test.defindex_contract.withdraw(&df_balance, &users[0]); - -// let df_balance = test.defindex_contract.balance(&users[0]); -// assert_eq!(df_balance, 0i128); - -// let user_balance = test.token0.balance(&users[0]); -// assert_eq!(user_balance, amount); -// } \ No newline at end of file diff --git a/apps/contracts/defindex/src/utils.rs b/apps/contracts/defindex/src/utils.rs deleted file mode 100644 index 82bb5900..00000000 --- a/apps/contracts/defindex/src/utils.rs +++ /dev/null @@ -1,235 +0,0 @@ -use soroban_sdk::{panic_with_error, Address, Env, Map, Vec}; - -use crate::{ - access::{AccessControl, AccessControlTrait, RolesDataKey}, funds::{fetch_invested_funds_for_asset, fetch_invested_funds_for_strategy, fetch_total_managed_funds}, models::AssetAllocation, token::VaultToken, ContractError - -}; - -pub const DAY_IN_LEDGERS: u32 = 17280; - -pub fn bump_instance(e: &Env) { - let max_ttl = e.storage().max_ttl(); - e.storage() - .instance() - .extend_ttl(max_ttl - DAY_IN_LEDGERS, max_ttl); -} - -pub fn check_initialized(e: &Env) -> Result<(), ContractError> { - //TODO: Should also check if adapters/strategies have been set - let access_control = AccessControl::new(&e); - if access_control.has_role(&RolesDataKey::Manager) { - Ok(()) - } else { - panic_with_error!(&e, ContractError::NotInitialized); - } -} - -pub fn check_nonnegative_amount(amount: i128) -> Result<(), ContractError> { - if amount < 0 { - Err(ContractError::NegativeNotAllowed) - } else { - Ok(()) - } -} - -/// From an amount, calculates how much to withdraw from each strategy; -/// returns a map of strategy address to token amount -pub fn calculate_withdrawal_amounts( - e: &Env, - amount: i128, - asset: AssetAllocation, -) -> Map { - let mut withdrawal_amounts = Map::::new(e); - - let total_invested_in_strategies: i128 = fetch_invested_funds_for_asset(&e, &asset); - - for strategy in asset.strategies.iter() { - // TODO: if strategy is paused but still holds assets on it shouldnt we withdraw them? - if strategy.paused { - continue; - } - - let strategy_invested_funds = fetch_invested_funds_for_strategy(e, &strategy.address); - - let strategy_share_of_withdrawal = (amount * strategy_invested_funds) / total_invested_in_strategies; - - withdrawal_amounts.set(strategy.address.clone(), strategy_share_of_withdrawal); - } - - withdrawal_amounts -} - -pub fn calculate_asset_amounts_for_dftokens( - env: &Env, - df_token_amount: i128, -) -> Map { - let mut asset_amounts: Map = Map::new(&env); - let total_supply = VaultToken::total_supply(env.clone()); - let total_managed_funds = fetch_total_managed_funds(&env); - - // Iterate over each asset and calculate the corresponding amount based on df_token_amount - for (asset_address, amount) in total_managed_funds.iter() { - let asset_amount = (amount * df_token_amount) / total_supply; - asset_amounts.set(asset_address.clone(), asset_amount); - } - - asset_amounts -} - -pub fn calculate_dftokens_from_asset_amounts( - env: &Env, - asset_amounts: Map, // The input asset amounts -) -> Result { - let total_supply = VaultToken::total_supply(env.clone()); // Total dfToken supply - let total_managed_funds = fetch_total_managed_funds(&env); // Fetch all managed assets - - // Initialize the minimum dfTokens corresponding to each asset - let mut min_df_tokens: Option = None; - - // Iterate over each asset in the input map - for (asset_address, input_amount) in asset_amounts.iter() { - // Get the total managed amount for this asset - let managed_amount = total_managed_funds.get(asset_address.clone()).unwrap_or(0); - - // Ensure the managed amount is not zero to prevent division by zero - if managed_amount == 0 { - return Err(ContractError::InsufficientManagedFunds); - } - - // Calculate the dfTokens corresponding to this asset's amount - let df_tokens_for_asset = (input_amount * total_supply) / managed_amount; - - // If this is the first asset or if the calculated df_tokens_for_asset is smaller, update the minimum df_tokens - if let Some(current_min_df_tokens) = min_df_tokens { - min_df_tokens = Some(current_min_df_tokens.min(df_tokens_for_asset)); - } else { - min_df_tokens = Some(df_tokens_for_asset); - } - } - - // Return the minimum dfTokens across all assets - min_df_tokens.ok_or(ContractError::NoAssetsProvided) -} - -pub fn calculate_optimal_amounts_and_shares_with_enforced_asset( - e: &Env, - total_managed_funds: &Map, - assets: &Vec, - amounts_desired: &Vec, - i: &u32, -) -> (Vec, i128) { - // we have to calculate the optimal amount to deposit for the rest of the assets - // we need the total amount managed by this vault in order for the deposit to be proportional - // reserve (total manage funds) of the asset we are enforcing - let reserve_target = total_managed_funds - .get(assets.get(*i).unwrap().address) - .unwrap(); // i128 - if reserve_target == 0 { - // return sum of amounts desired as shares - return (amounts_desired.clone(), amounts_desired.iter().sum()); // first shares will be equal to the first amounts_desired - // TODO, this amounts desired might be too little? - // this might be the first deposit... in this case, the ratio will be enforced by the first depositor - // TODO: might happen that the reserve_target is zero because everything is in one asset!? - // in this case we ned to check the ratio - // TODO VERY DANGEROUS. - } - let amount_desired_target = amounts_desired.get(*i).unwrap(); // i128 - - let mut optimal_amounts = Vec::new(e); - - for (j, (_asset_address, reserve)) in total_managed_funds.iter().enumerate() { - if j == (*i as usize) { - optimal_amounts.push_back(amount_desired_target); - } else { - // amount = amount_desired_target * reserve[j] / reserve_target - // factor is (amount_desired_target / reserve_target;) - let amount = reserve * amount_desired_target / reserve_target; - optimal_amounts.push_back(amount); - } - } - //TODO: calculate the shares to mint = total_supply * amount_desired_target / reserve_target - let shares_to_mint = VaultToken::total_supply(e.clone()) * amount_desired_target / reserve_target; - (optimal_amounts, shares_to_mint) -} -/// Calculates the optimal amounts to deposit for a set of assets, along with the shares to mint. -/// This function iterates over a list of assets and checks if the desired deposit amounts -/// match the optimal deposit strategy, based on current managed funds and asset ratios. -/// -/// If the desired amount for a given asset cannot be achieved due to constraints (e.g., it's below the minimum amount), -/// the function attempts to find an optimal solution by adjusting the amounts of subsequent assets. -/// -/// # Arguments -/// * `e` - The current environment. -/// * `assets` - A vector of assets for which deposits are being calculated. -/// * `amounts_desired` - A vector of desired amounts for each asset. -/// * `amounts_min` - A vector of minimum amounts for each asset, below which deposits are not allowed. -/// -/// # Returns -/// A tuple containing: -/// * A vector of optimal amounts to deposit for each asset. -/// * The number of shares to mint based on the optimal deposits. -/// -/// # Errors -/// If no valid deposit configuration can be found that satisfies the minimum amounts for all assets, the function -/// will return an error. -/// -/// # Panics -/// The function may panic if it encounters invalid states (e.g., insufficient amounts) but TODO: these should -/// be replaced with proper error handling. -pub fn calculate_deposit_amounts_and_shares_to_mint( - e: &Env, - assets: &Vec, - amounts_desired: &Vec, - amounts_min: &Vec, -) -> (Vec, i128) { - // Retrieve the total managed funds for each asset as a Map. - let total_managed_funds = fetch_total_managed_funds(e); - - // Iterate over each asset in the assets vector. - for i in 0..assets.len() { - // Calculate the optimal amounts and the number of shares to mint for the given asset at index `i`. - let (optimal_amounts, shares_to_mint) = - calculate_optimal_amounts_and_shares_with_enforced_asset( - &e, - &total_managed_funds, - &assets, - &amounts_desired, - &i, - ); - - // Flag to skip the current asset if necessary. - let mut should_skip = false; - - // Check if the calculated optimal amounts meet the desired or minimum requirements. - for j in i + 1..assets.len() { - // If the optimal amount for asset[j] is less than the desired amount, - // but at least greater than the minimum amount, it is acceptable. - if optimal_amounts.get(j).unwrap() <= amounts_desired.get(j).unwrap() { - // If the optimal amount is below the minimum, we cannot proceed with this asset. - if optimal_amounts.get(j).unwrap() < amounts_min.get(j).unwrap() { - panic!("insufficient amount"); // TODO: Replace panic with an error return. - } - } else { - // If the optimal amount exceeds the desired amount, we skip the current asset {i}. - should_skip = true; - - // If we've reached the last asset and still don't find a solution, throw an error. - if j == assets.len() - 1 { - panic!("didn't find optimal amounts"); // TODO: Replace panic with an error return. - } - break; - } - } - - // If we should skip this asset, continue to the next one. - if should_skip { - continue; - } else { - // Return the calculated optimal amounts and shares to mint. - return (optimal_amounts, shares_to_mint); - } - } - - // If no solution was found after iterating through all assets, throw an error. - panic!("didn't find optimal amounts"); -} \ No newline at end of file diff --git a/apps/contracts/factory/src/events.rs b/apps/contracts/factory/src/events.rs index 479cc43a..c0c23b03 100644 --- a/apps/contracts/factory/src/events.rs +++ b/apps/contracts/factory/src/events.rs @@ -8,14 +8,14 @@ use crate::defindex::AssetAllocation; pub struct InitializedEvent { pub admin: Address, pub defindex_receiver: Address, - pub fee_rate: u32, + pub defindex_fee: u32, } -pub(crate) fn emit_initialized(e: &Env, admin: Address, defindex_receiver: Address, fee_rate: u32) { +pub(crate) fn emit_initialized(e: &Env, admin: Address, defindex_receiver: Address, defindex_fee: u32) { let event: InitializedEvent = InitializedEvent { admin, defindex_receiver, - fee_rate, + defindex_fee, }; e.events() .publish(("DeFindexFactory", symbol_short!("init")), event); @@ -28,7 +28,7 @@ pub struct CreateDeFindexEvent { pub emergency_manager: Address, pub fee_receiver: Address, pub manager: Address, - pub vault_share: u32, + pub vault_fee: u32, pub assets: Vec } @@ -38,14 +38,14 @@ pub(crate) fn emit_create_defindex_vault( emergency_manager: Address, fee_receiver: Address, manager: Address, - vault_share: u32, + vault_fee: u32, assets: Vec, ) { let event = CreateDeFindexEvent { emergency_manager, fee_receiver, manager, - vault_share, + vault_fee, assets, }; @@ -81,16 +81,16 @@ pub(crate) fn emit_new_defindex_receiver(e: &Env, new_defindex_receiver: Address .publish(("DeFindexFactory", symbol_short!("nreceiver")), event); } -// NEW FEE RATE EVENT +// NEW DEFINDEX FEE EVENT #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct NewFeeRateEvent { - pub new_fee_rate: u32, + pub new_defindex_fee: u32, } -pub(crate) fn emit_new_fee_rate(e: &Env, new_fee_rate: u32) { - let event = NewFeeRateEvent { new_fee_rate }; +pub(crate) fn emit_new_defindex_fee(e: &Env, new_defindex_fee: u32) { + let event = NewFeeRateEvent { new_defindex_fee }; e.events() - .publish(("DeFindexFactory", symbol_short!("nfee_rate")), event); + .publish(("DeFindexFactory", symbol_short!("n_fee")), event); } \ No newline at end of file diff --git a/apps/contracts/factory/src/lib.rs b/apps/contracts/factory/src/lib.rs index f02aa883..f29d9b70 100644 --- a/apps/contracts/factory/src/lib.rs +++ b/apps/contracts/factory/src/lib.rs @@ -10,7 +10,7 @@ use soroban_sdk::{ }; use error::FactoryError; use defindex::{create_contract, AssetAllocation}; -use storage::{ add_new_defindex, extend_instance_ttl, get_admin, get_defi_wasm_hash, get_defindex_receiver, get_deployed_defindexes, get_fee_rate, has_admin, put_admin, put_defi_wasm_hash, put_defindex_receiver, put_fee_rate }; +use storage::{ add_new_defindex, extend_instance_ttl, get_admin, get_defi_wasm_hash, get_defindex_receiver, get_deployed_defindexes, get_fee_rate, has_admin, put_admin, put_defi_wasm_hash, put_defindex_receiver, put_defindex_fee }; fn check_initialized(e: &Env) -> Result<(), FactoryError> { if !has_admin(e) { @@ -26,7 +26,7 @@ pub trait FactoryTrait { /// * `e` - The environment in which the contract is running. /// * `admin` - The address of the contract administrator, who can manage settings. /// * `defindex_receiver` - The default address designated to receive a portion of fees. - /// * `fee_rate` - The initial annual fee rate (in basis points). + /// * `defindex_fee` - The initial annual fee rate (in basis points). /// * `defindex_wasm_hash` - The hash of the DeFindex Vault's WASM file for deploying new vaults. /// /// # Returns @@ -35,7 +35,7 @@ pub trait FactoryTrait { e: Env, admin: Address, defindex_receiver: Address, - fee_rate: u32, + defindex_fee: u32, defindex_wasm_hash: BytesN<32> ) -> Result<(), FactoryError>; @@ -45,7 +45,7 @@ pub trait FactoryTrait { /// * `e` - The environment in which the contract is running. /// * `emergency_manager` - The address assigned emergency control over the vault. /// * `fee_receiver` - The address designated to receive fees from the vault. - /// * `vault_share` - The percentage share of fees allocated to the vault's fee receiver. + /// * `vault_fee` - The percentage share of fees allocated to the vault's fee receiver. /// * `vault_name` - The name of the vault. /// * `vault_symbol` - The symbol of the vault. /// * `manager` - The address assigned as the vault manager. @@ -58,7 +58,7 @@ pub trait FactoryTrait { e: Env, emergency_manager: Address, fee_receiver: Address, - vault_share: u32, + vault_fee: u32, vault_name: String, vault_symbol: String, manager: Address, @@ -72,7 +72,7 @@ pub trait FactoryTrait { /// * `e` - The environment in which the contract is running. /// * `emergency_manager` - The address assigned emergency control over the vault. /// * `fee_receiver` - The address designated to receive fees from the vault. - /// * `vault_share` - The percentage share of fees allocated to the vault's fee receiver. + /// * `vault_fee` - The percentage share of fees allocated to the vault's fee receiver. /// * `vault_name` - The name of the vault. /// * `vault_symbol` - The symbol of the vault. /// * `manager` - The address assigned as the vault manager. @@ -87,7 +87,7 @@ pub trait FactoryTrait { caller: Address, emergency_manager: Address, fee_receiver: Address, - vault_share: u32, + vault_fee: u32, vault_name: String, vault_symbol: String, manager: Address, @@ -126,7 +126,7 @@ pub trait FactoryTrait { /// /// # Returns /// * `Result<(), FactoryError>` - Returns Ok(()) if successful, or an error if not authorized. - fn set_fee_rate(e: Env, new_fee_rate: u32) -> Result<(), FactoryError>; + fn set_defindex_fee(e: Env, new_fee_rate: u32) -> Result<(), FactoryError>; // --- Read Methods --- @@ -164,7 +164,7 @@ pub trait FactoryTrait { /// /// # Returns /// * `Result` - Returns the fee rate in basis points or an error if not found. - fn fee_rate(e: Env) -> Result; + fn defindex_fee(e: Env) -> Result; } #[contract] @@ -179,7 +179,7 @@ impl FactoryTrait for DeFindexFactory { /// * `e` - The environment in which the contract is running. /// * `admin` - The address of the contract administrator, who can manage settings. /// * `defindex_receiver` - The default address designated to receive a portion of fees. - /// * `fee_rate` - The initial annual fee rate (in basis points). + /// * `defindex_fee` - The initial annual fee rate (in basis points). /// * `defindex_wasm_hash` - The hash of the DeFindex Vault's WASM file for deploying new vaults. /// /// # Returns @@ -188,7 +188,7 @@ impl FactoryTrait for DeFindexFactory { e: Env, admin: Address, defindex_receiver: Address, - fee_rate: u32, + defindex_fee: u32, defi_wasm_hash: BytesN<32> ) -> Result<(), FactoryError> { if has_admin(&e) { @@ -198,9 +198,9 @@ impl FactoryTrait for DeFindexFactory { put_admin(&e, &admin); put_defindex_receiver(&e, &defindex_receiver); put_defi_wasm_hash(&e, defi_wasm_hash); - put_fee_rate(&e, &fee_rate); + put_defindex_fee(&e, &defindex_fee); - events::emit_initialized(&e, admin, defindex_receiver, fee_rate); + events::emit_initialized(&e, admin, defindex_receiver, defindex_fee); extend_instance_ttl(&e); Ok(()) } @@ -211,7 +211,7 @@ impl FactoryTrait for DeFindexFactory { /// * `e` - The environment in which the contract is running. /// * `emergency_manager` - The address assigned emergency control over the vault. /// * `fee_receiver` - The address designated to receive fees from the vault. - /// * `vault_share` - The percentage share of fees allocated to the vault's fee receiver. + /// * `vault_fee` - The percentage share of fees allocated to the vault's fee receiver. /// * `manager` - The address assigned as the vault manager. /// * `assets` - A vector of `AssetAllocation` structs that define the assets managed by the vault. /// * `salt` - A salt used for ensuring unique addresses for each deployed vault. @@ -222,7 +222,7 @@ impl FactoryTrait for DeFindexFactory { e: Env, emergency_manager: Address, fee_receiver: Address, - vault_share: u32, + vault_fee: u32, vault_name: String, vault_symbol: String, manager: Address, @@ -243,7 +243,7 @@ impl FactoryTrait for DeFindexFactory { &manager, &emergency_manager, &fee_receiver, - &vault_share, + &vault_fee, &defindex_receiver, ¤t_contract, &vault_name, @@ -251,7 +251,7 @@ impl FactoryTrait for DeFindexFactory { ); add_new_defindex(&e, defindex_address.clone()); - events::emit_create_defindex_vault(&e, emergency_manager, fee_receiver, manager, vault_share, assets); + events::emit_create_defindex_vault(&e, emergency_manager, fee_receiver, manager, vault_fee, assets); Ok(defindex_address) } @@ -261,7 +261,7 @@ impl FactoryTrait for DeFindexFactory { /// * `e` - The environment in which the contract is running. /// * `emergency_manager` - The address assigned emergency control over the vault. /// * `fee_receiver` - The address designated to receive fees from the vault. - /// * `vault_share` - The percentage share of fees allocated to the vault's fee receiver. + /// * `vault_fee` - The percentage share of fees allocated to the vault's fee receiver. /// * `vault_name` - The name of the vault. /// * `vault_symbol` - The symbol of the vault. /// * `manager` - The address assigned as the vault manager. @@ -276,7 +276,7 @@ impl FactoryTrait for DeFindexFactory { caller: Address, emergency_manager: Address, fee_receiver: Address, - vault_share: u32, + vault_fee: u32, vault_name: String, vault_symbol: String, manager: Address, @@ -305,7 +305,7 @@ impl FactoryTrait for DeFindexFactory { &manager, &emergency_manager, &fee_receiver, - &vault_share, + &vault_fee, &defindex_receiver, ¤t_contract, &vault_name, @@ -324,7 +324,7 @@ impl FactoryTrait for DeFindexFactory { ); add_new_defindex(&e, defindex_address.clone()); - events::emit_create_defindex_vault(&e, emergency_manager, fee_receiver, manager, vault_share, assets); + events::emit_create_defindex_vault(&e, emergency_manager, fee_receiver, manager, vault_fee, assets); Ok(defindex_address) } @@ -376,14 +376,14 @@ impl FactoryTrait for DeFindexFactory { /// /// # Returns /// * `Result<(), FactoryError>` - Returns Ok(()) if successful, or an error if not authorized. - fn set_fee_rate(e: Env, fee_rate: u32) -> Result<(), FactoryError> { + fn set_defindex_fee(e: Env, defindex_fee: u32) -> Result<(), FactoryError> { check_initialized(&e)?; extend_instance_ttl(&e); let admin = get_admin(&e); admin.require_auth(); - put_fee_rate(&e, &fee_rate); - events::emit_new_fee_rate(&e, fee_rate); + put_defindex_fee(&e, &defindex_fee); + events::emit_new_defindex_fee(&e, defindex_fee); Ok(()) } @@ -435,7 +435,7 @@ impl FactoryTrait for DeFindexFactory { /// /// # Returns /// * `Result` - Returns the fee rate in basis points or an error if not found. - fn fee_rate(e: Env) -> Result { + fn defindex_fee(e: Env) -> Result { check_initialized(&e)?; extend_instance_ttl(&e); Ok(get_fee_rate(&e)) diff --git a/apps/contracts/factory/src/storage.rs b/apps/contracts/factory/src/storage.rs index 92589bb5..bde91cd0 100644 --- a/apps/contracts/factory/src/storage.rs +++ b/apps/contracts/factory/src/storage.rs @@ -99,7 +99,7 @@ pub fn get_defindex_receiver(e: &Env) -> Address { } // Fee Rate BPS (MAX BPS = 10000) -pub fn put_fee_rate(e: &Env, value: &u32) { +pub fn put_defindex_fee(e: &Env, value: &u32) { e.storage().instance().set(&DataKey::FeeRate, value); } diff --git a/apps/contracts/factory/src/test.rs b/apps/contracts/factory/src/test.rs index dc9e065d..0a3d4368 100644 --- a/apps/contracts/factory/src/test.rs +++ b/apps/contracts/factory/src/test.rs @@ -43,7 +43,7 @@ mod defindex_vault_contract { // Create Test Token pub(crate) fn create_token_contract<'a>(e: &Env, admin: &Address) -> SorobanTokenClient<'a> { - SorobanTokenClient::new(e, &e.register_stellar_asset_contract(admin.clone())) + SorobanTokenClient::new(e, &e.register_stellar_asset_contract_v2(admin.clone()).address()) } pub(crate) fn get_token_admin_client<'a>( diff --git a/apps/contracts/factory/src/test/all_flow.rs b/apps/contracts/factory/src/test/all_flow.rs index fa0e5e9b..52405f9d 100644 --- a/apps/contracts/factory/src/test/all_flow.rs +++ b/apps/contracts/factory/src/test/all_flow.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{vec, BytesN, Map, String, Vec}; +use soroban_sdk::{vec, BytesN, String}; use crate::test::{create_asset_params, defindex_vault_contract::{self, Investment}, DeFindexFactoryTest}; diff --git a/apps/contracts/factory/src/test/initialize.rs b/apps/contracts/factory/src/test/initialize.rs index 7455baff..64cd8abc 100644 --- a/apps/contracts/factory/src/test/initialize.rs +++ b/apps/contracts/factory/src/test/initialize.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{vec, Address, BytesN, String, Vec}; +use soroban_sdk::{BytesN, String}; use crate::error::FactoryError; use crate::test::{create_asset_params, DeFindexFactoryTest}; diff --git a/apps/contracts/src/test.ts b/apps/contracts/src/test.ts index d8e91ae7..73b75e4f 100644 --- a/apps/contracts/src/test.ts +++ b/apps/contracts/src/test.ts @@ -84,7 +84,7 @@ export async function test_factory(addressBook: AddressBook) { const createDeFindexParams: xdr.ScVal[] = [ new Address(emergencyManager.publicKey()).toScVal(), new Address(feeReceiver.publicKey()).toScVal(), - nativeToScVal(100, { type: "u32" }), // Setting vault_share as 100 bps for demonstration + nativeToScVal(100, { type: "u32" }), // Setting vault_fee as 100 bps for demonstration nativeToScVal("Test Vault", { type: "string" }), nativeToScVal("DFT-Test-Vault", { type: "string" }), new Address(manager.publicKey()).toScVal(), diff --git a/apps/contracts/strategies/fixed_apr/Cargo.toml b/apps/contracts/strategies/fixed_apr/Cargo.toml new file mode 100644 index 00000000..add447c2 --- /dev/null +++ b/apps/contracts/strategies/fixed_apr/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "fixed_apr_strategy" +version = { workspace = true } +authors = ["coderipper "] +license = { workspace = true } +edition = { workspace = true } +publish = false +repository = { workspace = true } + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = { workspace = true } +defindex-strategy-core = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/apps/contracts/strategies/fixed_apr/Makefile b/apps/contracts/strategies/fixed_apr/Makefile new file mode 100644 index 00000000..480ff124 --- /dev/null +++ b/apps/contracts/strategies/fixed_apr/Makefile @@ -0,0 +1,17 @@ +default: build + +all: test + +test: build + cargo test + +build: + cargo build --target wasm32-unknown-unknown --release + soroban contract optimize --wasm ../../target/wasm32-unknown-unknown/release/fixed_apr_strategy.wasm + @rm ../../target/wasm32-unknown-unknown/release/fixed_apr_strategy.wasm + +fmt: + cargo fmt --all --check + +clean: + cargo clean \ No newline at end of file diff --git a/apps/contracts/strategies/fixed_apr/src/balance.rs b/apps/contracts/strategies/fixed_apr/src/balance.rs new file mode 100644 index 00000000..1f5751be --- /dev/null +++ b/apps/contracts/strategies/fixed_apr/src/balance.rs @@ -0,0 +1,43 @@ +use soroban_sdk::{Address, Env}; + +use crate::storage::{DataKey, INSTANCE_BUMP_AMOUNT, INSTANCE_LIFETIME_THRESHOLD}; +use crate::StrategyError; + +pub fn read_balance(e: &Env, addr: Address) -> i128 { + let key = DataKey::Balance(addr); + if let Some(balance) = e.storage().persistent().get::(&key) { + e.storage() + .persistent() + .extend_ttl(&key, INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + balance + } else { + 0 + } +} + +fn write_balance(e: &Env, addr: Address, amount: i128) { + let key = DataKey::Balance(addr); + e.storage().persistent().set(&key, &amount); + e.storage() + .persistent() + .extend_ttl(&key, INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); +} + +pub fn receive_balance(e: &Env, addr: Address, amount: i128) { + let balance = read_balance(e, addr.clone()); + + let new_balance = balance.checked_add(amount) + .expect("Integer overflow occurred while adding balance."); + + write_balance(e, addr, new_balance); +} + +pub fn spend_balance(e: &Env, addr: Address, amount: i128) -> Result<(), StrategyError> { + + let balance = read_balance(e, addr.clone()); + if balance < amount { + return Err(StrategyError::InsufficientBalance); + } + write_balance(e, addr, balance - amount); + Ok(()) +} diff --git a/apps/contracts/strategies/fixed_apr/src/constants.rs b/apps/contracts/strategies/fixed_apr/src/constants.rs new file mode 100644 index 00000000..1428e1b8 --- /dev/null +++ b/apps/contracts/strategies/fixed_apr/src/constants.rs @@ -0,0 +1,2 @@ +pub(crate) const MAX_BPS: i128 = 10_000; +pub(crate) const SECONDS_PER_YEAR: i128 = 31_536_000; diff --git a/apps/contracts/strategies/fixed_apr/src/lib.rs b/apps/contracts/strategies/fixed_apr/src/lib.rs new file mode 100644 index 00000000..a5474972 --- /dev/null +++ b/apps/contracts/strategies/fixed_apr/src/lib.rs @@ -0,0 +1,181 @@ +#![no_std] +use constants::{MAX_BPS, SECONDS_PER_YEAR}; +use soroban_sdk::{ + contract, contractimpl, token::Client as TokenClient, Address, Env, IntoVal, String, Val, Vec +}; + +mod balance; +mod constants; +mod storage; +mod yield_balance; + +use balance::{read_balance, receive_balance, spend_balance}; +use storage::{ + extend_instance_ttl, get_underlying_asset, is_initialized, set_initialized, + set_underlying_asset, set_apr, get_apr, set_last_harvest_time, get_last_harvest_time, +}; + +pub use defindex_strategy_core::{DeFindexStrategyTrait, StrategyError, event}; +use yield_balance::{read_yield, receive_yield, spend_yield}; + +pub fn check_nonnegative_amount(amount: i128) -> Result<(), StrategyError> { + if amount < 0 { + Err(StrategyError::NegativeNotAllowed) + } else { + Ok(()) + } +} + +fn check_initialized(e: &Env) -> Result<(), StrategyError> { + if is_initialized(e) { + Ok(()) + } else { + Err(StrategyError::NotInitialized) + } +} + +const STRATEGY_NAME: &str = "FixAprStrategy"; + +#[contract] +struct FixAprStrategy; + +#[contractimpl] +impl DeFindexStrategyTrait for FixAprStrategy { + fn initialize( + e: Env, + asset: Address, + init_args: Vec, + ) -> Result<(), StrategyError> { + if is_initialized(&e) { + return Err(StrategyError::AlreadyInitialized); + } + + // Extract APR from `init_args`, assumed to be the first argument + let apr_bps: u32 = init_args.get(0).ok_or(StrategyError::InvalidArgument)?.into_val(&e); + let caller: Address = init_args.get(1).ok_or(StrategyError::InvalidArgument)?.into_val(&e); + let amount: i128 = init_args.get(2).ok_or(StrategyError::InvalidArgument)?.into_val(&e); + + set_initialized(&e); + set_underlying_asset(&e, &asset); + set_apr(&e, apr_bps); + + // Should transfer tokens from the caller to the contract + caller.require_auth(); + TokenClient::new(&e, &asset).transfer(&caller, &e.current_contract_address(), &amount); + + event::emit_initialize(&e, String::from_str(&e, STRATEGY_NAME), asset); + extend_instance_ttl(&e); + Ok(()) + } + + fn asset(e: Env) -> Result { + check_initialized(&e)?; + extend_instance_ttl(&e); + Ok(get_underlying_asset(&e)) + } + + fn deposit( + e: Env, + amount: i128, + from: Address, + ) -> Result<(), StrategyError> { + check_initialized(&e)?; + check_nonnegative_amount(amount)?; + extend_instance_ttl(&e); + from.require_auth(); + + update_yield_balance(&e, &from); + + let contract_address = e.current_contract_address(); + let underlying_asset = get_underlying_asset(&e); + TokenClient::new(&e, &underlying_asset).transfer(&from, &contract_address, &amount); + + receive_balance(&e, from.clone(), amount); + + set_last_harvest_time(&e, e.ledger().timestamp(), from.clone()); + event::emit_deposit(&e, String::from_str(&e, STRATEGY_NAME), amount, from); + + Ok(()) + } + + fn harvest(e: Env, from: Address) -> Result<(), StrategyError> { + check_initialized(&e)?; + extend_instance_ttl(&e); + + let yield_balance = update_yield_balance(&e, &from); + + if yield_balance == 0 { + return Ok(()); + } + + // Transfer the reward tokens to the user's balance + spend_yield(&e, from.clone(), yield_balance)?; + receive_balance(&e, from.clone(), yield_balance); + + event::emit_harvest(&e, String::from_str(&e, STRATEGY_NAME), yield_balance, from); + + Ok(()) + } + + fn withdraw( + e: Env, + amount: i128, + from: Address, + ) -> Result { + from.require_auth(); + check_initialized(&e)?; + check_nonnegative_amount(amount)?; + extend_instance_ttl(&e); + + spend_balance(&e, from.clone(), amount)?; + + let contract_address = e.current_contract_address(); + let underlying_asset = get_underlying_asset(&e); + TokenClient::new(&e, &underlying_asset).transfer(&contract_address, &from, &amount); + event::emit_withdraw(&e, String::from_str(&e, STRATEGY_NAME), amount, from); + + Ok(amount) + } + + fn balance( + e: Env, + from: Address, + ) -> Result { + check_initialized(&e)?; + extend_instance_ttl(&e); + Ok(read_balance(&e, from)) + } +} + + +fn calculate_yield(user_balance: i128, apr: u32, time_elapsed: u64) -> i128 { + // Calculate yield based on the APR, time elapsed, and user's balance + let seconds_per_year = SECONDS_PER_YEAR; + let apr_bps = apr as i128; + let time_elapsed_i128 = time_elapsed as i128; + + (user_balance * apr_bps * time_elapsed_i128) / (seconds_per_year * MAX_BPS) +} + +fn update_yield_balance(e: &Env, from: &Address) -> i128 { + let apr = get_apr(e); + let last_harvest = get_last_harvest_time(e, from.clone()); + let time_elapsed = e.ledger().timestamp().saturating_sub(last_harvest); + + if time_elapsed == 0 { + return 0; + } + + let user_balance = read_balance(e, from.clone()); + let reward_amount = calculate_yield(user_balance, apr, time_elapsed); + + if reward_amount == 0 { + return 0; + } + + receive_yield(e, from.clone(), reward_amount); + set_last_harvest_time(e, e.ledger().timestamp(), from.clone()); + read_yield(e, from.clone()) +} + +mod test; \ No newline at end of file diff --git a/apps/contracts/strategies/fixed_apr/src/storage.rs b/apps/contracts/strategies/fixed_apr/src/storage.rs new file mode 100644 index 00000000..3df9aec5 --- /dev/null +++ b/apps/contracts/strategies/fixed_apr/src/storage.rs @@ -0,0 +1,57 @@ +use soroban_sdk::{contracttype, Address, Env}; + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Initialized, + UnderlyingAsset, + Balance(Address), + YieldBalance(Address), + Apr, + LastHarvestTime(Address), +} + +const DAY_IN_LEDGERS: u32 = 17280; +pub const INSTANCE_BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; +pub const INSTANCE_LIFETIME_THRESHOLD: u32 = INSTANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; + +pub fn extend_instance_ttl(e: &Env) { + e.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); +} + +pub fn set_initialized(e: &Env) { + e.storage().instance().set(&DataKey::Initialized, &true); +} + +pub fn is_initialized(e: &Env) -> bool { + e.storage().instance().has(&DataKey::Initialized) +} + +// Underlying asset +pub fn set_underlying_asset(e: &Env, address: &Address) { + e.storage().instance().set(&DataKey::UnderlyingAsset, &address); +} + +pub fn get_underlying_asset(e: &Env) -> Address { + e.storage().instance().get(&DataKey::UnderlyingAsset).unwrap() +} + +// Apr +pub fn set_apr(e: &Env, apr: u32) { + e.storage().instance().set(&DataKey::Apr, &apr); +} + +pub fn get_apr(e: &Env) -> u32 { + e.storage().instance().get(&DataKey::Apr).unwrap() +} + +// Last harvest time +pub fn set_last_harvest_time(e: &Env, timestamp: u64, from: Address) { + e.storage().instance().set(&DataKey::LastHarvestTime(from), ×tamp); +} + +pub fn get_last_harvest_time(e: &Env, from: Address) -> u64 { + e.storage().instance().get(&DataKey::LastHarvestTime(from)).unwrap_or(0) +} \ No newline at end of file diff --git a/apps/contracts/strategies/fixed_apr/src/test.rs b/apps/contracts/strategies/fixed_apr/src/test.rs new file mode 100644 index 00000000..3885aeb8 --- /dev/null +++ b/apps/contracts/strategies/fixed_apr/src/test.rs @@ -0,0 +1,64 @@ +#![cfg(test)] +extern crate std; +use crate::{FixAprStrategy, FixAprStrategyClient, StrategyError}; + +use soroban_sdk::token::TokenClient; + +use soroban_sdk::{ + Env, + Address, + testutils::Address as _, +}; + +use std::vec as stdvec; + +// Base Strategy Contract +fn create_fixapr_strategy<'a>(e: &Env) -> FixAprStrategyClient<'a> { + FixAprStrategyClient::new(e, &e.register_contract(None, FixAprStrategy {})) +} + +// Create Test Token +pub(crate) fn create_token_contract<'a>(e: &Env, admin: &Address) -> TokenClient<'a> { + TokenClient::new(e, &e.register_stellar_asset_contract_v2(admin.clone()).address()) +} + +pub struct FixAprStrategyTest<'a> { + env: Env, + strategy: FixAprStrategyClient<'a>, + token: TokenClient<'a>, + strategy_admin: Address, +} + +impl<'a> FixAprStrategyTest<'a> { + fn setup() -> Self { + + let env = Env::default(); + env.mock_all_auths(); + + let strategy = create_fixapr_strategy(&env); + let admin = Address::generate(&env); + let token = create_token_contract(&env, &admin); + + let strategy_admin = Address::generate(&env); + + FixAprStrategyTest { + env, + strategy, + token, + strategy_admin + } + } + + pub(crate) fn generate_random_users(e: &Env, users_count: u32) -> stdvec::Vec
{ + let mut users = stdvec![]; + for _c in 0..users_count { + users.push(Address::generate(e)); + } + users + } +} + +mod initialize; +mod deposit; +mod harvest; +mod withdraw; \ No newline at end of file diff --git a/apps/contracts/strategies/fixed_apr/src/test/deposit.rs b/apps/contracts/strategies/fixed_apr/src/test/deposit.rs new file mode 100644 index 00000000..6a68c39e --- /dev/null +++ b/apps/contracts/strategies/fixed_apr/src/test/deposit.rs @@ -0,0 +1,103 @@ +use crate::test::FixAprStrategyTest; +use crate::test::StrategyError; +use soroban_sdk::token::StellarAssetClient; +use soroban_sdk::vec; +use soroban_sdk::{IntoVal, Vec, Val}; + +// test deposit with negative amount +#[test] +fn deposit_with_negative_amount() { + let test = FixAprStrategyTest::setup(); + //MINT 100M to the strategy + let starting_amount = 100_000_000_000_0_000_000i128; + StellarAssetClient::new(&test.env, &test.token.address).mint(&test.strategy_admin, &starting_amount); + + let init_fn_args: Vec = vec![&test.env, + 1000u32.into_val(&test.env), + test.strategy_admin.into_val(&test.env), + starting_amount.into_val(&test.env), + ]; + + test.strategy.initialize(&test.token.address, &init_fn_args); + + let users = FixAprStrategyTest::generate_random_users(&test.env, 1); + + let amount = -123456; + + let result = test.strategy.try_deposit(&amount, &users[0]); + assert_eq!(result, Err(Ok(StrategyError::NegativeNotAllowed))); +} + +// test deposit with zero amount +#[test] +fn deposit_with_zero_amount() { + let test = FixAprStrategyTest::setup(); + // MINT 100M to the strategy + let starting_amount = 100_000_000_000_0_000_000i128; + StellarAssetClient::new(&test.env, &test.token.address).mint(&test.strategy_admin, &starting_amount); + + let init_fn_args: Vec = vec![&test.env, + 1000u32.into_val(&test.env), + test.strategy_admin.into_val(&test.env), + starting_amount.into_val(&test.env), + ]; + + test.strategy.initialize(&test.token.address, &init_fn_args); + + let users = FixAprStrategyTest::generate_random_users(&test.env, 1); + + let amount = 0; + + test.strategy.deposit(&amount, &users[0]); +} + +// test deposit with positive amount +#[test] +fn deposit() { + let test = FixAprStrategyTest::setup(); + // MINT 100M to the strategy + let starting_amount = 100_000_000_000_0_000_000i128; + StellarAssetClient::new(&test.env, &test.token.address).mint(&test.strategy_admin, &starting_amount); + + let init_fn_args: Vec = vec![&test.env, + 1000u32.into_val(&test.env), + test.strategy_admin.into_val(&test.env), + starting_amount.into_val(&test.env), + ]; + + test.strategy.initialize(&test.token.address, &init_fn_args); + + let users = FixAprStrategyTest::generate_random_users(&test.env, 1); + + let amount = 1_000_0_00_000; + StellarAssetClient::new(&test.env, &test.token.address).mint(&users[0], &amount); + + test.strategy.deposit(&amount, &users[0]); + let user_balance = test.token.balance(&users[0]); + assert_eq!(user_balance, 0); +} + +// test deposit with amount exceeding balance +#[test] +#[should_panic(expected = "HostError: Error(Contract, #10)")] // Unauthorized +fn deposit_with_exceeding_balance() { + let test = FixAprStrategyTest::setup(); + // MINT 100M to the strategy + let starting_amount = 100_000_000_000_0_000_000i128; + StellarAssetClient::new(&test.env, &test.token.address).mint(&test.strategy_admin, &starting_amount); + + let init_fn_args: Vec = vec![&test.env, + 1000u32.into_val(&test.env), + test.strategy_admin.into_val(&test.env), + starting_amount.into_val(&test.env), + ]; + + test.strategy.initialize(&test.token.address, &init_fn_args); + + let users = FixAprStrategyTest::generate_random_users(&test.env, 1); + + let amount = 1_000_0_00_000; + StellarAssetClient::new(&test.env, &test.token.address).mint(&users[0], &(&amount - 100_0_000_000)); + + test.strategy.deposit(&amount, &users[0]); +} diff --git a/apps/contracts/strategies/fixed_apr/src/test/harvest.rs b/apps/contracts/strategies/fixed_apr/src/test/harvest.rs new file mode 100644 index 00000000..17c2711e --- /dev/null +++ b/apps/contracts/strategies/fixed_apr/src/test/harvest.rs @@ -0,0 +1,72 @@ +use soroban_sdk::{testutils::Ledger, token::StellarAssetClient, vec, IntoVal, Val, Vec}; + +use crate::{calculate_yield, test::FixAprStrategyTest}; + +#[test] +fn test_harvest_yields_multiple_users() { + let test = FixAprStrategyTest::setup(); + + //MINT 100M to the strategy + let starting_amount = 100_000_000_000_0_000_000i128; + StellarAssetClient::new(&test.env, &test.token.address).mint(&test.strategy_admin, &starting_amount); + + let apr = 1000u32; + let init_fn_args: Vec = vec![&test.env, + apr.clone().into_val(&test.env), + test.strategy_admin.into_val(&test.env), + starting_amount.into_val(&test.env), + ]; + + test.strategy.initialize(&test.token.address, &init_fn_args); + assert_eq!(test.token.balance(&test.strategy.address), starting_amount); + + let users = FixAprStrategyTest::generate_random_users(&test.env, 4); + + // Mint tokens to users + let user1_amount = 1_000_0_000_000i128; + StellarAssetClient::new(&test.env, &test.token.address).mint(&users[0], &user1_amount); + let user2_amount = 2_000_0_000_000i128; + StellarAssetClient::new(&test.env, &test.token.address).mint(&users[1], &user2_amount); + let user3_amount = 500_0_000_000i128; + StellarAssetClient::new(&test.env, &test.token.address).mint(&users[2], &user3_amount); + let user4_amount = 10_000_0_000_000i128; + StellarAssetClient::new(&test.env, &test.token.address).mint(&users[3], &user4_amount); + + // Deposit tokens for each user + test.strategy.deposit(&user1_amount, &users[0]); + let user1_balance = test.token.balance(&users[0]); + assert_eq!(user1_balance, 0); + + test.strategy.deposit(&user2_amount, &users[1]); + let user2_balance = test.token.balance(&users[1]); + assert_eq!(user2_balance, 0); + + test.strategy.deposit(&user3_amount, &users[2]); + let user3_balance = test.token.balance(&users[2]); + assert_eq!(user3_balance, 0); + + test.strategy.deposit(&user4_amount, &users[3]); + let user4_balance = test.token.balance(&users[3]); + assert_eq!(user4_balance, 0); + + // Simulate one year passing + let one_year_in_seconds = 31_536_000u64; + test.env.ledger().set_timestamp(test.env.ledger().timestamp() + one_year_in_seconds); + + // Harvest for each user + test.strategy.harvest(&users[0]); + test.strategy.harvest(&users[1]); + test.strategy.harvest(&users[2]); + test.strategy.harvest(&users[3]); + + // Check the harvested rewards for each user are correct + let user1_expected_reward = calculate_yield(user1_amount, apr, one_year_in_seconds); + let user2_expected_reward = calculate_yield(user2_amount, apr, one_year_in_seconds); + let user3_expected_reward = calculate_yield(user3_amount, apr, one_year_in_seconds); + let user4_expected_reward = calculate_yield(user4_amount, apr, one_year_in_seconds); + + assert_eq!(test.strategy.balance(&users[0]), user1_amount + user1_expected_reward); + assert_eq!(test.strategy.balance(&users[1]), user2_amount + user2_expected_reward); + assert_eq!(test.strategy.balance(&users[2]), user3_amount + user3_expected_reward); + assert_eq!(test.strategy.balance(&users[3]), user4_amount + user4_expected_reward); +} \ No newline at end of file diff --git a/apps/contracts/strategies/fixed_apr/src/test/initialize.rs b/apps/contracts/strategies/fixed_apr/src/test/initialize.rs new file mode 100644 index 00000000..0d085a72 --- /dev/null +++ b/apps/contracts/strategies/fixed_apr/src/test/initialize.rs @@ -0,0 +1,55 @@ +// Cannot Initialize twice +extern crate std; +use soroban_sdk::token::StellarAssetClient; +use soroban_sdk::vec; +use soroban_sdk::{IntoVal, Vec, Val}; +use crate::test::FixAprStrategyTest; +use crate::test::StrategyError; + +#[test] +fn initialize() { + let test = FixAprStrategyTest::setup(); + + //MINT 100M to the strategy + let starting_amount = 100_000_000_000_0_000_000i128; + StellarAssetClient::new(&test.env, &test.token.address).mint(&test.strategy_admin, &starting_amount); + + let init_fn_args: Vec = vec![&test.env, + 1000u32.into_val(&test.env), + test.strategy_admin.into_val(&test.env), + starting_amount.into_val(&test.env), + ]; + + test.strategy.initialize(&test.token.address, &init_fn_args); + + // get asset should return underlying asset + let underlying_asset = test.strategy.asset(); + assert_eq!(underlying_asset, test.token.address); + + // get contract token amount + let contract_token_amount = test.token.balance(&test.strategy.address); + assert_eq!(contract_token_amount, starting_amount); +} + +#[test] +fn cannot_initialize_twice() { + let test = FixAprStrategyTest::setup(); + + //MINT 100M to the strategy + let starting_amount = 100_000_000_000_0_000_000i128; + StellarAssetClient::new(&test.env, &test.token.address).mint(&test.strategy_admin, &starting_amount); + + let init_fn_args: Vec = vec![&test.env, + 1000u32.into_val(&test.env), + test.strategy_admin.into_val(&test.env), + starting_amount.into_val(&test.env), + ]; + + test.strategy.initialize(&test.token.address, &init_fn_args); + let result = test.strategy.try_initialize(&test.token.address , &init_fn_args); + assert_eq!(result, Err(Ok(StrategyError::AlreadyInitialized))); + + // get asset should return underlying asset + let underlying_asset = test.strategy.asset(); + assert_eq!(underlying_asset, test.token.address); +} \ No newline at end of file diff --git a/apps/contracts/strategies/fixed_apr/src/test/withdraw.rs b/apps/contracts/strategies/fixed_apr/src/test/withdraw.rs new file mode 100644 index 00000000..b8f9592c --- /dev/null +++ b/apps/contracts/strategies/fixed_apr/src/test/withdraw.rs @@ -0,0 +1,143 @@ +use crate::{calculate_yield, test::FixAprStrategyTest}; +use defindex_strategy_core::StrategyError; +use soroban_sdk::testutils::Ledger; +use soroban_sdk::token::StellarAssetClient; +use soroban_sdk::vec; +use soroban_sdk::{IntoVal, Vec, Val}; + +#[test] +fn withdraw() { + let test = FixAprStrategyTest::setup(); + // MINT 100M to the strategy + let starting_amount = 100_000_000_000_0_000_000i128; + StellarAssetClient::new(&test.env, &test.token.address).mint(&test.strategy_admin, &starting_amount); + + let init_fn_args: Vec = vec![&test.env, + 1000u32.into_val(&test.env), + test.strategy_admin.into_val(&test.env), + starting_amount.into_val(&test.env), + ]; + + test.strategy.initialize(&test.token.address, &init_fn_args); + + let users = FixAprStrategyTest::generate_random_users(&test.env, 1); + + let amount = 1_000_0_00_000; + StellarAssetClient::new(&test.env, &test.token.address).mint(&users[0], &amount); + + test.strategy.deposit(&amount, &users[0]); + let user_balance = test.token.balance(&users[0]); + assert_eq!(user_balance, 0); + + test.strategy.withdraw(&amount, &users[0]); + let user_balance_after_withdraw = test.token.balance(&users[0]); + assert_eq!(user_balance_after_withdraw, amount); +} + +#[test] +fn withdraw_with_harvest() { + let test = FixAprStrategyTest::setup(); + // MINT 100M to the strategy + let starting_amount = 100_000_000_000_0_000_000i128; + StellarAssetClient::new(&test.env, &test.token.address).mint(&test.strategy_admin, &starting_amount); + + let init_fn_args: Vec = vec![&test.env, + 1000u32.into_val(&test.env), + test.strategy_admin.into_val(&test.env), + starting_amount.into_val(&test.env), + ]; + + test.strategy.initialize(&test.token.address, &init_fn_args); + + let users = FixAprStrategyTest::generate_random_users(&test.env, 1); + + let amount = 1_000_0_00_000; + StellarAssetClient::new(&test.env, &test.token.address).mint(&users[0], &amount); + + test.strategy.deposit(&amount, &users[0]); + let user_balance = test.token.balance(&users[0]); + assert_eq!(user_balance, 0); + + // Simulate one year passing + let one_year_in_seconds = 31_536_000u64; + test.env.ledger().set_timestamp(test.env.ledger().timestamp() + one_year_in_seconds); + + test.strategy.harvest(&users[0]); + + let expected_reward = calculate_yield(amount, 1000u32, one_year_in_seconds); + let user_balance_after_harvest = test.strategy.balance(&users[0]); + assert_eq!(user_balance_after_harvest, amount + expected_reward); + + test.strategy.withdraw(&amount, &users[0]); + let user_balance_after_withdraw = test.token.balance(&users[0]); + assert_eq!(user_balance_after_withdraw, amount); +} + +#[test] +fn withdraw_then_harvest_then_withdraw_again() { + let test = FixAprStrategyTest::setup(); + // MINT 100M to the strategy + let starting_amount = 100_000_000_000_0_000_000i128; + StellarAssetClient::new(&test.env, &test.token.address).mint(&test.strategy_admin, &starting_amount); + + let init_fn_args: Vec = vec![&test.env, + 1000u32.into_val(&test.env), + test.strategy_admin.into_val(&test.env), + starting_amount.into_val(&test.env), + ]; + + test.strategy.initialize(&test.token.address, &init_fn_args); + + let users = FixAprStrategyTest::generate_random_users(&test.env, 1); + + let amount = 1_000_0_000_000; + StellarAssetClient::new(&test.env, &test.token.address).mint(&users[0], &amount); + + test.strategy.deposit(&amount, &users[0]); + let user_balance = test.token.balance(&users[0]); + assert_eq!(user_balance, 0); + + // Simulate one year passing + let one_year_in_seconds = 31_536_000u64; + test.env.ledger().set_timestamp(test.env.ledger().timestamp() + one_year_in_seconds); + + let user_balance_before_harvest = test.strategy.balance(&users[0]); + assert_eq!(user_balance_before_harvest, amount); + + test.strategy.withdraw(&amount, &users[0]); + let user_balance_after_withdraw = test.token.balance(&users[0]); + assert_eq!(user_balance_after_withdraw, amount); + + // test.strategy.harvest(&users[0]); + + // let expected_reward = calculate_yield(amount, 1000u32, one_year_in_seconds); + // let user_balance_after_harvest = test.strategy.balance(&users[0]); + // assert_eq!(user_balance_after_harvest, expected_reward); + + // test.strategy.withdraw(&expected_reward, &users[0]); + // let user_balance_after_second_withdraw = test.token.balance(&users[0]); + // assert_eq!(user_balance_after_second_withdraw, amount + expected_reward); +} + +#[test] +fn withdraw_with_no_balance() { + let test = FixAprStrategyTest::setup(); + // MINT 100M to the strategy + let starting_amount = 100_000_000_000_0_000_000i128; + StellarAssetClient::new(&test.env, &test.token.address).mint(&test.strategy_admin, &starting_amount); + + let init_fn_args: Vec = vec![&test.env, + 1000u32.into_val(&test.env), + test.strategy_admin.into_val(&test.env), + starting_amount.into_val(&test.env), + ]; + + test.strategy.initialize(&test.token.address, &init_fn_args); + + let users = FixAprStrategyTest::generate_random_users(&test.env, 1); + + let amount = 1_000_0_00_000; + + let result = test.strategy.try_withdraw(&amount, &users[0]); + assert_eq!(result, Err(Ok(StrategyError::InsufficientBalance))); +} \ No newline at end of file diff --git a/apps/contracts/strategies/fixed_apr/src/yield_balance.rs b/apps/contracts/strategies/fixed_apr/src/yield_balance.rs new file mode 100644 index 00000000..bf60f929 --- /dev/null +++ b/apps/contracts/strategies/fixed_apr/src/yield_balance.rs @@ -0,0 +1,43 @@ +use soroban_sdk::{Address, Env}; + +use crate::storage::{DataKey, INSTANCE_BUMP_AMOUNT, INSTANCE_LIFETIME_THRESHOLD}; +use crate::StrategyError; + +pub fn read_yield(e: &Env, addr: Address) -> i128 { + let key = DataKey::YieldBalance(addr); + if let Some(balance) = e.storage().persistent().get::(&key) { + e.storage() + .persistent() + .extend_ttl(&key, INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + balance + } else { + 0 + } +} + +fn write_yield(e: &Env, addr: Address, amount: i128) { + let key = DataKey::YieldBalance(addr); + e.storage().persistent().set(&key, &amount); + e.storage() + .persistent() + .extend_ttl(&key, INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); +} + +pub fn receive_yield(e: &Env, addr: Address, amount: i128) { + let balance = read_yield(e, addr.clone()); + + let new_balance = balance.checked_add(amount) + .expect("Integer overflow occurred while adding balance."); + + write_yield(e, addr, new_balance); +} + +pub fn spend_yield(e: &Env, addr: Address, amount: i128) -> Result<(), StrategyError> { + + let balance = read_yield(e, addr.clone()); + if balance < amount { + return Err(StrategyError::InsufficientBalance); + } + write_yield(e, addr, balance - amount); + Ok(()) +} diff --git a/apps/contracts/strategies/soroswap/src/lib.rs b/apps/contracts/strategies/soroswap/src/lib.rs index 3d4bf827..77daf1b9 100644 --- a/apps/contracts/strategies/soroswap/src/lib.rs +++ b/apps/contracts/strategies/soroswap/src/lib.rs @@ -123,7 +123,7 @@ impl DeFindexStrategyTrait for SoroswapAdapter { let total_swapped_amount = swap_result.last().unwrap(); // Add liquidity - let result = soroswap_router_client.add_liquidity( + let _result = soroswap_router_client.add_liquidity( &usdc_address, &xlm_address, &swap_amount, @@ -137,7 +137,7 @@ impl DeFindexStrategyTrait for SoroswapAdapter { Ok(()) } - fn harvest(e: Env, from: Address) -> Result<(), StrategyError> { + fn harvest(e: Env, _from: Address) -> Result<(), StrategyError> { check_initialized(&e)?; extend_instance_ttl(&e); @@ -146,7 +146,7 @@ impl DeFindexStrategyTrait for SoroswapAdapter { fn withdraw( e: Env, - amount: i128, + _amount: i128, from: Address, ) -> Result { from.require_auth(); @@ -186,7 +186,7 @@ impl DeFindexStrategyTrait for SoroswapAdapter { ]); // Remove liquidity - let (usdc_amount, xlm_amount) = soroswap_router_client.remove_liquidity( + let (_usdc_amount, xlm_amount) = soroswap_router_client.remove_liquidity( &usdc_address, &xlm_address, &lp_balance, diff --git a/apps/contracts/strategies/xycloans/src/lib.rs b/apps/contracts/strategies/xycloans/src/lib.rs index c47d5732..8383087f 100644 --- a/apps/contracts/strategies/xycloans/src/lib.rs +++ b/apps/contracts/strategies/xycloans/src/lib.rs @@ -5,11 +5,11 @@ mod storage; mod soroswap_router; mod xycloans_pool; -use soroban_sdk::{auth::{ContractContext, InvokerContractAuthEntry, SubContractInvocation}, contract, contractimpl, vec, Address, Env, IntoVal, Symbol, Val, Vec}; +use soroban_sdk::{contract, contractimpl, Address, Env, IntoVal, Val, Vec}; use storage::{ - extend_instance_ttl, get_soroswap_router_address, get_pool_token, get_token_in, get_xycloans_pool_address, is_initialized, set_initialized, set_soroswap_router_address, set_pool_token, set_token_in, set_xycloans_pool_address, set_soroswap_factory_address, get_soroswap_factory_address + extend_instance_ttl, get_pool_token, get_token_in, get_xycloans_pool_address, is_initialized, set_initialized, set_soroswap_router_address, set_pool_token, set_token_in, set_xycloans_pool_address, set_soroswap_factory_address, get_soroswap_factory_address }; -use soroswap_router::{get_amount_out, get_reserves, pair_for, swap, SoroswapRouterClient}; +use soroswap_router::{get_amount_out, get_reserves, swap}; use xycloans_pool::XycloansPoolClient; use defindex_strategy_core::{StrategyError, DeFindexStrategyTrait}; @@ -92,7 +92,7 @@ impl DeFindexStrategyTrait for XycloansAdapter { Ok(()) } - fn harvest(e: Env, from: Address) -> Result<(), StrategyError> { + fn harvest(e: Env, _from: Address) -> Result<(), StrategyError> { check_initialized(&e)?; extend_instance_ttl(&e); @@ -101,7 +101,7 @@ impl DeFindexStrategyTrait for XycloansAdapter { fn withdraw( e: Env, - amount: i128, + _amount: i128, from: Address, ) -> Result { from.require_auth(); diff --git a/apps/contracts/defindex/Cargo.toml b/apps/contracts/vault/Cargo.toml similarity index 100% rename from apps/contracts/defindex/Cargo.toml rename to apps/contracts/vault/Cargo.toml diff --git a/apps/contracts/defindex/Makefile b/apps/contracts/vault/Makefile similarity index 100% rename from apps/contracts/defindex/Makefile rename to apps/contracts/vault/Makefile diff --git a/apps/contracts/defindex/src/access.rs b/apps/contracts/vault/src/access.rs similarity index 88% rename from apps/contracts/defindex/src/access.rs rename to apps/contracts/vault/src/access.rs index c601242a..d4f00a96 100644 --- a/apps/contracts/defindex/src/access.rs +++ b/apps/contracts/vault/src/access.rs @@ -6,7 +6,7 @@ use soroban_sdk::{contracttype, panic_with_error, Address, Env}; #[contracttype] pub enum RolesDataKey { EmergencyManager, // Role: Emergency Manager - FeeReceiver, // Role: Fee Receiver + VaultFeeReceiver, // Role: Fee Receiver Manager, // Role: Manager } @@ -82,13 +82,16 @@ impl AccessControlTrait for AccessControl { // Role-specific setters and getters impl AccessControl { - pub fn set_fee_receiver(&self, caller: &Address, fee_receiver: &Address) { - self.require_any_role(&[RolesDataKey::Manager, RolesDataKey::FeeReceiver], caller); - self.set_role(&RolesDataKey::FeeReceiver, fee_receiver); + pub fn set_fee_receiver(&self, caller: &Address, vault_fee_receiver: &Address) { + self.require_any_role( + &[RolesDataKey::Manager, RolesDataKey::VaultFeeReceiver], + caller, + ); + self.set_role(&RolesDataKey::VaultFeeReceiver, vault_fee_receiver); } pub fn get_fee_receiver(&self) -> Result { - self.check_role(&RolesDataKey::FeeReceiver) + self.check_role(&RolesDataKey::VaultFeeReceiver) } pub fn set_manager(&self, manager: &Address) { diff --git a/apps/contracts/defindex/src/aggregator.rs b/apps/contracts/vault/src/aggregator.rs similarity index 65% rename from apps/contracts/defindex/src/aggregator.rs rename to apps/contracts/vault/src/aggregator.rs index 70f5050c..c10e34db 100644 --- a/apps/contracts/defindex/src/aggregator.rs +++ b/apps/contracts/vault/src/aggregator.rs @@ -1,14 +1,18 @@ use soroban_sdk::{vec, Address, Env, IntoVal, Symbol, Val, Vec}; -use crate::{models::DexDistribution, storage::{get_assets, get_factory}, ContractError}; +use crate::{ + models::DexDistribution, + storage::{get_assets, get_factory}, + ContractError, +}; fn fetch_aggregator_address(e: &Env) -> Address { let factory_address = get_factory(e); e.invoke_contract( - &factory_address, - &Symbol::new(&e, "aggregator"), - Vec::new(&e) + &factory_address, + &Symbol::new(&e, "aggregator"), + Vec::new(&e), ) } @@ -16,8 +20,16 @@ fn is_supported_asset(e: &Env, token: &Address) -> bool { let assets = get_assets(e); assets.iter().any(|asset| &asset.address == token) } - -pub fn internal_swap_exact_tokens_for_tokens(e: &Env, token_in: &Address, token_out: &Address, amount_in: &i128, amount_out_min: &i128, distribution: &Vec, deadline: &u64) -> Result<(), ContractError> { + +pub fn internal_swap_exact_tokens_for_tokens( + e: &Env, + token_in: &Address, + token_out: &Address, + amount_in: &i128, + amount_out_min: &i128, + distribution: &Vec, + deadline: &u64, +) -> Result<(), ContractError> { let aggregator_address = fetch_aggregator_address(e); // Check if both tokens are supported by the vault @@ -35,13 +47,21 @@ pub fn internal_swap_exact_tokens_for_tokens(e: &Env, token_in: &Address, token_ init_args.push_back(deadline.into_val(e)); e.invoke_contract( - &aggregator_address, - &Symbol::new(&e, "swap_exact_tokens_for_tokens"), - Vec::new(&e) + &aggregator_address, + &Symbol::new(&e, "swap_exact_tokens_for_tokens"), + Vec::new(&e), ) } -pub fn internal_swap_tokens_for_exact_tokens(e: &Env, token_in: &Address, token_out: &Address, amount_out: &i128, amount_in_max: &i128, distribution: &Vec, deadline: &u64) -> Result<(), ContractError> { +pub fn internal_swap_tokens_for_exact_tokens( + e: &Env, + token_in: &Address, + token_out: &Address, + amount_out: &i128, + amount_in_max: &i128, + distribution: &Vec, + deadline: &u64, +) -> Result<(), ContractError> { let aggregator_address = fetch_aggregator_address(e); // Check if both tokens are supported by the vault @@ -59,8 +79,8 @@ pub fn internal_swap_tokens_for_exact_tokens(e: &Env, token_in: &Address, token_ init_args.push_back(deadline.into_val(e)); e.invoke_contract( - &aggregator_address, - &Symbol::new(&e, "swap_tokens_for_exact_tokens"), - Vec::new(&e) + &aggregator_address, + &Symbol::new(&e, "swap_tokens_for_exact_tokens"), + Vec::new(&e), ) -} \ No newline at end of file +} diff --git a/apps/contracts/vault/src/constants.rs b/apps/contracts/vault/src/constants.rs new file mode 100644 index 00000000..1428e1b8 --- /dev/null +++ b/apps/contracts/vault/src/constants.rs @@ -0,0 +1,2 @@ +pub(crate) const MAX_BPS: i128 = 10_000; +pub(crate) const SECONDS_PER_YEAR: i128 = 31_536_000; diff --git a/apps/contracts/defindex/src/error.rs b/apps/contracts/vault/src/error.rs similarity index 83% rename from apps/contracts/defindex/src/error.rs rename to apps/contracts/vault/src/error.rs index 24b4b6f0..583d8592 100644 --- a/apps/contracts/defindex/src/error.rs +++ b/apps/contracts/vault/src/error.rs @@ -8,16 +8,19 @@ pub enum ContractError { NotInitialized = 100, AlreadyInitialized = 101, InvalidRatio = 102, - StrategyDoesNotSupportAsset=103, + StrategyDoesNotSupportAsset = 103, + NoAssetAllocation = 104, // Validation Errors (11x) NegativeNotAllowed = 110, InsufficientBalance = 111, - WrongAmuntsLength = 112, + WrongAmountsLength = 112, NotEnoughIdleFunds = 113, InsufficientManagedFunds = 114, MissingInstructionData = 115, UnsupportedAsset = 116, + InsufficientAmount = 117, + NoOptimalAmounts = 118, //this should not happen // Arithmetic Errors (12x) ArithmeticError = 120, diff --git a/apps/contracts/defindex/src/events.rs b/apps/contracts/vault/src/events.rs similarity index 91% rename from apps/contracts/defindex/src/events.rs rename to apps/contracts/vault/src/events.rs index deca5b5d..c7d46eb7 100644 --- a/apps/contracts/defindex/src/events.rs +++ b/apps/contracts/vault/src/events.rs @@ -1,15 +1,15 @@ //! Definition of the Events used in the DeFindex Vault contract -use soroban_sdk::{contracttype, symbol_short, Address, Env, Vec}; use crate::models::AssetAllocation; +use soroban_sdk::{contracttype, symbol_short, Address, Env, Vec}; // INITIALIZED VAULT EVENT #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct InitializedVaultEvent { pub emergency_manager: Address, - pub fee_receiver: Address, + pub vault_fee_receiver: Address, pub manager: Address, - pub defindex_receiver: Address, + pub defindex_protocol_receiver: Address, pub assets: Vec, } @@ -17,16 +17,16 @@ pub struct InitializedVaultEvent { pub(crate) fn emit_initialized_vault( e: &Env, emergency_manager: Address, - fee_receiver: Address, + vault_fee_receiver: Address, manager: Address, - defindex_receiver: Address, + defindex_protocol_receiver: Address, assets: Vec, ) { let event = InitializedVaultEvent { emergency_manager, - fee_receiver, + vault_fee_receiver, manager, - defindex_receiver, + defindex_protocol_receiver, assets, }; @@ -178,9 +178,7 @@ pub struct ManagerChangedEvent { /// Publishes a `ManagerChangedEvent` to the event stream. pub(crate) fn emit_manager_changed_event(e: &Env, new_manager: Address) { - let event = ManagerChangedEvent { - new_manager, - }; + let event = ManagerChangedEvent { new_manager }; e.events() .publish(("DeFindexVault", symbol_short!("nmanager")), event); @@ -194,10 +192,7 @@ pub struct EmergencyManagerChangedEvent { } /// Publishes an `EmergencyManagerChangedEvent` to the event stream. -pub(crate) fn emit_emergency_manager_changed_event( - e: &Env, - new_emergency_manager: Address, -) { +pub(crate) fn emit_emergency_manager_changed_event(e: &Env, new_emergency_manager: Address) { let event = EmergencyManagerChangedEvent { new_emergency_manager, }; @@ -210,7 +205,7 @@ pub(crate) fn emit_emergency_manager_changed_event( #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct FeesMintedEvent { - pub defindex_receiver: Address, + pub defindex_protocol_receiver: Address, pub defindex_shares: i128, pub vault_receiver: Address, pub vault_shares: i128, @@ -219,13 +214,13 @@ pub struct FeesMintedEvent { /// Publishes an `EmergencyManagerChangedEvent` to the event stream. pub(crate) fn emit_fees_minted_event( e: &Env, - defindex_receiver: Address, + defindex_protocol_receiver: Address, defindex_shares: i128, vault_receiver: Address, vault_shares: i128, ) { let event = FeesMintedEvent { - defindex_receiver, + defindex_protocol_receiver, defindex_shares, vault_receiver, vault_shares, @@ -233,4 +228,4 @@ pub(crate) fn emit_fees_minted_event( e.events() .publish(("DeFindexVault", symbol_short!("mfees")), event); -} \ No newline at end of file +} diff --git a/apps/contracts/vault/src/fee.rs b/apps/contracts/vault/src/fee.rs new file mode 100644 index 00000000..8a4dd847 --- /dev/null +++ b/apps/contracts/vault/src/fee.rs @@ -0,0 +1,107 @@ +use soroban_sdk::{Env, Symbol, Vec}; + +use crate::{ + access::AccessControl, + constants::{MAX_BPS, SECONDS_PER_YEAR}, + events, + storage::{ + get_defindex_protocol_fee_receiver, get_factory, get_last_fee_assesment, get_vault_fee, + set_last_fee_assesment, + }, + token::{internal_mint, VaultToken}, + ContractError, +}; + +/// Fetches the current fee rate from the factory contract. +/// The fee rate is expressed in basis points (BPS). +fn fetch_defindex_fee(e: &Env) -> u32 { + let factory_address = get_factory(e); + // Interacts with the factory contract to get the fee rate. + e.invoke_contract( + &factory_address, + &Symbol::new(&e, "defindex_fee"), + Vec::new(&e) + ) +} + +/// Calculates the required fees in dfTokens based on the current APR fee rate. +fn calculate_fees(e: &Env, time_elapsed: u64, fee_rate: u32) -> Result { + let total_supply = VaultToken::total_supply(e.clone()); + + // fee_rate as i128 * total_supply * time_elapsed / SECONDS_PER_YEAR * MAX_BPS - fee_rate as i128 * time_elapsed; + let numerator = (fee_rate as i128) + .checked_mul(total_supply) + .unwrap() + .checked_mul(time_elapsed as i128) + .unwrap(); + let denominator = SECONDS_PER_YEAR + .checked_mul(MAX_BPS) + .unwrap() + .checked_sub((fee_rate as i128).checked_mul(time_elapsed as i128).unwrap()) + .unwrap(); + let fees = numerator.checked_div(denominator).unwrap(); + + Ok(fees) +} + +/// Collects and mints fees in dfTokens, distributing them to the appropriate fee receivers. +pub fn collect_fees(e: &Env) -> Result<(), ContractError> { + let current_timestamp = e.ledger().timestamp(); + // If last_fee_assesment was not set yet, this will be set to the current timestamp + let last_fee_assessment = get_last_fee_assesment(e); + + // Update the last fee assessment timestamp + // Set it now to Avoid Reentrancy Attack + set_last_fee_assesment(e, ¤t_timestamp); + + let time_elapsed = current_timestamp.checked_sub(last_fee_assessment).unwrap(); + + // If no time has passed since the last fee assessment, no fees are collected + if time_elapsed == 0 { + return Ok(()); + } + + // Fetch the individual fees for DeFindex and Vault, then calculate the total rate + let defindex_fee = fetch_defindex_fee(e); + let vault_fee = get_vault_fee(e); + let total_fee_rate = defindex_fee.checked_add(vault_fee).unwrap(); + + // Calculate the total fees in dfTokens based on the combined fee rate + let total_fees = calculate_fees(e, time_elapsed, total_fee_rate)?; + + // Mint and distribute the fees proportionally + mint_fees(e, total_fees, defindex_fee, vault_fee)?; + + + Ok(()) +} + +/// Mints dfTokens for fees and distributes them to the vault fee receiver and DeFindex receiver. +fn mint_fees(e: &Env, total_fees: i128, defindex_fee: u32, vault_fee: u32) -> Result<(), ContractError> { + let access_control = AccessControl::new(&e); + + let vault_fee_receiver = access_control.get_fee_receiver()?; + let defindex_protocol_receiver = get_defindex_protocol_fee_receiver(e); + + // Calculate shares for each receiver based on their fee proportion + let total_fee_bps = defindex_fee as i128 + vault_fee as i128; + let defindex_shares = (total_fees * defindex_fee as i128) / total_fee_bps; + let vault_shares = total_fees - defindex_shares; + + // Mint shares for both receivers + internal_mint(e.clone(), vault_fee_receiver.clone(), vault_shares); + internal_mint( + e.clone(), + defindex_protocol_receiver.clone(), + defindex_shares, + ); + + events::emit_fees_minted_event( + e, + defindex_protocol_receiver, + defindex_shares, + vault_fee_receiver, + vault_shares, + ); + Ok(()) +} diff --git a/apps/contracts/defindex/src/funds.rs b/apps/contracts/vault/src/funds.rs similarity index 96% rename from apps/contracts/defindex/src/funds.rs rename to apps/contracts/vault/src/funds.rs index 830ffa13..f02d879b 100644 --- a/apps/contracts/defindex/src/funds.rs +++ b/apps/contracts/vault/src/funds.rs @@ -5,28 +5,28 @@ use crate::models::AssetAllocation; use crate::storage::get_assets; use crate::strategies::get_strategy_client; -// Funds for AssetAllocation +// Funds for AssetAllocation /// Fetches the idle funds for a given asset. Idle funds refer to the balance of the asset /// that is currently not invested in any strategies. -/// +/// /// # Arguments /// * `e` - The current environment instance. /// * `asset` - The asset for which idle funds are being fetched. -/// +/// /// # Returns /// * The idle balance (i128) of the asset in the current contract address. fn fetch_idle_funds_for_asset(e: &Env, asset: &AssetAllocation) -> i128 { TokenClient::new(e, &asset.address).balance(&e.current_contract_address()) } -/// Fetches the total funds that are invested for a given asset. +/// Fetches the total funds that are invested for a given asset. /// It iterates through all the strategies associated with the asset and sums their balances. -/// +/// /// # Arguments /// * `e` - The current environment instance. /// * `asset` - The asset for which invested funds are being fetched. -/// +/// /// # Returns /// * The total invested balance (i128) of the asset across all strategies. pub fn fetch_invested_funds_for_strategy(e: &Env, strategy_address: &Address) -> i128 { @@ -34,13 +34,13 @@ pub fn fetch_invested_funds_for_strategy(e: &Env, strategy_address: &Address) -> strategy_client.balance(&e.current_contract_address()) } -/// Fetches the total funds that are invested for a given asset. +/// Fetches the total funds that are invested for a given asset. /// It iterates through all the strategies associated with the asset and sums their balances. -/// +/// /// # Arguments /// * `e` - The current environment instance. /// * `asset` - The asset for which invested funds are being fetched. -/// +/// /// # Returns /// * The total invested balance (i128) of the asset across all strategies. pub fn fetch_invested_funds_for_asset(e: &Env, asset: &AssetAllocation) -> i128 { @@ -51,15 +51,14 @@ pub fn fetch_invested_funds_for_asset(e: &Env, asset: &AssetAllocation) -> i128 invested_funds } - // Pub functions -/// Fetches the current idle funds for all assets managed by the contract. +/// Fetches the current idle funds for all assets managed by the contract. /// It returns a map where the key is the asset's address and the value is the idle balance. -/// +/// /// # Arguments /// * `e` - The current environment instance. -/// +/// /// # Returns /// * A map where each entry represents an asset's address and its corresponding idle balance. pub fn fetch_current_idle_funds(e: &Env) -> Map { @@ -71,12 +70,12 @@ pub fn fetch_current_idle_funds(e: &Env) -> Map { map } -/// Fetches the current invested funds for all assets managed by the contract. +/// Fetches the current invested funds for all assets managed by the contract. /// It returns a map where the key is the asset's address and the value is the invested balance. -/// +/// /// # Arguments /// * `e` - The current environment instance. -/// +/// /// # Returns /// * A map where each entry represents an asset's address and its corresponding invested balance. pub fn fetch_current_invested_funds(e: &Env) -> Map { @@ -92,12 +91,12 @@ pub fn fetch_current_invested_funds(e: &Env) -> Map { } /// Fetches the total managed funds for all assets. This includes both idle and invested funds. -/// It returns a map where the key is the asset's address and the value is the total managed balance +/// It returns a map where the key is the asset's address and the value is the total managed balance /// (idle + invested). With this map we can calculate the current managed funds ratio. -/// +/// /// # Arguments /// * `e` - The current environment instance. -/// +/// /// # Returns /// * A map where each entry represents an asset's address and its total managed balance. pub fn fetch_total_managed_funds(e: &Env) -> Map { diff --git a/apps/contracts/defindex/src/interface.rs b/apps/contracts/vault/src/interface.rs similarity index 68% rename from apps/contracts/defindex/src/interface.rs rename to apps/contracts/vault/src/interface.rs index 9dc76739..4e9a11c0 100644 --- a/apps/contracts/defindex/src/interface.rs +++ b/apps/contracts/vault/src/interface.rs @@ -6,57 +6,79 @@ use crate::{ }; pub trait VaultTrait { - /// Initializes the DeFindex Vault contract with the required parameters. /// - /// This function sets the roles for emergency manager, fee receiver, and manager. + /// This function sets the roles for manager, emergency manager, vault fee receiver, and manager. /// It also stores the list of assets to be managed by the vault, including strategies for each asset. - /// - /// # Arguments: - /// * `e` - The environment. - /// * `assets` - A vector of `AssetAllocation` structs representing the assets and their associated strategies. - /// * `manager` - The address responsible for managing the vault. - /// * `emergency_manager` - The address with emergency control over the vault. - /// * `fee_receiver` - The address that will receive fees from the vault. - /// * `vault_share` - The percentage of the vault's fees that will be sent to the DeFindex receiver. in BPS. - /// * `defindex_receiver` - The address that will receive fees for DeFindex from the vault. - /// * `factory` - The address of the factory that deployed the vault. /// - /// # Returns: - /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. + /// # Arguments + /// - `assets`: List of asset allocations for the vault, including strategies associated with each asset. + /// - `manager`: Primary vault manager with permissions for vault control. + /// - `emergency_manager`: Address with emergency access for emergency control over the vault. + /// - `vault_fee_receiver`: Address designated to receive the vault fee receiver's portion of management fees. + /// - `vault_fee`: Vault-specific fee percentage in basis points (typically set at 0-2% APR). + /// - `defindex_protocol_receiver`: Address receiving DeFindex’s protocol-wide fee in basis points (0.5% APR). + /// - `factory`: Factory contract address for deployment linkage. + /// - `vault_name`: Name of the vault token to be displayed in metadata. + /// - `vault_symbol`: Symbol representing the vault’s token. + /// + /// # Returns + /// - `Result<(), ContractError>`: Returns `Ok(())` if initialization succeeds, or a `ContractError` if + /// any setup fails (e.g., strategy mismatch with asset). + /// + /// # Errors + /// - `ContractError::AlreadyInitialized`: If the vault has already been initialized. + /// - `ContractError::StrategyDoesNotSupportAsset`: If a strategy within an asset does not support the asset’s contract. + /// fn initialize( e: Env, assets: Vec, manager: Address, emergency_manager: Address, - fee_receiver: Address, - vault_share: u32, - defindex_receiver: Address, + vault_fee_receiver: Address, + vault_fee: u32, + defindex_protocol_receiver: Address, factory: Address, vault_name: String, vault_symbol: String, ) -> Result<(), ContractError>; - /// Handles deposits into the DeFindex Vault. + /// Handles user deposits into the DeFindex Vault. /// - /// This function transfers the desired amounts of each asset into the vault, distributes the assets - /// across the strategies according to the vault's ratios, and mints dfTokens representing the user's - /// share in the vault. + /// This function processes a deposit by transferring each specified asset amount from the user's address to + /// the vault, allocating assets according to the vault's defined strategy ratios, and minting dfTokens that + /// represent the user's proportional share in the vault. The `amounts_desired` and `amounts_min` vectors should + /// align with the vault's asset order to ensure correct allocation. /// - /// # Arguments: - /// * `e` - The environment. - /// * `amounts_desired` - A vector of the amounts the user wishes to deposit for each asset. - /// * `amounts_min` - A vector of minimum amounts required for the deposit to proceed. + /// # Parameters + /// * `e` - The current environment reference (`Env`), for access to the contract state and utilities. + /// * `amounts_desired` - A vector specifying the user's intended deposit amounts for each asset. + /// * `amounts_min` - A vector of minimum deposit amounts required for the transaction to proceed. /// * `from` - The address of the user making the deposit. /// - /// # Returns: - /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. + /// # Returns + /// * `Result<(Vec, i128), ContractError>` - Returns the actual deposited `amounts` and `shares_to_mint` if successful, + /// otherwise a `ContractError`. + /// + /// # Function Flow + /// 1. **Fee Collection**: Collects accrued fees before processing the deposit. + /// 2. **Validation**: Checks that the lengths of `amounts_desired` and `amounts_min` match the vault's assets. + /// 3. **Share Calculation**: Calculates `shares_to_mint` based on the vault's total managed funds and the deposit amount. + /// 4. **Asset Transfer**: Transfers each specified amount from the user’s address to the vault as idle funds. + /// 5. **dfToken Minting**: Mints new dfTokens for the user to represent their ownership in the vault. + /// + /// # Notes + /// - For the first deposit, if the vault has only one asset, shares are calculated directly based on the deposit amount. + /// - For multiple assets, the function delegates to `calculate_deposit_amounts_and_shares_to_mint` + /// for precise share computation. + /// - An event is emitted to log the deposit, including the actual deposited amounts and minted shares. + /// fn deposit( e: Env, amounts_desired: Vec, amounts_min: Vec, from: Address, - ) -> Result<(), ContractError>; + ) -> Result<(Vec, i128), ContractError>; /// Withdraws assets from the DeFindex Vault by burning dfTokens. /// @@ -86,7 +108,11 @@ pub trait VaultTrait { /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. - fn emergency_withdraw(e: Env, strategy_address: Address, caller: Address) -> Result<(), ContractError>; + fn emergency_withdraw( + e: Env, + strategy_address: Address, + caller: Address, + ) -> Result<(), ContractError>; /// Pauses a strategy to prevent it from being used in the vault. /// @@ -100,7 +126,11 @@ pub trait VaultTrait { /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. - fn pause_strategy(e: Env, strategy_address: Address, caller: Address) -> Result<(), ContractError>; + fn pause_strategy( + e: Env, + strategy_address: Address, + caller: Address, + ) -> Result<(), ContractError>; /// Unpauses a previously paused strategy. /// @@ -114,7 +144,11 @@ pub trait VaultTrait { /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. - fn unpause_strategy(e: Env, strategy_address: Address, caller: Address) -> Result<(), ContractError>; + fn unpause_strategy( + e: Env, + strategy_address: Address, + caller: Address, + ) -> Result<(), ContractError>; /// Retrieves the list of assets managed by the DeFindex Vault. /// @@ -136,7 +170,7 @@ pub trait VaultTrait { /// # Returns: /// * `Map` - A map of asset addresses to their total managed amounts. fn fetch_total_managed_funds(e: &Env) -> Map; - + /// Returns the current invested funds, representing the total assets allocated to strategies. /// /// This function provides a map where the key is the asset address and the value is the total amount @@ -164,7 +198,6 @@ pub trait VaultTrait { // TODO: DELETE THIS, USED FOR TESTING /// Temporary method for testing purposes. fn get_asset_amounts_for_dftokens(e: Env, df_token: i128) -> Map; - } pub trait AdminInterfaceTrait { @@ -175,7 +208,7 @@ pub trait AdminInterfaceTrait { /// # Arguments: /// * `e` - The environment. /// * `caller` - The address initiating the change (must be the manager or emergency manager). - /// * `fee_receiver` - The new fee receiver address. + /// * `vault_fee_receiver` - The new fee receiver address. /// /// # Returns: /// * `()` - No return value. @@ -234,25 +267,24 @@ pub trait AdminInterfaceTrait { } pub trait VaultManagementTrait { - /// Invests the vault's idle funds into the specified strategies. - /// + /// /// # Arguments: /// * `e` - The environment. /// * `investment` - A vector of `Investment` structs representing the amount to invest in each strategy. /// * `caller` - The address of the caller. - /// + /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. fn invest(e: Env, investment: Vec) -> Result<(), ContractError>; /// Rebalances the vault by executing a series of instructions. - /// + /// /// # Arguments: /// * `e` - The environment. /// * `instructions` - A vector of `Instruction` structs representing actions (withdraw, invest, swap, zapper) to be taken. - /// + /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. fn rebalance(e: Env, instructions: Vec) -> Result<(), ContractError>; -} \ No newline at end of file +} diff --git a/apps/contracts/vault/src/investment.rs b/apps/contracts/vault/src/investment.rs new file mode 100644 index 00000000..889a41e8 --- /dev/null +++ b/apps/contracts/vault/src/investment.rs @@ -0,0 +1,54 @@ +use soroban_sdk::{Address, Env, Map, Vec}; + +use crate::{ + models::Investment, + strategies::{get_strategy_asset, invest_in_strategy}, + utils::check_nonnegative_amount, + ContractError, +}; + +pub fn prepare_investment( + e: &Env, + investments: Vec, + idle_funds: Map, +) -> Result, ContractError> { + let mut total_investment_per_asset: Map = Map::new(e); + + for investment in investments.iter() { + let strategy_address = &investment.strategy; + let amount_to_invest = investment.amount; + check_nonnegative_amount(amount_to_invest.clone())?; + + // Find the corresponding asset for the strategy + let asset = get_strategy_asset(&e, strategy_address)?; + + // Track investment per asset + let current_investment = total_investment_per_asset + .get(asset.address.clone()) + .unwrap_or(0); + let updated_investment = current_investment + .checked_add(amount_to_invest) + .ok_or(ContractError::Overflow)?; + + total_investment_per_asset.set(asset.address.clone(), updated_investment); + + // Check if total investment exceeds idle funds + let idle_balance = idle_funds.get(asset.address.clone()).unwrap_or(0); + if updated_investment > idle_balance { + return Err(ContractError::NotEnoughIdleFunds); + } + } + + Ok(total_investment_per_asset) +} + +pub fn execute_investment(e: &Env, investments: Vec) -> Result<(), ContractError> { + for investment in investments.iter() { + let strategy_address = &investment.strategy; + let amount_to_invest = &investment.amount; + + invest_in_strategy(e, strategy_address, amount_to_invest)? + } + + Ok(()) +} diff --git a/apps/contracts/defindex/src/lib.rs b/apps/contracts/vault/src/lib.rs similarity index 67% rename from apps/contracts/defindex/src/lib.rs rename to apps/contracts/vault/src/lib.rs index 044b1470..6f45c7c1 100755 --- a/apps/contracts/defindex/src/lib.rs +++ b/apps/contracts/vault/src/lib.rs @@ -1,9 +1,8 @@ #![no_std] -use aggregator::{internal_swap_exact_tokens_for_tokens, internal_swap_tokens_for_exact_tokens}; -use fee::collect_fees; -use investment::{execute_investment, prepare_investment}; use soroban_sdk::{ - contract, contractimpl, panic_with_error, token::{TokenClient, TokenInterface}, Address, Env, Map, String, Vec + contract, contractimpl, panic_with_error, + token::{TokenClient, TokenInterface}, + Address, Env, Map, String, Vec, }; use soroban_token_sdk::metadata::TokenMetadata; @@ -24,18 +23,32 @@ mod token; mod utils; use access::{AccessControl, AccessControlTrait, RolesDataKey}; +use aggregator::{internal_swap_exact_tokens_for_tokens, internal_swap_tokens_for_exact_tokens}; +use fee::collect_fees; use funds::{fetch_current_idle_funds, fetch_current_invested_funds, fetch_total_managed_funds}; -use interface::{AdminInterfaceTrait, VaultTrait, VaultManagementTrait}; -use models::{ActionType, AssetAllocation, Instruction, Investment, OptionalSwapDetailsExactIn, OptionalSwapDetailsExactOut}; +use interface::{AdminInterfaceTrait, VaultManagementTrait, VaultTrait}; +use investment::{execute_investment, prepare_investment}; +use models::{ + ActionType, AssetAllocation, Instruction, Investment, OptionalSwapDetailsExactIn, + OptionalSwapDetailsExactOut, +}; use storage::{ - get_assets, set_asset, set_defindex_receiver, set_factory, set_last_fee_assesment, set_total_assets, set_vault_share + get_assets, set_asset, set_defindex_protocol_fee_receiver, set_factory, + set_total_assets, set_vault_fee, extend_instance_ttl }; -use strategies::{get_asset_allocation_from_address, get_strategy_asset, get_strategy_client, get_strategy_struct, invest_in_strategy, pause_strategy, unpause_strategy, withdraw_from_strategy}; -use token::{internal_mint, internal_burn, write_metadata, VaultToken}; +use strategies::{ + get_asset_allocation_from_address, get_strategy_asset, get_strategy_client, + get_strategy_struct, invest_in_strategy, pause_strategy, unpause_strategy, + withdraw_from_strategy, +}; +use token::{internal_burn, internal_mint, write_metadata, VaultToken}; use utils::{ - calculate_asset_amounts_for_dftokens, calculate_deposit_amounts_and_shares_to_mint, calculate_withdrawal_amounts, check_initialized, check_nonnegative_amount + calculate_asset_amounts_for_dftokens, calculate_deposit_amounts_and_shares_to_mint, + calculate_withdrawal_amounts, check_initialized, check_nonnegative_amount, }; +use defindex_strategy_core::DeFindexStrategyClient; + pub use error::ContractError; #[contract] @@ -45,64 +58,78 @@ pub struct DeFindexVault; impl VaultTrait for DeFindexVault { /// Initializes the DeFindex Vault contract with the required parameters. /// - /// This function sets the roles for emergency manager, fee receiver, and manager. + /// This function sets the roles for manager, emergency manager, vault fee receiver, and manager. /// It also stores the list of assets to be managed by the vault, including strategies for each asset. - /// - /// # Arguments: - /// * `e` - The environment. - /// * `assets` - A vector of `AssetAllocation` structs representing the assets and their associated strategies. - /// * `manager` - The address responsible for managing the vault. - /// * `emergency_manager` - The address with emergency control over the vault. - /// * `fee_receiver` - The address that will receive fees from the vault. - /// * `vault_share` - The percentage of the vault's fees that will be sent to the DeFindex receiver. in BPS. - /// * `defindex_receiver` - The address that will receive fees for DeFindex from the vault. - /// * `factory` - The address of the factory that deployed the vault. /// - /// # Returns: - /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. + /// # Arguments + /// - `assets`: List of asset allocations for the vault, including strategies associated with each asset. + /// - `manager`: Primary vault manager with permissions for vault control. + /// - `emergency_manager`: Address with emergency access for emergency control over the vault. + /// - `vault_fee_receiver`: Address designated to receive the vault fee receiver's portion of management fees. + /// - `vault_fee`: Vault-specific fee percentage in basis points (typically set at 0-2% APR). + /// - `defindex_protocol_receiver`: Address receiving DeFindex’s protocol-wide fee in basis points (0.5% APR). + /// - `factory`: Factory contract address for deployment linkage. + /// - `vault_name`: Name of the vault token to be displayed in metadata. + /// - `vault_symbol`: Symbol representing the vault’s token. + /// + /// # Returns + /// - `Result<(), ContractError>`: Returns `Ok(())` if initialization succeeds, or a `ContractError` if + /// any setup fails (e.g., strategy mismatch with asset). + /// + /// # Errors + /// - `ContractError::AlreadyInitialized`: If the vault has already been initialized. + /// - `ContractError::StrategyDoesNotSupportAsset`: If a strategy within an asset does not support the asset’s contract. + /// fn initialize( e: Env, assets: Vec, manager: Address, emergency_manager: Address, - fee_receiver: Address, - vault_share: u32, - defindex_receiver: Address, + vault_fee_receiver: Address, + vault_fee: u32, + defindex_protocol_receiver: Address, factory: Address, vault_name: String, vault_symbol: String, ) -> Result<(), ContractError> { + extend_instance_ttl(&e); + let access_control = AccessControl::new(&e); if access_control.has_role(&RolesDataKey::Manager) { panic_with_error!(&e, ContractError::AlreadyInitialized); } access_control.set_role(&RolesDataKey::EmergencyManager, &emergency_manager); - access_control.set_role(&RolesDataKey::FeeReceiver, &fee_receiver); + access_control.set_role(&RolesDataKey::VaultFeeReceiver, &vault_fee_receiver); access_control.set_role(&RolesDataKey::Manager, &manager); - // Set Vault Share (in basis points) - set_vault_share(&e, &vault_share); + // Set Vault Fee (in basis points) + set_vault_fee(&e, &vault_fee); // Set Paltalabs Fee Receiver - set_defindex_receiver(&e, &defindex_receiver); + set_defindex_protocol_fee_receiver(&e, &defindex_protocol_receiver); // Set the factory address set_factory(&e, &factory); // Store Assets Objects let total_assets = assets.len(); + + // fails if the total assets is 0 + if total_assets == 0 { + panic_with_error!(&e, ContractError::NoAssetAllocation); + } + set_total_assets(&e, total_assets as u32); for (i, asset) in assets.iter().enumerate() { - // for every asset, we need to check that the list of strategyes indeed support this asset - - // TODO Fix, currently failing - // for strategy in asset.strategies.iter() { - // let strategy_client = DeFindexStrategyClient::new(&e, &strategy.address); - // if strategy_client.asset() != asset.address { - // panic_with_error!(&e, ContractError::StrategyDoesNotSupportAsset); - // } - // } + // for every asset, we need to check that the list of strategies indeed support this asset + + for strategy in asset.strategies.iter() { + let strategy_client = DeFindexStrategyClient::new(&e, &strategy.address); + if strategy_client.asset() != asset.address { + panic_with_error!(&e, ContractError::StrategyDoesNotSupportAsset); + } + } set_asset(&e, i as u32, &asset); } @@ -119,45 +146,72 @@ impl VaultTrait for DeFindexVault { }, ); - events::emit_initialized_vault(&e, emergency_manager, fee_receiver, manager, defindex_receiver, assets); + events::emit_initialized_vault( + &e, + emergency_manager, + vault_fee_receiver, + manager, + defindex_protocol_receiver, + assets, + ); Ok(()) } - /// Handles deposits into the DeFindex Vault. + /// Handles user deposits into the DeFindex Vault. /// - /// This function transfers the desired amounts of each asset into the vault, distributes the assets - /// across the strategies according to the vault's ratios, and mints dfTokens representing the user's - /// share in the vault. + /// This function processes a deposit by transferring each specified asset amount from the user's address to + /// the vault, allocating assets according to the vault's defined strategy ratios, and minting dfTokens that + /// represent the user's proportional share in the vault. The `amounts_desired` and `amounts_min` vectors should + /// align with the vault's asset order to ensure correct allocation. /// - /// # Arguments: - /// * `e` - The environment. - /// * `amounts_desired` - A vector of the amounts the user wishes to deposit for each asset. - /// * `amounts_min` - A vector of minimum amounts required for the deposit to proceed. + /// # Parameters + /// * `e` - The current environment reference (`Env`), for access to the contract state and utilities. + /// * `amounts_desired` - A vector specifying the user's intended deposit amounts for each asset. + /// * `amounts_min` - A vector of minimum deposit amounts required for the transaction to proceed. /// * `from` - The address of the user making the deposit. /// - /// # Returns: - /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. + /// # Returns + /// * `Result<(Vec, i128), ContractError>` - Returns the actual deposited `amounts` and `shares_to_mint` if successful, + /// otherwise a `ContractError`. + /// + /// # Function Flow + /// 1. **Fee Collection**: Collects accrued fees before processing the deposit. + /// 2. **Validation**: Checks that the lengths of `amounts_desired` and `amounts_min` match the vault's assets. + /// 3. **Share Calculation**: Calculates `shares_to_mint` based on the vault's total managed funds and the deposit amount. + /// 4. **Asset Transfer**: Transfers each specified amount from the user’s address to the vault as idle funds. + /// 5. **Vault shares Minting**: Mints vault shares for the user to represent their ownership in the vault. + /// + /// # Notes + /// - For the first deposit, if the vault has only one asset, shares are calculated directly based on the deposit amount. + /// - For multiple assets, the function delegates to `calculate_deposit_amounts_and_shares_to_mint` + /// for precise share computation. + /// - An event is emitted to log the deposit, including the actual deposited amounts and minted shares. + /// fn deposit( e: Env, amounts_desired: Vec, amounts_min: Vec, from: Address, - ) -> Result<(), ContractError> { + ) -> Result<(Vec, i128), ContractError> { + extend_instance_ttl(&e); check_initialized(&e)?; from.require_auth(); - // Set LastFeeAssessment if it is the first deposit - if VaultToken::total_supply(e.clone())==0{ - set_last_fee_assesment(&e, &e.ledger().timestamp()); - } + // Collect Fees + // If this was not done before, last_fee_assesment will set to be current timestamp and this will return without action + collect_fees(&e)?; + + // fees assesment + collect_fees(&e)?; // get assets let assets = get_assets(&e); - // assets lenght should be equal to amounts_desired and amounts_min length let assets_length = assets.len(); + + // assets lenght should be equal to amounts_desired and amounts_min length if assets_length != amounts_desired.len() || assets_length != amounts_min.len() { - panic_with_error!(&e, ContractError::WrongAmuntsLength); + panic_with_error!(&e, ContractError::WrongAmountsLength); } // for every amount desired, check non negative @@ -166,48 +220,66 @@ impl VaultTrait for DeFindexVault { } // for amount min is not necesary to check if it is negative + let total_supply = VaultToken::total_supply(e.clone()); let (amounts, shares_to_mint) = if assets_length == 1 { - // If Total Assets == 1 - let shares = if VaultToken::total_supply(e.clone())==0{ + let shares = if total_supply == 0 { + // If we have only one asset, and this is the first deposit, we will mint a share proportional to the amount desired // TODO In this case we might also want to mint a MINIMUM LIQUIDITY to be locked forever in the contract // this might be for security and practical reasons as well // shares will be equal to the amount desired to deposit, just for simplicity amounts_desired.get(0).unwrap() // here we have already check same lenght - } else{ - // in this case we will mint a share proportional to the total managed funds + } else { + // If we have only one asset, but we already have some shares minted + // we will mint a share proportional to the total managed funds + // read whitepaper! let total_managed_funds = fetch_total_managed_funds(&e); - VaultToken::total_supply(e.clone()) * amounts_desired.get(0).unwrap() / total_managed_funds.get(assets.get(0).unwrap().address.clone()).unwrap() + // if checked mul gives error, return ArithmeticError + VaultToken::total_supply(e.clone()).checked_mul(amounts_desired.get(0) + .unwrap()).unwrap_or_else(|| panic_with_error!(&e, ContractError::ArithmeticError)) + .checked_div(total_managed_funds.get(assets.get(0).unwrap().address.clone()) + .unwrap()).unwrap_or_else(|| panic_with_error!(&e, ContractError::ArithmeticError)) }; + // TODO check that min amount is ok (amounts_desired, shares) } else { - // If Total Assets > 1 - calculate_deposit_amounts_and_shares_to_mint( - &e, - &assets, - &amounts_desired, - &amounts_min, - ) + if total_supply == 0 { + // for ths first supply, we will consider the amounts desired, and the shares to mint will just be the sum + // of the amounts desired + (amounts_desired.clone(), amounts_desired.iter().sum()) + } + else { + // If Total Assets > 1 + calculate_deposit_amounts_and_shares_to_mint( + &e, + &assets, + &amounts_desired, + &amounts_min, + )? + } }; // for every asset for (i, amount) in amounts.iter().enumerate() { + // if amount is less than minimum, return error InsufficientAmount + if amount < amounts_min.get(i as u32).unwrap() { + panic_with_error!(&e, ContractError::InsufficientAmount); + } + // its possible that some amounts are 0. if amount > 0 { let asset = assets.get(i as u32).unwrap(); let asset_client = TokenClient::new(&e, &asset.address); - // send the current amount to this contract + // send the current amount to this contract. This will be held as idle funds. asset_client.transfer(&from, &e.current_contract_address(), &amount); } } - // now we mint the corresponding dfTOkenb + // now we mint the corresponding dfToken + // TODO. If total_sypply==0, mint minimum liquidity to be locked forever in the contract internal_mint(e.clone(), from.clone(), shares_to_mint); - events::emit_deposit_event(&e, from, amounts, shares_to_mint); + events::emit_deposit_event(&e, from, amounts.clone(), shares_to_mint.clone()); - // fees assesment - collect_fees(&e)?; - // TODO return amounts and shares to mint - Ok(()) + Ok((amounts, shares_to_mint)) } /// Withdraws assets from the DeFindex Vault by burning dfTokens. @@ -224,62 +296,69 @@ impl VaultTrait for DeFindexVault { /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. fn withdraw(e: Env, df_amount: i128, from: Address) -> Result, ContractError> { + extend_instance_ttl(&e); check_initialized(&e)?; check_nonnegative_amount(df_amount)?; from.require_auth(); + + // fees assesment + collect_fees(&e)?; // Check if the user has enough dfTokens let df_user_balance = VaultToken::balance(e.clone(), from.clone()); if df_user_balance < df_amount { return Err(ContractError::InsufficientBalance); } - + // Calculate the withdrawal amounts for each asset based on the dfToken amount let asset_amounts = calculate_asset_amounts_for_dftokens(&e, df_amount); // Burn the dfTokens after calculating the withdrawal amounts (so total supply is correct) internal_burn(e.clone(), from.clone(), df_amount); - + // Create a map to store the total amounts to transfer for each asset address let mut total_amounts_to_transfer: Map = Map::new(&e); - + // Get idle funds for each asset (Map) let idle_funds = fetch_current_idle_funds(&e); - + // Loop through each asset and handle the withdrawal for (asset_address, required_amount) in asset_amounts.iter() { // Check idle funds for this asset let idle_balance = idle_funds.get(asset_address.clone()).unwrap_or(0); let mut remaining_amount = required_amount; - + // Withdraw as much as possible from idle funds first if idle_balance > 0 { if idle_balance >= required_amount { // Idle funds cover the full amount total_amounts_to_transfer.set(asset_address.clone(), required_amount); - continue; // No need to withdraw from the strategy + continue; // No need to withdraw from the strategy } else { // Partial withdrawal from idle funds total_amounts_to_transfer.set(asset_address.clone(), idle_balance); - remaining_amount = required_amount - idle_balance; // Update remaining amount + remaining_amount = required_amount - idle_balance; // Update remaining amount } } - + // Find the corresponding asset address for this strategy let asset_allocation = get_asset_allocation_from_address(&e, asset_address.clone())?; - let withdrawal_amounts = calculate_withdrawal_amounts(&e, remaining_amount, asset_allocation); + let withdrawal_amounts = + calculate_withdrawal_amounts(&e, remaining_amount, asset_allocation); for (strategy_address, amount) in withdrawal_amounts.iter() { // TODO: What if the withdraw method exceeds the instructions limit? since im trying to ithdraw from all strategies of all assets... withdraw_from_strategy(&e, &strategy_address, &amount)?; - + // Update the total amounts to transfer map - let current_amount = total_amounts_to_transfer.get(strategy_address.clone()).unwrap_or(0); + let current_amount = total_amounts_to_transfer + .get(strategy_address.clone()) + .unwrap_or(0); total_amounts_to_transfer.set(asset_address.clone(), current_amount + amount); } } - + // Perform the transfers for the total amounts let mut amounts_withdrawn: Vec = Vec::new(&e); for (asset_address, total_amount) in total_amounts_to_transfer.iter() { @@ -292,9 +371,6 @@ impl VaultTrait for DeFindexVault { } events::emit_withdraw_event(&e, from, df_amount, amounts_withdrawn.clone()); - - // fees assesment - collect_fees(&e)?; Ok(amounts_withdrawn) } @@ -312,31 +388,39 @@ impl VaultTrait for DeFindexVault { /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. - fn emergency_withdraw(e: Env, strategy_address: Address, caller: Address) -> Result<(), ContractError> { + fn emergency_withdraw( + e: Env, + strategy_address: Address, + caller: Address, + ) -> Result<(), ContractError> { + extend_instance_ttl(&e); check_initialized(&e)?; - + // Ensure the caller is the Manager or Emergency Manager let access_control = AccessControl::new(&e); - access_control.require_any_role(&[RolesDataKey::EmergencyManager, RolesDataKey::Manager], &caller); - + access_control.require_any_role( + &[RolesDataKey::EmergencyManager, RolesDataKey::Manager], + &caller, + ); + // Find the strategy and its associated asset let asset = get_strategy_asset(&e, &strategy_address)?; // This ensures that the vault has this strategy in its list of assets let strategy = get_strategy_struct(&strategy_address, &asset)?; - + // Withdraw all assets from the strategy let strategy_client = get_strategy_client(&e, strategy.address.clone()); let strategy_balance = strategy_client.balance(&e.current_contract_address()); - + if strategy_balance > 0 { strategy_client.withdraw(&strategy_balance, &e.current_contract_address()); //TODO: Should we check if the idle funds are corresponding to the strategy balance withdrawed? } - + // Pause the strategy pause_strategy(&e, strategy_address.clone())?; - + events::emit_emergency_withdraw_event(&e, caller, strategy_address, strategy_balance); Ok(()) } @@ -353,17 +437,25 @@ impl VaultTrait for DeFindexVault { /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. - fn pause_strategy(e: Env, strategy_address: Address, caller: Address) -> Result<(), ContractError> { + fn pause_strategy( + e: Env, + strategy_address: Address, + caller: Address, + ) -> Result<(), ContractError> { + extend_instance_ttl(&e); // Ensure the caller is the Manager or Emergency Manager // TODO: Should check if the strategy has any amount invested on it, and return an error if it has, should we let the manager to pause a strategy with funds invested? let access_control = AccessControl::new(&e); - access_control.require_any_role(&[RolesDataKey::EmergencyManager, RolesDataKey::Manager], &caller); + access_control.require_any_role( + &[RolesDataKey::EmergencyManager, RolesDataKey::Manager], + &caller, + ); pause_strategy(&e, strategy_address.clone())?; events::emit_strategy_paused_event(&e, strategy_address, caller); Ok(()) } - + /// Unpauses a previously paused strategy. /// /// This function unpauses a strategy by setting its `paused` field to `false`, allowing it to be used @@ -376,10 +468,18 @@ impl VaultTrait for DeFindexVault { /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. - fn unpause_strategy(e: Env, strategy_address: Address, caller: Address) -> Result<(), ContractError> { + fn unpause_strategy( + e: Env, + strategy_address: Address, + caller: Address, + ) -> Result<(), ContractError> { + extend_instance_ttl(&e); // Ensure the caller is the Manager or Emergency Manager let access_control = AccessControl::new(&e); - access_control.require_any_role(&[RolesDataKey::EmergencyManager, RolesDataKey::Manager], &caller); + access_control.require_any_role( + &[RolesDataKey::EmergencyManager, RolesDataKey::Manager], + &caller, + ); unpause_strategy(&e, strategy_address.clone())?; events::emit_strategy_unpaused_event(&e, strategy_address, caller); @@ -394,6 +494,7 @@ impl VaultTrait for DeFindexVault { /// # Returns: /// * `Vec` - A vector of `AssetAllocation` structs representing the assets managed by the vault. fn get_assets(e: Env) -> Vec { + extend_instance_ttl(&e); get_assets(&e) } @@ -408,6 +509,7 @@ impl VaultTrait for DeFindexVault { /// # Returns: /// * `Map` - A map of asset addresses to their total managed amounts. fn fetch_total_managed_funds(e: &Env) -> Map { + extend_instance_ttl(&e); fetch_total_managed_funds(e) } @@ -422,6 +524,7 @@ impl VaultTrait for DeFindexVault { /// # Returns: /// * `Map` - A map of asset addresses to their total invested amounts. fn fetch_current_invested_funds(e: &Env) -> Map { + extend_instance_ttl(&e); fetch_current_invested_funds(e) } @@ -436,12 +539,14 @@ impl VaultTrait for DeFindexVault { /// # Returns: /// * `Map` - A map of asset addresses to their total idle amounts. fn fetch_current_idle_funds(e: &Env) -> Map { + extend_instance_ttl(&e); fetch_current_idle_funds(e) } // TODO: DELETE THIS, USED FOR TESTING /// Temporary method for testing purposes. fn get_asset_amounts_for_dftokens(e: Env, df_tokens: i128) -> Map { + extend_instance_ttl(&e); calculate_asset_amounts_for_dftokens(&e, df_tokens) } } @@ -455,11 +560,12 @@ impl AdminInterfaceTrait for DeFindexVault { /// # Arguments: /// * `e` - The environment. /// * `caller` - The address initiating the change (must be the manager or emergency manager). - /// * `fee_receiver` - The new fee receiver address. + /// * `vault_fee_receiver` - The new fee receiver address. /// /// # Returns: /// * `()` - No return value. fn set_fee_receiver(e: Env, caller: Address, new_fee_receiver: Address) { + extend_instance_ttl(&e); let access_control = AccessControl::new(&e); access_control.set_fee_receiver(&caller, &new_fee_receiver); @@ -474,6 +580,7 @@ impl AdminInterfaceTrait for DeFindexVault { /// # Returns: /// * `Result` - The fee receiver address if successful, otherwise returns a ContractError. fn get_fee_receiver(e: Env) -> Result { + extend_instance_ttl(&e); let access_control = AccessControl::new(&e); access_control.get_fee_receiver() } @@ -489,6 +596,7 @@ impl AdminInterfaceTrait for DeFindexVault { /// # Returns: /// * `()` - No return value. fn set_manager(e: Env, manager: Address) { + extend_instance_ttl(&e); let access_control = AccessControl::new(&e); access_control.set_manager(&manager); @@ -503,6 +611,7 @@ impl AdminInterfaceTrait for DeFindexVault { /// # Returns: /// * `Result` - The manager address if successful, otherwise returns a ContractError. fn get_manager(e: Env) -> Result { + extend_instance_ttl(&e); let access_control = AccessControl::new(&e); access_control.get_manager() } @@ -518,6 +627,7 @@ impl AdminInterfaceTrait for DeFindexVault { /// # Returns: /// * `()` - No return value. fn set_emergency_manager(e: Env, emergency_manager: Address) { + extend_instance_ttl(&e); let access_control = AccessControl::new(&e); access_control.set_emergency_manager(&emergency_manager); @@ -540,35 +650,36 @@ impl AdminInterfaceTrait for DeFindexVault { #[contractimpl] impl VaultManagementTrait for DeFindexVault { /// Invests the vault's idle funds into the specified strategies. - /// + /// /// # Arguments: /// * `e` - The environment. /// * `investment` - A vector of `Investment` structs representing the amount to invest in each strategy. /// * `caller` - The address of the caller. - /// + /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. fn invest(e: Env, investments: Vec) -> Result<(), ContractError> { + extend_instance_ttl(&e); check_initialized(&e)?; - + let access_control = AccessControl::new(&e); access_control.require_role(&RolesDataKey::Manager); e.current_contract_address().require_auth(); - + // Get the current idle funds for all assets let idle_funds = fetch_current_idle_funds(&e); - + // Prepare investments based on current idle funds // This checks if the total investment exceeds the idle funds prepare_investment(&e, investments.clone(), idle_funds)?; - + // Now proceed with the actual investments if all checks passed execute_investment(&e, investments)?; // auto invest mockup // if auto_invest { // let idle_funds = fetch_current_idle_funds(&e); - + // // Prepare investments based on current ratios of invested funds // let investments = calculate_investments_based_on_ratios(&e); // prepare_investment(&e, investments.clone(), idle_funds)?; @@ -578,20 +689,21 @@ impl VaultManagementTrait for DeFindexVault { } /// Rebalances the vault by executing a series of instructions. - /// + /// /// # Arguments: /// * `e` - The environment. /// * `instructions` - A vector of `Instruction` structs representing actions (withdraw, invest, swap, zapper) to be taken. - /// + /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. fn rebalance(e: Env, instructions: Vec) -> Result<(), ContractError> { + extend_instance_ttl(&e); check_initialized(&e)?; - + let access_control = AccessControl::new(&e); access_control.require_role(&RolesDataKey::Manager); e.current_contract_address().require_auth(); - + for instruction in instructions.iter() { match instruction.action { ActionType::Withdraw => match (&instruction.strategy, &instruction.amount) { @@ -609,13 +721,13 @@ impl VaultManagementTrait for DeFindexVault { ActionType::SwapExactIn => match &instruction.swap_details_exact_in { OptionalSwapDetailsExactIn::Some(swap_details) => { internal_swap_exact_tokens_for_tokens( - &e, - &swap_details.token_in, - &swap_details.token_out, - &swap_details.amount_in, - &swap_details.amount_out_min, - &swap_details.distribution, - &swap_details.deadline + &e, + &swap_details.token_in, + &swap_details.token_out, + &swap_details.amount_in, + &swap_details.amount_out_min, + &swap_details.distribution, + &swap_details.deadline, )?; } _ => return Err(ContractError::MissingInstructionData), @@ -623,23 +735,23 @@ impl VaultManagementTrait for DeFindexVault { ActionType::SwapExactOut => match &instruction.swap_details_exact_out { OptionalSwapDetailsExactOut::Some(swap_details) => { internal_swap_tokens_for_exact_tokens( - &e, - &swap_details.token_in, - &swap_details.token_out, - &swap_details.amount_out, - &swap_details.amount_in_max, - &swap_details.distribution, - &swap_details.deadline + &e, + &swap_details.token_in, + &swap_details.token_out, + &swap_details.amount_out, + &swap_details.amount_in_max, + &swap_details.distribution, + &swap_details.deadline, )?; } _ => return Err(ContractError::MissingInstructionData), }, ActionType::Zapper => { // TODO: Implement Zapper instructions - }, + } } } - + Ok(()) } -} \ No newline at end of file +} diff --git a/apps/contracts/defindex/src/models.rs b/apps/contracts/vault/src/models.rs similarity index 99% rename from apps/contracts/defindex/src/models.rs rename to apps/contracts/vault/src/models.rs index e5c182bb..042ec976 100644 --- a/apps/contracts/defindex/src/models.rs +++ b/apps/contracts/vault/src/models.rs @@ -87,4 +87,4 @@ pub enum OptionalSwapDetailsExactIn { pub enum OptionalSwapDetailsExactOut { Some(SwapDetailsExactOut), None, -} \ No newline at end of file +} diff --git a/apps/contracts/defindex/src/storage.rs b/apps/contracts/vault/src/storage.rs similarity index 52% rename from apps/contracts/defindex/src/storage.rs rename to apps/contracts/vault/src/storage.rs index 525e2b81..fa580480 100644 --- a/apps/contracts/defindex/src/storage.rs +++ b/apps/contracts/vault/src/storage.rs @@ -2,24 +2,42 @@ use soroban_sdk::{contracttype, Address, Env, Vec}; use crate::models::AssetAllocation; +const DAY_IN_LEDGERS: u32 = 17280; +const INSTANCE_BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; +const INSTANCE_LIFETIME_THRESHOLD: u32 = INSTANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; + +pub fn extend_instance_ttl(e: &Env) { + e.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); +} + + #[derive(Clone)] #[contracttype] enum DataKey { - AssetAllocation(u32), // AssetAllocation Addresse by index - TotalAssets, // Total number of tokens - DeFindexReceiver, + AssetAllocation(u32), // AssetAllocation Addresse by index + TotalAssets, // Total number of tokens + DeFindexProtocolFeeReceiver, Factory, LastFeeAssessment, - VaultShare, + VaultFee, } + + // Assets Management pub fn set_asset(e: &Env, index: u32, asset: &AssetAllocation) { - e.storage().instance().set(&DataKey::AssetAllocation(index), asset); + e.storage() + .instance() + .set(&DataKey::AssetAllocation(index), asset); } pub fn get_asset(e: &Env, index: u32) -> AssetAllocation { - e.storage().instance().get(&DataKey::AssetAllocation(index)).unwrap() + e.storage() + .instance() + .get(&DataKey::AssetAllocation(index)) + .unwrap() } pub fn set_total_assets(e: &Env, n: u32) { @@ -40,31 +58,26 @@ pub fn get_assets(e: &Env) -> Vec { } // DeFindex Fee Receiver -pub fn set_defindex_receiver(e: &Env, address: &Address) { +pub fn set_defindex_protocol_fee_receiver(e: &Env, address: &Address) { e.storage() .instance() - .set(&DataKey::DeFindexReceiver, address); + .set(&DataKey::DeFindexProtocolFeeReceiver, address); } -pub fn get_defindex_receiver(e: &Env) -> Address { +pub fn get_defindex_protocol_fee_receiver(e: &Env) -> Address { e.storage() .instance() - .get(&DataKey::DeFindexReceiver) + .get(&DataKey::DeFindexProtocolFeeReceiver) .unwrap() } // DeFindex Factory pub fn set_factory(e: &Env, address: &Address) { - e.storage() - .instance() - .set(&DataKey::Factory, address); + e.storage().instance().set(&DataKey::Factory, address); } pub fn get_factory(e: &Env) -> Address { - e.storage() - .instance() - .get(&DataKey::Factory) - .unwrap() + e.storage().instance().get(&DataKey::Factory).unwrap() } // Last Fee Assesment @@ -78,19 +91,25 @@ pub fn get_last_fee_assesment(e: &Env) -> u64 { e.storage() .instance() .get(&DataKey::LastFeeAssessment) - .unwrap() + .unwrap_or_else(|| { + let timestamp = &e.ledger().timestamp(); + e.storage() + .instance() + .set(&DataKey::LastFeeAssessment, timestamp); + timestamp.clone() + }) } // Vault Share -pub fn set_vault_share(e: &Env, vault_share: &u32) { +pub fn set_vault_fee(e: &Env, vault_fee: &u32) { e.storage() .instance() - .set(&DataKey::VaultShare, vault_share); + .set(&DataKey::VaultFee, vault_fee); } -pub fn get_vault_share(e: &Env) -> u32 { +pub fn get_vault_fee(e: &Env) -> u32 { e.storage() .instance() - .get(&DataKey::VaultShare) + .get(&DataKey::VaultFee) .unwrap() } diff --git a/apps/contracts/defindex/src/strategies.rs b/apps/contracts/vault/src/strategies.rs similarity index 80% rename from apps/contracts/defindex/src/strategies.rs rename to apps/contracts/vault/src/strategies.rs index a1019745..cde32336 100644 --- a/apps/contracts/defindex/src/strategies.rs +++ b/apps/contracts/vault/src/strategies.rs @@ -1,18 +1,29 @@ use defindex_strategy_core::DeFindexStrategyClient; -use soroban_sdk::{Env, Address}; +use soroban_sdk::{Address, Env}; -use crate::{models::{AssetAllocation, Strategy}, storage::{get_asset, get_assets, get_total_assets, set_asset}, ContractError}; +use crate::{ + models::{AssetAllocation, Strategy}, + storage::{get_asset, get_assets, get_total_assets, set_asset}, + ContractError, +}; pub fn get_strategy_client(e: &Env, address: Address) -> DeFindexStrategyClient { DeFindexStrategyClient::new(&e, &address) } /// Finds the asset corresponding to the given strategy address. -pub fn get_strategy_asset(e: &Env, strategy_address: &Address) -> Result { +pub fn get_strategy_asset( + e: &Env, + strategy_address: &Address, +) -> Result { let assets = get_assets(e); for asset in assets.iter() { - if asset.strategies.iter().any(|strategy| &strategy.address == strategy_address) { + if asset + .strategies + .iter() + .any(|strategy| &strategy.address == strategy_address) + { return Ok(asset); } } @@ -21,7 +32,10 @@ pub fn get_strategy_asset(e: &Env, strategy_address: &Address) -> Result Result { +pub fn get_asset_allocation_from_address( + e: &Env, + asset_address: Address, +) -> Result { let assets = get_assets(e); for asset in assets.iter() { @@ -34,7 +48,10 @@ pub fn get_asset_allocation_from_address(e: &Env, asset_address: Address) -> Res } /// Finds the strategy struct corresponding to the given strategy address within the given asset. -pub fn get_strategy_struct(strategy_address: &Address, asset: &AssetAllocation) -> Result { +pub fn get_strategy_struct( + strategy_address: &Address, + asset: &AssetAllocation, +) -> Result { asset .strategies .iter() @@ -104,20 +121,28 @@ pub fn unpause_strategy(e: &Env, strategy_address: Address) -> Result<(), Contra Err(ContractError::StrategyNotFound) } -pub fn withdraw_from_strategy(e: &Env, strategy_address: &Address, amount: &i128) -> Result<(), ContractError> { +pub fn withdraw_from_strategy( + e: &Env, + strategy_address: &Address, + amount: &i128, +) -> Result<(), ContractError> { let strategy_client = get_strategy_client(e, strategy_address.clone()); - + match strategy_client.try_withdraw(amount, &e.current_contract_address()) { Ok(Ok(_)) => Ok(()), Ok(Err(_)) | Err(_) => Err(ContractError::StrategyWithdrawError), } } -pub fn invest_in_strategy(e: &Env, strategy_address: &Address, amount: &i128) -> Result<(), ContractError> { +pub fn invest_in_strategy( + e: &Env, + strategy_address: &Address, + amount: &i128, +) -> Result<(), ContractError> { let strategy_client = get_strategy_client(&e, strategy_address.clone()); - + match strategy_client.try_deposit(amount, &e.current_contract_address()) { Ok(Ok(_)) => Ok(()), Ok(Err(_)) | Err(_) => Err(ContractError::StrategyInvestError), } -} \ No newline at end of file +} diff --git a/apps/contracts/defindex/src/test.rs b/apps/contracts/vault/src/test.rs similarity index 67% rename from apps/contracts/defindex/src/test.rs rename to apps/contracts/vault/src/test.rs index b23401cd..77cc783f 100755 --- a/apps/contracts/defindex/src/test.rs +++ b/apps/contracts/vault/src/test.rs @@ -8,28 +8,30 @@ use std::vec; // DeFindex Hodl Strategy Contract pub mod hodl_strategy { - soroban_sdk::contractimport!(file = "../target/wasm32-unknown-unknown/release/hodl_strategy.optimized.wasm"); + soroban_sdk::contractimport!( + file = "../target/wasm32-unknown-unknown/release/hodl_strategy.optimized.wasm" + ); pub type HodlStrategyClient<'a> = Client<'a>; } -use hodl_strategy::{HodlStrategyClient}; +use hodl_strategy::HodlStrategyClient; -fn create_hodl_strategy<'a>(e: & Env, asset: & Address) -> HodlStrategyClient<'a> { +fn create_hodl_strategy<'a>(e: &Env, asset: &Address) -> HodlStrategyClient<'a> { let contract_address = &e.register_contract_wasm(None, hodl_strategy::WASM); - let hodl_strategy = HodlStrategyClient::new(e, contract_address); + let hodl_strategy = HodlStrategyClient::new(e, contract_address); hodl_strategy.initialize(&asset, &sorobanvec![&e]); hodl_strategy } -// DeFindex Vault Contract +// DeFindex Vault Contract pub mod defindex_vault { - soroban_sdk::contractimport!(file = "../target/wasm32-unknown-unknown/release/defindex_vault.optimized.wasm"); + soroban_sdk::contractimport!( + file = "../target/wasm32-unknown-unknown/release/defindex_vault.optimized.wasm" + ); pub type DeFindexVaultClient<'a> = Client<'a>; } use defindex_vault::{DeFindexVaultClient, Strategy}; -fn create_defindex_vault<'a>( - e: & Env -) -> DeFindexVaultClient<'a> { +fn create_defindex_vault<'a>(e: &Env) -> DeFindexVaultClient<'a> { let address = &e.register_contract_wasm(None, defindex_vault::WASM); let client = DeFindexVaultClient::new(e, address); client @@ -52,7 +54,11 @@ fn create_defindex_vault<'a>( // Create Test Token pub(crate) fn create_token_contract<'a>(e: &Env, admin: &Address) -> SorobanTokenClient<'a> { - SorobanTokenClient::new(e, &e.register_stellar_asset_contract_v2(admin.clone()).address()) + SorobanTokenClient::new( + e, + &e.register_stellar_asset_contract_v2(admin.clone()) + .address(), + ) } pub(crate) fn get_token_admin_client<'a>( @@ -62,12 +68,23 @@ pub(crate) fn get_token_admin_client<'a>( SorobanTokenAdminClient::new(e, address) } -pub(crate) fn create_strategy_params(test: &DeFindexVaultTest) -> Vec { +pub(crate) fn create_strategy_params_token0(test: &DeFindexVaultTest) -> Vec { sorobanvec![ &test.env, Strategy { name: String::from_str(&test.env, "Strategy 1"), - address: test.strategy_client.address.clone(), + address: test.strategy_client_token0.address.clone(), + paused: false, + } + ] +} + +pub(crate) fn create_strategy_params_token1(test: &DeFindexVaultTest) -> Vec { + sorobanvec![ + &test.env, + Strategy { + name: String::from_str(&test.env, "Strategy 1"), + address: test.strategy_client_token1.address.clone(), paused: false, } ] @@ -82,10 +99,11 @@ pub struct DeFindexVaultTest<'a> { token1_admin_client: SorobanTokenAdminClient<'a>, token1: SorobanTokenClient<'a>, emergency_manager: Address, - fee_receiver: Address, - defindex_receiver: Address, + vault_fee_receiver: Address, + defindex_protocol_receiver: Address, manager: Address, - strategy_client: HodlStrategyClient<'a>, + strategy_client_token0: HodlStrategyClient<'a>, + strategy_client_token1: HodlStrategyClient<'a>, } impl<'a> DeFindexVaultTest<'a> { @@ -99,8 +117,8 @@ impl<'a> DeFindexVaultTest<'a> { let defindex_contract = create_defindex_vault(&env); let emergency_manager = Address::generate(&env); - let fee_receiver = Address::generate(&env); - let defindex_receiver = Address::generate(&env); + let vault_fee_receiver = Address::generate(&env); + let defindex_protocol_receiver = Address::generate(&env); let manager = Address::generate(&env); let token0_admin = Address::generate(&env); @@ -114,8 +132,11 @@ impl<'a> DeFindexVaultTest<'a> { // token1_admin_client.mint(to, amount); - let strategy_client = create_hodl_strategy(&env, &token0.address); + let strategy_client_token0 = create_hodl_strategy(&env, &token0.address); + let strategy_client_token1 = create_hodl_strategy(&env, &token1.address); + env.budget().reset_unlimited(); + DeFindexVaultTest { env, defindex_factory, @@ -125,10 +146,11 @@ impl<'a> DeFindexVaultTest<'a> { token1_admin_client, token1, emergency_manager, - fee_receiver, - defindex_receiver, + vault_fee_receiver, + defindex_protocol_receiver, manager, - strategy_client, + strategy_client_token0, + strategy_client_token1, } } @@ -142,8 +164,8 @@ impl<'a> DeFindexVaultTest<'a> { } mod admin; -mod initialize; -mod withdraw; mod deposit; mod emergency_withdraw; -mod rebalance; \ No newline at end of file +mod initialize; +mod rebalance; +mod withdraw; diff --git a/apps/contracts/defindex/src/test/admin.rs b/apps/contracts/vault/src/test/admin.rs similarity index 75% rename from apps/contracts/defindex/src/test/admin.rs rename to apps/contracts/vault/src/test/admin.rs index 384694be..c7b64d08 100644 --- a/apps/contracts/defindex/src/test/admin.rs +++ b/apps/contracts/vault/src/test/admin.rs @@ -1,8 +1,12 @@ use soroban_sdk::{ - testutils::{AuthorizedFunction, AuthorizedInvocation, MockAuth, MockAuthInvoke}, vec as sorobanvec, IntoVal, String, Symbol, Vec + testutils::{AuthorizedFunction, AuthorizedInvocation, MockAuth, MockAuthInvoke}, + vec as sorobanvec, IntoVal, String, Symbol, Vec, }; -use crate::test::{create_strategy_params, defindex_vault::AssetAllocation, DeFindexVaultTest}; +use crate::test::{ + create_strategy_params_token0, create_strategy_params_token1, defindex_vault::AssetAllocation, + DeFindexVaultTest, +}; extern crate alloc; use alloc::vec; @@ -10,17 +14,18 @@ use alloc::vec; #[test] fn test_set_new_fee_receiver_by_fee_receiver() { let test = DeFindexVaultTest::setup(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); let assets: Vec = sorobanvec![ &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() }, AssetAllocation { address: test.token1.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token1.clone() } ]; @@ -28,41 +33,44 @@ fn test_set_new_fee_receiver_by_fee_receiver() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), ); let fee_receiver_role = test.defindex_contract.get_fee_receiver(); - assert_eq!(fee_receiver_role, test.fee_receiver); + assert_eq!(fee_receiver_role, test.vault_fee_receiver); let users = DeFindexVaultTest::generate_random_users(&test.env, 1); // Fee Receiver is setting the new fee receiver test.defindex_contract .mock_auths(&[MockAuth { - address: &test.fee_receiver, + address: &test.vault_fee_receiver, invoke: &MockAuthInvoke { contract: &test.defindex_contract.address.clone(), fn_name: "set_fee_receiver", - args: (&test.fee_receiver, &users[0]).into_val(&test.env), + args: (&test.vault_fee_receiver, &users[0]).into_val(&test.env), sub_invokes: &[], }, }]) - .set_fee_receiver(&test.fee_receiver, &users[0]); + .set_fee_receiver(&test.vault_fee_receiver, &users[0]); let expected_auth = AuthorizedInvocation { // Top-level authorized function is `deploy` with all the arguments. function: AuthorizedFunction::Contract(( test.defindex_contract.address.clone(), Symbol::new(&test.env, "set_fee_receiver"), - (&test.fee_receiver, users[0].clone()).into_val(&test.env), + (&test.vault_fee_receiver, users[0].clone()).into_val(&test.env), )), sub_invocations: vec![], }; - assert_eq!(test.env.auths(), vec![(test.fee_receiver, expected_auth)]); + assert_eq!( + test.env.auths(), + vec![(test.vault_fee_receiver, expected_auth)] + ); let new_fee_receiver_role = test.defindex_contract.get_fee_receiver(); assert_eq!(new_fee_receiver_role, users[0]); @@ -71,7 +79,8 @@ fn test_set_new_fee_receiver_by_fee_receiver() { #[test] fn test_set_new_fee_receiver_by_manager() { let test = DeFindexVaultTest::setup(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); // let tokens: Vec
= sorobanvec![&test.env, test.token0.address.clone(), test.token1.address.clone()]; // let ratios: Vec = sorobanvec![&test.env, 1, 1]; @@ -79,11 +88,11 @@ fn test_set_new_fee_receiver_by_manager() { &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() }, AssetAllocation { address: test.token1.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token1.clone() } ]; @@ -91,16 +100,16 @@ fn test_set_new_fee_receiver_by_manager() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), ); let fee_receiver_role = test.defindex_contract.get_fee_receiver(); - assert_eq!(fee_receiver_role, test.fee_receiver); + assert_eq!(fee_receiver_role, test.vault_fee_receiver); let users = DeFindexVaultTest::generate_random_users(&test.env, 1); // Now Manager is setting the new fee receiver @@ -135,7 +144,8 @@ fn test_set_new_fee_receiver_by_manager() { #[should_panic(expected = "HostError: Error(Contract, #130)")] // Unauthorized fn test_set_new_fee_receiver_by_emergency_manager() { let test = DeFindexVaultTest::setup(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); // let tokens: Vec
= sorobanvec![&test.env, test.token0.address.clone(), test.token1.address.clone()]; // let ratios: Vec = sorobanvec![&test.env, 1, 1]; @@ -143,11 +153,11 @@ fn test_set_new_fee_receiver_by_emergency_manager() { &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() }, AssetAllocation { address: test.token1.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token1.clone() } ]; @@ -155,16 +165,16 @@ fn test_set_new_fee_receiver_by_emergency_manager() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), ); let fee_receiver_role = test.defindex_contract.get_fee_receiver(); - assert_eq!(fee_receiver_role, test.fee_receiver); + assert_eq!(fee_receiver_role, test.vault_fee_receiver); let users = DeFindexVaultTest::generate_random_users(&test.env, 1); // Now Emergency Manager is setting the new fee receiver @@ -176,17 +186,18 @@ fn test_set_new_fee_receiver_by_emergency_manager() { #[should_panic(expected = "HostError: Error(Contract, #130)")] // Unauthorized fn test_set_new_fee_receiver_invalid_sender() { let test = DeFindexVaultTest::setup(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); let assets: Vec = sorobanvec![ &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() }, AssetAllocation { address: test.token1.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token1.clone() } ]; @@ -194,16 +205,16 @@ fn test_set_new_fee_receiver_invalid_sender() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), ); let fee_receiver_role = test.defindex_contract.get_fee_receiver(); - assert_eq!(fee_receiver_role, test.fee_receiver); + assert_eq!(fee_receiver_role, test.vault_fee_receiver); let users = DeFindexVaultTest::generate_random_users(&test.env, 1); // Trying to set the new fee receiver with an invalid sender @@ -214,16 +225,17 @@ fn test_set_new_fee_receiver_invalid_sender() { #[test] fn test_set_new_manager_by_manager() { let test = DeFindexVaultTest::setup(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); let assets: Vec = sorobanvec![ &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() }, AssetAllocation { address: test.token1.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token1.clone() } ]; @@ -231,9 +243,9 @@ fn test_set_new_manager_by_manager() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), diff --git a/apps/contracts/vault/src/test/deposit.rs b/apps/contracts/vault/src/test/deposit.rs new file mode 100644 index 00000000..35626a23 --- /dev/null +++ b/apps/contracts/vault/src/test/deposit.rs @@ -0,0 +1,701 @@ +use soroban_sdk::{vec as sorobanvec, String, Vec, Map}; + +use crate::test::defindex_vault::{AssetAllocation, ContractError}; +use crate::test::{ + create_strategy_params_token0, create_strategy_params_token1, DeFindexVaultTest, +}; + +// Test deposit not yet initialized +#[test] +fn test_deposit_not_yet_initialized() { + let test = DeFindexVaultTest::setup(); + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); + + let result = test.defindex_contract.try_deposit( + &sorobanvec![&test.env, 100i128], + &sorobanvec![&test.env, 100i128], + &users[0], + ); + + assert_eq!(result, Err(Ok(ContractError::NotInitialized))); +} + + +#[test] +fn deposit_amounts_desired_less_length() { + 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); + + // initialize with 2 assets + let assets: Vec = sorobanvec![ + &test.env, + AssetAllocation { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + }, + AssetAllocation { + 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 = 1000i128; + + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); + + let response = test.defindex_contract.try_deposit( + &sorobanvec![&test.env, amount], // wrong amount desired + &sorobanvec![&test.env, amount, amount], + &users[0], + ); + + assert_eq!(response, Err(Ok(ContractError::WrongAmountsLength))); +} + +// test deposit amount desired more length +#[test] +fn deposit_amounts_desired_more_length() { + 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); + + // initialize with 2 assets + let assets: Vec = sorobanvec![ + &test.env, + AssetAllocation { + 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"), + ); + let amount = 1000i128; + + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); + + let response = test.defindex_contract.try_deposit( + &sorobanvec![&test.env, amount, amount], // wrong amount desired + &sorobanvec![&test.env, amount], + &users[0], + ); + + assert_eq!(response, Err(Ok(ContractError::WrongAmountsLength))); +} + +// test deposit amount min less length +#[test] +fn deposit_amounts_min_less_length() { + 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); + + // initialize with 2 assets + let assets: Vec = sorobanvec![ + &test.env, + AssetAllocation { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + }, + AssetAllocation { + 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 = 1000i128; + + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); + + let response = test.defindex_contract.try_deposit( + &sorobanvec![&test.env, amount, amount], + &sorobanvec![&test.env, amount], // wrong amount min + &users[0], + ); + + assert_eq!(response, Err(Ok(ContractError::WrongAmountsLength))); +} + + +// test deposit amount min more length +#[test] +fn deposit_amounts_min_more_length() { + 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); + + // initialize with 2 assets + let assets: Vec = sorobanvec![ + &test.env, + AssetAllocation { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + }, + AssetAllocation { + 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 = 1000i128; + + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); + + let response = test.defindex_contract.try_deposit( + &sorobanvec![&test.env, amount, amount], + &sorobanvec![&test.env, amount, amount, amount], // wrong amount min + &users[0], + ); + + assert_eq!(response, Err(Ok(ContractError::WrongAmountsLength))); +} + +// test amount desired negative +#[test] +fn deposit_amounts_desired_negative() { + 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); + + // initialize with 2 assets + let assets: Vec = sorobanvec![ + &test.env, + AssetAllocation { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + }, + AssetAllocation { + 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 = 1000i128; + + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); + + let response = test.defindex_contract.try_deposit( + &sorobanvec![&test.env, -amount, amount], + &sorobanvec![&test.env, amount, amount], + &users[0], + ); + + assert_eq!(response, Err(Ok(ContractError::NegativeNotAllowed))); +} + +// test deposit one asset success +#[test] +fn deposit_one_asset_success() { + let test = DeFindexVaultTest::setup(); + test.env.mock_all_auths(); + let strategy_params_token0 = create_strategy_params_token0(&test); + + // initialize with 1 assets + let assets: Vec = sorobanvec![ + &test.env, + AssetAllocation { + 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"), + ); + let amount = 123456789i128; + + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); + + // Balances before deposit + test.token0_admin_client.mint(&users[0], &amount); + let user_balance = test.token0.balance(&users[0]); + assert_eq!(user_balance, amount); + + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, 0i128); + + // deposit + test.defindex_contract.deposit( + &sorobanvec![&test.env, amount], + &sorobanvec![&test.env, amount], + &users[0], + ); + + // check balances after deposit + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, amount); + + let user_balance = test.token0.balance(&users[0]); + assert_eq!(user_balance, 0i128); + + //map shuould be map + let mut expected_map = Map::new(&test.env); + expected_map.set(test.token0.address.clone(), amount); + + // check that all the assets are in the vault + let vault_balance = test.token0.balance(&test.defindex_contract.address); + assert_eq!(vault_balance, amount); + + // check that fetch_total_managed_funds returns correct amount + let total_managed_funds = test.defindex_contract.fetch_total_managed_funds(); + assert_eq!(total_managed_funds, expected_map); + + // check current idle funds, + let current_idle_funds = test.defindex_contract.fetch_current_idle_funds(); + assert_eq!(current_idle_funds, expected_map); + + //map shuould be map + let mut expected_map = Map::new(&test.env); + expected_map.set(test.token0.address.clone(), 0i128); + + // check that current invested funds is now 0, funds still in idle funds + let current_invested_funds = test.defindex_contract.fetch_current_invested_funds(); + assert_eq!(current_invested_funds, expected_map); + + // Now user deposits for the second time + let amount2 = 987654321i128; + test.token0_admin_client.mint(&users[0], &amount2); + let user_balance = test.token0.balance(&users[0]); + assert_eq!(user_balance, amount2); + + // deposit + test.defindex_contract.deposit( + &sorobanvec![&test.env, amount2], + &sorobanvec![&test.env, amount2], + &users[0], + ); + + //map shuould be map + let mut expected_map = Map::new(&test.env); + expected_map.set(test.token0.address.clone(), amount + amount2); + + // check balances after deposit + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, amount + amount2); + + let user_balance = test.token0.balance(&users[0]); + assert_eq!(user_balance, 0i128); + + // check that all the assets are in the vault + let vault_balance = test.token0.balance(&test.defindex_contract.address); + assert_eq!(vault_balance, amount + amount2); + + // check that fetch_total_managed_funds returns correct amount + let total_managed_funds = test.defindex_contract.fetch_total_managed_funds(); + assert_eq!(total_managed_funds, expected_map); + + // check current idle funds + let current_idle_funds = test.defindex_contract.fetch_current_idle_funds(); + assert_eq!(current_idle_funds, expected_map); + + + //map shuould be map + let mut expected_map = Map::new(&test.env); + expected_map.set(test.token0.address.clone(), 0i128); + + // check that current invested funds is now 0, funds still in idle funds + let current_invested_funds = test.defindex_contract.fetch_current_invested_funds(); + assert_eq!(current_invested_funds, expected_map); + + + +} + +// test deposit one asset with minimum more than desired +#[test] +fn deposit_one_asset_min_more_than_desired() { + let test = DeFindexVaultTest::setup(); + test.env.mock_all_auths(); + let strategy_params_token0 = create_strategy_params_token0(&test); + + // initialize with 1 assets + let assets: Vec = sorobanvec![ + &test.env, + AssetAllocation { + 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"), + ); + let amount = 123456789i128; + + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); + + // Balances before deposit + test.token0_admin_client.mint(&users[0], &amount); + let user_balance = test.token0.balance(&users[0]); + assert_eq!(user_balance, amount); + + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, 0i128); + + // deposit + let result=test.defindex_contract.try_deposit( + &sorobanvec![&test.env, amount], + &sorobanvec![&test.env, amount + 1], + &users[0], + ); + // this should fail + assert_eq!(result, Err(Ok(ContractError::InsufficientAmount))); + +} + +// test deposit of several asset, considering different proportion of assets +#[test] +fn deposit_several_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); + + // initialize with 2 assets + let assets: Vec = sorobanvec![ + &test.env, + AssetAllocation { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + }, + AssetAllocation { + 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 amount0 = 123456789i128; + let amount1 = 987654321i128; + + let users = DeFindexVaultTest::generate_random_users(&test.env, 2); + + // Balances before deposit + test.token0_admin_client.mint(&users[0], &amount0); + test.token1_admin_client.mint(&users[0], &amount1); + let user_balance0 = test.token0.balance(&users[0]); + assert_eq!(user_balance0, amount0); + let user_balance1 = test.token1.balance(&users[0]); + assert_eq!(user_balance1, amount1); + + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, 0i128); + + // deposit + let deposit_result=test.defindex_contract.deposit( + &sorobanvec![&test.env, amount0, amount1], + &sorobanvec![&test.env, amount0, amount1], + &users[0], + ); + + // check deposit result + assert_eq!(deposit_result, (sorobanvec![&test.env, amount0, amount1], amount0 + amount1)); + + // check balances after deposit + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, amount0 + amount1); + + let user_balance0 = test.token0.balance(&users[0]); + assert_eq!(user_balance0,0i128); + let user_balance1 = test.token1.balance(&users[0]); + assert_eq!(user_balance1,0i128); + + // check vault balance of asset 0 + let vault_balance0 = test.token0.balance(&test.defindex_contract.address); + assert_eq!(vault_balance0, amount0); + // check vault balance of asset 1 + let vault_balance1 = test.token1.balance(&test.defindex_contract.address); + assert_eq!(vault_balance1, amount1); + + //map shuould be map + let mut expected_map = Map::new(&test.env); + expected_map.set(test.token0.address.clone(), amount0); + expected_map.set(test.token1.address.clone(), amount1); + + // check that fetch_total_managed_funds returns correct amount + let total_managed_funds = test.defindex_contract.fetch_total_managed_funds(); + assert_eq!(total_managed_funds, expected_map); + + // check current idle funds + let current_idle_funds = test.defindex_contract.fetch_current_idle_funds(); + assert_eq!(current_idle_funds, expected_map); + + //map shuould be map + let mut expected_map = Map::new(&test.env); + expected_map.set(test.token0.address.clone(), 0i128); + expected_map.set(test.token1.address.clone(), 0i128); + + // check that current invested funds is now 0, funds still in idle funds + let current_invested_funds = test.defindex_contract.fetch_current_invested_funds(); + assert_eq!(current_invested_funds, expected_map); + + + // new user wants to do a deposit with more assets 0 than the proportion, but with minium amount 0 + // multiply amount0 by 2 + let amount0_new = amount0*2 +100 ; + let amount1_new = amount1*2; + + // mint this to user 1 + test.token0_admin_client.mint(&users[1], &amount0_new); + test.token1_admin_client.mint(&users[1], &amount1_new); + + // check user balances + let user_balance0 = test.token0.balance(&users[1]); + assert_eq!(user_balance0, amount0_new); + let user_balance1 = test.token1.balance(&users[1]); + assert_eq!(user_balance1, amount1_new); + + + // user 1 deposits + let deposit_result=test.defindex_contract.deposit( + &sorobanvec![&test.env, amount0_new, amount1_new], + &sorobanvec![&test.env, 0i128, 0i128], + &users[1], + ); + + // check deposit result. Ok((amounts, shares_to_mint)) + // Vec, i128 + + assert_eq!(deposit_result, (sorobanvec![&test.env, amount0*2, amount1*2], amount0*2 + amount1*2)); + + + // check balances after deposit + let df_balance = test.defindex_contract.balance(&users[1]); + assert_eq!(df_balance, 2*(amount0 + amount1)); + + let user_balance0 = test.token0.balance(&users[1]); + assert_eq!(user_balance0, amount0_new - 2*amount0); + + let user_balance1 = test.token1.balance(&users[1]); + assert_eq!(user_balance1, amount1_new - 2*amount1); + + // check vault balance of asset 0 + let vault_balance0 = test.token0.balance(&test.defindex_contract.address); + assert_eq!(vault_balance0, 3*amount0); + // check vault balance of asset 1 + let vault_balance1 = test.token1.balance(&test.defindex_contract.address); + assert_eq!(vault_balance1, 3*amount1); + + //map shuould be map + let mut expected_map = Map::new(&test.env); + expected_map.set(test.token0.address.clone(), 3*amount0); + expected_map.set(test.token1.address.clone(), 3*amount1); + + // check that fetch_total_managed_funds returns correct amount + let total_managed_funds = test.defindex_contract.fetch_total_managed_funds(); + assert_eq!(total_managed_funds, expected_map); + + // check current idle funds + let current_idle_funds = test.defindex_contract.fetch_current_idle_funds(); + assert_eq!(current_idle_funds, expected_map); + + //map shuould be map + let mut expected_map = Map::new(&test.env); + expected_map.set(test.token0.address.clone(), 0i128); + expected_map.set(test.token1.address.clone(), 0i128); + + // check that current invested funds is now 0, funds still in idle funds + let current_invested_funds = test.defindex_contract.fetch_current_invested_funds(); + assert_eq!(current_invested_funds, expected_map); + + // we will repeat one more time, now enforcing the first asset + let amount0_new = amount0*2; + let amount1_new = amount1*2+100; + + // mint this to user 1 + test.token0_admin_client.mint(&users[1], &amount0_new); + test.token1_admin_client.mint(&users[1], &amount1_new); + + // check user balances + let user_balance0 = test.token0.balance(&users[1]); + assert_eq!(user_balance0, 100 + amount0_new); // we still have 100 from before + let user_balance1 = test.token1.balance(&users[1]); + assert_eq!(user_balance1, amount1_new); + + // user 1 deposits + let deposit_result=test.defindex_contract.deposit( + &sorobanvec![&test.env, amount0_new, amount1_new], + &sorobanvec![&test.env, 0i128, 0i128], + &users[1], + ); + + // check deposit result. Ok((amounts, shares_to_mint)) + // Vec, i128 + assert_eq!(deposit_result, (sorobanvec![&test.env, amount0*2, amount1*2], amount0*2 + amount1*2)); + +} + +// test deposit of several asset, imposing a minimum amount greater than optimal for asset 0 +#[test] +fn deposit_several_assets_min_greater_than_optimal() { + 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); + + // initialize with 2 assets + let assets: Vec = sorobanvec![ + &test.env, + AssetAllocation { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + }, + AssetAllocation { + 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 amount0 = 123456789i128; + let amount1 = 987654321i128; + + let users = DeFindexVaultTest::generate_random_users(&test.env, 2); + + // Balances before deposit + test.token0_admin_client.mint(&users[0], &amount0); + test.token1_admin_client.mint(&users[0], &amount1); + let user_balance0 = test.token0.balance(&users[0]); + assert_eq!(user_balance0, amount0); + let user_balance1 = test.token1.balance(&users[0]); + assert_eq!(user_balance1, amount1); + + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, 0i128); + + // deposit + let deposit_result=test.defindex_contract.try_deposit( + &sorobanvec![&test.env, amount0, amount1], + &sorobanvec![&test.env, amount0 + 1, amount1], + &users[0], + ); + + // this should fail + assert_eq!(deposit_result, Err(Ok(ContractError::InsufficientAmount))); + + // now we manage to deposit + test.defindex_contract.deposit( + &sorobanvec![&test.env, amount0, amount1], + &sorobanvec![&test.env, amount0, amount1], + &users[0], + ); + + // check deposit result + + // and now will try again with minimum more than optimal + + // new user wants to do a deposit with more assets 0 than the proportion, but with minium amount 0 + // multiply amount0 by 2 + let amount0_new = amount0*2 +100 ; + let amount1_new = amount1*2; + + // mint this to user + test.token0_admin_client.mint(&users[0], &amount0_new); + test.token1_admin_client.mint(&users[0], &amount1_new); + + let deposit_result=test.defindex_contract.try_deposit( + &sorobanvec![&test.env, amount0_new, amount1_new], + &sorobanvec![&test.env, amount0*2+1, amount1*2], + &users[0], + ); + + // this should fail + + assert_eq!(deposit_result, Err(Ok(ContractError::InsufficientAmount))); + +} \ No newline at end of file diff --git a/apps/contracts/defindex/src/test/emergency_withdraw.rs b/apps/contracts/vault/src/test/emergency_withdraw.rs similarity index 70% rename from apps/contracts/defindex/src/test/emergency_withdraw.rs rename to apps/contracts/vault/src/test/emergency_withdraw.rs index 1d2757f6..fee60772 100644 --- a/apps/contracts/defindex/src/test/emergency_withdraw.rs +++ b/apps/contracts/vault/src/test/emergency_withdraw.rs @@ -1,17 +1,21 @@ use soroban_sdk::{vec as sorobanvec, String, Vec}; -use crate::test::{create_strategy_params, defindex_vault::{AssetAllocation, Investment}, DeFindexVaultTest}; +use crate::test::{ + create_strategy_params_token0, + defindex_vault::{AssetAllocation, Investment}, + DeFindexVaultTest, +}; #[test] fn test_emergency_withdraw_success() { let test = DeFindexVaultTest::setup(); test.env.mock_all_auths(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); let assets: Vec = sorobanvec![ &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() } ]; @@ -19,26 +23,30 @@ fn test_emergency_withdraw_success() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), ); let amount = 1000i128; - + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); - + test.token0_admin_client.mint(&users[0], &amount); let user_balance = test.token0.balance(&users[0]); assert_eq!(user_balance, amount); let df_balance = test.defindex_contract.balance(&users[0]); assert_eq!(df_balance, 0i128); - + // Deposit - test.defindex_contract.deposit(&sorobanvec![&test.env, amount], &sorobanvec![&test.env, amount], &users[0]); + test.defindex_contract.deposit( + &sorobanvec![&test.env, amount], + &sorobanvec![&test.env, amount], + &users[0], + ); let df_balance = test.defindex_contract.balance(&users[0]); assert_eq!(df_balance, amount); @@ -52,23 +60,30 @@ fn test_emergency_withdraw_success() { &test.env, Investment { amount: amount.clone(), - strategy: strategy_params.first().unwrap().address.clone() + strategy: strategy_params_token0.first().unwrap().address.clone() } ]; test.defindex_contract.invest(&investments); - + // Balance of the token0 on the vault should be 0 let vault_balance_of_token = test.token0.balance(&test.defindex_contract.address); assert_eq!(vault_balance_of_token, 0); // Balance of the strategy should be `amount` - let strategy_balance = test.strategy_client.balance(&test.defindex_contract.address); + let strategy_balance = test + .strategy_client_token0 + .balance(&test.defindex_contract.address); assert_eq!(strategy_balance, amount); - test.defindex_contract.emergency_withdraw(&strategy_params.first().unwrap().address, &test.emergency_manager); + test.defindex_contract.emergency_withdraw( + &strategy_params_token0.first().unwrap().address, + &test.emergency_manager, + ); // Balance of the strategy should be 0 - let strategy_balance = test.strategy_client.balance(&test.defindex_contract.address); + let strategy_balance = test + .strategy_client_token0 + .balance(&test.defindex_contract.address); assert_eq!(strategy_balance, 0); // Balance of the token0 on the vault should be `amount` @@ -78,4 +93,4 @@ fn test_emergency_withdraw_success() { // check if strategy is paused let asset = test.defindex_contract.get_assets().first().unwrap(); assert_eq!(asset.strategies.first().unwrap().paused, true); -} \ No newline at end of file +} diff --git a/apps/contracts/defindex/src/test/initialize.rs b/apps/contracts/vault/src/test/initialize.rs similarity index 51% rename from apps/contracts/defindex/src/test/initialize.rs rename to apps/contracts/vault/src/test/initialize.rs index 68075239..9c9e5fa0 100644 --- a/apps/contracts/defindex/src/test/initialize.rs +++ b/apps/contracts/vault/src/test/initialize.rs @@ -1,20 +1,26 @@ use soroban_sdk::{vec as sorobanvec, String, Vec}; -use crate::test::{create_strategy_params, defindex_vault::{AssetAllocation, ContractError}, DeFindexVaultTest}; +use crate::test::{ + create_strategy_params_token0, create_strategy_params_token1, + defindex_vault::{AssetAllocation, ContractError}, + DeFindexVaultTest, +}; + #[test] fn test_initialize_and_get_roles() { let test = DeFindexVaultTest::setup(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); let assets: Vec = sorobanvec![ &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() }, AssetAllocation { address: test.token1.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token1.clone() } ]; @@ -22,9 +28,9 @@ fn test_initialize_and_get_roles() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), @@ -35,10 +41,70 @@ fn test_initialize_and_get_roles() { let emergency_manager_role = test.defindex_contract.get_emergency_manager(); assert_eq!(manager_role, test.manager); - assert_eq!(fee_receiver_role, test.fee_receiver); + assert_eq!(fee_receiver_role, test.vault_fee_receiver); assert_eq!(emergency_manager_role, test.emergency_manager); } + +// Test that if strategy does support other asset we get an error when initializing +#[test] +fn test_initialize_with_unsupported_strategy() { + let test = DeFindexVaultTest::setup(); + let strategy_params_token0 = create_strategy_params_token0(&test); + + let assets: Vec = sorobanvec![ + &test.env, + AssetAllocation { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + }, + AssetAllocation { + address: test.token1.address.clone(), + strategies: strategy_params_token0.clone() // Here Strategy 0 supports token0 + } + ]; + + let result = test.defindex_contract.try_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"), + ); + + assert_eq!( + result, + Err(Ok(ContractError::StrategyDoesNotSupportAsset)) + ); +} + +// test that if we try to initialize with an empty asset allocation fails +#[test] +fn test_initialize_with_empty_asset_allocation() { + let test = DeFindexVaultTest::setup(); + // let strategy_params_token0 = create_strategy_params_token0(&test); + + let assets: Vec = sorobanvec![&test.env]; + + let result = test.defindex_contract.try_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"), + ); + + assert_eq!(result, Err(Ok(ContractError::NoAssetAllocation))); +} + #[test] fn test_get_roles_not_yet_initialized() { let test = DeFindexVaultTest::setup(); @@ -54,17 +120,18 @@ fn test_get_roles_not_yet_initialized() { #[test] fn test_initialize_twice() { let test = DeFindexVaultTest::setup(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); let assets: Vec = sorobanvec![ &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() }, AssetAllocation { address: test.token1.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token1.clone() } ]; @@ -72,9 +139,9 @@ fn test_initialize_twice() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), @@ -84,9 +151,9 @@ fn test_initialize_twice() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), @@ -98,17 +165,6 @@ fn test_initialize_twice() { ); } -// TODO finish DEPOSIT when ready -// #[test] -// fn test_deposit_not_yet_initialized() { -// let test = DeFindexVaultTest::setup(); -// let users = DeFindexVaultTest::generate_random_users(&test.env, 1); - -// let result = test.defindex_contract.try_deposit(&100i128, &users[0]); - -// assert_eq!(result, Err(Ok(ContractError::NotInitialized))); -// } - #[test] fn test_withdraw_not_yet_initialized() { let test = DeFindexVaultTest::setup(); @@ -123,10 +179,10 @@ fn test_emergency_withdraw_not_yet_initialized() { let test = DeFindexVaultTest::setup(); let users = DeFindexVaultTest::generate_random_users(&test.env, 1); - let strategy_params = create_strategy_params(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); let result = test .defindex_contract - .try_emergency_withdraw(&strategy_params.first().unwrap().address, &users[0]); + .try_emergency_withdraw(&strategy_params_token1.first().unwrap().address, &users[0]); assert_eq!(result, Err(Ok(ContractError::NotInitialized))); } diff --git a/apps/contracts/defindex/src/test/rebalance.rs b/apps/contracts/vault/src/test/rebalance.rs similarity index 71% rename from apps/contracts/defindex/src/test/rebalance.rs rename to apps/contracts/vault/src/test/rebalance.rs index 49e7bbd3..5a39c049 100644 --- a/apps/contracts/defindex/src/test/rebalance.rs +++ b/apps/contracts/vault/src/test/rebalance.rs @@ -1,17 +1,24 @@ use soroban_sdk::{vec as sorobanvec, String, Vec}; -use crate::test::{create_strategy_params, defindex_vault::{ActionType, AssetAllocation, Instruction, Investment, OptionalSwapDetailsExactIn, OptionalSwapDetailsExactOut}, DeFindexVaultTest}; +use crate::test::{ + create_strategy_params_token0, + defindex_vault::{ + ActionType, AssetAllocation, Instruction, Investment, OptionalSwapDetailsExactIn, + OptionalSwapDetailsExactOut, + }, + DeFindexVaultTest, +}; #[test] fn rebalance() { let test = DeFindexVaultTest::setup(); test.env.mock_all_auths(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); let assets: Vec = sorobanvec![ &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() } ]; @@ -19,17 +26,17 @@ fn rebalance() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), ); let amount = 1000i128; - + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); - + test.token0_admin_client.mint(&users[0], &amount); let user_balance = test.token0.balance(&users[0]); assert_eq!(user_balance, amount); @@ -37,19 +44,23 @@ fn rebalance() { let df_balance = test.defindex_contract.balance(&users[0]); assert_eq!(df_balance, 0i128); - test.defindex_contract.deposit(&sorobanvec![&test.env, amount], &sorobanvec![&test.env, amount], &users[0]); + test.defindex_contract.deposit( + &sorobanvec![&test.env, amount], + &sorobanvec![&test.env, amount], + &users[0], + ); let df_balance = test.defindex_contract.balance(&users[0]); assert_eq!(df_balance, amount); let investments = sorobanvec![ - &test.env, + &test.env, Investment { - amount: amount, - strategy: test.strategy_client.address.clone() + amount: amount, + strategy: test.strategy_client_token0.address.clone() } ]; - + test.defindex_contract.invest(&investments); let vault_balance = test.token0.balance(&test.defindex_contract.address); @@ -60,19 +71,18 @@ fn rebalance() { let instruction_amount_0 = 200i128; let instruction_amount_1 = 100i128; - let instructions = sorobanvec![ - &test.env, + &test.env, Instruction { action: ActionType::Withdraw, - strategy: Some(test.strategy_client.address.clone()), + strategy: Some(test.strategy_client_token0.address.clone()), amount: Some(instruction_amount_0), swap_details_exact_in: OptionalSwapDetailsExactIn::None, swap_details_exact_out: OptionalSwapDetailsExactOut::None, }, Instruction { action: ActionType::Invest, - strategy: Some(test.strategy_client.address.clone()), + strategy: Some(test.strategy_client_token0.address.clone()), amount: Some(instruction_amount_1), swap_details_exact_in: OptionalSwapDetailsExactIn::None, swap_details_exact_out: OptionalSwapDetailsExactOut::None, @@ -83,5 +93,4 @@ fn rebalance() { let vault_balance = test.token0.balance(&test.defindex_contract.address); assert_eq!(vault_balance, instruction_amount_1); - -} \ No newline at end of file +} diff --git a/apps/contracts/defindex/src/test/withdraw.rs b/apps/contracts/vault/src/test/withdraw.rs similarity index 77% rename from apps/contracts/defindex/src/test/withdraw.rs rename to apps/contracts/vault/src/test/withdraw.rs index 2079a843..7080de89 100644 --- a/apps/contracts/defindex/src/test/withdraw.rs +++ b/apps/contracts/vault/src/test/withdraw.rs @@ -1,18 +1,22 @@ use soroban_sdk::{vec as sorobanvec, String, Vec}; -use crate::test::{create_strategy_params, defindex_vault::{AssetAllocation, Investment}, DeFindexVaultTest}; -use super::hodl_strategy::StrategyError; +// use super::hodl_strategy::StrategyError; +use crate::test::{ + create_strategy_params_token0, + defindex_vault::{AssetAllocation, Investment}, + DeFindexVaultTest, +}; #[test] fn test_withdraw_from_idle_success() { let test = DeFindexVaultTest::setup(); test.env.mock_all_auths(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); let assets: Vec = sorobanvec![ &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() } ]; @@ -20,17 +24,17 @@ fn test_withdraw_from_idle_success() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &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); let user_balance = test.token0.balance(&users[0]); assert_eq!(user_balance, amount); @@ -41,14 +45,18 @@ fn test_withdraw_from_idle_success() { // Deposit let amount_to_deposit = 567890i128; - test.defindex_contract.deposit(&sorobanvec![&test.env, amount_to_deposit], &sorobanvec![&test.env, amount_to_deposit], &users[0]); + test.defindex_contract.deposit( + &sorobanvec![&test.env, amount_to_deposit], + &sorobanvec![&test.env, amount_to_deposit], + &users[0], + ); // Check Balances after deposit - + // Token balance of user let user_balance = test.token0.balance(&users[0]); assert_eq!(user_balance, amount - amount_to_deposit); - + // Token balance of vault should be amount_to_deposit // Because balances are still in indle, balances are not in strategy, but in idle @@ -56,7 +64,7 @@ fn test_withdraw_from_idle_success() { assert_eq!(vault_balance, amount_to_deposit); // Token balance of hodl strategy should be 0 (all in idle) - let strategy_balance = test.token0.balance(&test.strategy_client.address); + 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 @@ -65,37 +73,43 @@ fn test_withdraw_from_idle_success() { // user decides to withdraw a portion of deposited amount let amount_to_withdraw = 123456i128; - test.defindex_contract.withdraw(&amount_to_withdraw, &users[0]); - + test.defindex_contract + .withdraw(&amount_to_withdraw, &users[0]); + // Check Balances after withdraw // Token balance of user should be amount - amount_to_deposit + amount_to_withdraw let user_balance = test.token0.balance(&users[0]); - assert_eq!(user_balance, amount - amount_to_deposit + amount_to_withdraw); + assert_eq!( + user_balance, + amount - amount_to_deposit + amount_to_withdraw + ); // Token balance of vault should be amount_to_deposit - amount_to_withdraw let vault_balance = test.token0.balance(&test.defindex_contract.address); assert_eq!(vault_balance, amount_to_deposit - amount_to_withdraw); // Token balance of hodl strategy should be 0 (all in idle) - let strategy_balance = test.token0.balance(&test.strategy_client.address); + 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 - amount_to_withdraw let df_balance = test.defindex_contract.balance(&users[0]); assert_eq!(df_balance, amount_to_deposit - amount_to_withdraw); - // user tries to withdraw more than deposited amount let amount_to_withdraw_more = amount_to_deposit + 1; - let result = test.defindex_contract.try_withdraw(&amount_to_withdraw_more, &users[0]); + let result = test + .defindex_contract + .try_withdraw(&amount_to_withdraw_more, &users[0]); // just check if is error assert_eq!(result.is_err(), true); - + // TODO test corresponding error // withdraw remaining balance - test.defindex_contract.withdraw(&(amount_to_deposit - amount_to_withdraw), &users[0]); + test.defindex_contract + .withdraw(&(amount_to_deposit - amount_to_withdraw), &users[0]); // // result is err @@ -114,12 +128,12 @@ fn test_withdraw_from_idle_success() { fn test_withdraw_from_strategy_success() { let test = DeFindexVaultTest::setup(); test.env.mock_all_auths(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); let assets: Vec = sorobanvec![ &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() } ]; @@ -127,17 +141,17 @@ fn test_withdraw_from_strategy_success() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), ); let amount = 1000i128; - + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); - + test.token0_admin_client.mint(&users[0], &amount); let user_balance = test.token0.balance(&users[0]); assert_eq!(user_balance, amount); @@ -146,29 +160,33 @@ fn test_withdraw_from_strategy_success() { let df_balance = test.defindex_contract.balance(&users[0]); assert_eq!(df_balance, 0i128); - test.defindex_contract.deposit(&sorobanvec![&test.env, amount], &sorobanvec![&test.env, amount], &users[0]); + test.defindex_contract.deposit( + &sorobanvec![&test.env, amount], + &sorobanvec![&test.env, amount], + &users[0], + ); let df_balance = test.defindex_contract.balance(&users[0]); assert_eq!(df_balance, amount); let investments = sorobanvec![ - &test.env, + &test.env, Investment { - amount: amount, - strategy: test.strategy_client.address.clone() - }]; - - + amount: amount, + strategy: test.strategy_client_token0.address.clone() + } + ]; + test.defindex_contract.invest(&investments); let vault_balance = test.token0.balance(&test.defindex_contract.address); assert_eq!(vault_balance, 0); test.defindex_contract.withdraw(&df_balance, &users[0]); - + let df_balance = test.defindex_contract.balance(&users[0]); assert_eq!(df_balance, 0i128); let user_balance = test.token0.balance(&users[0]); assert_eq!(user_balance, amount); -} \ No newline at end of file +} diff --git a/apps/contracts/defindex/src/token/allowance.rs b/apps/contracts/vault/src/token/allowance.rs similarity index 100% rename from apps/contracts/defindex/src/token/allowance.rs rename to apps/contracts/vault/src/token/allowance.rs diff --git a/apps/contracts/defindex/src/token/balance.rs b/apps/contracts/vault/src/token/balance.rs similarity index 100% rename from apps/contracts/defindex/src/token/balance.rs rename to apps/contracts/vault/src/token/balance.rs diff --git a/apps/contracts/defindex/src/token/contract.rs b/apps/contracts/vault/src/token/contract.rs similarity index 100% rename from apps/contracts/defindex/src/token/contract.rs rename to apps/contracts/vault/src/token/contract.rs diff --git a/apps/contracts/defindex/src/token/metadata.rs b/apps/contracts/vault/src/token/metadata.rs similarity index 100% rename from apps/contracts/defindex/src/token/metadata.rs rename to apps/contracts/vault/src/token/metadata.rs diff --git a/apps/contracts/defindex/src/token/mod.rs b/apps/contracts/vault/src/token/mod.rs similarity index 85% rename from apps/contracts/defindex/src/token/mod.rs rename to apps/contracts/vault/src/token/mod.rs index 77d0ace3..869bda67 100644 --- a/apps/contracts/defindex/src/token/mod.rs +++ b/apps/contracts/vault/src/token/mod.rs @@ -8,6 +8,6 @@ mod storage_types; mod total_supply; pub use contract::VaultToken; -pub use contract::VaultTokenClient; +// pub use contract::VaultTokenClient; pub use contract::{internal_burn, internal_mint}; pub use metadata::write_metadata; diff --git a/apps/contracts/defindex/src/token/storage_types.rs b/apps/contracts/vault/src/token/storage_types.rs similarity index 100% rename from apps/contracts/defindex/src/token/storage_types.rs rename to apps/contracts/vault/src/token/storage_types.rs diff --git a/apps/contracts/defindex/src/token/total_supply.rs b/apps/contracts/vault/src/token/total_supply.rs similarity index 100% rename from apps/contracts/defindex/src/token/total_supply.rs rename to apps/contracts/vault/src/token/total_supply.rs diff --git a/apps/contracts/vault/src/utils.rs b/apps/contracts/vault/src/utils.rs new file mode 100644 index 00000000..fd893167 --- /dev/null +++ b/apps/contracts/vault/src/utils.rs @@ -0,0 +1,242 @@ +use soroban_sdk::{panic_with_error, Address, Env, Map, Vec}; + +use crate::{ + access::{AccessControl, AccessControlTrait, RolesDataKey}, + funds::{ + fetch_invested_funds_for_asset, fetch_invested_funds_for_strategy, + fetch_total_managed_funds, + }, + models::AssetAllocation, + token::VaultToken, + ContractError, +}; + +pub const DAY_IN_LEDGERS: u32 = 17280; + +pub fn bump_instance(e: &Env) { + let max_ttl = e.storage().max_ttl(); + e.storage() + .instance() + .extend_ttl(max_ttl - DAY_IN_LEDGERS, max_ttl); +} + +pub fn check_initialized(e: &Env) -> Result<(), ContractError> { + //TODO: Should also check if adapters/strategies have been set + let access_control = AccessControl::new(&e); + if access_control.has_role(&RolesDataKey::Manager) { + Ok(()) + } else { + panic_with_error!(&e, ContractError::NotInitialized); + } +} + +pub fn check_nonnegative_amount(amount: i128) -> Result<(), ContractError> { + if amount < 0 { + Err(ContractError::NegativeNotAllowed) + } else { + Ok(()) + } +} + +/// From an amount, calculates how much to withdraw from each strategy; +/// returns a map of strategy address to token amount +pub fn calculate_withdrawal_amounts( + e: &Env, + amount: i128, + asset: AssetAllocation, +) -> Map { + let mut withdrawal_amounts = Map::::new(e); + + let total_invested_in_strategies: i128 = fetch_invested_funds_for_asset(&e, &asset); + + for strategy in asset.strategies.iter() { + // TODO: if strategy is paused but still holds assets on it shouldnt we withdraw them? + if strategy.paused { + continue; + } + + let strategy_invested_funds = fetch_invested_funds_for_strategy(e, &strategy.address); + + let strategy_share_of_withdrawal = + (amount * strategy_invested_funds) / total_invested_in_strategies; + + withdrawal_amounts.set(strategy.address.clone(), strategy_share_of_withdrawal); + } + + withdrawal_amounts +} + +pub fn calculate_asset_amounts_for_dftokens( + env: &Env, + df_token_amount: i128, +) -> Map { + let mut asset_amounts: Map = Map::new(&env); + let total_supply = VaultToken::total_supply(env.clone()); + let total_managed_funds = fetch_total_managed_funds(&env); + + // Iterate over each asset and calculate the corresponding amount based on df_token_amount + for (asset_address, amount) in total_managed_funds.iter() { + let asset_amount = (amount * df_token_amount) / total_supply; + asset_amounts.set(asset_address.clone(), asset_amount); + } + + asset_amounts +} + +// pub fn calculate_dftokens_from_asset_amounts( +// env: &Env, +// asset_amounts: Map, // The input asset amounts +// total_managed_funds: Map, // The total managed funds for each asset +// ) -> Result { +// let total_supply = VaultToken::total_supply(env.clone()); // Total dfToken supply + +// // Initialize the minimum dfTokens corresponding to each asset +// let mut min_df_tokens: Option = None; + +// // Iterate over each asset in the input map +// for (asset_address, input_amount) in asset_amounts.iter() { +// // Get the total managed amount for this asset +// let managed_amount = total_managed_funds.get(asset_address.clone()).unwrap_or(0); + +// // Ensure the managed amount is not zero to prevent division by zero +// if managed_amount == 0 { +// return Err(ContractError::InsufficientManagedFunds); +// } + +// // Calculate the dfTokens corresponding to this asset's amount +// let df_tokens_for_asset = (input_amount * total_supply) / managed_amount; + +// // If this is the first asset or if the calculated df_tokens_for_asset is smaller, update the minimum df_tokens +// if let Some(current_min_df_tokens) = min_df_tokens { +// min_df_tokens = Some(current_min_df_tokens.min(df_tokens_for_asset)); +// } else { +// min_df_tokens = Some(df_tokens_for_asset); +// } +// } + +// // Return the minimum dfTokens across all assets +// min_df_tokens.ok_or(ContractError::NoAssetsProvided) +// } + +pub fn calculate_optimal_amounts_and_shares_with_enforced_asset( + e: &Env, + total_managed_funds: &Map, + assets: &Vec, + amounts_desired: &Vec, + i: &u32, +) -> (Vec, i128) { + // we have to calculate the optimal amount to deposit for the rest of the assets + // we need the total amount managed by this vault in order for the deposit to be proportional + // reserve (total manage funds) of the asset we are enforcing + let reserve_target = total_managed_funds + .get(assets.get(*i).unwrap_or_else(|| panic_with_error!(&e, ContractError::WrongAmountsLength)).address) + .unwrap_or_else(|| panic_with_error!(&e, ContractError::WrongAmountsLength)); + + // If reserve target is zero, we cannot calculate the optimal amounts + if reserve_target == 0 { + panic_with_error!(&e, ContractError::InsufficientManagedFunds); + } + + let amount_desired_target = amounts_desired.get(*i).unwrap_or_else(|| panic_with_error!(&e, ContractError::WrongAmountsLength)); + + let mut optimal_amounts = Vec::new(e); + + for (j, asset) in assets.iter().enumerate() { + if j == (*i as usize) { + optimal_amounts.push_back(amount_desired_target); + } else { + let reserve = total_managed_funds.get(asset.address).unwrap_or_else(|| panic_with_error!(&e, ContractError::WrongAmountsLength)); + let amount = reserve.checked_mul(amount_desired_target) + .unwrap_or_else(|| panic_with_error!(&e, ContractError::ArithmeticError)) + .checked_div(reserve_target) + .unwrap_or_else(|| panic_with_error!(&e, ContractError::ArithmeticError)); + optimal_amounts.push_back(amount); + } + } + + //TODO: calculate the shares to mint = total_supply * amount_desired_target / reserve_target + let shares_to_mint = + VaultToken::total_supply(e.clone()) + .checked_mul(amount_desired_target) + .unwrap_or_else(|| panic_with_error!(&e, ContractError::ArithmeticError)) + .checked_div(reserve_target) + .unwrap_or_else(|| panic_with_error!(&e, ContractError::ArithmeticError)); + + (optimal_amounts, shares_to_mint) +} +/// Calculates the optimal amounts to deposit for a set of assets, along with the shares to mint. +/// This function iterates over a list of assets and checks if the desired deposit amounts +/// match the optimal deposit strategy, based on current managed funds and asset ratios. +/// +/// If the desired amount for a given asset cannot be achieved due to constraints (e.g., it's below the minimum amount), +/// the function attempts to find an optimal solution by adjusting the amounts of subsequent assets. +/// +/// # Arguments +/// * `e` - The current environment. +/// * `assets` - A vector of assets for which deposits are being calculated. +/// * `amounts_desired` - A vector of desired amounts for each asset. +/// * `amounts_min` - A vector of minimum amounts for each asset, below which deposits are not allowed. +/// +/// # Returns +/// A tuple containing: +/// * A vector of optimal amounts to deposit for each asset. +/// * The number of shares to mint based on the optimal deposits. +/// +/// # Errors +/// If no valid deposit configuration can be found that satisfies the minimum amounts for all assets, the function +/// will return an error. +/// +/// # Panics +/// The function may panic if it encounters invalid states (e.g., insufficient amounts) but TODO: these should +/// be replaced with proper error handling. +pub fn calculate_deposit_amounts_and_shares_to_mint( + e: &Env, + assets: &Vec, + amounts_desired: &Vec, + amounts_min: &Vec, +) -> Result<(Vec, i128), ContractError> { + // Retrieve the total managed funds for each asset as a Map. + let total_managed_funds = fetch_total_managed_funds(e); + + for i in 0..assets.len() { + // Calculate the optimal amounts and shares to mint for asset `i`. + let (optimal_amounts, shares_to_mint) = calculate_optimal_amounts_and_shares_with_enforced_asset( + &e, + &total_managed_funds, + &assets, + &amounts_desired, + &i, + ); + + let mut should_skip = false; + + for j in i + 1..assets.len() { + // Retrieve the desired and minimum amounts, returning an error if unavailable. + let desired_amount = amounts_desired.get(j).ok_or(ContractError::WrongAmountsLength)?; + let min_amount = amounts_min.get(j).ok_or(ContractError::WrongAmountsLength)?; + let optimal_amount = optimal_amounts.get(j).ok_or(ContractError::WrongAmountsLength)?; + + // Check if optimal amount meets the desired or minimum requirements. + if optimal_amount <= desired_amount { + if optimal_amount < min_amount { + return Err(ContractError::InsufficientAmount); // Insufficient amount error. + } + } else { // if not, we should try the next asset as enforced asset + should_skip = true; + // If we have already analized all assets as enforced (i), return an error. + if i == assets.len() - 1 { + return Err(ContractError::NoOptimalAmounts); // probably enforcing 0? We should never reach this point + } + break; + } + } + + // If valid amounts found, return the results; otherwise, skip to the next asset. + if !should_skip { + return Ok((optimal_amounts, shares_to_mint)); + } + } + + // Return an error if no valid deposit configuration is found. + Err(ContractError::NoOptimalAmounts) +} diff --git a/apps/dapp/jest.config.js b/apps/dapp/jest.config.js index afde621b..b9d2cd1a 100644 --- a/apps/dapp/jest.config.js +++ b/apps/dapp/jest.config.js @@ -1,3 +1,4 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} **/ const nextJest = require("next/jest"); const createJestConfig = nextJest({ @@ -9,6 +10,9 @@ const createJestConfig = nextJest({ const customJestConfig = { setupFilesAfterEnv: ["/jest.setup.js"], testEnvironment: "jsdom", + transform: { + "^.+.tsx?$": ["ts-jest",{}], + } }; // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async diff --git a/apps/dapp/package.json b/apps/dapp/package.json index dfbdcd52..5e1f6f34 100644 --- a/apps/dapp/package.json +++ b/apps/dapp/package.json @@ -30,7 +30,8 @@ "react": "19.0.0-rc-f994737d14-20240522", "react-dom": "19.0.0-rc-f994737d14-20240522", "react-icons": "^5.3.0", - "react-redux": "^9.1.2" + "react-redux": "^9.1.2", + "ts-jest": "^29.2.5" }, "devDependencies": { "@chakra-ui/cli": "^3.0.0", diff --git a/apps/dapp/src/components/DeployVault/ConfirmDelpoyModal.tsx b/apps/dapp/src/components/DeployVault/ConfirmDelpoyModal.tsx index d6372293..1f8d5b44 100644 --- a/apps/dapp/src/components/DeployVault/ConfirmDelpoyModal.tsx +++ b/apps/dapp/src/components/DeployVault/ConfirmDelpoyModal.tsx @@ -52,6 +52,8 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo const newVault: NewVaultState = useAppSelector(state => state.newVault); const strategies: Strategy[] = newVault.strategies; const indexName = useAppSelector(state => state.newVault.name) + const indexSymbol = useAppSelector(state => state.newVault.symbol) + const indexShare = useAppSelector(state => state.newVault.vaultShare) const managerString = useAppSelector(state => state.newVault.manager) const emergencyManagerString = useAppSelector(state => state.newVault.emergencyManager) const feeReceiverString = useAppSelector(state => state.newVault.feeReceiver) @@ -80,6 +82,7 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo && emergencyManagerString !== "" && feeReceiverString !== "" && assets.length > 0 + && !indexShare && loadingAssets === false ) { setDeployDisabled(false); @@ -105,6 +108,7 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo }) ); const assetsArray = await Promise.all(assetsPromises); + console.log(assetsArray) setAssets(assetsArray); setLoadingAssets(false); } catch (error) { @@ -119,6 +123,14 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo const deployDefindex = async () => { + if (indexName === "" || indexName === undefined) { + console.log("Set index name please") + return + } + if (indexSymbol === "" || indexSymbol === undefined) { + console.log("Set index symbol please") + return + } if (managerString === "" || managerString === undefined) { console.log("Set manager please") return @@ -131,14 +143,26 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo console.log("Set fee receiver please") return } - + const vaultName = nativeToScVal(indexName, { type: "string" }) + const vaultSymbol = nativeToScVal(indexSymbol, { type: "string" }) + const vaultShare = nativeToScVal(indexShare, { type: "u32" }) const emergencyManager = new Address(emergencyManagerString) const feeReceiver = new Address(feeReceiverString) const manager = new Address(managerString) const salt = randomBytes(32) const ratios = [1]; - + /* + pub struct AssetAllocation { + pub address: Address, + pub strategies: Vec, + } + pub struct Strategy { + pub address: Address, + pub name: String, + pub paused: bool, + } + */ const strategyParamsScVal = strategies.map((param) => { return xdr.ScVal.scvMap([ new xdr.ScMapEntry({ @@ -149,18 +173,47 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo key: xdr.ScVal.scvSymbol('name'), val: nativeToScVal(param.name, { type: "string" }), }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol('paused'), + val: nativeToScVal(false, { type: "bool" }), + }), ]); }); const strategyParamsScValVec = xdr.ScVal.scvVec(strategyParamsScVal); + const assetParamsScVal = assets.map((asset) => { + return xdr.ScVal.scvMap([ + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol('address'), + val: new Address(asset).toScVal(), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol('strategies'), + val: strategyParamsScValVec, + }), + ]); + }); + + + /* fn create_defindex_vault( + emergency_manager: address, + fee_receiver: address, + vault_share: u32, + vault_name: string, + vault_symbol: string, + manager: address, + assets: vec, + salt: bytesn<32>) -> result + */ const createDefindexParams: xdr.ScVal[] = [ emergencyManager.toScVal(), feeReceiver.toScVal(), + vaultShare, + vaultName, + vaultSymbol, manager.toScVal(), - xdr.ScVal.scvVec(assets.map((token) => new Address(token).toScVal())), - xdr.ScVal.scvVec(ratios.map((ratio) => nativeToScVal(ratio, { type: "u32" }))), - strategyParamsScValVec, + xdr.ScVal.scvVec(assetParamsScVal), nativeToScVal(salt), ]; diff --git a/apps/dapp/src/components/DeployVault/DeployVault.tsx b/apps/dapp/src/components/DeployVault/DeployVault.tsx index b698a579..8d84f409 100644 --- a/apps/dapp/src/components/DeployVault/DeployVault.tsx +++ b/apps/dapp/src/components/DeployVault/DeployVault.tsx @@ -13,7 +13,7 @@ import ItemSlider from './Slider' import AddNewStrategyButton from './AddNewStrategyButton' import { useAppDispatch, useAppSelector } from '@/store/lib/storeHooks' import { ConfirmDelpoyModal } from './ConfirmDelpoyModal' -import { setName } from '@/store/lib/features/vaultStore' +import { setName, setSymbol } from '@/store/lib/features/vaultStore' import { Strategy } from '@/store/lib/features/walletStore' import { DialogBody, DialogCloseTrigger, DialogContent, DialogFooter, DialogHeader, DialogRoot, DialogTitle } from '../ui/dialog' @@ -21,6 +21,8 @@ export const DeployVault = () => { const dispatch = useAppDispatch() const strategies: Strategy[] = useAppSelector(state => state.newVault.strategies) const totalValues = useAppSelector(state => state.newVault.totalValues) + const vaultName = useAppSelector(state => state.newVault.name) + const vaultSymbol = useAppSelector(state => state.newVault.symbol) const [openConfirm, setOpenConfirm] = useState(false) const handleClose = () => { @@ -31,6 +33,10 @@ export const DeployVault = () => { await dispatch(setName(e.target.value)) } + const setVaultSymbol = async (e: any) => { + await dispatch(setSymbol(e.target.value)) + } + return ( @@ -39,9 +45,13 @@ export const DeployVault = () => { alignSelf={'end'} alignContent={'center'} mb={4} + gap={6} > - - + + + + + @@ -60,17 +70,16 @@ export const DeployVault = () => { setOpenConfirm(e.open)}> - - - + diff --git a/apps/dapp/src/components/DeployVault/VaultPreview.tsx b/apps/dapp/src/components/DeployVault/VaultPreview.tsx index 8b8a0b25..d93a71e5 100644 --- a/apps/dapp/src/components/DeployVault/VaultPreview.tsx +++ b/apps/dapp/src/components/DeployVault/VaultPreview.tsx @@ -9,19 +9,31 @@ import { IconButton, Fieldset, Stack, + Icon, } from '@chakra-ui/react' import { shortenAddress } from '@/helpers/shortenAddress' import { ChartData } from './ConfirmDelpoyModal' -import { setEmergencyManager, setFeeReceiver, setManager } from '@/store/lib/features/vaultStore' -import { useAppDispatch } from '@/store/lib/storeHooks' +import { setEmergencyManager, setFeeReceiver, setManager, setVaultShare } from '@/store/lib/features/vaultStore' +import { useAppDispatch, useAppSelector } from '@/store/lib/storeHooks' import { StrKey } from '@stellar/stellar-sdk' import { FaRegPaste } from "react-icons/fa6"; import { useSorobanReact } from '@soroban-react/core' import { InputGroup } from '../ui/input-group' import { Tooltip } from '../ui/tooltip' +import { + AccordionItem, + AccordionItemContent, + AccordionItemTrigger, + AccordionRoot, +} from "@chakra-ui/react" +import { IoIosArrowDown, IoIosArrowUp } from 'react-icons/io' - +enum AccordionItems { + STRATEGY_DETAILS = 'strategy-details', + MANAGER_CONFIGS = 'manager-configs', + FEES_CONFIGS = 'fees-configs', +} interface FormControlInterface { manager: { isValid: boolean | undefined; @@ -35,11 +47,88 @@ interface FormControlInterface { isValid: boolean | undefined; value: string | undefined; }, + vaultShare: number +} +const CustomAccordionTrigger = ({ title, type, accordionValue, setAccordionValue }: { title: string, type: AccordionItems, accordionValue: AccordionItems[], setAccordionValue: React.Dispatch> }) => { + return ( + { + if (accordionValue[0] === type) { + setAccordionValue([]) + } + }}> + + + + {title} settings + + + + + {accordionValue[0] === type ? + + + + : + + + + } + + + + + ) +} + +const CustomInputField = ({ + label, + value, + onChange, + handleClick, + placeholder, + invalid +}: { + label: string, + value: string, + onChange: (e: any) => void, + handleClick: (address: string) => void, + placeholder: string, + invalid: boolean +}) => { + const { address } = useSorobanReact() + if (!address) return null + return ( + + {label} + + + handleClick(address)} + > + + + + }> + + + + A valid Stellar / Soroban address is required. + + ) } export const VaultPreview = ({ data }: { data: ChartData[] }) => { const dispatch = useAppDispatch() const { address } = useSorobanReact() + const vaultShare = useAppSelector(state => state.newVault.vaultShare) + const [accordionValue, setAccordionValue] = useState([AccordionItems.STRATEGY_DETAILS]) const [formControl, setFormControl] = useState({ manager: { isValid: undefined, @@ -53,6 +142,7 @@ export const VaultPreview = ({ data }: { data: ChartData[] }) => { isValid: undefined, value: undefined }, + vaultShare: 0 }) const isValidAddress = (address: string) => { if (StrKey.isValidEd25519PublicKey(address) || StrKey.isValidMed25519PublicKey(address) || StrKey.isValidContract(address)) { @@ -134,6 +224,17 @@ export const VaultPreview = ({ data }: { data: ChartData[] }) => { }) dispatch(setFeeReceiver(input)) }; + const handleVaultShareChange = (input: any) => { + if (isNaN(input)) return + if (input < 0 || input > 100) return + const decimalRegex = /^(\d+)?(\.\d{0,2})?$/ + if (!decimalRegex.test(input)) return + setFormControl({ + ...formControl, + vaultShare: input + }) + dispatch(setVaultShare(input * 100)) + } return ( <> @@ -148,126 +249,88 @@ export const VaultPreview = ({ data }: { data: ChartData[] }) => { height={200} /> */} - - Strategies - - - - - Name - Address - Percentage - - - - {data.map((strategy: ChartData, index: number) => ( - - {strategy.label} - - - {strategy.address ? shortenAddress(strategy.address) : '-'} - - - {strategy.value}% - - ))} - - - - - - - Manager - - - handleManagerChange(address!)} - > - - - - }> - handleManagerChange(event?.target.value)} - value={formControl.manager.value} - placeholder='GAFS3TLVM...' - /> - - - A valid Stellar / Soroban address is required. - - - - - - Emergency manager - - - handleEmergencyManagerChange(address!)} - > - - - - }> - handleEmergencyManagerChange(event?.target.value)} - value={formControl.emergencyManager.value} - placeholder='GAFS3TLVM...' - /> - - - A valid Stellar / Soroban address is required. - - - - - - Fee reciever - - - handleFeeReceiverChange(address!)} - > - - - - }> - handleFeeReceiverChange(event?.target.value)} - value={formControl.feeReceiver.value} - placeholder='GAFS3TLVM...' - /> - - - A valid Stellar / Soroban address is required. - - - - - - + setAccordionValue(e.value)}> + + + + + + + Name + Address + Percentage + + + + {data.map((strategy: ChartData, index: number) => ( + + {strategy.label} + + {strategy.address ? shortenAddress(strategy.address) : '-'} + + {strategy.value}% + + ))} + + + + + + + + handleManagerChange(e.target.value)} + handleClick={(address: string) => handleManagerChange(address)} + placeholder='GAFS3TLVM...' + invalid={formControl.manager.isValid === false} + /> + handleEmergencyManagerChange(e.target.value)} + handleClick={(address: string) => handleEmergencyManagerChange(address)} + placeholder='GAFS3TLVM...' + invalid={formControl.emergencyManager.isValid === false} + /> + + + + + + handleFeeReceiverChange(e.target.value)} + handleClick={(address: string) => handleFeeReceiverChange(address)} + placeholder='GAFS3TLVM...' + invalid={formControl.feeReceiver.isValid === false} + /> + + { handleVaultShareChange(e.target.value) }} + /> + + + + ) -} +} \ No newline at end of file diff --git a/apps/dapp/src/components/InteractWithVault/InteractWithVault.tsx b/apps/dapp/src/components/InteractWithVault/InteractWithVault.tsx index 20f4fcd9..c54896a8 100644 --- a/apps/dapp/src/components/InteractWithVault/InteractWithVault.tsx +++ b/apps/dapp/src/components/InteractWithVault/InteractWithVault.tsx @@ -27,33 +27,24 @@ export const InteractWithVault = () => { const vaultOperation = async () => { if (!address || !vaultMethod) return; - if (vaultMethod != VaultMethod.EMERGENCY_WITHDRAW) return; + if (!amount) throw new Error('Amount is required'); + if (vaultMethod != VaultMethod.DEPOSIT) return; + const depositParams: xdr.ScVal[] = [ + xdr.ScVal.scvVec([nativeToScVal((amount * Math.pow(10, 7)), { type: "i128" })]), + xdr.ScVal.scvVec([nativeToScVal(((amount * 0.9) * Math.pow(10, 7)), { type: "i128" })]), + new Address(address).toScVal(), + ] console.log('Vault method:', vaultMethod) - const args: xdr.ScVal[] = [ - new Address(selectedVault.address).toScVal() - ]; - if (vaultMethod === VaultMethod.EMERGENCY_WITHDRAW) { - if (!selectedVault?.totalValues) throw new Error('Total values is required'); - args.unshift(nativeToScVal((0), { type: "i128" }),) + try { const result = await vault( - VaultMethod.EMERGENCY_WITHDRAW, + vaultMethod!, selectedVault?.address!, - [ - new Address(selectedVault.address).toScVal() - ], + depositParams, true, ) - return result - } else { - if (!amount) throw new Error('Amount is required'); - args.unshift(nativeToScVal((amount * Math.pow(10, 7)), { type: "i128" }),) + } catch (error) { + console.error('Error:', error) } - const result = await vault( - vaultMethod!, - selectedVault?.address!, - args, - true, - ) } const setAmount = (e: any) => { diff --git a/apps/dapp/src/components/ManageVaults/AllVaults.tsx b/apps/dapp/src/components/ManageVaults/AllVaults.tsx index 49b68b77..4e9c636f 100644 --- a/apps/dapp/src/components/ManageVaults/AllVaults.tsx +++ b/apps/dapp/src/components/ManageVaults/AllVaults.tsx @@ -71,17 +71,20 @@ export const AllVaults = ({ vault(VaultMethod.GETEMERGENCYMANAGER, selectedVault, undefined, false).then((res: any) => scValToNative(res)), vault(VaultMethod.GETFEERECEIVER, selectedVault, undefined, false).then((res: any) => scValToNative(res)), vault(VaultMethod.GETNAME, selectedVault, undefined, false).then((res: any) => scValToNative(res)), - vault(VaultMethod.GETSTRATEGIES, selectedVault, undefined, false).then((res: any) => scValToNative(res)), + vault(VaultMethod.GETASSETS, selectedVault, undefined, false).then((res: any) => scValToNative(res)), vault(VaultMethod.GETTOTALVALUES, selectedVault, undefined, false).then((res: any) => scValToNative(res)), ]); + const TVValues = Object.values(totalValues) + const totalValuesArray = TVValues.map((value: any) => Number(value)) + const accTotalValues = totalValuesArray.reduce((a: number, b: number) => a + b, 0) const newData: VaultData = { name: name || '', address: selectedVault, manager: manager, emergencyManager: emergencyManager, feeReceiver: feeReceiver, - strategies: strategies || [], - totalValues: totalValues || 0, + strategies: strategies[0].strategies || [], + totalValues: accTotalValues || 0, } return newData } catch (e: any) { @@ -183,7 +186,6 @@ export const AllVaults = ({ handleOpenDeposit(VaultMethod.DEPOSIT, vault)} @@ -194,10 +196,8 @@ export const AllVaults = ({ handleOpenDeposit(VaultMethod.WITHDRAW, vault)} > @@ -244,7 +244,6 @@ export const AllVaults = ({ handleOpenDeposit(VaultMethod.DEPOSIT, vault)} @@ -254,7 +253,6 @@ export const AllVaults = ({ handleOpenDeposit(VaultMethod.WITHDRAW, vault)} @@ -265,7 +263,6 @@ export const AllVaults = ({ {(address == vault.manager) && handleOpenDeployVault('edit_vault', true, vault)} @@ -276,7 +273,6 @@ export const AllVaults = ({ {(address == vault.emergencyManager || address == vault.manager) && handleOpenDeposit(VaultMethod.EMERGENCY_WITHDRAW, vault)} diff --git a/apps/dapp/src/components/ManageVaults/ManageVaults.tsx b/apps/dapp/src/components/ManageVaults/ManageVaults.tsx index 13d27620..d406b036 100644 --- a/apps/dapp/src/components/ManageVaults/ManageVaults.tsx +++ b/apps/dapp/src/components/ManageVaults/ManageVaults.tsx @@ -51,7 +51,6 @@ export const ManageVaults = () => { const selectedVault = vaults.find(vault => vault.address === args.address) if (!selectedVault) return; for (const item of selectedVault.strategies) { - console.log(item) const newStrategy: Strategy = { ...item, share: selectedVault.strategies.length > 1 ? 100 / selectedVault.strategies.length : 100 }; await dispatch(pushStrategy(newStrategy)) } diff --git a/apps/dapp/src/components/ui/number-input.tsx b/apps/dapp/src/components/ui/number-input.tsx new file mode 100644 index 00000000..b178664c --- /dev/null +++ b/apps/dapp/src/components/ui/number-input.tsx @@ -0,0 +1,23 @@ +import { NumberInput as ChakraNumberInput } from "@chakra-ui/react" +import { forwardRef } from "react" + +export interface NumberInputProps extends ChakraNumberInput.RootProps {} + +export const NumberInputRoot = forwardRef( + function NumberInput(props, ref) { + const { children, ...rest } = props + return ( + + {children} + + + + + + ) + }, +) + +export const NumberInputField = ChakraNumberInput.Input +export const NumberInputScruber = ChakraNumberInput.Scrubber +export const NumberInputLabel = ChakraNumberInput.Label diff --git a/apps/dapp/src/hooks/useVault.ts b/apps/dapp/src/hooks/useVault.ts index 37d76f95..4490a9d4 100644 --- a/apps/dapp/src/hooks/useVault.ts +++ b/apps/dapp/src/hooks/useVault.ts @@ -13,8 +13,8 @@ export enum VaultMethod { GETFEERECEIVER = "get_fee_receiver", EMERGENCY_WITHDRAW = "emergency_withdraw", GETNAME= "name", - GETTOTALVALUES = "current_invested_funds", - GETSTRATEGIES = "get_strategies", + GETTOTALVALUES = "fetch_current_invested_funds", + GETASSETS = "get_assets", } const isObject = (val: unknown) => typeof val === 'object' && val !== null && !Array.isArray(val); diff --git a/apps/dapp/src/store/lib/features/vaultStore.ts b/apps/dapp/src/store/lib/features/vaultStore.ts index 1aae364a..62e3c1e6 100644 --- a/apps/dapp/src/store/lib/features/vaultStore.ts +++ b/apps/dapp/src/store/lib/features/vaultStore.ts @@ -1,16 +1,17 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import type { RootState } from '../store' -import axios from 'axios' import { getRemoteConfig } from '@/helpers/getRemoteConfig'; import { Strategy } from './walletStore'; export interface NewVaultState { address: string; - emergencyManager?: string; - feeReceiver?: string; - manager?: string; + emergencyManager: string; + feeReceiver: string; + manager: string; + vaultShare: number; name: string; + symbol: string; strategies: Strategy[]; totalValues?: number; } @@ -22,6 +23,8 @@ const initialState: NewVaultState = { feeReceiver: "", manager: "", name: "", + symbol: "", + vaultShare: 0, strategies: [ { address: "", @@ -103,6 +106,9 @@ export const newVaultSlice = createSlice({ setName: ((state, action: PayloadAction) => { state.name = action.payload; }), + setSymbol: ((state, action: PayloadAction) => { + state.symbol = action.payload; + }), setManager: ((state, action: PayloadAction) => { state.manager = action.payload; }), @@ -112,6 +118,9 @@ export const newVaultSlice = createSlice({ setFeeReceiver: ((state, action: PayloadAction) => { state.feeReceiver = action.payload; }), + setVaultShare: ((state, action: PayloadAction) => { + state.vaultShare = action.payload; + }), } }) @@ -122,9 +131,11 @@ export const { setStrategyValue, resetStrategyValue, setName, + setSymbol, setManager, setEmergencyManager, - setFeeReceiver + setFeeReceiver, + setVaultShare } = newVaultSlice.actions // Other code such as selectors can use the imported `RootState` type diff --git a/apps/dapp/src/utils/factory.ts b/apps/dapp/src/utils/factory.ts index 007987d5..76619c28 100644 --- a/apps/dapp/src/utils/factory.ts +++ b/apps/dapp/src/utils/factory.ts @@ -12,6 +12,7 @@ export async function fetchFactoryAddress(network: string): Promise { if (network !== "testnet" && network !== "mainnet") { throw new Error(`Invalid network: ${network}. It should be testnet or mainnet`); } + const url = `https://raw.githubusercontent.com/paltalabs/defindex/refs/heads/main/public/${network}.contracts.json`; try { const response = await fetch(url); diff --git a/apps/docs/10-whitepaper/03-the-defindex-approach/02-contracts/01-vault-contract.md b/apps/docs/10-whitepaper/03-the-defindex-approach/02-contracts/01-vault-contract.md index 47992ad7..28105b59 100644 --- a/apps/docs/10-whitepaper/03-the-defindex-approach/02-contracts/01-vault-contract.md +++ b/apps/docs/10-whitepaper/03-the-defindex-approach/02-contracts/01-vault-contract.md @@ -6,14 +6,14 @@ While anyone can invest in a DeFindex, only the Manager and Emergency Manager ha The contract also holds funds not currently invested in any strategy, known as **IDLE funds**. These funds act as a safety buffer, allowing the Emergency Manager to withdraw assets from underperforming or unhealthy strategies and store them as IDLE funds. (also to enable fast small withdrawals) -### Underlying Assets +## Underlying Assets Each DeFindex Vault will use a defined set of underlying assets to be invested in one or more strategies. Because Strategies are the only one that know exactly the current balance of the asset, the Vault relies on the strategies in order to know the exact total balance for each underlying asset.?? Or if the Vault executes Strategies at its own name (auth), it should execute a speficic `get_assets_balance` function in the strategy contract to know exactely how many assets it has at a specific moment. -### Initialization +## Initialization The DeFindex Vault contract is structured with specific roles and strategies for managing assets effectively. The key roles include the **Fee Receiver**, **Manager**, and **Emergency Manager**, each responsible for different tasks in managing the Vault. Additionally, a predefined set of strategies determines how assets will be allocated within the Vault. A management fee is also established at the time of initialization, which can later be adjusted by the Fee Receiver. Further details on fee handling are explained later in the document. The allocation ratios for these strategies are not set during the deployment but are defined during the first deposit made into the Vault. For example, imagine a scenario where the Vault is set up to allocate 20% of its assets to a USDC lending pool (like Blend), 30% to another USDC lending pool (such as YieldBlox), and 50% to a USDC-XLM liquidity pool on an Automated Market Maker (AMM) platform (like Soroswap). @@ -31,19 +31,49 @@ In summary: 4. The **Manager** can adjust allocations but cannot add new strategies, ensuring security and stability. +## Investing: Deposit -### Investing: Deposit -When a user deposits assets into the DeFindex, they receive dfTokens that represent their proportional share of the DeFindex's assets. These dfTokens can later be burned to withdraw the corresponding assets. +When a user deposits assets into the DeFindex Vault, they receive dfTokens, representing their proportional share of the Vault’s total assets. These dfTokens can later be burned to redeem the user’s share of assets. -Upon calling the `deposit()` function, **the assets are transferred to the DeFindex in accordance with the current asset ratio**. For example, if the current ratio is 1 token A, 2 tokens B, and 3 tokens C for each dfToken, this ratio is maintained when assets are deposited. In return, the user receives dfTokens that represent their participation in the DeFindex Vault. +Upon calling the `deposit()` function, assets are transferred to the DeFindex Vault and allocated based on the current asset ratios. For example, if the Vault maintains a 1:2:3 ratio for assets A, B, and C per dfToken, this ratio will be applied to new deposits. The user receives dfTokens reflecting their share of the Vault’s total assets. -When the user wishes to withdraw their assets, they call the `withdraw` function to burn their dfTokens. The **withdrawn assets will be dispensed according to the asset ratio at the time of withdrawal**. +To withdraw assets, users call the `withdraw` function to burn their dfTokens, releasing assets according to the current asset ratio. -Thus, the price per dfToken reflects a multi-asset price. For instance, using the earlier example, because in order to mint 1 dfToken, the user needs to deposit 1 token A, 2 tokens B, and 3 tokens C, the price per 1 dfToken will be `p(dfToken)=(1A, 2B, 3C)`. +Thus, the value per dfToken reflects a multi-asset backing. Using the above example, to mint 1 dfToken, a user would need to deposit 1 unit of asset A, 2 units of asset B, and 3 units of asset C. Therefore, the value of 1 dfToken can be represented as: +$$ +p(\text{dfToken}) = (1 \text{A}, 2 \text{B}, 3 \text{C}) +$$ + +### Depositing When Total Assets = 1 + +When the Vault only holds one asset, the deposit process is straightforward: the amount deposited by the user will be directly used to mint shares proportional to the total funds in the Vault. + +1. **First Deposit**: + For the initial deposit, `shares_to_deposit` is set equal to the `amount` sent by the user, simplifying the initial setup. + +2. **When There Are Existing Funds**: + If the Vault already holds funds, `shares_to_deposit` are calculated based on the current `total_managed_funds` and `total_supply` (i.e., the current number of shares), according to the following formula: + +Let’s denote the total supply at time 0 as $s_0$ and the total managed funds as $v_0$. At time 1, a user wants to deposit an additional amount $v'$, and new shares $s'$ are minted. The value of any share $val(s)$ at time $t$ is calculated as: + +$$ +val(s)_t = \frac{v_t}{s_t} \cdot s +$$ +At time $t_1$, this must hold: -### Withdrawals +$$ +val(s') = \frac{v_1}{s_1} \cdot s' +$$ + +Given that $v_1 = v_0 + v'$ and $s_1 = s_0 + s'$, we can rearrange terms to find the new shares: + +$$ +s' = \frac{v'}{v_0} \cdot s_0 +$$ + +## Withdrawals When a user wishes to withdraw funds, they must burn a corresponding amount of dfTokens (shares) to receive their **assets at the ratio of the time of withdrawal**. If there are sufficient **IDLE funds** available, the withdrawal is fulfilled directly from these IDLE funds. If additional assets are needed beyond what is available in the IDLE funds, a liquidation process is triggered to release the required assets. @@ -81,14 +111,14 @@ Where: - $a_{i, \text{IDLE}}$: Amount of asset $i$ to get from the IDLE funds - $a_{i, \text{Strategy}}$: Amount of asset $i$ to get from the strategies -### Rebalancing +## Rebalancing Rebalancing is overseen by the **Manager**, who adjusts the allocation of funds between different strategies to maintain or change the ratio of underlying assets. For example, a DeFindex might start with a ratio of 2 USDC to 1 XLM, as initially set by the Deployer. However, this ratio can be modified by the Manager based on strategy performance or market conditions. Upon deployment, the Deployer establishes the initial strategies and asset ratios for the DeFindex. The Manager has the authority to adjust these ratios as needed to respond to evolving conditions or to optimize performance. To ensure accurate representation of asset proportions, strategies are required to **report** the amount of each underlying asset they hold. This reporting ensures that when dfTokens are minted or redeemed, the DeFindex maintains the correct asset ratios in line with the current balance and strategy allocations. -#### Functions +### Functions - `assets()`: Returns the assets addresses and amount of each of them in the DeFindex (and hence its current ratio). `[[adress0, amount0], [address1, amount1]]`. TODO: Separate in 2 functions. - `withdraw_from_strategies`: Allows the Manager to withdraw assets from one or more strategies, letting them as IDLE funds. @@ -99,22 +129,143 @@ To ensure accurate representation of asset proportions, strategies are required Then, a rebalance execution will withdraw assets from the strategies, swap them, and invest them back in the strategies. - `emergency_withdraw`: Allows the Emergency Manager to withdraw all assets from a specific Strategy. As arguments, it receives the the address of a Strategy. It also turns off the strategy. -### Emergency Management +## Emergency Management The Emergency Manager has the authority to withdraw assets from the DeFindex in case of an emergency. This role is designed to protect users' assets in the event of a critical situation, such as a hack of a underlying protocol or a if a strategy gets unhealthy. The Emergency Manager can withdraw assets from the Strategy and store them as IDLE funds inside the Vault until the situation is resolved. -### Management +## Management Every DeFindex has a manager, who is responsible for managing the DeFindex. The Manager can ebalance the Vault, and invest IDLE funds in strategies. +## Fees + +### Fee Receivers +The DeFindex protocol defines two distinct fee receivers to reward both the creators of the DeFindex Protocol and the deployers of individual Vaults: + +1. **DeFindex Protocol Fee Receiver**: Receives a fixed protocol fee of 0.5% APR. +2. **Vault Fee Receiver**: Receives a fee set by the vault deployer, typically recommended between 0.5% and 2% APR. + +The Total Management Fee consists of both the protocol fee and the vault fee. Thus, each Vault has a total APR fee rate $f_{\text{total}}$ such that: + +$$ +f_{\text{total}} = f_{\text{DeFindex}} + f_{\text{Vault}} +$$ + +where $f_{\text{DeFindex}} = 0.5\%$ is a fixed `defindex_fee` that goes to the DeFindex Protocol Fee Receiver address, and $f_{\text{Vault}}$ is a variable APR `vault_fee`, typically between 0.5% and 2%, that goes to the Vault Fee Receiver address. + +### Fee Collection Methodology + +The fee collection process mints new shares, or dfTokens, to cover the accrued management fees. These shares are calculated based on the elapsed time since the last fee assessment, ensuring fees are accrued based on the actual period of asset management. The fee collection is triggered whenever there is a vault interaction, such as a `deposit`, `withdrawal`, or even an explicit `fee_collection` call, with calculations based on the time elapsed since the last fee assessment. + +### Mathematical Derivation of New Fees + +Let: + +- $V_0$ be the Total Value Locked (TVL) at the last assessment, +- $s_0$ be the Total Shares (dfTokens) at the last assessment, +- $f_{\text{total}}$ be the Total Management Fee (APR). + +Over a time period $\Delta t$, the fees due for collection are derived as a value represented by newly minted shares. + +To mint new shares for fee distribution, we calculate the required number of new shares, $s_f$, that correspond to the total management fee over the elapsed period. + +After a period $\Delta t$ (expressed in seconds), and after the fee collection process the new total shares $s_1$ should be: + +$$ +s_1 = s_0 + s_f +$$ + +Since `fee_collection` is always called before any `deposit` or `withdrawal`, we assume that the Total Value $V_1$ remains equal to $V_0$. + +We establish the following condition to ensure the number of minted shares accurately reflects the management fee accrued over $\Delta t$. The value of the new minted shares $s_f$ should equal the prorated APR fee share of the total value of the vault. In mathematical terms: + +$$ +\frac{V_0}{s_1} \times s_f = V_0 \times f_{\text{total}} \times \frac{\Delta t}{\text{SECONDS PER YEAR}} +$$ + +Rearranging terms, we get: + +$$ +s_f = \frac{f_{\text{total}} \times s_0 \times \Delta t}{\text{SECONDS PER YEAR} - f_{\text{total}} \times \Delta t} +$$ + +This equation gives the precise quantity of new shares $s_f$ to mint as dfTokens for the management fee over the period $\Delta t$. + +### Distribution of Fees + +Once the total fees, $s_f$, are calculated, the shares are split proportionally between the DeFindex Protocol Fee Receiver and the Vault Fee Receiver. This is done by calculating the ratio of each fee receiver’s APR to the total APR: + +$$ +s_{\text{DeFindex}} = \frac{s_f \times f_{\text{DeFindex}}}{f_{\text{total}}} +$$ + +$$ +s_{\text{Vault}} = s_f - s_{\text{DeFindex}} +$$ + +This ensures that each fee receiver is allocated their respective share of dfTokens based on their fee contribution to $f_{\text{total}}$. The dfTokens are then minted to each receiver’s address as a direct representation of the fees collected. + + +### Example + +Suppose a DeFindex vault begins with an initial value of 1 USDC per share and a total of 100 shares (dfTokens), representing an investment of 100 USDC. This investment is placed in a lending protocol with an 8% APY. The DeFindex protocol has a total management fee of 1% APR, split between a 0.5% protocol fee and a 0.5% vault fee. + +After one year, the investment grows to 108 USDC due to the 8% APY. + +#### Step 1: Calculate the Shares to Mint for Fees + +Using the formula: + +$ +s_f = \frac{f_{\text{total}} \times s_0 \times \Delta t}{\text{SECONDS PER YEAR} - f_{\text{total}} \times \Delta t} +$ + +where: +- \( f_{\text{total}} = 0.01 \) (1% APR management fee), +- \( s_0 = 100 \) (initial shares), +- \( \Delta t = \text{SECONDS PER YEAR} \) (since this example spans a full year), + +we calculate \( s_f \), the number of shares to mint for the fee collection. + +Substituting values: + +$ +s_f = \frac{0.01 \times 100 \times \text{SECONDS PER YEAR}}{\text{SECONDS PER YEAR} - (0.01 \times \text{SECONDS PER YEAR})} +$ + +Simplifying: + +$ +s_f = \frac{1 \times \text{SECONDS PER YEAR}}{0.99 \times \text{SECONDS PER YEAR}} \approx 1.0101 +$ + +Thus, approximately 1.01 dfTokens are minted as fees. + +#### Step 2: Update Total Shares and Calculate Price per Share + +With the fee tokens minted, the total dfTokens increase from 100 to 101.01. + +The vault now holds 108 USDC backing 101.01 dfTokens, so the new price per share is: + +$ +\text{Price per Share} = \frac{108}{101.01} \approx 1.069 \, \text{USDC} +$ + +#### Step 3: Determine the Value for a User Holding 100 dfTokens + +For a user holding 100 dfTokens, the value of their holdings after one year is approximately: + +$ +100 \, \text{dfTokens} \times 1.069 \, \text{USDC per share} = 106.9 \, \text{USDC} +$ -### Fee Collection -The revenues generated by the strategies are collected as shares of the DeFindex. These shares, or dfTokens, are minted whenever a deposit or withdrawal takes place. +The remaining 1.01 dfTokens represent the collected fee, backed by around: -The deployer can set a management fee, which can later be adjusted by the Fee Receiver. Additionally, **palta**labs charges a 0.5% APR on the vault shares. +$ +1.01 \, \text{dfTokens} \times 1.069 \, \text{USDC per share} \approx 1.08 \, \text{USDC} +$ -The recommended initial setup suggests a fee of **0.5%-2% APR on these shares**. For example, if a DeFindex has 100 shares and the fee is set at 0.5% APR, the fees collected annually would be 1 share, split as 0.5 for the Fee Receiver and 0.5 for **palta**labs. The **palta**labs fee is defined in the Factory contract. +--- -These allocations are recalculated and minted whenever a user deposits, withdraws from the DeFindex, or when rebalancing occurs. +This breakdown clarifies how the investment grows and the management fee is deducted by minting new dfTokens, resulting in a proportional share value for both users and fee recipients. -For instance, let's say a DeFindex starts with an initial value of 1 USDC per share and 100 shares (dfTokens). These 100 USDC are invested in a lending protocol offering an 8% APY. The DeFindex also has a 0.5% APR fee. After one year, the investment grows to 108 USDC. Additionally, 1 dfToken is minted as a fee. This means the DeFindex now holds 101 dfTokens backed by 108 USDC, making the price per share approximately 1.07 USDC. As a result, a user holding 100 dfTokens will have a value equivalent to around 107 USDC, while the collected fee will be backed by about 1.07 USDC. It is expected that the Fee Receiver is associated with the manager, allowing the entity managing the Vault to be compensated through the Fee Receiver. In other words, the Fee Receiver could be the manager using the same address, or it could be a different entity such as a streaming contract, a DAO, or another party. diff --git a/public/testnet.contracts.json b/public/testnet.contracts.json index 3ab38679..2e72700a 100644 --- a/public/testnet.contracts.json +++ b/public/testnet.contracts.json @@ -1,13 +1,11 @@ { "ids": { - "defindex_factory": "CCBQ4WFNNWZWV7GCLUTRAQZXX34WSYNSCXMY4WEKV7BXRTYMM53JMGIS", - "hodl_strategy": "CDIBYFBYBV3D3DQNSUDMVQNWYCQPYKSOLKSEOF3WEQOIXB56K54Q4G6W", - "blend_strategy": "CDABYFBYBV3D3DQNSUDMVQNWYCQPYKSOLKSEOF3WEQOIXB56K54Q4G6W" + "defindex_factory": "CBBV4GTOEZD3HALMAKMFBUSFQZV6HFW6QTVKBF6EV3H4OPJO6NQEKCRL", + "hodl_strategy": "CDIBYFBYBV3D3DQNSUDMVQNWYCQPYKSOLKSEOF3WEQOIXB56K54Q4G6W" }, "hashes": { - "defindex_vault": "6ac83e448a7a68b4e27fc9dd3c2d3e8cb423d88e2646137b7b32238e9df95980", - "defindex_factory": "552b67db3c57921fada99cfee520f18a649855a376c22e8cb5ee20b52632d497", - "hodl_strategy": "8b5f64742f6726b587b93d250653b13094e52adcb57bc6e7926c98d2b9cecd2e", - "blend_strategy": "8b5g64742f6726b587b93d250653b13094e52adcb57bc6e7926c98d2b9cecd2e" + "defindex_vault": "f4a339d8749fda772ac9fa7757e59ffee443dd240e7ee9581ef60ccbf148c0da", + "defindex_factory": "39111229dc8da070cc32f39c624152aa674e8b94c4a03f402e355293346417b8", + "hodl_strategy": "8b5f64742f6726b587b93d250653b13094e52adcb57bc6e7926c98d2b9cecd2e" } } \ No newline at end of file diff --git a/scf-tracker.md b/scf-tracker.md index f27416b6..68079a7b 100644 --- a/scf-tracker.md +++ b/scf-tracker.md @@ -19,7 +19,7 @@ Contract will be available on [GitHub](https://github.com/paltalabs). Code is reviewed, tested, and successfully passes security audits. - **Result:** - - ✅ Code available on [GitHub](https://github.com/paltalabs/defindex/tree/main/apps/contracts/defindex) + - ✅ Code available on [GitHub](https://github.com/paltalabs/defindex/tree/main/apps/contracts/vault) - 🛠️ Security Audits --- diff --git a/yarn.lock b/yarn.lock index f39f2dcc..301d5c23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15909,7 +15909,7 @@ ts-jest@^25.3.1: semver "6.x" yargs-parser "18.x" -ts-jest@^29.1.2: +ts-jest@^29.1.2, ts-jest@^29.2.5: version "29.2.5" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.2.5.tgz#591a3c108e1f5ebd013d3152142cb5472b399d63" integrity sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==