diff --git a/apps/contracts/Cargo.toml b/apps/contracts/Cargo.toml index dd4a992d..9dee20c8 100644 --- a/apps/contracts/Cargo.toml +++ b/apps/contracts/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["strategies/*", "defindex", "factory"] +members = ["strategies/*", "vault", "factory"] exclude = [ "strategies/external_wasms", ] diff --git a/apps/contracts/Makefile b/apps/contracts/Makefile index 9c88fe8c..30807186 100644 --- a/apps/contracts/Makefile +++ b/apps/contracts/Makefile @@ -1,4 +1,4 @@ -SUBDIRS = strategies defindex factory +SUBDIRS = strategies vault factory default: build diff --git a/apps/contracts/README.md b/apps/contracts/README.md index db900e43..fff530c2 100644 --- a/apps/contracts/README.md +++ b/apps/contracts/README.md @@ -32,7 +32,7 @@ Make sure you have the hodl strategy deployed, if not, you can run: yarn deploy-hodl ``` -to test the factory to deploy a DeFindex Vault +to test the factory to deploy a DeFindex Vault, and deposit there two times, you can run: ```bash yarn test diff --git a/apps/contracts/defindex/src/constants.rs b/apps/contracts/defindex/src/constants.rs deleted file mode 100644 index 772d4f54..00000000 --- a/apps/contracts/defindex/src/constants.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) const MAX_BPS: i128 = 10_000; -pub(crate) const SECONDS_PER_YEAR: i128 = 31_536_000; \ No newline at end of file diff --git a/apps/contracts/defindex/src/investment.rs b/apps/contracts/defindex/src/investment.rs deleted file mode 100644 index 3882dd27..00000000 --- a/apps/contracts/defindex/src/investment.rs +++ /dev/null @@ -1,43 +0,0 @@ -use soroban_sdk::{Address, Env, Map, Vec}; - -use crate::{models::Investment, strategies::{get_strategy_asset, get_strategy_client, invest_in_strategy}, utils::check_nonnegative_amount, ContractError}; - -pub fn prepare_investment(e: &Env, investments: Vec, idle_funds: Map) -> Result, ContractError> { - let mut total_investment_per_asset: Map = Map::new(e); - - for investment in investments.iter() { - let strategy_address = &investment.strategy; - let amount_to_invest = investment.amount; - check_nonnegative_amount(amount_to_invest.clone())?; - - // Find the corresponding asset for the strategy - let asset = get_strategy_asset(&e, strategy_address)?; - - // Track investment per asset - let current_investment = total_investment_per_asset - .get(asset.address.clone()) - .unwrap_or(0); - let updated_investment = current_investment.checked_add(amount_to_invest).ok_or(ContractError::Overflow)?; - - total_investment_per_asset.set(asset.address.clone(), updated_investment); - - // Check if total investment exceeds idle funds - let idle_balance = idle_funds.get(asset.address.clone()).unwrap_or(0); - if updated_investment > idle_balance { - return Err(ContractError::NotEnoughIdleFunds); - } - } - - Ok(total_investment_per_asset) -} - -pub fn execute_investment(e: &Env, investments: Vec) -> Result<(), ContractError> { - for investment in investments.iter() { - let strategy_address = &investment.strategy; - let amount_to_invest = &investment.amount; - - invest_in_strategy(e, strategy_address, amount_to_invest)? - } - - Ok(()) -} \ No newline at end of file diff --git a/apps/contracts/defindex/src/test/withdraw.rs b/apps/contracts/defindex/src/test/withdraw.rs deleted file mode 100644 index fc2209ca..00000000 --- a/apps/contracts/defindex/src/test/withdraw.rs +++ /dev/null @@ -1,116 +0,0 @@ -use soroban_sdk::{vec as sorobanvec, Address, String, Vec}; - -use crate::test::{create_strategy_params, defindex_vault::{AssetAllocation, Investment}, DeFindexVaultTest}; - -#[test] -fn test_withdraw_from_idle_success() { - let test = DeFindexVaultTest::setup(); - test.env.mock_all_auths(); - let strategy_params = create_strategy_params(&test); - let assets: Vec = sorobanvec![ - &test.env, - AssetAllocation { - address: test.token0.address.clone(), - strategies: strategy_params.clone() - } - ]; - - test.defindex_contract.initialize( - &assets, - &test.manager, - &test.emergency_manager, - &test.fee_receiver, - &2000u32, - &test.defindex_receiver, - &test.defindex_factory, - &String::from_str(&test.env, "dfToken"), - &String::from_str(&test.env, "DFT"), - ); - let amount = 1000i128; - - let users = DeFindexVaultTest::generate_random_users(&test.env, 1); - - test.token0_admin_client.mint(&users[0], &amount); - let user_balance = test.token0.balance(&users[0]); - assert_eq!(user_balance, amount); - // here youll need to create a client for a token with the same address - - let df_balance = test.defindex_contract.balance(&users[0]); - assert_eq!(df_balance, 0i128); - - test.defindex_contract.deposit(&sorobanvec![&test.env, amount], &sorobanvec![&test.env, amount], &users[0]); - - let df_balance = test.defindex_contract.balance(&users[0]); - assert_eq!(df_balance, amount); - - test.defindex_contract.withdraw(&df_balance, &users[0]); - - let df_balance = test.defindex_contract.balance(&users[0]); - assert_eq!(df_balance, 0i128); - - let user_balance = test.token0.balance(&users[0]); - assert_eq!(user_balance, amount); -} - -#[test] -fn test_withdraw_from_strategy_success() { - let test = DeFindexVaultTest::setup(); - test.env.mock_all_auths(); - let strategy_params = create_strategy_params(&test); - let assets: Vec = sorobanvec![ - &test.env, - AssetAllocation { - address: test.token0.address.clone(), - strategies: strategy_params.clone() - } - ]; - - test.defindex_contract.initialize( - &assets, - &test.manager, - &test.emergency_manager, - &test.fee_receiver, - &2000u32, - &test.defindex_receiver, - &test.defindex_factory, - &String::from_str(&test.env, "dfToken"), - &String::from_str(&test.env, "DFT"), - ); - let amount = 1000i128; - - let users = DeFindexVaultTest::generate_random_users(&test.env, 1); - - test.token0_admin_client.mint(&users[0], &amount); - let user_balance = test.token0.balance(&users[0]); - assert_eq!(user_balance, amount); - // here youll need to create a client for a token with the same address - - let df_balance = test.defindex_contract.balance(&users[0]); - assert_eq!(df_balance, 0i128); - - test.defindex_contract.deposit(&sorobanvec![&test.env, amount], &sorobanvec![&test.env, amount], &users[0]); - - let df_balance = test.defindex_contract.balance(&users[0]); - assert_eq!(df_balance, amount); - - let investments = sorobanvec![ - &test.env, - Investment { - amount: amount, - strategy: test.strategy_client.address.clone() - }]; - - - test.defindex_contract.invest(&investments); - - let vault_balance = test.token0.balance(&test.defindex_contract.address); - assert_eq!(vault_balance, 0); - - test.defindex_contract.withdraw(&df_balance, &users[0]); - - let df_balance = test.defindex_contract.balance(&users[0]); - assert_eq!(df_balance, 0i128); - - let user_balance = test.token0.balance(&users[0]); - assert_eq!(user_balance, amount); -} \ No newline at end of file diff --git a/apps/contracts/package.json b/apps/contracts/package.json index 329bffed..3bfb5bda 100644 --- a/apps/contracts/package.json +++ b/apps/contracts/package.json @@ -8,7 +8,8 @@ "deploy-hodl": "tsc && node dist/deploy_hodl.js", "publish-addresses": "tsc && node dist/publish_addresses.js", "test": "tsc && node dist/test.js", - "test-vault": "tsc && node dist/tests/testOnlyVault.js" + "test-vault": "tsc && node dist/tests/testOnlyVault.js", + "test-dev": "tsc && node dist/tests/dev.js" }, "type": "module", "devDependencies": { diff --git a/apps/contracts/src/deploy_hodl.ts b/apps/contracts/src/deploy_hodl.ts index a3bda80e..ff71ad4e 100644 --- a/apps/contracts/src/deploy_hodl.ts +++ b/apps/contracts/src/deploy_hodl.ts @@ -47,7 +47,7 @@ export async function deployContracts(addressBook: AddressBook) { const emptyVecScVal = xdr.ScVal.scvVec([]); - console.log("Initializing Soroswap Adapter"); + console.log("Initializing DeFindex HODL Strategy"); await invokeContract( "hodl_strategy", addressBook, diff --git a/apps/contracts/src/test.ts b/apps/contracts/src/test.ts index 5abe8656..73b75e4f 100644 --- a/apps/contracts/src/test.ts +++ b/apps/contracts/src/test.ts @@ -4,13 +4,15 @@ import { nativeToScVal, Networks, scValToNative, - xdr + xdr, + Keypair } from "@stellar/stellar-sdk"; import { randomBytes } from "crypto"; -import { depositToVault } from "./tests/vault.js"; +import { depositToVault, withdrawFromVault } from "./tests/vault.js"; import { AddressBook } from "./utils/address_book.js"; import { airdropAccount, invokeContract } from "./utils/contract.js"; import { config } from "./utils/env_config.js"; +import { checkUserBalance, depositToStrategy, withdrawFromStrategy} from "./tests/strategy.js"; export async function test_factory(addressBook: AddressBook) { @@ -26,6 +28,7 @@ export async function test_factory(addressBook: AddressBook) { console.log("Testing Create DeFindex on Factory"); console.log("-------------------------------------------------------"); + console.log("Setting Emergengy Manager, Fee Receiver and Manager accounts"); const emergencyManager = loadedConfig.getUser("DEFINDEX_EMERGENCY_MANAGER_SECRET_KEY"); if (network !== "mainnet") await airdropAccount(emergencyManager); @@ -108,9 +111,114 @@ const passphrase = network === "mainnet" ? Networks.PUBLIC : network === "testne const loadedConfig = config(network); +// Deposit directly on Strategy + +const myUser = Keypair.random(); +if (network !== "mainnet") await airdropAccount(myUser); + console.log("New user publicKey:", myUser.publicKey()); +const{ + user: strategyUser, + balanceBefore: balanceBeforeStrategyDeposit, + result: strategyDepositResult, + balanceAfter: balanceAfterStrategyDeposit} + = await depositToStrategy(addressBook.getContractId("hodl_strategy"), myUser, 1234567890); + + +console.log(" -- ") +console.log(" -- ") +console.log("Step 0: Deposited directly in Strategy, with balance before:", balanceBeforeStrategyDeposit, "and balance after:", balanceAfterStrategyDeposit); + +console.log(" -- ") +console.log(" -- ") + +// Withdraw directly from strategy +const { + user: strategyWithdrawUser, + balanceBefore: balanceBeforeStrategyWithdraw, + result: strategyWithdrawResult, + balanceAfter: balanceAfterStrategyWithdraw +} = await withdrawFromStrategy(addressBook.getContractId("hodl_strategy"), myUser, 567890); + +console.log(" -- ") +console.log(" -- ") +console.log("Step 1: Withdrawn directly from Strategy, with balance before:", balanceBeforeStrategyWithdraw, "and balance after:", balanceAfterStrategyWithdraw); +console.log(" -- ") +console.log(" -- ") + + +// Step 0: Deploy the vault const deployedVault = await test_factory(addressBook); -await depositToVault(deployedVault); -await depositToVault(deployedVault); +console.log(" -- ") +console.log(" -- ") +console.log("Step 0: Deployed Vault:", deployedVault); +console.log(" -- ") +console.log(" -- ") + + +// Step 1: Deposit to vault and capture initial balances +const { user, balanceBefore: depositBalanceBefore, result: depositResult, balanceAfter: depositBalanceAfter } + = await depositToVault(deployedVault, 987654321); +console.log(" -- ") +console.log(" -- ") +console.log("Step 1: Deposited to Vault using user:", user.publicKey(), "with balance before:", depositBalanceBefore, "and balance after:", depositBalanceAfter); +console.log(" -- ") +console.log(" -- ") + +// Step 2: Check strategy balance after deposit +const strategyBalanceAfterDeposit = await checkUserBalance(addressBook.getContractId("hodl_strategy"), user.publicKey(), user); +console.log(" -- ") +console.log(" -- ") +console.log("Step 2: Strategy balance after deposit:", strategyBalanceAfterDeposit); +console.log(" -- ") +console.log(" -- ") + +// check vault balance of XLM after deposit +let xlmContractId: string; + switch (network) { + case "testnet": + xlmContractId = xlm.contractId(Networks.TESTNET); + break; + case "mainnet": + xlmContractId = xlm.contractId(Networks.PUBLIC); + break; + default: + console.log("Invalid network:", network, "It should be either testnet or mainnet"); + throw new Error("Invalid network"); + } +const xlmAddress = new Address(xlmContractId); +const vaultBalanceAfterDeposit = await checkUserBalance(xlmContractId, deployedVault, user); +console.log(" -- ") +console.log(" -- ") +console.log("Step 2: Vault balance after deposit:", vaultBalanceAfterDeposit); +console.log(" -- ") +console.log(" -- ") + +// Step 3: Withdraw from the vault +const { balanceBefore: withdrawBalanceBefore, result: withdrawResult, balanceAfter: withdrawBalanceAfter } + = await withdrawFromVault(deployedVault, 4321, user); + +// Step 4: Check strategy balance after withdrawal +const strategyBalanceAfterWithdraw = await checkUserBalance(addressBook.getContractId("hodl_strategy"), user.publicKey(), user); + +// Log a table with all balances +console.table([ + { + Operation: "Deposit", + "Balance Before": depositBalanceBefore, + "Deposit Result": depositResult, + "Balance After": depositBalanceAfter, + "Strategy Balance After": strategyBalanceAfterDeposit + }, + { + Operation: "Withdraw", + "Balance Before": withdrawBalanceBefore, + "Withdraw Result": withdrawResult, + "Balance After": withdrawBalanceAfter, + "Strategy Balance After": strategyBalanceAfterWithdraw + } +]); + +await depositToVault(deployedVault, 98765421); // await getDfTokenBalance("CCL54UEU2IGTCMIJOYXELIMVA46CLT3N5OG35XN45APXDZYHYLABF53N", "GDAMXOJUSW6O67UVI6U4LBHI5IIJFUKQVDHPKNFKOIYRLYB2LA6YDAFI", loadedConfig.admin) // await depositToVault("CCIOE3BLPYOYDFB5KALLDXED2CZT3GJDZSHY453U4TTOIRZLAKMKZPLR"); diff --git a/apps/contracts/src/tests/dev.ts b/apps/contracts/src/tests/dev.ts new file mode 100644 index 00000000..9177b236 --- /dev/null +++ b/apps/contracts/src/tests/dev.ts @@ -0,0 +1,15 @@ +import { airdropAccount } from "../utils/contract.js"; +import { withdrawFromVault } from "./vault.js"; +import { Keypair } from "@stellar/stellar-sdk"; + +const user = Keypair.fromSecret("SA77N6PLHDFRYDNYE3YJQBPTRNODMVYP5WWF2SG42DXB52GW2FWOG2B3") +const contract = "CCNWF3D7FJCZKYCAD6FAO3JNPRHG6SVXHO5YTFDZRXSPOJXL6TIBWP3Y" +console.log("πŸš€ ~ file: dev.ts ~ line 6 ~ user", user.publicKey()) +const withdrawResult = + await withdrawFromVault(contract, 10000, user) +// await withdrawFromVault(contract, BigInt(10000), user) + +console.log('πŸš€ ~ withdrawResult:', withdrawResult); +// const badUser = Keypair.random() +// await airdropAccount(badUser); +// await withdrawFromVault(contract, BigInt(1000), badUser) \ No newline at end of file diff --git a/apps/contracts/src/tests/strategy.ts b/apps/contracts/src/tests/strategy.ts new file mode 100644 index 00000000..18c86afa --- /dev/null +++ b/apps/contracts/src/tests/strategy.ts @@ -0,0 +1,145 @@ +import { + Address, + scValToNative, + xdr, + Keypair, + nativeToScVal, +} from "@stellar/stellar-sdk"; +import { invokeCustomContract } from "../utils/contract.js"; + +/** + * Description: Retrieves the balance for a specified user from the contract. + * + * @param {string} contractAddress - The address of the deployed contract. + * @param {string} userPublicKey - The public key of the user whose balance to check. + * @param {Keypair} [source] - Optional; the Keypair instance for authorization if required. + * @returns {Promise} The balance of the specified user in the contract. + * @throws Will throw an error if the balance retrieval fails. + * @example + * const balance = await checkUserBalance("CCE7MLKC7R6TIQA37A7EHWEUC3AIXIH5DSOQUSVAARCWDD7257HS4RUG", "GB6JL..."); + */ + +export async function checkUserBalance(contractAddress: string, userPublicKey: string, source?: Keypair): Promise { + const userAddress = new Address(userPublicKey).toScVal(); + const methodName = "balance"; + + try { + // Call the `balance` method from the contract + const result = await invokeCustomContract( + contractAddress, + methodName, + [userAddress], + source ? source : Keypair.random() + ); + + // Convert the result to a native JavaScript number + const balance = scValToNative(result.returnValue) as number; + console.log(`Balance for user ${userPublicKey}:`, balance); + + return balance; + } catch (error) { + console.error(`Failed to retrieve balance for user ${userPublicKey}:`, error); + throw error; + } +} + + + +export async function depositToStrategy(deployedStrategy: string, user: Keypair, amount: number) { + + let balanceBefore: number; + let balanceAfter: number; + let result: any; + + // Define deposit parameters + const depositAmount = BigInt(amount); + const amountsDesired = [depositAmount]; + const amountsMin = [BigInt(0)]; // Minimum amount for transaction to succeed + + const depositParams: xdr.ScVal[] = [ + nativeToScVal(depositAmount, { type: "i128" }), + (new Address(user.publicKey())).toScVal() + ]; + + try { + // Check the user's strategy + balanceBefore = await checkUserBalance(deployedStrategy, user.publicKey(), user); + console.log("πŸ”’ Β« strategy balance before deposit:", balanceBefore); + } catch (error) { + console.error("❌ Balance check before deposit failed:", error); + throw error; + } + + try { + result = await invokeCustomContract( + deployedStrategy, + "deposit", + depositParams, + user + ); + console.log("πŸš€ Β« Deposit successful:", scValToNative(result.returnValue)); + } catch (error) { + console.error("❌ Deposit failed:", error); + throw error; + } + + try { + // Check the user's balance after the deposit + balanceAfter = await checkUserBalance(deployedStrategy, user.publicKey(), user); + console.log("πŸ”’ Β« dfToken balance after deposit:", balanceAfter); + } catch (error) { + console.error("❌ Balance check after deposit failed:", error); + throw error; + } + + return { user, balanceBefore, result, balanceAfter }; +} + + +export async function withdrawFromStrategy(deployedStrategy: string, user: Keypair, amount: number) { + let balanceBefore: number; + let balanceAfter: number; + let result: any; + + // Define withdraw parameters + const withdrawAmount = BigInt(amount); + const amountsToWithdraw = [withdrawAmount]; + + const withdrawParams: xdr.ScVal[] = [ + nativeToScVal(amount, { type: "i128" }), + (new Address(user.publicKey())).toScVal() + ]; + + try { + // Check the user's balance before the withdrawal + balanceBefore = await checkUserBalance(deployedStrategy, user.publicKey(), user); + console.log("πŸ”’ Β« strategy balance before withdraw:", balanceBefore); + } catch (error) { + console.error("❌ Balance check before withdraw failed:", error); + throw error; + } + + try { + result = await invokeCustomContract( + deployedStrategy, + "withdraw", + withdrawParams, + user + ); + console.log("πŸš€ Β« Withdrawal successful:", scValToNative(result.returnValue)); + } catch (error) { + console.error("❌ Withdrawal failed:", error); + throw error; + } + + try { + // Check the user's balance after the withdrawal + balanceAfter = await checkUserBalance(deployedStrategy, user.publicKey(), user); + console.log("πŸ”’ Β« strategy balance after withdraw:", balanceAfter); + } catch (error) { + console.error("❌ Balance check after withdraw failed:", error); + throw error; + } + + return { user, balanceBefore, result, balanceAfter }; +} \ No newline at end of file diff --git a/apps/contracts/src/tests/testOnlyVault.ts b/apps/contracts/src/tests/testOnlyVault.ts index 97649f0c..430c3c62 100644 --- a/apps/contracts/src/tests/testOnlyVault.ts +++ b/apps/contracts/src/tests/testOnlyVault.ts @@ -1,4 +1,4 @@ import { depositToVault } from "./vault.js"; // modify the address to the deployed vault -await depositToVault("CBUTM5B7CG7ISTX5PD5XMDDMSTKFRGVXNKRMZO3TZW5IFGHN5274JJY7"); +await depositToVault("CBUTM5B7CG7ISTX5PD5XMDDMSTKFRGVXNKRMZO3TZW5IFGHN5274JJY7", 986754321); diff --git a/apps/contracts/src/tests/vault.ts b/apps/contracts/src/tests/vault.ts index 5ef8934e..83104d0d 100644 --- a/apps/contracts/src/tests/vault.ts +++ b/apps/contracts/src/tests/vault.ts @@ -1,15 +1,14 @@ /** - * Description: Tests the vault by creating a new user, airdropping funds, and making a deposit. + * Description: Deposits a specified amount to the vault for a user and returns the user details along with pre- and post-deposit balances. * * @param {string} deployedVault - The address of the deployed vault contract. - * @returns {Promise} Logs the result of the deposit action. + * @param {Keypair} [user] - The user Keypair making the deposit. If not provided, a new user will be created. + * @returns {Promise<{ user: Keypair, balanceBefore: number, result: any, balanceAfter: number }>} Returns an object with the user, balance before, deposit result, and balance after. * @throws Will throw an error if the deposit fails or any step encounters an issue. * @example - * await test_vault("CCE7MLKC7R6TIQA37A7EHWEUC3AIXIH5DSOQUSVAARCWDD7257HS4RUG"); + * const { user, balanceBefore, result, balanceAfter } = await depositToVault("CCE7MLKC7R6TIQA37A7EHWEUC3AIXIH5DSOQUSVAARCWDD7257HS4RUG", user); */ -// ./tests/vault.ts - import { Address, nativeToScVal, @@ -24,18 +23,21 @@ import { config } from "../utils/env_config.js"; const network = process.argv[2]; -export async function depositToVault(deployedVault: string, user?: Keypair) { - // Create and fund a new user account +export async function depositToVault(deployedVault: string, amount: number, user?: Keypair, ) { + // Create and fund a new user account if not provided const newUser = user ? user : Keypair.random(); console.log('πŸš€ ~ depositToVault ~ newUser.publicKey():', newUser.publicKey()); console.log('πŸš€ ~ depositToVault ~ newUser.secret():', newUser.secret()); if (network !== "mainnet") await airdropAccount(newUser); - console.log("New user publicKey:", newUser.publicKey()); + let balanceBefore: number; + let balanceAfter: number; + let result: any; + // Define deposit parameters - const depositAmount = BigInt(10000000); // 1 XLM in stroops (1 XLM = 10^7 stroops) + const depositAmount = BigInt(amount); // 1 XLM in stroops (1 XLM = 10^7 stroops) const amountsDesired = [depositAmount]; const amountsMin = [BigInt(0)]; // Minimum amount for transaction to succeed @@ -44,40 +46,42 @@ export async function depositToVault(deployedVault: string, user?: Keypair) { xdr.ScVal.scvVec(amountsMin.map((min) => nativeToScVal(min, { type: "i128" }))), (new Address(newUser.publicKey())).toScVal() ]; - // console.log('πŸš€ ~ depositToVault ~ depositParams:', depositParams); try { - - // Check the user's balance after the deposit - const balanceBefore = await getDfTokenBalance(deployedVault, newUser.publicKey(), newUser); - console.log("πŸ”’ Β« dfToken balance before deposit:", balanceBefore) + // Check the user's balance before the deposit + balanceBefore = await getDfTokenBalance(deployedVault, newUser.publicKey(), newUser); + console.log("πŸ”’ Β« dfToken balance before deposit:", balanceBefore); } catch (error) { - console.error("❌ Balance failed:", error); + console.error("❌ Balance check before deposit failed:", error); + throw error; } + try { - // TODO: Would this work on Mainnet or Standalone? How does it know which network to use? - const result = await invokeCustomContract( + result = await invokeCustomContract( deployedVault, "deposit", depositParams, newUser ); - console.log("πŸš€ Β« Deposit successful:", scValToNative(result.returnValue)); - } catch (error) { console.error("❌ Deposit failed:", error); + throw error; } - try { + try { // Check the user's balance after the deposit - const balanceAfter = await getDfTokenBalance(deployedVault, newUser.publicKey(), newUser); - console.log("πŸ”’ Β« dfToken balance after deposit:", balanceAfter) + balanceAfter = await getDfTokenBalance(deployedVault, newUser.publicKey(), newUser); + console.log("πŸ”’ Β« dfToken balance after deposit:", balanceAfter); } catch (error) { - console.error("❌ Balance failed:", error); + console.error("❌ Balance check after deposit failed:", error); + throw error; } + + return { user: newUser, balanceBefore, result, balanceAfter }; } + /** * Description: Retrieves the dfToken balance for a specified user from the vault contract. * @@ -109,3 +113,84 @@ export async function getDfTokenBalance(deployedVault: string, userPublicKey: st throw error; } } + +/** + * Description: Withdraws a specified amount from the vault for the user and returns the pre- and post-withdrawal balances. + * + * @param {string} deployedVault - The address of the deployed vault contract. + * @param {BigInt | number} withdrawAmount - The amount in stroops to withdraw (1 XLM = 10^7 stroops). + * @param {Keypair} user - The user Keypair requesting the withdrawal. + * @returns {Promise<{ balanceBefore: number, result: any, balanceAfter: number }>} Returns an object with balance before, the withdrawal result, and balance after. + * @throws Will throw an error if the withdrawal fails or any step encounters an issue. + * @example + * const { balanceBefore, result, balanceAfter } = await withdrawFromVault("CCE7MLKC7R6TIQA37A7EHWEUC3AIXIH5DSOQUSVAARCWDD7257HS4RUG", 10000000, user); + */ + +export async function withdrawFromVault(deployedVault: string, withdrawAmount: number, user: Keypair) { + console.log('πŸš€ ~ withdrawFromVault ~ User publicKey:', user.publicKey()); + + let balanceBefore: number; + let balanceAfter: number; + let result: any; + + try { + // Check the user's balance before the withdrawal + balanceBefore = await getDfTokenBalance(deployedVault, user.publicKey(), user); + console.log("πŸ”’ Β« dfToken balance before withdraw:", balanceBefore); + } catch (error) { + console.error("❌ Balance check before withdraw failed:", error); + throw error; + } + + // Define withdraw parameters + // const amountsToWithdraw = [BigInt(withdrawAmount)]; + // const withdrawParams: xdr.ScVal[] = [ + // xdr.ScVal.scvVec(amountsToWithdraw.map((amount) => nativeToScVal(amount, { type: "i128" }))), + // (new Address(user.publicKey())).toScVal() + // ]; + + const withdrawParams: xdr.ScVal[] = [ + nativeToScVal(BigInt(withdrawAmount), { type: "i128" }), + (new Address(user.publicKey())).toScVal() + ]; + + try { + result = await invokeCustomContract( + deployedVault, + "withdraw", + withdrawParams, + user + ); + console.log("πŸš€ Β« Withdrawal successful:", scValToNative(result.returnValue)); + } catch (error) { + console.error("❌ Withdrawal failed:", error); + throw error; + } + + try { + // Check the user's balance after the withdrawal + balanceAfter = await getDfTokenBalance(deployedVault, user.publicKey(), user); + console.log("πŸ”’ Β« dfToken balance after withdraw:", balanceAfter); + } catch (error) { + console.error("❌ Balance check after withdraw failed:", error); + throw error; + } + + return { balanceBefore, result, balanceAfter }; +} + +/** + * Retrieves the current idle funds of the vault. + * + * @param {string} deployedVault - The address of the deployed vault contract. + * @returns {Promise>} A promise that resolves with a map of asset addresses to idle amounts. + */ +export async function fetchCurrentIdleFunds(deployedVault: string, user: Keypair): Promise> { + try { + const result = await invokeCustomContract(deployedVault, "fetch_current_idle_funds", [], user); + return result.map(scValToNative); // Convert result to native format if needed + } catch (error) { + console.error("❌ Failed to fetch current idle funds:", error); + throw error; + } +} diff --git a/apps/contracts/src/utils/env_config.ts b/apps/contracts/src/utils/env_config.ts index 96996bf6..89083d03 100644 --- a/apps/contracts/src/utils/env_config.ts +++ b/apps/contracts/src/utils/env_config.ts @@ -81,10 +81,11 @@ class EnvConfig { horizon_rpc_url === undefined || (network != "mainnet" && friendbot_url === undefined) || passphrase === undefined || - admin === undefined + admin === undefined || + admin === "" ) { throw new Error( - "Error: Configuration is missing required fields, include " + "Error: Configuration is missing required fields. Please check your .env file." ); } diff --git a/apps/contracts/strategies/core/src/error.rs b/apps/contracts/strategies/core/src/error.rs index bb0f8ddc..309cccba 100644 --- a/apps/contracts/strategies/core/src/error.rs +++ b/apps/contracts/strategies/core/src/error.rs @@ -11,6 +11,7 @@ pub enum StrategyError { // Validation Errors NegativeNotAllowed = 410, InvalidArgument = 411, + InsufficientBalance = 412, // Protocol Errors ProtocolAddressNotFound = 420, diff --git a/apps/contracts/strategies/hodl/src/balance.rs b/apps/contracts/strategies/hodl/src/balance.rs index 88aa258b..1f5751be 100644 --- a/apps/contracts/strategies/hodl/src/balance.rs +++ b/apps/contracts/strategies/hodl/src/balance.rs @@ -1,6 +1,7 @@ use soroban_sdk::{Address, Env}; use crate::storage::{DataKey, INSTANCE_BUMP_AMOUNT, INSTANCE_LIFETIME_THRESHOLD}; +use crate::StrategyError; pub fn read_balance(e: &Env, addr: Address) -> i128 { let key = DataKey::Balance(addr); @@ -31,10 +32,12 @@ pub fn receive_balance(e: &Env, addr: Address, amount: i128) { write_balance(e, addr, new_balance); } -pub fn spend_balance(e: &Env, addr: Address, amount: i128) { +pub fn spend_balance(e: &Env, addr: Address, amount: i128) -> Result<(), StrategyError> { + let balance = read_balance(e, addr.clone()); if balance < amount { - panic!("insufficient balance"); + return Err(StrategyError::InsufficientBalance); } write_balance(e, addr, balance - amount); + Ok(()) } diff --git a/apps/contracts/strategies/hodl/src/lib.rs b/apps/contracts/strategies/hodl/src/lib.rs index a6a68bdf..648947a6 100644 --- a/apps/contracts/strategies/hodl/src/lib.rs +++ b/apps/contracts/strategies/hodl/src/lib.rs @@ -1,16 +1,34 @@ #![no_std] -use balance::{read_balance, receive_balance, spend_balance}; use soroban_sdk::{ - contract, contractimpl, Address, Env, String, Val, Vec}; -use soroban_sdk::token::Client as TokenClient; + contract, + contractimpl, + Address, + Env, + String, + token::Client as TokenClient, + Val, + Vec}; mod balance; mod storage; +use balance::{ + read_balance, + receive_balance, + spend_balance}; + use storage::{ - extend_instance_ttl, get_underlying_asset, is_initialized, set_initialized, set_underlying_asset + extend_instance_ttl, + get_underlying_asset, + is_initialized, + set_initialized, + set_underlying_asset }; -use defindex_strategy_core::{DeFindexStrategyTrait, StrategyError, event}; + +pub use defindex_strategy_core::{ + DeFindexStrategyTrait, + StrategyError, + event}; pub fn check_nonnegative_amount(amount: i128) -> Result<(), StrategyError> { if amount < 0 { @@ -98,13 +116,11 @@ impl DeFindexStrategyTrait for HodlStrategy { check_nonnegative_amount(amount)?; extend_instance_ttl(&e); + spend_balance(&e, from.clone(), amount)?; + let contract_address = e.current_contract_address(); - let underlying_asset = get_underlying_asset(&e); TokenClient::new(&e, &underlying_asset).transfer(&contract_address, &from, &amount); - - spend_balance(&e, from.clone(), amount); - event::emit_withdraw(&e, String::from_str(&e, STARETEGY_NAME), amount, from); Ok(amount) @@ -114,7 +130,6 @@ impl DeFindexStrategyTrait for HodlStrategy { e: Env, from: Address, ) -> Result { - from.require_auth(); check_initialized(&e)?; extend_instance_ttl(&e); diff --git a/apps/contracts/strategies/hodl/src/test.rs b/apps/contracts/strategies/hodl/src/test.rs index 3b4ee3cc..f8944046 100644 --- a/apps/contracts/strategies/hodl/src/test.rs +++ b/apps/contracts/strategies/hodl/src/test.rs @@ -1,17 +1,13 @@ #![cfg(test)] -extern crate std; -use crate::{HodlStrategy, HodlStrategyClient}; -use soroban_sdk::token::{ - StellarAssetClient as SorobanTokenAdminClient, TokenClient as SorobanTokenClient, -}; -use soroban_sdk::{IntoVal, Val}; +use crate::{HodlStrategy, HodlStrategyClient, StrategyError}; + +use soroban_sdk::token::{TokenClient, StellarAssetClient}; + use soroban_sdk::{ Env, Address, testutils::Address as _, - Vec, }; -use std::vec; // Base Strategy Contract fn create_hodl_strategy<'a>(e: &Env) -> HodlStrategyClient<'a> { @@ -19,23 +15,15 @@ fn create_hodl_strategy<'a>(e: &Env) -> HodlStrategyClient<'a> { } // Create Test Token -pub(crate) fn create_token_contract<'a>(e: &Env, admin: &Address) -> SorobanTokenClient<'a> { - SorobanTokenClient::new(e, &e.register_stellar_asset_contract(admin.clone())) -} - -pub(crate) fn get_token_admin_client<'a>( - e: &Env, - address: &Address, -) -> SorobanTokenAdminClient<'a> { - SorobanTokenAdminClient::new(e, address) +pub(crate) fn create_token_contract<'a>(e: &Env, admin: &Address) -> TokenClient<'a> { + TokenClient::new(e, &e.register_stellar_asset_contract_v2(admin.clone()).address()) } - pub struct HodlStrategyTest<'a> { env: Env, strategy: HodlStrategyClient<'a>, - token0_admin_client: SorobanTokenAdminClient<'a>, - token0: SorobanTokenClient<'a>, + token: TokenClient<'a>, + user: Address, } impl<'a> HodlStrategyTest<'a> { @@ -43,72 +31,33 @@ impl<'a> HodlStrategyTest<'a> { let env = Env::default(); env.mock_all_auths(); + let strategy = create_hodl_strategy(&env); - - let token0_admin = Address::generate(&env); - let token0 = create_token_contract(&env, &token0_admin); - let token0_admin_client = get_token_admin_client(&env, &token0.address.clone()); + let admin = Address::generate(&env); + let token = create_token_contract(&env, &admin); + let user = Address::generate(&env); - let init_fn_args: Vec = (0,).into_val(&env); - strategy.initialize(&token0.address, &init_fn_args); + // Mint 1,000,000,000 to user + StellarAssetClient::new(&env, &token.address).mint(&user, &1_000_000_000); HodlStrategyTest { env, strategy, - token0_admin_client, - token0, + token, + user } } - pub(crate) fn generate_random_users(e: &Env, users_count: u32) -> vec::Vec
{ - let mut users = vec![]; - for _c in 0..users_count { - users.push(Address::generate(e)); - } - users - } + // pub(crate) fn generate_random_users(e: &Env, users_count: u32) -> vec::Vec
{ + // let mut users = vec![]; + // for _c in 0..users_count { + // users.push(Address::generate(e)); + // } + // users + // } } -#[test] -fn test_deposit_and_withdrawal_flow() { - let test = HodlStrategyTest::setup(); - let users = HodlStrategyTest::generate_random_users(&test.env, 1); - - let amount: i128 = 10_000_000; - // Minting token 0 to the user - test.token0_admin_client.mint(&users[0], &amount); - - // Reading user 0 balance - let balance = test.token0.balance(&users[0]); - assert_eq!(balance, amount); - - // Depositing token 0 to the strategy from user - test.strategy.deposit(&amount, &users[0]); - - // Reading user 0 balance - let balance = test.token0.balance(&users[0]); - assert_eq!(balance, 0); - - // Reading strategy balance - let balance = test.token0.balance(&test.strategy.address); - assert_eq!(balance, amount); - - // Reading user balance on strategy contract - let user_balance = test.strategy.balance(&users[0]); - assert_eq!(user_balance, amount); - - // Withdrawing token 0 from the strategy to user - test.strategy.withdraw(&amount, &users[0]); - - // Reading user 0 balance - let balance = test.token0.balance(&users[0]); - assert_eq!(balance, amount); - - // Reading strategy balance - let balance = test.token0.balance(&test.strategy.address); - assert_eq!(balance, 0); - - // Reading user balance on strategy contract - let user_balance = test.strategy.balance(&users[0]); - assert_eq!(user_balance, 0); -} \ No newline at end of file +mod initialize; +mod deposit; +mod events; +mod withdraw; \ No newline at end of file diff --git a/apps/contracts/strategies/hodl/src/test/deposit.rs b/apps/contracts/strategies/hodl/src/test/deposit.rs new file mode 100644 index 00000000..3267f974 --- /dev/null +++ b/apps/contracts/strategies/hodl/src/test/deposit.rs @@ -0,0 +1,79 @@ +use crate::test::HodlStrategyTest; +use crate::test::StrategyError; +use soroban_sdk::{IntoVal, Vec, Val}; + +// test deposit with negative amount +#[test] +fn deposit_with_negative_amount() { + let test = HodlStrategyTest::setup(); + let init_fn_args: Vec = (0,).into_val(&test.env); + test.strategy.initialize(&test.token.address, &init_fn_args); + + let amount = -123456; + + let result = test.strategy.try_deposit(&amount, &test.user); + assert_eq!(result, Err(Ok(StrategyError::NegativeNotAllowed))); +} + +// check auth +#[test] +fn deposit_mock_auths() { + todo!() +} + +#[test] +fn deposit_and_withdrawal_flow() { + let test = HodlStrategyTest::setup(); + // let users = HodlStrategyTest::generate_random_users(&test.env, 1); + + // try deposit should return NotInitialized error before being initialize + + let result = test.strategy.try_deposit(&10_000_000, &test.user); + assert_eq!(result, Err(Ok(StrategyError::NotInitialized))); + + // initialize + let init_fn_args: Vec = (0,).into_val(&test.env); + test.strategy.initialize(&test.token.address, &init_fn_args); + + // Initial user token balance + let balance = test.token.balance(&test.user); + + let amount = 123456; + + // Deposit amount of token from the user to the strategy + test.strategy.deposit(&amount, &test.user); + + let balance_after_deposit = test.token.balance(&test.user); + assert_eq!(balance_after_deposit, balance - amount); + + // Reading strategy balance + let strategy_balance_after_deposit = test.token.balance(&test.strategy.address); + assert_eq!(strategy_balance_after_deposit, amount); + + // Reading user balance on strategy contract + let user_balance_on_strategy = test.strategy.balance(&test.user); + assert_eq!(user_balance_on_strategy, amount); + + + let amount_to_withdraw = 100_000; + // Withdrawing token from the strategy to user + test.strategy.withdraw(&amount_to_withdraw, &test.user); + + // Reading user balance in token + let balance = test.token.balance(&test.user); + assert_eq!(balance, balance_after_deposit + amount_to_withdraw); + + // Reading strategy balance in token + let balance = test.token.balance(&test.strategy.address); + assert_eq!(balance, amount - amount_to_withdraw); + + // Reading user balance on strategy contract + let user_balance = test.strategy.balance(&test.user); + assert_eq!(user_balance, amount - amount_to_withdraw); + + // now we will want to withdraw more of the remaining balance + let amount_to_withdraw = 200_000; + let result = test.strategy.try_withdraw(&amount_to_withdraw, &test.user); + assert_eq!(result, Err(Ok(StrategyError::InsufficientBalance))); + +} \ No newline at end of file diff --git a/apps/contracts/strategies/hodl/src/test/events.rs b/apps/contracts/strategies/hodl/src/test/events.rs new file mode 100644 index 00000000..239a9bd1 --- /dev/null +++ b/apps/contracts/strategies/hodl/src/test/events.rs @@ -0,0 +1,6 @@ +// TODO: Write tests for events + +#[test] +fn test_events() { + todo!() +} \ No newline at end of file diff --git a/apps/contracts/strategies/hodl/src/test/initialize.rs b/apps/contracts/strategies/hodl/src/test/initialize.rs new file mode 100644 index 00000000..41037473 --- /dev/null +++ b/apps/contracts/strategies/hodl/src/test/initialize.rs @@ -0,0 +1,21 @@ +// Cannot Initialize twice +extern crate std; +use soroban_sdk::{IntoVal, Vec, Val}; +use crate::test::HodlStrategyTest; +use crate::test::StrategyError; + +#[test] +fn cannot_initialize_twice() { + let test = HodlStrategyTest::setup(); + + let init_fn_args: Vec = (0,).into_val(&test.env); + + test.strategy.initialize(&test.token.address, &init_fn_args); + let result = test.strategy.try_initialize(&test.token.address , &init_fn_args); + assert_eq!(result, Err(Ok(StrategyError::AlreadyInitialized))); + + // get asset should return underlying asset + + let underlying_asset = test.strategy.asset(); + assert_eq!(underlying_asset, test.token.address); +} \ No newline at end of file diff --git a/apps/contracts/strategies/hodl/src/test/withdraw.rs b/apps/contracts/strategies/hodl/src/test/withdraw.rs new file mode 100644 index 00000000..dd8aa9d5 --- /dev/null +++ b/apps/contracts/strategies/hodl/src/test/withdraw.rs @@ -0,0 +1,5 @@ + +#[test] +fn withdraw() { + todo!() +} \ No newline at end of file diff --git a/apps/contracts/defindex/Cargo.toml b/apps/contracts/vault/Cargo.toml similarity index 100% rename from apps/contracts/defindex/Cargo.toml rename to apps/contracts/vault/Cargo.toml diff --git a/apps/contracts/defindex/Makefile b/apps/contracts/vault/Makefile similarity index 100% rename from apps/contracts/defindex/Makefile rename to apps/contracts/vault/Makefile diff --git a/apps/contracts/defindex/src/access.rs b/apps/contracts/vault/src/access.rs similarity index 88% rename from apps/contracts/defindex/src/access.rs rename to apps/contracts/vault/src/access.rs index c601242a..d4f00a96 100644 --- a/apps/contracts/defindex/src/access.rs +++ b/apps/contracts/vault/src/access.rs @@ -6,7 +6,7 @@ use soroban_sdk::{contracttype, panic_with_error, Address, Env}; #[contracttype] pub enum RolesDataKey { EmergencyManager, // Role: Emergency Manager - FeeReceiver, // Role: Fee Receiver + VaultFeeReceiver, // Role: Fee Receiver Manager, // Role: Manager } @@ -82,13 +82,16 @@ impl AccessControlTrait for AccessControl { // Role-specific setters and getters impl AccessControl { - pub fn set_fee_receiver(&self, caller: &Address, fee_receiver: &Address) { - self.require_any_role(&[RolesDataKey::Manager, RolesDataKey::FeeReceiver], caller); - self.set_role(&RolesDataKey::FeeReceiver, fee_receiver); + pub fn set_fee_receiver(&self, caller: &Address, vault_fee_receiver: &Address) { + self.require_any_role( + &[RolesDataKey::Manager, RolesDataKey::VaultFeeReceiver], + caller, + ); + self.set_role(&RolesDataKey::VaultFeeReceiver, vault_fee_receiver); } pub fn get_fee_receiver(&self) -> Result { - self.check_role(&RolesDataKey::FeeReceiver) + self.check_role(&RolesDataKey::VaultFeeReceiver) } pub fn set_manager(&self, manager: &Address) { diff --git a/apps/contracts/defindex/src/aggregator.rs b/apps/contracts/vault/src/aggregator.rs similarity index 65% rename from apps/contracts/defindex/src/aggregator.rs rename to apps/contracts/vault/src/aggregator.rs index 70f5050c..c10e34db 100644 --- a/apps/contracts/defindex/src/aggregator.rs +++ b/apps/contracts/vault/src/aggregator.rs @@ -1,14 +1,18 @@ use soroban_sdk::{vec, Address, Env, IntoVal, Symbol, Val, Vec}; -use crate::{models::DexDistribution, storage::{get_assets, get_factory}, ContractError}; +use crate::{ + models::DexDistribution, + storage::{get_assets, get_factory}, + ContractError, +}; fn fetch_aggregator_address(e: &Env) -> Address { let factory_address = get_factory(e); e.invoke_contract( - &factory_address, - &Symbol::new(&e, "aggregator"), - Vec::new(&e) + &factory_address, + &Symbol::new(&e, "aggregator"), + Vec::new(&e), ) } @@ -16,8 +20,16 @@ fn is_supported_asset(e: &Env, token: &Address) -> bool { let assets = get_assets(e); assets.iter().any(|asset| &asset.address == token) } - -pub fn internal_swap_exact_tokens_for_tokens(e: &Env, token_in: &Address, token_out: &Address, amount_in: &i128, amount_out_min: &i128, distribution: &Vec, deadline: &u64) -> Result<(), ContractError> { + +pub fn internal_swap_exact_tokens_for_tokens( + e: &Env, + token_in: &Address, + token_out: &Address, + amount_in: &i128, + amount_out_min: &i128, + distribution: &Vec, + deadline: &u64, +) -> Result<(), ContractError> { let aggregator_address = fetch_aggregator_address(e); // Check if both tokens are supported by the vault @@ -35,13 +47,21 @@ pub fn internal_swap_exact_tokens_for_tokens(e: &Env, token_in: &Address, token_ init_args.push_back(deadline.into_val(e)); e.invoke_contract( - &aggregator_address, - &Symbol::new(&e, "swap_exact_tokens_for_tokens"), - Vec::new(&e) + &aggregator_address, + &Symbol::new(&e, "swap_exact_tokens_for_tokens"), + Vec::new(&e), ) } -pub fn internal_swap_tokens_for_exact_tokens(e: &Env, token_in: &Address, token_out: &Address, amount_out: &i128, amount_in_max: &i128, distribution: &Vec, deadline: &u64) -> Result<(), ContractError> { +pub fn internal_swap_tokens_for_exact_tokens( + e: &Env, + token_in: &Address, + token_out: &Address, + amount_out: &i128, + amount_in_max: &i128, + distribution: &Vec, + deadline: &u64, +) -> Result<(), ContractError> { let aggregator_address = fetch_aggregator_address(e); // Check if both tokens are supported by the vault @@ -59,8 +79,8 @@ pub fn internal_swap_tokens_for_exact_tokens(e: &Env, token_in: &Address, token_ init_args.push_back(deadline.into_val(e)); e.invoke_contract( - &aggregator_address, - &Symbol::new(&e, "swap_tokens_for_exact_tokens"), - Vec::new(&e) + &aggregator_address, + &Symbol::new(&e, "swap_tokens_for_exact_tokens"), + Vec::new(&e), ) -} \ No newline at end of file +} diff --git a/apps/contracts/vault/src/constants.rs b/apps/contracts/vault/src/constants.rs new file mode 100644 index 00000000..1428e1b8 --- /dev/null +++ b/apps/contracts/vault/src/constants.rs @@ -0,0 +1,2 @@ +pub(crate) const MAX_BPS: i128 = 10_000; +pub(crate) const SECONDS_PER_YEAR: i128 = 31_536_000; diff --git a/apps/contracts/defindex/src/error.rs b/apps/contracts/vault/src/error.rs similarity index 92% rename from apps/contracts/defindex/src/error.rs rename to apps/contracts/vault/src/error.rs index 24b4b6f0..66874708 100644 --- a/apps/contracts/defindex/src/error.rs +++ b/apps/contracts/vault/src/error.rs @@ -8,12 +8,12 @@ pub enum ContractError { NotInitialized = 100, AlreadyInitialized = 101, InvalidRatio = 102, - StrategyDoesNotSupportAsset=103, + StrategyDoesNotSupportAsset = 103, // Validation Errors (11x) NegativeNotAllowed = 110, InsufficientBalance = 111, - WrongAmuntsLength = 112, + WrongAmountsLength = 112, NotEnoughIdleFunds = 113, InsufficientManagedFunds = 114, MissingInstructionData = 115, diff --git a/apps/contracts/defindex/src/events.rs b/apps/contracts/vault/src/events.rs similarity index 91% rename from apps/contracts/defindex/src/events.rs rename to apps/contracts/vault/src/events.rs index deca5b5d..c7d46eb7 100644 --- a/apps/contracts/defindex/src/events.rs +++ b/apps/contracts/vault/src/events.rs @@ -1,15 +1,15 @@ //! Definition of the Events used in the DeFindex Vault contract -use soroban_sdk::{contracttype, symbol_short, Address, Env, Vec}; use crate::models::AssetAllocation; +use soroban_sdk::{contracttype, symbol_short, Address, Env, Vec}; // INITIALIZED VAULT EVENT #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct InitializedVaultEvent { pub emergency_manager: Address, - pub fee_receiver: Address, + pub vault_fee_receiver: Address, pub manager: Address, - pub defindex_receiver: Address, + pub defindex_protocol_receiver: Address, pub assets: Vec, } @@ -17,16 +17,16 @@ pub struct InitializedVaultEvent { pub(crate) fn emit_initialized_vault( e: &Env, emergency_manager: Address, - fee_receiver: Address, + vault_fee_receiver: Address, manager: Address, - defindex_receiver: Address, + defindex_protocol_receiver: Address, assets: Vec, ) { let event = InitializedVaultEvent { emergency_manager, - fee_receiver, + vault_fee_receiver, manager, - defindex_receiver, + defindex_protocol_receiver, assets, }; @@ -178,9 +178,7 @@ pub struct ManagerChangedEvent { /// Publishes a `ManagerChangedEvent` to the event stream. pub(crate) fn emit_manager_changed_event(e: &Env, new_manager: Address) { - let event = ManagerChangedEvent { - new_manager, - }; + let event = ManagerChangedEvent { new_manager }; e.events() .publish(("DeFindexVault", symbol_short!("nmanager")), event); @@ -194,10 +192,7 @@ pub struct EmergencyManagerChangedEvent { } /// Publishes an `EmergencyManagerChangedEvent` to the event stream. -pub(crate) fn emit_emergency_manager_changed_event( - e: &Env, - new_emergency_manager: Address, -) { +pub(crate) fn emit_emergency_manager_changed_event(e: &Env, new_emergency_manager: Address) { let event = EmergencyManagerChangedEvent { new_emergency_manager, }; @@ -210,7 +205,7 @@ pub(crate) fn emit_emergency_manager_changed_event( #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct FeesMintedEvent { - pub defindex_receiver: Address, + pub defindex_protocol_receiver: Address, pub defindex_shares: i128, pub vault_receiver: Address, pub vault_shares: i128, @@ -219,13 +214,13 @@ pub struct FeesMintedEvent { /// Publishes an `EmergencyManagerChangedEvent` to the event stream. pub(crate) fn emit_fees_minted_event( e: &Env, - defindex_receiver: Address, + defindex_protocol_receiver: Address, defindex_shares: i128, vault_receiver: Address, vault_shares: i128, ) { let event = FeesMintedEvent { - defindex_receiver, + defindex_protocol_receiver, defindex_shares, vault_receiver, vault_shares, @@ -233,4 +228,4 @@ pub(crate) fn emit_fees_minted_event( e.events() .publish(("DeFindexVault", symbol_short!("mfees")), event); -} \ No newline at end of file +} diff --git a/apps/contracts/defindex/src/fee.rs b/apps/contracts/vault/src/fee.rs similarity index 74% rename from apps/contracts/defindex/src/fee.rs rename to apps/contracts/vault/src/fee.rs index 6fe55464..3a472e71 100644 --- a/apps/contracts/defindex/src/fee.rs +++ b/apps/contracts/vault/src/fee.rs @@ -1,24 +1,35 @@ use soroban_sdk::{Address, Env, Map, Symbol, Vec}; -use crate::{access::AccessControl, constants::{MAX_BPS, SECONDS_PER_YEAR}, events, funds::fetch_total_managed_funds, storage::{get_defindex_receiver, get_factory, get_last_fee_assesment, get_vault_fee, set_last_fee_assesment}, token::internal_mint, utils::calculate_dftokens_from_asset_amounts, ContractError}; +use crate::{ + access::AccessControl, + constants::{MAX_BPS, SECONDS_PER_YEAR}, + events, + funds::fetch_total_managed_funds, + storage::{ + get_defindex_protocol_fee_receiver, get_factory, get_last_fee_assesment, get_vault_fee, + set_last_fee_assesment, + }, + token::internal_mint, + utils::calculate_dftokens_from_asset_amounts, + ContractError, +}; /// Fetches the current fee rate from the factory contract. /// The fee rate is expressed in basis points (BPS). fn fetch_defindex_fee(e: &Env) -> u32 { - let factory_address = get_factory(e); - // Interacts with the factory contract to get the fee rate. - e.invoke_contract( - &factory_address, - &Symbol::new(&e, "defindex_fee"), - Vec::new(&e) - ) + let factory_address = get_factory(e); + // Interacts with the factory contract to get the fee rate. + e.invoke_contract( + &factory_address, + &Symbol::new(&e, "defindex_fee"), + Vec::new(&e) + ) } /// Calculates the required fees in dfTokens based on the current APR fee rate. fn calculate_fees(e: &Env, time_elapsed: u64, fee_rate: u32) -> Result { - let total_managed_funds = fetch_total_managed_funds(e); // Get total managed funds per asset - + let seconds_per_year = SECONDS_PER_YEAR; // 365 days in seconds let mut total_fees_per_asset: Map = Map::new(&e); @@ -29,10 +40,10 @@ fn calculate_fees(e: &Env, time_elapsed: u64, fee_rate: u32) -> Result Re let access_control = AccessControl::new(&e); let vault_fee_receiver = access_control.get_fee_receiver()?; - let defindex_receiver = get_defindex_receiver(e); + let defindex_protocol_receiver = get_defindex_protocol_fee_receiver(e); // Calculate shares for each receiver based on their fee proportion let total_fee_bps = defindex_fee as i128 + vault_fee as i128; @@ -83,8 +94,18 @@ fn mint_fees(e: &Env, total_fees: i128, defindex_fee: u32, vault_fee: u32) -> Re // Mint shares for both receivers internal_mint(e.clone(), vault_fee_receiver.clone(), vault_shares); - internal_mint(e.clone(), defindex_receiver.clone(), defindex_shares); - - events::emit_fees_minted_event(e, defindex_receiver, defindex_shares, vault_fee_receiver, vault_shares); + internal_mint( + e.clone(), + defindex_protocol_receiver.clone(), + defindex_shares, + ); + + events::emit_fees_minted_event( + e, + defindex_protocol_receiver, + defindex_shares, + vault_fee_receiver, + vault_shares, + ); Ok(()) -} \ No newline at end of file +} diff --git a/apps/contracts/defindex/src/funds.rs b/apps/contracts/vault/src/funds.rs similarity index 96% rename from apps/contracts/defindex/src/funds.rs rename to apps/contracts/vault/src/funds.rs index 830ffa13..f02d879b 100644 --- a/apps/contracts/defindex/src/funds.rs +++ b/apps/contracts/vault/src/funds.rs @@ -5,28 +5,28 @@ use crate::models::AssetAllocation; use crate::storage::get_assets; use crate::strategies::get_strategy_client; -// Funds for AssetAllocation +// Funds for AssetAllocation /// Fetches the idle funds for a given asset. Idle funds refer to the balance of the asset /// that is currently not invested in any strategies. -/// +/// /// # Arguments /// * `e` - The current environment instance. /// * `asset` - The asset for which idle funds are being fetched. -/// +/// /// # Returns /// * The idle balance (i128) of the asset in the current contract address. fn fetch_idle_funds_for_asset(e: &Env, asset: &AssetAllocation) -> i128 { TokenClient::new(e, &asset.address).balance(&e.current_contract_address()) } -/// Fetches the total funds that are invested for a given asset. +/// Fetches the total funds that are invested for a given asset. /// It iterates through all the strategies associated with the asset and sums their balances. -/// +/// /// # Arguments /// * `e` - The current environment instance. /// * `asset` - The asset for which invested funds are being fetched. -/// +/// /// # Returns /// * The total invested balance (i128) of the asset across all strategies. pub fn fetch_invested_funds_for_strategy(e: &Env, strategy_address: &Address) -> i128 { @@ -34,13 +34,13 @@ pub fn fetch_invested_funds_for_strategy(e: &Env, strategy_address: &Address) -> strategy_client.balance(&e.current_contract_address()) } -/// Fetches the total funds that are invested for a given asset. +/// Fetches the total funds that are invested for a given asset. /// It iterates through all the strategies associated with the asset and sums their balances. -/// +/// /// # Arguments /// * `e` - The current environment instance. /// * `asset` - The asset for which invested funds are being fetched. -/// +/// /// # Returns /// * The total invested balance (i128) of the asset across all strategies. pub fn fetch_invested_funds_for_asset(e: &Env, asset: &AssetAllocation) -> i128 { @@ -51,15 +51,14 @@ pub fn fetch_invested_funds_for_asset(e: &Env, asset: &AssetAllocation) -> i128 invested_funds } - // Pub functions -/// Fetches the current idle funds for all assets managed by the contract. +/// Fetches the current idle funds for all assets managed by the contract. /// It returns a map where the key is the asset's address and the value is the idle balance. -/// +/// /// # Arguments /// * `e` - The current environment instance. -/// +/// /// # Returns /// * A map where each entry represents an asset's address and its corresponding idle balance. pub fn fetch_current_idle_funds(e: &Env) -> Map { @@ -71,12 +70,12 @@ pub fn fetch_current_idle_funds(e: &Env) -> Map { map } -/// Fetches the current invested funds for all assets managed by the contract. +/// Fetches the current invested funds for all assets managed by the contract. /// It returns a map where the key is the asset's address and the value is the invested balance. -/// +/// /// # Arguments /// * `e` - The current environment instance. -/// +/// /// # Returns /// * A map where each entry represents an asset's address and its corresponding invested balance. pub fn fetch_current_invested_funds(e: &Env) -> Map { @@ -92,12 +91,12 @@ pub fn fetch_current_invested_funds(e: &Env) -> Map { } /// Fetches the total managed funds for all assets. This includes both idle and invested funds. -/// It returns a map where the key is the asset's address and the value is the total managed balance +/// It returns a map where the key is the asset's address and the value is the total managed balance /// (idle + invested). With this map we can calculate the current managed funds ratio. -/// +/// /// # Arguments /// * `e` - The current environment instance. -/// +/// /// # Returns /// * A map where each entry represents an asset's address and its total managed balance. pub fn fetch_total_managed_funds(e: &Env) -> Map { diff --git a/apps/contracts/defindex/src/interface.rs b/apps/contracts/vault/src/interface.rs similarity index 92% rename from apps/contracts/defindex/src/interface.rs rename to apps/contracts/vault/src/interface.rs index 65538e17..28bc7104 100644 --- a/apps/contracts/defindex/src/interface.rs +++ b/apps/contracts/vault/src/interface.rs @@ -1,25 +1,24 @@ use soroban_sdk::{Address, Env, Map, String, Vec}; use crate::{ - models::{AssetAllocation, DexDistribution, Instruction, Investment}, + models::{AssetAllocation, Instruction, Investment}, ContractError, }; pub trait VaultTrait { - /// Initializes the DeFindex Vault contract with the required parameters. /// /// This function sets the roles for emergency manager, fee receiver, and manager. /// It also stores the list of assets to be managed by the vault, including strategies for each asset. - /// + /// /// # Arguments: /// * `e` - The environment. /// * `assets` - A vector of `AssetAllocation` structs representing the assets and their associated strategies. /// * `manager` - The address responsible for managing the vault. /// * `emergency_manager` - The address with emergency control over the vault. - /// * `fee_receiver` - The address that will receive fees from the vault. + /// * `vault_fee_receiver` - The address that will receive fees from the vault. /// * `vault_fee` - The percentage of the vault's fees that will be sent to the DeFindex receiver. in BPS. - /// * `defindex_receiver` - The address that will receive fees for DeFindex from the vault. + /// * `defindex_protocol_receiver` - The address that will receive fees for DeFindex from the vault. /// * `factory` - The address of the factory that deployed the vault. /// /// # Returns: @@ -29,9 +28,9 @@ pub trait VaultTrait { assets: Vec, manager: Address, emergency_manager: Address, - fee_receiver: Address, + vault_fee_receiver: Address, vault_fee: u32, - defindex_receiver: Address, + defindex_protocol_receiver: Address, factory: Address, vault_name: String, vault_symbol: String, @@ -86,7 +85,11 @@ pub trait VaultTrait { /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. - fn emergency_withdraw(e: Env, strategy_address: Address, caller: Address) -> Result<(), ContractError>; + fn emergency_withdraw( + e: Env, + strategy_address: Address, + caller: Address, + ) -> Result<(), ContractError>; /// Pauses a strategy to prevent it from being used in the vault. /// @@ -100,7 +103,11 @@ pub trait VaultTrait { /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. - fn pause_strategy(e: Env, strategy_address: Address, caller: Address) -> Result<(), ContractError>; + fn pause_strategy( + e: Env, + strategy_address: Address, + caller: Address, + ) -> Result<(), ContractError>; /// Unpauses a previously paused strategy. /// @@ -114,7 +121,11 @@ pub trait VaultTrait { /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. - fn unpause_strategy(e: Env, strategy_address: Address, caller: Address) -> Result<(), ContractError>; + fn unpause_strategy( + e: Env, + strategy_address: Address, + caller: Address, + ) -> Result<(), ContractError>; /// Retrieves the list of assets managed by the DeFindex Vault. /// @@ -136,7 +147,7 @@ pub trait VaultTrait { /// # Returns: /// * `Map` - A map of asset addresses to their total managed amounts. fn fetch_total_managed_funds(e: &Env) -> Map; - + /// Returns the current invested funds, representing the total assets allocated to strategies. /// /// This function provides a map where the key is the asset address and the value is the total amount @@ -164,7 +175,6 @@ pub trait VaultTrait { // TODO: DELETE THIS, USED FOR TESTING /// Temporary method for testing purposes. fn get_asset_amounts_for_dftokens(e: Env, df_token: i128) -> Map; - } pub trait AdminInterfaceTrait { @@ -175,7 +185,7 @@ pub trait AdminInterfaceTrait { /// # Arguments: /// * `e` - The environment. /// * `caller` - The address initiating the change (must be the manager or emergency manager). - /// * `fee_receiver` - The new fee receiver address. + /// * `vault_fee_receiver` - The new fee receiver address. /// /// # Returns: /// * `()` - No return value. @@ -234,25 +244,24 @@ pub trait AdminInterfaceTrait { } pub trait VaultManagementTrait { - /// Invests the vault's idle funds into the specified strategies. - /// + /// /// # Arguments: /// * `e` - The environment. /// * `investment` - A vector of `Investment` structs representing the amount to invest in each strategy. /// * `caller` - The address of the caller. - /// + /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. fn invest(e: Env, investment: Vec) -> Result<(), ContractError>; /// Rebalances the vault by executing a series of instructions. - /// + /// /// # Arguments: /// * `e` - The environment. /// * `instructions` - A vector of `Instruction` structs representing actions (withdraw, invest, swap, zapper) to be taken. - /// + /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. fn rebalance(e: Env, instructions: Vec) -> Result<(), ContractError>; -} \ No newline at end of file +} diff --git a/apps/contracts/vault/src/investment.rs b/apps/contracts/vault/src/investment.rs new file mode 100644 index 00000000..889a41e8 --- /dev/null +++ b/apps/contracts/vault/src/investment.rs @@ -0,0 +1,54 @@ +use soroban_sdk::{Address, Env, Map, Vec}; + +use crate::{ + models::Investment, + strategies::{get_strategy_asset, invest_in_strategy}, + utils::check_nonnegative_amount, + ContractError, +}; + +pub fn prepare_investment( + e: &Env, + investments: Vec, + idle_funds: Map, +) -> Result, ContractError> { + let mut total_investment_per_asset: Map = Map::new(e); + + for investment in investments.iter() { + let strategy_address = &investment.strategy; + let amount_to_invest = investment.amount; + check_nonnegative_amount(amount_to_invest.clone())?; + + // Find the corresponding asset for the strategy + let asset = get_strategy_asset(&e, strategy_address)?; + + // Track investment per asset + let current_investment = total_investment_per_asset + .get(asset.address.clone()) + .unwrap_or(0); + let updated_investment = current_investment + .checked_add(amount_to_invest) + .ok_or(ContractError::Overflow)?; + + total_investment_per_asset.set(asset.address.clone(), updated_investment); + + // Check if total investment exceeds idle funds + let idle_balance = idle_funds.get(asset.address.clone()).unwrap_or(0); + if updated_investment > idle_balance { + return Err(ContractError::NotEnoughIdleFunds); + } + } + + Ok(total_investment_per_asset) +} + +pub fn execute_investment(e: &Env, investments: Vec) -> Result<(), ContractError> { + for investment in investments.iter() { + let strategy_address = &investment.strategy; + let amount_to_invest = &investment.amount; + + invest_in_strategy(e, strategy_address, amount_to_invest)? + } + + Ok(()) +} diff --git a/apps/contracts/defindex/src/lib.rs b/apps/contracts/vault/src/lib.rs similarity index 80% rename from apps/contracts/defindex/src/lib.rs rename to apps/contracts/vault/src/lib.rs index 8f328d20..0aff3cc1 100755 --- a/apps/contracts/defindex/src/lib.rs +++ b/apps/contracts/vault/src/lib.rs @@ -1,9 +1,8 @@ #![no_std] -use aggregator::{internal_swap_exact_tokens_for_tokens, internal_swap_tokens_for_exact_tokens}; -use fee::collect_fees; -use investment::{execute_investment, prepare_investment}; use soroban_sdk::{ - contract, contractimpl, panic_with_error, token::{TokenClient, TokenInterface}, Address, Env, Map, String, Vec + contract, contractimpl, panic_with_error, + token::{TokenClient, TokenInterface}, + Address, Env, Map, String, Vec, }; use soroban_token_sdk::metadata::TokenMetadata; @@ -24,18 +23,32 @@ mod token; mod utils; use access::{AccessControl, AccessControlTrait, RolesDataKey}; +use aggregator::{internal_swap_exact_tokens_for_tokens, internal_swap_tokens_for_exact_tokens}; +use fee::collect_fees; use funds::{fetch_current_idle_funds, fetch_current_invested_funds, fetch_total_managed_funds}; -use interface::{AdminInterfaceTrait, VaultTrait, VaultManagementTrait}; -use models::{ActionType, AssetAllocation, Instruction, Investment, OptionalSwapDetailsExactIn, OptionalSwapDetailsExactOut}; +use interface::{AdminInterfaceTrait, VaultManagementTrait, VaultTrait}; +use investment::{execute_investment, prepare_investment}; +use models::{ + ActionType, AssetAllocation, Instruction, Investment, OptionalSwapDetailsExactIn, + OptionalSwapDetailsExactOut, +}; use storage::{ - get_assets, set_asset, set_defindex_receiver, set_factory, set_last_fee_assesment, set_total_assets, set_vault_fee + get_assets, set_asset, set_defindex_protocol_fee_receiver, set_factory, set_last_fee_assesment, + 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 strategies::{get_asset_allocation_from_address, get_strategy_asset, get_strategy_client, get_strategy_struct, invest_in_strategy, pause_strategy, unpause_strategy, withdraw_from_strategy}; -use token::{internal_mint, internal_burn, write_metadata, VaultToken}; +use token::{internal_burn, internal_mint, write_metadata, VaultToken}; use utils::{ - calculate_asset_amounts_for_dftokens, calculate_deposit_amounts_and_shares_to_mint, calculate_withdrawal_amounts, check_initialized, check_nonnegative_amount + calculate_asset_amounts_for_dftokens, calculate_deposit_amounts_and_shares_to_mint, + calculate_withdrawal_amounts, check_initialized, check_nonnegative_amount, }; +use defindex_strategy_core::DeFindexStrategyClient; + pub use error::ContractError; #[contract] @@ -45,29 +58,36 @@ pub struct DeFindexVault; impl VaultTrait for DeFindexVault { /// Initializes the DeFindex Vault contract with the required parameters. /// - /// This function sets the roles for emergency manager, fee receiver, and manager. + /// This function sets the roles for manager, emergency manager, vault fee receiver, and manager. /// It also stores the list of assets to be managed by the vault, including strategies for each asset. - /// - /// # Arguments: - /// * `e` - The environment. - /// * `assets` - A vector of `AssetAllocation` structs representing the assets and their associated strategies. - /// * `manager` - The address responsible for managing the vault. - /// * `emergency_manager` - The address with emergency control over the vault. - /// * `fee_receiver` - The address that will receive fees from the vault. - /// * `vault_fee` - The percentage of the vault's fees that will be sent to the DeFindex receiver. in BPS. - /// * `defindex_receiver` - The address that will receive fees for DeFindex from the vault. - /// * `factory` - The address of the factory that deployed the vault. /// - /// # Returns: - /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. + /// # Arguments + /// - `assets`: List of asset allocations for the vault, including strategies associated with each asset. + /// - `manager`: Primary vault manager with permissions for vault control. + /// - `emergency_manager`: Address with emergency access for emergency control over the vault. + /// - `vault_fee_receiver`: Address designated to receive the vault fee receiver's portion of management fees. + /// - `vault_fee`: Vault-specific fee percentage in basis points (typically set at 0-2% APR). + /// - `defindex_protocol_receiver`: Address receiving DeFindex’s protocol-wide fee in basis points (0.5% APR). + /// - `factory`: Factory contract address for deployment linkage. + /// - `vault_name`: Name of the vault token to be displayed in metadata. + /// - `vault_symbol`: Symbol representing the vault’s token. + /// + /// # Returns + /// - `Result<(), ContractError>`: Returns `Ok(())` if initialization succeeds, or a `ContractError` if + /// any setup fails (e.g., strategy mismatch with asset). + /// + /// # Errors + /// - `ContractError::AlreadyInitialized`: If the vault has already been initialized. + /// - `ContractError::StrategyDoesNotSupportAsset`: If a strategy within an asset does not support the asset’s contract. + /// fn initialize( e: Env, assets: Vec, manager: Address, emergency_manager: Address, - fee_receiver: Address, + vault_fee_receiver: Address, vault_fee: u32, - defindex_receiver: Address, + defindex_protocol_receiver: Address, factory: Address, vault_name: String, vault_symbol: String, @@ -78,31 +98,32 @@ impl VaultTrait for DeFindexVault { } access_control.set_role(&RolesDataKey::EmergencyManager, &emergency_manager); - access_control.set_role(&RolesDataKey::FeeReceiver, &fee_receiver); + access_control.set_role(&RolesDataKey::VaultFeeReceiver, &vault_fee_receiver); access_control.set_role(&RolesDataKey::Manager, &manager); - // Set Vault Share (in basis points) + // Set Vault Fee (in basis points) set_vault_fee(&e, &vault_fee); // Set Paltalabs Fee Receiver - set_defindex_receiver(&e, &defindex_receiver); + set_defindex_protocol_fee_receiver(&e, &defindex_protocol_receiver); // Set the factory address set_factory(&e, &factory); // Store Assets Objects let total_assets = assets.len(); + // TODO Require minimum 1 asset set_total_assets(&e, total_assets as u32); for (i, asset) in assets.iter().enumerate() { // for every asset, we need to check that the list of strategyes indeed support this asset - + // TODO Fix, currently failing - // for strategy in asset.strategies.iter() { - // let strategy_client = DeFindexStrategyClient::new(&e, &strategy.address); - // if strategy_client.asset() != asset.address { - // panic_with_error!(&e, ContractError::StrategyDoesNotSupportAsset); - // } - // } + for strategy in asset.strategies.iter() { + let strategy_client = DeFindexStrategyClient::new(&e, &strategy.address); + if strategy_client.asset() != asset.address { + panic_with_error!(&e, ContractError::StrategyDoesNotSupportAsset); + } + } set_asset(&e, i as u32, &asset); } @@ -119,7 +140,14 @@ impl VaultTrait for DeFindexVault { }, ); - events::emit_initialized_vault(&e, emergency_manager, fee_receiver, manager, defindex_receiver, assets); + events::emit_initialized_vault( + &e, + emergency_manager, + vault_fee_receiver, + manager, + defindex_protocol_receiver, + assets, + ); Ok(()) } @@ -148,7 +176,7 @@ impl VaultTrait for DeFindexVault { from.require_auth(); // Set LastFeeAssessment if it is the first deposit - if VaultToken::total_supply(e.clone())==0{ + if VaultToken::total_supply(e.clone()) == 0 { set_last_fee_assesment(&e, &e.ledger().timestamp()); } @@ -157,10 +185,11 @@ impl VaultTrait for DeFindexVault { // get assets let assets = get_assets(&e); - // assets lenght should be equal to amounts_desired and amounts_min length let assets_length = assets.len(); + + // assets lenght should be equal to amounts_desired and amounts_min length if assets_length != amounts_desired.len() || assets_length != amounts_min.len() { - panic_with_error!(&e, ContractError::WrongAmuntsLength); + panic_with_error!(&e, ContractError::WrongAmountsLength); } // for every amount desired, check non negative @@ -170,20 +199,23 @@ impl VaultTrait for DeFindexVault { // for amount min is not necesary to check if it is negative let (amounts, shares_to_mint) = if assets_length == 1 { - // If Total Assets == 1 - let shares = if VaultToken::total_supply(e.clone())==0{ + // If Total Assets == 1 + let shares = if VaultToken::total_supply(e.clone()) == 0 { // 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{ + } else { // in this case we will mint a share proportional to the total managed funds let total_managed_funds = fetch_total_managed_funds(&e); - VaultToken::total_supply(e.clone()) * amounts_desired.get(0).unwrap() / total_managed_funds.get(assets.get(0).unwrap().address.clone()).unwrap() + VaultToken::total_supply(e.clone()) * amounts_desired.get(0).unwrap() + / total_managed_funds + .get(assets.get(0).unwrap().address.clone()) + .unwrap() }; (amounts_desired, shares) } else { - // If Total Assets > 1 + // If Total Assets > 1 calculate_deposit_amounts_and_shares_to_mint( &e, &assets, @@ -237,53 +269,56 @@ impl VaultTrait for DeFindexVault { if df_user_balance < df_amount { return Err(ContractError::InsufficientBalance); } - + // Calculate the withdrawal amounts for each asset based on the dfToken amount let asset_amounts = calculate_asset_amounts_for_dftokens(&e, df_amount); // Burn the dfTokens after calculating the withdrawal amounts (so total supply is correct) internal_burn(e.clone(), from.clone(), df_amount); - + // Create a map to store the total amounts to transfer for each asset address let mut total_amounts_to_transfer: Map = Map::new(&e); - + // Get idle funds for each asset (Map) let idle_funds = fetch_current_idle_funds(&e); - + // Loop through each asset and handle the withdrawal for (asset_address, required_amount) in asset_amounts.iter() { // Check idle funds for this asset let idle_balance = idle_funds.get(asset_address.clone()).unwrap_or(0); let mut remaining_amount = required_amount; - + // Withdraw as much as possible from idle funds first if idle_balance > 0 { if idle_balance >= required_amount { // Idle funds cover the full amount total_amounts_to_transfer.set(asset_address.clone(), required_amount); - continue; // No need to withdraw from the strategy + continue; // No need to withdraw from the strategy } else { // Partial withdrawal from idle funds total_amounts_to_transfer.set(asset_address.clone(), idle_balance); - remaining_amount = required_amount - idle_balance; // Update remaining amount + remaining_amount = required_amount - idle_balance; // Update remaining amount } } - + // Find the corresponding asset address for this strategy let asset_allocation = get_asset_allocation_from_address(&e, asset_address.clone())?; - let withdrawal_amounts = calculate_withdrawal_amounts(&e, remaining_amount, asset_allocation); + let withdrawal_amounts = + calculate_withdrawal_amounts(&e, remaining_amount, asset_allocation); for (strategy_address, amount) in withdrawal_amounts.iter() { // TODO: What if the withdraw method exceeds the instructions limit? since im trying to ithdraw from all strategies of all assets... withdraw_from_strategy(&e, &strategy_address, &amount)?; - + // Update the total amounts to transfer map - let current_amount = total_amounts_to_transfer.get(strategy_address.clone()).unwrap_or(0); + let current_amount = total_amounts_to_transfer + .get(strategy_address.clone()) + .unwrap_or(0); total_amounts_to_transfer.set(asset_address.clone(), current_amount + amount); } } - + // Perform the transfers for the total amounts let mut amounts_withdrawn: Vec = Vec::new(&e); for (asset_address, total_amount) in total_amounts_to_transfer.iter() { @@ -313,31 +348,38 @@ impl VaultTrait for DeFindexVault { /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. - fn emergency_withdraw(e: Env, strategy_address: Address, caller: Address) -> Result<(), ContractError> { + fn emergency_withdraw( + e: Env, + strategy_address: Address, + caller: Address, + ) -> Result<(), ContractError> { check_initialized(&e)?; - + // Ensure the caller is the Manager or Emergency Manager let access_control = AccessControl::new(&e); - access_control.require_any_role(&[RolesDataKey::EmergencyManager, RolesDataKey::Manager], &caller); - + access_control.require_any_role( + &[RolesDataKey::EmergencyManager, RolesDataKey::Manager], + &caller, + ); + // Find the strategy and its associated asset let asset = get_strategy_asset(&e, &strategy_address)?; // This ensures that the vault has this strategy in its list of assets let strategy = get_strategy_struct(&strategy_address, &asset)?; - + // Withdraw all assets from the strategy let strategy_client = get_strategy_client(&e, strategy.address.clone()); let strategy_balance = strategy_client.balance(&e.current_contract_address()); - + if strategy_balance > 0 { strategy_client.withdraw(&strategy_balance, &e.current_contract_address()); //TODO: Should we check if the idle funds are corresponding to the strategy balance withdrawed? } - + // Pause the strategy pause_strategy(&e, strategy_address.clone())?; - + events::emit_emergency_withdraw_event(&e, caller, strategy_address, strategy_balance); Ok(()) } @@ -354,17 +396,24 @@ impl VaultTrait for DeFindexVault { /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. - fn pause_strategy(e: Env, strategy_address: Address, caller: Address) -> Result<(), ContractError> { + fn pause_strategy( + e: Env, + strategy_address: Address, + caller: Address, + ) -> Result<(), ContractError> { // Ensure the caller is the Manager or Emergency Manager // TODO: Should check if the strategy has any amount invested on it, and return an error if it has, should we let the manager to pause a strategy with funds invested? let access_control = AccessControl::new(&e); - access_control.require_any_role(&[RolesDataKey::EmergencyManager, RolesDataKey::Manager], &caller); + access_control.require_any_role( + &[RolesDataKey::EmergencyManager, RolesDataKey::Manager], + &caller, + ); pause_strategy(&e, strategy_address.clone())?; events::emit_strategy_paused_event(&e, strategy_address, caller); Ok(()) } - + /// Unpauses a previously paused strategy. /// /// This function unpauses a strategy by setting its `paused` field to `false`, allowing it to be used @@ -377,10 +426,17 @@ impl VaultTrait for DeFindexVault { /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. - fn unpause_strategy(e: Env, strategy_address: Address, caller: Address) -> Result<(), ContractError> { + fn unpause_strategy( + e: Env, + strategy_address: Address, + caller: Address, + ) -> Result<(), ContractError> { // Ensure the caller is the Manager or Emergency Manager let access_control = AccessControl::new(&e); - access_control.require_any_role(&[RolesDataKey::EmergencyManager, RolesDataKey::Manager], &caller); + access_control.require_any_role( + &[RolesDataKey::EmergencyManager, RolesDataKey::Manager], + &caller, + ); unpause_strategy(&e, strategy_address.clone())?; events::emit_strategy_unpaused_event(&e, strategy_address, caller); @@ -456,7 +512,7 @@ impl AdminInterfaceTrait for DeFindexVault { /// # Arguments: /// * `e` - The environment. /// * `caller` - The address initiating the change (must be the manager or emergency manager). - /// * `fee_receiver` - The new fee receiver address. + /// * `vault_fee_receiver` - The new fee receiver address. /// /// # Returns: /// * `()` - No return value. @@ -541,35 +597,35 @@ impl AdminInterfaceTrait for DeFindexVault { #[contractimpl] impl VaultManagementTrait for DeFindexVault { /// Invests the vault's idle funds into the specified strategies. - /// + /// /// # Arguments: /// * `e` - The environment. /// * `investment` - A vector of `Investment` structs representing the amount to invest in each strategy. /// * `caller` - The address of the caller. - /// + /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. fn invest(e: Env, investments: Vec) -> Result<(), ContractError> { check_initialized(&e)?; - + let access_control = AccessControl::new(&e); access_control.require_role(&RolesDataKey::Manager); e.current_contract_address().require_auth(); - + // Get the current idle funds for all assets let idle_funds = fetch_current_idle_funds(&e); - + // Prepare investments based on current idle funds // This checks if the total investment exceeds the idle funds prepare_investment(&e, investments.clone(), idle_funds)?; - + // Now proceed with the actual investments if all checks passed execute_investment(&e, investments)?; // auto invest mockup // if auto_invest { // let idle_funds = fetch_current_idle_funds(&e); - + // // Prepare investments based on current ratios of invested funds // let investments = calculate_investments_based_on_ratios(&e); // prepare_investment(&e, investments.clone(), idle_funds)?; @@ -579,20 +635,20 @@ impl VaultManagementTrait for DeFindexVault { } /// Rebalances the vault by executing a series of instructions. - /// + /// /// # Arguments: /// * `e` - The environment. /// * `instructions` - A vector of `Instruction` structs representing actions (withdraw, invest, swap, zapper) to be taken. - /// + /// /// # Returns: /// * `Result<(), ContractError>` - Ok if successful, otherwise returns a ContractError. fn rebalance(e: Env, instructions: Vec) -> Result<(), ContractError> { check_initialized(&e)?; - + let access_control = AccessControl::new(&e); access_control.require_role(&RolesDataKey::Manager); e.current_contract_address().require_auth(); - + for instruction in instructions.iter() { match instruction.action { ActionType::Withdraw => match (&instruction.strategy, &instruction.amount) { @@ -610,13 +666,13 @@ impl VaultManagementTrait for DeFindexVault { ActionType::SwapExactIn => match &instruction.swap_details_exact_in { OptionalSwapDetailsExactIn::Some(swap_details) => { internal_swap_exact_tokens_for_tokens( - &e, - &swap_details.token_in, - &swap_details.token_out, - &swap_details.amount_in, - &swap_details.amount_out_min, - &swap_details.distribution, - &swap_details.deadline + &e, + &swap_details.token_in, + &swap_details.token_out, + &swap_details.amount_in, + &swap_details.amount_out_min, + &swap_details.distribution, + &swap_details.deadline, )?; } _ => return Err(ContractError::MissingInstructionData), @@ -624,23 +680,23 @@ impl VaultManagementTrait for DeFindexVault { ActionType::SwapExactOut => match &instruction.swap_details_exact_out { OptionalSwapDetailsExactOut::Some(swap_details) => { internal_swap_tokens_for_exact_tokens( - &e, - &swap_details.token_in, - &swap_details.token_out, - &swap_details.amount_out, - &swap_details.amount_in_max, - &swap_details.distribution, - &swap_details.deadline + &e, + &swap_details.token_in, + &swap_details.token_out, + &swap_details.amount_out, + &swap_details.amount_in_max, + &swap_details.distribution, + &swap_details.deadline, )?; } _ => return Err(ContractError::MissingInstructionData), }, ActionType::Zapper => { // TODO: Implement Zapper instructions - }, + } } } - + Ok(()) } -} \ No newline at end of file +} diff --git a/apps/contracts/defindex/src/models.rs b/apps/contracts/vault/src/models.rs similarity index 99% rename from apps/contracts/defindex/src/models.rs rename to apps/contracts/vault/src/models.rs index e5c182bb..042ec976 100644 --- a/apps/contracts/defindex/src/models.rs +++ b/apps/contracts/vault/src/models.rs @@ -87,4 +87,4 @@ pub enum OptionalSwapDetailsExactIn { pub enum OptionalSwapDetailsExactOut { Some(SwapDetailsExactOut), None, -} \ No newline at end of file +} diff --git a/apps/contracts/defindex/src/storage.rs b/apps/contracts/vault/src/storage.rs similarity index 69% rename from apps/contracts/defindex/src/storage.rs rename to apps/contracts/vault/src/storage.rs index 4f873b5b..a546223a 100644 --- a/apps/contracts/defindex/src/storage.rs +++ b/apps/contracts/vault/src/storage.rs @@ -5,21 +5,26 @@ use crate::models::AssetAllocation; #[derive(Clone)] #[contracttype] enum DataKey { - AssetAllocation(u32), // AssetAllocation Addresse by index - TotalAssets, // Total number of tokens - DeFindexReceiver, + AssetAllocation(u32), // AssetAllocation Addresse by index + TotalAssets, // Total number of tokens + DeFindexProtocolFeeReceiver, Factory, LastFeeAssessment, - VaultShare, + VaultFee, } // Assets Management pub fn set_asset(e: &Env, index: u32, asset: &AssetAllocation) { - e.storage().instance().set(&DataKey::AssetAllocation(index), asset); + e.storage() + .instance() + .set(&DataKey::AssetAllocation(index), asset); } pub fn get_asset(e: &Env, index: u32) -> AssetAllocation { - e.storage().instance().get(&DataKey::AssetAllocation(index)).unwrap() + e.storage() + .instance() + .get(&DataKey::AssetAllocation(index)) + .unwrap() } pub fn set_total_assets(e: &Env, n: u32) { @@ -40,31 +45,26 @@ pub fn get_assets(e: &Env) -> Vec { } // DeFindex Fee Receiver -pub fn set_defindex_receiver(e: &Env, address: &Address) { +pub fn set_defindex_protocol_fee_receiver(e: &Env, address: &Address) { e.storage() .instance() - .set(&DataKey::DeFindexReceiver, address); + .set(&DataKey::DeFindexProtocolFeeReceiver, address); } -pub fn get_defindex_receiver(e: &Env) -> Address { +pub fn get_defindex_protocol_fee_receiver(e: &Env) -> Address { e.storage() .instance() - .get(&DataKey::DeFindexReceiver) + .get(&DataKey::DeFindexProtocolFeeReceiver) .unwrap() } // DeFindex Factory pub fn set_factory(e: &Env, address: &Address) { - e.storage() - .instance() - .set(&DataKey::Factory, address); + e.storage().instance().set(&DataKey::Factory, address); } pub fn get_factory(e: &Env) -> Address { - e.storage() - .instance() - .get(&DataKey::Factory) - .unwrap() + e.storage().instance().get(&DataKey::Factory).unwrap() } // Last Fee Assesment @@ -85,12 +85,12 @@ pub fn get_last_fee_assesment(e: &Env) -> u64 { pub fn set_vault_fee(e: &Env, vault_fee: &u32) { e.storage() .instance() - .set(&DataKey::VaultShare, vault_fee); + .set(&DataKey::VaultFee, vault_fee); } pub fn get_vault_fee(e: &Env) -> u32 { e.storage() .instance() - .get(&DataKey::VaultShare) + .get(&DataKey::VaultFee) .unwrap() } diff --git a/apps/contracts/defindex/src/strategies.rs b/apps/contracts/vault/src/strategies.rs similarity index 80% rename from apps/contracts/defindex/src/strategies.rs rename to apps/contracts/vault/src/strategies.rs index a1019745..cde32336 100644 --- a/apps/contracts/defindex/src/strategies.rs +++ b/apps/contracts/vault/src/strategies.rs @@ -1,18 +1,29 @@ use defindex_strategy_core::DeFindexStrategyClient; -use soroban_sdk::{Env, Address}; +use soroban_sdk::{Address, Env}; -use crate::{models::{AssetAllocation, Strategy}, storage::{get_asset, get_assets, get_total_assets, set_asset}, ContractError}; +use crate::{ + models::{AssetAllocation, Strategy}, + storage::{get_asset, get_assets, get_total_assets, set_asset}, + ContractError, +}; pub fn get_strategy_client(e: &Env, address: Address) -> DeFindexStrategyClient { DeFindexStrategyClient::new(&e, &address) } /// Finds the asset corresponding to the given strategy address. -pub fn get_strategy_asset(e: &Env, strategy_address: &Address) -> Result { +pub fn get_strategy_asset( + e: &Env, + strategy_address: &Address, +) -> Result { let assets = get_assets(e); for asset in assets.iter() { - if asset.strategies.iter().any(|strategy| &strategy.address == strategy_address) { + if asset + .strategies + .iter() + .any(|strategy| &strategy.address == strategy_address) + { return Ok(asset); } } @@ -21,7 +32,10 @@ pub fn get_strategy_asset(e: &Env, strategy_address: &Address) -> Result Result { +pub fn get_asset_allocation_from_address( + e: &Env, + asset_address: Address, +) -> Result { let assets = get_assets(e); for asset in assets.iter() { @@ -34,7 +48,10 @@ pub fn get_asset_allocation_from_address(e: &Env, asset_address: Address) -> Res } /// Finds the strategy struct corresponding to the given strategy address within the given asset. -pub fn get_strategy_struct(strategy_address: &Address, asset: &AssetAllocation) -> Result { +pub fn get_strategy_struct( + strategy_address: &Address, + asset: &AssetAllocation, +) -> Result { asset .strategies .iter() @@ -104,20 +121,28 @@ pub fn unpause_strategy(e: &Env, strategy_address: Address) -> Result<(), Contra Err(ContractError::StrategyNotFound) } -pub fn withdraw_from_strategy(e: &Env, strategy_address: &Address, amount: &i128) -> Result<(), ContractError> { +pub fn withdraw_from_strategy( + e: &Env, + strategy_address: &Address, + amount: &i128, +) -> Result<(), ContractError> { let strategy_client = get_strategy_client(e, strategy_address.clone()); - + match strategy_client.try_withdraw(amount, &e.current_contract_address()) { Ok(Ok(_)) => Ok(()), Ok(Err(_)) | Err(_) => Err(ContractError::StrategyWithdrawError), } } -pub fn invest_in_strategy(e: &Env, strategy_address: &Address, amount: &i128) -> Result<(), ContractError> { +pub fn invest_in_strategy( + e: &Env, + strategy_address: &Address, + amount: &i128, +) -> Result<(), ContractError> { let strategy_client = get_strategy_client(&e, strategy_address.clone()); - + match strategy_client.try_deposit(amount, &e.current_contract_address()) { Ok(Ok(_)) => Ok(()), Ok(Err(_)) | Err(_) => Err(ContractError::StrategyInvestError), } -} \ No newline at end of file +} diff --git a/apps/contracts/defindex/src/test.rs b/apps/contracts/vault/src/test.rs similarity index 68% rename from apps/contracts/defindex/src/test.rs rename to apps/contracts/vault/src/test.rs index d1bd4ad7..bd168890 100755 --- a/apps/contracts/defindex/src/test.rs +++ b/apps/contracts/vault/src/test.rs @@ -7,29 +7,31 @@ use soroban_sdk::{testutils::Address as _, vec as sorobanvec, Address, Env, Stri use std::vec; // DeFindex Hodl Strategy Contract -mod hodl_strategy { - soroban_sdk::contractimport!(file = "../target/wasm32-unknown-unknown/release/hodl_strategy.optimized.wasm"); +pub mod hodl_strategy { + soroban_sdk::contractimport!( + file = "../target/wasm32-unknown-unknown/release/hodl_strategy.optimized.wasm" + ); pub type HodlStrategyClient<'a> = Client<'a>; } use hodl_strategy::HodlStrategyClient; -fn create_hodl_strategy<'a>(e: & Env, asset: & Address) -> HodlStrategyClient<'a> { +fn create_hodl_strategy<'a>(e: &Env, asset: &Address) -> HodlStrategyClient<'a> { let contract_address = &e.register_contract_wasm(None, hodl_strategy::WASM); - let hodl_strategy = HodlStrategyClient::new(e, contract_address); + let hodl_strategy = HodlStrategyClient::new(e, contract_address); hodl_strategy.initialize(&asset, &sorobanvec![&e]); hodl_strategy } // DeFindex Vault Contract pub mod defindex_vault { - soroban_sdk::contractimport!(file = "../target/wasm32-unknown-unknown/release/defindex_vault.optimized.wasm"); + soroban_sdk::contractimport!( + file = "../target/wasm32-unknown-unknown/release/defindex_vault.optimized.wasm" + ); pub type DeFindexVaultClient<'a> = Client<'a>; } use defindex_vault::{DeFindexVaultClient, Strategy}; -fn create_defindex_vault<'a>( - e: & Env -) -> DeFindexVaultClient<'a> { +fn create_defindex_vault<'a>(e: &Env) -> DeFindexVaultClient<'a> { let address = &e.register_contract_wasm(None, defindex_vault::WASM); let client = DeFindexVaultClient::new(e, address); client @@ -52,7 +54,11 @@ fn create_defindex_vault<'a>( // Create Test Token pub(crate) fn create_token_contract<'a>(e: &Env, admin: &Address) -> SorobanTokenClient<'a> { - SorobanTokenClient::new(e, &e.register_stellar_asset_contract(admin.clone())) + SorobanTokenClient::new( + e, + &e.register_stellar_asset_contract_v2(admin.clone()) + .address(), + ) } pub(crate) fn get_token_admin_client<'a>( @@ -62,12 +68,23 @@ pub(crate) fn get_token_admin_client<'a>( SorobanTokenAdminClient::new(e, address) } -pub(crate) fn create_strategy_params(test: &DeFindexVaultTest) -> Vec { +pub(crate) fn create_strategy_params_token0(test: &DeFindexVaultTest) -> Vec { sorobanvec![ &test.env, Strategy { name: String::from_str(&test.env, "Strategy 1"), - address: test.strategy_client.address.clone(), + address: test.strategy_client_token0.address.clone(), + paused: false, + } + ] +} + +pub(crate) fn create_strategy_params_token1(test: &DeFindexVaultTest) -> Vec { + sorobanvec![ + &test.env, + Strategy { + name: String::from_str(&test.env, "Strategy 1"), + address: test.strategy_client_token1.address.clone(), paused: false, } ] @@ -82,10 +99,11 @@ pub struct DeFindexVaultTest<'a> { token1_admin_client: SorobanTokenAdminClient<'a>, token1: SorobanTokenClient<'a>, emergency_manager: Address, - fee_receiver: Address, - defindex_receiver: Address, + vault_fee_receiver: Address, + defindex_protocol_receiver: Address, manager: Address, - strategy_client: HodlStrategyClient<'a>, + strategy_client_token0: HodlStrategyClient<'a>, + strategy_client_token1: HodlStrategyClient<'a>, } impl<'a> DeFindexVaultTest<'a> { @@ -99,8 +117,8 @@ impl<'a> DeFindexVaultTest<'a> { let defindex_contract = create_defindex_vault(&env); let emergency_manager = Address::generate(&env); - let fee_receiver = Address::generate(&env); - let defindex_receiver = Address::generate(&env); + let vault_fee_receiver = Address::generate(&env); + let defindex_protocol_receiver = Address::generate(&env); let manager = Address::generate(&env); let token0_admin = Address::generate(&env); @@ -114,7 +132,8 @@ impl<'a> DeFindexVaultTest<'a> { // token1_admin_client.mint(to, amount); - let strategy_client = create_hodl_strategy(&env, &token0.address); + let strategy_client_token0 = create_hodl_strategy(&env, &token0.address); + let strategy_client_token1 = create_hodl_strategy(&env, &token1.address); DeFindexVaultTest { env, @@ -125,10 +144,11 @@ impl<'a> DeFindexVaultTest<'a> { token1_admin_client, token1, emergency_manager, - fee_receiver, - defindex_receiver, + vault_fee_receiver, + defindex_protocol_receiver, manager, - strategy_client, + strategy_client_token0, + strategy_client_token1, } } @@ -142,8 +162,8 @@ impl<'a> DeFindexVaultTest<'a> { } mod admin; -mod initialize; -mod withdraw; mod deposit; mod emergency_withdraw; -mod rebalance; \ No newline at end of file +mod initialize; +mod rebalance; +mod withdraw; diff --git a/apps/contracts/defindex/src/test/admin.rs b/apps/contracts/vault/src/test/admin.rs similarity index 74% rename from apps/contracts/defindex/src/test/admin.rs rename to apps/contracts/vault/src/test/admin.rs index ce047843..c7b64d08 100644 --- a/apps/contracts/defindex/src/test/admin.rs +++ b/apps/contracts/vault/src/test/admin.rs @@ -1,8 +1,12 @@ use soroban_sdk::{ - testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation, MockAuth, MockAuthInvoke}, vec as sorobanvec, Address, IntoVal, String, Symbol, Vec + testutils::{AuthorizedFunction, AuthorizedInvocation, MockAuth, MockAuthInvoke}, + vec as sorobanvec, IntoVal, String, Symbol, Vec, }; -use crate::test::{create_strategy_params, defindex_vault::AssetAllocation, DeFindexVaultTest}; +use crate::test::{ + create_strategy_params_token0, create_strategy_params_token1, defindex_vault::AssetAllocation, + DeFindexVaultTest, +}; extern crate alloc; use alloc::vec; @@ -10,17 +14,18 @@ use alloc::vec; #[test] fn test_set_new_fee_receiver_by_fee_receiver() { let test = DeFindexVaultTest::setup(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); let assets: Vec = sorobanvec![ &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() }, AssetAllocation { address: test.token1.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token1.clone() } ]; @@ -28,41 +33,44 @@ fn test_set_new_fee_receiver_by_fee_receiver() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), ); let fee_receiver_role = test.defindex_contract.get_fee_receiver(); - assert_eq!(fee_receiver_role, test.fee_receiver); + assert_eq!(fee_receiver_role, test.vault_fee_receiver); let users = DeFindexVaultTest::generate_random_users(&test.env, 1); // Fee Receiver is setting the new fee receiver test.defindex_contract .mock_auths(&[MockAuth { - address: &test.fee_receiver, + address: &test.vault_fee_receiver, invoke: &MockAuthInvoke { contract: &test.defindex_contract.address.clone(), fn_name: "set_fee_receiver", - args: (&test.fee_receiver, &users[0]).into_val(&test.env), + args: (&test.vault_fee_receiver, &users[0]).into_val(&test.env), sub_invokes: &[], }, }]) - .set_fee_receiver(&test.fee_receiver, &users[0]); + .set_fee_receiver(&test.vault_fee_receiver, &users[0]); let expected_auth = AuthorizedInvocation { // Top-level authorized function is `deploy` with all the arguments. function: AuthorizedFunction::Contract(( test.defindex_contract.address.clone(), Symbol::new(&test.env, "set_fee_receiver"), - (&test.fee_receiver, users[0].clone()).into_val(&test.env), + (&test.vault_fee_receiver, users[0].clone()).into_val(&test.env), )), sub_invocations: vec![], }; - assert_eq!(test.env.auths(), vec![(test.fee_receiver, expected_auth)]); + assert_eq!( + test.env.auths(), + vec![(test.vault_fee_receiver, expected_auth)] + ); let new_fee_receiver_role = test.defindex_contract.get_fee_receiver(); assert_eq!(new_fee_receiver_role, users[0]); @@ -71,7 +79,8 @@ fn test_set_new_fee_receiver_by_fee_receiver() { #[test] fn test_set_new_fee_receiver_by_manager() { let test = DeFindexVaultTest::setup(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); // let tokens: Vec
= sorobanvec![&test.env, test.token0.address.clone(), test.token1.address.clone()]; // let ratios: Vec = sorobanvec![&test.env, 1, 1]; @@ -79,11 +88,11 @@ fn test_set_new_fee_receiver_by_manager() { &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() }, AssetAllocation { address: test.token1.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token1.clone() } ]; @@ -91,16 +100,16 @@ fn test_set_new_fee_receiver_by_manager() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), ); let fee_receiver_role = test.defindex_contract.get_fee_receiver(); - assert_eq!(fee_receiver_role, test.fee_receiver); + assert_eq!(fee_receiver_role, test.vault_fee_receiver); let users = DeFindexVaultTest::generate_random_users(&test.env, 1); // Now Manager is setting the new fee receiver @@ -135,7 +144,8 @@ fn test_set_new_fee_receiver_by_manager() { #[should_panic(expected = "HostError: Error(Contract, #130)")] // Unauthorized fn test_set_new_fee_receiver_by_emergency_manager() { let test = DeFindexVaultTest::setup(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); // let tokens: Vec
= sorobanvec![&test.env, test.token0.address.clone(), test.token1.address.clone()]; // let ratios: Vec = sorobanvec![&test.env, 1, 1]; @@ -143,11 +153,11 @@ fn test_set_new_fee_receiver_by_emergency_manager() { &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() }, AssetAllocation { address: test.token1.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token1.clone() } ]; @@ -155,16 +165,16 @@ fn test_set_new_fee_receiver_by_emergency_manager() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), ); let fee_receiver_role = test.defindex_contract.get_fee_receiver(); - assert_eq!(fee_receiver_role, test.fee_receiver); + assert_eq!(fee_receiver_role, test.vault_fee_receiver); let users = DeFindexVaultTest::generate_random_users(&test.env, 1); // Now Emergency Manager is setting the new fee receiver @@ -176,17 +186,18 @@ fn test_set_new_fee_receiver_by_emergency_manager() { #[should_panic(expected = "HostError: Error(Contract, #130)")] // Unauthorized fn test_set_new_fee_receiver_invalid_sender() { let test = DeFindexVaultTest::setup(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); let assets: Vec = sorobanvec![ &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() }, AssetAllocation { address: test.token1.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token1.clone() } ]; @@ -194,16 +205,16 @@ fn test_set_new_fee_receiver_invalid_sender() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), ); let fee_receiver_role = test.defindex_contract.get_fee_receiver(); - assert_eq!(fee_receiver_role, test.fee_receiver); + assert_eq!(fee_receiver_role, test.vault_fee_receiver); let users = DeFindexVaultTest::generate_random_users(&test.env, 1); // Trying to set the new fee receiver with an invalid sender @@ -214,16 +225,17 @@ fn test_set_new_fee_receiver_invalid_sender() { #[test] fn test_set_new_manager_by_manager() { let test = DeFindexVaultTest::setup(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); let assets: Vec = sorobanvec![ &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() }, AssetAllocation { address: test.token1.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token1.clone() } ]; @@ -231,9 +243,9 @@ fn test_set_new_manager_by_manager() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), diff --git a/apps/contracts/defindex/src/test/deposit.rs b/apps/contracts/vault/src/test/deposit.rs similarity index 77% rename from apps/contracts/defindex/src/test/deposit.rs rename to apps/contracts/vault/src/test/deposit.rs index b765e1d3..30bb840d 100644 --- a/apps/contracts/defindex/src/test/deposit.rs +++ b/apps/contracts/vault/src/test/deposit.rs @@ -1,25 +1,27 @@ use soroban_sdk::{vec as sorobanvec, String, Vec}; -use crate::test::{create_strategy_params, DeFindexVaultTest}; use crate::test::defindex_vault::{AssetAllocation, ContractError}; +use crate::test::{ + create_strategy_params_token0, create_strategy_params_token1, DeFindexVaultTest, +}; #[test] fn deposit_amounts_desired_wrong_length() { - let test = DeFindexVaultTest::setup(); test.env.mock_all_auths(); - let strategy_params = create_strategy_params(&test); - + let strategy_params_token0 = create_strategy_params_token0(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); + // initialize with 2 assets let assets: Vec = sorobanvec![ &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() }, AssetAllocation { address: test.token1.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token1.clone() } ]; @@ -27,34 +29,31 @@ fn deposit_amounts_desired_wrong_length() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), ); let amount = 1000i128; - + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); - let response = test.defindex_contract.try_deposit( &sorobanvec![&test.env, amount], // wrong amount desired - &sorobanvec![&test.env, amount, amount], - &users[0]); - - assert_eq!(response, Err(Ok(ContractError::WrongAmuntsLength))); + &sorobanvec![&test.env, amount, amount], + &users[0], + ); + assert_eq!(response, Err(Ok(ContractError::WrongAmountsLength))); } - #[test] fn deposit_amounts_min_wrong_length() { todo!(); } - #[test] fn deposit_amounts_desired_negative() { todo!(); @@ -65,7 +64,6 @@ fn deposit_one_asset() { todo!(); } - #[test] fn deposit_several_assets() { todo!(); @@ -79,7 +77,8 @@ fn deposit_several_assets() { // fn test_withdraw_success() { // let test = DeFindexVaultTest::setup(); // test.env.mock_all_auths(); -// let strategy_params = create_strategy_params(&test); +// let strategy_params_token0 = create_strategy_params_token0(&test); +// let strategy_params_token1 = create_strategy_params_token1(&test); // let assets: Vec = sorobanvec![ // &test.env, // AssetAllocation { @@ -93,13 +92,13 @@ fn deposit_several_assets() { // &assets, // &test.manager, // &test.emergency_manager, -// &test.fee_receiver, -// &test.defindex_receiver, +// &test.vault_fee_receiver, +// &test.defindex_protocol_receiver, // ); // let amount = 1000i128; - + // let users = DeFindexVaultTest::generate_random_users(&test.env, 1); - + // test.token0_admin_client.mint(&users[0], &amount); // let user_balance = test.token0.balance(&users[0]); // assert_eq!(user_balance, amount); @@ -114,10 +113,10 @@ fn deposit_several_assets() { // assert_eq!(df_balance, amount); // test.defindex_contract.withdraw(&df_balance, &users[0]); - + // let df_balance = test.defindex_contract.balance(&users[0]); // assert_eq!(df_balance, 0i128); // let user_balance = test.token0.balance(&users[0]); // assert_eq!(user_balance, amount); -// } \ No newline at end of file +// } diff --git a/apps/contracts/defindex/src/test/emergency_withdraw.rs b/apps/contracts/vault/src/test/emergency_withdraw.rs similarity index 70% rename from apps/contracts/defindex/src/test/emergency_withdraw.rs rename to apps/contracts/vault/src/test/emergency_withdraw.rs index 1d2757f6..fee60772 100644 --- a/apps/contracts/defindex/src/test/emergency_withdraw.rs +++ b/apps/contracts/vault/src/test/emergency_withdraw.rs @@ -1,17 +1,21 @@ use soroban_sdk::{vec as sorobanvec, String, Vec}; -use crate::test::{create_strategy_params, defindex_vault::{AssetAllocation, Investment}, DeFindexVaultTest}; +use crate::test::{ + create_strategy_params_token0, + defindex_vault::{AssetAllocation, Investment}, + DeFindexVaultTest, +}; #[test] fn test_emergency_withdraw_success() { let test = DeFindexVaultTest::setup(); test.env.mock_all_auths(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); let assets: Vec = sorobanvec![ &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() } ]; @@ -19,26 +23,30 @@ fn test_emergency_withdraw_success() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), ); let amount = 1000i128; - + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); - + test.token0_admin_client.mint(&users[0], &amount); let user_balance = test.token0.balance(&users[0]); assert_eq!(user_balance, amount); let df_balance = test.defindex_contract.balance(&users[0]); assert_eq!(df_balance, 0i128); - + // Deposit - test.defindex_contract.deposit(&sorobanvec![&test.env, amount], &sorobanvec![&test.env, amount], &users[0]); + test.defindex_contract.deposit( + &sorobanvec![&test.env, amount], + &sorobanvec![&test.env, amount], + &users[0], + ); let df_balance = test.defindex_contract.balance(&users[0]); assert_eq!(df_balance, amount); @@ -52,23 +60,30 @@ fn test_emergency_withdraw_success() { &test.env, Investment { amount: amount.clone(), - strategy: strategy_params.first().unwrap().address.clone() + strategy: strategy_params_token0.first().unwrap().address.clone() } ]; test.defindex_contract.invest(&investments); - + // Balance of the token0 on the vault should be 0 let vault_balance_of_token = test.token0.balance(&test.defindex_contract.address); assert_eq!(vault_balance_of_token, 0); // Balance of the strategy should be `amount` - let strategy_balance = test.strategy_client.balance(&test.defindex_contract.address); + let strategy_balance = test + .strategy_client_token0 + .balance(&test.defindex_contract.address); assert_eq!(strategy_balance, amount); - test.defindex_contract.emergency_withdraw(&strategy_params.first().unwrap().address, &test.emergency_manager); + test.defindex_contract.emergency_withdraw( + &strategy_params_token0.first().unwrap().address, + &test.emergency_manager, + ); // Balance of the strategy should be 0 - let strategy_balance = test.strategy_client.balance(&test.defindex_contract.address); + let strategy_balance = test + .strategy_client_token0 + .balance(&test.defindex_contract.address); assert_eq!(strategy_balance, 0); // Balance of the token0 on the vault should be `amount` @@ -78,4 +93,4 @@ fn test_emergency_withdraw_success() { // check if strategy is paused let asset = test.defindex_contract.get_assets().first().unwrap(); assert_eq!(asset.strategies.first().unwrap().paused, true); -} \ No newline at end of file +} diff --git a/apps/contracts/defindex/src/test/initialize.rs b/apps/contracts/vault/src/test/initialize.rs similarity index 73% rename from apps/contracts/defindex/src/test/initialize.rs rename to apps/contracts/vault/src/test/initialize.rs index 9af00d37..df040047 100644 --- a/apps/contracts/defindex/src/test/initialize.rs +++ b/apps/contracts/vault/src/test/initialize.rs @@ -1,20 +1,25 @@ -use soroban_sdk::{vec as sorobanvec, Address, String, Vec}; +use soroban_sdk::{vec as sorobanvec, String, Vec}; -use crate::test::{create_strategy_params, defindex_vault::{AssetAllocation, ContractError}, DeFindexVaultTest}; +use crate::test::{ + create_strategy_params_token0, create_strategy_params_token1, + defindex_vault::{AssetAllocation, ContractError}, + DeFindexVaultTest, +}; #[test] fn test_initialize_and_get_roles() { let test = DeFindexVaultTest::setup(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); let assets: Vec = sorobanvec![ &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() }, AssetAllocation { address: test.token1.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token1.clone() } ]; @@ -22,9 +27,9 @@ fn test_initialize_and_get_roles() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), @@ -35,7 +40,7 @@ fn test_initialize_and_get_roles() { let emergency_manager_role = test.defindex_contract.get_emergency_manager(); assert_eq!(manager_role, test.manager); - assert_eq!(fee_receiver_role, test.fee_receiver); + assert_eq!(fee_receiver_role, test.vault_fee_receiver); assert_eq!(emergency_manager_role, test.emergency_manager); } @@ -54,17 +59,18 @@ fn test_get_roles_not_yet_initialized() { #[test] fn test_initialize_twice() { let test = DeFindexVaultTest::setup(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); let assets: Vec = sorobanvec![ &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() }, AssetAllocation { address: test.token1.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token1.clone() } ]; @@ -72,9 +78,9 @@ fn test_initialize_twice() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), @@ -84,9 +90,9 @@ fn test_initialize_twice() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), @@ -123,10 +129,11 @@ fn test_emergency_withdraw_not_yet_initialized() { let test = DeFindexVaultTest::setup(); let users = DeFindexVaultTest::generate_random_users(&test.env, 1); - let strategy_params = create_strategy_params(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); + let strategy_params_token1 = create_strategy_params_token1(&test); let result = test .defindex_contract - .try_emergency_withdraw(&strategy_params.first().unwrap().address, &users[0]); + .try_emergency_withdraw(&strategy_params_token1.first().unwrap().address, &users[0]); assert_eq!(result, Err(Ok(ContractError::NotInitialized))); } diff --git a/apps/contracts/defindex/src/test/rebalance.rs b/apps/contracts/vault/src/test/rebalance.rs similarity index 71% rename from apps/contracts/defindex/src/test/rebalance.rs rename to apps/contracts/vault/src/test/rebalance.rs index 49e7bbd3..5a39c049 100644 --- a/apps/contracts/defindex/src/test/rebalance.rs +++ b/apps/contracts/vault/src/test/rebalance.rs @@ -1,17 +1,24 @@ use soroban_sdk::{vec as sorobanvec, String, Vec}; -use crate::test::{create_strategy_params, defindex_vault::{ActionType, AssetAllocation, Instruction, Investment, OptionalSwapDetailsExactIn, OptionalSwapDetailsExactOut}, DeFindexVaultTest}; +use crate::test::{ + create_strategy_params_token0, + defindex_vault::{ + ActionType, AssetAllocation, Instruction, Investment, OptionalSwapDetailsExactIn, + OptionalSwapDetailsExactOut, + }, + DeFindexVaultTest, +}; #[test] fn rebalance() { let test = DeFindexVaultTest::setup(); test.env.mock_all_auths(); - let strategy_params = create_strategy_params(&test); + let strategy_params_token0 = create_strategy_params_token0(&test); let assets: Vec = sorobanvec![ &test.env, AssetAllocation { address: test.token0.address.clone(), - strategies: strategy_params.clone() + strategies: strategy_params_token0.clone() } ]; @@ -19,17 +26,17 @@ fn rebalance() { &assets, &test.manager, &test.emergency_manager, - &test.fee_receiver, + &test.vault_fee_receiver, &2000u32, - &test.defindex_receiver, + &test.defindex_protocol_receiver, &test.defindex_factory, &String::from_str(&test.env, "dfToken"), &String::from_str(&test.env, "DFT"), ); let amount = 1000i128; - + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); - + test.token0_admin_client.mint(&users[0], &amount); let user_balance = test.token0.balance(&users[0]); assert_eq!(user_balance, amount); @@ -37,19 +44,23 @@ fn rebalance() { let df_balance = test.defindex_contract.balance(&users[0]); assert_eq!(df_balance, 0i128); - test.defindex_contract.deposit(&sorobanvec![&test.env, amount], &sorobanvec![&test.env, amount], &users[0]); + test.defindex_contract.deposit( + &sorobanvec![&test.env, amount], + &sorobanvec![&test.env, amount], + &users[0], + ); let df_balance = test.defindex_contract.balance(&users[0]); assert_eq!(df_balance, amount); let investments = sorobanvec![ - &test.env, + &test.env, Investment { - amount: amount, - strategy: test.strategy_client.address.clone() + amount: amount, + strategy: test.strategy_client_token0.address.clone() } ]; - + test.defindex_contract.invest(&investments); let vault_balance = test.token0.balance(&test.defindex_contract.address); @@ -60,19 +71,18 @@ fn rebalance() { let instruction_amount_0 = 200i128; let instruction_amount_1 = 100i128; - let instructions = sorobanvec![ - &test.env, + &test.env, Instruction { action: ActionType::Withdraw, - strategy: Some(test.strategy_client.address.clone()), + strategy: Some(test.strategy_client_token0.address.clone()), amount: Some(instruction_amount_0), swap_details_exact_in: OptionalSwapDetailsExactIn::None, swap_details_exact_out: OptionalSwapDetailsExactOut::None, }, Instruction { action: ActionType::Invest, - strategy: Some(test.strategy_client.address.clone()), + strategy: Some(test.strategy_client_token0.address.clone()), amount: Some(instruction_amount_1), swap_details_exact_in: OptionalSwapDetailsExactIn::None, swap_details_exact_out: OptionalSwapDetailsExactOut::None, @@ -83,5 +93,4 @@ fn rebalance() { let vault_balance = test.token0.balance(&test.defindex_contract.address); assert_eq!(vault_balance, instruction_amount_1); - -} \ No newline at end of file +} diff --git a/apps/contracts/vault/src/test/withdraw.rs b/apps/contracts/vault/src/test/withdraw.rs new file mode 100644 index 00000000..e28058a7 --- /dev/null +++ b/apps/contracts/vault/src/test/withdraw.rs @@ -0,0 +1,192 @@ +use soroban_sdk::{vec as sorobanvec, String, Vec}; + +use super::hodl_strategy::StrategyError; +use crate::test::{ + create_strategy_params_token0, + defindex_vault::{AssetAllocation, Investment}, + DeFindexVaultTest, +}; + +#[test] +fn test_withdraw_from_idle_success() { + let test = DeFindexVaultTest::setup(); + test.env.mock_all_auths(); + let strategy_params_token0 = create_strategy_params_token0(&test); + let assets: Vec = sorobanvec![ + &test.env, + AssetAllocation { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + } + ]; + + test.defindex_contract.initialize( + &assets, + &test.manager, + &test.emergency_manager, + &test.vault_fee_receiver, + &2000u32, + &test.defindex_protocol_receiver, + &test.defindex_factory, + &String::from_str(&test.env, "dfToken"), + &String::from_str(&test.env, "DFT"), + ); + let amount = 1234567890i128; + + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); + + test.token0_admin_client.mint(&users[0], &amount); + let user_balance = test.token0.balance(&users[0]); + assert_eq!(user_balance, amount); + // here youll need to create a client for a token with the same address + + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, 0i128); + + // Deposit + let amount_to_deposit = 567890i128; + test.defindex_contract.deposit( + &sorobanvec![&test.env, amount_to_deposit], + &sorobanvec![&test.env, amount_to_deposit], + &users[0], + ); + + // Check Balances after deposit + + // Token balance of user + let user_balance = test.token0.balance(&users[0]); + assert_eq!(user_balance, amount - amount_to_deposit); + + // Token balance of vault should be amount_to_deposit + // Because balances are still in indle, balances are not in strategy, but in idle + + let vault_balance = test.token0.balance(&test.defindex_contract.address); + assert_eq!(vault_balance, amount_to_deposit); + + // Token balance of hodl strategy should be 0 (all in idle) + let strategy_balance = test.token0.balance(&test.strategy_client_token0.address); + assert_eq!(strategy_balance, 0); + + // Df balance of user should be equal to deposited amount + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, amount_to_deposit); + + // user decides to withdraw a portion of deposited amount + let amount_to_withdraw = 123456i128; + test.defindex_contract + .withdraw(&amount_to_withdraw, &users[0]); + + // Check Balances after withdraw + + // Token balance of user should be amount - amount_to_deposit + amount_to_withdraw + let user_balance = test.token0.balance(&users[0]); + assert_eq!( + user_balance, + amount - amount_to_deposit + amount_to_withdraw + ); + + // Token balance of vault should be amount_to_deposit - amount_to_withdraw + let vault_balance = test.token0.balance(&test.defindex_contract.address); + assert_eq!(vault_balance, amount_to_deposit - amount_to_withdraw); + + // Token balance of hodl strategy should be 0 (all in idle) + let strategy_balance = test.token0.balance(&test.strategy_client_token0.address); + assert_eq!(strategy_balance, 0); + + // Df balance of user should be equal to deposited amount - amount_to_withdraw + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, amount_to_deposit - amount_to_withdraw); + + // user tries to withdraw more than deposited amount + let amount_to_withdraw_more = amount_to_deposit + 1; + let result = test + .defindex_contract + .try_withdraw(&amount_to_withdraw_more, &users[0]); + // just check if is error + assert_eq!(result.is_err(), true); + + // TODO test corresponding error + + // withdraw remaining balance + test.defindex_contract + .withdraw(&(amount_to_deposit - amount_to_withdraw), &users[0]); + + // // result is err + + // assert_eq!(result, Err(Ok(StrategyError::InsufficientBalance))); + + // result should be error from contract + + // let df_balance = test.defindex_contract.balance(&users[0]); + // assert_eq!(df_balance, 0i128); + + // let user_balance = test.token0.balance(&users[0]); + // assert_eq!(user_balance, amount); +} + +#[test] +fn test_withdraw_from_strategy_success() { + let test = DeFindexVaultTest::setup(); + test.env.mock_all_auths(); + let strategy_params_token0 = create_strategy_params_token0(&test); + let assets: Vec = sorobanvec![ + &test.env, + AssetAllocation { + address: test.token0.address.clone(), + strategies: strategy_params_token0.clone() + } + ]; + + test.defindex_contract.initialize( + &assets, + &test.manager, + &test.emergency_manager, + &test.vault_fee_receiver, + &2000u32, + &test.defindex_protocol_receiver, + &test.defindex_factory, + &String::from_str(&test.env, "dfToken"), + &String::from_str(&test.env, "DFT"), + ); + let amount = 1000i128; + + let users = DeFindexVaultTest::generate_random_users(&test.env, 1); + + test.token0_admin_client.mint(&users[0], &amount); + let user_balance = test.token0.balance(&users[0]); + assert_eq!(user_balance, amount); + // here youll need to create a client for a token with the same address + + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, 0i128); + + test.defindex_contract.deposit( + &sorobanvec![&test.env, amount], + &sorobanvec![&test.env, amount], + &users[0], + ); + + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, amount); + + let investments = sorobanvec![ + &test.env, + Investment { + amount: amount, + strategy: test.strategy_client_token0.address.clone() + } + ]; + + test.defindex_contract.invest(&investments); + + let vault_balance = test.token0.balance(&test.defindex_contract.address); + assert_eq!(vault_balance, 0); + + test.defindex_contract.withdraw(&df_balance, &users[0]); + + let df_balance = test.defindex_contract.balance(&users[0]); + assert_eq!(df_balance, 0i128); + + let user_balance = test.token0.balance(&users[0]); + assert_eq!(user_balance, amount); +} diff --git a/apps/contracts/defindex/src/token/allowance.rs b/apps/contracts/vault/src/token/allowance.rs similarity index 100% rename from apps/contracts/defindex/src/token/allowance.rs rename to apps/contracts/vault/src/token/allowance.rs diff --git a/apps/contracts/defindex/src/token/balance.rs b/apps/contracts/vault/src/token/balance.rs similarity index 100% rename from apps/contracts/defindex/src/token/balance.rs rename to apps/contracts/vault/src/token/balance.rs diff --git a/apps/contracts/defindex/src/token/contract.rs b/apps/contracts/vault/src/token/contract.rs similarity index 100% rename from apps/contracts/defindex/src/token/contract.rs rename to apps/contracts/vault/src/token/contract.rs diff --git a/apps/contracts/defindex/src/token/metadata.rs b/apps/contracts/vault/src/token/metadata.rs similarity index 100% rename from apps/contracts/defindex/src/token/metadata.rs rename to apps/contracts/vault/src/token/metadata.rs diff --git a/apps/contracts/defindex/src/token/mod.rs b/apps/contracts/vault/src/token/mod.rs similarity index 100% rename from apps/contracts/defindex/src/token/mod.rs rename to apps/contracts/vault/src/token/mod.rs diff --git a/apps/contracts/defindex/src/token/storage_types.rs b/apps/contracts/vault/src/token/storage_types.rs similarity index 100% rename from apps/contracts/defindex/src/token/storage_types.rs rename to apps/contracts/vault/src/token/storage_types.rs diff --git a/apps/contracts/defindex/src/token/total_supply.rs b/apps/contracts/vault/src/token/total_supply.rs similarity index 100% rename from apps/contracts/defindex/src/token/total_supply.rs rename to apps/contracts/vault/src/token/total_supply.rs diff --git a/apps/contracts/defindex/src/utils.rs b/apps/contracts/vault/src/utils.rs similarity index 93% rename from apps/contracts/defindex/src/utils.rs rename to apps/contracts/vault/src/utils.rs index aa215ee3..96f0ed88 100644 --- a/apps/contracts/defindex/src/utils.rs +++ b/apps/contracts/vault/src/utils.rs @@ -1,8 +1,14 @@ -use soroban_sdk::{contracttype, panic_with_error, Address, Env, Map, Vec}; +use soroban_sdk::{panic_with_error, Address, Env, Map, Vec}; use crate::{ - access::{AccessControl, AccessControlTrait, RolesDataKey}, funds::{fetch_invested_funds_for_asset, fetch_invested_funds_for_strategy, fetch_total_managed_funds}, models::AssetAllocation, token::VaultToken, ContractError - + access::{AccessControl, AccessControlTrait, RolesDataKey}, + funds::{ + fetch_invested_funds_for_asset, fetch_invested_funds_for_strategy, + fetch_total_managed_funds, + }, + models::AssetAllocation, + token::VaultToken, + ContractError, }; pub const DAY_IN_LEDGERS: u32 = 17280; @@ -51,7 +57,8 @@ pub fn calculate_withdrawal_amounts( let strategy_invested_funds = fetch_invested_funds_for_strategy(e, &strategy.address); - let strategy_share_of_withdrawal = (amount * strategy_invested_funds) / total_invested_in_strategies; + let strategy_share_of_withdrawal = + (amount * strategy_invested_funds) / total_invested_in_strategies; withdrawal_amounts.set(strategy.address.clone(), strategy_share_of_withdrawal); } @@ -131,7 +138,7 @@ pub fn calculate_optimal_amounts_and_shares_with_enforced_asset( // this might be the first deposit... in this case, the ratio will be enforced by the first depositor // TODO: might happen that the reserve_target is zero because everything is in one asset!? // in this case we ned to check the ratio - // TODO VERY DANGEROUS. + // TODO VERY DANGEROUS. } let amount_desired_target = amounts_desired.get(*i).unwrap(); // i128 @@ -148,22 +155,23 @@ pub fn calculate_optimal_amounts_and_shares_with_enforced_asset( } } //TODO: calculate the shares to mint = total_supply * amount_desired_target / reserve_target - let shares_to_mint = VaultToken::total_supply(e.clone()) * amount_desired_target / reserve_target; + let shares_to_mint = + VaultToken::total_supply(e.clone()) * amount_desired_target / reserve_target; (optimal_amounts, shares_to_mint) } /// Calculates the optimal amounts to deposit for a set of assets, along with the shares to mint. /// This function iterates over a list of assets and checks if the desired deposit amounts /// match the optimal deposit strategy, based on current managed funds and asset ratios. -/// +/// /// If the desired amount for a given asset cannot be achieved due to constraints (e.g., it's below the minimum amount), /// the function attempts to find an optimal solution by adjusting the amounts of subsequent assets. -/// +/// /// # Arguments /// * `e` - The current environment. /// * `assets` - A vector of assets for which deposits are being calculated. /// * `amounts_desired` - A vector of desired amounts for each asset. /// * `amounts_min` - A vector of minimum amounts for each asset, below which deposits are not allowed. -/// +/// /// # Returns /// A tuple containing: /// * A vector of optimal amounts to deposit for each asset. @@ -183,7 +191,7 @@ pub fn calculate_deposit_amounts_and_shares_to_mint( amounts_min: &Vec, ) -> (Vec, i128) { // Retrieve the total managed funds for each asset as a Map. - let total_managed_funds = fetch_total_managed_funds(e); + let total_managed_funds = fetch_total_managed_funds(e); // Iterate over each asset in the assets vector. for i in 0..assets.len() { @@ -232,4 +240,4 @@ pub fn calculate_deposit_amounts_and_shares_to_mint( // If no solution was found after iterating through all assets, throw an error. panic!("didn't find optimal amounts"); -} \ No newline at end of file +} diff --git a/apps/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 fd357bae..2f2783d7 100644 --- a/apps/docs/10-whitepaper/03-the-defindex-approach/02-contracts/01-vault-contract.md +++ b/apps/docs/10-whitepaper/03-the-defindex-approach/02-contracts/01-vault-contract.md @@ -6,14 +6,14 @@ While anyone can invest in a DeFindex, only the Manager and Emergency Manager ha The contract also holds funds not currently invested in any strategy, known as **IDLE funds**. These funds act as a safety buffer, allowing the Emergency Manager to withdraw assets from underperforming or unhealthy strategies and store them as IDLE funds. (also to enable fast small withdrawals) -### Underlying Assets +## Underlying Assets Each DeFindex Vault will use a defined set of underlying assets to be invested in one or more strategies. Because Strategies are the only one that know exactly the current balance of the asset, the Vault relies on the strategies in order to know the exact total balance for each underlying asset.?? Or if the Vault executes Strategies at its own name (auth), it should execute a speficic `get_assets_balance` function in the strategy contract to know exactely how many assets it has at a specific moment. -### Initialization +## Initialization The DeFindex Vault contract is structured with specific roles and strategies for managing assets effectively. The key roles include the **Fee Receiver**, **Manager**, and **Emergency Manager**, each responsible for different tasks in managing the Vault. Additionally, a predefined set of strategies determines how assets will be allocated within the Vault. A management fee is also established at the time of initialization, which can later be adjusted by the Fee Receiver. Further details on fee handling are explained later in the document. The allocation ratios for these strategies are not set during the deployment but are defined during the first deposit made into the Vault. For example, imagine a scenario where the Vault is set up to allocate 20% of its assets to a USDC lending pool (like Blend), 30% to another USDC lending pool (such as YieldBlox), and 50% to a USDC-XLM liquidity pool on an Automated Market Maker (AMM) platform (like Soroswap). @@ -32,7 +32,7 @@ In summary: -### Investing: Deposit +## Investing: Deposit When a user deposits assets into the DeFindex, they receive dfTokens that represent their proportional share of the DeFindex's assets. These dfTokens can later be burned to withdraw the corresponding assets. Upon calling the `deposit()` function, **the assets are transferred to the DeFindex in accordance with the current asset ratio**. For example, if the current ratio is 1 token A, 2 tokens B, and 3 tokens C for each dfToken, this ratio is maintained when assets are deposited. In return, the user receives dfTokens that represent their participation in the DeFindex Vault. @@ -43,7 +43,7 @@ Thus, the price per dfToken reflects a multi-asset price. For instance, using th -### Withdrawals +## Withdrawals When a user wishes to withdraw funds, they must burn a corresponding amount of dfTokens (shares) to receive their **assets at the ratio of the time of withdrawal**. If there are sufficient **IDLE funds** available, the withdrawal is fulfilled directly from these IDLE funds. If additional assets are needed beyond what is available in the IDLE funds, a liquidation process is triggered to release the required assets. @@ -81,14 +81,14 @@ Where: - $a_{i, \text{IDLE}}$: Amount of asset $i$ to get from the IDLE funds - $a_{i, \text{Strategy}}$: Amount of asset $i$ to get from the strategies -### Rebalancing +## Rebalancing Rebalancing is overseen by the **Manager**, who adjusts the allocation of funds between different strategies to maintain or change the ratio of underlying assets. For example, a DeFindex might start with a ratio of 2 USDC to 1 XLM, as initially set by the Deployer. However, this ratio can be modified by the Manager based on strategy performance or market conditions. Upon deployment, the Deployer establishes the initial strategies and asset ratios for the DeFindex. The Manager has the authority to adjust these ratios as needed to respond to evolving conditions or to optimize performance. To ensure accurate representation of asset proportions, strategies are required to **report** the amount of each underlying asset they hold. This reporting ensures that when dfTokens are minted or redeemed, the DeFindex maintains the correct asset ratios in line with the current balance and strategy allocations. -#### Functions +### Functions - `assets()`: Returns the assets addresses and amount of each of them in the DeFindex (and hence its current ratio). `[[adress0, amount0], [address1, amount1]]`. TODO: Separate in 2 functions. - `withdraw_from_strategies`: Allows the Manager to withdraw assets from one or more strategies, letting them as IDLE funds. @@ -99,58 +99,55 @@ To ensure accurate representation of asset proportions, strategies are required Then, a rebalance execution will withdraw assets from the strategies, swap them, and invest them back in the strategies. - `emergency_withdraw`: Allows the Emergency Manager to withdraw all assets from a specific Strategy. As arguments, it receives the the address of a Strategy. It also turns off the strategy. -### Emergency Management +## Emergency Management The Emergency Manager has the authority to withdraw assets from the DeFindex in case of an emergency. This role is designed to protect users' assets in the event of a critical situation, such as a hack of a underlying protocol or a if a strategy gets unhealthy. The Emergency Manager can withdraw assets from the Strategy and store them as IDLE funds inside the Vault until the situation is resolved. -### Management +## Management Every DeFindex has a manager, who is responsible for managing the DeFindex. The Manager can ebalance the Vault, and invest IDLE funds in strategies. +## Fees -### Fee Structure, Collection, and Distribution +### Fee Receivers +The DeFindex protocol defines two distinct fee receivers to reward both the creators of the DeFindex Protocol and the deployers of individual Vaults: -#### Fee Receivers +1. **DeFindex Protocol Fee Receiver**: Receives a fixed protocol fee of 0.5% APR. +2. **Vault Fee Receiver**: Receives a fee set by the vault deployer, typically recommended between 0.5% and 2% APR. -The DeFindex protocol defines two distinct fee receivers to reward both the creators of the DeFindex Protocol and the deployers of individual vaults: - -1. DeFindex Protocol Fee Receiver: Receives a fixed protocol fee of 0.5% APR. -2. Vault Fee Receiver: Receives a fee set by the vault deployer, typically recommended between 0.5% and 2% APR. - -The Total Management Fee is the sum of these two fees. Thus, each vault has a total APR fee rate $f_{\text{total}}$ such that: - -$f_{\text{total}} = f_{\text{DeFindex}} + f_{\text{Vault}}$ +The Total Management Fee consists of both the protocol fee and the vault fee. Thus, each Vault has a total APR fee rate $f_{\text{total}}$ such that: +$$ +f_{\text{total}} = f_{\text{DeFindex}} + f_{\text{Vault}} +$$ -where $f_{\text{DeFindex}} = 0.5\%$ (fixed) and $f_{\text{Vault}}$ is a variable APR, typically between 0.5% and 2%. +where $f_{\text{DeFindex}} = 0.5\%$ is a fixed `defindex_fee` that goes to the DeFindex Protocol Fee Receiver address, and $f_{\text{Vault}}$ is a variable APR `vault_fee`, typically between 0.5% and 2%, that goes to the Vault Fee Receiver address. -#### Fee Collection Methodology +### Fee Collection Methodology -The fee collection process mints new shares, or dfTokens, representing the management fees. These shares are calculated based on the elapsed time since the last fee assessment, ensuring fees are accrued in alignment with the actual period of asset management. The fee collection is triggered whenever there is a vault interaction, such as a deposit or withdrawal with calculations based on the time elapsed since the last fee assessment. +The fee collection process mints new shares, or dfTokens, to cover the accrued management fees. These shares are calculated based on the elapsed time since the last fee assessment, ensuring fees are accrued based on the actual period of asset management. The fee collection is triggered whenever there is a vault interaction, such as a `deposit`, `withdrawal`, or even an explicit `fee_collection` call, with calculations based on the time elapsed since the last fee assessment. -#### Mathematical Derivation of New Fees +### Mathematical Derivation of New Fees Let: - $V_0$ be the Total Value Locked (TVL) at the last assessment, -- $V_1$ be the Total Value Locked (TVL) at the current assessment, just before any deposits or withdrawals, - $s_0$ be the Total Shares (dfTokens) at the last assessment, -- $s_f$ be the Total Shares (dfTokens) minted for fee distribution, - $f_{\text{total}}$ be the Total Management Fee (APR). -- $Pps_1$ be the Price per Share (dfToken) at the current assessment, - -Over a time period $\Delta t$ , the fees due for collection are derived by the value equivalent in shares. -To mint new shares for fee distribution, we calculate the required number of new shares, $s_f$, that correspond to the management fee over the elapsed period. +Over a time period $\Delta t$, the fees due for collection are derived as a value represented by newly minted shares. -After a period $\Delta t$ (expressed in seconds), the total shares $s_1$ should be: +To mint new shares for fee distribution, we calculate the required number of new shares, $s_f$, that correspond to the total management fee over the elapsed period. -$s_1 = s_0 + s_f$ - -We establish the following condition to ensure the minted shares represent the accrued fee: +After a period $\Delta t$ (expressed in seconds), and after the fee collection process the new total shares $s_1$ should be: $$ -Pps_1 \times s_f = V_1 \times f_{\text{total}} \times \frac{\Delta t}{\text{SECONDS PER YEAR}} +s_1 = s_0 + s_f $$ + +Since `fee_collection` is always called before any `deposit` or `withdrawal`, we assume that the Total Value $V_1$ remains equal to $V_0$. + +We establish the following condition to ensure the number of minted shares accurately reflects the management fee accrued over $\Delta t$. The value of the new minted shares $s_f$ should equal the prorated APR fee share of the total value of the vault. In mathematical terms: + $$ -\frac{V_1}{s_1} \times s_f = V_1 \times f_{\text{total}} \times \frac{\Delta t}{\text{SECONDS PER YEAR}} +\frac{V_0}{s_1} \times s_f = V_0 \times f_{\text{total}} \times \frac{\Delta t}{\text{SECONDS PER YEAR}} $$ Rearranging terms, we get: @@ -159,22 +156,88 @@ $$ s_f = \frac{f_{\text{total}} \times s_0 \times \Delta t}{\text{SECONDS PER YEAR} - f_{\text{total}} \times \Delta t} $$ -This equation gives the precise share quantity $s_f$ to mint as dfTokens for the management fee over the period $\Delta t$. - -#### Distribution of Fees +This equation gives the precise quantity of new shares $s_f$ to mint as dfTokens for the management fee over the period $\Delta t$. -Once the total fees, $s_f$ , are calculated, the shares are split proportionally between the DeFindex Protocol Fee Receiver and the Vault Fee Receiver. This is done by calculating the ratio of each fee receiver’s APR to the total APR: +### Distribution of Fees -$s_{\text{DeFindex}} = \frac{s_f \times f_{\text{DeFindex}}}{f_{\text{total}}}$ +Once the total fees, $s_f$, are calculated, the shares are split proportionally between the DeFindex Protocol Fee Receiver and the Vault Fee Receiver. This is done by calculating the ratio of each fee receiver’s APR to the total APR: +$$ +s_{\text{DeFindex}} = \frac{s_f \times f_{\text{DeFindex}}}{f_{\text{total}}} +$$ -$s_{\text{Vault}} = s_f - s_{\text{DeFindex}}$ +$$ +s_{\text{Vault}} = s_f - s_{\text{DeFindex}} +$$ This ensures that each fee receiver is allocated their respective share of dfTokens based on their fee contribution to $f_{\text{total}}$. The dfTokens are then minted to each receiver’s address as a direct representation of the fees collected. -#### Fee Calculation Efficiency -This calculation is performed only upon each vault interaction and considers only the time elapsed since the last assessment. This approach minimizes computational overhead and gas costs, ensuring that fee collection remains efficient. +### Example + +Suppose a DeFindex vault begins with an initial value of 1 USDC per share and a total of 100 shares (dfTokens), representing an investment of 100 USDC. This investment is placed in a lending protocol with an 8% APY. The DeFindex protocol has a total management fee of 1% APR, split between a 0.5% protocol fee and a 0.5% vault fee. + +After one year, the investment grows to 108 USDC due to the 8% APY. + +#### Step 1: Calculate the Shares to Mint for Fees + +Using the formula: + +$ +s_f = \frac{f_{\text{total}} \times s_0 \times \Delta t}{\text{SECONDS PER YEAR} - f_{\text{total}} \times \Delta t} +$ + +where: +- \( f_{\text{total}} = 0.01 \) (1% APR management fee), +- \( s_0 = 100 \) (initial shares), +- \( \Delta t = \text{SECONDS PER YEAR} \) (since this example spans a full year), + +we calculate \( s_f \), the number of shares to mint for the fee collection. + +Substituting values: + +$ +s_f = \frac{0.01 \times 100 \times \text{SECONDS PER YEAR}}{\text{SECONDS PER YEAR} - (0.01 \times \text{SECONDS PER YEAR})} +$ + +Simplifying: + +$ +s_f = \frac{1 \times \text{SECONDS PER YEAR}}{0.99 \times \text{SECONDS PER YEAR}} \approx 1.0101 +$ + +Thus, approximately 1.01 dfTokens are minted as fees. + +#### Step 2: Update Total Shares and Calculate Price per Share + +With the fee tokens minted, the total dfTokens increase from 100 to 101.01. + +The vault now holds 108 USDC backing 101.01 dfTokens, so the new price per share is: + +$ +\text{Price per Share} = \frac{108}{101.01} \approx 1.069 \, \text{USDC} +$ + +#### Step 3: Determine the Value for a User Holding 100 dfTokens + +For a user holding 100 dfTokens, the value of their holdings after one year is approximately: + +$ +100 \, \text{dfTokens} \times 1.069 \, \text{USDC per share} = 106.9 \, \text{USDC} +$ + +The remaining 1.01 dfTokens represent the collected fee, backed by around: + +$ +1.01 \, \text{dfTokens} \times 1.069 \, \text{USDC per share} \approx 1.08 \, \text{USDC} +$ + +--- + +This breakdown clarifies how the investment grows and the management fee is deducted by minting new dfTokens, resulting in a proportional share value for both users and fee recipients. + + +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.