diff --git a/apps/contracts/Cargo.lock b/apps/contracts/Cargo.lock index 234d602b..e656b7bc 100644 --- a/apps/contracts/Cargo.lock +++ b/apps/contracts/Cargo.lock @@ -152,6 +152,13 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "common" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -283,6 +290,8 @@ dependencies = [ name = "defindex-factory" version = "0.1.0" dependencies = [ + "common", + "defindex-strategy-core", "soroban-sdk", ] @@ -297,6 +306,7 @@ dependencies = [ name = "defindex-vault" version = "0.1.0" dependencies = [ + "common", "defindex-strategy-core", "soroban-sdk", "soroban-token-sdk", @@ -603,6 +613,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" +[[package]] +name = "integration-test" +version = "0.0.0" +dependencies = [ + "common", + "defindex-factory", + "defindex-vault", + "hodl_strategy", + "soroban-sdk", +] + [[package]] name = "itertools" version = "0.11.0" diff --git a/apps/contracts/Cargo.toml b/apps/contracts/Cargo.toml index 79b3c9e0..3b0ecf8b 100644 --- a/apps/contracts/Cargo.toml +++ b/apps/contracts/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["strategies/*", "vault", "factory"] +members = ["common", "strategies/*", "vault", "factory", "integration-test"] exclude = [ "strategies/external_wasms", ] @@ -17,6 +17,7 @@ soroban-sdk = "21.7.6" soroban-token-sdk = { version = "21.0.1-preview.3" } # soroswap-library = "0.3.0" defindex-strategy-core={ path="./strategies/core", package="defindex-strategy-core" } +common={ path="./common", package="common" } [profile.release] opt-level = "z" diff --git a/apps/contracts/common/Cargo.toml b/apps/contracts/common/Cargo.toml new file mode 100644 index 00000000..37476ab1 --- /dev/null +++ b/apps/contracts/common/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "common" +version = { workspace = true } +authors = ["coderipper "] +license = { workspace = true } +edition = { workspace = true } +publish = false +repository = { workspace = true } + +[dependencies] +soroban-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } \ No newline at end of file diff --git a/apps/contracts/common/src/lib.rs b/apps/contracts/common/src/lib.rs new file mode 100644 index 00000000..bc7fe786 --- /dev/null +++ b/apps/contracts/common/src/lib.rs @@ -0,0 +1,3 @@ +#![no_std] + +pub mod models; \ No newline at end of file diff --git a/apps/contracts/common/src/models.rs b/apps/contracts/common/src/models.rs new file mode 100644 index 00000000..401e620a --- /dev/null +++ b/apps/contracts/common/src/models.rs @@ -0,0 +1,16 @@ +use soroban_sdk::{contracttype, Address, String, Vec}; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Strategy { + pub address: Address, + pub name: String, + pub paused: bool, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AssetStrategySet { + pub address: Address, + pub strategies: Vec, +} \ No newline at end of file diff --git a/apps/contracts/factory/Cargo.toml b/apps/contracts/factory/Cargo.toml index 21f2a8b5..a4b2ad8c 100644 --- a/apps/contracts/factory/Cargo.toml +++ b/apps/contracts/factory/Cargo.toml @@ -12,6 +12,8 @@ crate-type = ["cdylib"] [dependencies] soroban-sdk = { workspace = true } +defindex-strategy-core = { workspace = true } +common = { workspace = true } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/apps/contracts/factory/src/defindex.rs b/apps/contracts/factory/src/defindex.rs deleted file mode 100644 index 1b6a2d19..00000000 --- a/apps/contracts/factory/src/defindex.rs +++ /dev/null @@ -1,26 +0,0 @@ -// Import necessary types from the Soroban SDK -#![allow(unused)] -use soroban_sdk::{contracttype, contracterror, xdr::ToXdr, Address, Bytes, BytesN, Env, Vec}; - -soroban_sdk::contractimport!( - file = "../target/wasm32-unknown-unknown/release/defindex_vault.optimized.wasm" -); - -// Define a function to create a new contract instance -pub fn create_contract( - e: &Env, // Pass in the current environment as an argument - defindex_wasm_hash: BytesN<32>, // Pass in the hash of the token contract's WASM file - salt: BytesN<32>, -) -> Address { - - // Append the bytes of the address and name to the salt - // salt.append(&adapters.clone().to_xdr(e)); - // let mut value = [0u8; 32]; - // e.prng().fill(&mut value); - // salt = Bytes::from_array(&e, &value); - - // Use the deployer() method of the current environment to create a new contract instance - e.deployer() - .with_current_contract(e.crypto().sha256(&salt.into())) // Use the salt as a unique identifier for the new contract instance - .deploy(defindex_wasm_hash) // Deploy the new contract instance using the given pair_wasm_hash value -} \ No newline at end of file diff --git a/apps/contracts/factory/src/events.rs b/apps/contracts/factory/src/events.rs index 92eaa54c..baa1e041 100644 --- a/apps/contracts/factory/src/events.rs +++ b/apps/contracts/factory/src/events.rs @@ -1,6 +1,6 @@ //! Definition of the Events used in the contract +use common::models::AssetStrategySet; use soroban_sdk::{contracttype, symbol_short, Address, Env, Vec}; -use crate::defindex::AssetStrategySet; // INITIALIZED #[contracttype] diff --git a/apps/contracts/factory/src/lib.rs b/apps/contracts/factory/src/lib.rs index 033908be..43175e34 100644 --- a/apps/contracts/factory/src/lib.rs +++ b/apps/contracts/factory/src/lib.rs @@ -1,16 +1,17 @@ #![no_std] -mod defindex; +mod vault; mod events; mod storage; mod error; +use common::models::AssetStrategySet; use soroban_sdk::{ - contract, contractimpl, Address, BytesN, Env, Map, String, Vec + contract, contractimpl, vec, Address, BytesN, Env, Map, String, Symbol, Val, Vec, IntoVal }; use error::FactoryError; -use defindex::{create_contract, AssetStrategySet}; -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 }; +pub use vault::create_contract; +use storage::{ add_new_defindex, extend_instance_ttl, get_admin, get_vault_wasm_hash, get_defindex_receiver, get_deployed_defindexes, get_fee_rate, has_admin, put_admin, put_vault_wasm_hash, put_defindex_receiver, put_defindex_fee }; fn check_initialized(e: &Env) -> Result<(), FactoryError> { if !has_admin(e) { @@ -27,7 +28,7 @@ pub trait FactoryTrait { /// * `admin` - The address of the contract administrator, who can manage settings. /// * `defindex_receiver` - The default address designated to receive a portion of fees. /// * `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. + /// * `vault_wasm_hash` - The hash of the DeFindex Vault's WASM file for deploying new vaults. /// /// # Returns /// * `Result<(), FactoryError>` - Returns Ok(()) if successful, otherwise an error. @@ -36,7 +37,7 @@ pub trait FactoryTrait { admin: Address, defindex_receiver: Address, defindex_fee: u32, - defindex_wasm_hash: BytesN<32> + vault_wasm_hash: BytesN<32> ) -> Result<(), FactoryError>; /// Creates a new DeFindex Vault with specified parameters. @@ -180,7 +181,7 @@ impl FactoryTrait for DeFindexFactory { /// * `admin` - The address of the contract administrator, who can manage settings. /// * `defindex_receiver` - The default address designated to receive a portion of fees. /// * `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. + /// * `vault_wasm_hash` - The hash of the DeFindex Vault's WASM file for deploying new vaults. /// /// # Returns /// * `Result<(), FactoryError>` - Returns Ok(()) if successful, otherwise an error. @@ -189,7 +190,7 @@ impl FactoryTrait for DeFindexFactory { admin: Address, defindex_receiver: Address, defindex_fee: u32, - defi_wasm_hash: BytesN<32> + vault_wasm_hash: BytesN<32> ) -> Result<(), FactoryError> { if has_admin(&e) { return Err(FactoryError::AlreadyInitialized); @@ -197,7 +198,7 @@ impl FactoryTrait for DeFindexFactory { put_admin(&e, &admin); put_defindex_receiver(&e, &defindex_receiver); - put_defi_wasm_hash(&e, defi_wasm_hash); + put_vault_wasm_hash(&e, vault_wasm_hash); put_defindex_fee(&e, &defindex_fee); events::emit_initialized(&e, admin, defindex_receiver, defindex_fee); @@ -233,22 +234,23 @@ impl FactoryTrait for DeFindexFactory { let current_contract = e.current_contract_address(); - let defi_wasm_hash = get_defi_wasm_hash(&e)?; - let defindex_address = create_contract(&e, defi_wasm_hash, salt); + let vault_wasm_hash = get_vault_wasm_hash(&e)?; + let defindex_address = create_contract(&e, vault_wasm_hash, salt); let defindex_receiver = get_defindex_receiver(&e); - defindex::Client::new(&e, &defindex_address).initialize( - &assets, - &manager, - &emergency_manager, - &fee_receiver, - &vault_fee, - &defindex_receiver, - ¤t_contract, - &vault_name, - &vault_symbol, - ); + let mut init_args: Vec = vec![&e]; + init_args.push_back(assets.to_val()); + init_args.push_back(manager.to_val()); + init_args.push_back(emergency_manager.to_val()); + init_args.push_back(fee_receiver.to_val()); + init_args.push_back(vault_fee.into_val(&e)); + init_args.push_back(defindex_receiver.to_val()); + init_args.push_back(current_contract.to_val()); + init_args.push_back(vault_name.to_val()); + init_args.push_back(vault_symbol.to_val()); + + e.invoke_contract::(&defindex_address, &Symbol::new(&e, "initialize"), init_args); add_new_defindex(&e, defindex_address.clone()); events::emit_create_defindex_vault(&e, emergency_manager, fee_receiver, manager, vault_fee, assets); @@ -293,35 +295,35 @@ impl FactoryTrait for DeFindexFactory { let current_contract = e.current_contract_address(); - let defi_wasm_hash = get_defi_wasm_hash(&e)?; - let defindex_address = create_contract(&e, defi_wasm_hash, salt); + let vault_wasm_hash = get_vault_wasm_hash(&e)?; + let defindex_address = create_contract(&e, vault_wasm_hash, salt); let defindex_receiver = get_defindex_receiver(&e); - let defindex_client = defindex::Client::new(&e, &defindex_address); + let mut init_args: Vec = vec![&e]; + init_args.push_back(assets.to_val()); + init_args.push_back(manager.to_val()); + init_args.push_back(emergency_manager.to_val()); + init_args.push_back(fee_receiver.to_val()); + init_args.push_back(vault_fee.into_val(&e)); + init_args.push_back(defindex_receiver.to_val()); + init_args.push_back(current_contract.to_val()); + init_args.push_back(vault_name.to_val()); + init_args.push_back(vault_symbol.to_val()); - defindex_client.initialize( - &assets, - &manager, - &emergency_manager, - &fee_receiver, - &vault_fee, - &defindex_receiver, - ¤t_contract, - &vault_name, - &vault_symbol, - ); + e.invoke_contract::(&defindex_address, &Symbol::new(&e, "initialize"), init_args); let mut amounts_min = Vec::new(&e); for _ in 0..amounts.len() { amounts_min.push_back(0i128); } - defindex_client.deposit( - &amounts, - &amounts_min, - &caller - ); + let mut deposit_args: Vec = vec![&e]; + deposit_args.push_back(amounts.to_val()); + deposit_args.push_back(amounts_min.to_val()); + deposit_args.push_back(caller.to_val()); + + e.invoke_contract::(&defindex_address, &Symbol::new(&e, "deposit"), deposit_args); add_new_defindex(&e, defindex_address.clone()); events::emit_create_defindex_vault(&e, emergency_manager, fee_receiver, manager, vault_fee, assets); diff --git a/apps/contracts/factory/src/storage.rs b/apps/contracts/factory/src/storage.rs index bde91cd0..7dca9473 100644 --- a/apps/contracts/factory/src/storage.rs +++ b/apps/contracts/factory/src/storage.rs @@ -42,14 +42,14 @@ fn get_persistent_extend_or_error>( } } -pub fn get_defi_wasm_hash(e: &Env) -> Result, FactoryError>{ +pub fn get_vault_wasm_hash(e: &Env) -> Result, FactoryError>{ let key = DataKey::DeFindexWasmHash; get_persistent_extend_or_error(&e, &key, FactoryError::NotInitialized) } -pub fn put_defi_wasm_hash(e: &Env, pair_wasm_hash: BytesN<32>) { +pub fn put_vault_wasm_hash(e: &Env, vault_wasm_hash: BytesN<32>) { let key = DataKey::DeFindexWasmHash; - e.storage().persistent().set(&key, &pair_wasm_hash); + e.storage().persistent().set(&key, &vault_wasm_hash); e.storage() .persistent() .extend_ttl(&key, PERSISTENT_LIFETIME_THRESHOLD, PERSISTENT_BUMP_AMOUNT) diff --git a/apps/contracts/factory/src/test.rs b/apps/contracts/factory/src/test.rs index bf5f7980..77d207c4 100644 --- a/apps/contracts/factory/src/test.rs +++ b/apps/contracts/factory/src/test.rs @@ -1,7 +1,7 @@ #![cfg(test)] extern crate std; -use crate::defindex::{AssetStrategySet, Strategy}; use crate::{DeFindexFactory, DeFindexFactoryClient}; +use common::models::{AssetStrategySet, Strategy}; use soroban_sdk::token::{ StellarAssetClient as SorobanTokenAdminClient, TokenClient as SorobanTokenClient, }; @@ -157,5 +157,4 @@ impl<'a> DeFindexFactoryTest<'a> { mod admin; mod initialize; -mod create_defindex; -mod all_flow; \ No newline at end of file +mod create_defindex; \ No newline at end of file diff --git a/apps/contracts/factory/src/test/all_flow.rs b/apps/contracts/factory/src/test/all_flow.rs deleted file mode 100644 index a9fe93f4..00000000 --- a/apps/contracts/factory/src/test/all_flow.rs +++ /dev/null @@ -1,192 +0,0 @@ -use soroban_sdk::{vec, BytesN, String}; - -use crate::test::{ - create_asset_params, - defindex_vault_contract::{ - self, - AssetInvestmentAllocation, - StrategyInvestment, - }, DeFindexFactoryTest}; - -#[test] -fn test_deposit_success() { - let test = DeFindexFactoryTest::setup(); - test.env.mock_all_auths(); - - test.factory_contract.initialize(&test.admin, &test.defindex_receiver, &100u32, &test.defindex_wasm_hash); - - let asset_params = create_asset_params(&test); - - let salt = BytesN::from_array(&test.env, &[0; 32]); - - test.factory_contract.create_defindex_vault( - &test.emergency_manager, - &test.fee_receiver, - &2000u32, - &String::from_str(&test.env, "dfToken"), - &String::from_str(&test.env, "DFT"), - &test.manager, - &asset_params, - &salt - ); - - let deployed_defindexes = test.factory_contract.deployed_defindexes(); - assert_eq!(deployed_defindexes.len(), 1); - - let defindex_address = deployed_defindexes.get(0).unwrap(); - let defindex_contract = defindex_vault_contract::Client::new(&test.env, &defindex_address); - - let amount_token0 = 1_000i128; - let amount_token1 = 12_000i128; - - let users = DeFindexFactoryTest::generate_random_users(&test.env, 1); - - test.token0_admin_client.mint(&users[0], &amount_token0); - let user_balance = test.token0.balance(&users[0]); - assert_eq!(user_balance, amount_token0); - - test.token1_admin_client.mint(&users[0], &amount_token1); - let user_balance = test.token1.balance(&users[0]); - assert_eq!(user_balance, amount_token1); - - let df_balance = defindex_contract.balance(&users[0]); - assert_eq!(df_balance, 0i128); - - defindex_contract.deposit(&vec![&test.env, amount_token0, amount_token1], &vec![&test.env, 0, 0], &users[0]); - - let df_balance = defindex_contract.balance(&users[0]); - assert_eq!(df_balance, amount_token0 + amount_token1 - 1000); // TODO: The amount of dfTokens minted is the sum of both asset deposited? - - - // defindex_contract.withdraw(&df_balance, &users[0]); - - // let df_balance = defindex_contract.user_balance(&users[0]); - // assert_eq!(df_balance, 0i128); - - // let user_balance = test.token0.balance(&users[0]); - // assert_eq!(user_balance, amount); - -} - -#[test] -fn test_withdraw_success() { - let test = DeFindexFactoryTest::setup(); - test.env.mock_all_auths(); - - test.factory_contract.initialize(&test.admin, &test.defindex_receiver, &100u32, &test.defindex_wasm_hash); - - let asset_params = create_asset_params(&test); - - let salt = BytesN::from_array(&test.env, &[0; 32]); - - test.factory_contract.create_defindex_vault( - &test.emergency_manager, - &test.fee_receiver, - &2000u32, - &String::from_str(&test.env, "dfToken"), - &String::from_str(&test.env, "DFT"), - &test.manager, - &asset_params, - &salt - ); - - let deployed_defindexes = test.factory_contract.deployed_defindexes(); - assert_eq!(deployed_defindexes.len(), 1); - - let defindex_address = deployed_defindexes.get(0).unwrap(); - let defindex_contract = defindex_vault_contract::Client::new(&test.env, &defindex_address); - - let amount_token0 = 1_000i128; - let amount_token1 = 12_000i128; - - let users = DeFindexFactoryTest::generate_random_users(&test.env, 1); - - test.token0_admin_client.mint(&users[0], &amount_token0); - let user_balance = test.token0.balance(&users[0]); - assert_eq!(user_balance, amount_token0); - - test.token1_admin_client.mint(&users[0], &amount_token1); - let user_balance = test.token1.balance(&users[0]); - assert_eq!(user_balance, amount_token1); - - let df_balance = defindex_contract.balance(&users[0]); - assert_eq!(df_balance, 0i128); - - defindex_contract.deposit(&vec![&test.env, amount_token0, amount_token1], &vec![&test.env, 0, 0], &users[0]); - - let df_balance = defindex_contract.balance(&users[0]); - assert_eq!(df_balance.clone(), amount_token0 + amount_token1 - 1000); // TODO: The amount of dfTokens minted is the sum of both asset deposited? - - let vault_token0_balance = test.token0.balance(&defindex_contract.address); - assert_eq!(vault_token0_balance, amount_token0); - - let vault_token1_balance = test.token1.balance(&defindex_contract.address); - assert_eq!(vault_token1_balance, amount_token1); - - let investments = vec![ - &test.env, - Some(AssetInvestmentAllocation { - asset: test.token0.address.clone(), - strategy_investments: vec![ - &test.env, - Some(StrategyInvestment { - strategy: test.strategy_contract_token0.address.clone(), - amount: amount_token0, - }), - ], - }), - Some(AssetInvestmentAllocation { - asset: test.token1.address.clone(), - strategy_investments: vec![ - &test.env, - Some(StrategyInvestment { - strategy: test.strategy_contract_token1.address.clone(), - amount: amount_token1, - }), - ], - }) - ]; - - - defindex_contract.invest(&investments); - - let vault_token0_balance = test.token0.balance(&defindex_contract.address); - assert_eq!(vault_token0_balance, 0i128); - - let vault_token1_balance = test.token1.balance(&defindex_contract.address); - assert_eq!(vault_token1_balance, 0i128); - - let strategy_token0_balance = test.token0.balance(&test.strategy_contract_token0.address); - assert_eq!(strategy_token0_balance, amount_token0); - - let strategy_token1_balance = test.token1.balance(&test.strategy_contract_token1.address); - assert_eq!(strategy_token1_balance, amount_token1); - - // let test_fee = defindex_contract.asses_fees(); - // assert_eq!(test_fee, 0i128); - - let withdraw_result = defindex_contract.withdraw(&df_balance, &users[0]); - assert_eq!(withdraw_result, vec![&test.env, 12000i128, 1000i128]); - - let df_balance = defindex_contract.balance(&users[0]); - assert_eq!(df_balance, 0i128); - - let user_balance = test.token0.balance(&users[0]); - assert_eq!(user_balance, amount_token0); - - let user_balance = test.token1.balance(&users[0]); - assert_eq!(user_balance, amount_token1); - - let vault_token0_balance = test.token0.balance(&defindex_contract.address); - assert_eq!(vault_token0_balance, 0i128); - - let vault_token1_balance = test.token1.balance(&defindex_contract.address); - assert_eq!(vault_token1_balance, 0i128); - - let strategy_token0_balance = test.token0.balance(&test.strategy_contract_token0.address); - assert_eq!(strategy_token0_balance, 0i128); - - let strategy_token1_balance = test.token1.balance(&test.strategy_contract_token1.address); - assert_eq!(strategy_token1_balance, 0i128); - -} \ No newline at end of file diff --git a/apps/contracts/factory/src/vault.rs b/apps/contracts/factory/src/vault.rs new file mode 100644 index 00000000..2df724f3 --- /dev/null +++ b/apps/contracts/factory/src/vault.rs @@ -0,0 +1,14 @@ +#![allow(unused)] +use soroban_sdk::{contracttype, contracterror, xdr::ToXdr, Address, Bytes, BytesN, Env, Vec}; + +// Define a function to create a new contract instance +pub fn create_contract( + e: &Env, // Pass in the current environment as an argument + defindex_wasm_hash: BytesN<32>, // Pass in the hash of the token contract's WASM file + salt: BytesN<32>, +) -> Address { + + e.deployer() + .with_current_contract(e.crypto().sha256(&salt.into())) + .deploy(defindex_wasm_hash) +} \ No newline at end of file diff --git a/apps/contracts/integration-test/Cargo.toml b/apps/contracts/integration-test/Cargo.toml new file mode 100644 index 00000000..4debd7cf --- /dev/null +++ b/apps/contracts/integration-test/Cargo.toml @@ -0,0 +1,14 @@ + +[package] +name = "integration-test" +version = "0.0.0" +authors = ["coderipper "] +edition = "2021" +publish = false + +[dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } +factory = { path = "../factory", package = "defindex-factory" } +vault = { path = "../vault", package = "defindex-vault"} +hodl-strategy={ path="../strategies/hodl", package="hodl_strategy"} +common = { workspace = true } \ No newline at end of file diff --git a/apps/contracts/integration-test/src/factory.rs b/apps/contracts/integration-test/src/factory.rs new file mode 100644 index 00000000..c69fddd0 --- /dev/null +++ b/apps/contracts/integration-test/src/factory.rs @@ -0,0 +1,16 @@ +mod factory_contract { + soroban_sdk::contractimport!(file = "../target/wasm32-unknown-unknown/release/defindex_factory.optimized.wasm"); + pub type DeFindexFactoryClient<'a> = Client<'a>; +} + +pub use factory_contract::{AssetStrategySet, Strategy, DeFindexFactoryClient}; +use soroban_sdk::{Address, BytesN, Env}; + +// DeFindex Factory Contract +pub fn create_factory_contract<'a>(e: &Env, admin: &Address, defindex_receiver: &Address, defindex_fee: &u32, vault_wasm_hash: &BytesN<32>) -> DeFindexFactoryClient<'a> { + let address = &e.register_contract_wasm(None, factory_contract::WASM); + let factory = DeFindexFactoryClient::new(e, address); + + factory.initialize(admin, defindex_receiver, defindex_fee, vault_wasm_hash); + factory +} \ No newline at end of file diff --git a/apps/contracts/integration-test/src/fixed_strategy.rs b/apps/contracts/integration-test/src/fixed_strategy.rs new file mode 100644 index 00000000..1b920aa2 --- /dev/null +++ b/apps/contracts/integration-test/src/fixed_strategy.rs @@ -0,0 +1,18 @@ +// DeFindex Hodl Strategy Contract +mod fixed_strategy { + soroban_sdk::contractimport!(file = "../target/wasm32-unknown-unknown/release/fixed_apr_strategy.optimized.wasm"); + pub type FixedStrategyClient<'a> = Client<'a>; +} + +pub use fixed_strategy::FixedStrategyClient; +use soroban_sdk::{Address, Env, Val, Vec}; + +pub fn create_fixed_strategy_contract<'a>(e: &Env, asset: &Address, init_args: &Vec) -> FixedStrategyClient<'a> { + let address = &e.register_contract_wasm(None, fixed_strategy::WASM); + let strategy = FixedStrategyClient::new(e, address); + + strategy.mock_all_auths().initialize(asset, init_args); + strategy +} + + diff --git a/apps/contracts/integration-test/src/hodl_strategy.rs b/apps/contracts/integration-test/src/hodl_strategy.rs new file mode 100644 index 00000000..bea4c737 --- /dev/null +++ b/apps/contracts/integration-test/src/hodl_strategy.rs @@ -0,0 +1,15 @@ +// DeFindex Hodl Strategy Contract +mod hodl_strategy { + soroban_sdk::contractimport!(file = "../target/wasm32-unknown-unknown/release/hodl_strategy.optimized.wasm"); + pub type HodlStrategyClient<'a> = Client<'a>; +} + +pub use hodl_strategy::HodlStrategyClient; +use soroban_sdk::{Address, Env, Val, Vec}; + +pub fn create_hodl_strategy_contract<'a>(e: &Env, asset: &Address, init_args: &Vec) -> HodlStrategyClient<'a> { + let address = &e.register_contract_wasm(None, hodl_strategy::WASM); + let strategy = HodlStrategyClient::new(e, address); + strategy.initialize(asset, init_args); + strategy +} \ No newline at end of file diff --git a/apps/contracts/integration-test/src/lib.rs b/apps/contracts/integration-test/src/lib.rs new file mode 100644 index 00000000..5eaa850f --- /dev/null +++ b/apps/contracts/integration-test/src/lib.rs @@ -0,0 +1,8 @@ +#![allow(clippy::all)] +pub mod test; +pub mod token; +pub mod hodl_strategy; +pub mod fixed_strategy; +pub mod vault; +pub mod factory; +pub mod setup; \ No newline at end of file diff --git a/apps/contracts/integration-test/src/setup.rs b/apps/contracts/integration-test/src/setup.rs new file mode 100644 index 00000000..e815b96a --- /dev/null +++ b/apps/contracts/integration-test/src/setup.rs @@ -0,0 +1,263 @@ +use soroban_sdk::token::{StellarAssetClient, TokenClient}; +use soroban_sdk::{BytesN, Val, IntoVal}; +use soroban_sdk::{testutils::Address as _, vec as sorobanvec, Address, String, Vec}; + +use crate::fixed_strategy::{create_fixed_strategy_contract, FixedStrategyClient}; +use crate::hodl_strategy::{create_hodl_strategy_contract, HodlStrategyClient}; +use crate::test::IntegrationTest; +use crate::token::create_token; +use crate::factory::{AssetStrategySet, Strategy}; +use crate::vault::defindex_vault_contract::VaultContractClient; + +pub struct VaultOneAseetHodlStrategy<'a> { + pub setup: IntegrationTest<'a>, + pub token: TokenClient<'a>, + pub token_admin: Address, + pub token_admin_client: StellarAssetClient<'a>, + pub strategy_contract: HodlStrategyClient<'a>, + pub vault_contract: VaultContractClient<'a>, + pub manager: Address, + pub emergency_manager: Address, + pub fee_receiver: Address, + pub vault_fee: u32, +} + +pub static VAULT_FEE: u32 = 100; + +pub fn create_vault_one_asset_hodl_strategy<'a>() -> VaultOneAseetHodlStrategy<'a> { + let setup = IntegrationTest::setup(); + + let token_admin = Address::generate(&setup.env); + let (token, token_admin_client) = create_token(&setup.env, &token_admin); + + let strategy_contract = create_hodl_strategy_contract(&setup.env, &token.address, &sorobanvec![&setup.env]); + + let emergency_manager = Address::generate(&setup.env); + let fee_receiver = Address::generate(&setup.env); + let vault_fee = VAULT_FEE; + let vault_name = String::from_str(&setup.env, "HodlVault"); + let vault_symbol = String::from_str(&setup.env, "HVLT"); + let manager = Address::generate(&setup.env); + + let assets = sorobanvec![ + &setup.env, + AssetStrategySet { + address: token.address.clone(), + strategies: sorobanvec![ + &setup.env, + Strategy { + address: strategy_contract.address.clone(), + name: String::from_str(&setup.env, "Hodl Strategy"), + paused: false, + } + ], + } + ]; + + let salt = BytesN::from_array(&setup.env, &[0; 32]); + + let vault_contract_address = setup.factory_contract.create_defindex_vault( + &emergency_manager, + &fee_receiver, + &vault_fee, + &vault_name, + &vault_symbol, + &manager, + &assets, + &salt + ); + + let vault_contract = VaultContractClient::new(&setup.env, &vault_contract_address); + + VaultOneAseetHodlStrategy { + setup, + token, + token_admin, + token_admin_client, + strategy_contract, + vault_contract, + manager, + emergency_manager, + fee_receiver, + vault_fee, + } +} + +pub struct VaultOneAseetFixedStrategy<'a> { + pub setup: IntegrationTest<'a>, + pub token: TokenClient<'a>, + pub token_admin: Address, + pub token_admin_client: StellarAssetClient<'a>, + pub strategy_contract: FixedStrategyClient<'a>, + pub vault_contract: VaultContractClient<'a>, + pub manager: Address, + pub emergency_manager: Address, + pub fee_receiver: Address, + pub vault_fee: u32, +} + +pub fn create_vault_one_asset_fixed_strategy<'a>() -> VaultOneAseetFixedStrategy<'a> { + let setup = IntegrationTest::setup(); + + let token_admin = Address::generate(&setup.env); + let (token, token_admin_client) = create_token(&setup.env, &token_admin); + + let strategy_admin = Address::generate(&setup.env); + let starting_amount = 100_000_000_000_0_000_000i128; + token_admin_client.mock_all_auths().mint(&strategy_admin, &starting_amount); + + let init_fn_args: Vec = sorobanvec![&setup.env, + 1000u32.into_val(&setup.env), + strategy_admin.into_val(&setup.env), + starting_amount.into_val(&setup.env), + ]; + + let strategy_contract = create_fixed_strategy_contract(&setup.env, &token.address, &init_fn_args); + + let emergency_manager = Address::generate(&setup.env); + let fee_receiver = Address::generate(&setup.env); + let vault_fee = VAULT_FEE; + let vault_name = String::from_str(&setup.env, "FixedVault"); + let vault_symbol = String::from_str(&setup.env, "FVLT"); + let manager = Address::generate(&setup.env); + + let assets = sorobanvec![ + &setup.env, + AssetStrategySet { + address: token.address.clone(), + strategies: sorobanvec![ + &setup.env, + Strategy { + address: strategy_contract.address.clone(), + name: String::from_str(&setup.env, "Fixed Strategy"), + paused: false, + } + ], + } + ]; + + let salt = BytesN::from_array(&setup.env, &[0; 32]); + + let vault_contract_address = setup.factory_contract.create_defindex_vault( + &emergency_manager, + &fee_receiver, + &vault_fee, + &vault_name, + &vault_symbol, + &manager, + &assets, + &salt + ); + + let vault_contract = VaultContractClient::new(&setup.env, &vault_contract_address); + + VaultOneAseetFixedStrategy { + setup, + token, + token_admin, + token_admin_client, + strategy_contract, + vault_contract, + manager, + emergency_manager, + fee_receiver, + vault_fee, + } +} + +#[cfg(test)] +mod tests { + use crate::vault::{VaultAssetStrategySet, VaultStrategy}; + + use super::*; + + #[test] + fn test_create_vault_one_asset_hodl_strategy() { + let enviroment = create_vault_one_asset_hodl_strategy(); + let setup = enviroment.setup; + assert_eq!(setup.factory_contract.deployed_defindexes().len(), 1); + + let strategy_token = enviroment.strategy_contract.asset(); + assert_eq!(strategy_token, enviroment.token.address); + + let assets = sorobanvec![ + &setup.env, + VaultAssetStrategySet { + address: enviroment.token.address.clone(), + strategies: sorobanvec![ + &setup.env, + VaultStrategy { + address: enviroment.strategy_contract.address.clone(), + name: String::from_str(&setup.env, "Hodl Strategy"), + paused: false, + } + ], + } + ]; + + let vault_assets = enviroment.vault_contract.get_assets(); + assert_eq!(vault_assets, assets); + + let vault_emergency_manager = enviroment.vault_contract.get_emergency_manager(); + assert_eq!(vault_emergency_manager, enviroment.emergency_manager); + + let vault_fee_receiver = enviroment.vault_contract.get_fee_receiver(); + assert_eq!(vault_fee_receiver, enviroment.fee_receiver); + + let vault_manager = enviroment.vault_contract.get_manager(); + assert_eq!(vault_manager, enviroment.manager); + + let vault_name = enviroment.vault_contract.name(); + assert_eq!(vault_name, String::from_str(&setup.env, "HodlVault")); + + let vault_symbol = enviroment.vault_contract.symbol(); + assert_eq!(vault_symbol, String::from_str(&setup.env, "HVLT")); + } + + #[test] + fn test_create_vault_one_asset_fixed_strategy() { + let enviroment = create_vault_one_asset_fixed_strategy(); + let setup = enviroment.setup; + assert_eq!(setup.factory_contract.deployed_defindexes().len(), 1); + + let strategy_token = enviroment.strategy_contract.asset(); + assert_eq!(strategy_token, enviroment.token.address); + + let assets = sorobanvec![ + &setup.env, + VaultAssetStrategySet { + address: enviroment.token.address.clone(), + strategies: sorobanvec![ + &setup.env, + VaultStrategy { + address: enviroment.strategy_contract.address.clone(), + name: String::from_str(&setup.env, "Fixed Strategy"), + paused: false, + } + ], + } + ]; + + let vault_assets = enviroment.vault_contract.get_assets(); + assert_eq!(vault_assets, assets); + + let strategy_contract_balance = enviroment.token.balance(&enviroment.strategy_contract.address); + assert_eq!(strategy_contract_balance, 100_000_000_000_0_000_000i128); + + let vault_emergency_manager = enviroment.vault_contract.get_emergency_manager(); + assert_eq!(vault_emergency_manager, enviroment.emergency_manager); + + let vault_fee_receiver = enviroment.vault_contract.get_fee_receiver(); + assert_eq!(vault_fee_receiver, enviroment.fee_receiver); + + let vault_manager = enviroment.vault_contract.get_manager(); + assert_eq!(vault_manager, enviroment.manager); + + let vault_name = enviroment.vault_contract.name(); + assert_eq!(vault_name, String::from_str(&setup.env, "FixedVault")); + + let vault_symbol = enviroment.vault_contract.symbol(); + assert_eq!(vault_symbol, String::from_str(&setup.env, "FVLT")); + } + +} \ No newline at end of file diff --git a/apps/contracts/integration-test/src/test.rs b/apps/contracts/integration-test/src/test.rs new file mode 100644 index 00000000..e3d16982 --- /dev/null +++ b/apps/contracts/integration-test/src/test.rs @@ -0,0 +1,58 @@ +extern crate std; +use crate::factory::DeFindexFactoryClient; +use soroban_sdk::{ + Env, + Address, + testutils::Address as _, +}; +use std::vec as std_vec; + +use crate::factory::create_factory_contract; +use crate::vault::defindex_vault_contract; + +pub static ONE_YEAR_IN_SECONDS: u64 = 31_536_000; +pub static DEFINDEX_FEE: u32 = 50; + +pub struct IntegrationTest<'a> { + pub env: Env, + pub factory_contract: DeFindexFactoryClient<'a>, + pub admin: Address, + pub defindex_receiver: Address, + pub defindex_fee: u32 +} + +impl<'a> IntegrationTest<'a> { + pub fn setup() -> Self { + let env = Env::default(); + + let admin = Address::generate(&env); + let defindex_receiver = Address::generate(&env); + + let vault_wasm_hash = env.deployer().upload_contract_wasm(defindex_vault_contract::WASM); + let defindex_fee = DEFINDEX_FEE; + + let factory_contract = create_factory_contract(&env, &admin, &defindex_receiver, &defindex_fee, &vault_wasm_hash); + + env.budget().reset_unlimited(); + + IntegrationTest { + env, + factory_contract, + admin, + defindex_receiver, + defindex_fee + } + } + + pub fn generate_random_users(e: &Env, users_count: u32) -> std_vec::Vec
{ + let mut users = std_vec![]; + for _c in 0..users_count { + users.push(Address::generate(e)); + } + users + } +} + +#[cfg(test)] +mod test_vault_one_hodl_strategy; +mod test_vault_one_fixed_strategy; \ No newline at end of file diff --git a/apps/contracts/integration-test/src/test/test_vault_one_fixed_strategy/mod.rs b/apps/contracts/integration-test/src/test/test_vault_one_fixed_strategy/mod.rs new file mode 100644 index 00000000..33ee3550 --- /dev/null +++ b/apps/contracts/integration-test/src/test/test_vault_one_fixed_strategy/mod.rs @@ -0,0 +1,13 @@ +use super::ONE_YEAR_IN_SECONDS; + +mod test_deposit; +mod test_withdraw; + +pub 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 = ONE_YEAR_IN_SECONDS; + let apr_bps = apr as i128; + let time_elapsed_i128 = time_elapsed as i128; + + (user_balance * apr_bps * time_elapsed_i128) / (seconds_per_year as i128 * 10000) +} \ No newline at end of file diff --git a/apps/contracts/integration-test/src/test/test_vault_one_fixed_strategy/test_deposit.rs b/apps/contracts/integration-test/src/test/test_vault_one_fixed_strategy/test_deposit.rs new file mode 100644 index 00000000..693ff7c8 --- /dev/null +++ b/apps/contracts/integration-test/src/test/test_vault_one_fixed_strategy/test_deposit.rs @@ -0,0 +1,320 @@ +use crate::{setup::create_vault_one_asset_fixed_strategy, test::IntegrationTest, vault::{VaultContractError, MINIMUM_LIQUIDITY}}; +use soroban_sdk::{testutils::{MockAuth, MockAuthInvoke}, vec as svec, IntoVal, Vec}; + +#[test] +fn test_fixed_apr_deposit_success() { + let enviroment = create_vault_one_asset_fixed_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 100_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + let user_balance = enviroment.token.balance(user); + assert_eq!(user_balance, user_starting_balance); + + let deposit_amount = 10_000_0_000_000i128; + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user); + + let vault_balance = enviroment.token.balance(&enviroment.vault_contract.address); + assert_eq!(vault_balance, deposit_amount); + + let user_balance_after_deposit = enviroment.token.balance(user); + assert_eq!(user_balance_after_deposit, user_starting_balance - deposit_amount); + + let df_balance = enviroment.vault_contract.balance(&user); + assert_eq!(df_balance, deposit_amount - MINIMUM_LIQUIDITY); + + let total_supply = enviroment.vault_contract.total_supply(); + assert_eq!(total_supply, deposit_amount); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #10)")] +fn test_fixed_apr_deposit_insufficient_balance() { + let enviroment = create_vault_one_asset_fixed_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 5_000_0_000_000i128; // Less than deposit amount + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + let user_balance = enviroment.token.balance(user); + assert_eq!(user_balance, user_starting_balance); + + let deposit_amount = 10_000_0_000_000i128; + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user); +} + +#[test] +fn test_fixed_apr_deposit_multiple_users() { + let enviroment = create_vault_one_asset_fixed_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 100_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 2); + let user1 = &users[0]; + let user2 = &users[1]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user1, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user1, &user_starting_balance); + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user2, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user2, &user_starting_balance); + + let user1_balance = enviroment.token.balance(user1); + let user2_balance = enviroment.token.balance(user2); + assert_eq!(user1_balance, user_starting_balance); + assert_eq!(user2_balance, user_starting_balance); + + let deposit_amount = 10_000_0_000_000i128; + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user1.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user1.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user1.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user1); + + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user2.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user2.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user2.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user2); + + let vault_balance = enviroment.token.balance(&enviroment.vault_contract.address); + assert_eq!(vault_balance, deposit_amount * 2); + + let user1_balance_after_deposit = enviroment.token.balance(user1); + let user2_balance_after_deposit = enviroment.token.balance(user2); + assert_eq!(user1_balance_after_deposit, user_starting_balance - deposit_amount); + assert_eq!(user2_balance_after_deposit, user_starting_balance - deposit_amount); + + let df_balance_user1 = enviroment.vault_contract.balance(&user1); + let df_balance_user2 = enviroment.vault_contract.balance(&user2); + assert_eq!(df_balance_user1, deposit_amount - MINIMUM_LIQUIDITY); + assert_eq!(df_balance_user2, deposit_amount); + + let total_supply = enviroment.vault_contract.total_supply(); + assert_eq!(total_supply, deposit_amount * 2); +} + +#[test] +fn test_fixed_apr_deposit_zero_amount() { + let enviroment = create_vault_one_asset_fixed_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 100_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + let user_balance = enviroment.token.balance(user); + assert_eq!(user_balance, user_starting_balance); + + let deposit_amount = 0i128; + let result = enviroment.vault_contract.mock_all_auths().try_deposit( + &svec![&setup.env, deposit_amount], + &svec![&setup.env, deposit_amount], + &user, + ); + + assert_eq!(result, Err(Ok(VaultContractError::InsufficientAmount))); +} + +#[test] +fn test_fixed_apr_deposit_negative_amount() { + let enviroment = create_vault_one_asset_fixed_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 100_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + let user_balance = enviroment.token.balance(user); + assert_eq!(user_balance, user_starting_balance); + + let deposit_amount = -10_000_0_000_000i128; + let result = enviroment.vault_contract.mock_all_auths().try_deposit( + &svec![&setup.env, deposit_amount], + &svec![&setup.env, deposit_amount], + &user, + ); + + assert_eq!(result, Err(Ok(VaultContractError::NegativeNotAllowed))); +} + +#[test] +fn test_fixed_apr_deposit_insufficient_minimum_liquidity() { + let enviroment = create_vault_one_asset_fixed_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 100_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + let user_balance = enviroment.token.balance(user); + assert_eq!(user_balance, user_starting_balance); + + let deposit_amount = MINIMUM_LIQUIDITY - 1; + let result = enviroment.vault_contract.mock_all_auths().try_deposit( + &svec![&setup.env, deposit_amount], + &svec![&setup.env, deposit_amount], + &user, + ); + + assert_eq!(result, Err(Ok(VaultContractError::InsufficientAmount))); +} \ No newline at end of file diff --git a/apps/contracts/integration-test/src/test/test_vault_one_fixed_strategy/test_withdraw.rs b/apps/contracts/integration-test/src/test/test_vault_one_fixed_strategy/test_withdraw.rs new file mode 100644 index 00000000..708d05f4 --- /dev/null +++ b/apps/contracts/integration-test/src/test/test_vault_one_fixed_strategy/test_withdraw.rs @@ -0,0 +1,210 @@ +use crate::{setup::{create_vault_one_asset_fixed_strategy, VAULT_FEE}, test::{test_vault_one_fixed_strategy::calculate_yield, IntegrationTest, DEFINDEX_FEE, ONE_YEAR_IN_SECONDS}, vault::{defindex_vault_contract::{AssetInvestmentAllocation, StrategyInvestment}, MINIMUM_LIQUIDITY}}; +use soroban_sdk::{testutils::{Ledger, MockAuth, MockAuthInvoke}, vec as svec, IntoVal, Vec}; + +#[test] +fn test_fixed_apr_no_invest_withdraw_success() { + let enviroment = create_vault_one_asset_fixed_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 10_000_0_000_000i128; + let deposit_amount = 10_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + + enviroment.vault_contract.mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user); + + setup.env.ledger().set_timestamp(setup.env.ledger().timestamp() + ONE_YEAR_IN_SECONDS); + + // // TODO: The vault should call harvest method on the strategy contract + // enviroment.strategy_contract.mock_all_auths().harvest(&enviroment.vault_contract.address); + + let df_balance_before_withdraw = enviroment.vault_contract.balance(&user); + assert_eq!(df_balance_before_withdraw, deposit_amount - MINIMUM_LIQUIDITY); + + enviroment.vault_contract.mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "withdraw", + args: ( + df_balance_before_withdraw.clone(), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[] + }, + }]) + .withdraw(&df_balance_before_withdraw, &user); + + let charged_fee_user = (deposit_amount - MINIMUM_LIQUIDITY) * (DEFINDEX_FEE as i128 + VAULT_FEE as i128) / 10000; + let expected_amount_user = deposit_amount - MINIMUM_LIQUIDITY - charged_fee_user; + + let user_balance_after_withdraw = enviroment.token.balance(user); + assert_eq!(user_balance_after_withdraw, expected_amount_user); + + let vault_balance = enviroment.token.balance(&enviroment.vault_contract.address); + assert_eq!(vault_balance, charged_fee_user + MINIMUM_LIQUIDITY); + + let df_balance = enviroment.vault_contract.balance(&user); + assert_eq!(df_balance, 0); +} + +#[test] +fn test_fixed_apr_invest_withdraw_success() { + let enviroment = create_vault_one_asset_fixed_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 10_000_0_000_000i128; + let deposit_amount = 10_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + + enviroment.vault_contract.mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user); + + let investments = svec![ + &setup.env, + Some(AssetInvestmentAllocation { + asset: enviroment.token.address.clone(), + strategy_investments: svec![ + &setup.env, + Some(StrategyInvestment { + strategy: enviroment.strategy_contract.address.clone(), + amount: deposit_amount, + }), + ], + }), + ]; + + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &enviroment.manager.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "invest", + args: ( + Vec::from_array(&setup.env,[ + Some( + AssetInvestmentAllocation { + asset: enviroment.token.address.clone(), + strategy_investments: + svec![&setup.env, + Some(StrategyInvestment { + strategy: enviroment.strategy_contract.address.clone(), + amount: deposit_amount, + }) + ] + } + ) + ]), + ).into_val(&setup.env), + sub_invokes: &[] + }, + }]) + .invest(&investments); + + setup.env.ledger().set_timestamp(setup.env.ledger().timestamp() + ONE_YEAR_IN_SECONDS); + + // TODO: The vault should call harvest method on the strategy contract + enviroment.strategy_contract.mock_all_auths().harvest(&enviroment.vault_contract.address); + + let df_balance_before_withdraw = enviroment.vault_contract.balance(&user); + assert_eq!(df_balance_before_withdraw, deposit_amount - MINIMUM_LIQUIDITY); + + enviroment.vault_contract.mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "withdraw", + args: ( + df_balance_before_withdraw.clone(), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[] + }, + }]) + .withdraw(&df_balance_before_withdraw, &user); + + let user_expected_reward = calculate_yield(deposit_amount.clone(), 1000u32, ONE_YEAR_IN_SECONDS); + + let charged_fee_user = (deposit_amount + user_expected_reward - MINIMUM_LIQUIDITY) * (DEFINDEX_FEE as i128 + VAULT_FEE as i128) / 10000; + let expected_amount_user = deposit_amount + user_expected_reward - MINIMUM_LIQUIDITY - charged_fee_user; + + let user_balance_after_withdraw = enviroment.token.balance(user); + //TODO: 98 missing? + assert_eq!(user_balance_after_withdraw, expected_amount_user - 98); + + let vault_balance = enviroment.token.balance(&enviroment.vault_contract.address); + assert_eq!(vault_balance, 0); + + let df_balance = enviroment.vault_contract.balance(&user); + assert_eq!(df_balance, 0); +} \ No newline at end of file diff --git a/apps/contracts/integration-test/src/test/test_vault_one_hodl_strategy/mod.rs b/apps/contracts/integration-test/src/test/test_vault_one_hodl_strategy/mod.rs new file mode 100644 index 00000000..46f7cb99 --- /dev/null +++ b/apps/contracts/integration-test/src/test/test_vault_one_hodl_strategy/mod.rs @@ -0,0 +1,3 @@ +mod test_deposit; +mod test_withdraw; +mod test_invest; \ No newline at end of file diff --git a/apps/contracts/integration-test/src/test/test_vault_one_hodl_strategy/test_deposit.rs b/apps/contracts/integration-test/src/test/test_vault_one_hodl_strategy/test_deposit.rs new file mode 100644 index 00000000..3ae650b0 --- /dev/null +++ b/apps/contracts/integration-test/src/test/test_vault_one_hodl_strategy/test_deposit.rs @@ -0,0 +1,320 @@ +use crate::{setup::create_vault_one_asset_hodl_strategy, test::IntegrationTest, vault::{VaultContractError, MINIMUM_LIQUIDITY}}; +use soroban_sdk::{testutils::{MockAuth, MockAuthInvoke}, vec as svec, IntoVal, Vec}; + +#[test] +fn test_deposit_success() { + let enviroment = create_vault_one_asset_hodl_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 100_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + let user_balance = enviroment.token.balance(user); + assert_eq!(user_balance, user_starting_balance); + + let deposit_amount = 10_000_0_000_000i128; + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user); + + let vault_balance = enviroment.token.balance(&enviroment.vault_contract.address); + assert_eq!(vault_balance, deposit_amount); + + let user_balance_after_deposit = enviroment.token.balance(user); + assert_eq!(user_balance_after_deposit, user_starting_balance - deposit_amount); + + let df_balance = enviroment.vault_contract.balance(&user); + assert_eq!(df_balance, deposit_amount - MINIMUM_LIQUIDITY); + + let total_supply = enviroment.vault_contract.total_supply(); + assert_eq!(total_supply, deposit_amount); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #10)")] +fn test_deposit_insufficient_balance() { + let enviroment = create_vault_one_asset_hodl_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 5_000_0_000_000i128; // Less than deposit amount + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + let user_balance = enviroment.token.balance(user); + assert_eq!(user_balance, user_starting_balance); + + let deposit_amount = 10_000_0_000_000i128; + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user); +} + +#[test] +fn test_deposit_multiple_users() { + let enviroment = create_vault_one_asset_hodl_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 100_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 2); + let user1 = &users[0]; + let user2 = &users[1]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user1, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user1, &user_starting_balance); + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user2, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user2, &user_starting_balance); + + let user1_balance = enviroment.token.balance(user1); + let user2_balance = enviroment.token.balance(user2); + assert_eq!(user1_balance, user_starting_balance); + assert_eq!(user2_balance, user_starting_balance); + + let deposit_amount = 10_000_0_000_000i128; + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user1.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user1.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user1.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user1); + + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user2.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user2.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user2.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user2); + + let vault_balance = enviroment.token.balance(&enviroment.vault_contract.address); + assert_eq!(vault_balance, deposit_amount * 2); + + let user1_balance_after_deposit = enviroment.token.balance(user1); + let user2_balance_after_deposit = enviroment.token.balance(user2); + assert_eq!(user1_balance_after_deposit, user_starting_balance - deposit_amount); + assert_eq!(user2_balance_after_deposit, user_starting_balance - deposit_amount); + + let df_balance_user1 = enviroment.vault_contract.balance(&user1); + let df_balance_user2 = enviroment.vault_contract.balance(&user2); + assert_eq!(df_balance_user1, deposit_amount - MINIMUM_LIQUIDITY); + assert_eq!(df_balance_user2, deposit_amount); + + let total_supply = enviroment.vault_contract.total_supply(); + assert_eq!(total_supply, deposit_amount * 2); +} + +#[test] +fn test_deposit_zero_amount() { + let enviroment = create_vault_one_asset_hodl_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 100_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + let user_balance = enviroment.token.balance(user); + assert_eq!(user_balance, user_starting_balance); + + let deposit_amount = 0i128; + let result = enviroment.vault_contract.mock_all_auths().try_deposit( + &svec![&setup.env, deposit_amount], + &svec![&setup.env, deposit_amount], + &user, + ); + + assert_eq!(result, Err(Ok(VaultContractError::InsufficientAmount))); +} + +#[test] +fn test_deposit_negative_amount() { + let enviroment = create_vault_one_asset_hodl_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 100_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + let user_balance = enviroment.token.balance(user); + assert_eq!(user_balance, user_starting_balance); + + let deposit_amount = -10_000_0_000_000i128; + let result = enviroment.vault_contract.mock_all_auths().try_deposit( + &svec![&setup.env, deposit_amount], + &svec![&setup.env, deposit_amount], + &user, + ); + + assert_eq!(result, Err(Ok(VaultContractError::NegativeNotAllowed))); +} + +#[test] +fn test_deposit_insufficient_minimum_liquidity() { + let enviroment = create_vault_one_asset_hodl_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 100_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + let user_balance = enviroment.token.balance(user); + assert_eq!(user_balance, user_starting_balance); + + let deposit_amount = MINIMUM_LIQUIDITY - 1; + let result = enviroment.vault_contract.mock_all_auths().try_deposit( + &svec![&setup.env, deposit_amount], + &svec![&setup.env, deposit_amount], + &user, + ); + + assert_eq!(result, Err(Ok(VaultContractError::InsufficientAmount))); +} \ No newline at end of file diff --git a/apps/contracts/integration-test/src/test/test_vault_one_hodl_strategy/test_invest.rs b/apps/contracts/integration-test/src/test/test_vault_one_hodl_strategy/test_invest.rs new file mode 100644 index 00000000..035faf50 --- /dev/null +++ b/apps/contracts/integration-test/src/test/test_vault_one_hodl_strategy/test_invest.rs @@ -0,0 +1,426 @@ +use crate::{setup::create_vault_one_asset_hodl_strategy, test::{IntegrationTest, ONE_YEAR_IN_SECONDS}, vault::{defindex_vault_contract::{AssetInvestmentAllocation, StrategyInvestment}, VaultContractError, MINIMUM_LIQUIDITY}}; +use soroban_sdk::{testutils::{Ledger, MockAuth, MockAuthInvoke, Address as _}, vec as svec, Address, IntoVal, Vec}; + +#[test] +fn test_invest_success() { + let enviroment = create_vault_one_asset_hodl_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 100_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + let user_balance = enviroment.token.balance(user); + assert_eq!(user_balance, user_starting_balance); + + let deposit_amount = 10_000_0_000_000i128; + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user); + + let df_balance = enviroment.vault_contract.balance(&user); + assert_eq!(df_balance, deposit_amount - MINIMUM_LIQUIDITY); + + // Create investment strategies for the deposited tokens + let investments = svec![ + &setup.env, + Some(AssetInvestmentAllocation { + asset: enviroment.token.address.clone(), + strategy_investments: svec![ + &setup.env, + Some(StrategyInvestment { + strategy: enviroment.strategy_contract.address.clone(), + amount: deposit_amount, + }), + ], + }), + ]; + + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &enviroment.manager.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "invest", + args: ( + Vec::from_array(&setup.env,[ + Some( + AssetInvestmentAllocation { + asset: enviroment.token.address.clone(), + strategy_investments: + svec![&setup.env, + Some(StrategyInvestment { + strategy: enviroment.strategy_contract.address.clone(), + amount: deposit_amount, + }) + ] + } + ) + ]), + ).into_val(&setup.env), + sub_invokes: &[] + }, + }]) + .invest(&investments); + + setup.env.ledger().set_timestamp(setup.env.ledger().timestamp() + ONE_YEAR_IN_SECONDS); + + let token_balance_after_invest = enviroment.token.balance(&enviroment.vault_contract.address); + assert_eq!(token_balance_after_invest, 0); + + let strategy_balance = enviroment.strategy_contract.balance(&enviroment.vault_contract.address); + assert_eq!(strategy_balance, deposit_amount); +} + +#[test] +fn test_invest_exceeding_investing_lenght() { + let enviroment = create_vault_one_asset_hodl_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 100_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + let user_balance = enviroment.token.balance(user); + assert_eq!(user_balance, user_starting_balance); + + let deposit_amount = 10_000_0_000_000i128; + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user); + + let df_balance = enviroment.vault_contract.balance(&user); + assert_eq!(df_balance, deposit_amount - MINIMUM_LIQUIDITY); + + let asset_address_2 = Address::generate(&setup.env); + let strategy_address_2 = Address::generate(&setup.env); + // Create investment strategies exceeding the allowed number + let investments = svec![ + &setup.env, + Some(AssetInvestmentAllocation { + asset: enviroment.token.address.clone(), + strategy_investments: svec![ + &setup.env, + Some(StrategyInvestment { + strategy: enviroment.strategy_contract.address.clone(), + amount: deposit_amount, + }), + ], + }), + Some(AssetInvestmentAllocation { + asset: asset_address_2.clone(), + strategy_investments: svec![ + &setup.env, + Some(StrategyInvestment { + strategy: strategy_address_2.clone(), + amount: deposit_amount, + }), + ], + }), + ]; + + let result = enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &enviroment.manager.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "invest", + args: ( + Vec::from_array(&setup.env,[ + Some( + AssetInvestmentAllocation { + asset: enviroment.token.address.clone(), + strategy_investments: + svec![&setup.env, + Some(StrategyInvestment { + strategy: enviroment.strategy_contract.address.clone(), + amount: deposit_amount, + }) + ] + } + ), + Some( + AssetInvestmentAllocation { + asset: asset_address_2.clone(), + strategy_investments: + svec![&setup.env, + Some(StrategyInvestment { + strategy: strategy_address_2.clone(), + amount: deposit_amount, + }) + ] + } + ) + ]), + ).into_val(&setup.env), + sub_invokes: &[] + }, + }]) + .try_invest(&investments); + + assert_eq!(result, Err(Ok(VaultContractError::WrongInvestmentLength))); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #10)")] +fn test_invest_insufficient_balance() { + let enviroment = create_vault_one_asset_hodl_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 5_000_0_000_000i128; // Less than deposit amount + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + let user_balance = enviroment.token.balance(user); + assert_eq!(user_balance, user_starting_balance); + + let deposit_amount = 10_000_0_000_000i128; + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user); +} + +#[test] +fn test_invest_multiple_users() { + let enviroment = create_vault_one_asset_hodl_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 100_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 2); + let user1 = &users[0]; + let user2 = &users[1]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user1, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user1, &user_starting_balance); + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user2, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user2, &user_starting_balance); + + let user1_balance = enviroment.token.balance(user1); + let user2_balance = enviroment.token.balance(user2); + assert_eq!(user1_balance, user_starting_balance); + assert_eq!(user2_balance, user_starting_balance); + + let deposit_amount = 10_000_0_000_000i128; + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user1.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user1.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user1.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user1); + + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user2.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user2.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user2.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user2); + + let df_balance_user1 = enviroment.vault_contract.balance(&user1); + let df_balance_user2 = enviroment.vault_contract.balance(&user2); + assert_eq!(df_balance_user1, deposit_amount - MINIMUM_LIQUIDITY); + assert_eq!(df_balance_user2, deposit_amount); + + // Create investment strategies for the deposited tokens + let investments = svec![ + &setup.env, + Some(AssetInvestmentAllocation { + asset: enviroment.token.address.clone(), + strategy_investments: svec![ + &setup.env, + Some(StrategyInvestment { + strategy: enviroment.strategy_contract.address.clone(), + amount: deposit_amount*2, + }), + ], + }), + ]; + + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &enviroment.manager.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "invest", + args: ( + Vec::from_array(&setup.env,[ + Some( + AssetInvestmentAllocation { + asset: enviroment.token.address.clone(), + strategy_investments: + svec![&setup.env, + Some(StrategyInvestment { + strategy: enviroment.strategy_contract.address.clone(), + amount: deposit_amount*2, + }) + ] + } + ) + ]), + ).into_val(&setup.env), + sub_invokes: &[] + }, + }]) + .invest(&investments); + + setup.env.ledger().set_timestamp(setup.env.ledger().timestamp() + ONE_YEAR_IN_SECONDS); + + let token_balance_after_invest = enviroment.token.balance(&enviroment.vault_contract.address); + assert_eq!(token_balance_after_invest, 0); + + let strategy_balance = enviroment.strategy_contract.balance(&enviroment.vault_contract.address); + assert_eq!(strategy_balance, deposit_amount * 2); +} \ No newline at end of file diff --git a/apps/contracts/integration-test/src/test/test_vault_one_hodl_strategy/test_withdraw.rs b/apps/contracts/integration-test/src/test/test_vault_one_hodl_strategy/test_withdraw.rs new file mode 100644 index 00000000..057524ae --- /dev/null +++ b/apps/contracts/integration-test/src/test/test_vault_one_hodl_strategy/test_withdraw.rs @@ -0,0 +1,728 @@ +use crate::{setup::{create_vault_one_asset_hodl_strategy, VAULT_FEE}, test::{IntegrationTest, DEFINDEX_FEE, ONE_YEAR_IN_SECONDS}, vault::{defindex_vault_contract::{AssetInvestmentAllocation, StrategyInvestment}, VaultContractError, MINIMUM_LIQUIDITY}}; +use soroban_sdk::{testutils::{Ledger, MockAuth, MockAuthInvoke}, vec as svec, IntoVal, Vec}; + +#[test] +fn test_withdraw_no_invest_success() { + let enviroment = create_vault_one_asset_hodl_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 100_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + let user_balance = enviroment.token.balance(user); + assert_eq!(user_balance, user_starting_balance); + + let deposit_amount = 10_000_0_000_000i128; + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user); + + let df_balance = enviroment.vault_contract.balance(&user); + assert_eq!(df_balance, deposit_amount - MINIMUM_LIQUIDITY); + + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "withdraw", + args: ( + df_balance.clone(), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[] + }, + }]) + .withdraw(&df_balance, &user); + + let df_balance = enviroment.vault_contract.balance(&user); + assert_eq!(df_balance, 0); + + let vault_balance_after_withdraw = enviroment.token.balance(&enviroment.vault_contract.address); + assert_eq!(vault_balance_after_withdraw, MINIMUM_LIQUIDITY); + + let user_balance_after_withdraw = enviroment.token.balance(user); + assert_eq!(user_balance_after_withdraw, user_starting_balance - MINIMUM_LIQUIDITY); + + let df_balance = enviroment.vault_contract.balance(&user); + assert_eq!(df_balance, 0); + + let total_supply = enviroment.vault_contract.total_supply(); + assert_eq!(total_supply, MINIMUM_LIQUIDITY); +} + +#[test] +fn test_withdraw_partial_success() { + let enviroment = create_vault_one_asset_hodl_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 100_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + let user_balance = enviroment.token.balance(user); + assert_eq!(user_balance, user_starting_balance); + + let deposit_amount = 10_000_0_000_000i128; + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user); + + let df_balance = enviroment.vault_contract.balance(&user); + assert_eq!(df_balance, deposit_amount - MINIMUM_LIQUIDITY); + + let withdraw_amount = df_balance / 2; + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "withdraw", + args: ( + withdraw_amount.clone(), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[] + }, + }]) + .withdraw(&withdraw_amount, &user); + + let df_balance = enviroment.vault_contract.balance(&user); + assert_eq!(df_balance, withdraw_amount); + + let vault_balance_after_withdraw = enviroment.token.balance(&enviroment.vault_contract.address); + assert_eq!(vault_balance_after_withdraw, deposit_amount - withdraw_amount); + + let user_balance_after_withdraw = enviroment.token.balance(user); + assert_eq!(user_balance_after_withdraw, user_starting_balance - (deposit_amount - withdraw_amount)); +} + +#[test] +fn test_withdraw_insufficient_balance() { + let enviroment = create_vault_one_asset_hodl_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 100_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + let user_balance = enviroment.token.balance(user); + assert_eq!(user_balance, user_starting_balance); + + let deposit_amount = 10_000_0_000_000i128; + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user); + + let df_balance = enviroment.vault_contract.balance(&user); + assert_eq!(df_balance, deposit_amount - MINIMUM_LIQUIDITY); + + let withdraw_amount = df_balance + 1; // Attempt to withdraw more than the balance + let result = enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "withdraw", + args: ( + withdraw_amount.clone(), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[] + }, + }]) + .try_withdraw(&withdraw_amount, &user); + assert_eq!(result, Err(Ok(VaultContractError::InsufficientBalance))); + + let df_balance = enviroment.vault_contract.balance(&user); + assert_eq!(df_balance, deposit_amount - MINIMUM_LIQUIDITY); + + let vault_balance_after_withdraw = enviroment.token.balance(&enviroment.vault_contract.address); + assert_eq!(vault_balance_after_withdraw, deposit_amount); + + let user_balance_after_withdraw = enviroment.token.balance(user); + assert_eq!(user_balance_after_withdraw, user_starting_balance - deposit_amount); +} + +#[test] +fn test_withdraw_after_invest() { + let enviroment = create_vault_one_asset_hodl_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 10_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 1); + let user = &users[0]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user, &user_starting_balance); + let user_balance = enviroment.token.balance(user); + assert_eq!(user_balance, user_starting_balance); + + let deposit_amount = 10_000_0_000_000i128; + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user); + + let user_balance_after_deposit = enviroment.token.balance(user); + assert_eq!(user_balance_after_deposit, 0); + + let df_balance = enviroment.vault_contract.balance(&user); + assert_eq!(df_balance, deposit_amount - MINIMUM_LIQUIDITY); + + // Create investment strategies for the deposited tokens + let investments = svec![ + &setup.env, + Some(AssetInvestmentAllocation { + asset: enviroment.token.address.clone(), + strategy_investments: svec![ + &setup.env, + Some(StrategyInvestment { + strategy: enviroment.strategy_contract.address.clone(), + amount: deposit_amount, + }), + ], + }), + ]; + + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &enviroment.manager.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "invest", + args: ( + Vec::from_array(&setup.env,[ + Some( + AssetInvestmentAllocation { + asset: enviroment.token.address.clone(), + strategy_investments: + svec![&setup.env, + Some(StrategyInvestment { + strategy: enviroment.strategy_contract.address.clone(), + amount: deposit_amount, + }) + ] + } + ) + ]), + ).into_val(&setup.env), + sub_invokes: &[] + }, + }]) + .invest(&investments); + + setup.env.ledger().set_timestamp(setup.env.ledger().timestamp() + ONE_YEAR_IN_SECONDS); + + let token_balance_after_invest = enviroment.token.balance(&enviroment.vault_contract.address); + assert_eq!(token_balance_after_invest, 0); + + let strategy_balance = enviroment.strategy_contract.balance(&enviroment.vault_contract.address); + assert_eq!(strategy_balance, deposit_amount); + + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "withdraw", + args: ( + df_balance.clone(), + user.clone() + ).into_val(&setup.env), + sub_invokes: &[] + }, + }]) + .withdraw(&df_balance, &user); + + let df_balance = enviroment.vault_contract.balance(&user); + assert_eq!(df_balance, 0); + + let token_balance_after_withdraw = enviroment.token.balance(&enviroment.vault_contract.address); + assert_eq!(token_balance_after_withdraw, 0); + + let charged_fee = (deposit_amount - MINIMUM_LIQUIDITY) * (DEFINDEX_FEE as i128 + VAULT_FEE as i128) / 10000; + let expected_amount = deposit_amount - MINIMUM_LIQUIDITY - charged_fee; + + let user_balance_after_withdraw = enviroment.token.balance(user); + assert_eq!(user_balance_after_withdraw, expected_amount); + + let strategy_balance = enviroment.strategy_contract.balance(&enviroment.vault_contract.address); + assert_eq!(strategy_balance, charged_fee + MINIMUM_LIQUIDITY); + + let vault_balance_after_withdraw = enviroment.token.balance(&enviroment.vault_contract.address); + assert_eq!(vault_balance_after_withdraw, 0); +} + +#[test] +fn test_withdraw_multiple_users() { + let enviroment = create_vault_one_asset_hodl_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 100_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 2); + let user1 = &users[0]; + let user2 = &users[1]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user1, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user1, &user_starting_balance); + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user2, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user2, &user_starting_balance); + + let user1_balance = enviroment.token.balance(user1); + let user2_balance = enviroment.token.balance(user2); + assert_eq!(user1_balance, user_starting_balance); + assert_eq!(user2_balance, user_starting_balance); + + let deposit_amount = 10_000_0_000_000i128; + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user1.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user1.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user1.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user1); + + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user2.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user2.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user2.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user2); + + let df_balance_user1 = enviroment.vault_contract.balance(&user1); + let df_balance_user2 = enviroment.vault_contract.balance(&user2); + assert_eq!(df_balance_user1, deposit_amount - MINIMUM_LIQUIDITY); + assert_eq!(df_balance_user2, deposit_amount); + + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user1.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "withdraw", + args: ( + df_balance_user1.clone(), + user1.clone() + ).into_val(&setup.env), + sub_invokes: &[] + }, + }]) + .withdraw(&df_balance_user1, &user1); + + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user2.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "withdraw", + args: ( + df_balance_user2.clone(), + user2.clone() + ).into_val(&setup.env), + sub_invokes: &[] + }, + }]) + .withdraw(&df_balance_user2, &user2); + + let df_balance_user1 = enviroment.vault_contract.balance(&user1); + let df_balance_user2 = enviroment.vault_contract.balance(&user2); + assert_eq!(df_balance_user1, 0); + assert_eq!(df_balance_user2, 0); + + let vault_balance_after_withdraw = enviroment.token.balance(&enviroment.vault_contract.address); + assert_eq!(vault_balance_after_withdraw, MINIMUM_LIQUIDITY); + + let user1_balance_after_withdraw = enviroment.token.balance(user1); + let user2_balance_after_withdraw = enviroment.token.balance(user2); + assert_eq!(user1_balance_after_withdraw, user_starting_balance - MINIMUM_LIQUIDITY); + assert_eq!(user2_balance_after_withdraw, user_starting_balance); + + let total_supply = enviroment.vault_contract.total_supply(); + assert_eq!(total_supply, MINIMUM_LIQUIDITY); +} + +#[test] +fn test_withdraw_after_invest_multiple_users() { + let enviroment = create_vault_one_asset_hodl_strategy(); + let setup = enviroment.setup; + + let user_starting_balance = 10_000_0_000_000i128; + + let users = IntegrationTest::generate_random_users(&setup.env, 2); + let user1 = &users[0]; + let user2 = &users[1]; + + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user1, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user1, &user_starting_balance); + enviroment.token_admin_client.mock_auths(&[MockAuth { + address: &enviroment.token_admin.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "mint", + args: (user2, user_starting_balance,).into_val(&setup.env), + sub_invokes: &[], + }, + }]).mint(user2, &user_starting_balance); + + let user1_balance = enviroment.token.balance(user1); + let user2_balance = enviroment.token.balance(user2); + assert_eq!(user1_balance, user_starting_balance); + assert_eq!(user2_balance, user_starting_balance); + + let deposit_amount = 10_000_0_000_000i128; + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user1.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user1.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user1.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user1); + + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user2.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "deposit", + args: ( + Vec::from_array(&setup.env,[deposit_amount]), + Vec::from_array(&setup.env,[deposit_amount]), + user2.clone() + ).into_val(&setup.env), + sub_invokes: &[ + MockAuthInvoke { + contract: &enviroment.token.address.clone(), + fn_name: "transfer", + args: ( + user2.clone(), + &enviroment.vault_contract.address.clone(), + deposit_amount + ).into_val(&setup.env), + sub_invokes: &[] + } + ] + }, + }]) + .deposit(&svec![&setup.env, deposit_amount], &svec![&setup.env, deposit_amount], &user2); + + let df_balance_user1 = enviroment.vault_contract.balance(&user1); + let df_balance_user2 = enviroment.vault_contract.balance(&user2); + assert_eq!(df_balance_user1, deposit_amount - MINIMUM_LIQUIDITY); + assert_eq!(df_balance_user2, deposit_amount); + + // Create investment strategies for the deposited tokens + let investments = svec![ + &setup.env, + Some(AssetInvestmentAllocation { + asset: enviroment.token.address.clone(), + strategy_investments: svec![ + &setup.env, + Some(StrategyInvestment { + strategy: enviroment.strategy_contract.address.clone(), + amount: deposit_amount * 2, + }), + ], + }), + ]; + + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &enviroment.manager.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "invest", + args: ( + Vec::from_array(&setup.env,[ + Some( + AssetInvestmentAllocation { + asset: enviroment.token.address.clone(), + strategy_investments: + svec![&setup.env, + Some(StrategyInvestment { + strategy: enviroment.strategy_contract.address.clone(), + amount: deposit_amount * 2, + }) + ] + } + ) + ]), + ).into_val(&setup.env), + sub_invokes: &[] + }, + }]) + .invest(&investments); + + setup.env.ledger().set_timestamp(setup.env.ledger().timestamp() + ONE_YEAR_IN_SECONDS); + + let token_balance_after_invest = enviroment.token.balance(&enviroment.vault_contract.address); + assert_eq!(token_balance_after_invest, 0); + + let strategy_balance = enviroment.strategy_contract.balance(&enviroment.vault_contract.address); + assert_eq!(strategy_balance, deposit_amount * 2); + + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user1.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "withdraw", + args: ( + df_balance_user1.clone(), + user1.clone() + ).into_val(&setup.env), + sub_invokes: &[] + }, + }]) + .withdraw(&df_balance_user1, &user1); + + enviroment.vault_contract + .mock_auths(&[MockAuth { + address: &user2.clone(), + invoke: &MockAuthInvoke { + contract: &enviroment.vault_contract.address.clone(), + fn_name: "withdraw", + args: ( + df_balance_user2.clone(), + user2.clone() + ).into_val(&setup.env), + sub_invokes: &[] + }, + }]) + .withdraw(&df_balance_user2, &user2); + + let df_balance_user1 = enviroment.vault_contract.balance(&user1); + let df_balance_user2 = enviroment.vault_contract.balance(&user2); + assert_eq!(df_balance_user1, 0); + assert_eq!(df_balance_user2, 0); + + let token_balance_after_withdraw = enviroment.token.balance(&enviroment.vault_contract.address); + assert_eq!(token_balance_after_withdraw, 0); + + let charged_fee_user1 = (deposit_amount - MINIMUM_LIQUIDITY) * (DEFINDEX_FEE as i128 + VAULT_FEE as i128) / 10000; + let expected_amount_user1 = deposit_amount - MINIMUM_LIQUIDITY - charged_fee_user1; + + let charged_fee_user2 = deposit_amount * (DEFINDEX_FEE as i128 + VAULT_FEE as i128) / 10000; + let expected_amount_user2 = deposit_amount - charged_fee_user2; + + let user1_balance_after_withdraw = enviroment.token.balance(user1); + let user2_balance_after_withdraw = enviroment.token.balance(user2); + assert_eq!(user1_balance_after_withdraw, expected_amount_user1); + assert_eq!(user2_balance_after_withdraw, expected_amount_user2); + + let strategy_balance = enviroment.strategy_contract.balance(&enviroment.vault_contract.address); + assert_eq!(strategy_balance, charged_fee_user1 + charged_fee_user2 + MINIMUM_LIQUIDITY); + + let vault_balance_after_withdraw = enviroment.token.balance(&enviroment.vault_contract.address); + assert_eq!(vault_balance_after_withdraw, 0); +} \ No newline at end of file diff --git a/apps/contracts/integration-test/src/token.rs b/apps/contracts/integration-test/src/token.rs new file mode 100644 index 00000000..ef729253 --- /dev/null +++ b/apps/contracts/integration-test/src/token.rs @@ -0,0 +1,20 @@ +use soroban_sdk::{token::{TokenClient as SorobanTokenClient, StellarAssetClient as SorobanTokenAdminClient}, Address, Env}; + +fn create_token_contract<'a>(e: &Env, admin: &Address) -> SorobanTokenClient<'a> { + SorobanTokenClient::new(e, &e.register_stellar_asset_contract_v2(admin.clone()).address()) +} + +fn get_token_admin_client<'a>( + e: &Env, + address: &Address, +) -> SorobanTokenAdminClient<'a> { + SorobanTokenAdminClient::new(e, address) +} + +pub fn create_token<'a>(e: &Env, admin: &Address) -> (SorobanTokenClient<'a>, SorobanTokenAdminClient<'a>) { + let token = create_token_contract(e, admin); + + let token_admin_client = get_token_admin_client(e, &token.address); + + (token, token_admin_client) +} \ No newline at end of file diff --git a/apps/contracts/integration-test/src/vault.rs b/apps/contracts/integration-test/src/vault.rs new file mode 100644 index 00000000..13987ab1 --- /dev/null +++ b/apps/contracts/integration-test/src/vault.rs @@ -0,0 +1,11 @@ +// DeFindex Vault Contract +pub mod defindex_vault_contract { + soroban_sdk::contractimport!(file = "../target/wasm32-unknown-unknown/release/defindex_vault.optimized.wasm"); + + pub type VaultContractClient<'a> = Client<'a>; +} + +pub use defindex_vault_contract::{AssetStrategySet as VaultAssetStrategySet, Strategy as VaultStrategy, ContractError as VaultContractError}; + +pub static MINIMUM_LIQUIDITY: i128 = 1000; + diff --git a/apps/contracts/src/tests/testTwoStrategiesVault.ts b/apps/contracts/src/tests/testTwoStrategiesVault.ts index e2285976..d8f22be6 100644 --- a/apps/contracts/src/tests/testTwoStrategiesVault.ts +++ b/apps/contracts/src/tests/testTwoStrategiesVault.ts @@ -10,7 +10,7 @@ import { randomBytes } from "crypto"; import { AddressBook } from "../utils/address_book.js"; import { airdropAccount, invokeContract, invokeCustomContract } from "../utils/contract.js"; import { config } from "../utils/env_config.js"; -import { ActionType, AssetInvestmentAllocation, depositToVault, Instruction, investVault, rebalanceVault } from "./vault.js"; +import { ActionType, AssetInvestmentAllocation, depositToVault, getVaultBalanceInStrategy, Instruction, investVault, rebalanceVault, fetchParsedCurrentIdleFunds, fetchCurrentInvestedFunds } from "./vault.js"; const soroswapUSDC = new Address("CAAFIHB4I7WQMJMKC22CZVQNNX7EONWSOMT6SUXK6I3G3F6J4XFRWNDI"); @@ -110,6 +110,8 @@ export async function deployVaultTwoStrategies(addressBook: AddressBook) { const network = process.argv[2]; const addressBook = AddressBook.loadFromFile(network); +const hodl_strategy = addressBook.getContractId("hodl_strategy"); +const fixed_apr_strategy = addressBook.getContractId("fixed_apr_strategy"); const xlm: Asset = Asset.native() const loadedConfig = config(network); @@ -138,7 +140,7 @@ const mintToken = async () => { await mintToken(); // Step 1: Deposit to vault and capture initial balances -const { user, balanceBefore: depositBalanceBefore, result: depositResult, balanceAfter: depositBalanceAfter } +const { user, balanceBefore: depositBalanceBefore, result: depositResult, balanceAfter: depositBalanceAfter, status: depositStatus } = await depositToVault(deployedVault, [initialAmount], testUser); console.log(" -- ") console.log(" -- ") @@ -146,6 +148,14 @@ console.log("Step 1: Deposited to Vault using user:", user.publicKey(), "with ba console.log(" -- ") console.log(" -- ") + + +const idleFundsAfterDeposit = await fetchParsedCurrentIdleFunds(deployedVault, user); +const investedFundsAfterDeposit = await fetchCurrentInvestedFunds(deployedVault, user); + +const hodlBalanceBeforeInvest = await getVaultBalanceInStrategy(hodl_strategy, deployedVault, user); +const fixedBalanceBeforeInvest = await getVaultBalanceInStrategy(fixed_apr_strategy, deployedVault, user); + // Step 2: Invest in vault idle funds const investParams: AssetInvestmentAllocation[] = [ { @@ -167,12 +177,19 @@ const manager = loadedConfig.getUser("DEFINDEX_MANAGER_SECRET_KEY"); const investResult = await investVault(deployedVault, investParams, manager) console.log('🚀 « investResult:', investResult); +const idleFundsAfterInvest = await fetchParsedCurrentIdleFunds(deployedVault, user); +const investedFundsAfterInvest = await fetchCurrentInvestedFunds(deployedVault, user); + +const afterInvestHodlBalance = await getVaultBalanceInStrategy(hodl_strategy, deployedVault, user); +const afterInvestFixedBalance = await getVaultBalanceInStrategy(fixed_apr_strategy, deployedVault, user); + // 10000 USDC -> Total Balance // 1500 USDC -> Hodl Strategy // 2000 USDC -> Fixed Strategy // 6500 USDC -> Idle // Step 3: Rebalance Vault + const rebalanceParams: Instruction[] = [ { action: ActionType.Withdraw, @@ -192,6 +209,8 @@ const rebalanceParams: Instruction[] = [ console.log('🚀 « rebalanceParams:', rebalanceParams); +// this should leave us with: + // this should leave us with: // 10000 USDC -> Total Balance // 1000 USDC -> Hodl Strategy @@ -199,4 +218,45 @@ console.log('🚀 « rebalanceParams:', rebalanceParams); // 3500 USDC -> Idle const rebalanceResult = await rebalanceVault(deployedVault, rebalanceParams, manager); -console.log('🚀 « rebalanceResult:', rebalanceResult); \ No newline at end of file +console.log('🚀 « rebalanceResult:', rebalanceResult) + +const idleFundsAfterRebalance = await fetchParsedCurrentIdleFunds(deployedVault, user); +const investedFundsAfterRebalance = await fetchCurrentInvestedFunds(deployedVault, user); + +const afterRebalanceHodlBalance = await getVaultBalanceInStrategy(hodl_strategy, deployedVault, user); +const afterRebalanceFixedBalance = await getVaultBalanceInStrategy(fixed_apr_strategy, deployedVault, user); + +console.table({ + hodlStrategy: { + 'Balance before invest': hodlBalanceBeforeInvest, + 'Balance after invest': afterInvestHodlBalance, + 'Balance after rebalance': afterRebalanceHodlBalance + }, + fixedStrategy: { + 'Balance before invest': fixedBalanceBeforeInvest, + 'Balance after invest': afterInvestFixedBalance, + 'Balance after rebalance': afterRebalanceFixedBalance + }, + 'Invested funds': { + 'Balance before invest': investedFundsAfterDeposit[0].amount, + 'Balance after invest': investedFundsAfterInvest[0].amount, + 'Balance after rebalance': investedFundsAfterRebalance[0].amount + }, + 'Idle funds': { + 'Balance before invest': idleFundsAfterDeposit[0].amount, + 'Balance after invest': idleFundsAfterInvest[0].amount, + 'Balance after rebalance': idleFundsAfterRebalance[0].amount + } +}) + +console.table({ + deposit: { + 'Status': depositStatus ? '🟢 Success' : '🔴 Failed', + }, + invest: { + 'Status': investResult.status ? '🟢 Success' : '🔴 Failed', + }, + rebalance: { + 'Status': rebalanceResult.status ? '🟢 Success' : '🔴 Failed', + } +}) \ No newline at end of file diff --git a/apps/contracts/src/tests/vault.ts b/apps/contracts/src/tests/vault.ts index 87eb311a..4260782b 100644 --- a/apps/contracts/src/tests/vault.ts +++ b/apps/contracts/src/tests/vault.ts @@ -75,7 +75,7 @@ export async function depositToVault(deployedVault: string, amount: number[], us throw error; } - return { user: newUser, balanceBefore, result, balanceAfter }; + return { user: newUser, balanceBefore, result, balanceAfter, status:true }; } @@ -191,6 +191,21 @@ export async function fetchCurrentIdleFunds(deployedVault: string, user: Keypair throw error; } } + +export async function fetchParsedCurrentIdleFunds(deployedVault: string, user: Keypair) { + try { + const res = await invokeCustomContract(deployedVault, "fetch_current_idle_funds", [], user); + const funds = scValToNative(res.returnValue); + const mappedFunds = Object.entries(funds).map(([key, value]) => ({ + address: key, + amount: value, + })); + return mappedFunds; + } catch (error) { + console.error("Error:", error); + throw error; + } +} export interface AssetInvestmentAllocation { asset: Address; strategy_investments: { amount: bigint, strategy: Address }[]; @@ -239,7 +254,7 @@ export async function investVault( manager ); console.log("Investment successful:", scValToNative(investResult.returnValue)); - return investResult; + return {result: investResult, status: true}; } catch (error) { console.error("Investment failed:", error); throw error; @@ -337,7 +352,7 @@ export async function rebalanceVault(deployedVault: string, instructions: Instru manager ); console.log("Rebalance successful:", scValToNative(investResult.returnValue)); - return investResult; + return {result: investResult, status: true}; } catch (error) { console.error("Rebalance failed:", error); throw error; @@ -436,4 +451,30 @@ function mapSwapDetailsExactOut(details: SwapDetailsExactOut) { ), }), ]; +} + +export async function getVaultBalanceInStrategy(strategyAddress: string, vaultAddress: string, user: Keypair) { + const address = new Address(vaultAddress); + try { + const res = await invokeCustomContract(strategyAddress, "balance",[address.toScVal()],user) + return scValToNative(res.returnValue); + } catch (error) { + console.error('🔴 « error:', error); + return 0; + } + } + +export async function fetchCurrentInvestedFunds(deployedVault:string, user:Keypair) { + try { + const res = await invokeCustomContract(deployedVault, "fetch_current_invested_funds", [], user); + const funds = scValToNative(res.returnValue); + const mappedFunds = Object.entries(funds).map(([key, value]) => ({ + address: key, + amount: value, + })); + return mappedFunds; + } catch (error) { + console.error("Error:", error); + throw error; + } } \ No newline at end of file diff --git a/apps/contracts/vault/Cargo.toml b/apps/contracts/vault/Cargo.toml index ccdcb113..a2c4b93c 100755 --- a/apps/contracts/vault/Cargo.toml +++ b/apps/contracts/vault/Cargo.toml @@ -14,6 +14,7 @@ crate-type = ["cdylib"] soroban-sdk = { workspace = true } soroban-token-sdk = { workspace = true } defindex-strategy-core = { workspace = true } +common = { workspace = true } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/apps/contracts/vault/src/aggregator.rs b/apps/contracts/vault/src/aggregator.rs index c10e34db..6923e188 100644 --- a/apps/contracts/vault/src/aggregator.rs +++ b/apps/contracts/vault/src/aggregator.rs @@ -37,19 +37,19 @@ pub fn internal_swap_exact_tokens_for_tokens( return Err(ContractError::UnsupportedAsset); } - let mut init_args: Vec = vec![&e]; - init_args.push_back(token_in.to_val()); - init_args.push_back(token_out.to_val()); - init_args.push_back(amount_in.into_val(e)); - init_args.push_back(amount_out_min.into_val(e)); - init_args.push_back(distribution.into_val(e)); - init_args.push_back(e.current_contract_address().to_val()); - init_args.push_back(deadline.into_val(e)); + let mut swap_args: Vec = vec![&e]; + swap_args.push_back(token_in.to_val()); + swap_args.push_back(token_out.to_val()); + swap_args.push_back(amount_in.into_val(e)); + swap_args.push_back(amount_out_min.into_val(e)); + swap_args.push_back(distribution.into_val(e)); + swap_args.push_back(e.current_contract_address().to_val()); + swap_args.push_back(deadline.into_val(e)); e.invoke_contract( &aggregator_address, &Symbol::new(&e, "swap_exact_tokens_for_tokens"), - Vec::new(&e), + swap_args, ) } @@ -69,18 +69,18 @@ pub fn internal_swap_tokens_for_exact_tokens( return Err(ContractError::UnsupportedAsset); } - let mut init_args: Vec = vec![&e]; - init_args.push_back(token_in.to_val()); - init_args.push_back(token_out.to_val()); - init_args.push_back(amount_out.into_val(e)); - init_args.push_back(amount_in_max.into_val(e)); - init_args.push_back(distribution.into_val(e)); - init_args.push_back(e.current_contract_address().to_val()); - init_args.push_back(deadline.into_val(e)); + let mut swap_args: Vec = vec![&e]; + swap_args.push_back(token_in.to_val()); + swap_args.push_back(token_out.to_val()); + swap_args.push_back(amount_out.into_val(e)); + swap_args.push_back(amount_in_max.into_val(e)); + swap_args.push_back(distribution.into_val(e)); + swap_args.push_back(e.current_contract_address().to_val()); + swap_args.push_back(deadline.into_val(e)); e.invoke_contract( &aggregator_address, &Symbol::new(&e, "swap_tokens_for_exact_tokens"), - Vec::new(&e), + swap_args, ) } diff --git a/apps/contracts/vault/src/deposit.rs b/apps/contracts/vault/src/deposit.rs new file mode 100644 index 00000000..88c413b8 --- /dev/null +++ b/apps/contracts/vault/src/deposit.rs @@ -0,0 +1,155 @@ +use common::models::AssetStrategySet; +use soroban_sdk::{panic_with_error, token::TokenClient, Address, Env, Vec}; + +use crate::{ + funds::{ + fetch_invested_funds_for_asset, fetch_invested_funds_for_strategy, + fetch_total_managed_funds, + }, + investment::check_and_execute_investments, + models::{AssetInvestmentAllocation, StrategyInvestment}, + storage::get_assets, + token::{internal_mint, VaultToken}, + utils::{calculate_deposit_amounts_and_shares_to_mint, check_nonnegative_amount}, + ContractError, MINIMUM_LIQUIDITY, +}; + +/// Common logic for processing deposits. +pub fn process_deposit( + e: &Env, + assets: &Vec, + amounts_desired: &Vec, + amounts_min: &Vec, + from: &Address, +) -> Result<(Vec, i128), ContractError> { + let assets_length = assets.len(); + + // Validate inputs + if assets_length != amounts_desired.len() || assets_length != amounts_min.len() { + panic_with_error!(&e, ContractError::WrongAmountsLength); + } + + for amount in amounts_desired.iter() { + check_nonnegative_amount(amount)?; + } + + let total_supply = VaultToken::total_supply(e.clone()); + let (amounts, shares_to_mint) = if assets_length == 1 { + calculate_single_asset_shares(e, amounts_desired, total_supply)? + } else { + if total_supply == 0 { + (amounts_desired.clone(), amounts_desired.iter().sum()) + } else { + calculate_deposit_amounts_and_shares_to_mint(&e, &assets, amounts_desired, amounts_min)? + } + }; + + // Transfer assets + for (i, amount) in amounts.iter().enumerate() { + if amount < amounts_min.get(i as u32).unwrap() { + panic_with_error!(&e, ContractError::InsufficientAmount); + } + if amount > 0 { + let asset = assets.get(i as u32).unwrap(); + let asset_client = TokenClient::new(&e, &asset.address); + asset_client.transfer(&from, &e.current_contract_address(), &amount); + } + } + + // Mint shares + mint_shares(e, total_supply, shares_to_mint, from.clone())?; + + Ok((amounts, shares_to_mint)) +} + +/// Calculate shares for single-asset deposits. +fn calculate_single_asset_shares( + e: &Env, + amounts_desired: &Vec, + total_supply: i128, +) -> Result<(Vec, i128), ContractError> { + let shares = if total_supply == 0 { + amounts_desired.get(0).unwrap() + } else { + let total_managed_funds = fetch_total_managed_funds(&e); + 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(get_assets(&e).get(0).unwrap().address.clone()) + .unwrap(), + ) + .unwrap_or_else(|| panic_with_error!(&e, ContractError::ArithmeticError)) + }; + Ok((amounts_desired.clone(), shares)) +} + +/// Mint vault shares. +fn mint_shares( + e: &Env, + total_supply: i128, + shares_to_mint: i128, + from: Address, +) -> Result<(), ContractError> { + if total_supply == 0 { + if shares_to_mint < MINIMUM_LIQUIDITY { + panic_with_error!(&e, ContractError::InsufficientAmount); + } + internal_mint(e.clone(), e.current_contract_address(), MINIMUM_LIQUIDITY); + internal_mint( + e.clone(), + from.clone(), + shares_to_mint.checked_sub(MINIMUM_LIQUIDITY).unwrap(), + ); + } else { + internal_mint(e.clone(), from, shares_to_mint); + } + Ok(()) +} + +/// Generate investment allocations and execute them. +pub fn generate_and_execute_investments( + e: &Env, + amounts: &Vec, + assets: &Vec, +) -> Result<(), ContractError> { + let mut asset_investments = Vec::new(&e); + + for (i, amount) in amounts.iter().enumerate() { + let asset = assets.get(i as u32).unwrap(); + let asset_invested_funds = fetch_invested_funds_for_asset(&e, &asset); + + let mut strategy_investments = Vec::new(&e); + let mut remaining_amount = amount; + + for (j, strategy) in asset.strategies.iter().enumerate() { + let strategy_invested_funds = fetch_invested_funds_for_strategy(&e, &strategy.address); + + let mut invest_amount = if asset_invested_funds > 0 { + (amount * strategy_invested_funds) / asset_invested_funds + } else { + 0 + }; + + if j == asset.strategies.len() as usize - 1 { + invest_amount = remaining_amount; + } + + remaining_amount -= invest_amount; + + strategy_investments.push_back(Some(StrategyInvestment { + strategy: strategy.address.clone(), + amount: invest_amount, + })); + } + + asset_investments.push_back(Some(AssetInvestmentAllocation { + asset: asset.address.clone(), + strategy_investments, + })); + } + + check_and_execute_investments(e.clone(), assets.clone(), asset_investments)?; + Ok(()) +} diff --git a/apps/contracts/vault/src/events.rs b/apps/contracts/vault/src/events.rs index 2d48293e..755d4408 100644 --- a/apps/contracts/vault/src/events.rs +++ b/apps/contracts/vault/src/events.rs @@ -1,5 +1,5 @@ //! Definition of the Events used in the DeFindex Vault contract -use crate::models::AssetStrategySet; +use common::models::AssetStrategySet; use soroban_sdk::{contracttype, symbol_short, Address, Env, Vec}; // INITIALIZED VAULT EVENT diff --git a/apps/contracts/vault/src/funds.rs b/apps/contracts/vault/src/funds.rs index 8d19cee9..d34c4efb 100644 --- a/apps/contracts/vault/src/funds.rs +++ b/apps/contracts/vault/src/funds.rs @@ -1,7 +1,7 @@ use soroban_sdk::token::TokenClient; use soroban_sdk::{Address, Env, Map}; -use crate::models::AssetStrategySet; +use common::models::AssetStrategySet; use crate::storage::get_assets; use crate::strategies::get_strategy_client; diff --git a/apps/contracts/vault/src/interface.rs b/apps/contracts/vault/src/interface.rs index 82d684a2..e1ff1d10 100644 --- a/apps/contracts/vault/src/interface.rs +++ b/apps/contracts/vault/src/interface.rs @@ -1,9 +1,10 @@ use soroban_sdk::{Address, Env, Map, String, Vec}; use crate::{ - models::{AssetStrategySet, Instruction, AssetInvestmentAllocation}, + models::{Instruction, AssetInvestmentAllocation}, ContractError, }; +use common::models::AssetStrategySet; pub trait VaultTrait { /// Initializes the DeFindex Vault contract with the required parameters. @@ -78,6 +79,7 @@ pub trait VaultTrait { amounts_desired: Vec, amounts_min: Vec, from: Address, + invest: bool, ) -> Result<(Vec, i128), ContractError>; /// Withdraws assets from the DeFindex Vault by burning dfTokens. diff --git a/apps/contracts/vault/src/investment.rs b/apps/contracts/vault/src/investment.rs index 840c2e9f..3d2d8ffb 100644 --- a/apps/contracts/vault/src/investment.rs +++ b/apps/contracts/vault/src/investment.rs @@ -1,11 +1,12 @@ use soroban_sdk::{Env, Vec, panic_with_error}; use crate::{ - models::{AssetStrategySet, AssetInvestmentAllocation}, + models::AssetInvestmentAllocation, strategies::invest_in_strategy, utils::{check_nonnegative_amount}, ContractError, }; +use common::models::AssetStrategySet; /// Checks and executes the investments for each asset based on provided allocations. /// The function iterates through the specified assets and asset investments to ensure validity diff --git a/apps/contracts/vault/src/lib.rs b/apps/contracts/vault/src/lib.rs index 7204973f..7f1c5fa6 100755 --- a/apps/contracts/vault/src/lib.rs +++ b/apps/contracts/vault/src/lib.rs @@ -1,4 +1,5 @@ #![no_std] +use deposit::{generate_and_execute_investments, process_deposit}; use soroban_sdk::{ contract, contractimpl, panic_with_error, token::{TokenClient, TokenInterface}, @@ -9,6 +10,7 @@ use soroban_token_sdk::metadata::TokenMetadata; mod access; mod aggregator; mod constants; +mod deposit; mod error; mod events; mod fee; @@ -27,25 +29,27 @@ use aggregator::{internal_swap_exact_tokens_for_tokens, internal_swap_tokens_for use fee::{collect_fees, fetch_defindex_fee}; use funds::{fetch_current_idle_funds, fetch_current_invested_funds, fetch_total_managed_funds}; //, fetch_idle_funds_for_asset}; use interface::{AdminInterfaceTrait, VaultManagementTrait, VaultTrait}; -use investment::{check_and_execute_investments}; +use investment::check_and_execute_investments; use models::{ - ActionType, AssetStrategySet, Instruction, AssetInvestmentAllocation, OptionalSwapDetailsExactIn, + ActionType, AssetInvestmentAllocation, Instruction, OptionalSwapDetailsExactIn, OptionalSwapDetailsExactOut, }; use storage::{ - extend_instance_ttl, get_assets, get_vault_fee, set_asset, set_defindex_protocol_fee_receiver, set_factory, set_total_assets, set_vault_fee + extend_instance_ttl, get_assets, get_vault_fee, set_asset, set_defindex_protocol_fee_receiver, + set_factory, set_total_assets, set_vault_fee, }; 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 token::{internal_burn, 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_withdrawal_amounts, check_initialized, + check_nonnegative_amount, }; +use common::models::AssetStrategySet; use defindex_strategy_core::DeFindexStrategyClient; static MINIMUM_LIQUIDITY: i128 = 1000; @@ -120,7 +124,7 @@ impl VaultTrait for DeFindexVault { 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 strategies indeed support this asset @@ -162,8 +166,8 @@ impl VaultTrait for DeFindexVault { /// Handles user deposits into the DeFindex 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 + /// 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. /// /// # Parameters @@ -194,6 +198,7 @@ impl VaultTrait for DeFindexVault { amounts_desired: Vec, amounts_min: Vec, from: Address, + invest: bool, ) -> Result<(Vec, i128), ContractError> { extend_instance_ttl(&e); check_initialized(&e)?; @@ -201,91 +206,19 @@ impl VaultTrait for DeFindexVault { // 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)?; + collect_fees(&e)?; - // get assets let assets = get_assets(&e); - 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::WrongAmountsLength); - } - - // for every amount desired, check non negative - for amount in amounts_desired.iter() { - check_nonnegative_amount(amount)?; - } - // 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 { - 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 { - // 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); - // 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_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. This will be held as idle funds. - asset_client.transfer(&from, &e.current_contract_address(), &amount); - } - } + let (amounts, shares_to_mint) = + process_deposit(&e, &assets, &amounts_desired, &amounts_min, &from)?; + events::emit_deposit_event(&e, from, amounts.clone(), shares_to_mint.clone()); - // Now we mint the corresponding dfToken shares to the user - // If total_sypply==0, mint minimum liquidity to be locked forever in the contract. So we will never come again to total_supply==0 - if total_supply == 0 { - if shares_to_mint < MINIMUM_LIQUIDITY { - panic_with_error!(&e, ContractError::InsufficientAmount); - } - internal_mint(e.clone(), e.current_contract_address(), MINIMUM_LIQUIDITY); - internal_mint(e.clone(), from.clone(), shares_to_mint.checked_sub(MINIMUM_LIQUIDITY).unwrap()); - } - else { - internal_mint(e.clone(), from.clone(), shares_to_mint); + if invest { + // Generate investment allocations and execute them + generate_and_execute_investments(&e, &amounts, &assets)?; } - events::emit_deposit_event(&e, from, amounts.clone(), shares_to_mint.clone()); - Ok((amounts, shares_to_mint)) } @@ -302,11 +235,7 @@ impl VaultTrait for DeFindexVault { /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. - fn withdraw( - e: Env, - shares_amount: i128, - from: Address) -> Result, ContractError> { - + fn withdraw(e: Env, shares_amount: i128, from: Address) -> Result, ContractError> { extend_instance_ttl(&e); check_initialized(&e)?; check_nonnegative_amount(shares_amount)?; @@ -314,7 +243,7 @@ impl VaultTrait for DeFindexVault { // fees assesment collect_fees(&e)?; - + // Check if the user has enough dfTokens. // TODO, we can move this error into the internal_burn function let df_user_balance = VaultToken::balance(e.clone(), from.clone()); if df_user_balance < shares_amount { @@ -324,7 +253,6 @@ impl VaultTrait for DeFindexVault { // result.push_back(shares_amount); // return Ok(result); - return Err(ContractError::InsufficientBalance); } @@ -389,7 +317,7 @@ impl VaultTrait for DeFindexVault { } events::emit_withdraw_event(&e, from, shares_amount, amounts_withdrawn.clone()); - + Ok(amounts_withdrawn) } @@ -685,7 +613,7 @@ impl VaultManagementTrait for DeFindexVault { /// /// # Arguments /// * `e` - The current environment reference. - /// * `asset_investments` - A vector of optional `AssetInvestmentAllocation` structures, where each element + /// * `asset_investments` - A vector of optional `AssetInvestmentAllocation` structures, where each element /// represents an allocation for a specific asset. The vector must match the number of vault assets in length. /// /// # Returns @@ -708,8 +636,8 @@ impl VaultManagementTrait for DeFindexVault { /// # Security /// - Only addresses with the `Manager` role can call this function, ensuring restricted access to managing investments. fn invest( - e: Env, - asset_investments: Vec> + e: Env, + asset_investments: Vec>, ) -> Result<(), ContractError> { extend_instance_ttl(&e); check_initialized(&e)?; @@ -719,7 +647,7 @@ impl VaultManagementTrait for DeFindexVault { access_control.require_role(&RolesDataKey::Manager); let assets = get_assets(&e); - + // Ensure the length of `asset_investments` matches the number of vault assets if asset_investments.len() != assets.len() { panic_with_error!(&e, ContractError::WrongInvestmentLength); @@ -727,12 +655,10 @@ impl VaultManagementTrait for DeFindexVault { // Check and execute investments for each asset allocation check_and_execute_investments(e, assets, asset_investments)?; - + Ok(()) } - - /// Rebalances the vault by executing a series of instructions. /// /// # Arguments: @@ -763,8 +689,7 @@ impl VaultManagementTrait for DeFindexVault { ActionType::Invest => match (&instruction.strategy, &instruction.amount) { (Some(strategy_address), Some(amount)) => { let asset_address = get_strategy_asset(&e, strategy_address)?; - invest_in_strategy( - &e, &asset_address.address, strategy_address, amount)?; + invest_in_strategy(&e, &asset_address.address, strategy_address, amount)?; } _ => return Err(ContractError::MissingInstructionData), }, diff --git a/apps/contracts/vault/src/models.rs b/apps/contracts/vault/src/models.rs index c0eb3db3..28d6b3d4 100644 --- a/apps/contracts/vault/src/models.rs +++ b/apps/contracts/vault/src/models.rs @@ -1,20 +1,5 @@ use soroban_sdk::{contracttype, Address, String, Vec}; -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Strategy { - pub address: Address, - pub name: String, - pub paused: bool, -} - -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct AssetStrategySet { - pub address: Address, - pub strategies: Vec, -} - // Investment Allocation in Strategies #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/apps/contracts/vault/src/storage.rs b/apps/contracts/vault/src/storage.rs index 0cafff50..8dfb2ee3 100644 --- a/apps/contracts/vault/src/storage.rs +++ b/apps/contracts/vault/src/storage.rs @@ -1,6 +1,6 @@ use soroban_sdk::{contracttype, Address, Env, Vec}; -use crate::models::AssetStrategySet; +use common::models::AssetStrategySet; const DAY_IN_LEDGERS: u32 = 17280; const INSTANCE_BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; diff --git a/apps/contracts/vault/src/strategies.rs b/apps/contracts/vault/src/strategies.rs index 7589a90d..51046e02 100644 --- a/apps/contracts/vault/src/strategies.rs +++ b/apps/contracts/vault/src/strategies.rs @@ -4,11 +4,12 @@ use soroban_sdk::auth::{ContractContext, InvokerContractAuthEntry, SubContractIn use crate::{ - models::{AssetStrategySet, Strategy}, storage::{get_asset, get_assets, get_total_assets, set_asset}, ContractError, }; +use common::models::{AssetStrategySet, Strategy}; + pub fn get_strategy_client(e: &Env, address: Address) -> DeFindexStrategyClient { DeFindexStrategyClient::new(&e, &address) } diff --git a/apps/contracts/vault/src/test.rs b/apps/contracts/vault/src/test.rs index f696f7ee..de9bb0d3 100755 --- a/apps/contracts/vault/src/test.rs +++ b/apps/contracts/vault/src/test.rs @@ -174,3 +174,4 @@ mod invest; mod withdraw; mod emergency_withdraw; mod rebalance; +mod deposit_and_invest; diff --git a/apps/contracts/vault/src/test/deposit.rs b/apps/contracts/vault/src/test/deposit.rs index 5b9ffd6f..54217e6b 100644 --- a/apps/contracts/vault/src/test/deposit.rs +++ b/apps/contracts/vault/src/test/deposit.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{vec as sorobanvec, String, Vec, Map}; +use soroban_sdk::{vec as sorobanvec, InvokeError, Map, String, Vec}; use crate::test::defindex_vault::{AssetStrategySet, ContractError}; use crate::test::{ @@ -15,6 +15,7 @@ fn test_deposit_not_yet_initialized() { &sorobanvec![&test.env, 100i128], &sorobanvec![&test.env, 100i128], &users[0], + &false, ); assert_eq!(result, Err(Ok(ContractError::NotInitialized))); @@ -60,6 +61,7 @@ fn deposit_amounts_desired_less_length() { &sorobanvec![&test.env, amount], // wrong amount desired &sorobanvec![&test.env, amount, amount], &users[0], + &false, ); assert_eq!(response, Err(Ok(ContractError::WrongAmountsLength))); @@ -101,6 +103,7 @@ fn deposit_amounts_desired_more_length() { &sorobanvec![&test.env, amount, amount], // wrong amount desired &sorobanvec![&test.env, amount], &users[0], + &false, ); assert_eq!(response, Err(Ok(ContractError::WrongAmountsLength))); @@ -146,6 +149,7 @@ fn deposit_amounts_min_less_length() { &sorobanvec![&test.env, amount, amount], &sorobanvec![&test.env, amount], // wrong amount min &users[0], + &false, ); assert_eq!(response, Err(Ok(ContractError::WrongAmountsLength))); @@ -192,6 +196,7 @@ fn deposit_amounts_min_more_length() { &sorobanvec![&test.env, amount, amount], &sorobanvec![&test.env, amount, amount, amount], // wrong amount min &users[0], + &false, ); assert_eq!(response, Err(Ok(ContractError::WrongAmountsLength))); @@ -237,6 +242,7 @@ fn deposit_amounts_desired_negative() { &sorobanvec![&test.env, -amount, amount], &sorobanvec![&test.env, amount, amount], &users[0], + &false, ); assert_eq!(response, Err(Ok(ContractError::NegativeNotAllowed))); @@ -286,6 +292,7 @@ fn deposit_one_asset_success() { &sorobanvec![&test.env, amount], &sorobanvec![&test.env, amount], &users[0], + &false, ); // check balances after deposit @@ -330,6 +337,7 @@ fn deposit_one_asset_success() { &sorobanvec![&test.env, amount2], &sorobanvec![&test.env, amount2], &users[0], + &false, ); //map shuould be map @@ -412,6 +420,7 @@ fn deposit_one_asset_min_more_than_desired() { &sorobanvec![&test.env, amount], &sorobanvec![&test.env, amount + 1], &users[0], + &false, ); // this should fail assert_eq!(result, Err(Ok(ContractError::InsufficientAmount))); @@ -471,6 +480,7 @@ fn deposit_several_assets_success() { &sorobanvec![&test.env, amount0, amount1], &sorobanvec![&test.env, amount0, amount1], &users[0], + &false, ); // check deposit result @@ -541,6 +551,7 @@ fn deposit_several_assets_success() { &sorobanvec![&test.env, amount0_new, amount1_new], &sorobanvec![&test.env, 0i128, 0i128], &users[1], + &false, ); // check deposit result. Ok((amounts, shares_to_mint)) @@ -607,6 +618,7 @@ fn deposit_several_assets_success() { &sorobanvec![&test.env, amount0_new, amount1_new], &sorobanvec![&test.env, 0i128, 0i128], &users[1], + &false, ); // check deposit result. Ok((amounts, shares_to_mint)) @@ -668,6 +680,7 @@ fn deposit_several_assets_min_greater_than_optimal() { &sorobanvec![&test.env, amount0, amount1], &sorobanvec![&test.env, amount0 + 1, amount1], &users[0], + &false, ); // this should fail @@ -678,6 +691,7 @@ fn deposit_several_assets_min_greater_than_optimal() { &sorobanvec![&test.env, amount0, amount1], &sorobanvec![&test.env, amount0, amount1], &users[0], + &false, ); // check deposit result @@ -697,6 +711,7 @@ fn deposit_several_assets_min_greater_than_optimal() { &sorobanvec![&test.env, amount0_new, amount1_new], &sorobanvec![&test.env, amount0*2+1, amount1*2], &users[0], + &false, ); // this should fail @@ -705,3 +720,306 @@ fn deposit_several_assets_min_greater_than_optimal() { } +//test deposit amounts_min greater than amounts_desired +#[test] +fn deposit_amounts_min_greater_than_amounts_desired(){ + 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, + AssetStrategySet { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + }, + AssetStrategySet { + address: test.token1.address.clone(), + strategies: strategy_params_token1.clone() + } + ]; + + test.defindex_contract.initialize( + &assets, + &test.manager, + &test.emergency_manager, + &test.vault_fee_receiver, + &2000u32, + &test.defindex_protocol_receiver, + &test.defindex_factory, + &String::from_str(&test.env, "dfToken"), + &String::from_str(&test.env, "DFT"), + ); + let 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 + 1], + &users[0], + ); + + // this should fail + assert_eq!(deposit_result, Err(Ok(ContractError::InsufficientAmount))); +} + +//Test token transfer from user to vault on deposit +#[test] +fn deposit_transfers_tokens_from_user_to_vault(){ + 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, + AssetStrategySet { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + }, + AssetStrategySet { + address: test.token1.address.clone(), + strategies: strategy_params_token1.clone() + } + ]; + + test.defindex_contract.initialize( + &assets, + &test.manager, + &test.emergency_manager, + &test.vault_fee_receiver, + &2000u32, + &test.defindex_protocol_receiver, + &test.defindex_factory, + &String::from_str(&test.env, "dfToken"), + &String::from_str(&test.env, "DFT"), + ); + let 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 + test.defindex_contract.deposit( + &sorobanvec![&test.env, amount0, amount1], + &sorobanvec![&test.env, amount0, amount1], + &users[0], + ); + + // check balances after deposit + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, amount0 + amount1 - 1000); + + let user_balance0 = test.token0.balance(&users[0]); + assert_eq!(user_balance0, 0i128); +} + +#[test] +fn test_deposit_arithmetic_error() { + let test = DeFindexVaultTest::setup(); + test.env.mock_all_auths(); + let strategy_params_token0 = create_strategy_params_token0(&test); + + // Initialize with 1 asset + let assets: Vec = sorobanvec![ + &test.env, + AssetStrategySet { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + } + ]; + + test.defindex_contract.initialize( + &assets, + &test.manager, + &test.emergency_manager, + &test.vault_fee_receiver, + &2000u32, + &test.defindex_protocol_receiver, + &test.defindex_factory, + &String::from_str(&test.env, "dfToken"), + &String::from_str(&test.env, "DFT"), + ); + + // Mock the environment to provoke a division by zero + let mut mock_map = Map::new(&test.env); + mock_map.set(test.token0.address.clone(), 0i128); // Total funds for token0 is 0 + + let amount = 123456789i128; + + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); + + // Mint tokens to user + test.token0_admin_client.mint(&users[0], &amount); + + let large_amount = i128::MAX / 2; + test.token0_admin_client.mint(&users[0], &large_amount); + + //first deposit to overflow the balance + test.defindex_contract.deposit( + &sorobanvec![&test.env, large_amount], + &sorobanvec![&test.env, large_amount], + &users[0], + ); + + // Try to deposit a large amount + let result = test.defindex_contract.try_deposit( + &sorobanvec![&test.env, large_amount], + &sorobanvec![&test.env, large_amount], + &users[0], + ); + + // Verify that the returned error is ContractError::ArithmeticError + assert_eq!(result, Err(Ok(ContractError::ArithmeticError))); +} + +//all amounts are cero +#[test] +fn deposit_amounts_desired_zero() { + let test = DeFindexVaultTest::setup(); + test.env.mock_all_auths(); + let strategy_params_token0 = create_strategy_params_token0(&test); + + // Initialize with 1 asset + let assets: Vec = sorobanvec![ + &test.env, + AssetStrategySet { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + } + ]; + + test.defindex_contract.initialize( + &assets, + &test.manager, + &test.emergency_manager, + &test.vault_fee_receiver, + &2000u32, + &test.defindex_protocol_receiver, + &test.defindex_factory, + &String::from_str(&test.env, "dfToken"), + &String::from_str(&test.env, "DFT"), + ); + + let amount = 123456789i128; + + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); + + // Mint tokens to user + test.token0_admin_client.mint(&users[0], &amount); + + // Balances before deposit + let user_balance_before = test.token0.balance(&users[0]); + assert_eq!(user_balance_before, amount); + + let vault_balance_before = test.token0.balance(&test.defindex_contract.address); + assert_eq!(vault_balance_before, 0i128); + + let df_balance_before = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance_before, 0i128); + + // Attempt to deposit with amounts_desired all set to 0 + let deposit_result = test.defindex_contract.try_deposit( + &sorobanvec![&test.env, 0i128], + &sorobanvec![&test.env, 0i128], + &users[0], + ); + + // Verify that the returned error is ContractError::InsufficientAmount + assert_eq!(deposit_result, Err(Ok(ContractError::InsufficientAmount))); +} + + + // Deposit with insufficient funds and check for specific error message +#[test] +fn deposit_insufficient_funds_with_error_message() { + 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, + AssetStrategySet { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + }, + AssetStrategySet { + address: test.token1.address.clone(), + strategies: strategy_params_token1.clone() + } + ]; + + test.defindex_contract.initialize( + &assets, + &test.manager, + &test.emergency_manager, + &test.vault_fee_receiver, + &2000u32, + &test.defindex_protocol_receiver, + &test.defindex_factory, + &String::from_str(&test.env, "dfToken"), + &String::from_str(&test.env, "DFT"), + ); + + let amount0 = 123456789i128; + let amount1 = 987654321i128; + + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); + + // Mint tokens to user + test.token0_admin_client.mint(&users[0], &amount0); + test.token1_admin_client.mint(&users[0], &amount1); + + // Balances before deposit + 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); + + // Attempt to deposit more than available balance + let deposit_result = test.defindex_contract.try_deposit( + &sorobanvec![&test.env, amount0 + 1, amount1 + 1], + &sorobanvec![&test.env, amount0 + 1, amount1 + 1], + &users[0], + ); + + if deposit_result == Err(Err(InvokeError::Contract(10))) { + return; + } else { + panic!("Expected error not returned"); + } + +} \ No newline at end of file diff --git a/apps/contracts/vault/src/test/deposit_and_invest.rs b/apps/contracts/vault/src/test/deposit_and_invest.rs new file mode 100644 index 00000000..a85ff29b --- /dev/null +++ b/apps/contracts/vault/src/test/deposit_and_invest.rs @@ -0,0 +1,325 @@ +use soroban_sdk::{vec as sorobanvec, String, Vec, Map}; + +use crate::test::defindex_vault::{AssetStrategySet}; +use crate::test::{ + create_strategy_params_token0, create_strategy_params_token1, DeFindexVaultTest, +}; + +// test deposit one asset success +#[test] +fn deposit_and_invest_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, + AssetStrategySet { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + } + ]; + + test.defindex_contract.initialize( + &assets, + &test.manager, + &test.emergency_manager, + &test.vault_fee_receiver, + &2000u32, + &test.defindex_protocol_receiver, + &test.defindex_factory, + &String::from_str(&test.env, "dfToken"), + &String::from_str(&test.env, "DFT"), + ); + 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], + &true, + ); + + // check balances after deposit + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, amount - 1000); + + let user_balance = test.token0.balance(&users[0]); + assert_eq!(user_balance, 0i128); + + //map shuould be map + let mut expected_invested_map = Map::new(&test.env); + expected_invested_map.set(test.token0.address.clone(), amount); + + let mut expected_idle_map = Map::new(&test.env); + expected_idle_map.set(test.token0.address.clone(), 0); + + // check that all the assets are invested + let vault_balance = test.token0.balance(&test.defindex_contract.address); + assert_eq!(vault_balance, 0); + + // 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_invested_map); + + // check current idle funds, + let current_idle_funds = test.defindex_contract.fetch_current_idle_funds(); + assert_eq!(current_idle_funds, expected_idle_map); + + // 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_invested_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], + &true, + ); + + //map shuould be map + let mut expected_invested_map = Map::new(&test.env); + expected_invested_map.set(test.token0.address.clone(), amount + amount2); + + let mut expected_idle_map = Map::new(&test.env); + expected_idle_map.set(test.token0.address.clone(), 0); + + // check balances after deposit + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, amount + amount2 - 1000); + + let user_balance = test.token0.balance(&users[0]); + assert_eq!(user_balance, 0i128); + + // check that the assets are not in the vault + let vault_balance = test.token0.balance(&test.defindex_contract.address); + assert_eq!(vault_balance, 0); + + // 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_invested_map); + + // check current idle funds + let current_idle_funds = test.defindex_contract.fetch_current_idle_funds(); + assert_eq!(current_idle_funds, expected_idle_map); + + // 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_invested_map); +} + +// test deposit of several asset, considering different proportion of assets +#[test] +fn deposit_and_invest_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, + AssetStrategySet { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + }, + AssetStrategySet { + address: test.token1.address.clone(), + strategies: strategy_params_token1.clone() + } + ]; + + test.defindex_contract.initialize( + &assets, + &test.manager, + &test.emergency_manager, + &test.vault_fee_receiver, + &2000u32, + &test.defindex_protocol_receiver, + &test.defindex_factory, + &String::from_str(&test.env, "dfToken"), + &String::from_str(&test.env, "DFT"), + ); + let 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], + &true, + ); + + // 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]); + // For first deposit, a minimum amount LIQUIDITY OF 1000 is being locked in the contract + assert_eq!(df_balance, amount0 + amount1 - 1000); + + // check that the vault holds 1000 shares + let vault_df_shares = test.defindex_contract.balance(&test.defindex_contract.address); + assert_eq!(vault_df_shares, 1000i128); + + 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, 0); + // check vault balance of asset 1 + let vault_balance1 = test.token1.balance(&test.defindex_contract.address); + assert_eq!(vault_balance1, 0); + + //map shuould be map + let mut expected_invested_map = Map::new(&test.env); + expected_invested_map.set(test.token0.address.clone(), amount0); + expected_invested_map.set(test.token1.address.clone(), amount1); + + let mut expected_idle_map = Map::new(&test.env); + expected_idle_map.set(test.token0.address.clone(), 0); + expected_idle_map.set(test.token1.address.clone(), 0); + + // 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_invested_map); + + // check current idle funds + let current_idle_funds = test.defindex_contract.fetch_current_idle_funds(); + assert_eq!(current_idle_funds, expected_idle_map); + + // check that current invested funds is now correct, funds should be invested + let current_invested_funds = test.defindex_contract.fetch_current_invested_funds(); + assert_eq!(current_invested_funds, expected_invested_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], + &true, + ); + + // 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, 0); + // check vault balance of asset 1 + let vault_balance1 = test.token1.balance(&test.defindex_contract.address); + assert_eq!(vault_balance1, 0); + + //map shuould be map + let mut expected_invested_map = Map::new(&test.env); + expected_invested_map.set(test.token0.address.clone(), 3*amount0); + expected_invested_map.set(test.token1.address.clone(), 3*amount1); + + let mut expected_idle_map = Map::new(&test.env); + expected_idle_map.set(test.token0.address.clone(), 0); + expected_idle_map.set(test.token1.address.clone(), 0); + + // 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_invested_map); + + // check current idle funds + let current_idle_funds = test.defindex_contract.fetch_current_idle_funds(); + assert_eq!(current_idle_funds, expected_idle_map); + + // 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_invested_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], + &true, + ); + + // 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)); + +} \ No newline at end of file diff --git a/apps/contracts/vault/src/test/emergency_withdraw.rs b/apps/contracts/vault/src/test/emergency_withdraw.rs index 1c014738..cf74ec8b 100644 --- a/apps/contracts/vault/src/test/emergency_withdraw.rs +++ b/apps/contracts/vault/src/test/emergency_withdraw.rs @@ -49,6 +49,7 @@ fn test_emergency_withdraw_success() { &sorobanvec![&test.env, amount], &sorobanvec![&test.env, amount], &users[0], + &false ); let df_balance = test.defindex_contract.balance(&users[0]); diff --git a/apps/contracts/vault/src/test/invest.rs b/apps/contracts/vault/src/test/invest.rs index 338c2c2a..b6f7d93f 100644 --- a/apps/contracts/vault/src/test/invest.rs +++ b/apps/contracts/vault/src/test/invest.rs @@ -446,6 +446,7 @@ fn test_invest_in_strategy() { &sorobanvec![&test.env, amount_0, amount_1], // asset 0 &sorobanvec![&test.env, amount_0, amount_1], // asset 1 &users[0], + &false, ); @@ -609,6 +610,7 @@ fn test_invest_more_than_idle_funds() { &sorobanvec![&test.env, amount_0, amount_1], // asset 0 &sorobanvec![&test.env, amount_0, amount_1], // asset 1 &users[0], + &false, ); // check vault balances @@ -724,7 +726,8 @@ fn test_invest_without_mock_all_auths() { args: ( Vec::from_array(&test.env,[amount_0, amount_1]), Vec::from_array(&test.env,[amount_0, amount_1]), - users[0].clone() + users[0].clone(), + false ).into_val(&test.env), // mock toke 0 and token 1 subtransfer sub_invokes: &[ @@ -753,6 +756,7 @@ fn test_invest_without_mock_all_auths() { &sorobanvec![&test.env, amount_0, amount_1], // asset 0 &sorobanvec![&test.env, amount_0, amount_1], // asset 1 &users[0], + &false, ); // TODO check that the blockchain saw this authorizations diff --git a/apps/contracts/vault/src/test/rebalance.rs b/apps/contracts/vault/src/test/rebalance.rs index d81888ab..397c5c5d 100644 --- a/apps/contracts/vault/src/test/rebalance.rs +++ b/apps/contracts/vault/src/test/rebalance.rs @@ -54,6 +54,7 @@ fn rebalance_multi_instructions() { &sorobanvec![&test.env, amount], &sorobanvec![&test.env, amount], &users[0], + &false ); let df_balance = test.defindex_contract.balance(&users[0]); diff --git a/apps/contracts/vault/src/test/withdraw.rs b/apps/contracts/vault/src/test/withdraw.rs index b5fe4ef8..38edb80f 100644 --- a/apps/contracts/vault/src/test/withdraw.rs +++ b/apps/contracts/vault/src/test/withdraw.rs @@ -54,6 +54,7 @@ fn test_withdraw_from_idle_success() { &sorobanvec![&test.env, amount_to_deposit], &sorobanvec![&test.env, amount_to_deposit], &users[0], + &false ); // Check Balances after deposit @@ -165,6 +166,7 @@ fn test_withdraw_from_strategy_success() { &sorobanvec![&test.env, amount], &sorobanvec![&test.env, amount], &users[0], + &false ); let df_balance = test.defindex_contract.balance(&users[0]); diff --git a/apps/contracts/vault/src/token/contract.rs b/apps/contracts/vault/src/token/contract.rs index 98d09427..430cf746 100644 --- a/apps/contracts/vault/src/token/contract.rs +++ b/apps/contracts/vault/src/token/contract.rs @@ -54,13 +54,6 @@ impl VaultToken { pub fn total_supply(e: Env) -> i128 { read_total_supply(&e) } - - #[cfg(test)] - pub fn get_allowance(e: Env, from: Address, spender: Address) -> Option { - let key = DataKey::Allowance(AllowanceDataKey { from, spender }); - let allowance = e.storage().temporary().get::<_, AllowanceValue>(&key); - allowance - } } #[contractimpl] diff --git a/apps/contracts/vault/src/utils.rs b/apps/contracts/vault/src/utils.rs index db1360ed..985e2762 100644 --- a/apps/contracts/vault/src/utils.rs +++ b/apps/contracts/vault/src/utils.rs @@ -6,10 +6,10 @@ use crate::{ fetch_invested_funds_for_asset, fetch_invested_funds_for_strategy, fetch_total_managed_funds, }, - models::AssetStrategySet, token::VaultToken, ContractError, }; +use common::models::AssetStrategySet; pub const DAY_IN_LEDGERS: u32 = 17280; diff --git a/apps/dapp/src/components/DeployVault/AddNewStrategyButton.tsx b/apps/dapp/src/components/DeployVault/AddNewStrategyButton.tsx index 653eec68..c06df63f 100644 --- a/apps/dapp/src/components/DeployVault/AddNewStrategyButton.tsx +++ b/apps/dapp/src/components/DeployVault/AddNewStrategyButton.tsx @@ -101,19 +101,13 @@ function AddNewStrategyButton() { } const handleAmountInput = async (e: any) => { - const input = e.target.value - if (!input) { + if (!e) { console.log('input is empty') setSelectedAsset({ ...selectedAsset, amount: 0 }) } - console.log(input) const decimalRegex = /^(\d+)?(\.\d{0,7})?$/ - if (!decimalRegex.test(input)) return - if (input.startsWith('.')) { - setAmountInput({ amount: 0 + input, enabled: true }); - return - } - setAmountInput({ amount: input, enabled: true }); + if (!decimalRegex.test(e)) return + setAmountInput({ amount: e, enabled: true }); } const strategyExists = (strategy: Strategy) => { const exists = newVault.assets.some((asset) => asset.strategies.some((str) => str.address === strategy.address)) @@ -178,18 +172,22 @@ function AddNewStrategyButton() { )} {amountInput.enabled && ( - - Amount: - - + + Amount: + + + - - - - + handleAmountInput(Number(e.value))} + > + + + + + )} diff --git a/apps/dapp/src/components/DeployVault/ConfirmDelpoyModal.tsx b/apps/dapp/src/components/DeployVault/ConfirmDelpoyModal.tsx index 7a285b10..d0c7fbad 100644 --- a/apps/dapp/src/components/DeployVault/ConfirmDelpoyModal.tsx +++ b/apps/dapp/src/components/DeployVault/ConfirmDelpoyModal.tsx @@ -41,6 +41,7 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo const sorobanContext = useSorobanReact(); const { activeChain, address } = sorobanContext; const factory = useFactoryCallback(); + const { getInvestedFunds } = useVault(); const newVault: NewVaultState = useAppSelector(state => state.newVault); const indexName = useAppSelector(state => state.newVault.name) const indexSymbol = useAppSelector(state => state.newVault.symbol) @@ -50,7 +51,7 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo const feeReceiverString = useAppSelector(state => state.newVault.feeReceiver) const { transactionStatusModal: txModal, deployVaultModal: deployModal } = useContext(ModalContext); const dispatch = useAppDispatch(); - const { getIdleFunds, getInvestedFunds, getTVL, getUserBalance } = useVault() + const { getFees } = useVault() const [deployDisabled, setDeployDisabled] = useState(true); @@ -59,7 +60,7 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo managerString !== "" && emergencyManagerString !== "" && feeReceiverString !== "" - && !indexShare + && !indexShare ) { setDeployDisabled(false); } else { @@ -180,20 +181,20 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo if (newVault.assets[index]?.amount === 0) return nativeToScVal(0, { type: "i128" }); return nativeToScVal(convertedAmount, { type: "i128" }); }); - /* const amountsScVal = newVault.amounts.map((amount) => { - return nativeToScVal((amount * Math.pow(10, 7)), { type: "i128" }); - }); */ + /* const amountsScVal = newVault.amounts.map((amount) => { + return nativeToScVal((amount * Math.pow(10, 7)), { type: "i128" }); + }); */ const amountsScValVec = xdr.ScVal.scvVec(amountsScVal); - /* 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 - */ + /* 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 +*/ let result: any; @@ -258,6 +259,8 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo amount: newVault.assets[index]?.amount || 0 } }) + const investedFunds = await getInvestedFunds(parsedResult); + const fees = await getFees(parsedResult) const tempVault: VaultData = { ...newVault, address: parsedResult, @@ -268,6 +271,7 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo totalSupply: 0, idleFunds: idleFunds, investedFunds: [{ address: '', amount: 0 }], + fees: fees, } await txModal.handleSuccess(result.txHash); dispatch(pushVault(tempVault)); @@ -284,7 +288,7 @@ export const ConfirmDelpoyModal = ({ isOpen, onClose }: { isOpen: boolean, onClo Deploying {indexName === "" ? 'new index' : indexName} - + - diff --git a/apps/dapp/src/components/DeployVault/DeployVault.tsx b/apps/dapp/src/components/DeployVault/DeployVault.tsx index 55b1cce2..43726f92 100644 --- a/apps/dapp/src/components/DeployVault/DeployVault.tsx +++ b/apps/dapp/src/components/DeployVault/DeployVault.tsx @@ -44,7 +44,6 @@ export const DeployVault = () => { const handleRemoveStrategy = (strategy: Strategy) => { const asset = assets.find((a) => a.strategies.includes(strategy)) dispatch(removeStrategy(strategy)) - console.log(asset) if (asset?.strategies.length === 1) { dispatch(removeAsset(asset.address)) } @@ -126,7 +125,7 @@ export const DeployVault = () => { colorScheme="green" size="lg" w={['100%', null, 'auto']} - > + > Create Vault diff --git a/apps/dapp/src/components/DeployVault/VaultPreview.tsx b/apps/dapp/src/components/DeployVault/VaultPreview.tsx index e1945d56..1a876538 100644 --- a/apps/dapp/src/components/DeployVault/VaultPreview.tsx +++ b/apps/dapp/src/components/DeployVault/VaultPreview.tsx @@ -27,6 +27,7 @@ import { import { IoIosArrowDown, IoIosArrowUp } from 'react-icons/io' import { Asset } from '@/store/lib/types' import { InfoTip } from '../ui/toggle-tip' +import { NumberInputField, NumberInputRoot } from '../ui/number-input' export enum AccordionItems { @@ -260,10 +261,6 @@ export const VaultPreview: React.FC = ({ data, accordionValue if (input < 0 || input > 100) return const decimalRegex = /^(\d+)?(\.\d{0,2})?$/ if (!decimalRegex.test(input)) return - if (input.startsWith('.')) { - setFormControl({ ...formControl, vaultShare: 0 + input }); - return - } setFormControl({ ...formControl, vaultShare: input @@ -374,11 +371,12 @@ export const VaultPreview: React.FC = ({ data, accordionValue } /> - { handleVaultShareChange(e.target.value) }} + { handleVaultShareChange(Number(e.value)) }} required - /> + > + + This field is required. diff --git a/apps/dapp/src/components/InteractWithVault/EditVault.tsx b/apps/dapp/src/components/InteractWithVault/EditVault.tsx index 2be0c278..41f768df 100644 --- a/apps/dapp/src/components/InteractWithVault/EditVault.tsx +++ b/apps/dapp/src/components/InteractWithVault/EditVault.tsx @@ -24,6 +24,7 @@ import { import { InfoTip } from '../ui/toggle-tip' import { Tooltip } from '../ui/tooltip' import { FaRegPaste } from 'react-icons/fa6' +import { LuSettings2 } from "react-icons/lu"; import { dropdownData } from '../DeployVault/VaultPreview' import { Button } from '../ui/button' @@ -71,6 +72,7 @@ const CustomInputField = ({ css={{ "--bg": "{colors.red.400/40}" }} aria-label='Connected address' size={'sm'} + variant={'ghost'} onClick={() => handleClick(address!)} > @@ -98,7 +100,10 @@ export const EditVaultModal = () => { const vaultCB = useVaultCallback() const vault = useVault() const dispatch = useAppDispatch() - const { transactionStatusModal: statusModal, interactWithVaultModal: interactModal, inspectVaultModal: inspectModal } = useContext(ModalContext) + const { + transactionStatusModal: statusModal, + rebalanceVaultModal: rebalanceModal + } = useContext(ModalContext) const [formControl, setFormControl] = useState({ feeReceiver: { value: selectedVault?.feeReceiver ?? '', @@ -207,7 +212,15 @@ export const EditVaultModal = () => { <> - Manage {selectedVault.name} + + Manage {selectedVault.name} + rebalanceModal.setIsOpen(true)} + size={'sm'} + > + + + diff --git a/apps/dapp/src/components/InteractWithVault/InvestStrategies.tsx b/apps/dapp/src/components/InteractWithVault/InvestStrategies.tsx new file mode 100644 index 00000000..1c8488e4 --- /dev/null +++ b/apps/dapp/src/components/InteractWithVault/InvestStrategies.tsx @@ -0,0 +1,215 @@ +import { Box, Button, For, HStack, NumberInput, NumberInputRoot, Stack, Text } from "@chakra-ui/react" +import { DialogBody, DialogContent, DialogHeader } from "../ui/dialog" +import { useAppDispatch, useAppSelector } from "@/store/lib/storeHooks" +import { useVault, useVaultCallback, VaultMethod } from "@/hooks/useVault" +import { setStrategyTempAmount, updateVaultData } from "@/store/lib/features/walletStore" +import { useContext, useEffect, useState } from "react" +import { InputGroup } from "../ui/input-group" +import { NumberInputField } from "../ui/number-input" +import { AssetInvestmentAllocation } from "@/hooks/types" +import { Address, Asset, nativeToScVal, xdr } from "@stellar/stellar-sdk" +import { ModalContext } from "@/contexts" +import { useSorobanReact } from "@soroban-react/core" +import { Field } from "../ui/field" + +interface InvestState extends AssetInvestmentAllocation { + total: number +} + +export const InvestStrategies = () => { + const { selectedVault } = useAppSelector(state => state.wallet.vaults) + const { getUserBalance, getIdleFunds, getInvestedFunds } = useVault() + const vaultCB = useVaultCallback() + const dispatch = useAppDispatch() + const { + transactionStatusModal: txModal, + investStrategiesModal: investModal, + inspectVaultModal: inspectModal + } = useContext(ModalContext) + const { address } = useSorobanReact() + const [investment, setInvestment] = useState([]) + const [invalidAmount, setInvalidAmount] = useState(false) + + const handleInvestInput = (assetIndex: number, strategyIndex: number, amount: number) => { + const newInvestment = [...investment] + newInvestment[assetIndex]!.strategy_investments[strategyIndex]!.amount = amount + newInvestment[assetIndex]!.total = newInvestment[assetIndex]!.strategy_investments.reduce((acc, curr) => acc + curr.amount, 0) + setInvestment(newInvestment) + } + + const handleInvest = async () => { + if (!selectedVault || !address) return + txModal.initModal() + investModal.setIsOpen(false) + const mappedParam = xdr.ScVal.scvVec( + investment.map((entry) => + xdr.ScVal.scvMap([ + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("asset"), + val: new Address(entry.asset).toScVal()// Convert asset address to ScVal + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("strategy_investments"), + val: xdr.ScVal.scvVec( + entry.strategy_investments.map((strategy_investment) => { + return xdr.ScVal.scvMap([ + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("amount"), + val: nativeToScVal(BigInt((strategy_investment.amount ?? 0) * 10 ** 7), { type: "i128" }), // Ensure i128 conversion + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("strategy"), + val: new Address(strategy_investment.strategy).toScVal() // Convert strategy address + }), + ]) + }) + ), + }), + ]) + ) + ); + try { + const response = await vaultCB(VaultMethod.INVEST, selectedVault.address, [mappedParam], true) + await txModal.handleSuccess(response.txHash) + const newInvestedFunds = await getInvestedFunds(selectedVault.address) + const newIdleFunds = await getIdleFunds(selectedVault.address) + await dispatch(updateVaultData({ + address: selectedVault.address, + idleFunds: newIdleFunds, + investedFunds: newInvestedFunds + })) + + } catch (error: any) { + await txModal.handleError(error.toString()) + console.error('Could not invest: ', error) + } + } + + useEffect(() => { + const totals = investment.map((asset) => { + const totalAmount = asset.strategy_investments.reduce((acc, curr) => acc + curr.amount, 0) + const element = { + asset: asset.asset, + total: totalAmount + } + return element + }) + totals.forEach((asset) => { + const assetFunds = selectedVault?.idleFunds.find((fund) => { + return fund.address === asset.asset + })?.amount + if (assetFunds && assetFunds < asset.total) { + setInvalidAmount(true) + return + } else if (assetFunds && assetFunds >= asset.total) { + setInvalidAmount(false) + } + }) + }, [investment]) + + useEffect(() => { + if (!selectedVault) return + if (selectedVault.assets && investModal.isOpen) { + selectedVault.assets.forEach((asset) => { + const investmentAllocation: InvestState = { + asset: asset.address, + strategy_investments: [], + total: 0 + } + asset.strategies.forEach((strategy) => { + investmentAllocation.strategy_investments.push({ + amount: 0, + strategy: strategy.address + }) + }) + if (investment.length === 0) { + setInvestment([investmentAllocation]) + } else { + const assetIndex = investment.findIndex((a) => a.asset === asset.address) + if (assetIndex === -1) { + setInvestment([...investment, investmentAllocation]) + } + } + }) + } else if (!investModal.isOpen) { + setInvestment([]) + } + }, [selectedVault, selectedVault?.assets, investModal.isOpen]) + + useEffect(() => { + if (!selectedVault) return + if (selectedVault.assets) { + selectedVault.assets.forEach(async (asset) => { + asset.strategies.forEach(async (strategy) => { + const tempAmount = await getUserBalance(selectedVault.address, strategy.address) + dispatch(setStrategyTempAmount({ + vaultAddress: selectedVault?.address!, + strategyAddress: strategy.address, + amount: tempAmount ?? 0 + })) + }) + }) + } + }, [selectedVault, selectedVault?.assets]) + + if (!selectedVault) return null + return ( + + +

Invest in strategies

+
+ + + Idle funds: + + {(fund, i) => ( + + $ {fund.amount ?? 0} + + {selectedVault?.assets.find(asset => asset.address === fund.address)?.symbol} + + + )} + + Strategies: + + {(asset, j) => ( + + + + {(strategy, k) => ( + + + {strategy.name} + + Balance: + $ {strategy.tempAmount} + {asset.symbol} + + + + {asset.symbol} + }> + handleInvestInput(j, k, Number(e.value))} + > + + + + + + )} + + + Total of investment: ${investment[j]?.total.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 7 })} + + )} + + + + +
+ ) +} \ No newline at end of file diff --git a/apps/dapp/src/components/InteractWithVault/RebalanceVault.tsx b/apps/dapp/src/components/InteractWithVault/RebalanceVault.tsx new file mode 100644 index 00000000..04b62332 --- /dev/null +++ b/apps/dapp/src/components/InteractWithVault/RebalanceVault.tsx @@ -0,0 +1,283 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { useAppDispatch, useAppSelector } from '@/store/lib/storeHooks'; +import { useSorobanReact } from '@soroban-react/core'; +import { DialogBody, DialogContent, DialogHeader } from '../ui/dialog'; +import { Box, Button, For, Grid, GridItem, HStack, IconButton, Input, List, NativeSelectField, Separator, Stack, Text } from '@chakra-ui/react'; +import { Strategy } from '@/store/lib/types'; +import { NativeSelectRoot } from '../ui/native-select'; +import { InputGroup } from '../ui/input-group'; +import { NumberInputField, NumberInputRoot } from '../ui/number-input'; +import { useVault, useVaultCallback, VaultMethod } from '@/hooks/useVault'; +import { ActionType, RebalanceInstruction } from '@/hooks/types'; +import { setStrategyTempAmount, updateVaultData } from '@/store/lib/features/walletStore'; +import { IoMdAdd } from "react-icons/io"; +import { Address, nativeToScVal, scValToNative, xdr } from '@stellar/stellar-sdk'; +import { FaRegTrashCan } from "react-icons/fa6"; +import { ModalContext } from '@/contexts'; + +interface RebalanceInstructionState { + action: ActionType | undefined; + amount: number; + strategy: string; + descritpion: string; +} + +const RebalanceVault: React.FC = (() => { + const { address } = useSorobanReact() + const { selectedVault } = useAppSelector(state => state.wallet.vaults); + const { getUserBalance, getIdleFunds, getInvestedFunds } = useVault(); + const vaultCB = useVaultCallback(); + const dispatch = useAppDispatch(); + const { + transactionStatusModal: txModal, + inspectVaultModal: inspectModal, + rebalanceVaultModal: rebalanceModal + } = useContext(ModalContext) + const [instructions, setInstructions] = useState([]) + const [tempInstruction, setTempInstruction] = useState({ + action: undefined, + amount: 0, + strategy: '', + descritpion: '' + }) + const validActions = [ + "Invest", + "Withdraw" + ] + + const generateDescription = (action: ActionType, amount: number, strategy: string) => { + const strategyName = selectedVault?.assets[0]?.strategies.find((s) => s.address === strategy)?.name + return `${ActionType[action]} ${amount} ${selectedVault?.assets[0]?.symbol} ${action == 1 ? 'to' : 'from'} ${strategyName}` + } + + const handleRemoveInstruction = (index: number) => { + const newInstructions = instructions.filter((_, i) => i !== index) + setInstructions(newInstructions) + } + + const handleRebalanceVault = async (instructions: RebalanceInstructionState[]) => { + txModal.initModal() + if (!selectedVault) return + const mappedArgs = xdr.ScVal.scvVec( + instructions.map((instruction) => + xdr.ScVal.scvMap([ + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("action"), + val: nativeToScVal(instruction.action, { type: "u32" }), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("amount"), + val: instruction.amount !== undefined + ? nativeToScVal((instruction.amount * 10 ** 7), { type: "i128" }) + : xdr.ScVal.scvVoid(), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("strategy"), + val: instruction.strategy + ? new Address(instruction.strategy).toScVal() + : xdr.ScVal.scvVoid(), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("swap_details_exact_in"), + val: xdr.ScVal.scvVec([xdr.ScVal.scvSymbol("None")]), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("swap_details_exact_out"), + val: xdr.ScVal.scvVec([xdr.ScVal.scvSymbol("None")]), + }), + ]) + ) + ); + try { + const result = await vaultCB(VaultMethod.REBALANCE, selectedVault?.address!, [mappedArgs], true) + await txModal.handleSuccess(result.txHash) + const newInvestedFunds = await getInvestedFunds(selectedVault.address) + const newIdleFunds = await getIdleFunds(selectedVault.address) + await dispatch(updateVaultData({ + address: selectedVault.address, + idleFunds: newIdleFunds, + investedFunds: newInvestedFunds + })) + await setTimeout(() => { + rebalanceModal.setIsOpen(false) + inspectModal.setIsOpen(false) + }, 4500) + } catch (e: any) { + console.error(e) + await txModal.handleError(e) + } + } + + useEffect(() => { + if (!selectedVault) return + if (selectedVault.assets) { + selectedVault.assets.forEach(async (asset) => { + asset.strategies.forEach(async (strategy) => { + const tempAmount = await getUserBalance(selectedVault.address, strategy.address) + dispatch(setStrategyTempAmount({ + vaultAddress: selectedVault?.address!, + strategyAddress: strategy.address, + amount: tempAmount ?? 0 + })) + }) + }) + } + }, [selectedVault]); + + useEffect(() => { + setTempInstruction({ + ...tempInstruction, + descritpion: generateDescription(tempInstruction.action!, tempInstruction.amount, tempInstruction.strategy) + }) + }, [tempInstruction.action, tempInstruction.amount, tempInstruction.strategy]) + + useEffect(() => { + if (!rebalanceModal.isOpen) { + setInstructions([]) + } + }, [rebalanceModal.isOpen]) + + return ( + + + Rebalance + + + + + + {(asset, i) => ( + + {asset.symbol} strategies: + + {(strategy, j) => ( + + {strategy.name} ${strategy.tempAmount} + {selectedVault?.assets[0]?.symbol} + + )} + + + )} + + + + + + Idle funds: + + {selectedVault?.idleFunds.map((idleFund, i) => ( + + ${idleFund.amount} {selectedVault.assets[i]?.symbol} + + ))} + + + + + Invested funds: + + {selectedVault?.investedFunds.map((investedFund, i) => ( + + ${investedFund.amount} {selectedVault.assets[i]?.symbol} + + ))} + + + + + + + + + setTempInstruction({ ...tempInstruction, action: ActionType[e.currentTarget.value as keyof typeof ActionType] })}> + + + {(action, index) => ( + + )} + + + + + + + setTempInstruction({ ...tempInstruction, strategy: e.currentTarget.value })}> + + + {(strategy, index) => ( + + )} + + + + + + + {selectedVault?.assets[0]?.symbol} + + } + > + setTempInstruction({ ...tempInstruction, amount: Number(e.value) })} + > + + + + + + { + setInstructions([...instructions, tempInstruction as RebalanceInstructionState]) + } + } + > + + + + + + {instructions.length > 0 && + + {(instruction, index) => ( + + + + {instruction.descritpion} + + handleRemoveInstruction(index)} + colorPalette={'red'} + > + + + + + )} + + } + + + + + + ); +}); + +export default RebalanceVault; \ No newline at end of file diff --git a/apps/dapp/src/components/ManageVaults/InspectVault.tsx b/apps/dapp/src/components/ManageVaults/InspectVault.tsx index e3917600..2964f0c9 100644 --- a/apps/dapp/src/components/ManageVaults/InspectVault.tsx +++ b/apps/dapp/src/components/ManageVaults/InspectVault.tsx @@ -27,7 +27,7 @@ export const InspectVault = ({ }) => { const selectedVault: VaultData | undefined = useAppSelector(state => state.wallet.vaults.selectedVault) const { address } = useSorobanReact() - const { editVaultModal: editModal } = useContext(ModalContext) + const { editVaultModal: editModal, investStrategiesModal: investModal, rebalanceVaultModal: rebalanceModal } = useContext(ModalContext) if (!selectedVault) return null return ( @@ -68,7 +68,7 @@ export const InspectVault = ({ Strategies: {selectedVault.assets.map((asset: Asset, index: number) => ( - + {(strategy: Strategy, index: number) => ( @@ -109,6 +109,15 @@ export const InspectVault = ({ ))} + + Fees: + + Defindex fee: {(selectedVault.fees[0]! / 100).toLocaleString('en-US', { style: 'decimal', maximumFractionDigits: 2 })} % + + + Vault fee: {(selectedVault.fees[1]! / 100).toLocaleString('en-US', { style: 'decimal', maximumFractionDigits: 2 })} % + + {(address && selectedVault.userBalance) && User balance: @@ -123,6 +132,10 @@ export const InspectVault = ({ {address && } + {(address && selectedVault.idleFunds[0]?.amount! > 0) && + + } + {(address === selectedVault.manager) && } {(address === selectedVault.emergencyManager || address === selectedVault.manager) && } diff --git a/apps/dapp/src/components/ManageVaults/ManageVaults.tsx b/apps/dapp/src/components/ManageVaults/ManageVaults.tsx index aee2fc75..d6392076 100644 --- a/apps/dapp/src/components/ManageVaults/ManageVaults.tsx +++ b/apps/dapp/src/components/ManageVaults/ManageVaults.tsx @@ -27,6 +27,8 @@ import { Stack, } from "@chakra-ui/react" import { EditVaultModal } from "../InteractWithVault/EditVault" +import RebalanceVault from "../InteractWithVault/RebalanceVault" +import { InvestStrategies } from "../InteractWithVault/InvestStrategies" export const ManageVaults = () => { const { address, activeChain } = useSorobanReact() @@ -35,7 +37,9 @@ export const ManageVaults = () => { deployVaultModal: deployModal, interactWithVaultModal: interactModal, transactionStatusModal: txModal, - editVaultModal: editModal + editVaultModal: editModal, + rebalanceVaultModal: rebalanceModal, + investStrategiesModal: investModal, } = useContext(ModalContext) const dispatch = useAppDispatch() const modalContext = useContext(ModalContext) @@ -137,6 +141,8 @@ export const ManageVaults = () => { } + + {/* Interact with vault */} { + + {/* Inspect vault */} { inspectModal.setIsOpen(e.open) }} - size={'lg'} + size={'xl'} placement={'center'} > @@ -162,6 +170,8 @@ export const ManageVaults = () => { onClose={() => { inspectModal.setIsOpen(false) }} /> + + {/* Edit vault */} { editModal.setIsOpen(e.open) }} @@ -171,6 +181,8 @@ export const ManageVaults = () => { + + {/* Transaction status modal */} { txModal.setIsOpen(e.open) }} @@ -180,6 +192,24 @@ export const ManageVaults = () => { + { rebalanceModal.setIsOpen(e.open) }} + size={'lg'} + placement={'center'} + > + + + + { investModal.setIsOpen(e.open) }} + size={'lg'} + placement={'center'} + > + + + ) diff --git a/apps/dapp/src/components/ui/field.tsx b/apps/dapp/src/components/ui/field.tsx new file mode 100644 index 00000000..dd3b66f1 --- /dev/null +++ b/apps/dapp/src/components/ui/field.tsx @@ -0,0 +1,33 @@ +import { Field as ChakraField } from "@chakra-ui/react" +import * as React from "react" + +export interface FieldProps extends Omit { + label?: React.ReactNode + helperText?: React.ReactNode + errorText?: React.ReactNode + optionalText?: React.ReactNode +} + +export const Field = React.forwardRef( + function Field(props, ref) { + const { label, children, helperText, errorText, optionalText, ...rest } = + props + return ( + + {label && ( + + {label} + + + )} + {children} + {helperText && ( + {helperText} + )} + {errorText && ( + {errorText} + )} + + ) + }, +) diff --git a/apps/dapp/src/contexts/index.ts b/apps/dapp/src/contexts/index.ts index b4d4e28d..7b67807e 100644 --- a/apps/dapp/src/contexts/index.ts +++ b/apps/dapp/src/contexts/index.ts @@ -41,6 +41,8 @@ export type ModalContextType = { inspectVaultModal: ToggleModalProps, interactWithVaultModal: ToggleModalProps, editVaultModal: ToggleModalProps, + rebalanceVaultModal: ToggleModalProps, + investStrategiesModal: ToggleModalProps, }; export const ModalContext = React.createContext({ @@ -78,4 +80,12 @@ export const ModalContext = React.createContext({ isOpen: false, setIsOpen: () => {}, }, + rebalanceVaultModal: { + isOpen: false, + setIsOpen: () => {}, + }, + investStrategiesModal: { + isOpen: false, + setIsOpen: () => {}, + }, }); \ No newline at end of file diff --git a/apps/dapp/src/hooks/types.ts b/apps/dapp/src/hooks/types.ts new file mode 100644 index 00000000..d152dadb --- /dev/null +++ b/apps/dapp/src/hooks/types.ts @@ -0,0 +1,38 @@ +/* +#[contracttype] +struct StrategyInvestment { + amount: i128, + strategy: address +} + +#[contracttype] +struct AssetInvestmentAllocation { + asset: address, + strategy_investments: vec> +} +*/ + + +export interface StrategyInvestment { + amount: number; + strategy: string; +} + +export interface AssetInvestmentAllocation { + asset: string; + strategy_investments: StrategyInvestment[]; +} +export interface RebalanceInstruction { + action: ActionType; + amount: number; + strategy: string; + swapDetailsExactIn?: any//SwapDetails; + swapDetailsExactOut?: any//SwapDetails; +} +export enum ActionType { + Withdraw = 0, + Invest = 1, + SwapExactIn = 2, + SwapExactOut = 3, + Zapper = 4, +} \ No newline at end of file diff --git a/apps/dapp/src/hooks/useVault.ts b/apps/dapp/src/hooks/useVault.ts index 4ce916aa..06f6b2f3 100644 --- a/apps/dapp/src/hooks/useVault.ts +++ b/apps/dapp/src/hooks/useVault.ts @@ -24,6 +24,9 @@ export enum VaultMethod { GETIDLEFUNDS = "fetch_current_idle_funds", GETINVESTEDFUNDS = "fetch_current_invested_funds", SETFEERECIEVER = "set_fee_receiver", + INVEST = "invest", + REBALANCE= "rebalance", + GETFEES = "get_fees", } const isObject = (val: unknown) => typeof val === 'object' && val !== null && !Array.isArray(val); @@ -54,7 +57,6 @@ export function useVaultCallback() { export const useVault = (vaultAddress?: string | undefined) => { const vault = useVaultCallback(); const sorobanContext = useSorobanReact(); - const {address} = sorobanContext; const getVaultInfo = async (vaultAddress: string) => { if (!vaultAddress) return; try { @@ -67,7 +69,8 @@ export const useVault = (vaultAddress?: string | undefined) => { TVL, totalSupply, idleFunds, - investedFunds + investedFunds, + fees ] = await Promise.all([ getVaultManager(vaultAddress), getVaultEmergencyManager(vaultAddress), @@ -77,7 +80,8 @@ export const useVault = (vaultAddress?: string | undefined) => { getTVL(vaultAddress), getVaultTotalSupply(vaultAddress), getIdleFunds(vaultAddress), - getInvestedFunds(vaultAddress) + getInvestedFunds(vaultAddress), + getFees(vaultAddress) ]); for (let asset of assets){ const symbol = await getTokenSymbol(asset.address, sorobanContext); @@ -96,6 +100,7 @@ export const useVault = (vaultAddress?: string | undefined) => { totalSupply: totalSupply || 0, idleFunds: idleFunds || [], investedFunds: investedFunds || [], + fees: fees || [50,0], } return newData } catch (error) { @@ -201,6 +206,14 @@ export const useVault = (vaultAddress?: string | undefined) => { console.error(error); } } + const getFees = async (vaultAddress: string) => { + try { + const fees = await vault(VaultMethod.GETFEES, vaultAddress, undefined, false).then((res: any) => scValToNative(res)); + return fees || [50,0]; + } catch (error) { + console.error(error); + } + } const vaultInfo = getVaultInfo(vaultAddress!); return { @@ -215,6 +228,7 @@ export const useVault = (vaultAddress?: string | undefined) => { getUserBalance, getTVL, getIdleFunds, - getInvestedFunds, + getInvestedFunds, + getFees }; } \ No newline at end of file diff --git a/apps/dapp/src/providers/modal-provider.tsx b/apps/dapp/src/providers/modal-provider.tsx index 132e2139..830ecafc 100644 --- a/apps/dapp/src/providers/modal-provider.tsx +++ b/apps/dapp/src/providers/modal-provider.tsx @@ -18,6 +18,8 @@ export const ModalProvider = ({ const [isInspectVaultModalOpen, setIsInspectVaultModalOpen] = React.useState(false) const [isInteractWithVaultModalOpen, setIsInteractWithVaultModalOpen] = React.useState(false) const [isEditVaultModalOpen, setIsEditVaultModalOpen] = React.useState(false) + const [isRebalanceModalOpen, setIsRebalanceModalOpen] = React.useState(false) + const [isInvestStrategiesModalOpen, setIsInvestStrategiesModalOpen] = React.useState(false) const [isTransactionStatusModalOpen, setIsTransactionStatusModalOpen] = React.useState(false) const [transactionStatusModalStep, setTransactionStatusModalStep] = React.useState(0) @@ -103,6 +105,14 @@ export const ModalProvider = ({ isOpen: isEditVaultModalOpen, setIsOpen: setIsEditVaultModalOpen, }, + rebalanceVaultModal: { + isOpen: isRebalanceModalOpen, + setIsOpen: setIsRebalanceModalOpen, + }, + investStrategiesModal: { + isOpen: isInvestStrategiesModalOpen, + setIsOpen: setIsInvestStrategiesModalOpen, + }, } return ( diff --git a/apps/dapp/src/store/lib/features/vaultStore.ts b/apps/dapp/src/store/lib/features/vaultStore.ts index ccfabad7..5a2a916c 100644 --- a/apps/dapp/src/store/lib/features/vaultStore.ts +++ b/apps/dapp/src/store/lib/features/vaultStore.ts @@ -29,6 +29,7 @@ export const getDefaultStrategies = async (network: string) => { address: remoteStrategies.ids[strategy], name: parsedName ? prettierName : '', paused: false, + tempAmount: 0 }) } } diff --git a/apps/dapp/src/store/lib/features/walletStore.ts b/apps/dapp/src/store/lib/features/walletStore.ts index cfb0d1d3..d9d9c5ae 100644 --- a/apps/dapp/src/store/lib/features/walletStore.ts +++ b/apps/dapp/src/store/lib/features/walletStore.ts @@ -5,7 +5,6 @@ import { ChainMetadata } from '@soroban-react/types' import vaults from '@/constants/constants.json' import { Networks } from '@stellar/stellar-sdk' import { SelectedVault, VaultData, WalletState } from '../types' -import { VaultMethod } from '@/hooks/useVault' const getDefaultVaults = async (network: string) => { const filteredVaults = vaults.filter(vault => { @@ -120,6 +119,15 @@ export const walletSlice = createSlice({ } }) }, + setStrategyTempAmount: (state, action: PayloadAction<{vaultAddress: string, strategyAddress: string, amount: number}>) => { + state.vaults.selectedVault?.assets.forEach(asset => { + asset.strategies.forEach(strategy => { + if (strategy.address === action.payload.strategyAddress) { + strategy.tempAmount = action.payload.amount + } + }) + }) + } }, extraReducers(builder) { builder.addCase(fetchDefaultAddresses.pending, (state) => { @@ -148,7 +156,8 @@ export const { resetSelectedVault, setVaultFeeReceiver, setVaultUserBalance, - updateVaultData + updateVaultData, + setStrategyTempAmount } = walletSlice.actions // Other code such as selectors can use the imported `RootState` type diff --git a/apps/dapp/src/store/lib/types.ts b/apps/dapp/src/store/lib/types.ts index a699867a..e8f8542b 100644 --- a/apps/dapp/src/store/lib/types.ts +++ b/apps/dapp/src/store/lib/types.ts @@ -23,6 +23,7 @@ export interface Strategy { address: string; name: string; paused: boolean; + tempAmount: number; } export interface AssetAmmount { @@ -39,7 +40,8 @@ export interface VaultData { TVL: number; totalSupply: number; idleFunds: AssetAmmount[]; - investedFunds: AssetAmmount[] + investedFunds: AssetAmmount[]; + fees: number[]; userBalance?: number; } diff --git a/apps/docs/10-whitepaper/02-state-of-the-art/01-yearn-finance.md b/apps/docs/10-whitepaper/02-state-of-the-art/01-yearn-finance.md index 13744f2f..10530709 100644 --- a/apps/docs/10-whitepaper/02-state-of-the-art/01-yearn-finance.md +++ b/apps/docs/10-whitepaper/02-state-of-the-art/01-yearn-finance.md @@ -240,3 +240,14 @@ performance_fees = total_fees - protocol_fees = 18 Fees are collected when a strategy reports gains or losses via the report() function. During the report, the strategy will calculate the gains since the last report and then calculate the fees based on the gains. This fees are then distributed as shares of the vault. Then, fees are collected per strategy. + +Accountant reports the fees or refunds to the vault, from the gains or losses of the strategy. Then, the vault will calculate the fees and the protocol fees and then distribute the fees to the vault manager and the protocol fee recipient. This accountant is an interface and apparently it depends on the vault. + +Yearn burns shares when there is fees or losses. When there is a loss and there is still fees not paid, the vault will burn shares to pay the fees. + +The Vaults utilizes several mechanisms to mitigate price per share (pps) fluctuations and manipulation: +1. Internal accounting is used instead of balanceOf() to keep track of the vault's debt and idle. +2. A profit locking machenism designed by V3 Vaults locks profits or accountant's refunds by issuing new shares to the vault itself that are slowly burnt over the unlock perior. +3. In the event of losses or fees, the vault will always try to offset them by butning locked shares it owns. the price per share is expected to decrease only when excess losses or fees occur upon processing a report, or a loss occurs upon force revoking a strategy. + [reference](https://github.com/yearn/yearn-security/blob/master/audits/20240504_ChainSecurity_Yearn_V3/Yearn-Smart-Contract-Audit_V3_Vaults_-ChainSecurity.pdf) + \ No newline at end of file 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 d0b94513..911e48cc 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 @@ -147,36 +147,38 @@ The fees collected are from the gains of the strategies. Thus, it is a performan ### Fee Collection Methodology -The fee collection process locks the fees in the vault until they are distributed. These fees are got from the gains of the strategies. +The DeFindex fee collection process is designed to track fees in the vault until distribution, with fees originating from the strategy gains. This ensures an organized and accountable fee handling system. -Let's see an example of how this works. -1. The fees for the DeFindex Protocol Fee Receiver is set to 5% and the fees for the Vault Fee Receiver is set to 15%. -2. A user deposits 100 USDC into the vault that only has one strategy. -3. The strategy gains 10 USDC. -4. The strategy reports the gains to the vault. -5. The vault collects the fees from the gains. In this case, 2 USDC (20% of 10 USDC). -6. The vault distributes the fees to the protocol and the manager. +#### General Overview +Fees are charged on a per-strategy basis, meaning each strategy independently calculates its gains and the corresponding fees. These fees are then collected and distributed to the protocol and manager. The fee percentages are fixed per vault and they are decided when creating it. -Now the total assets of the vault are 100 USDC + 10 USDC - 2 USDC = 108 USDC. +#### Detailed Workflow -And the 2 USDC are locked in the vault, for future distributions. +1. **Fee Structure Example**: + - Protocol Fee Receiver: 5% + - Vault Fee Receiver: 15% -These fees are collected in a per strategy basis. So, if there are multiple strategies, each strategy will have its own fees. +2. **Execution Example**: + - A user deposits 100 USDC into a vault with one strategy. + - The strategy earns 10 USDC in gains. + - The vault collects 20% of the gains as fees (2 USDC). + - Fees are distributed between the protocol (0.5 USDC) and the manager (1.5 USDC). + - The total assets of the vault become \(100 + 10 - 2 = 108\) USDC. -In order to account for the fees, as fees depend on the gains of the strategy, we need to track the gains of the strategy. -So, we create a function called `report()` that will keep record of the gains and losses of the strategy. -In this function, we will: -- store gains and losses from the strategy since the last time we checked. - -Let's see a pseudocode for this function: +#### Strategy Gains Tracking +Since fees depend on strategy performance, gains and losses must be tracked meticulously. To achieve this, a `report()` function is implemented (in the vault contract) to log the gains or losses since the last update. +**Pseudocode for Tracking Gains and Losses**: ```rust fn report(strategy: Address) -> (u256, u256) { let current_balance = get_current_balance(strategy); let prev_balance = get_prev_balance(strategy); + let previous_gains_or_losses = get_gains_or_losses(strategy); + let gains_or_losses = current_balance - prev_balance; - - store_gains_or_losses(strategy, gains_or_losses); + let current_gains_or_losses = previous_gains_or_losses + gains_or_losses; + + store_gains_or_losses(strategy, current_gains_or_losses); store_prev_balance(strategy, current_balance); } @@ -186,29 +188,34 @@ fn report_all_strategies() { } } ``` -This report_all_strategies() function is called at the beginning whenever the manager wants to rebalance the DeFindex, or a user wants to deposit or withdraw. -Then, at the end of a rebalance, deposit or withdrawal function call, the store_prev_balance() function is called to update the previous balance of the strategy. +- **Usage**: The `report_all_strategies()` function is invoked during key operations such as rebalancing, deposits, or withdrawals to ensure accurate gain tracking. -Then another function will be called to distribute the fees to the protocol and the manager. And it will set the gains and losses to 0. - -Here is a pseudocode for the fee distribution function: +#### Fee Distribution +Once gains are tracked, fees are calculated and distributed accordingly. After distribution, the gains and losses for each strategy are reset to 0. +**Pseudocode for Fee Distribution**: ```rust fn distribute_fees() { for strategy in strategies { let gains_or_losses = get_gains_or_losses(strategy); - let protocol_fee = gains_or_losses * protocol_fee_receiver/MAX_BPS; - let vault_fee = gains_or_losses * vault_fee_receiver/MAX_BPS; - transfer_from_strategy(strategy.asset, protocol_fee_receiver, protocol_fee); - transfer_from_strategy(strategy.asset, vault_fee_receiver, vault_fee); - reset_gains_or_losses(strategy); + if gains_or_losses > 0 { + let protocol_fee = gains_or_losses * protocol_fee_receiver / MAX_BPS; + let vault_fee = gains_or_losses * vault_fee_receiver / MAX_BPS; + transfer_from_strategy(strategy.asset, protocol_fee_receiver, protocol_fee); + transfer_from_strategy(strategy.asset, vault_fee_receiver, vault_fee); + reset_gains_or_losses(strategy); + } } } ``` -**Note:** in order to show the current balance for users, we should discount the fees (if there is gains) from the total assets of the Vaults. - This function is public and can be called by anyone. +#### Displaying User Balances +To provide users with an accurate view of their balances, any outstanding fees should be deducted offchain from the total assets when showing the current balances. + +By following this structured methodology, DeFindex ensures transparent and fair fee collection, tracking, and distribution processes. + + 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.