From 4ef4360668a5c690220bbaf33fd33667c8aeca73 Mon Sep 17 00:00:00 2001 From: esteblock Date: Tue, 5 Nov 2024 11:34:06 -0300 Subject: [PATCH 01/39] minted shares formula --- .../02-contracts/01-vault-contract.md | 94 ++++++++++++++++++- 1 file changed, 89 insertions(+), 5 deletions(-) diff --git a/apps/docs/10-whitepaper/03-the-defindex-approach/02-contracts/01-vault-contract.md b/apps/docs/10-whitepaper/03-the-defindex-approach/02-contracts/01-vault-contract.md index 47992ad7..f08ec9ba 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 @@ -105,14 +105,98 @@ The Emergency Manager has the authority to withdraw assets from the DeFindex in ### 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. -### Fee Collection -The revenues generated by the strategies are collected as shares of the DeFindex. These shares, or dfTokens, are minted whenever a deposit or withdrawal takes place. +### Fees: Receivers, Structure and Collection -The deployer can set a management fee, which can later be adjusted by the Fee Receiver. Additionally, **palta**labs charges a 0.5% APR on the vault shares. +**Fee Receivers:** Every DeFindex user will pay a Management Fee for the total amount of funds managed by the DeFindex Vault. These fees will finally be received by two different fee receivers: -The recommended initial setup suggests a fee of **0.5%-2% APR on these shares**. For example, if a DeFindex has 100 shares and the fee is set at 0.5% APR, the fees collected annually would be 1 share, split as 0.5 for the Fee Receiver and 0.5 for **palta**labs. The **palta**labs fee is defined in the Factory contract. +1. **DeFindex Protocol Fee Receiver**. For creating the protocol. +2. **Vault Fee Receiver**. For creating the vault. -These allocations are recalculated and minted whenever a user deposits, withdraws from the DeFindex, or when rebalancing occurs. +**Fee Structure:** +The Total Management fee is the sum of **DeFindex Fee** and the **Vault Fee**. This sum can be from 0.5% to 2.5%, and it is defined as follows: + +1. **The DeFindex Fee** is fixed for all DeFindex Vaults, and this fee is fixed to 0.5% APR. This fee is fixed in the DeFindex Factory Contract. +2. **The Vault Fee** is set by the Vault Deployer. The recommended initial setup suggests a fee of **0.5% - 2% APR**. + +**Fee Collection:** +In order for a vault to collect fees, the DeFindex vault will mint and collect new shares (or dfTokens) whenever a deposit or withdrawal takes place. + +**Mathematical Explanation** +Lets consider $V_0$, total value locked at $t_0$, total shares $s_0$ at $t_0$ and total management fee of $f$. +This fee $f$ its considered as APR. + +After one period (one year), new shares $s_f$ should be minted in order to pay this fee. + +$s_1 = s_0 + s_f$ + +$V_1 =V_0$ as we consider that no new funds where added + +$s_f$ should comply with: + +The value of the mited share $\frac{V_0}{s_1} * s_f$ should be equal of the fraction of the total value locked $V_0 * f$ + +$\frac{V_0}{s_1} * s_f = V_0 * f$ + +so this gets simplified + +$s_f = f \cdot s_1 = f \cdot (s_0 + s_f)$ + +$s_f = \frac{f \cdot s_0}{(1-f)}$ + + + + + +**Example:** +For example, if a Vault has 100 shares and the Vault Fee is set at 0.5% APR, the annually collected fees would be 1 share, splited as 0.5 for the Fee Receiver and 0.5 for the DeFindex Protocol. + +total funds:100 (does not change) +previous shares: 1 +Y≈1.0101 + +100/(100+Y) *Y = 1 USDC +total funds/(previous total shars+fee shares) *fee shares = fee rate of total funds + + + + +t0 amount0 +t1 amount0 cobra fee por el tiempo (t1-t0) y se suma amount1 +t2 (amount0 y amount 1) cobra fee por el tiempo (t2-t1) + +100 shares, 100 USDC, price per share = 100/100 = 1 + +despues de un año, deposito de 100. +sin fee: se mintean 100 shares +new shares: 200, new funds: 200 USDC + +con fee: se mintean X shares al depositor, Y shares al fee receiver +total shares: (100 + X + Y) +total funds: (200 USDC) +value of fee shares (Y) +200/(100+X+Y) *Y = 1 USDC +200/(100+X+Y) *X = 100 USDC +X=101.0101 +Y=1.0101 + +si no se deposita nada +100/(100+Y) *Y = 1 USDC +100/(100+X+Y) *X = 100 USDC + + +300/(100+X+Y) *Y = 1 USDC +300/(100+X+Y) *X = 200 USDC + + + + +mint x shares, total new shares = (100+x) +my value of this is x/(100+x) = 1% + + +101 shares, 1 share is equal to 1/101 + +These allocations are recalculated and minted whenever a user deposits, withdraws from the DeFindex. For instance, let's say a DeFindex starts with an initial value of 1 USDC per share and 100 shares (dfTokens). These 100 USDC are invested in a lending protocol offering an 8% APY. The DeFindex also has a 0.5% APR fee. After one year, the investment grows to 108 USDC. Additionally, 1 dfToken is minted as a fee. This means the DeFindex now holds 101 dfTokens backed by 108 USDC, making the price per share approximately 1.07 USDC. As a result, a user holding 100 dfTokens will have a value equivalent to around 107 USDC, while the collected fee will be backed by about 1.07 USDC. From feda583bf5f4ce52356f4697c80e5e28f57acafb Mon Sep 17 00:00:00 2001 From: Matias Poblete <86752543+MattPoblete@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:51:31 -0300 Subject: [PATCH 02/39] =?UTF-8?q?=E2=AC=86=EF=B8=8FUpdate=20jest=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/dapp/jest.config.js | 4 ++++ apps/dapp/package.json | 3 ++- yarn.lock | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/dapp/jest.config.js b/apps/dapp/jest.config.js index afde621b..b9d2cd1a 100644 --- a/apps/dapp/jest.config.js +++ b/apps/dapp/jest.config.js @@ -1,3 +1,4 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} **/ const nextJest = require("next/jest"); const createJestConfig = nextJest({ @@ -9,6 +10,9 @@ const createJestConfig = nextJest({ const customJestConfig = { setupFilesAfterEnv: ["/jest.setup.js"], testEnvironment: "jsdom", + transform: { + "^.+.tsx?$": ["ts-jest",{}], + } }; // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async diff --git a/apps/dapp/package.json b/apps/dapp/package.json index dfbdcd52..5e1f6f34 100644 --- a/apps/dapp/package.json +++ b/apps/dapp/package.json @@ -30,7 +30,8 @@ "react": "19.0.0-rc-f994737d14-20240522", "react-dom": "19.0.0-rc-f994737d14-20240522", "react-icons": "^5.3.0", - "react-redux": "^9.1.2" + "react-redux": "^9.1.2", + "ts-jest": "^29.2.5" }, "devDependencies": { "@chakra-ui/cli": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index f39f2dcc..301d5c23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15909,7 +15909,7 @@ ts-jest@^25.3.1: semver "6.x" yargs-parser "18.x" -ts-jest@^29.1.2: +ts-jest@^29.1.2, ts-jest@^29.2.5: version "29.2.5" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.2.5.tgz#591a3c108e1f5ebd013d3152142cb5472b399d63" integrity sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA== From e0d9ace2db3c6e5163368882328c545723d0d7f9 Mon Sep 17 00:00:00 2001 From: Matias Poblete <86752543+MattPoblete@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:52:05 -0300 Subject: [PATCH 03/39] =?UTF-8?q?=E2=9C=A8Add=20workflow=20to=20run=20test?= =?UTF-8?q?=20on=20pr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/run-tests.yml | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/run-tests.yml diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 00000000..43c73b59 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,58 @@ +name: Run dapp tests + +on: + pull_request: + workflow_dispatch: +permissions: + contents: write + pull-requests: write + issues: read + packages: none + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + - uses: actions/cache@v3 + with: + path: | + node_modules + key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} + + - name: Set up Node.js + uses: actions/setup-node@v4.0.2 + with: + node-version: '20.18.0' + + - name: Install dependencies + run: yarn install + + - name: Build app + run: yarn build --filter dapp + + run-tests: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4.1.1 + - uses: actions/cache@v3 + with: + path: | + node_modules + key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} + + - name: Set up Node.js + uses: actions/setup-node@v4.0.2 + with: + node-version: '20.18.0' + + - name: navigate to the dapp directory + run: cd apps/dapp + + - name: Run jest tests + run: yarn exec jest \ No newline at end of file From b746908406db7b810364c6d0e5b48f8ec9b04cb9 Mon Sep 17 00:00:00 2001 From: Matias Poblete <86752543+MattPoblete@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:10:07 -0300 Subject: [PATCH 04/39] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Updated=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/run-tests.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 43c73b59..7d17046f 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -51,8 +51,7 @@ jobs: with: node-version: '20.18.0' - - name: navigate to the dapp directory - run: cd apps/dapp - - - name: Run jest tests - run: yarn exec jest \ No newline at end of file + - name: Run jest tests in dapp directory + run: | + cd apps/dapp + yarn test From e1e9d500ab69074f84389d737fda84e64d1f13a9 Mon Sep 17 00:00:00 2001 From: coderipper Date: Tue, 5 Nov 2024 13:09:57 -0300 Subject: [PATCH 05/39] separated fees for defindex and vault --- apps/contracts/defindex/src/fee.rs | 40 +++++++++++-------- apps/contracts/defindex/src/interface.rs | 4 +- apps/contracts/defindex/src/lib.rs | 19 ++++----- apps/contracts/defindex/src/storage.rs | 6 +-- apps/contracts/defindex/src/utils.rs | 2 +- apps/contracts/factory/src/events.rs | 22 +++++------ apps/contracts/factory/src/lib.rs | 50 ++++++++++++------------ apps/contracts/factory/src/storage.rs | 2 +- apps/contracts/src/test.ts | 2 +- 9 files changed, 78 insertions(+), 69 deletions(-) diff --git a/apps/contracts/defindex/src/fee.rs b/apps/contracts/defindex/src/fee.rs index 3d2e765d..6fe55464 100644 --- a/apps/contracts/defindex/src/fee.rs +++ b/apps/contracts/defindex/src/fee.rs @@ -1,19 +1,20 @@ use soroban_sdk::{Address, Env, Map, Symbol, Vec}; -use crate::{access::AccessControl, constants::{MAX_BPS, SECONDS_PER_YEAR}, events, funds::fetch_total_managed_funds, storage::{get_defindex_receiver, get_factory, get_last_fee_assesment, get_vault_share, set_last_fee_assesment}, token::internal_mint, utils::calculate_dftokens_from_asset_amounts, ContractError}; +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}; /// Fetches the current fee rate from the factory contract. /// The fee rate is expressed in basis points (BPS). -fn fetch_fee_rate(e: &Env) -> u32 { +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, "fee_rate"), + &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 @@ -34,27 +35,33 @@ fn calculate_fees(e: &Env, time_elapsed: u64, fee_rate: u32) -> Result Result<(), ContractError> { let current_timestamp = e.ledger().timestamp(); - let last_fee_assessment = get_last_fee_assesment(e); + let last_fee_assessment = get_last_fee_assesment(e); let time_elapsed = current_timestamp.checked_sub(last_fee_assessment).unwrap(); + // If no time has passed since the last fee assessment, no fees are collected if time_elapsed == 0 { return Ok(()); } - let fee_rate = fetch_fee_rate(e); + // Fetch the individual fees for DeFindex and Vault, then calculate the total rate + let defindex_fee = fetch_defindex_fee(e); + let vault_fee = get_vault_fee(e); + let total_fee_rate = defindex_fee.checked_add(vault_fee).unwrap(); - let total_fees = calculate_fees(e, time_elapsed, fee_rate)?; + // Calculate the total fees in dfTokens based on the combined fee rate + let total_fees = calculate_fees(e, time_elapsed, total_fee_rate)?; - // Mint the total fees as dfTokens - mint_fees(e, total_fees)?; + // Mint and distribute the fees proportionally + mint_fees(e, total_fees, defindex_fee, vault_fee)?; // Update the last fee assessment timestamp set_last_fee_assesment(e, ¤t_timestamp); @@ -62,18 +69,19 @@ pub fn collect_fees(e: &Env) -> Result<(), ContractError> { Ok(()) } -fn mint_fees(e: &Env, total_fees: i128) -> Result<(), ContractError> { +/// Mints dfTokens for fees and distributes them to the vault fee receiver and DeFindex receiver. +fn mint_fees(e: &Env, total_fees: i128, defindex_fee: u32, vault_fee: u32) -> Result<(), ContractError> { let access_control = AccessControl::new(&e); - + let vault_fee_receiver = access_control.get_fee_receiver()?; let defindex_receiver = get_defindex_receiver(e); - let vault_share_bps = get_vault_share(e); - - let vault_shares = (total_fees * vault_share_bps as i128) / MAX_BPS; - - let defindex_shares = total_fees - vault_shares; + // Calculate shares for each receiver based on their fee proportion + let total_fee_bps = defindex_fee as i128 + vault_fee as i128; + let defindex_shares = (total_fees * defindex_fee as i128) / total_fee_bps; + let vault_shares = total_fees - defindex_shares; + // Mint shares for both receivers internal_mint(e.clone(), vault_fee_receiver.clone(), vault_shares); internal_mint(e.clone(), defindex_receiver.clone(), defindex_shares); diff --git a/apps/contracts/defindex/src/interface.rs b/apps/contracts/defindex/src/interface.rs index f3873a90..65538e17 100644 --- a/apps/contracts/defindex/src/interface.rs +++ b/apps/contracts/defindex/src/interface.rs @@ -18,7 +18,7 @@ pub trait VaultTrait { /// * `manager` - The address responsible for managing the vault. /// * `emergency_manager` - The address with emergency control over the vault. /// * `fee_receiver` - The address that will receive fees from the vault. - /// * `vault_share` - The percentage of the vault's fees that will be sent to the DeFindex receiver. in BPS. + /// * `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. /// @@ -30,7 +30,7 @@ pub trait VaultTrait { manager: Address, emergency_manager: Address, fee_receiver: Address, - vault_share: u32, + vault_fee: u32, defindex_receiver: Address, factory: Address, vault_name: String, diff --git a/apps/contracts/defindex/src/lib.rs b/apps/contracts/defindex/src/lib.rs index 044b1470..8f328d20 100755 --- a/apps/contracts/defindex/src/lib.rs +++ b/apps/contracts/defindex/src/lib.rs @@ -28,7 +28,7 @@ use funds::{fetch_current_idle_funds, fetch_current_invested_funds, fetch_total_ use interface::{AdminInterfaceTrait, VaultTrait, VaultManagementTrait}; use models::{ActionType, AssetAllocation, Instruction, Investment, OptionalSwapDetailsExactIn, OptionalSwapDetailsExactOut}; use storage::{ - get_assets, set_asset, set_defindex_receiver, set_factory, set_last_fee_assesment, set_total_assets, set_vault_share + get_assets, set_asset, set_defindex_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 token::{internal_mint, internal_burn, write_metadata, VaultToken}; @@ -54,7 +54,7 @@ impl VaultTrait for DeFindexVault { /// * `manager` - The address responsible for managing the vault. /// * `emergency_manager` - The address with emergency control over the vault. /// * `fee_receiver` - The address that will receive fees from the vault. - /// * `vault_share` - The percentage of the vault's fees that will be sent to the DeFindex receiver. in BPS. + /// * `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. /// @@ -66,7 +66,7 @@ impl VaultTrait for DeFindexVault { manager: Address, emergency_manager: Address, fee_receiver: Address, - vault_share: u32, + vault_fee: u32, defindex_receiver: Address, factory: Address, vault_name: String, @@ -82,7 +82,7 @@ impl VaultTrait for DeFindexVault { access_control.set_role(&RolesDataKey::Manager, &manager); // Set Vault Share (in basis points) - set_vault_share(&e, &vault_share); + set_vault_fee(&e, &vault_fee); // Set Paltalabs Fee Receiver set_defindex_receiver(&e, &defindex_receiver); @@ -152,6 +152,9 @@ impl VaultTrait for DeFindexVault { set_last_fee_assesment(&e, &e.ledger().timestamp()); } + // fees assesment + collect_fees(&e)?; + // get assets let assets = get_assets(&e); // assets lenght should be equal to amounts_desired and amounts_min length @@ -204,8 +207,6 @@ impl VaultTrait for DeFindexVault { events::emit_deposit_event(&e, from, amounts, shares_to_mint); - // fees assesment - collect_fees(&e)?; // TODO return amounts and shares to mint Ok(()) } @@ -227,6 +228,9 @@ impl VaultTrait for DeFindexVault { check_initialized(&e)?; check_nonnegative_amount(df_amount)?; from.require_auth(); + + // fees assesment + collect_fees(&e)?; // Check if the user has enough dfTokens let df_user_balance = VaultToken::balance(e.clone(), from.clone()); @@ -292,9 +296,6 @@ impl VaultTrait for DeFindexVault { } events::emit_withdraw_event(&e, from, df_amount, amounts_withdrawn.clone()); - - // fees assesment - collect_fees(&e)?; Ok(amounts_withdrawn) } diff --git a/apps/contracts/defindex/src/storage.rs b/apps/contracts/defindex/src/storage.rs index 525e2b81..4f873b5b 100644 --- a/apps/contracts/defindex/src/storage.rs +++ b/apps/contracts/defindex/src/storage.rs @@ -82,13 +82,13 @@ pub fn get_last_fee_assesment(e: &Env) -> u64 { } // Vault Share -pub fn set_vault_share(e: &Env, vault_share: &u32) { +pub fn set_vault_fee(e: &Env, vault_fee: &u32) { e.storage() .instance() - .set(&DataKey::VaultShare, vault_share); + .set(&DataKey::VaultShare, vault_fee); } -pub fn get_vault_share(e: &Env) -> u32 { +pub fn get_vault_fee(e: &Env) -> u32 { e.storage() .instance() .get(&DataKey::VaultShare) diff --git a/apps/contracts/defindex/src/utils.rs b/apps/contracts/defindex/src/utils.rs index 3fab13cf..aa215ee3 100644 --- a/apps/contracts/defindex/src/utils.rs +++ b/apps/contracts/defindex/src/utils.rs @@ -79,9 +79,9 @@ pub fn calculate_asset_amounts_for_dftokens( pub fn calculate_dftokens_from_asset_amounts( env: &Env, asset_amounts: Map, // The input asset amounts + total_managed_funds: Map, // The total managed funds for each asset ) -> Result { let total_supply = VaultToken::total_supply(env.clone()); // Total dfToken supply - let total_managed_funds = fetch_total_managed_funds(&env); // Fetch all managed assets // Initialize the minimum dfTokens corresponding to each asset let mut min_df_tokens: Option = None; diff --git a/apps/contracts/factory/src/events.rs b/apps/contracts/factory/src/events.rs index 479cc43a..c0c23b03 100644 --- a/apps/contracts/factory/src/events.rs +++ b/apps/contracts/factory/src/events.rs @@ -8,14 +8,14 @@ use crate::defindex::AssetAllocation; pub struct InitializedEvent { pub admin: Address, pub defindex_receiver: Address, - pub fee_rate: u32, + pub defindex_fee: u32, } -pub(crate) fn emit_initialized(e: &Env, admin: Address, defindex_receiver: Address, fee_rate: u32) { +pub(crate) fn emit_initialized(e: &Env, admin: Address, defindex_receiver: Address, defindex_fee: u32) { let event: InitializedEvent = InitializedEvent { admin, defindex_receiver, - fee_rate, + defindex_fee, }; e.events() .publish(("DeFindexFactory", symbol_short!("init")), event); @@ -28,7 +28,7 @@ pub struct CreateDeFindexEvent { pub emergency_manager: Address, pub fee_receiver: Address, pub manager: Address, - pub vault_share: u32, + pub vault_fee: u32, pub assets: Vec } @@ -38,14 +38,14 @@ pub(crate) fn emit_create_defindex_vault( emergency_manager: Address, fee_receiver: Address, manager: Address, - vault_share: u32, + vault_fee: u32, assets: Vec, ) { let event = CreateDeFindexEvent { emergency_manager, fee_receiver, manager, - vault_share, + vault_fee, assets, }; @@ -81,16 +81,16 @@ pub(crate) fn emit_new_defindex_receiver(e: &Env, new_defindex_receiver: Address .publish(("DeFindexFactory", symbol_short!("nreceiver")), event); } -// NEW FEE RATE EVENT +// NEW DEFINDEX FEE EVENT #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct NewFeeRateEvent { - pub new_fee_rate: u32, + pub new_defindex_fee: u32, } -pub(crate) fn emit_new_fee_rate(e: &Env, new_fee_rate: u32) { - let event = NewFeeRateEvent { new_fee_rate }; +pub(crate) fn emit_new_defindex_fee(e: &Env, new_defindex_fee: u32) { + let event = NewFeeRateEvent { new_defindex_fee }; e.events() - .publish(("DeFindexFactory", symbol_short!("nfee_rate")), event); + .publish(("DeFindexFactory", symbol_short!("n_fee")), event); } \ No newline at end of file diff --git a/apps/contracts/factory/src/lib.rs b/apps/contracts/factory/src/lib.rs index f02aa883..f29d9b70 100644 --- a/apps/contracts/factory/src/lib.rs +++ b/apps/contracts/factory/src/lib.rs @@ -10,7 +10,7 @@ use soroban_sdk::{ }; use error::FactoryError; use defindex::{create_contract, AssetAllocation}; -use storage::{ add_new_defindex, extend_instance_ttl, get_admin, get_defi_wasm_hash, get_defindex_receiver, get_deployed_defindexes, get_fee_rate, has_admin, put_admin, put_defi_wasm_hash, put_defindex_receiver, put_fee_rate }; +use storage::{ add_new_defindex, extend_instance_ttl, get_admin, get_defi_wasm_hash, get_defindex_receiver, get_deployed_defindexes, get_fee_rate, has_admin, put_admin, put_defi_wasm_hash, put_defindex_receiver, put_defindex_fee }; fn check_initialized(e: &Env) -> Result<(), FactoryError> { if !has_admin(e) { @@ -26,7 +26,7 @@ pub trait FactoryTrait { /// * `e` - The environment in which the contract is running. /// * `admin` - The address of the contract administrator, who can manage settings. /// * `defindex_receiver` - The default address designated to receive a portion of fees. - /// * `fee_rate` - The initial annual fee rate (in basis points). + /// * `defindex_fee` - The initial annual fee rate (in basis points). /// * `defindex_wasm_hash` - The hash of the DeFindex Vault's WASM file for deploying new vaults. /// /// # Returns @@ -35,7 +35,7 @@ pub trait FactoryTrait { e: Env, admin: Address, defindex_receiver: Address, - fee_rate: u32, + defindex_fee: u32, defindex_wasm_hash: BytesN<32> ) -> Result<(), FactoryError>; @@ -45,7 +45,7 @@ pub trait FactoryTrait { /// * `e` - The environment in which the contract is running. /// * `emergency_manager` - The address assigned emergency control over the vault. /// * `fee_receiver` - The address designated to receive fees from the vault. - /// * `vault_share` - The percentage share of fees allocated to the vault's fee receiver. + /// * `vault_fee` - The percentage share of fees allocated to the vault's fee receiver. /// * `vault_name` - The name of the vault. /// * `vault_symbol` - The symbol of the vault. /// * `manager` - The address assigned as the vault manager. @@ -58,7 +58,7 @@ pub trait FactoryTrait { e: Env, emergency_manager: Address, fee_receiver: Address, - vault_share: u32, + vault_fee: u32, vault_name: String, vault_symbol: String, manager: Address, @@ -72,7 +72,7 @@ pub trait FactoryTrait { /// * `e` - The environment in which the contract is running. /// * `emergency_manager` - The address assigned emergency control over the vault. /// * `fee_receiver` - The address designated to receive fees from the vault. - /// * `vault_share` - The percentage share of fees allocated to the vault's fee receiver. + /// * `vault_fee` - The percentage share of fees allocated to the vault's fee receiver. /// * `vault_name` - The name of the vault. /// * `vault_symbol` - The symbol of the vault. /// * `manager` - The address assigned as the vault manager. @@ -87,7 +87,7 @@ pub trait FactoryTrait { caller: Address, emergency_manager: Address, fee_receiver: Address, - vault_share: u32, + vault_fee: u32, vault_name: String, vault_symbol: String, manager: Address, @@ -126,7 +126,7 @@ pub trait FactoryTrait { /// /// # Returns /// * `Result<(), FactoryError>` - Returns Ok(()) if successful, or an error if not authorized. - fn set_fee_rate(e: Env, new_fee_rate: u32) -> Result<(), FactoryError>; + fn set_defindex_fee(e: Env, new_fee_rate: u32) -> Result<(), FactoryError>; // --- Read Methods --- @@ -164,7 +164,7 @@ pub trait FactoryTrait { /// /// # Returns /// * `Result` - Returns the fee rate in basis points or an error if not found. - fn fee_rate(e: Env) -> Result; + fn defindex_fee(e: Env) -> Result; } #[contract] @@ -179,7 +179,7 @@ impl FactoryTrait for DeFindexFactory { /// * `e` - The environment in which the contract is running. /// * `admin` - The address of the contract administrator, who can manage settings. /// * `defindex_receiver` - The default address designated to receive a portion of fees. - /// * `fee_rate` - The initial annual fee rate (in basis points). + /// * `defindex_fee` - The initial annual fee rate (in basis points). /// * `defindex_wasm_hash` - The hash of the DeFindex Vault's WASM file for deploying new vaults. /// /// # Returns @@ -188,7 +188,7 @@ impl FactoryTrait for DeFindexFactory { e: Env, admin: Address, defindex_receiver: Address, - fee_rate: u32, + defindex_fee: u32, defi_wasm_hash: BytesN<32> ) -> Result<(), FactoryError> { if has_admin(&e) { @@ -198,9 +198,9 @@ impl FactoryTrait for DeFindexFactory { put_admin(&e, &admin); put_defindex_receiver(&e, &defindex_receiver); put_defi_wasm_hash(&e, defi_wasm_hash); - put_fee_rate(&e, &fee_rate); + put_defindex_fee(&e, &defindex_fee); - events::emit_initialized(&e, admin, defindex_receiver, fee_rate); + events::emit_initialized(&e, admin, defindex_receiver, defindex_fee); extend_instance_ttl(&e); Ok(()) } @@ -211,7 +211,7 @@ impl FactoryTrait for DeFindexFactory { /// * `e` - The environment in which the contract is running. /// * `emergency_manager` - The address assigned emergency control over the vault. /// * `fee_receiver` - The address designated to receive fees from the vault. - /// * `vault_share` - The percentage share of fees allocated to the vault's fee receiver. + /// * `vault_fee` - The percentage share of fees allocated to the vault's fee receiver. /// * `manager` - The address assigned as the vault manager. /// * `assets` - A vector of `AssetAllocation` structs that define the assets managed by the vault. /// * `salt` - A salt used for ensuring unique addresses for each deployed vault. @@ -222,7 +222,7 @@ impl FactoryTrait for DeFindexFactory { e: Env, emergency_manager: Address, fee_receiver: Address, - vault_share: u32, + vault_fee: u32, vault_name: String, vault_symbol: String, manager: Address, @@ -243,7 +243,7 @@ impl FactoryTrait for DeFindexFactory { &manager, &emergency_manager, &fee_receiver, - &vault_share, + &vault_fee, &defindex_receiver, ¤t_contract, &vault_name, @@ -251,7 +251,7 @@ impl FactoryTrait for DeFindexFactory { ); add_new_defindex(&e, defindex_address.clone()); - events::emit_create_defindex_vault(&e, emergency_manager, fee_receiver, manager, vault_share, assets); + events::emit_create_defindex_vault(&e, emergency_manager, fee_receiver, manager, vault_fee, assets); Ok(defindex_address) } @@ -261,7 +261,7 @@ impl FactoryTrait for DeFindexFactory { /// * `e` - The environment in which the contract is running. /// * `emergency_manager` - The address assigned emergency control over the vault. /// * `fee_receiver` - The address designated to receive fees from the vault. - /// * `vault_share` - The percentage share of fees allocated to the vault's fee receiver. + /// * `vault_fee` - The percentage share of fees allocated to the vault's fee receiver. /// * `vault_name` - The name of the vault. /// * `vault_symbol` - The symbol of the vault. /// * `manager` - The address assigned as the vault manager. @@ -276,7 +276,7 @@ impl FactoryTrait for DeFindexFactory { caller: Address, emergency_manager: Address, fee_receiver: Address, - vault_share: u32, + vault_fee: u32, vault_name: String, vault_symbol: String, manager: Address, @@ -305,7 +305,7 @@ impl FactoryTrait for DeFindexFactory { &manager, &emergency_manager, &fee_receiver, - &vault_share, + &vault_fee, &defindex_receiver, ¤t_contract, &vault_name, @@ -324,7 +324,7 @@ impl FactoryTrait for DeFindexFactory { ); add_new_defindex(&e, defindex_address.clone()); - events::emit_create_defindex_vault(&e, emergency_manager, fee_receiver, manager, vault_share, assets); + events::emit_create_defindex_vault(&e, emergency_manager, fee_receiver, manager, vault_fee, assets); Ok(defindex_address) } @@ -376,14 +376,14 @@ impl FactoryTrait for DeFindexFactory { /// /// # Returns /// * `Result<(), FactoryError>` - Returns Ok(()) if successful, or an error if not authorized. - fn set_fee_rate(e: Env, fee_rate: u32) -> Result<(), FactoryError> { + fn set_defindex_fee(e: Env, defindex_fee: u32) -> Result<(), FactoryError> { check_initialized(&e)?; extend_instance_ttl(&e); let admin = get_admin(&e); admin.require_auth(); - put_fee_rate(&e, &fee_rate); - events::emit_new_fee_rate(&e, fee_rate); + put_defindex_fee(&e, &defindex_fee); + events::emit_new_defindex_fee(&e, defindex_fee); Ok(()) } @@ -435,7 +435,7 @@ impl FactoryTrait for DeFindexFactory { /// /// # Returns /// * `Result` - Returns the fee rate in basis points or an error if not found. - fn fee_rate(e: Env) -> Result { + fn defindex_fee(e: Env) -> Result { check_initialized(&e)?; extend_instance_ttl(&e); Ok(get_fee_rate(&e)) diff --git a/apps/contracts/factory/src/storage.rs b/apps/contracts/factory/src/storage.rs index 92589bb5..bde91cd0 100644 --- a/apps/contracts/factory/src/storage.rs +++ b/apps/contracts/factory/src/storage.rs @@ -99,7 +99,7 @@ pub fn get_defindex_receiver(e: &Env) -> Address { } // Fee Rate BPS (MAX BPS = 10000) -pub fn put_fee_rate(e: &Env, value: &u32) { +pub fn put_defindex_fee(e: &Env, value: &u32) { e.storage().instance().set(&DataKey::FeeRate, value); } diff --git a/apps/contracts/src/test.ts b/apps/contracts/src/test.ts index bb33af73..5abe8656 100644 --- a/apps/contracts/src/test.ts +++ b/apps/contracts/src/test.ts @@ -81,7 +81,7 @@ export async function test_factory(addressBook: AddressBook) { const createDeFindexParams: xdr.ScVal[] = [ new Address(emergencyManager.publicKey()).toScVal(), new Address(feeReceiver.publicKey()).toScVal(), - nativeToScVal(100, { type: "u32" }), // Setting vault_share as 100 bps for demonstration + nativeToScVal(100, { type: "u32" }), // Setting vault_fee as 100 bps for demonstration nativeToScVal("Test Vault", { type: "string" }), nativeToScVal("DFT-Test-Vault", { type: "string" }), new Address(manager.publicKey()).toScVal(), From 5def99f8a3328e3791cd7b9e864381054e480f14 Mon Sep 17 00:00:00 2001 From: coderipper Date: Tue, 5 Nov 2024 13:10:52 -0300 Subject: [PATCH 06/39] whitepaper fees --- .../02-contracts/01-vault-contract.md | 66 +++++++++++++++++-- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/apps/docs/10-whitepaper/03-the-defindex-approach/02-contracts/01-vault-contract.md b/apps/docs/10-whitepaper/03-the-defindex-approach/02-contracts/01-vault-contract.md index 47992ad7..e40955da 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 @@ -105,18 +105,70 @@ The Emergency Manager has the authority to withdraw assets from the DeFindex in ### 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. -### Fee Collection -The revenues generated by the strategies are collected as shares of the DeFindex. These shares, or dfTokens, are minted whenever a deposit or withdrawal takes place. +### Fee Structure, Collection, and Distribution -The deployer can set a management fee, which can later be adjusted by the Fee Receiver. Additionally, **palta**labs charges a 0.5% APR on the vault shares. +1. Fee Receivers -The recommended initial setup suggests a fee of **0.5%-2% APR on these shares**. For example, if a DeFindex has 100 shares and the fee is set at 0.5% APR, the fees collected annually would be 1 share, split as 0.5 for the Fee Receiver and 0.5 for **palta**labs. The **palta**labs fee is defined in the Factory contract. +The DeFindex protocol defines two distinct fee receivers to reward both the creators of the DeFindex Protocol and the deployers of individual vaults: -These allocations are recalculated and minted whenever a user deposits, withdraws from the DeFindex, or when rebalancing occurs. +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. -For instance, let's say a DeFindex starts with an initial value of 1 USDC per share and 100 shares (dfTokens). These 100 USDC are invested in a lending protocol offering an 8% APY. The DeFindex also has a 0.5% APR fee. After one year, the investment grows to 108 USDC. Additionally, 1 dfToken is minted as a fee. This means the DeFindex now holds 101 dfTokens backed by 108 USDC, making the price per share approximately 1.07 USDC. As a result, a user holding 100 dfTokens will have a value equivalent to around 107 USDC, while the collected fee will be backed by about 1.07 USDC. +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: -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. +$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%. + +2. 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. + +Let: + +- $V_0$ be the Total Value Locked (TVL) at the last assessment, +- $s_0$ be the Total Shares (dfTokens) at the last assessment, +- $f_{\text{total}}$ be the Total Management Fee (APR). + +Over a time period $\Delta t$ , the fees due for collection are derived by the value equivalent in shares. + +3. Mathematical Derivation of New Fees + +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. + +After a period $\Delta t$ (expressed in seconds), the total shares $s_1$ should be: + +$s_1 = s_0 + s_f$ + +The total value $V_1$ remains $V_0$, assuming no deposits or withdrawals during this period. + +We establish the following condition to ensure the minted shares represent the accrued fee: + +$\frac{V_0}{s_1} \times s_f = V_0 \times f_{\text{total}} \times \frac{\Delta t}{\text{SECONDS\_PER\_YEAR}}$ + +Rearranging terms, we get: + +$$ +s_f = \frac{f_{\text{total}} \times s_0 \times \Delta t}{\text{SECONDS\_PER\_YEAR} - f_{\text{total}} \times \Delta t} +$$ + +This equation gives the precise share quantity $s_f$ to mint as dfTokens for the management fee over the period $\Delta t$. + +4. Distribution of Fees + +Once the total fees, $s_f$ , are calculated, the shares are split proportionally between the DeFindex Protocol Fee Receiver and the Vault Fee Receiver. This is done by calculating the ratio of each fee receiver’s APR to the total APR: + +$s_{\text{DeFindex}} = \frac{s_f \times f_{\text{DeFindex}}}{f_{\text{total}}}$ + + +$s_{\text{Vault}} = s_f - s_{\text{DeFindex}}$ + +This ensures that each fee receiver is allocated their respective share of dfTokens based on their fee contribution to $f_{\text{total}}$. The dfTokens are then minted to each receiver’s address as a direct representation of the fees collected. + +5. 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.