diff --git a/apps/contracts/Cargo.lock b/apps/contracts/Cargo.lock index e656b7b..4226274 100644 --- a/apps/contracts/Cargo.lock +++ b/apps/contracts/Cargo.lock @@ -97,6 +97,7 @@ name = "blend_strategy" version = "0.1.0" dependencies = [ "defindex-strategy-core", + "soroban-fixed-point-math", "soroban-sdk", ] @@ -1114,6 +1115,15 @@ dependencies = [ "syn", ] +[[package]] +name = "soroban-fixed-point-math" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d386a1ca0a148121b21331f9da68f33bf3dfb6de69646f719935d2dec3d49c" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "soroban-ledger-snapshot" version = "21.7.6" diff --git a/apps/contracts/strategies/blend/Cargo.toml b/apps/contracts/strategies/blend/Cargo.toml index cf1a7b2..15acfa0 100644 --- a/apps/contracts/strategies/blend/Cargo.toml +++ b/apps/contracts/strategies/blend/Cargo.toml @@ -13,6 +13,7 @@ crate-type = ["cdylib"] [dependencies] soroban-sdk = { workspace = true } defindex-strategy-core = { workspace = true } +soroban-fixed-point-math = "1.2.0" [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/apps/contracts/strategies/blend/src/blend_pool.rs b/apps/contracts/strategies/blend/src/blend_pool.rs index b1ad419..1f8c324 100644 --- a/apps/contracts/strategies/blend/src/blend_pool.rs +++ b/apps/contracts/strategies/blend/src/blend_pool.rs @@ -1,10 +1,11 @@ -use soroban_sdk::{auth::{ContractContext, InvokerContractAuthEntry, SubContractInvocation}, vec, Address, Env, IntoVal, Symbol, Vec}; +use defindex_strategy_core::StrategyError; +use soroban_sdk::{auth::{ContractContext, InvokerContractAuthEntry, SubContractInvocation}, panic_with_error, token::TokenClient, vec, Address, Env, IntoVal, Symbol, Vec}; -use crate::storage::get_blend_pool; +use crate::storage::Config; soroban_sdk::contractimport!( - file = "../external_wasms/blend/blend_pool.wasm" -); + file = "../external_wasms/blend/blend_pool.wasm" + ); pub type BlendPoolClient<'a> = Client<'a>; // Define the RequestType enum with explicit u32 values @@ -30,13 +31,19 @@ impl RequestType { } } -pub fn supply(e: &Env, from: &Address, underlying_asset: Address, amount: i128) -> Positions { - let blend_pool_address = get_blend_pool(e); - let blend_pool_client = BlendPoolClient::new(e, &blend_pool_address); +pub fn supply(e: &Env, from: &Address, amount: &i128, config: &Config) -> i128 { + let pool_client = BlendPoolClient::new(e, &config.pool); + + // Get deposit amount pre-supply + let pre_supply = pool_client + .get_positions(&e.current_contract_address()) + .supply + .get(config.reserve_id) + .unwrap_or(0); let requests: Vec = vec![&e, Request { - address: underlying_asset.clone(), - amount, + address: config.asset.clone(), + amount: amount.clone(), request_type: RequestType::Supply.to_u32(), }]; @@ -44,58 +51,67 @@ pub fn supply(e: &Env, from: &Address, underlying_asset: Address, amount: i128) &e, InvokerContractAuthEntry::Contract(SubContractInvocation { context: ContractContext { - contract: underlying_asset.clone(), + contract: config.asset.clone(), fn_name: Symbol::new(&e, "transfer"), args: ( e.current_contract_address(), - blend_pool_address.clone(), + config.pool.clone(), amount.clone()).into_val(e), }, sub_invocations: vec![&e], }), ]); - blend_pool_client.submit( - &from, + let new_positions = pool_client.submit( + &e.current_contract_address(), &e.current_contract_address(), &from, &requests - ) + ); + + // Calculate the amount of bTokens received + let b_tokens_amount = new_positions.supply.get_unchecked(config.reserve_id) - pre_supply; + b_tokens_amount } -pub fn withdraw(e: &Env, from: &Address, underlying_asset: Address, amount: i128) -> Positions { - let blend_pool_address = get_blend_pool(e); - let blend_pool_client = BlendPoolClient::new(e, &blend_pool_address); +pub fn withdraw(e: &Env, from: &Address, amount: &i128, config: &Config) -> (i128, i128) { + let pool_client = BlendPoolClient::new(e, &config.pool); + + let pre_supply = pool_client + .get_positions(&e.current_contract_address()) + .supply + .get(config.reserve_id) + .unwrap_or_else(|| panic_with_error!(e, StrategyError::InsufficientBalance)); + + // Get balance pre-withdraw, as the pool can modify the withdrawal amount + let pre_withdrawal_balance = TokenClient::new(&e, &config.asset).balance(&from); let requests: Vec = vec![&e, Request { - address: underlying_asset.clone(), - amount, + address: config.asset.clone(), + amount: amount.clone(), request_type: RequestType::Withdraw.to_u32(), }]; - let new_positions = blend_pool_client.submit( - &from, - &from, + // Execute the withdrawal - the tokens are transferred from the pool to the vault + let new_positions = pool_client.submit( + &e.current_contract_address(), + &e.current_contract_address(), &from, &requests ); - new_positions + // Calculate the amount of tokens withdrawn and bTokens burnt + let post_withdrawal_balance = TokenClient::new(&e, &config.asset).balance(&from); + let real_amount = post_withdrawal_balance - pre_withdrawal_balance; + + // position entry is deleted if the position is cleared + let b_tokens_amount = pre_supply - new_positions.supply.get(config.reserve_id).unwrap_or(0); + (real_amount, b_tokens_amount) } -pub fn claim(e: &Env, from: &Address) -> i128 { - // Setting up Blend Pool client - let blend_pool_address = get_blend_pool(e); - let blend_pool_client = BlendPoolClient::new(e, &blend_pool_address); +pub fn claim(e: &Env, from: &Address, config: &Config) -> i128 { + let pool_client = BlendPoolClient::new(e, &config.pool); // TODO: Check reserve_token_ids and how to get the correct one - blend_pool_client.claim(from, &vec![&e, 3u32], from) -} - -pub fn get_positions(e: &Env, from: &Address) -> Positions { - // Setting up Blend Pool client - let blend_pool_address = get_blend_pool(e); - let blend_pool_client = BlendPoolClient::new(e, &blend_pool_address); - - blend_pool_client.get_positions(from) + pool_client.claim(from, &vec![&e, config.reserve_id], from) } \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/constants.rs b/apps/contracts/strategies/blend/src/constants.rs new file mode 100644 index 0000000..903615c --- /dev/null +++ b/apps/contracts/strategies/blend/src/constants.rs @@ -0,0 +1,6 @@ +/// 1 with 7 decimal places +pub const SCALAR_7: i128 = 1_0000000; +/// 1 with 9 decimal places +pub const SCALAR_9: i128 = 1_000_000_000; +/// The minimum amount of tokens than can be deposited or withdrawn from the vault +pub const MIN_DUST: i128 = 0_0010000; diff --git a/apps/contracts/strategies/blend/src/lib.rs b/apps/contracts/strategies/blend/src/lib.rs index da4c66b..fd11355 100644 --- a/apps/contracts/strategies/blend/src/lib.rs +++ b/apps/contracts/strategies/blend/src/lib.rs @@ -1,13 +1,14 @@ #![no_std] +use constants::MIN_DUST; use soroban_sdk::{ - contract, contractimpl, token::TokenClient, Address, Env, IntoVal, String, Val, Vec}; + contract, contractimpl, panic_with_error, token::TokenClient, Address, Env, IntoVal, String, Val, Vec}; mod blend_pool; +mod constants; +mod reserves; mod storage; -use storage::{ - extend_instance_ttl, get_reserve_id, get_underlying_asset, is_initialized, set_blend_pool, set_initialized, set_reserve_id, set_underlying_asset -}; +use storage::{extend_instance_ttl, is_initialized, set_initialized, Config}; pub use defindex_strategy_core::{ DeFindexStrategyTrait, @@ -46,14 +47,19 @@ impl DeFindexStrategyTrait for BlendStrategy { return Err(StrategyError::AlreadyInitialized); } - let blend_pool_address = init_args.get(0).ok_or(StrategyError::InvalidArgument)?.into_val(&e); - let reserve_id = init_args.get(1).ok_or(StrategyError::InvalidArgument)?.into_val(&e); + let blend_pool_address: Address = init_args.get(0).ok_or(StrategyError::InvalidArgument)?.into_val(&e); + let reserve_id: u32 = init_args.get(1).ok_or(StrategyError::InvalidArgument)?.into_val(&e); set_initialized(&e); - set_blend_pool(&e, blend_pool_address); - set_reserve_id(&e, reserve_id); - set_underlying_asset(&e, &asset); + let config = Config { + asset: asset.clone(), + pool: blend_pool_address.clone(), + reserve_id: reserve_id.clone(), + }; + + storage::set_config(&e, config); + event::emit_initialize(&e, String::from_str(&e, STARETEGY_NAME), asset); extend_instance_ttl(&e); Ok(()) @@ -63,7 +69,7 @@ impl DeFindexStrategyTrait for BlendStrategy { check_initialized(&e)?; extend_instance_ttl(&e); - Ok(get_underlying_asset(&e)) + Ok(storage::get_config(&e).asset) } fn deposit( @@ -76,11 +82,28 @@ impl DeFindexStrategyTrait for BlendStrategy { extend_instance_ttl(&e); from.require_auth(); - // transfer tokens from the vault to the contract - let underlying_asset = get_underlying_asset(&e); - TokenClient::new(&e, &underlying_asset).transfer(&from, &e.current_contract_address(), &amount); + // protect against rouding of reserve_vault::update_rate, as small amounts + // can cause incorrect b_rate calculations due to the pool rounding + if amount < MIN_DUST { + return Err(StrategyError::InvalidArgument); //TODO: create a new error type for this + } + + // Harvest if rewards exceed threshold + // let rewards = blend_pool::claim_rewards(&e); + // if rewards > REWARD_THRESHOLD { + // blend_pool::reinvest_rewards(&e, rewards); + // } + + let reserves = storage::get_strategy_reserves(&e); - blend_pool::supply(&e, &from, underlying_asset, amount); + let config = storage::get_config(&e); + // transfer tokens from the vault to the strategy contract + TokenClient::new(&e, &config.asset).transfer(&from, &e.current_contract_address(), &amount); + + let b_tokens_minted = blend_pool::supply(&e, &from, &amount, &config); + + // Keeping track of the total deposited amount and the total bTokens owned by the strategy depositors + reserves::deposit(&e, reserves, &from, amount, b_tokens_minted); event::emit_deposit(&e, String::from_str(&e, STARETEGY_NAME), amount, from); Ok(()) @@ -89,9 +112,15 @@ impl DeFindexStrategyTrait for BlendStrategy { fn harvest(e: Env, from: Address) -> Result<(), StrategyError> { check_initialized(&e)?; extend_instance_ttl(&e); - from.require_auth(); + from.require_auth(); + + let config = storage::get_config(&e); + let _harvested_blend = blend_pool::claim(&e, &from, &config); - blend_pool::claim(&e, &from); + // should swap to usdc + // should supply to the pool + + // etcetc event::emit_harvest(&e, String::from_str(&e, STARETEGY_NAME), 0i128, from); Ok(()) @@ -106,13 +135,24 @@ impl DeFindexStrategyTrait for BlendStrategy { check_nonnegative_amount(amount)?; extend_instance_ttl(&e); from.require_auth(); - - let underlying_asset = get_underlying_asset(&e); - blend_pool::withdraw(&e, &from, underlying_asset, amount); + + // protect against rouding of reserve_vault::update_rate, as small amounts + // can cause incorrect b_rate calculations due to the pool rounding + if amount < MIN_DUST { + return Err(StrategyError::InvalidArgument) //TODO: create a new error type for this + } + + let reserves = storage::get_strategy_reserves(&e); + + let config = storage::get_config(&e); + + let (tokens_withdrawn, b_tokens_burnt) = blend_pool::withdraw(&e, &from, &amount, &config); + + let _burnt_shares = reserves::withdraw(&e, reserves, &from, tokens_withdrawn, b_tokens_burnt); event::emit_withdraw(&e, String::from_str(&e, STARETEGY_NAME), amount, from); - Ok(amount) + Ok(tokens_withdrawn) } fn balance( @@ -122,12 +162,10 @@ impl DeFindexStrategyTrait for BlendStrategy { check_initialized(&e)?; extend_instance_ttl(&e); - let positions = blend_pool::get_positions(&e, &from); - let reserve_id = get_reserve_id(&e); + let vault_shares = storage::get_vault_shares(&e, &from); - let supply = positions.supply.get(reserve_id).unwrap_or(0i128); - Ok(supply) + Ok(vault_shares) } } -mod test; \ No newline at end of file +// mod test; \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/reserves.rs b/apps/contracts/strategies/blend/src/reserves.rs new file mode 100644 index 0000000..de2dcde --- /dev/null +++ b/apps/contracts/strategies/blend/src/reserves.rs @@ -0,0 +1,126 @@ +use defindex_strategy_core::StrategyError; +use soroban_fixed_point_math::{i128, FixedPoint}; +use soroban_sdk::{contracttype, panic_with_error, Address, Env}; + +use crate::{constants::SCALAR_9, storage}; + +#[contracttype] +pub struct StrategyReserves { + /// The total deposited amount of the underlying asset + pub total_shares: i128, + /// The total bToken deposits owned by the strategy depositors. + pub total_b_tokens: i128, + /// The reserve's last bRate + pub b_rate: i128, +} + +impl StrategyReserves { + /// Converts a b_token amount to shares rounding down + pub fn b_tokens_to_shares_down(&self, amount: i128) -> i128 { + if self.total_shares == 0 || self.total_b_tokens == 0 { + return amount; + } + amount + .fixed_mul_floor(self.total_shares, self.total_b_tokens) + .unwrap() + } + + /// Converts a b_token amount to shares rounding up + pub fn b_tokens_to_shares_up(&self, amount: i128) -> i128 { + if self.total_shares == 0 || self.total_b_tokens == 0 { + return amount; + } + amount + .fixed_mul_ceil(self.total_shares, self.total_b_tokens) + .unwrap() + } + + /// Coverts a share amount to a b_token amount rounding down + pub fn shares_to_b_tokens_down(&self, amount: i128) -> i128 { + amount + .fixed_div_floor(self.total_shares, self.total_b_tokens) + .unwrap() + } + + pub fn update_rate(&mut self, amount: i128, b_tokens: i128) { + // Calculate the new bRate - 9 decimal places of precision + // Update the reserve's bRate + let new_rate = amount + .fixed_div_floor(b_tokens, SCALAR_9) + .unwrap(); + + self.b_rate = new_rate; + } + +} + +/// Deposit into the reserve vault. This function expects the deposit to have already been made +/// into the pool, and accounts for the deposit in the reserve vault. +pub fn deposit( + e: &Env, + mut reserves: StrategyReserves, + from: &Address, + underlying_amount: i128, + b_tokens_amount: i128, +) -> i128 { + if underlying_amount <= 0 { + panic_with_error!(e, StrategyError::InvalidArgument); //TODO: create a new error type for this + } + + if b_tokens_amount <= 0 { + panic_with_error!(e, StrategyError::InvalidArgument); //TODO: create a new error type for this + } + + reserves.update_rate(underlying_amount, b_tokens_amount); + + let mut vault_shares = storage::get_vault_shares(&e, &from); + let share_amount: i128 = reserves.b_tokens_to_shares_down(b_tokens_amount); + + reserves.total_shares += share_amount; + reserves.total_b_tokens += b_tokens_amount; + + vault_shares += share_amount; + + storage::set_strategy_reserves(&e, reserves); + storage::set_vault_shares(&e, &from, vault_shares); + share_amount +} + +/// Withdraw from the reserve vault. This function expects the withdraw to have already been made +/// from the pool, and only accounts for the withdraw from the reserve vault. +pub fn withdraw( + e: &Env, + mut reserves: StrategyReserves, + from: &Address, + underlying_amount: i128, + b_tokens_amount: i128, +) -> i128 { + if underlying_amount <= 0 { + panic_with_error!(e, StrategyError::InvalidArgument); + } + if b_tokens_amount <= 0 { + panic_with_error!(e, StrategyError::InvalidArgument); + } + + reserves.update_rate(underlying_amount, b_tokens_amount); + + let mut vault_shares = storage::get_vault_shares(&e, &from); + let share_amount = reserves.b_tokens_to_shares_up(b_tokens_amount); + + if reserves.total_shares < share_amount || reserves.total_b_tokens < b_tokens_amount { + panic_with_error!(e, StrategyError::InvalidArgument); + } + + reserves.total_shares -= share_amount; + reserves.total_b_tokens -= b_tokens_amount; + + if share_amount > vault_shares { + panic_with_error!(e, StrategyError::InvalidArgument); + } + + vault_shares -= share_amount; + storage::set_strategy_reserves(&e, reserves); + storage::set_vault_shares(&e, &from, vault_shares); + + share_amount +} \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/storage.rs b/apps/contracts/strategies/blend/src/storage.rs index fa6f51b..42fd963 100644 --- a/apps/contracts/strategies/blend/src/storage.rs +++ b/apps/contracts/strategies/blend/src/storage.rs @@ -1,19 +1,29 @@ use soroban_sdk::{contracttype, Address, Env}; +use crate::reserves::StrategyReserves; + +#[contracttype] +pub struct Config { + pub asset: Address, + pub pool: Address, + pub reserve_id: u32, +} + #[derive(Clone)] #[contracttype] pub enum DataKey { Initialized, - UnderlyingAsset, - BlendPool, - Balance(Address), - ReserveId + Config, + Reserves, + VaultPos(Address) // Vaults Positions } const DAY_IN_LEDGERS: u32 = 17280; pub const INSTANCE_BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; pub const INSTANCE_LIFETIME_THRESHOLD: u32 = INSTANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; +const LEDGER_BUMP: u32 = 120 * DAY_IN_LEDGERS; +const LEDGER_THRESHOLD: u32 = LEDGER_BUMP - 20 * DAY_IN_LEDGERS; pub fn extend_instance_ttl(e: &Env) { e.storage() @@ -29,28 +39,51 @@ pub fn is_initialized(e: &Env) -> bool { e.storage().instance().has(&DataKey::Initialized) } -// Underlying asset -pub fn set_underlying_asset(e: &Env, address: &Address) { - e.storage().instance().set(&DataKey::UnderlyingAsset, &address); +// Config +pub fn set_config(e: &Env, config: Config) { + e.storage().instance().set(&DataKey::Config, &config); } -pub fn get_underlying_asset(e: &Env) -> Address { - e.storage().instance().get(&DataKey::UnderlyingAsset).unwrap() +pub fn get_config(e: &Env) -> Config { + e.storage().instance().get(&DataKey::Config).unwrap() +} + +// Vault Position +/// Set the number of shares shares a user owns. Shares are stored with 7 decimal places of precision. +pub fn set_vault_shares(e: &Env, address: &Address, shares: i128) { + let key = DataKey::VaultPos(address.clone()); + e.storage().persistent().set::(&key, &shares); + e.storage() + .persistent() + .extend_ttl(&key, LEDGER_THRESHOLD, LEDGER_BUMP); } -// Blend Pool Address -pub fn set_blend_pool(e: &Env, address: Address) { - e.storage().instance().set(&DataKey::BlendPool, &address); +/// Get the number of strategy shares a user owns. Shares are stored with 7 decimal places of precision. +pub fn get_vault_shares(e: &Env, address: &Address) -> i128 { + let result = e.storage().persistent().get::(&DataKey::VaultPos(address.clone())); + match result { + Some(shares) => { + e.storage() + .persistent() + .extend_ttl(&DataKey::VaultPos(address.clone()), LEDGER_THRESHOLD, LEDGER_BUMP); + shares + } + None => 0, + } } -pub fn get_blend_pool(e: &Env) -> Address { - e.storage().instance().get(&DataKey::BlendPool).unwrap() +// Strategy Reserves +pub fn set_strategy_reserves(e: &Env, new_reserves: StrategyReserves) { + e.storage().instance().set(&DataKey::Reserves, &new_reserves); } -pub fn set_reserve_id(e: &Env, id: u32) { - e.storage().instance().set(&DataKey::ReserveId, &id); +pub fn get_strategy_reserves(e: &Env) -> StrategyReserves { + e.storage().instance().get(&DataKey::Reserves).unwrap_or( + StrategyReserves { + total_shares: 0, + total_b_tokens: 0, + b_rate: 0, + } + ) } -pub fn get_reserve_id(e: &Env) -> u32 { - e.storage().instance().get(&DataKey::ReserveId).unwrap() -} \ No newline at end of file