From 839cc6e88655a0d539c329542ae5f53b19f44c95 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Fri, 22 Mar 2024 17:58:54 +0000 Subject: [PATCH 01/42] Started running tests --- Cargo.toml | 4 + .../examples/install_savings_app.rs | 120 +-- contracts/carrot-app/src/contract.rs | 14 +- contracts/carrot-app/src/error.rs | 15 + contracts/carrot-app/src/handlers/execute.rs | 688 ++++++++---------- .../carrot-app/src/handlers/instantiate.rs | 73 +- contracts/carrot-app/src/handlers/internal.rs | 135 ++++ contracts/carrot-app/src/handlers/mod.rs | 8 +- contracts/carrot-app/src/handlers/query.rs | 132 ++-- contracts/carrot-app/src/helpers.rs | 30 +- contracts/carrot-app/src/lib.rs | 1 + contracts/carrot-app/src/msg.rs | 85 ++- .../carrot-app/src/replies/after_swaps.rs | 28 + contracts/carrot-app/src/replies/mod.rs | 15 +- .../replies/{ => osmosis}/add_to_position.rs | 16 +- .../replies/{ => osmosis}/create_position.rs | 18 +- .../carrot-app/src/replies/osmosis/mod.rs | 2 + contracts/carrot-app/src/state.rs | 72 +- contracts/carrot-app/src/yield_sources.rs | 266 +++++++ .../carrot-app/src/yield_sources/mars.rs | 70 ++ .../src/yield_sources/osmosis_cl_pool.rs | 240 ++++++ .../src/yield_sources/yield_type.rs | 84 +++ contracts/carrot-app/tests/autocompound.rs | 330 ++++----- contracts/carrot-app/tests/common.rs | 221 +----- .../carrot-app/tests/deposit_withdraw.rs | 289 ++++---- .../carrot-app/tests/recreate_position.rs | 530 +++++++------- contracts/carrot-app/tests/strategy.rs | 152 ++++ 27 files changed, 2108 insertions(+), 1530 deletions(-) create mode 100644 contracts/carrot-app/src/handlers/internal.rs create mode 100644 contracts/carrot-app/src/replies/after_swaps.rs rename contracts/carrot-app/src/replies/{ => osmosis}/add_to_position.rs (71%) rename contracts/carrot-app/src/replies/{ => osmosis}/create_position.rs (70%) create mode 100644 contracts/carrot-app/src/replies/osmosis/mod.rs create mode 100644 contracts/carrot-app/src/yield_sources.rs create mode 100644 contracts/carrot-app/src/yield_sources/mars.rs create mode 100644 contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs create mode 100644 contracts/carrot-app/src/yield_sources/yield_type.rs create mode 100644 contracts/carrot-app/tests/strategy.rs diff --git a/Cargo.toml b/Cargo.toml index 27b7fcd0..0a21eb50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,7 @@ abstract-app = { version = "0.21.0" } abstract-interface = { version = "0.21.0" } abstract-dex-adapter = { git = "https://github.com/abstractsdk/abstract.git", tag = "v0.21.0" } abstract-client = { version = "0.21.0" } + + +[patch.crates-io] +osmosis-test-tube = { path = "../test-tube/packages/osmosis-test-tube" } diff --git a/contracts/carrot-app/examples/install_savings_app.rs b/contracts/carrot-app/examples/install_savings_app.rs index 869b3f20..bdb84469 100644 --- a/contracts/carrot-app/examples/install_savings_app.rs +++ b/contracts/carrot-app/examples/install_savings_app.rs @@ -1,7 +1,7 @@ #![allow(unused)] use abstract_app::objects::{AccountId, AssetEntry}; use abstract_client::AbstractClient; -use cosmwasm_std::{Coin, Uint128, Uint256, Uint64}; +use cosmwasm_std::{coins, Coin, Uint128, Uint256, Uint64}; use cw_orch::{ anyhow, daemon::{networks::OSMOSIS_1, Daemon, DaemonBuilder}, @@ -11,8 +11,10 @@ use cw_orch::{ use dotenv::dotenv; use carrot_app::{ - msg::{AppInstantiateMsg, CreatePositionMessage}, - state::AutocompoundRewardsConfig, + contract::OSMOSIS, + msg::AppInstantiateMsg, + state::{AutocompoundConfig, AutocompoundRewardsConfig}, + yield_sources::BalanceStrategy, }; use osmosis_std::types::cosmos::authz::v1beta1::MsgGrantResponse; @@ -54,29 +56,21 @@ fn main() -> anyhow::Result<()> { let app_data = usdc_usdc_ax::app_data(funds, 100_000_000_000_000, 100_000_000_000_000); - // Give all authzs and create subaccount with app in single tx - let mut msgs = utils::give_authorizations_msgs(&client, savings_app_addr, &app_data)?; - + let mut msgs = vec![]; let init_msg = AppInstantiateMsg { - pool_id: app_data.pool_id, - autocompound_cooldown_seconds: Uint64::new(AUTOCOMPOUND_COOLDOWN_SECONDS), - autocompound_rewards_config: AutocompoundRewardsConfig { - gas_asset: AssetEntry::new(utils::REWARD_ASSET), - swap_asset: app_data.swap_asset, - reward: Uint128::new(50_000), - min_gas_balance: Uint128::new(1000000), - max_gas_balance: Uint128::new(3000000), + autocompound_config: AutocompoundConfig { + cooldown_seconds: Uint64::new(AUTOCOMPOUND_COOLDOWN_SECONDS), + rewards: AutocompoundRewardsConfig { + gas_asset: AssetEntry::new(utils::REWARD_ASSET), + swap_asset: app_data.swap_asset, + reward: Uint128::new(50_000), + min_gas_balance: Uint128::new(1000000), + max_gas_balance: Uint128::new(3000000), + }, }, - create_position: Some(CreatePositionMessage { - lower_tick: app_data.lower_tick, - upper_tick: app_data.upper_tick, - funds: app_data.funds, - asset0: app_data.asset0, - asset1: app_data.asset1, - max_spread: None, - belief_price0: None, - belief_price1: None, - }), + balance_strategy: BalanceStrategy(vec![]), + deposit: Some(coins(100, "usdc")), + dex: OSMOSIS.to_string(), }; let create_sub_account_message = utils::create_account_message(&client, init_msg)?; @@ -200,84 +194,6 @@ mod utils { use prost_types::Any; use std::iter; - pub fn give_authorizations_msgs( - client: &AbstractClient, - savings_app_addr: impl Into, - app_data: &CarrotAppInitData, - ) -> Result, anyhow::Error> { - let dex_fee_account = client.account_from(AccountId::local(0))?; - let dex_fee_addr = dex_fee_account.proxy()?.to_string(); - let chain = client.environment().clone(); - - let authorization_urls = [ - MsgCreatePosition::TYPE_URL, - MsgSwapExactAmountIn::TYPE_URL, - MsgAddToPosition::TYPE_URL, - MsgWithdrawPosition::TYPE_URL, - MsgCollectIncentives::TYPE_URL, - MsgCollectSpreadRewards::TYPE_URL, - ] - .map(ToOwned::to_owned); - let savings_app_addr: String = savings_app_addr.into(); - let granter = chain.sender().to_string(); - let grantee = savings_app_addr.clone(); - - let reward_denom = client - .name_service() - .resolve(&AssetEntry::new(REWARD_ASSET))?; - - let mut dex_spend_limit = vec![ - cw_orch::osmosis_test_tube::osmosis_test_tube::osmosis_std::types::cosmos::base::v1beta1::Coin { - denom: app_data.denom0.to_string(), - amount: LOTS.to_string(), - }, - cw_orch::osmosis_test_tube::osmosis_test_tube::osmosis_std::types::cosmos::base::v1beta1::Coin { - denom: app_data.denom1.to_string(), - amount: LOTS.to_string(), - }, - cw_orch::osmosis_test_tube::osmosis_test_tube::osmosis_std::types::cosmos::base::v1beta1::Coin { - denom: reward_denom.to_string(), - amount: LOTS.to_string(), - }]; - dex_spend_limit.sort_unstable_by(|a, b| a.denom.cmp(&b.denom)); - let dex_fee_authorization = Any { - value: MsgGrant { - granter: chain.sender().to_string(), - grantee: grantee.clone(), - grant: Some(Grant { - authorization: Some( - SendAuthorization { - spend_limit: dex_spend_limit, - allow_list: vec![dex_fee_addr, savings_app_addr], - } - .to_any(), - ), - expiration: None, - }), - } - .encode_to_vec(), - type_url: MsgGrant::TYPE_URL.to_owned(), - }; - - let msgs: Vec = authorization_urls - .into_iter() - .map(|msg| Any { - value: MsgGrant { - granter: granter.clone(), - grantee: grantee.clone(), - grant: Some(Grant { - authorization: Some(GenericAuthorization { msg }.to_any()), - expiration: None, - }), - } - .encode_to_vec(), - type_url: MsgGrant::TYPE_URL.to_owned(), - }) - .chain(iter::once(dex_fee_authorization)) - .collect(); - Ok(msgs) - } - pub fn create_account_message( client: &AbstractClient, init_msg: AppInstantiateMsg, diff --git a/contracts/carrot-app/src/contract.rs b/contracts/carrot-app/src/contract.rs index 7b3e918d..d60fc116 100644 --- a/contracts/carrot-app/src/contract.rs +++ b/contracts/carrot-app/src/contract.rs @@ -7,10 +7,13 @@ use crate::{ handlers, msg::{AppExecuteMsg, AppInstantiateMsg, AppMigrateMsg, AppQueryMsg}, replies::{ - add_to_position_reply, create_position_reply, ADD_TO_POSITION_ID, CREATE_POSITION_ID, + add_to_position_reply, after_swap_reply, create_position_reply, + OSMOSIS_ADD_TO_POSITION_REPLY_ID, OSMOSIS_CREATE_POSITION_REPLY_ID, REPLY_AFTER_SWAPS_STEP, }, }; +pub const OSMOSIS: &str = "osmosis"; + /// The version of your app pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); /// The id of the app @@ -22,8 +25,6 @@ pub type AppResult = Result; /// The type of the app that is used to build your app and access the Abstract SDK features. pub type App = AppContract; -pub(crate) const OSMOSIS: &str = "osmosis"; - const DEX_DEPENDENCY: StaticDependency = StaticDependency::new( abstract_dex_adapter::DEX_ADAPTER_ID, &[abstract_dex_adapter::contract::CONTRACT_VERSION], @@ -33,10 +34,11 @@ const APP: App = App::new(APP_ID, APP_VERSION, None) .with_instantiate(handlers::instantiate_handler) .with_execute(handlers::execute_handler) .with_query(handlers::query_handler) - .with_migrate(handlers::migrate_handler) + // .with_migrate(handlers::migrate_handler) .with_replies(&[ - (CREATE_POSITION_ID, create_position_reply), - (ADD_TO_POSITION_ID, add_to_position_reply), + (OSMOSIS_CREATE_POSITION_REPLY_ID, create_position_reply), + (OSMOSIS_ADD_TO_POSITION_REPLY_ID, add_to_position_reply), + (REPLY_AFTER_SWAPS_STEP, after_swap_reply), ]) .with_dependencies(&[DEX_DEPENDENCY]); diff --git a/contracts/carrot-app/src/error.rs b/contracts/carrot-app/src/error.rs index 1c9f18a0..782530fe 100644 --- a/contracts/carrot-app/src/error.rs +++ b/contracts/carrot-app/src/error.rs @@ -73,4 +73,19 @@ pub enum AppError { #[error("Operation exceeds max spread limit, price: {price}")] MaxSpreadAssertion { price: Decimal }, + + #[error( + "The given strategy is not valid, the sum of share : {} is not 1", + share_sum + )] + InvalidStrategySum { share_sum: Decimal }, + + #[error("The given strategy is not valid, there must be at least one element")] + InvalidEmptyStrategy {}, + + #[error("Exchange Rate not given for {0}")] + NoExchangeRate(String), + + #[error("Deposited total value is zero")] + NoDeposit {}, } diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index 9998d021..a9ff24a6 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -1,23 +1,24 @@ -use super::swap_helpers::{swap_msg, swap_to_enter_position}; use crate::{ - contract::{App, AppResult, OSMOSIS}, + contract::{App, AppResult}, error::AppError, - helpers::{get_balance, get_user}, - msg::{AppExecuteMsg, CreatePositionMessage, ExecuteMsg}, - replies::{ADD_TO_POSITION_ID, CREATE_POSITION_ID}, + handlers::query::query_balance, + helpers::{add_funds, get_balance, get_proxy_balance}, + msg::{AppExecuteMsg, ExecuteMsg}, + replies::REPLY_AFTER_SWAPS_STEP, state::{ - assert_contract, get_osmosis_position, get_position, get_position_status, Config, CONFIG, + assert_contract, Config, CONFIG, TEMP_CURRENT_COIN, TEMP_DEPOSIT_COINS, + TEMP_EXPECTED_SWAP_COIN, }, + yield_sources::{yield_type::YieldType, DepositStep, OneDepositStrategy}, }; -use abstract_app::abstract_sdk::AuthZInterface; use abstract_app::{abstract_sdk::features::AbstractResponse, objects::AnsAsset}; use abstract_dex_adapter::DexInterface; -use abstract_sdk::{features::AbstractNameService, Resolve}; +use abstract_sdk::features::AbstractNameService; use cosmwasm_std::{ - to_json_binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, SubMsg, Uint128, - WasmMsg, + to_json_binary, wasm_execute, Coin, Coins, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, + StdError, SubMsg, Uint128, WasmMsg, }; -use cw_asset::Asset; +use cw_asset::{Asset, AssetInfo}; use osmosis_std::{ cosmwasm_to_proto_coins, try_proto_to_cosmwasm_coins, types::osmosis::concentratedliquidity::v1beta1::{ @@ -25,7 +26,12 @@ use osmosis_std::{ MsgWithdrawPosition, }, }; -use std::str::FromStr; +use std::{collections::HashMap, str::FromStr}; + +use super::{ + internal::{deposit_one_strategy, execute_finalize_deposit, execute_one_deposit_step}, + query::{query_exchange_rate, query_strategy}, +}; pub fn execute_handler( deps: DepsMut, @@ -35,113 +41,36 @@ pub fn execute_handler( msg: AppExecuteMsg, ) -> AppResult { match msg { - AppExecuteMsg::CreatePosition(create_position_msg) => { - create_position(deps, env, info, app, create_position_msg) + AppExecuteMsg::Deposit { funds } => deposit(deps, env, info, funds, app), + AppExecuteMsg::Withdraw { amount } => withdraw(deps, env, info, amount, app), + AppExecuteMsg::Autocompound {} => todo!(), + AppExecuteMsg::Rebalance { strategy } => todo!(), + + // Endpoints called by the contract directly + AppExecuteMsg::DepositOneStrategy { + swap_strategy, + yield_type, + } => deposit_one_strategy(deps, env, info, swap_strategy, yield_type, app), + AppExecuteMsg::ExecuteOneDepositSwapStep { + asset_in, + denom_out, + expected_amount, + } => execute_one_deposit_step(deps, env, info, asset_in, denom_out, expected_amount, app), + AppExecuteMsg::FinalizeDeposit { yield_type } => { + execute_finalize_deposit(deps, env, info, yield_type, app) } - AppExecuteMsg::Deposit { - funds, - max_spread, - belief_price0, - belief_price1, - } => deposit( - deps, - env, - info, - funds, - max_spread, - belief_price0, - belief_price1, - app, - ), - AppExecuteMsg::Withdraw { amount } => withdraw(deps, env, info, Some(amount), app), - AppExecuteMsg::WithdrawAll {} => withdraw(deps, env, info, None, app), - AppExecuteMsg::Autocompound {} => autocompound(deps, env, info, app), } } -/// In this function, we want to create a new position for the user. -/// This operation happens in multiple steps: -/// 1. Withdraw a potential existing position and add the funds to the current position being created -/// 2. Create a new position using the existing funds (if any) + the funds that the user wishes to deposit additionally -fn create_position( - deps: DepsMut, - env: Env, - info: MessageInfo, - app: App, - create_position_msg: CreatePositionMessage, -) -> AppResult { - // TODO verify authz permissions before creating the position +fn deposit(deps: DepsMut, env: Env, info: MessageInfo, funds: Vec, app: App) -> AppResult { + // Only the admin (manager contracts or account owner) can deposit app.admin.assert_admin(deps.as_ref(), &info.sender)?; - // We start by checking if there is already a position - if get_osmosis_position(deps.as_ref()).is_ok() { - return Err(AppError::PositionExists {}); - // If the position still has incentives to claim, the user is able to override it - }; - - let (swap_messages, create_position_msg) = - _create_position(deps.as_ref(), &env, &app, create_position_msg)?; - - Ok(app - .response("create_position") - .add_messages(swap_messages) - .add_submessage(create_position_msg)) -} -#[allow(clippy::too_many_arguments)] -fn deposit( - deps: DepsMut, - env: Env, - info: MessageInfo, - funds: Vec, - max_spread: Option, - belief_price0: Option, - belief_price1: Option, - app: App, -) -> AppResult { - // Only the admin (manager contracts or account owner) + the smart contract can deposit - app.admin - .assert_admin(deps.as_ref(), &info.sender) - .or(assert_contract(&info, &env))?; - - let pool = get_osmosis_position(deps.as_ref())?; - let position = pool.position.unwrap(); - - let asset0 = try_proto_to_cosmwasm_coins(pool.asset0.clone())?[0].clone(); - let asset1 = try_proto_to_cosmwasm_coins(pool.asset1.clone())?[0].clone(); - - // When depositing, we start by adapting the available funds to the expected pool funds ratio - // We do so by computing the swap information - - let (swap_msgs, resulting_assets) = swap_to_enter_position( - deps.as_ref(), - &env, - funds, - &app, - asset0, - asset1, - max_spread, - belief_price0, - belief_price1, - )?; - - let user = get_user(deps.as_ref(), &app)?; - - let deposit_msg = app.auth_z(deps.as_ref(), Some(user.clone()))?.execute( - &env.contract.address, - MsgAddToPosition { - position_id: position.position_id, - sender: user.to_string(), - amount0: resulting_assets[0].amount.to_string(), - amount1: resulting_assets[1].amount.to_string(), - token_min_amount0: "0".to_string(), - token_min_amount1: "0".to_string(), - }, - ); - - Ok(app - .response("deposit") - .add_messages(swap_msgs) - .add_submessage(SubMsg::reply_on_success(deposit_msg, ADD_TO_POSITION_ID))) + let deposit_msgs = _inner_deposit(deps.as_ref(), &env, funds, &app)?; + deps.api + .debug(&format!("All deposit messages {:?}", deposit_msgs)); + + Ok(app.response("deposit").add_messages(deposit_msgs)) } fn withdraw( @@ -154,299 +83,266 @@ fn withdraw( // Only the authorized addresses (admin ?) can withdraw app.admin.assert_admin(deps.as_ref(), &info.sender)?; - let (withdraw_msg, withdraw_amount, total_amount, _withdrawn_funds) = - _inner_withdraw(deps, &env, amount, &app)?; + let msgs = _inner_withdraw(deps, &env, amount, &app)?; - Ok(app - .response("withdraw") - .add_attribute("withdraw_amount", withdraw_amount) - .add_attribute("total_amount", total_amount) - .add_message(withdraw_msg)) + Ok(app.response("withdraw").add_messages(msgs)) } -/// Auto-compound the position with earned fees and incentives. - -fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResult { - // Everyone can autocompound - - let position = get_osmosis_position(deps.as_ref())?; - let position_details = position.position.unwrap(); - - let mut rewards = cosmwasm_std::Coins::default(); - let mut collect_rewards_msgs = vec![]; - - // Get app's user and set up authz. - let user = get_user(deps.as_ref(), &app)?; - let authz = app.auth_z(deps.as_ref(), Some(user.clone()))?; - - // If there are external incentives, claim them. - if !position.claimable_incentives.is_empty() { - for coin in try_proto_to_cosmwasm_coins(position.claimable_incentives)? { - rewards.add(coin)?; - } - collect_rewards_msgs.push(authz.execute( - &env.contract.address, - MsgCollectIncentives { - position_ids: vec![position_details.position_id], - sender: user.to_string(), - }, - )); - } - - // If there is income from swap fees, claim them. - if !position.claimable_spread_rewards.is_empty() { - for coin in try_proto_to_cosmwasm_coins(position.claimable_spread_rewards)? { - rewards.add(coin)?; - } - collect_rewards_msgs.push(authz.execute( - &env.contract.address, - MsgCollectSpreadRewards { - position_ids: vec![position_details.position_id], - sender: position_details.address.clone(), - }, - )) - } - - // If there are no rewards, we can't do anything - if rewards.is_empty() { - return Err(crate::error::AppError::NoRewards {}); - } - - // Finally we deposit of all rewarded tokens into the position - let msg_deposit = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: env.contract.address.to_string(), - msg: to_json_binary(&ExecuteMsg::Module(AppExecuteMsg::Deposit { - funds: rewards.into(), - max_spread: None, - belief_price0: None, - belief_price1: None, - }))?, - funds: vec![], +// /// Auto-compound the position with earned fees and incentives. + +// fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResult { +// // Everyone can autocompound + +// let position = get_osmosis_position(deps.as_ref())?; +// let position_details = position.position.unwrap(); + +// let mut rewards = cosmwasm_std::Coins::default(); +// let mut collect_rewards_msgs = vec![]; + +// // Get app's user and set up authz. +// let user = get_user(deps.as_ref(), &app)?; +// let authz = app.auth_z(deps.as_ref(), Some(user.clone()))?; + +// // If there are external incentives, claim them. +// if !position.claimable_incentives.is_empty() { +// for coin in try_proto_to_cosmwasm_coins(position.claimable_incentives)? { +// rewards.add(coin)?; +// } +// collect_rewards_msgs.push(authz.execute( +// &env.contract.address, +// MsgCollectIncentives { +// position_ids: vec![position_details.position_id], +// sender: user.to_string(), +// }, +// )); +// } + +// // If there is income from swap fees, claim them. +// if !position.claimable_spread_rewards.is_empty() { +// for coin in try_proto_to_cosmwasm_coins(position.claimable_spread_rewards)? { +// rewards.add(coin)?; +// } +// collect_rewards_msgs.push(authz.execute( +// &env.contract.address, +// MsgCollectSpreadRewards { +// position_ids: vec![position_details.position_id], +// sender: position_details.address.clone(), +// }, +// )) +// } + +// // If there are no rewards, we can't do anything +// if rewards.is_empty() { +// return Err(crate::error::AppError::NoRewards {}); +// } + +// // Finally we deposit of all rewarded tokens into the position +// let msg_deposit = CosmosMsg::Wasm(WasmMsg::Execute { +// contract_addr: env.contract.address.to_string(), +// msg: to_json_binary(&ExecuteMsg::Module(AppExecuteMsg::Deposit { +// funds: rewards.into(), +// max_spread: None, +// belief_price0: None, +// belief_price1: None, +// }))?, +// funds: vec![], +// }); + +// let mut response = app +// .response("auto-compound") +// .add_messages(collect_rewards_msgs) +// .add_message(msg_deposit); + +// // If called by non-admin and reward cooldown has ended, send rewards to the contract caller. +// let config = CONFIG.load(deps.storage)?; +// if !app.admin.is_admin(deps.as_ref(), &info.sender)? +// && get_position_status( +// deps.storage, +// &env, +// config.autocompound_cooldown_seconds.u64(), +// )? +// .is_ready() +// { +// let executor_reward_messages = autocompound_executor_rewards( +// deps.as_ref(), +// &env, +// info.sender.into_string(), +// &app, +// config, +// )?; + +// response = response.add_messages(executor_reward_messages); +// } + +// Ok(response) +// } + +pub fn _inner_deposit( + deps: Deps, + env: &Env, + funds: Vec, + app: &App, +) -> AppResult> { + // We determine the value of all the tokens that were received with USD + + let all_strategy_exchange_rates = query_strategy(deps)?.strategy.0.into_iter().flat_map(|s| { + s.yield_source + .expected_tokens + .into_iter() + .map(|(denom, _)| { + Ok::<_, AppError>(( + denom.clone(), + query_exchange_rate(deps, denom.clone(), app)?, + )) + }) }); - - let mut response = app - .response("auto-compound") - .add_messages(collect_rewards_msgs) - .add_message(msg_deposit); - - // If called by non-admin and reward cooldown has ended, send rewards to the contract caller. - let config = CONFIG.load(deps.storage)?; - if !app.admin.is_admin(deps.as_ref(), &info.sender)? - && get_position_status( - deps.storage, - &env, - config.autocompound_cooldown_seconds.u64(), - )? - .is_ready() - { - let executor_reward_messages = autocompound_executor_rewards( - deps.as_ref(), - &env, - info.sender.into_string(), - &app, - config, - )?; - - response = response.add_messages(executor_reward_messages); - } - - Ok(response) + let exchange_rates = funds + .iter() + .map(|f| { + Ok::<_, AppError>(( + f.denom.clone(), + query_exchange_rate(deps, f.denom.clone(), app)?, + )) + }) + .chain(all_strategy_exchange_rates) + .collect::, _>>()?; + + let deposit_strategies = query_strategy(deps)? + .strategy + .fill_all(funds, &exchange_rates)?; + + // We select the target shares depending on the strategy selected + let deposit_msgs = deposit_strategies + .iter() + .zip( + query_strategy(deps)? + .strategy + .0 + .iter() + .map(|s| s.yield_source.ty.clone()), + ) + .map(|(strategy, yield_type)| strategy.deposit_msgs(env, yield_type)) + .collect::, _>>()?; + + Ok(deposit_msgs) } fn _inner_withdraw( deps: DepsMut, - env: &Env, - amount: Option, + _env: &Env, + value: Option, app: &App, -) -> AppResult<(CosmosMsg, String, String, Vec)> { - let position = get_osmosis_position(deps.as_ref())?; - let position_details = position.position.unwrap(); - - let total_liquidity = position_details.liquidity.replace('.', ""); - - let liquidity_amount = if let Some(amount) = amount { - amount.to_string() - } else { - // TODO: it's decimals inside contracts - total_liquidity.clone() - }; - let user = get_user(deps.as_ref(), app)?; - - // We need to execute withdraw on the user's behalf - let msg = app.auth_z(deps.as_ref(), Some(user.clone()))?.execute( - &env.contract.address, - MsgWithdrawPosition { - position_id: position_details.position_id, - sender: user.to_string(), - liquidity_amount: liquidity_amount.clone(), - }, - ); - - let withdrawn_funds = vec![ - try_proto_to_cosmwasm_coins(position.asset0)? - .first() - .map(|c| { - Ok::<_, AppError>(Coin { - denom: c.denom.clone(), - amount: c.amount * Uint128::from_str(&liquidity_amount)? - / Uint128::from_str(&total_liquidity)?, +) -> AppResult> { + // We need to select the share of each investment that needs to be withdrawn + let withdraw_share = value + .map(|value| { + let total_deposit = query_balance(deps.as_ref(), app)?; + let total_value = total_deposit + .balances + .into_iter() + .map(|balance| { + let exchange_rate = query_exchange_rate(deps.as_ref(), balance.denom, app)?; + + Ok::<_, AppError>(exchange_rate * balance.amount) }) - }) - .transpose()?, - try_proto_to_cosmwasm_coins(position.asset1)? - .first() - .map(|c| { - Ok::<_, AppError>(Coin { - denom: c.denom.clone(), - amount: c.amount * Uint128::from_str(&liquidity_amount)? - / Uint128::from_str(&total_liquidity)?, + .sum::>()?; + + if total_value.is_zero() { + return Err(AppError::NoDeposit {}); + } + Ok(Decimal::from_ratio(value, total_value)) + }) + .transpose()?; + + // We withdraw the necessary share from all investments + let withdraw_msgs = query_strategy(deps.as_ref())? + .strategy + .0 + .into_iter() + .map(|s| { + let this_withdraw_amount = withdraw_share + .map(|share| { + let this_amount = s.yield_source.ty.user_liquidity(deps.as_ref(), app)?; + let this_withdraw_amount = share * this_amount; + + Ok::<_, AppError>(this_withdraw_amount) }) - }) - .transpose()?, - ] - .into_iter() - .flatten() - .collect(); - - Ok((msg, liquidity_amount, total_liquidity, withdrawn_funds)) + .transpose()?; + s.yield_source + .ty + .withdraw(deps.as_ref(), this_withdraw_amount, app) + }) + .collect::, _>>()?; + + Ok(withdraw_msgs.into_iter().flatten().collect()) } -/// This function creates a position for the user, -/// 1. Swap the indicated funds to match the asset0/asset1 ratio and deposit as much as possible in the pool for the given parameters -/// 2. Create a new position -/// 3. Store position id from create position response -/// -/// * `lower_tick` - Concentrated liquidity pool parameter -/// * `upper_tick` - Concentrated liquidity pool parameter -/// * `funds` - Funds that will be deposited from the user wallet directly into the pool. DO NOT SEND FUNDS TO THIS ENDPOINT -/// * `asset0` - The target amount of asset0.denom that the user will deposit inside the pool -/// * `asset1` - The target amount of asset1.denom that the user will deposit inside the pool -/// -/// asset0 and asset1 are only used in a ratio to each other. They are there to make sure that the deposited funds will ALL land inside the pool. -/// We don't use an asset ratio because either one of the amounts can be zero -/// See https://docs.osmosis.zone/osmosis-core/modules/concentrated-liquidity for more details -/// -pub(crate) fn _create_position( - deps: Deps, - env: &Env, - app: &App, - create_position_msg: CreatePositionMessage, -) -> AppResult<(Vec, SubMsg)> { - let config = CONFIG.load(deps.storage)?; - - let CreatePositionMessage { - lower_tick, - upper_tick, - funds, - asset0, - asset1, - max_spread, - belief_price0, - belief_price1, - } = create_position_msg; - - // 1. Swap the assets - let (swap_msgs, resulting_assets) = swap_to_enter_position( - deps, - env, - funds, - app, - asset0, - asset1, - max_spread, - belief_price0, - belief_price1, - )?; - let sender = get_user(deps, app)?; - - // 2. Create a position - let tokens = cosmwasm_to_proto_coins(resulting_assets); - let create_msg = app.auth_z(deps, Some(sender.clone()))?.execute( - &env.contract.address, - MsgCreatePosition { - pool_id: config.pool_config.pool_id, - sender: sender.to_string(), - lower_tick, - upper_tick, - tokens_provided: tokens, - token_min_amount0: "0".to_string(), - token_min_amount1: "0".to_string(), - }, - ); - - Ok(( - swap_msgs, - // 3. Use a reply to get the stored position id - SubMsg::reply_on_success(create_msg, CREATE_POSITION_ID), - )) -} - -/// Sends autocompound rewards to the executor. -/// In case user does not have not enough gas token the contract will swap some -/// tokens for gas tokens. -pub fn autocompound_executor_rewards( - deps: Deps, - env: &Env, - executor: String, - app: &App, - config: Config, -) -> AppResult> { - let rewards_config = config.autocompound_rewards_config; - let position = get_position(deps)?; - let user = position.owner; - - // Get user balance of gas denom - let gas_denom = rewards_config - .gas_asset - .resolve(&deps.querier, &app.ans_host(deps)?)?; - let user_gas_balance = gas_denom.query_balance(&deps.querier, user.clone())?; - - let mut rewards_messages = vec![]; - - // If not enough gas coins - swap for some amount - if user_gas_balance < rewards_config.min_gas_balance { - // Get asset entries - let dex = app.ans_dex(deps, OSMOSIS.to_string()); - - // Do reverse swap to find approximate amount we need to swap - let need_gas_coins = rewards_config.max_gas_balance - user_gas_balance; - let simulate_swap_response = dex.simulate_swap( - AnsAsset::new(rewards_config.gas_asset.clone(), need_gas_coins), - rewards_config.swap_asset.clone(), - )?; - - // Get user balance of swap denom - let user_swap_balance = - get_balance(rewards_config.swap_asset.clone(), deps, user.clone(), app)?; - - // Swap as much as available if not enough for max_gas_balance - let swap_amount = simulate_swap_response.return_amount.min(user_swap_balance); - - let msgs = swap_msg( - deps, - env, - AnsAsset::new(rewards_config.swap_asset, swap_amount), - rewards_config.gas_asset, - app, - )?; - rewards_messages.extend(msgs); - } - - let reward_asset = Asset::new(gas_denom, rewards_config.reward); - let msg_send = reward_asset.transfer_msg(env.contract.address.to_string())?; - - // To avoid giving general `MsgSend` authorization to any address we do 2 sends here - // 1) From user to the contract - // 2) From contract to the executor - // That way we can limit the `MsgSend` authorization to the contract address only. - let send_reward_to_contract_msg = app - .auth_z(deps, Some(cosmwasm_std::Addr::unchecked(user)))? - .execute(&env.contract.address, msg_send); - rewards_messages.push(send_reward_to_contract_msg); - - let send_reward_to_executor_msg = reward_asset.transfer_msg(executor)?; - - rewards_messages.push(send_reward_to_executor_msg); - - Ok(rewards_messages) -} +// /// Sends autocompound rewards to the executor. +// /// In case user does not have not enough gas token the contract will swap some +// /// tokens for gas tokens. +// pub fn autocompound_executor_rewards( +// deps: Deps, +// env: &Env, +// executor: String, +// app: &App, +// config: Config, +// ) -> AppResult> { +// let rewards_config = config.autocompound_rewards_config; +// let position = get_position(deps)?; +// let user = position.owner; + +// // Get user balance of gas denom +// let gas_denom = rewards_config +// .gas_asset +// .resolve(&deps.querier, &app.ans_host(deps)?)?; +// let user_gas_balance = gas_denom.query_balance(&deps.querier, user.clone())?; + +// let mut rewards_messages = vec![]; + +// // If not enough gas coins - swap for some amount +// if user_gas_balance < rewards_config.min_gas_balance { +// // Get asset entries +// let dex = app.ans_dex(deps, OSMOSIS.to_string()); + +// // Do reverse swap to find approximate amount we need to swap +// let need_gas_coins = rewards_config.max_gas_balance - user_gas_balance; +// let simulate_swap_response = dex.simulate_swap( +// AnsAsset::new(rewards_config.gas_asset.clone(), need_gas_coins), +// rewards_config.swap_asset.clone(), +// )?; + +// // Get user balance of swap denom +// let user_swap_balance = +// get_balance(rewards_config.swap_asset.clone(), deps, user.clone(), app)?; + +// // Swap as much as available if not enough for max_gas_balance +// let swap_amount = simulate_swap_response.return_amount.min(user_swap_balance); + +// let msgs = swap_msg( +// deps, +// env, +// AnsAsset::new(rewards_config.swap_asset, swap_amount), +// rewards_config.gas_asset, +// app, +// )?; +// rewards_messages.extend(msgs); +// } + +// let reward_asset = Asset::new(gas_denom, rewards_config.reward); +// let msg_send = reward_asset.transfer_msg(env.contract.address.to_string())?; + +// // To avoid giving general `MsgSend` authorization to any address we do 2 sends here +// // 1) From user to the contract +// // 2) From contract to the executor +// // That way we can limit the `MsgSend` authorization to the contract address only. +// let send_reward_to_contract_msg = app +// .auth_z(deps, Some(cosmwasm_std::Addr::unchecked(user)))? +// .execute(&env.contract.address, msg_send); +// rewards_messages.push(send_reward_to_contract_msg); + +// let send_reward_to_executor_msg = reward_asset.transfer_msg(executor)?; + +// rewards_messages.push(send_reward_to_executor_msg); + +// Ok(rewards_messages) +// } diff --git a/contracts/carrot-app/src/handlers/instantiate.rs b/contracts/carrot-app/src/handlers/instantiate.rs index a515ce7c..12e80130 100644 --- a/contracts/carrot-app/src/handlers/instantiate.rs +++ b/contracts/carrot-app/src/handlers/instantiate.rs @@ -1,19 +1,12 @@ -use abstract_app::abstract_core::ans_host::{AssetPairingFilter, AssetPairingMapEntry}; -use abstract_app::abstract_sdk::{features::AbstractNameService, AbstractResponse}; -use cosmwasm_std::{DepsMut, Env, MessageInfo}; -use cw_asset::AssetInfo; -use osmosis_std::types::osmosis::{ - concentratedliquidity::v1beta1::Pool, poolmanager::v1beta1::PoolmanagerQuerier, -}; - use crate::{ contract::{App, AppResult}, - error::AppError, msg::AppInstantiateMsg, - state::{Config, PoolConfig, CONFIG}, + state::{Config, CONFIG}, }; +use abstract_app::abstract_sdk::{features::AbstractNameService, AbstractResponse}; +use cosmwasm_std::{DepsMut, Env, MessageInfo}; -use super::execute::_create_position; +use super::execute::_inner_deposit; pub fn instantiate_handler( deps: DepsMut, @@ -22,61 +15,33 @@ pub fn instantiate_handler( app: App, msg: AppInstantiateMsg, ) -> AppResult { - let pool: Pool = PoolmanagerQuerier::new(&deps.querier) - .pool(msg.pool_id)? - .pool - .unwrap() - .try_into()?; + // We check the balance strategy is valid + msg.balance_strategy.check()?; + + // We don't check the dex on instantiation // We query the ANS for useful information on the tokens and pool let ans = app.name_service(deps.as_ref()); - // ANS Asset entries to indentify the assets inside Abstract - let asset_entries = ans.query(&vec![ - AssetInfo::Native(pool.token0.clone()), - AssetInfo::Native(pool.token1.clone()), - ])?; - let asset0 = asset_entries[0].clone(); - let asset1 = asset_entries[1].clone(); - let asset_pairing_resp: Vec = ans.pool_list( - Some(AssetPairingFilter { - asset_pair: Some((asset0.clone(), asset1.clone())), - dex: None, - }), - None, - None, - )?; - let pair = asset_pairing_resp - .into_iter() - .find(|(_, refs)| !refs.is_empty()) - .ok_or(AppError::NoSwapPossibility {})? - .0; - let dex_name = pair.dex(); - - let autocompound_rewards_config = msg.autocompound_rewards_config; // Check validity of autocompound rewards - autocompound_rewards_config.check(deps.as_ref(), dex_name, ans.host())?; + msg.autocompound_config + .rewards + .check(deps.as_ref(), &msg.dex, ans.host())?; let config: Config = Config { - pool_config: PoolConfig { - pool_id: msg.pool_id, - token0: pool.token0.clone(), - token1: pool.token1.clone(), - asset0, - asset1, - }, - autocompound_cooldown_seconds: msg.autocompound_cooldown_seconds, - autocompound_rewards_config, + dex: msg.dex, + balance_strategy: msg.balance_strategy, + autocompound_config: msg.autocompound_config, }; CONFIG.save(deps.storage, &config)?; let mut response = app.response("instantiate_savings_app"); - // If provided - create position - if let Some(create_position_msg) = msg.create_position { - let (swap_msgs, create_msg) = - _create_position(deps.as_ref(), &env, &app, create_position_msg)?; - response = response.add_messages(swap_msgs).add_submessage(create_msg); + // If provided - do an initial deposit + if let Some(funds) = msg.deposit { + let deposit_msgs = _inner_deposit(deps.as_ref(), &env, funds, &app)?; + + response = response.add_messages(deposit_msgs); } Ok(response) } diff --git a/contracts/carrot-app/src/handlers/internal.rs b/contracts/carrot-app/src/handlers/internal.rs new file mode 100644 index 00000000..962b69b7 --- /dev/null +++ b/contracts/carrot-app/src/handlers/internal.rs @@ -0,0 +1,135 @@ +use crate::{ + contract::{App, AppResult}, + error::AppError, + helpers::{add_funds, get_proxy_balance}, + msg::{AppExecuteMsg, ExecuteMsg}, + replies::REPLY_AFTER_SWAPS_STEP, + state::{CONFIG, TEMP_CURRENT_COIN, TEMP_DEPOSIT_COINS, TEMP_EXPECTED_SWAP_COIN}, + yield_sources::{yield_type::YieldType, DepositStep, OneDepositStrategy}, +}; +use abstract_app::{abstract_sdk::features::AbstractResponse, objects::AnsAsset}; +use abstract_dex_adapter::DexInterface; +use abstract_sdk::features::AbstractNameService; +use cosmwasm_std::{wasm_execute, Coin, DepsMut, Env, MessageInfo, SubMsg, Uint128}; +use cw_asset::AssetInfo; + +use super::query::query_exchange_rate; + +pub fn deposit_one_strategy( + deps: DepsMut, + env: Env, + info: MessageInfo, + strategy: OneDepositStrategy, + yield_type: YieldType, + app: App, +) -> AppResult { + if info.sender != env.contract.address { + return Err(AppError::Unauthorized {}); + } + + TEMP_DEPOSIT_COINS.save(deps.storage, &vec![])?; + + // We go through all deposit steps. + // If the step is a swap, we execute with a reply to catch the amount change and get the exact deposit amount + let msg = strategy + .0 + .into_iter() + .map(|s| { + s.into_iter() + .map(|step| match step { + DepositStep::Swap { + asset_in, + denom_out, + expected_amount, + } => wasm_execute( + env.contract.address.clone(), + &ExecuteMsg::Module(AppExecuteMsg::ExecuteOneDepositSwapStep { + asset_in, + denom_out, + expected_amount, + }), + vec![], + ) + .map(|msg| Some(SubMsg::reply_on_success(msg, REPLY_AFTER_SWAPS_STEP))), + + DepositStep::UseFunds { asset } => { + TEMP_DEPOSIT_COINS.update(deps.storage, |funds| add_funds(funds, asset))?; + Ok(None) + } + }) + .collect::>, _>>() + }) + .collect::, _>>()?; + + let msgs = msg.into_iter().flatten().flatten().collect::>(); + + // Finalize and execute the deposit + let last_step = wasm_execute( + env.contract.address.clone(), + &ExecuteMsg::Module(AppExecuteMsg::FinalizeDeposit { yield_type }), + vec![], + )?; + + Ok(app + .response("deposit-one") + .add_submessages(msgs) + .add_message(last_step)) +} + +pub fn execute_one_deposit_step( + deps: DepsMut, + env: Env, + info: MessageInfo, + asset_in: Coin, + denom_out: String, + expected_amount: Uint128, + app: App, +) -> AppResult { + if info.sender != env.contract.address { + return Err(AppError::Unauthorized {}); + } + + let config = CONFIG.load(deps.storage)?; + + let exchange_rate_in = query_exchange_rate(deps.as_ref(), asset_in.denom.clone(), &app)?; + let exchange_rate_out = query_exchange_rate(deps.as_ref(), denom_out.clone(), &app)?; + + let ans = app.name_service(deps.as_ref()); + + let asset_entries = ans.query(&vec![ + AssetInfo::native(asset_in.denom.clone()), + AssetInfo::native(denom_out.clone()), + ])?; + let in_asset = asset_entries[0].clone(); + let out_asset = asset_entries[1].clone(); + + let msg = app.ans_dex(deps.as_ref(), config.dex).swap( + AnsAsset::new(in_asset, asset_in.amount), + out_asset, + None, + Some(exchange_rate_in / exchange_rate_out), + )?; + + let proxy_balance_before = get_proxy_balance(deps.as_ref(), &app, denom_out)?; + TEMP_CURRENT_COIN.save(deps.storage, &proxy_balance_before)?; + TEMP_EXPECTED_SWAP_COIN.save(deps.storage, &expected_amount)?; + + Ok(app.response("one-deposit-step").add_message(msg)) +} + +pub fn execute_finalize_deposit( + deps: DepsMut, + env: Env, + info: MessageInfo, + yield_type: YieldType, + app: App, +) -> AppResult { + if info.sender != env.contract.address { + return Err(AppError::Unauthorized {}); + } + let available_deposit_coins = TEMP_DEPOSIT_COINS.load(deps.storage)?; + + let msgs = yield_type.deposit(deps.as_ref(), &env, available_deposit_coins, &app)?; + + Ok(app.response("one-deposit-step").add_submessages(msgs)) +} diff --git a/contracts/carrot-app/src/handlers/mod.rs b/contracts/carrot-app/src/handlers/mod.rs index e9de48a0..dc4aa2da 100644 --- a/contracts/carrot-app/src/handlers/mod.rs +++ b/contracts/carrot-app/src/handlers/mod.rs @@ -1,9 +1,11 @@ pub mod execute; pub mod instantiate; -pub mod migrate; +pub mod internal; +// pub mod migrate; pub mod query; -pub mod swap_helpers; +// pub mod swap_helpers; pub use crate::handlers::{ - execute::execute_handler, instantiate::instantiate_handler, migrate::migrate_handler, + execute::execute_handler, + instantiate::instantiate_handler, // migrate::migrate_handler, query::query_handler, }; diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index f62ce102..94e94558 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -1,22 +1,21 @@ +use abstract_app::traits::AccountIdentification; use abstract_app::{ abstract_core::objects::AnsAsset, traits::{AbstractNameService, Resolve}, }; use abstract_dex_adapter::DexInterface; -use cosmwasm_std::{ensure, to_json_binary, Binary, Coin, Decimal, Deps, Env}; +use cosmwasm_std::{to_json_binary, Binary, Coins, Decimal, Deps, Env}; use cw_asset::Asset; -use osmosis_std::try_proto_to_cosmwasm_coins; use crate::{ - contract::{App, AppResult, OSMOSIS}, + contract::{App, AppResult}, error::AppError, - handlers::swap_helpers::DEFAULT_SLIPPAGE, - helpers::{get_balance, get_user}, + helpers::get_balance, msg::{ AppQueryMsg, AssetsBalanceResponse, AvailableRewardsResponse, CompoundStatusResponse, - PositionResponse, + StrategyResponse, }, - state::{get_osmosis_position, get_position_status, Config, CONFIG, POSITION}, + state::{get_autocompound_status, Config, CONFIG}, }; pub fn query_handler(deps: Deps, env: Env, app: &App, msg: AppQueryMsg) -> AppResult { @@ -24,8 +23,9 @@ pub fn query_handler(deps: Deps, env: Env, app: &App, msg: AppQueryMsg) -> AppRe AppQueryMsg::Balance {} => to_json_binary(&query_balance(deps, app)?), AppQueryMsg::AvailableRewards {} => to_json_binary(&query_rewards(deps, app)?), AppQueryMsg::Config {} => to_json_binary(&query_config(deps)?), - AppQueryMsg::Position {} => to_json_binary(&query_position(deps)?), + AppQueryMsg::Strategy {} => to_json_binary(&query_strategy(deps)?), AppQueryMsg::CompoundStatus {} => to_json_binary(&query_compound_status(deps, env, app)?), + AppQueryMsg::RebalancePreview {} => todo!(), } .map_err(Into::into) } @@ -34,20 +34,21 @@ pub fn query_handler(deps: Deps, env: Env, app: &App, msg: AppQueryMsg) -> AppRe /// Accounts for the user's ability to pay for the gas fees of executing the contract. fn query_compound_status(deps: Deps, env: Env, app: &App) -> AppResult { let config = CONFIG.load(deps.storage)?; - let status = get_position_status( + let status = get_autocompound_status( deps.storage, &env, - config.autocompound_cooldown_seconds.u64(), + config.autocompound_config.cooldown_seconds.u64(), )?; let gas_denom = config - .autocompound_rewards_config + .autocompound_config + .rewards .gas_asset .resolve(&deps.querier, &app.ans_host(deps)?)?; - let reward = Asset::new(gas_denom.clone(), config.autocompound_rewards_config.reward); + let reward = Asset::new(gas_denom.clone(), config.autocompound_config.rewards.reward); - let user = get_user(deps, app)?; + let user = app.account_base(deps)?.proxy; let user_gas_balance = gas_denom.query_balance(&deps.querier, user.clone())?; @@ -55,8 +56,8 @@ fn query_compound_status(deps: Deps, env: Env, app: &App) -> AppResult AppResult AppResult { - let position = POSITION.may_load(deps.storage)?; +pub fn query_strategy(deps: Deps) -> AppResult { + let config = CONFIG.load(deps.storage)?; - Ok(PositionResponse { position }) + Ok(StrategyResponse { + strategy: config.balance_strategy, + }) } fn query_config(deps: Deps) -> AppResult { Ok(CONFIG.load(deps.storage)?) } -fn query_balance(deps: Deps, _app: &App) -> AppResult { - let pool = get_osmosis_position(deps)?; - let balances = try_proto_to_cosmwasm_coins(vec![pool.asset0.unwrap(), pool.asset1.unwrap()])?; - let liquidity = pool.position.unwrap().liquidity.replace('.', ""); +pub fn query_balance(deps: Deps, app: &App) -> AppResult { + let mut funds = Coins::default(); + query_strategy(deps)?.strategy.0.iter().try_for_each(|s| { + let deposit_value = s.yield_source.ty.user_deposit(deps, app)?; + for fund in deposit_value { + funds.add(fund)?; + } + Ok::<_, AppError>(()) + })?; + Ok(AssetsBalanceResponse { - balances, - liquidity, + balances: funds.into(), }) } -fn query_rewards(deps: Deps, _app: &App) -> AppResult { - let pool = get_osmosis_position(deps)?; - - let mut rewards = cosmwasm_std::Coins::default(); - for coin in try_proto_to_cosmwasm_coins(pool.claimable_incentives)? { - rewards.add(coin)?; - } +fn query_rewards(deps: Deps, app: &App) -> AppResult { + let strategy = query_strategy(deps)?.strategy; - for coin in try_proto_to_cosmwasm_coins(pool.claimable_spread_rewards)? { - rewards.add(coin)?; - } + let mut rewards = Coins::default(); + strategy.0.into_iter().try_for_each(|s| { + let this_rewards = s.yield_source.ty.user_rewards(deps, app)?; + for fund in this_rewards { + rewards.add(fund)?; + } + Ok::<_, AppError>(()) + })?; Ok(AvailableRewardsResponse { available_rewards: rewards.into(), }) } -pub fn query_price( - deps: Deps, - funds: &[Coin], - app: &App, - max_spread: Option, - belief_price0: Option, - belief_price1: Option, -) -> AppResult { - let config = CONFIG.load(deps.storage)?; - - let amount0 = funds - .iter() - .find(|c| c.denom == config.pool_config.token0) - .map(|c| c.amount) - .unwrap_or_default(); - let amount1 = funds - .iter() - .find(|c| c.denom == config.pool_config.token1) - .map(|c| c.amount) - .unwrap_or_default(); - - // We take the biggest amount and simulate a swap for the corresponding asset - let price = if amount0 > amount1 { - let simulation_result = app.ans_dex(deps, OSMOSIS.to_string()).simulate_swap( - AnsAsset::new(config.pool_config.asset0, amount0), - config.pool_config.asset1, - )?; - - let price = Decimal::from_ratio(amount0, simulation_result.return_amount); - if let Some(belief_price) = belief_price1 { - ensure!( - belief_price.abs_diff(price) <= max_spread.unwrap_or(DEFAULT_SLIPPAGE), - AppError::MaxSpreadAssertion { price } - ); - } - price - } else { - let simulation_result = app.ans_dex(deps, OSMOSIS.to_string()).simulate_swap( - AnsAsset::new(config.pool_config.asset1, amount1), - config.pool_config.asset0, - )?; - - let price = Decimal::from_ratio(simulation_result.return_amount, amount1); - if let Some(belief_price) = belief_price0 { - ensure!( - belief_price.abs_diff(price) <= max_spread.unwrap_or(DEFAULT_SLIPPAGE), - AppError::MaxSpreadAssertion { price } - ); - } - price - }; - - Ok(price) +pub fn query_exchange_rate(_deps: Deps, _denom: String, _app: &App) -> AppResult { + // In the first iteration, all deposited tokens are assumed to be equal to 1 + Ok(Decimal::one()) } diff --git a/contracts/carrot-app/src/helpers.rs b/contracts/carrot-app/src/helpers.rs index 8c60838f..25ab7326 100644 --- a/contracts/carrot-app/src/helpers.rs +++ b/contracts/carrot-app/src/helpers.rs @@ -1,23 +1,23 @@ +use crate::contract::{App, AppResult}; +use abstract_app::traits::AccountIdentification; use abstract_app::{objects::AssetEntry, traits::AbstractNameService}; use abstract_sdk::Resolve; -use cosmwasm_std::{Addr, Deps, Uint128}; - -use crate::{ - contract::{App, AppResult}, - error::AppError, -}; - -pub fn get_user(deps: Deps, app: &App) -> AppResult { - Ok(app - .admin - .query_account_owner(deps)? - .admin - .ok_or(AppError::NoTopLevelAccount {}) - .map(|admin| deps.api.addr_validate(&admin))??) -} +use cosmwasm_std::{Addr, Coin, Coins, Deps, StdResult, Uint128}; pub fn get_balance(a: AssetEntry, deps: Deps, address: Addr, app: &App) -> AppResult { let denom = a.resolve(&deps.querier, &app.ans_host(deps)?)?; let user_gas_balance = denom.query_balance(&deps.querier, address.clone())?; Ok(user_gas_balance) } + +pub fn get_proxy_balance(deps: Deps, app: &App, denom: String) -> AppResult { + Ok(deps + .querier + .query_balance(app.account_base(deps)?.proxy, denom.clone())?) +} + +pub fn add_funds(funds: Vec, to_add: Coin) -> StdResult> { + let mut funds: Coins = funds.try_into()?; + funds.add(to_add)?; + Ok(funds.into()) +} diff --git a/contracts/carrot-app/src/lib.rs b/contracts/carrot-app/src/lib.rs index 8c33c3b9..feba3e76 100644 --- a/contracts/carrot-app/src/lib.rs +++ b/contracts/carrot-app/src/lib.rs @@ -5,6 +5,7 @@ pub mod helpers; pub mod msg; mod replies; pub mod state; +pub mod yield_sources; #[cfg(feature = "interface")] pub use contract::interface::AppInterface; diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index dddb15ea..1d86d91c 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -1,10 +1,11 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Coin, Decimal, Uint128, Uint64}; +use cosmwasm_std::{Coin, Uint128, Uint64}; use cw_asset::AssetBase; use crate::{ contract::App, - state::{AutocompoundRewardsConfig, Position}, + state::AutocompoundConfig, + yield_sources::{yield_type::YieldType, BalanceStrategy, OneDepositStrategy}, }; // This is used for type safety and re-exporting the contract endpoint structs. @@ -13,30 +14,15 @@ abstract_app::app_msg_types!(App, AppExecuteMsg, AppQueryMsg); /// App instantiate message #[cosmwasm_schema::cw_serde] pub struct AppInstantiateMsg { - /// Id of the pool used to get rewards - pub pool_id: u64, - /// Seconds to wait before autocompound is incentivized. - pub autocompound_cooldown_seconds: Uint64, - /// Configuration of rewards to the address who helped to execute autocompound - pub autocompound_rewards_config: AutocompoundRewardsConfig, + /// Strategy to use to dispatch the deposited funds + pub balance_strategy: BalanceStrategy, + /// Configuration of the aut-compounding procedure + pub autocompound_config: AutocompoundConfig, + /// Target dex to swap things on + pub dex: String, /// Create position with instantiation. /// Will not create position if omitted - pub create_position: Option, -} - -#[cosmwasm_schema::cw_serde] -pub struct CreatePositionMessage { - pub lower_tick: i64, - pub upper_tick: i64, - // Funds to use to deposit on the account - pub funds: Vec, - /// The two next fields indicate the token0/token1 ratio we want to deposit inside the current ticks - pub asset0: Coin, - pub asset1: Coin, - // Slippage - pub max_spread: Option, - pub belief_price0: Option, - pub belief_price1: Option, + pub deposit: Option>, } /// App execute messages @@ -44,21 +30,32 @@ pub struct CreatePositionMessage { #[cfg_attr(feature = "interface", derive(cw_orch::ExecuteFns))] #[cfg_attr(feature = "interface", impl_into(ExecuteMsg))] pub enum AppExecuteMsg { - /// Create the initial liquidity position - CreatePosition(CreatePositionMessage), /// Deposit funds onto the app - Deposit { - funds: Vec, - max_spread: Option, - belief_price0: Option, - belief_price1: Option, - }, + /// Those funds will be distributed between yield sources according to the current strategy + /// TODO : for now only send stable coins that have the same value as USD + /// More tokens can be included when the oracle adapter is live + Deposit { funds: Vec }, /// Partial withdraw of the funds available on the app - Withdraw { amount: Uint128 }, - /// Withdraw everything that is on the app - WithdrawAll {}, + /// If amount is omitted, withdraws everything that is on the app + Withdraw { amount: Option }, /// Auto-compounds the pool rewards into the pool Autocompound {}, + /// Rebalances all investments according to a new balance strategy + Rebalance { strategy: BalanceStrategy }, + + /// Only called by the contract internally + DepositOneStrategy { + swap_strategy: OneDepositStrategy, + yield_type: YieldType, + }, + /// Execute one Deposit Swap Step + ExecuteOneDepositSwapStep { + asset_in: Coin, + denom_out: String, + expected_amount: Uint128, + }, + /// Finalize the deposit after all swaps are executed + FinalizeDeposit { yield_type: YieldType }, } /// App query messages @@ -75,12 +72,18 @@ pub enum AppQueryMsg { /// Returns [`AvailableRewardsResponse`] #[returns(AvailableRewardsResponse)] AvailableRewards {}, - #[returns(PositionResponse)] - Position {}, /// Get the status of the compounding logic of the application /// Returns [`CompoundStatusResponse`] #[returns(CompoundStatusResponse)] CompoundStatus {}, + /// Returns the current strategy + /// Returns [`StrategyResponse`] + #[returns(StrategyResponse)] + Strategy {}, + /// Returns a preview of the rebalance distribution + /// Returns [`RebalancePreviewResponse`] + #[returns(RebalancePreviewResponse)] + RebalancePreview {}, } #[cosmwasm_schema::cw_serde] @@ -98,12 +101,11 @@ pub struct AvailableRewardsResponse { #[cw_serde] pub struct AssetsBalanceResponse { pub balances: Vec, - pub liquidity: String, } #[cw_serde] -pub struct PositionResponse { - pub position: Option, +pub struct StrategyResponse { + pub strategy: BalanceStrategy, } #[cw_serde] @@ -130,3 +132,6 @@ impl CompoundStatus { matches!(self, Self::Ready {}) } } + +#[cw_serde] +pub struct RebalancePreviewResponse {} diff --git a/contracts/carrot-app/src/replies/after_swaps.rs b/contracts/carrot-app/src/replies/after_swaps.rs new file mode 100644 index 00000000..0ef2b648 --- /dev/null +++ b/contracts/carrot-app/src/replies/after_swaps.rs @@ -0,0 +1,28 @@ +use abstract_sdk::AbstractResponse; +use cosmwasm_std::{coin, DepsMut, Env, Reply}; + +use crate::{ + contract::{App, AppResult}, + helpers::{add_funds, get_proxy_balance}, + state::{TEMP_CURRENT_COIN, TEMP_DEPOSIT_COINS}, +}; + +pub fn after_swap_reply(deps: DepsMut, _env: Env, app: App, _reply: Reply) -> AppResult { + let coins_before = TEMP_CURRENT_COIN.load(deps.storage)?; + let current_coins = get_proxy_balance(deps.as_ref(), &app, coins_before.denom)?; + // We just update the coins to deposit after the swap + if current_coins.amount > coins_before.amount { + TEMP_DEPOSIT_COINS.update(deps.storage, |f| { + add_funds( + f, + coin( + (current_coins.amount - coins_before.amount).into(), + current_coins.denom, + ), + ) + })?; + } + deps.api.debug("Swap reply over"); + + Ok(app.response("after_swap_reply")) +} diff --git a/contracts/carrot-app/src/replies/mod.rs b/contracts/carrot-app/src/replies/mod.rs index a45bd96e..4590d673 100644 --- a/contracts/carrot-app/src/replies/mod.rs +++ b/contracts/carrot-app/src/replies/mod.rs @@ -1,8 +1,11 @@ -mod add_to_position; -mod create_position; +mod after_swaps; +mod osmosis; -pub const CREATE_POSITION_ID: u64 = 1; -pub const ADD_TO_POSITION_ID: u64 = 2; +pub const OSMOSIS_CREATE_POSITION_REPLY_ID: u64 = 1; +pub const OSMOSIS_ADD_TO_POSITION_REPLY_ID: u64 = 2; -pub use add_to_position::add_to_position_reply; -pub use create_position::create_position_reply; +pub const REPLY_AFTER_SWAPS_STEP: u64 = 3; + +pub use after_swaps::after_swap_reply; +pub use osmosis::add_to_position::add_to_position_reply; +pub use osmosis::create_position::create_position_reply; diff --git a/contracts/carrot-app/src/replies/add_to_position.rs b/contracts/carrot-app/src/replies/osmosis/add_to_position.rs similarity index 71% rename from contracts/carrot-app/src/replies/add_to_position.rs rename to contracts/carrot-app/src/replies/osmosis/add_to_position.rs index 61f67902..3e6ba43e 100644 --- a/contracts/carrot-app/src/replies/add_to_position.rs +++ b/contracts/carrot-app/src/replies/osmosis/add_to_position.rs @@ -5,11 +5,11 @@ use osmosis_std::types::osmosis::concentratedliquidity::v1beta1::MsgAddToPositio use crate::{ contract::{App, AppResult}, error::AppError, - helpers::get_user, - state::{Position, POSITION}, + state::OSMOSIS_POSITION, + yield_sources::osmosis_cl_pool::OsmosisPosition, }; -pub fn add_to_position_reply(deps: DepsMut, env: Env, app: App, reply: Reply) -> AppResult { +pub fn add_to_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) -> AppResult { let SubMsgResult::Ok(SubMsgResponse { data: Some(b), .. }) = reply.result else { return Err(AppError::Std(StdError::generic_err( "Failed to create position", @@ -22,17 +22,11 @@ pub fn add_to_position_reply(deps: DepsMut, env: Env, app: App, reply: Reply) -> // Parse the position response from the message let response: MsgAddToPositionResponse = parsed.data.unwrap_or_default().try_into()?; - // We get the creator of the position - let creator = get_user(deps.as_ref(), &app)?; - // We update the position - let position = Position { - owner: creator, + let position = OsmosisPosition { position_id: response.position_id, - last_compound: env.block.time, }; - - POSITION.save(deps.storage, &position)?; + OSMOSIS_POSITION.save(deps.storage, &position)?; Ok(app .response("create_position_reply") diff --git a/contracts/carrot-app/src/replies/create_position.rs b/contracts/carrot-app/src/replies/osmosis/create_position.rs similarity index 70% rename from contracts/carrot-app/src/replies/create_position.rs rename to contracts/carrot-app/src/replies/osmosis/create_position.rs index d7e3e18b..1f0004d0 100644 --- a/contracts/carrot-app/src/replies/create_position.rs +++ b/contracts/carrot-app/src/replies/osmosis/create_position.rs @@ -5,33 +5,29 @@ use osmosis_std::types::osmosis::concentratedliquidity::v1beta1::MsgCreatePositi use crate::{ contract::{App, AppResult}, error::AppError, - helpers::get_user, - state::{Position, POSITION}, + state::OSMOSIS_POSITION, + yield_sources::osmosis_cl_pool::OsmosisPosition, }; -pub fn create_position_reply(deps: DepsMut, env: Env, app: App, reply: Reply) -> AppResult { +pub fn create_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) -> AppResult { let SubMsgResult::Ok(SubMsgResponse { data: Some(b), .. }) = reply.result else { return Err(AppError::Std(StdError::generic_err( "Failed to create position", ))); }; + deps.api + .debug(&format!("Inside create position reply : {:x?}", b)); let parsed = cw_utils::parse_execute_response_data(&b)?; // Parse create position response let response: MsgCreatePositionResponse = parsed.data.clone().unwrap_or_default().try_into()?; - // We get the creator of the position - let creator = get_user(deps.as_ref(), &app)?; - // We save the position - let position = Position { - owner: creator, + let position = OsmosisPosition { position_id: response.position_id, - last_compound: env.block.time, }; - - POSITION.save(deps.storage, &position)?; + OSMOSIS_POSITION.save(deps.storage, &position)?; Ok(app .response("create_position_reply") diff --git a/contracts/carrot-app/src/replies/osmosis/mod.rs b/contracts/carrot-app/src/replies/osmosis/mod.rs new file mode 100644 index 00000000..7200e5be --- /dev/null +++ b/contracts/carrot-app/src/replies/osmosis/mod.rs @@ -0,0 +1,2 @@ +pub mod add_to_position; +pub mod create_position; diff --git a/contracts/carrot-app/src/state.rs b/contracts/carrot-app/src/state.rs index d08ba83f..d00b5769 100644 --- a/contracts/carrot-app/src/state.rs +++ b/contracts/carrot-app/src/state.rs @@ -1,23 +1,48 @@ +use std::collections::HashMap; + use abstract_app::abstract_sdk::{feature_objects::AnsHost, Resolve}; use abstract_app::{abstract_core::objects::AssetEntry, objects::DexAssetPairing}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ensure, Addr, Deps, Env, MessageInfo, Storage, Timestamp, Uint128, Uint64}; +use cosmwasm_std::{ + ensure, Addr, Coin, Decimal, Deps, Env, MessageInfo, Storage, Timestamp, Uint128, Uint64, +}; use cw_storage_plus::Item; use osmosis_std::types::osmosis::concentratedliquidity::v1beta1::{ ConcentratedliquidityQuerier, FullPositionBreakdown, }; +use crate::yield_sources::osmosis_cl_pool::OsmosisPosition; +use crate::yield_sources::BalanceStrategy; use crate::{contract::AppResult, error::AppError, msg::CompoundStatus}; pub const CONFIG: Item = Item::new("config"); -pub const POSITION: Item = Item::new("position"); +pub const POSITION: Item = Item::new("position"); pub const CURRENT_EXECUTOR: Item = Item::new("executor"); +// TEMP VARIABLES FOR DEPOSITING INTO ONE STRATEGY +pub const TEMP_CURRENT_COIN: Item = Item::new("temp_current_coins"); +pub const TEMP_EXPECTED_SWAP_COIN: Item = Item::new("temp_expected_swap_coin"); +pub const TEMP_DEPOSIT_COINS: Item> = Item::new("temp_deposit_coins"); + +// Storage for each yield source +pub const OSMOSIS_POSITION: Item = Item::new("osmosis_cl_position"); + #[cw_serde] pub struct Config { - pub pool_config: PoolConfig, - pub autocompound_cooldown_seconds: Uint64, - pub autocompound_rewards_config: AutocompoundRewardsConfig, + pub balance_strategy: BalanceStrategy, + pub autocompound_config: AutocompoundConfig, + pub dex: String, +} + +/// General auto-compound parameters. +/// Includes the cool down and the technical funds config +#[cw_serde] +pub struct AutocompoundConfig { + /// Seconds to wait before autocompound is incentivized. + /// Allows the user to configure when the auto-compound happens + pub cooldown_seconds: Uint64, + /// Configuration of rewards to the address who helped to execute autocompound + pub rewards: AutocompoundRewardsConfig, } /// Configuration on how rewards should be distributed @@ -68,6 +93,21 @@ pub struct PoolConfig { pub asset1: AssetEntry, } +pub fn compute_total_value( + funds: &[Coin], + exchange_rates: &HashMap, +) -> AppResult { + funds + .iter() + .map(|c| { + let exchange_rate = exchange_rates + .get(&c.denom) + .ok_or(AppError::NoExchangeRate(c.denom.clone()))?; + Ok(c.amount * *exchange_rate) + }) + .sum() +} + pub fn assert_contract(info: &MessageInfo, env: &Env) -> AppResult<()> { if info.sender == env.contract.address { Ok(()) @@ -77,29 +117,11 @@ pub fn assert_contract(info: &MessageInfo, env: &Env) -> AppResult<()> { } #[cw_serde] -pub struct Position { - pub owner: Addr, - pub position_id: u64, +pub struct AutocompoundState { pub last_compound: Timestamp, } -pub fn get_position(deps: Deps) -> AppResult { - POSITION - .load(deps.storage) - .map_err(|_| AppError::NoPosition {}) -} - -pub fn get_osmosis_position(deps: Deps) -> AppResult { - let position = get_position(deps)?; - - ConcentratedliquidityQuerier::new(&deps.querier) - .position_by_id(position.position_id) - .map_err(|e| AppError::UnableToQueryPosition(position.position_id, e))? - .position - .ok_or(AppError::NoPosition {}) -} - -pub fn get_position_status( +pub fn get_autocompound_status( storage: &dyn Storage, env: &Env, cooldown_seconds: u64, diff --git a/contracts/carrot-app/src/yield_sources.rs b/contracts/carrot-app/src/yield_sources.rs new file mode 100644 index 00000000..6b648323 --- /dev/null +++ b/contracts/carrot-app/src/yield_sources.rs @@ -0,0 +1,266 @@ +pub mod mars; +pub mod osmosis_cl_pool; +pub mod yield_type; + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ + coin, ensure, ensure_eq, wasm_execute, Coin, Coins, CosmosMsg, Decimal, Env, Uint128, +}; +use std::collections::HashMap; + +use crate::{ + contract::AppResult, + error::AppError, + msg::{AppExecuteMsg, ExecuteMsg}, + state::compute_total_value, +}; + +use self::yield_type::YieldType; + +/// A yield sources has the following elements +/// A vector of tokens that NEED to be deposited inside the yield source with a repartition of tokens +/// A type that allows routing to the right smart-contract integration internally +#[cw_serde] +pub struct YieldSource { + /// This id (denom, share) + pub expected_tokens: Vec<(String, Decimal)>, + pub ty: YieldType, +} + +impl YieldSource { + pub fn check(&self) -> AppResult<()> { + // First we check the share sums the 100 + let share_sum: Decimal = self.expected_tokens.iter().map(|e| e.1).sum(); + ensure_eq!( + share_sum, + Decimal::one(), + AppError::InvalidStrategySum { share_sum } + ); + ensure!( + !self.expected_tokens.is_empty(), + AppError::InvalidEmptyStrategy {} + ); + + // Then we check every yield strategy underneath + Ok(()) + } +} + +// Related to balance strategies +#[cw_serde] +pub struct BalanceStrategy(pub Vec); + +#[cw_serde] +pub struct BalanceStrategyElement { + pub yield_source: YieldSource, + pub share: Decimal, +} +impl BalanceStrategyElement { + pub fn check(&self) -> AppResult<()> { + self.yield_source.check() + } +} + +impl BalanceStrategy { + pub fn check(&self) -> AppResult<()> { + // First we check the share sums the 100 + let share_sum: Decimal = self.0.iter().map(|e| e.share).sum(); + ensure_eq!( + share_sum, + Decimal::one(), + AppError::InvalidStrategySum { share_sum } + ); + ensure!(!self.0.is_empty(), AppError::InvalidEmptyStrategy {}); + + // Then we check every yield strategy underneath + for yield_source in &self.0 { + yield_source.check()?; + } + + Ok(()) + } + + // We dispatch the available funds directly into the Strategies + // This returns : + // 0 : Funds that are used for specific strategies. And remaining amounts to fill those strategies + // 1 : Funds that are still available to fill those strategies + // This is the algorithm that is implemented here + pub fn fill_sources( + &self, + funds: Vec, + exchange_rates: &HashMap, + ) -> AppResult<(StrategyStatus, Coins)> { + let total_value = compute_total_value(&funds, exchange_rates)?; + let mut remaining_funds = Coins::default(); + + // We create the vector that holds the funds information + let mut yield_source_status = self + .0 + .iter() + .map(|source| { + source + .yield_source + .expected_tokens + .iter() + .map(|(denom, share)| B { + denom: denom.clone(), + raw_funds: Uint128::zero(), + remaining_amount: share * source.share * total_value, + }) + .collect::>() + }) + .collect::>(); + + for this_coin in funds { + let mut remaining_amount = this_coin.amount; + // We distribute those funds in to the accepting strategies + for (strategy, status) in self.0.iter().zip(yield_source_status.iter_mut()) { + // Find the share for the specific denom inside the strategy + let this_denom_status = strategy + .yield_source + .expected_tokens + .iter() + .zip(status.iter_mut()) + .find(|((denom, _share), _status)| this_coin.denom.eq(denom)) + .map(|(_, status)| status); + + if let Some(status) = this_denom_status { + // We fill the needed value with the remaining_amount + let funds_to_use_here = remaining_amount.min(status.remaining_amount); + + // Those funds are not available for other yield sources + remaining_amount -= funds_to_use_here; + + status.raw_funds += funds_to_use_here; + status.remaining_amount -= funds_to_use_here; + } + } + remaining_funds.add(coin(remaining_amount.into(), this_coin.denom))?; + } + + Ok((yield_source_status.into(), remaining_funds)) + } + + pub fn fill_all( + &self, + funds: Vec, + exchange_rates: &HashMap, + ) -> AppResult> { + let (status, remaining_funds) = self.fill_sources(funds, exchange_rates)?; + status.fill_with_remaining_funds(remaining_funds, exchange_rates) + } +} + +#[cw_serde] +pub struct B { + pub denom: String, + pub raw_funds: Uint128, + pub remaining_amount: Uint128, +} + +/// This contains information about the strategy status +/// AFTER filling with unrelated coins +/// Before filling with related coins +#[cw_serde] +pub struct StrategyStatus(pub Vec>); + +impl From>> for StrategyStatus { + fn from(value: Vec>) -> Self { + Self(value) + } +} + +impl StrategyStatus { + pub fn fill_with_remaining_funds( + &self, + mut funds: Coins, + exchange_rates: &HashMap, + ) -> AppResult> { + self.0 + .iter() + .map(|f| { + f.clone() + .iter_mut() + .map(|status| { + let mut swaps = vec![]; + for fund in funds.to_vec() { + let direct_e_r = exchange_rates + .get(&fund.denom) + .ok_or(AppError::NoExchangeRate(fund.denom.clone()))? + / exchange_rates + .get(&status.denom) + .ok_or(AppError::NoExchangeRate(status.denom.clone()))?; + let available_coin_in_destination_amount = fund.amount * direct_e_r; + + let fill_amount = + available_coin_in_destination_amount.min(status.remaining_amount); + + let swap_in_amount = fill_amount * (Decimal::one() / direct_e_r); + + if swap_in_amount != Uint128::zero() { + status.remaining_amount -= fill_amount; + let swap_funds = coin(swap_in_amount.into(), fund.denom); + funds.sub(swap_funds.clone())?; + swaps.push(DepositStep::Swap { + asset_in: swap_funds, + denom_out: status.denom.clone(), + expected_amount: fill_amount, + }); + } + } + if !status.raw_funds.is_zero() { + swaps.push(DepositStep::UseFunds { + asset: coin(status.raw_funds.into(), status.denom.clone()), + }) + } + + Ok::<_, AppError>(swaps) + }) + .collect::, _>>() + .map(Into::into) + }) + .collect::, _>>() + } +} + +#[cw_serde] +pub enum DepositStep { + Swap { + asset_in: Coin, + denom_out: String, + expected_amount: Uint128, + }, + UseFunds { + asset: Coin, + }, +} + +#[cw_serde] +pub struct OneDepositStrategy(pub Vec>); + +impl From>> for OneDepositStrategy { + fn from(value: Vec>) -> Self { + Self(value) + } +} + +impl OneDepositStrategy { + pub fn deposit_msgs(&self, env: &Env, yield_type: YieldType) -> AppResult { + // For each strategy, we send a message on the contract to execute it + Ok(wasm_execute( + env.contract.address.clone(), + &ExecuteMsg::Module(AppExecuteMsg::DepositOneStrategy { + swap_strategy: self.clone(), + yield_type, + }), + vec![], + )? + .into()) + } +} + +#[cw_serde] +pub enum DepositStepResult { + Todo(DepositStep), + Done { amount: Coin }, +} diff --git a/contracts/carrot-app/src/yield_sources/mars.rs b/contracts/carrot-app/src/yield_sources/mars.rs new file mode 100644 index 00000000..bcbb921f --- /dev/null +++ b/contracts/carrot-app/src/yield_sources/mars.rs @@ -0,0 +1,70 @@ +use crate::contract::{App, AppResult}; +use abstract_app::traits::AccountIdentification; +use abstract_app::{ + objects::{AnsAsset, AssetEntry}, + traits::AbstractNameService, +}; +use cosmwasm_std::{Coin, CosmosMsg, Deps, SubMsg, Uint128}; +use cw_asset::AssetInfo; + +pub fn deposit(deps: Deps, denom: String, amount: Uint128, app: &App) -> AppResult> { + let ans = app.name_service(deps); + let ans_fund = ans.query(&AssetInfo::native(denom))?; + + // TODO after MM Adapter is merged + // Ok(vec![app + // .ans_money_market(deps)? + // .deposit(AnsAsset::new(ans_fund, amount))? + // .into()]) + + Ok(vec![]) +} + +pub fn withdraw( + deps: Deps, + denom: String, + amount: Option, + app: &App, +) -> AppResult> { + let ans = app.name_service(deps); + + let amount = if let Some(amount) = amount { + amount + } else { + user_deposit(deps, denom.clone(), &app)? + }; + + let ans_fund = ans.query(&AssetInfo::native(denom))?; + + // TODO after MM Adapter is merged + // Ok(vec![app + // .ans_money_market(deps)? + // .withdraw(AnsAsset::new(ans_fund, amount))? + // .into()]) + + Ok(vec![]) +} + +pub fn user_deposit(deps: Deps, denom: String, app: &App) -> AppResult { + let ans = app.name_service(deps); + let ans_fund = ans.query(&AssetInfo::native(denom))?; + let user = app.account_base(deps)?.proxy; + + // TODO after MM Adapter is merged + // Ok(app + // .ans_money_market(deps)? + // .user_deposit(user, ans_fund)? + // .into()) + Ok(Uint128::zero()) +} + +/// Returns an amount representing a user's liquidity +pub fn user_liquidity(deps: Deps, denom: String, app: &App) -> AppResult { + user_deposit(deps, denom, app) +} + +pub fn user_rewards(deps: Deps, denom: String, app: &App) -> AppResult> { + // No rewards, because mars is self-auto-compounding + + Ok(vec![]) +} diff --git a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs new file mode 100644 index 00000000..74f9aa8d --- /dev/null +++ b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs @@ -0,0 +1,240 @@ +use std::str::FromStr; + +use crate::{ + contract::{App, AppResult}, + error::AppError, + replies::{OSMOSIS_ADD_TO_POSITION_REPLY_ID, OSMOSIS_CREATE_POSITION_REPLY_ID}, + state::OSMOSIS_POSITION, +}; +use abstract_app::traits::AccountIdentification; +use abstract_sdk::{AccountAction, Execution}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Coin, CosmosMsg, Deps, Env, ReplyOn, SubMsg, Uint128}; +use osmosis_std::{ + cosmwasm_to_proto_coins, try_proto_to_cosmwasm_coins, + types::osmosis::concentratedliquidity::v1beta1::{ + ConcentratedliquidityQuerier, FullPositionBreakdown, MsgAddToPosition, MsgCreatePosition, + MsgWithdrawPosition, + }, +}; + +use super::yield_type::ConcentratedPoolParams; + +/// This function creates a position for the user, +/// 1. Swap the indicated funds to match the asset0/asset1 ratio and deposit as much as possible in the pool for the given parameters +/// 2. Create a new position +/// 3. Store position id from create position response +/// +/// * `lower_tick` - Concentrated liquidity pool parameter +/// * `upper_tick` - Concentrated liquidity pool parameter +/// * `funds` - Funds that will be deposited from the user wallet directly into the pool. DO NOT SEND FUNDS TO THIS ENDPOINT +/// * `asset0` - The target amount of asset0.denom that the user will deposit inside the pool +/// * `asset1` - The target amount of asset1.denom that the user will deposit inside the pool +/// +/// asset0 and asset1 are only used in a ratio to each other. They are there to make sure that the deposited funds will ALL land inside the pool. +/// We don't use an asset ratio because either one of the amounts can be zero +/// See https://docs.osmosis.zone/osmosis-core/modules/concentrated-liquidity for more details +/// +fn create_position( + deps: Deps, + params: ConcentratedPoolParams, + funds: Vec, + app: &App, + // create_position_msg: CreatePositionMessage, +) -> AppResult> { + let proxy_addr = app.account_base(deps)?.proxy; + + // 2. Create a position + let tokens = cosmwasm_to_proto_coins(funds); + let msg = app.executor(deps).execute_with_reply( + vec![AccountAction::from_vec(vec![MsgCreatePosition { + pool_id: params.pool_id, + sender: proxy_addr.to_string(), + lower_tick: params.lower_tick, + upper_tick: params.upper_tick, + tokens_provided: tokens, + token_min_amount0: "0".to_string(), + token_min_amount1: "0".to_string(), + }])], + ReplyOn::Success, + OSMOSIS_CREATE_POSITION_REPLY_ID, + )?; + + deps.api.debug("Created position messages"); + + Ok(vec![msg]) +} + +fn raw_deposit(deps: Deps, funds: Vec, app: &App) -> AppResult> { + let pool = get_osmosis_position(deps)?; + let position = pool.position.unwrap(); + + let proxy_addr = app.account_base(deps)?.proxy; + let deposit_msg = app.executor(deps).execute_with_reply_and_data( + MsgAddToPosition { + position_id: position.position_id, + sender: proxy_addr.to_string(), + amount0: funds[0].amount.to_string(), + amount1: funds[1].amount.to_string(), + token_min_amount0: "0".to_string(), + token_min_amount1: "0".to_string(), + } + .into(), + cosmwasm_std::ReplyOn::Success, + OSMOSIS_ADD_TO_POSITION_REPLY_ID, + )?; + + Ok(vec![deposit_msg]) +} + +pub fn deposit( + deps: Deps, + env: &Env, + params: ConcentratedPoolParams, + funds: Vec, + app: &App, +) -> AppResult> { + // We verify there is a position stored + let osmosis_position = OSMOSIS_POSITION.may_load(deps.storage)?; + if let Some(position) = osmosis_position { + // We just deposit + raw_deposit(deps, funds, app) + } else { + // We need to create a position + create_position(deps, params, funds, app) + } +} + +pub fn withdraw(deps: Deps, amount: Option, app: &App) -> AppResult> { + let position = get_osmosis_position(deps)?; + let position_details = position.position.unwrap(); + + let total_liquidity = position_details.liquidity.replace('.', ""); + + let liquidity_amount = if let Some(amount) = amount { + amount.to_string() + } else { + // TODO: it's decimals inside contracts + total_liquidity.clone() + }; + let user = app.account_base(deps)?.proxy; + + // We need to execute withdraw on the user's behalf + Ok(vec![MsgWithdrawPosition { + position_id: position_details.position_id, + sender: user.to_string(), + liquidity_amount: liquidity_amount.clone(), + } + .into()]) +} + +pub fn user_deposit(deps: Deps, _app: &App) -> AppResult> { + let position = get_osmosis_position(deps)?; + + Ok([ + try_proto_to_cosmwasm_coins(position.asset0)?, + try_proto_to_cosmwasm_coins(position.asset1)?, + ] + .into_iter() + .flatten() + .collect()) +} + +/// Returns an amount representing a user's liquidity +pub fn user_liquidity(deps: Deps, _app: &App) -> AppResult { + let position = get_osmosis_position(deps)?; + let total_liquidity = position.position.unwrap().liquidity.replace('.', ""); + + Ok(Uint128::from_str(&total_liquidity)?) +} + +pub fn user_rewards(deps: Deps, _app: &App) -> AppResult> { + let pool = get_osmosis_position(deps)?; + + let mut rewards = cosmwasm_std::Coins::default(); + for coin in try_proto_to_cosmwasm_coins(pool.claimable_incentives)? { + rewards.add(coin)?; + } + + for coin in try_proto_to_cosmwasm_coins(pool.claimable_spread_rewards)? { + rewards.add(coin)?; + } + + Ok(rewards.into()) +} + +// pub fn query_price( +// deps: Deps, +// funds: &[Coin], +// app: &App, +// max_spread: Option, +// belief_price0: Option, +// belief_price1: Option, +// ) -> AppResult { +// let config = CONFIG.load(deps.storage)?; + +// let amount0 = funds +// .iter() +// .find(|c| c.denom == config.pool_config.token0) +// .map(|c| c.amount) +// .unwrap_or_default(); +// let amount1 = funds +// .iter() +// .find(|c| c.denom == config.pool_config.token1) +// .map(|c| c.amount) +// .unwrap_or_default(); + +// // We take the biggest amount and simulate a swap for the corresponding asset +// let price = if amount0 > amount1 { +// let simulation_result = app.ans_dex(deps, OSMOSIS.to_string()).simulate_swap( +// AnsAsset::new(config.pool_config.asset0, amount0), +// config.pool_config.asset1, +// )?; + +// let price = Decimal::from_ratio(amount0, simulation_result.return_amount); +// if let Some(belief_price) = belief_price1 { +// ensure!( +// belief_price.abs_diff(price) <= max_spread.unwrap_or(DEFAULT_SLIPPAGE), +// AppError::MaxSpreadAssertion { price } +// ); +// } +// price +// } else { +// let simulation_result = app.ans_dex(deps, OSMOSIS.to_string()).simulate_swap( +// AnsAsset::new(config.pool_config.asset1, amount1), +// config.pool_config.asset0, +// )?; + +// let price = Decimal::from_ratio(simulation_result.return_amount, amount1); +// if let Some(belief_price) = belief_price0 { +// ensure!( +// belief_price.abs_diff(price) <= max_spread.unwrap_or(DEFAULT_SLIPPAGE), +// AppError::MaxSpreadAssertion { price } +// ); +// } +// price +// }; + +// Ok(price) +// } + +#[cw_serde] +pub struct OsmosisPosition { + pub position_id: u64, +} + +pub fn get_position(deps: Deps) -> AppResult { + OSMOSIS_POSITION + .load(deps.storage) + .map_err(|_| AppError::NoPosition {}) +} + +pub fn get_osmosis_position(deps: Deps) -> AppResult { + let position = get_position(deps)?; + + ConcentratedliquidityQuerier::new(&deps.querier) + .position_by_id(position.position_id) + .map_err(|e| AppError::UnableToQueryPosition(position.position_id, e))? + .position + .ok_or(AppError::NoPosition {}) +} diff --git a/contracts/carrot-app/src/yield_sources/yield_type.rs b/contracts/carrot-app/src/yield_sources/yield_type.rs new file mode 100644 index 00000000..699a104a --- /dev/null +++ b/contracts/carrot-app/src/yield_sources/yield_type.rs @@ -0,0 +1,84 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{coins, Coin, CosmosMsg, Deps, Env, SubMsg, Uint128}; + +use crate::contract::{App, AppResult}; + +use super::{mars, osmosis_cl_pool}; + +#[cw_serde] +pub enum YieldType { + /// For osmosis CL Pools, you need a pool id to do your deposit, and that's all + ConcentratedLiquidityPool(ConcentratedPoolParams), + /// For Mars CL Pools, you just need to deposit in the RedBank + /// You need to indicate the denom of the funds you want to deposit + Mars(String), +} + +impl YieldType { + pub fn deposit( + self, + deps: Deps, + env: &Env, + funds: Vec, + app: &App, + ) -> AppResult> { + match self { + YieldType::ConcentratedLiquidityPool(params) => { + osmosis_cl_pool::deposit(deps, env, params, funds, app) + } + YieldType::Mars(denom) => mars::deposit(deps, denom, funds[0].amount, app), + } + } + + pub fn withdraw( + self, + deps: Deps, + amount: Option, + app: &App, + ) -> AppResult> { + match self { + YieldType::ConcentratedLiquidityPool(_params) => { + osmosis_cl_pool::withdraw(deps, amount, app) + } + YieldType::Mars(denom) => mars::withdraw(deps, denom, amount, app), + } + } + + pub fn user_deposit(&self, deps: Deps, app: &App) -> AppResult> { + match self { + YieldType::ConcentratedLiquidityPool(_params) => { + osmosis_cl_pool::user_deposit(deps, app) + } + YieldType::Mars(denom) => Ok(coins( + mars::user_deposit(deps, denom.clone(), app)?.into(), + denom, + )), + } + } + + pub fn user_rewards(&self, deps: Deps, app: &App) -> AppResult> { + match self { + YieldType::ConcentratedLiquidityPool(_params) => { + osmosis_cl_pool::user_rewards(deps, app) + } + YieldType::Mars(denom) => mars::user_rewards(deps, denom.clone(), app), + } + } + + pub fn user_liquidity(&self, deps: Deps, app: &App) -> AppResult { + match self { + YieldType::ConcentratedLiquidityPool(_params) => { + osmosis_cl_pool::user_liquidity(deps, app) + } + YieldType::Mars(denom) => mars::user_liquidity(deps, denom.clone(), app), + } + } +} + +#[cw_serde] +pub struct ConcentratedPoolParams { + pub pool_id: u64, + pub lower_tick: i64, + pub upper_tick: i64, + pub position_id: u64, +} diff --git a/contracts/carrot-app/tests/autocompound.rs b/contracts/carrot-app/tests/autocompound.rs index a25b2627..9fc1fdcc 100644 --- a/contracts/carrot-app/tests/autocompound.rs +++ b/contracts/carrot-app/tests/autocompound.rs @@ -1,165 +1,165 @@ -mod common; - -use crate::common::{ - create_position, setup_test_tube, DEX_NAME, GAS_DENOM, LOTS, REWARD_DENOM, USDC, USDT, -}; -use abstract_app::abstract_interface::{Abstract, AbstractAccount}; -use carrot_app::msg::{ - AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse, AvailableRewardsResponse, - CompoundStatus, CompoundStatusResponse, -}; -use cosmwasm_std::{coin, coins, Uint128}; -use cw_asset::AssetBase; -use cw_orch::osmosis_test_tube::osmosis_test_tube::Account; -use cw_orch::{anyhow, prelude::*}; - -#[test] -fn check_autocompound() -> anyhow::Result<()> { - let (_, carrot_app) = setup_test_tube(false)?; - - let chain = carrot_app.get_chain().clone(); - - // Create position - create_position( - &carrot_app, - coins(100_000, USDT.to_owned()), - coin(1_000_000, USDT.to_owned()), - coin(1_000_000, USDC.to_owned()), - )?; - - // Do some swaps - let dex: abstract_dex_adapter::interface::DexAdapter<_> = carrot_app.module()?; - let abs = Abstract::load_from(chain.clone())?; - let account_id = carrot_app.account().id()?; - let account = AbstractAccount::new(&abs, account_id); - chain.bank_send( - account.proxy.addr_str()?, - vec![ - coin(200_000, USDC.to_owned()), - coin(200_000, USDT.to_owned()), - ], - )?; - for _ in 0..10 { - dex.ans_swap((USDC, 50_000), USDT, DEX_NAME.to_string(), &account)?; - dex.ans_swap((USDT, 50_000), USDC, DEX_NAME.to_string(), &account)?; - } - - // Check autocompound adds liquidity from the rewards and user balance remain unchanged - - // Check it has some rewards to autocompound first - let rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; - assert!(!rewards.available_rewards.is_empty()); - - // Save balances - let balance_before_autocompound: AssetsBalanceResponse = carrot_app.balance()?; - let balance_usdc_before_autocompound = chain - .bank_querier() - .balance(chain.sender(), Some(USDC.to_owned()))? - .pop() - .unwrap(); - let balance_usdt_before_autocompound = chain - .bank_querier() - .balance(chain.sender(), Some(USDT.to_owned()))? - .pop() - .unwrap(); - - // Autocompound - chain.wait_seconds(300)?; - carrot_app.autocompound()?; - - // Save new balances - let balance_after_autocompound: AssetsBalanceResponse = carrot_app.balance()?; - let balance_usdc_after_autocompound = chain - .bank_querier() - .balance(chain.sender(), Some(USDC.to_owned()))? - .pop() - .unwrap(); - let balance_usdt_after_autocompound = chain - .bank_querier() - .balance(chain.sender(), Some(USDT.to_owned()))? - .pop() - .unwrap(); - - // Liquidity added - assert!(balance_after_autocompound.liquidity > balance_before_autocompound.liquidity); - // Only rewards went in there - assert!(balance_usdc_after_autocompound.amount >= balance_usdc_before_autocompound.amount); - assert!(balance_usdt_after_autocompound.amount >= balance_usdt_before_autocompound.amount,); - // Check it used all of the rewards - let rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; - assert!(rewards.available_rewards.is_empty()); - - Ok(()) -} - -#[test] -fn stranger_autocompound() -> anyhow::Result<()> { - let (_, carrot_app) = setup_test_tube(false)?; - - let mut chain = carrot_app.get_chain().clone(); - let stranger = chain.init_account(coins(LOTS, GAS_DENOM))?; - - // Create position - create_position( - &carrot_app, - coins(100_000, USDT.to_owned()), - coin(1_000_000, USDT.to_owned()), - coin(1_000_000, USDC.to_owned()), - )?; - - // Do some swaps - let dex: abstract_dex_adapter::interface::DexAdapter<_> = carrot_app.module()?; - let abs = Abstract::load_from(chain.clone())?; - let account_id = carrot_app.account().id()?; - let account = AbstractAccount::new(&abs, account_id); - chain.bank_send( - account.proxy.addr_str()?, - vec![ - coin(200_000, USDC.to_owned()), - coin(200_000, USDT.to_owned()), - ], - )?; - for _ in 0..10 { - dex.ans_swap((USDC, 50_000), USDT, DEX_NAME.to_string(), &account)?; - dex.ans_swap((USDT, 50_000), USDC, DEX_NAME.to_string(), &account)?; - } - - // Check autocompound adds liquidity from the rewards, user balance remain unchanged - // and rewards gets passed to the "stranger" - - // Check it has some rewards to autocompound first - let rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; - assert!(!rewards.available_rewards.is_empty()); - - // Save balances - let balance_before_autocompound: AssetsBalanceResponse = carrot_app.balance()?; - - // Autocompound by stranger - chain.wait_seconds(300)?; - // Check query is able to compute rewards, when swap is required - let compound_status: CompoundStatusResponse = carrot_app.compound_status()?; - assert_eq!( - compound_status, - CompoundStatusResponse { - status: CompoundStatus::Ready {}, - reward: AssetBase::native(REWARD_DENOM, 1000u128), - rewards_available: true - } - ); - carrot_app.call_as(&stranger).autocompound()?; - - // Save new balances - let balance_after_autocompound: AssetsBalanceResponse = carrot_app.balance()?; - - // Liquidity added - assert!(balance_after_autocompound.liquidity > balance_before_autocompound.liquidity); - - // Check it used all of the rewards - let rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; - assert!(rewards.available_rewards.is_empty()); - - // Check stranger gets rewarded - let stranger_reward_balance = chain.query_balance(stranger.address().as_str(), REWARD_DENOM)?; - assert_eq!(stranger_reward_balance, Uint128::new(1000)); - Ok(()) -} +// mod common; + +// use crate::common::{ +// create_position, setup_test_tube, DEX_NAME, GAS_DENOM, LOTS, REWARD_DENOM, USDC, USDT, +// }; +// use abstract_app::abstract_interface::{Abstract, AbstractAccount}; +// use carrot_app::msg::{ +// AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse, AvailableRewardsResponse, +// CompoundStatus, CompoundStatusResponse, +// }; +// use cosmwasm_std::{coin, coins, Uint128}; +// use cw_asset::AssetBase; +// use cw_orch::osmosis_test_tube::osmosis_test_tube::Account; +// use cw_orch::{anyhow, prelude::*}; + +// #[test] +// fn check_autocompound() -> anyhow::Result<()> { +// let (_, carrot_app) = setup_test_tube(false)?; + +// let chain = carrot_app.get_chain().clone(); + +// // Create position +// create_position( +// &carrot_app, +// coins(100_000, USDT.to_owned()), +// coin(1_000_000, USDT.to_owned()), +// coin(1_000_000, USDC.to_owned()), +// )?; + +// // Do some swaps +// let dex: abstract_dex_adapter::interface::DexAdapter<_> = carrot_app.module()?; +// let abs = Abstract::load_from(chain.clone())?; +// let account_id = carrot_app.account().id()?; +// let account = AbstractAccount::new(&abs, account_id); +// chain.bank_send( +// account.proxy.addr_str()?, +// vec![ +// coin(200_000, USDC.to_owned()), +// coin(200_000, USDT.to_owned()), +// ], +// )?; +// for _ in 0..10 { +// dex.ans_swap((USDC, 50_000), USDT, DEX_NAME.to_string(), &account)?; +// dex.ans_swap((USDT, 50_000), USDC, DEX_NAME.to_string(), &account)?; +// } + +// // Check autocompound adds liquidity from the rewards and user balance remain unchanged + +// // Check it has some rewards to autocompound first +// let rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; +// assert!(!rewards.available_rewards.is_empty()); + +// // Save balances +// let balance_before_autocompound: AssetsBalanceResponse = carrot_app.balance()?; +// let balance_usdc_before_autocompound = chain +// .bank_querier() +// .balance(chain.sender(), Some(USDC.to_owned()))? +// .pop() +// .unwrap(); +// let balance_usdt_before_autocompound = chain +// .bank_querier() +// .balance(chain.sender(), Some(USDT.to_owned()))? +// .pop() +// .unwrap(); + +// // Autocompound +// chain.wait_seconds(300)?; +// carrot_app.autocompound()?; + +// // Save new balances +// let balance_after_autocompound: AssetsBalanceResponse = carrot_app.balance()?; +// let balance_usdc_after_autocompound = chain +// .bank_querier() +// .balance(chain.sender(), Some(USDC.to_owned()))? +// .pop() +// .unwrap(); +// let balance_usdt_after_autocompound = chain +// .bank_querier() +// .balance(chain.sender(), Some(USDT.to_owned()))? +// .pop() +// .unwrap(); + +// // Liquidity added +// assert!(balance_after_autocompound.liquidity > balance_before_autocompound.liquidity); +// // Only rewards went in there +// assert!(balance_usdc_after_autocompound.amount >= balance_usdc_before_autocompound.amount); +// assert!(balance_usdt_after_autocompound.amount >= balance_usdt_before_autocompound.amount,); +// // Check it used all of the rewards +// let rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; +// assert!(rewards.available_rewards.is_empty()); + +// Ok(()) +// } + +// #[test] +// fn stranger_autocompound() -> anyhow::Result<()> { +// let (_, carrot_app) = setup_test_tube(false)?; + +// let mut chain = carrot_app.get_chain().clone(); +// let stranger = chain.init_account(coins(LOTS, GAS_DENOM))?; + +// // Create position +// create_position( +// &carrot_app, +// coins(100_000, USDT.to_owned()), +// coin(1_000_000, USDT.to_owned()), +// coin(1_000_000, USDC.to_owned()), +// )?; + +// // Do some swaps +// let dex: abstract_dex_adapter::interface::DexAdapter<_> = carrot_app.module()?; +// let abs = Abstract::load_from(chain.clone())?; +// let account_id = carrot_app.account().id()?; +// let account = AbstractAccount::new(&abs, account_id); +// chain.bank_send( +// account.proxy.addr_str()?, +// vec![ +// coin(200_000, USDC.to_owned()), +// coin(200_000, USDT.to_owned()), +// ], +// )?; +// for _ in 0..10 { +// dex.ans_swap((USDC, 50_000), USDT, DEX_NAME.to_string(), &account)?; +// dex.ans_swap((USDT, 50_000), USDC, DEX_NAME.to_string(), &account)?; +// } + +// // Check autocompound adds liquidity from the rewards, user balance remain unchanged +// // and rewards gets passed to the "stranger" + +// // Check it has some rewards to autocompound first +// let rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; +// assert!(!rewards.available_rewards.is_empty()); + +// // Save balances +// let balance_before_autocompound: AssetsBalanceResponse = carrot_app.balance()?; + +// // Autocompound by stranger +// chain.wait_seconds(300)?; +// // Check query is able to compute rewards, when swap is required +// let compound_status: CompoundStatusResponse = carrot_app.compound_status()?; +// assert_eq!( +// compound_status, +// CompoundStatusResponse { +// status: CompoundStatus::Ready {}, +// reward: AssetBase::native(REWARD_DENOM, 1000u128), +// rewards_available: true +// } +// ); +// carrot_app.call_as(&stranger).autocompound()?; + +// // Save new balances +// let balance_after_autocompound: AssetsBalanceResponse = carrot_app.balance()?; + +// // Liquidity added +// assert!(balance_after_autocompound.liquidity > balance_before_autocompound.liquidity); + +// // Check it used all of the rewards +// let rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; +// assert!(rewards.available_rewards.is_empty()); + +// // Check stranger gets rewarded +// let stranger_reward_balance = chain.query_balance(stranger.address().as_str(), REWARD_DENOM)?; +// assert_eq!(stranger_reward_balance, Uint128::new(1000)); +// Ok(()) +// } diff --git a/contracts/carrot-app/tests/common.rs b/contracts/carrot-app/tests/common.rs index 1f301645..028eff15 100644 --- a/contracts/carrot-app/tests/common.rs +++ b/contracts/carrot-app/tests/common.rs @@ -7,9 +7,11 @@ use abstract_app::objects::module::ModuleInfo; use abstract_client::{AbstractClient, Application, Environment, Namespace}; use abstract_dex_adapter::DEX_ADAPTER_ID; use abstract_sdk::core::manager::{self, ModuleInstallConfig}; -use carrot_app::contract::APP_ID; -use carrot_app::msg::{AppInstantiateMsg, CreatePositionMessage}; -use carrot_app::state::AutocompoundRewardsConfig; +use carrot_app::contract::{APP_ID, OSMOSIS}; +use carrot_app::msg::AppInstantiateMsg; +use carrot_app::state::{AutocompoundConfig, AutocompoundRewardsConfig}; +use carrot_app::yield_sources::yield_type::{ConcentratedPoolParams, YieldType}; +use carrot_app::yield_sources::{BalanceStrategy, BalanceStrategyElement, YieldSource}; use cosmwasm_std::{coin, coins, to_json_binary, to_json_vec, Decimal, Uint128, Uint64}; use cw_asset::AssetInfoUnchecked; use cw_orch::osmosis_test_tube::osmosis_test_tube::Gamm; @@ -64,7 +66,7 @@ pub fn deploy( chain: Chain, pool_id: u64, gas_pool_id: u64, - create_position: Option, + initial_deposit: Option>, ) -> anyhow::Result>> { let asset0 = USDT.to_owned(); let asset1 = USDC.to_owned(); @@ -114,66 +116,39 @@ pub fn deploy( // The savings app publisher.publish_app::>()?; - let create_position_on_init = create_position.is_some(); let init_msg = AppInstantiateMsg { - pool_id, // 5 mins - autocompound_cooldown_seconds: Uint64::new(300), - autocompound_rewards_config: AutocompoundRewardsConfig { - gas_asset: AssetEntry::new(REWARD_ASSET), - swap_asset: AssetEntry::new(USDC), - reward: Uint128::new(1000), - min_gas_balance: Uint128::new(2000), - max_gas_balance: Uint128::new(10000), + autocompound_config: AutocompoundConfig { + cooldown_seconds: Uint64::new(300), + rewards: AutocompoundRewardsConfig { + gas_asset: AssetEntry::new(REWARD_ASSET), + swap_asset: AssetEntry::new(USDC), + reward: Uint128::new(1000), + min_gas_balance: Uint128::new(2000), + max_gas_balance: Uint128::new(10000), + }, }, - create_position, + balance_strategy: BalanceStrategy(vec![BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: vec![ + (USDT.to_string(), Decimal::percent(50)), + (USDC.to_string(), Decimal::percent(50)), + ], + ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + pool_id, + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: 0, + }), + }, + share: Decimal::one(), + }]), + deposit: initial_deposit, + dex: OSMOSIS.to_string(), }; - // If we create position on instantiate - give auth - let carrot_app = if create_position_on_init { - // TODO: We can't get account factory or module factory objects from the client. - // get Account id of the upcoming sub-account - let next_local_account_id = client.next_local_account_id()?; - - let savings_app_addr = client - .module_instantiate2_address::>(&AccountId::local( - next_local_account_id, - ))?; - // Give all authzs and create subaccount with app in single tx - let mut msgs = give_authorizations_msgs(&client, savings_app_addr)?; - let create_sub_account_message = Any { - type_url: MsgExecuteContract::TYPE_URL.to_owned(), - value: MsgExecuteContract { - sender: chain.sender().to_string(), - contract: publisher.account().manager()?.to_string(), - msg: to_json_vec(&manager::ExecuteMsg::CreateSubAccount { - name: "bob".to_owned(), - description: None, - link: None, - base_asset: None, - namespace: None, - install_modules: vec![ - ModuleInstallConfig::new(ModuleInfo::from_id_latest(DEX_ADAPTER_ID)?, None), - ModuleInstallConfig::new( - ModuleInfo::from_id_latest(APP_ID)?, - Some(to_json_binary(&init_msg)?), - ), - ], - account_id: Some(next_local_account_id), - })?, - funds: vec![], - } - .to_proto_bytes(), - }; - msgs.push(create_sub_account_message); - let _ = chain.commit_any::(msgs, None)?; - - // Now get Application struct - let account = client.account_from(AccountId::local(next_local_account_id))?; - account.application::>()? - } else { - // We install the carrot-app - let carrot_app: Application> = + // We install the carrot-app + let carrot_app: Application> = publisher .account() .install_app_with_dependencies::>( @@ -181,8 +156,6 @@ pub fn deploy( Empty {}, &[], )?; - carrot_app - }; // We update authorized addresses on the adapter for the app dex_adapter.execute( &abstract_dex_adapter::msg::ExecuteMsg::Base( @@ -200,29 +173,6 @@ pub fn deploy( Ok(carrot_app) } -pub fn create_position( - app: &Application>, - funds: Vec, - asset0: Coin, - asset1: Coin, -) -> anyhow::Result { - app.execute( - &carrot_app::msg::AppExecuteMsg::CreatePosition(CreatePositionMessage { - lower_tick: INITIAL_LOWER_TICK, - upper_tick: INITIAL_UPPER_TICK, - funds, - asset0, - asset1, - max_spread: None, - belief_price0: None, - belief_price1: None, - }) - .into(), - None, - ) - .map_err(Into::into) -} - pub fn create_pool(mut chain: OsmosisTestTube) -> anyhow::Result<(u64, u64)> { chain.add_balance(chain.sender(), coins(LOTS, USDC))?; chain.add_balance(chain.sender(), coins(LOTS, USDT))?; @@ -322,107 +272,10 @@ pub fn setup_test_tube( // We create a usdt-usdc pool let (pool_id, gas_pool_id) = create_pool(chain.clone())?; - let create_position_msg = create_position.then(|| + let initial_deposit = create_position.then(|| // TODO: Requires instantiate2 to test it (we need to give authz authorization before instantiating) - CreatePositionMessage { - lower_tick: INITIAL_LOWER_TICK, - upper_tick: INITIAL_UPPER_TICK, - funds: coins(100_000, USDT), - asset0: coin(1_000_000, USDT), - asset1: coin(1_000_000, USDC), - max_spread: None, - belief_price0: None, - belief_price1: None, - }); - let carrot_app = deploy(chain.clone(), pool_id, gas_pool_id, create_position_msg)?; + vec![coin(1_000_000, USDT),coin(1_000_000, USDC)]); + let carrot_app = deploy(chain.clone(), pool_id, gas_pool_id, initial_deposit)?; - // Give authorizations if not given already - if !create_position { - let client = AbstractClient::new(chain)?; - give_authorizations(&client, carrot_app.addr_str()?)?; - } Ok((pool_id, carrot_app)) } - -pub fn give_authorizations_msgs( - client: &AbstractClient, - savings_app_addr: impl Into, -) -> Result, anyhow::Error> { - let dex_fee_account = client.account_from(AccountId::local(0))?; - let dex_fee_addr = dex_fee_account.proxy()?.to_string(); - let chain = client.environment().clone(); - - let authorization_urls = [ - MsgCreatePosition::TYPE_URL, - MsgSwapExactAmountIn::TYPE_URL, - MsgAddToPosition::TYPE_URL, - MsgWithdrawPosition::TYPE_URL, - MsgCollectIncentives::TYPE_URL, - MsgCollectSpreadRewards::TYPE_URL, - ] - .map(ToOwned::to_owned); - let savings_app_addr: String = savings_app_addr.into(); - let granter = chain.sender().to_string(); - let grantee = savings_app_addr.clone(); - - let dex_spend_limit = vec![ - cw_orch::osmosis_test_tube::osmosis_test_tube::osmosis_std::types::cosmos::base::v1beta1::Coin { - denom: USDC.to_owned(), - amount: LOTS.to_string(), - }, - cw_orch::osmosis_test_tube::osmosis_test_tube::osmosis_std::types::cosmos::base::v1beta1::Coin { - denom: USDT.to_owned(), - amount: LOTS.to_string(), - }, - cw_orch::osmosis_test_tube::osmosis_test_tube::osmosis_std::types::cosmos::base::v1beta1::Coin { - denom: REWARD_DENOM.to_owned(), - amount: LOTS.to_string(), - }]; - let dex_fee_authorization = Any { - value: MsgGrant { - granter: chain.sender().to_string(), - grantee: grantee.clone(), - grant: Some(Grant { - authorization: Some( - SendAuthorization { - spend_limit: dex_spend_limit, - allow_list: vec![dex_fee_addr, savings_app_addr], - } - .to_any(), - ), - expiration: None, - }), - } - .encode_to_vec(), - type_url: MsgGrant::TYPE_URL.to_owned(), - }; - - let msgs: Vec = authorization_urls - .into_iter() - .map(|msg| Any { - value: MsgGrant { - granter: granter.clone(), - grantee: grantee.clone(), - grant: Some(Grant { - authorization: Some(GenericAuthorization { msg }.to_any()), - expiration: None, - }), - } - .encode_to_vec(), - type_url: MsgGrant::TYPE_URL.to_owned(), - }) - .chain(iter::once(dex_fee_authorization)) - .collect(); - Ok(msgs) -} - -pub fn give_authorizations( - client: &AbstractClient, - savings_app_addr: impl Into, -) -> Result<(), anyhow::Error> { - let msgs = give_authorizations_msgs(client, savings_app_addr)?; - client - .environment() - .commit_any::(msgs, None)?; - Ok(()) -} diff --git a/contracts/carrot-app/tests/deposit_withdraw.rs b/contracts/carrot-app/tests/deposit_withdraw.rs index d472e48a..8bd10c66 100644 --- a/contracts/carrot-app/tests/deposit_withdraw.rs +++ b/contracts/carrot-app/tests/deposit_withdraw.rs @@ -1,7 +1,10 @@ mod common; -use crate::common::{create_position, setup_test_tube, USDC, USDT}; -use carrot_app::msg::{AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse, PositionResponse}; +use crate::common::{setup_test_tube, USDC, USDT}; +use carrot_app::{ + msg::{AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse}, + yield_sources::osmosis_cl_pool::OsmosisPosition, +}; use cosmwasm_std::{coin, coins, Decimal, Uint128}; use cw_orch::{ anyhow, @@ -19,28 +22,17 @@ fn deposit_lands() -> anyhow::Result<()> { let deposit_amount = 5_000; let max_fee = Uint128::new(deposit_amount).mul_floor(Decimal::percent(3)); - // Create position - create_position( - &carrot_app, - coins(deposit_amount, USDT.to_owned()), - coin(1_000_000, USDT.to_owned()), - coin(1_000_000, USDC.to_owned()), + + // We should add funds to the account proxy + let deposit_coins = coins(deposit_amount, USDT.to_owned()); + let mut chain = carrot_app.get_chain().clone(); + chain.add_balance( + carrot_app.account().proxy()?.to_string(), + deposit_coins.clone(), )?; - // Check almost everything landed - let balance: AssetsBalanceResponse = carrot_app.balance()?; - let sum = balance - .balances - .iter() - .fold(Uint128::zero(), |acc, e| acc + e.amount); - assert!(sum.u128() > deposit_amount - max_fee.u128()); // Do the deposit - carrot_app.deposit( - vec![coin(deposit_amount, USDT.to_owned())], - None, - None, - None, - )?; + carrot_app.deposit(deposit_coins.clone())?; // Check almost everything landed let balance: AssetsBalanceResponse = carrot_app.balance()?; let sum = balance @@ -50,12 +42,7 @@ fn deposit_lands() -> anyhow::Result<()> { assert!(sum.u128() > (deposit_amount - max_fee.u128()) * 2); // Do the second deposit - carrot_app.deposit( - vec![coin(deposit_amount, USDT.to_owned())], - None, - None, - None, - )?; + carrot_app.deposit(vec![coin(deposit_amount, USDT.to_owned())])?; // Check almost everything landed let balance: AssetsBalanceResponse = carrot_app.balance()?; let sum = balance @@ -72,13 +59,7 @@ fn withdraw_position() -> anyhow::Result<()> { let chain = carrot_app.get_chain().clone(); - // Create position - create_position( - &carrot_app, - coins(10_000, USDT.to_owned()), - coin(1_000_000, USDT.to_owned()), - coin(1_000_000, USDC.to_owned()), - )?; + carrot_app.deposit(coins(10_000, USDT.to_owned()))?; let balance: AssetsBalanceResponse = carrot_app.balance()?; let balance_usdc_before_withdraw = chain @@ -92,10 +73,10 @@ fn withdraw_position() -> anyhow::Result<()> { .pop() .unwrap(); - // Withdraw half of liquidity - let liquidity_amount: Uint128 = balance.liquidity.parse().unwrap(); + // Withdraw some of the value + let liquidity_amount: Uint128 = balance.balances[0].amount; let half_of_liquidity = liquidity_amount / Uint128::new(2); - carrot_app.withdraw(half_of_liquidity)?; + carrot_app.withdraw(Some(half_of_liquidity))?; let balance_usdc_after_half_withdraw = chain .bank_querier() @@ -112,7 +93,7 @@ fn withdraw_position() -> anyhow::Result<()> { assert!(balance_usdt_after_half_withdraw.amount > balance_usdt_before_withdraw.amount); // Withdraw rest of liquidity - carrot_app.withdraw_all()?; + carrot_app.withdraw(None)?; let balance_usdc_after_full_withdraw = chain .bank_querier() .balance(chain.sender(), Some(USDT.to_owned()))? @@ -130,131 +111,119 @@ fn withdraw_position() -> anyhow::Result<()> { } #[test] -fn deposit_both_assets() -> anyhow::Result<()> { +fn deposit_multiple_assets() -> anyhow::Result<()> { let (_, carrot_app) = setup_test_tube(false)?; - // Create position - create_position( - &carrot_app, - coins(10_000, USDT.to_owned()), - coin(1_000_000, USDT.to_owned()), - coin(1_000_000, USDC.to_owned()), - )?; + carrot_app.deposit(vec![coin(258, USDT.to_owned()), coin(234, USDC.to_owned())])?; - carrot_app.deposit( - vec![coin(258, USDT.to_owned()), coin(234, USDC.to_owned())], - None, - None, - None, - )?; - - Ok(()) -} - -#[test] -fn create_position_on_instantiation() -> anyhow::Result<()> { - let (_, carrot_app) = setup_test_tube(true)?; - - let position: PositionResponse = carrot_app.position()?; - assert!(position.position.is_some()); Ok(()) } -#[test] -fn withdraw_after_user_withdraw_liquidity_manually() -> anyhow::Result<()> { - let (_, carrot_app) = setup_test_tube(true)?; - let chain = carrot_app.get_chain().clone(); - - let position: PositionResponse = carrot_app.position()?; - let position_id = position.position.unwrap().position_id; - - let test_tube = chain.app.borrow(); - let cl = ConcentratedLiquidity::new(&*test_tube); - let position_breakdown = cl - .query_position_by_id(&PositionByIdRequest { position_id })? - .position - .unwrap(); - let position = position_breakdown.position.unwrap(); - - cl.withdraw_position( - MsgWithdrawPosition { - position_id: position.position_id, - sender: chain.sender().to_string(), - liquidity_amount: position.liquidity, - }, - &chain.sender, - )?; - - // Ensure it errors - carrot_app.withdraw_all().unwrap_err(); - - // Ensure position deleted - let position_not_found = cl - .query_position_by_id(&PositionByIdRequest { position_id }) - .unwrap_err(); - assert!(position_not_found - .to_string() - .contains("position not found")); - Ok(()) -} - -#[test] -fn deposit_slippage() -> anyhow::Result<()> { - let (_, carrot_app) = setup_test_tube(false)?; - - let deposit_amount = 5_000; - let max_fee = Uint128::new(deposit_amount).mul_floor(Decimal::percent(3)); - // Create position - create_position( - &carrot_app, - coins(deposit_amount, USDT.to_owned()), - coin(1_000_000, USDT.to_owned()), - coin(1_000_000, USDC.to_owned()), - )?; - - // Do the deposit of asset0 with incorrect belief_price1 - let e = carrot_app - .deposit( - vec![coin(deposit_amount, USDT.to_owned())], - None, - Some(Decimal::zero()), - None, - ) - .unwrap_err(); - assert!(e.to_string().contains("exceeds max spread limit")); - - // Do the deposit of asset1 with incorrect belief_price0 - let e = carrot_app - .deposit( - vec![coin(deposit_amount, USDC.to_owned())], - Some(Decimal::zero()), - None, - None, - ) - .unwrap_err(); - assert!(e.to_string().contains("exceeds max spread limit")); - - // Do the deposits of asset0 with correct belief_price - carrot_app.deposit( - vec![coin(deposit_amount, USDT.to_owned())], - None, - Some(Decimal::one()), - Some(Decimal::percent(10)), - )?; - // Do the deposits of asset1 with correct belief_price - carrot_app.deposit( - vec![coin(deposit_amount, USDT.to_owned())], - Some(Decimal::one()), - None, - Some(Decimal::percent(10)), - )?; - - // Check almost everything landed - let balance: AssetsBalanceResponse = carrot_app.balance()?; - let sum = balance - .balances - .iter() - .fold(Uint128::zero(), |acc, e| acc + e.amount); - assert!(sum.u128() > (deposit_amount - max_fee.u128()) * 3); - Ok(()) -} +// #[test] +// fn create_position_on_instantiation() -> anyhow::Result<()> { +// let (_, carrot_app) = setup_test_tube(true)?; +// carrot_app.deposit(vec![coin(258, USDT.to_owned()), coin(234, USDC.to_owned())])?; + +// let position: OsmosisPositionResponse = carrot_app.position()?; +// assert!(position.position.is_some()); +// Ok(()) +// } + +// #[test] +// fn withdraw_after_user_withdraw_liquidity_manually() -> anyhow::Result<()> { +// let (_, carrot_app) = setup_test_tube(true)?; +// let chain = carrot_app.get_chain().clone(); + +// let position: PositionResponse = carrot_app.position()?; +// let position_id = position.position.unwrap().position_id; + +// let test_tube = chain.app.borrow(); +// let cl = ConcentratedLiquidity::new(&*test_tube); +// let position_breakdown = cl +// .query_position_by_id(&PositionByIdRequest { position_id })? +// .position +// .unwrap(); +// let position = position_breakdown.position.unwrap(); + +// cl.withdraw_position( +// MsgWithdrawPosition { +// position_id: position.position_id, +// sender: chain.sender().to_string(), +// liquidity_amount: position.liquidity, +// }, +// &chain.sender, +// )?; + +// // Ensure it errors +// carrot_app.withdraw_all().unwrap_err(); + +// // Ensure position deleted +// let position_not_found = cl +// .query_position_by_id(&PositionByIdRequest { position_id }) +// .unwrap_err(); +// assert!(position_not_found +// .to_string() +// .contains("position not found")); +// Ok(()) +// } + +// #[test] +// fn deposit_slippage() -> anyhow::Result<()> { +// let (_, carrot_app) = setup_test_tube(false)?; + +// let deposit_amount = 5_000; +// let max_fee = Uint128::new(deposit_amount).mul_floor(Decimal::percent(3)); +// // Create position +// create_position( +// &carrot_app, +// coins(deposit_amount, USDT.to_owned()), +// coin(1_000_000, USDT.to_owned()), +// coin(1_000_000, USDC.to_owned()), +// )?; + +// // Do the deposit of asset0 with incorrect belief_price1 +// let e = carrot_app +// .deposit( +// vec![coin(deposit_amount, USDT.to_owned())], +// None, +// Some(Decimal::zero()), +// None, +// ) +// .unwrap_err(); +// assert!(e.to_string().contains("exceeds max spread limit")); + +// // Do the deposit of asset1 with incorrect belief_price0 +// let e = carrot_app +// .deposit( +// vec![coin(deposit_amount, USDC.to_owned())], +// Some(Decimal::zero()), +// None, +// None, +// ) +// .unwrap_err(); +// assert!(e.to_string().contains("exceeds max spread limit")); + +// // Do the deposits of asset0 with correct belief_price +// carrot_app.deposit( +// vec![coin(deposit_amount, USDT.to_owned())], +// None, +// Some(Decimal::one()), +// Some(Decimal::percent(10)), +// )?; +// // Do the deposits of asset1 with correct belief_price +// carrot_app.deposit( +// vec![coin(deposit_amount, USDT.to_owned())], +// Some(Decimal::one()), +// None, +// Some(Decimal::percent(10)), +// )?; + +// // Check almost everything landed +// let balance: AssetsBalanceResponse = carrot_app.balance()?; +// let sum = balance +// .balances +// .iter() +// .fold(Uint128::zero(), |acc, e| acc + e.amount); +// assert!(sum.u128() > (deposit_amount - max_fee.u128()) * 3); +// Ok(()) +// } diff --git a/contracts/carrot-app/tests/recreate_position.rs b/contracts/carrot-app/tests/recreate_position.rs index f4109645..a7cac577 100644 --- a/contracts/carrot-app/tests/recreate_position.rs +++ b/contracts/carrot-app/tests/recreate_position.rs @@ -1,265 +1,265 @@ -mod common; - -use crate::common::{ - create_position, give_authorizations, setup_test_tube, INITIAL_LOWER_TICK, INITIAL_UPPER_TICK, - USDC, USDT, -}; -use abstract_app::objects::{AccountId, AssetEntry}; -use abstract_client::{AbstractClient, Environment}; -use carrot_app::error::AppError; -use carrot_app::msg::{ - AppExecuteMsgFns, AppInstantiateMsg, AppQueryMsgFns, AssetsBalanceResponse, - CreatePositionMessage, PositionResponse, -}; -use carrot_app::state::AutocompoundRewardsConfig; -use common::REWARD_ASSET; -use cosmwasm_std::{coin, coins, Uint128, Uint64}; -use cw_orch::{ - anyhow, - osmosis_test_tube::osmosis_test_tube::{ - osmosis_std::types::osmosis::concentratedliquidity::v1beta1::MsgWithdrawPosition, - ConcentratedLiquidity, Module, - }, - prelude::*, -}; -use osmosis_std::types::osmosis::concentratedliquidity::v1beta1::PositionByIdRequest; - -#[test] -fn create_multiple_positions() -> anyhow::Result<()> { - let (_, carrot_app) = setup_test_tube(false)?; - - // Create position - create_position( - &carrot_app, - coins(10_000, USDT.to_owned()), - coin(1_000_000, USDT.to_owned()), - coin(1_000_000, USDC.to_owned()), - )?; - - // Create position second time, it should fail - let position_err = create_position( - &carrot_app, - coins(5_000, USDT.to_owned()), - coin(1_000_000, USDT.to_owned()), - coin(1_000_000, USDC.to_owned()), - ) - .unwrap_err(); - - assert!(position_err - .to_string() - .contains(&AppError::PositionExists {}.to_string())); - Ok(()) -} - -#[test] -fn create_multiple_positions_after_withdraw() -> anyhow::Result<()> { - let (_, carrot_app) = setup_test_tube(false)?; - - // Create position - create_position( - &carrot_app, - coins(10_000, USDT.to_owned()), - coin(1_000_000, USDT.to_owned()), - coin(1_000_000, USDC.to_owned()), - )?; - - // Withdraw half of liquidity - let balance: AssetsBalanceResponse = carrot_app.balance()?; - let liquidity_amount: Uint128 = balance.liquidity.parse().unwrap(); - let half_of_liquidity = liquidity_amount / Uint128::new(2); - carrot_app.withdraw(half_of_liquidity)?; - - // Create position second time, it should fail - let position_err = create_position( - &carrot_app, - coins(5_000, USDT.to_owned()), - coin(1_000_000, USDT.to_owned()), - coin(1_000_000, USDC.to_owned()), - ) - .unwrap_err(); - - assert!(position_err - .to_string() - .contains(&AppError::PositionExists {}.to_string())); - - // Withdraw whole liquidity - let balance: AssetsBalanceResponse = carrot_app.balance()?; - let liquidity_amount: Uint128 = balance.liquidity.parse().unwrap(); - carrot_app.withdraw(liquidity_amount)?; - - // Create position second time, it should fail - create_position( - &carrot_app, - coins(5_000, USDT.to_owned()), - coin(1_000_000, USDT.to_owned()), - coin(1_000_000, USDC.to_owned()), - )?; - - Ok(()) -} - -#[test] -fn create_multiple_positions_after_withdraw_all() -> anyhow::Result<()> { - let (_, carrot_app) = setup_test_tube(false)?; - - // Create position - create_position( - &carrot_app, - coins(10_000, USDT.to_owned()), - coin(1_000_000, USDT.to_owned()), - coin(1_000_000, USDC.to_owned()), - )?; - - // Withdraw whole liquidity - carrot_app.withdraw_all()?; - - // Create position second time, it should succeed - create_position( - &carrot_app, - coins(5_000, USDT.to_owned()), - coin(1_000_000, USDT.to_owned()), - coin(1_000_000, USDC.to_owned()), - )?; - Ok(()) -} - -#[test] -fn create_position_after_user_withdraw_liquidity_manually() -> anyhow::Result<()> { - let (_, carrot_app) = setup_test_tube(true)?; - let chain = carrot_app.get_chain().clone(); - - let position = carrot_app.position()?; - - let test_tube = chain.app.borrow(); - let cl = ConcentratedLiquidity::new(&*test_tube); - let position_breakdown = cl - .query_position_by_id(&PositionByIdRequest { - position_id: position.position.unwrap().position_id, - })? - .position - .unwrap(); - let position = position_breakdown.position.unwrap(); - - cl.withdraw_position( - MsgWithdrawPosition { - position_id: position.position_id, - sender: chain.sender().to_string(), - liquidity_amount: position.liquidity, - }, - &chain.sender, - )?; - - // Create position, ignoring it was manually withdrawn - create_position( - &carrot_app, - coins(10_000, USDT.to_owned()), - coin(1_000_000, USDT.to_owned()), - coin(1_000_000, USDC.to_owned()), - )?; - - let position = carrot_app.position()?; - assert!(position.position.is_some()); - Ok(()) -} - -#[test] -fn install_on_sub_account() -> anyhow::Result<()> { - let (pool_id, app) = setup_test_tube(false)?; - let owner_account = app.account(); - let chain = owner_account.environment(); - let client = AbstractClient::new(chain)?; - let next_id = client.next_local_account_id()?; - - let init_msg = AppInstantiateMsg { - pool_id, - // 5 mins - autocompound_cooldown_seconds: Uint64::new(300), - autocompound_rewards_config: AutocompoundRewardsConfig { - gas_asset: AssetEntry::new(REWARD_ASSET), - swap_asset: AssetEntry::new(USDC), - reward: Uint128::new(1000), - min_gas_balance: Uint128::new(2000), - max_gas_balance: Uint128::new(10000), - }, - create_position: None, - }; - - let account = client - .account_builder() - .sub_account(owner_account) - .account_id(next_id) - .name("carrot-sub-acc") - .install_app_with_dependencies::>( - &init_msg, - Empty {}, - )? - .build()?; - - let carrot_app = account.application::>()?; - - give_authorizations(&client, carrot_app.addr_str()?)?; - create_position( - &carrot_app, - coins(10_000, USDT.to_owned()), - coin(1_000_000, USDT.to_owned()), - coin(1_000_000, USDC.to_owned()), - )?; - - let position: PositionResponse = carrot_app.position()?; - assert!(position.position.is_some()); - Ok(()) -} - -#[test] -fn install_on_sub_account_create_position_on_install() -> anyhow::Result<()> { - let (pool_id, app) = setup_test_tube(false)?; - let owner_account = app.account(); - let chain = owner_account.environment(); - let client = AbstractClient::new(chain)?; - let next_id = client.next_local_account_id()?; - let carrot_app_address = client - .module_instantiate2_address::>( - &AccountId::local(next_id), - )?; - - give_authorizations(&client, carrot_app_address)?; - let init_msg = AppInstantiateMsg { - pool_id, - // 5 mins - autocompound_cooldown_seconds: Uint64::new(300), - autocompound_rewards_config: AutocompoundRewardsConfig { - gas_asset: AssetEntry::new(REWARD_ASSET), - swap_asset: AssetEntry::new(USDC), - reward: Uint128::new(500_000), - min_gas_balance: Uint128::new(1_000_000), - max_gas_balance: Uint128::new(3_000_000), - }, - create_position: Some(CreatePositionMessage { - lower_tick: INITIAL_LOWER_TICK, - upper_tick: INITIAL_UPPER_TICK, - funds: coins(100_000, USDC), - asset0: coin(1_000_672_899, USDT), - asset1: coin(10_000_000_000, USDC), - max_spread: None, - belief_price0: None, - belief_price1: None, - }), - }; - - let account = client - .account_builder() - .sub_account(owner_account) - .account_id(next_id) - .name("carrot-sub-acc") - .install_app_with_dependencies::>( - &init_msg, - Empty {}, - )? - .build()?; - - let carrot_app = account.application::>()?; - - let position: PositionResponse = carrot_app.position()?; - assert!(position.position.is_some()); - Ok(()) -} +// mod common; + +// use crate::common::{ +// create_position, give_authorizations, setup_test_tube, INITIAL_LOWER_TICK, INITIAL_UPPER_TICK, +// USDC, USDT, +// }; +// use abstract_app::objects::{AccountId, AssetEntry}; +// use abstract_client::{AbstractClient, Environment}; +// use carrot_app::error::AppError; +// use carrot_app::msg::{ +// AppExecuteMsgFns, AppInstantiateMsg, AppQueryMsgFns, AssetsBalanceResponse, +// CreatePositionMessage, PositionResponse, +// }; +// use carrot_app::state::AutocompoundRewardsConfig; +// use common::REWARD_ASSET; +// use cosmwasm_std::{coin, coins, Uint128, Uint64}; +// use cw_orch::{ +// anyhow, +// osmosis_test_tube::osmosis_test_tube::{ +// osmosis_std::types::osmosis::concentratedliquidity::v1beta1::MsgWithdrawPosition, +// ConcentratedLiquidity, Module, +// }, +// prelude::*, +// }; +// use osmosis_std::types::osmosis::concentratedliquidity::v1beta1::PositionByIdRequest; + +// #[test] +// fn create_multiple_positions() -> anyhow::Result<()> { +// let (_, carrot_app) = setup_test_tube(false)?; + +// // Create position +// create_position( +// &carrot_app, +// coins(10_000, USDT.to_owned()), +// coin(1_000_000, USDT.to_owned()), +// coin(1_000_000, USDC.to_owned()), +// )?; + +// // Create position second time, it should fail +// let position_err = create_position( +// &carrot_app, +// coins(5_000, USDT.to_owned()), +// coin(1_000_000, USDT.to_owned()), +// coin(1_000_000, USDC.to_owned()), +// ) +// .unwrap_err(); + +// assert!(position_err +// .to_string() +// .contains(&AppError::PositionExists {}.to_string())); +// Ok(()) +// } + +// #[test] +// fn create_multiple_positions_after_withdraw() -> anyhow::Result<()> { +// let (_, carrot_app) = setup_test_tube(false)?; + +// // Create position +// create_position( +// &carrot_app, +// coins(10_000, USDT.to_owned()), +// coin(1_000_000, USDT.to_owned()), +// coin(1_000_000, USDC.to_owned()), +// )?; + +// // Withdraw half of liquidity +// let balance: AssetsBalanceResponse = carrot_app.balance()?; +// let liquidity_amount: Uint128 = balance.liquidity.parse().unwrap(); +// let half_of_liquidity = liquidity_amount / Uint128::new(2); +// carrot_app.withdraw(half_of_liquidity)?; + +// // Create position second time, it should fail +// let position_err = create_position( +// &carrot_app, +// coins(5_000, USDT.to_owned()), +// coin(1_000_000, USDT.to_owned()), +// coin(1_000_000, USDC.to_owned()), +// ) +// .unwrap_err(); + +// assert!(position_err +// .to_string() +// .contains(&AppError::PositionExists {}.to_string())); + +// // Withdraw whole liquidity +// let balance: AssetsBalanceResponse = carrot_app.balance()?; +// let liquidity_amount: Uint128 = balance.liquidity.parse().unwrap(); +// carrot_app.withdraw(liquidity_amount)?; + +// // Create position second time, it should fail +// create_position( +// &carrot_app, +// coins(5_000, USDT.to_owned()), +// coin(1_000_000, USDT.to_owned()), +// coin(1_000_000, USDC.to_owned()), +// )?; + +// Ok(()) +// } + +// #[test] +// fn create_multiple_positions_after_withdraw_all() -> anyhow::Result<()> { +// let (_, carrot_app) = setup_test_tube(false)?; + +// // Create position +// create_position( +// &carrot_app, +// coins(10_000, USDT.to_owned()), +// coin(1_000_000, USDT.to_owned()), +// coin(1_000_000, USDC.to_owned()), +// )?; + +// // Withdraw whole liquidity +// carrot_app.withdraw_all()?; + +// // Create position second time, it should succeed +// create_position( +// &carrot_app, +// coins(5_000, USDT.to_owned()), +// coin(1_000_000, USDT.to_owned()), +// coin(1_000_000, USDC.to_owned()), +// )?; +// Ok(()) +// } + +// #[test] +// fn create_position_after_user_withdraw_liquidity_manually() -> anyhow::Result<()> { +// let (_, carrot_app) = setup_test_tube(true)?; +// let chain = carrot_app.get_chain().clone(); + +// let position = carrot_app.position()?; + +// let test_tube = chain.app.borrow(); +// let cl = ConcentratedLiquidity::new(&*test_tube); +// let position_breakdown = cl +// .query_position_by_id(&PositionByIdRequest { +// position_id: position.position.unwrap().position_id, +// })? +// .position +// .unwrap(); +// let position = position_breakdown.position.unwrap(); + +// cl.withdraw_position( +// MsgWithdrawPosition { +// position_id: position.position_id, +// sender: chain.sender().to_string(), +// liquidity_amount: position.liquidity, +// }, +// &chain.sender, +// )?; + +// // Create position, ignoring it was manually withdrawn +// create_position( +// &carrot_app, +// coins(10_000, USDT.to_owned()), +// coin(1_000_000, USDT.to_owned()), +// coin(1_000_000, USDC.to_owned()), +// )?; + +// let position = carrot_app.position()?; +// assert!(position.position.is_some()); +// Ok(()) +// } + +// #[test] +// fn install_on_sub_account() -> anyhow::Result<()> { +// let (pool_id, app) = setup_test_tube(false)?; +// let owner_account = app.account(); +// let chain = owner_account.environment(); +// let client = AbstractClient::new(chain)?; +// let next_id = client.next_local_account_id()?; + +// let init_msg = AppInstantiateMsg { +// pool_id, +// // 5 mins +// autocompound_cooldown_seconds: Uint64::new(300), +// autocompound_rewards_config: AutocompoundRewardsConfig { +// gas_asset: AssetEntry::new(REWARD_ASSET), +// swap_asset: AssetEntry::new(USDC), +// reward: Uint128::new(1000), +// min_gas_balance: Uint128::new(2000), +// max_gas_balance: Uint128::new(10000), +// }, +// create_position: None, +// }; + +// let account = client +// .account_builder() +// .sub_account(owner_account) +// .account_id(next_id) +// .name("carrot-sub-acc") +// .install_app_with_dependencies::>( +// &init_msg, +// Empty {}, +// )? +// .build()?; + +// let carrot_app = account.application::>()?; + +// give_authorizations(&client, carrot_app.addr_str()?)?; +// create_position( +// &carrot_app, +// coins(10_000, USDT.to_owned()), +// coin(1_000_000, USDT.to_owned()), +// coin(1_000_000, USDC.to_owned()), +// )?; + +// let position: PositionResponse = carrot_app.position()?; +// assert!(position.position.is_some()); +// Ok(()) +// } + +// #[test] +// fn install_on_sub_account_create_position_on_install() -> anyhow::Result<()> { +// let (pool_id, app) = setup_test_tube(false)?; +// let owner_account = app.account(); +// let chain = owner_account.environment(); +// let client = AbstractClient::new(chain)?; +// let next_id = client.next_local_account_id()?; +// let carrot_app_address = client +// .module_instantiate2_address::>( +// &AccountId::local(next_id), +// )?; + +// give_authorizations(&client, carrot_app_address)?; +// let init_msg = AppInstantiateMsg { +// pool_id, +// // 5 mins +// autocompound_cooldown_seconds: Uint64::new(300), +// autocompound_rewards_config: AutocompoundRewardsConfig { +// gas_asset: AssetEntry::new(REWARD_ASSET), +// swap_asset: AssetEntry::new(USDC), +// reward: Uint128::new(500_000), +// min_gas_balance: Uint128::new(1_000_000), +// max_gas_balance: Uint128::new(3_000_000), +// }, +// create_position: Some(CreatePositionMessage { +// lower_tick: INITIAL_LOWER_TICK, +// upper_tick: INITIAL_UPPER_TICK, +// funds: coins(100_000, USDC), +// asset0: coin(1_000_672_899, USDT), +// asset1: coin(10_000_000_000, USDC), +// max_spread: None, +// belief_price0: None, +// belief_price1: None, +// }), +// }; + +// let account = client +// .account_builder() +// .sub_account(owner_account) +// .account_id(next_id) +// .name("carrot-sub-acc") +// .install_app_with_dependencies::>( +// &init_msg, +// Empty {}, +// )? +// .build()?; + +// let carrot_app = account.application::>()?; + +// let position: PositionResponse = carrot_app.position()?; +// assert!(position.position.is_some()); +// Ok(()) +// } diff --git a/contracts/carrot-app/tests/strategy.rs b/contracts/carrot-app/tests/strategy.rs new file mode 100644 index 00000000..bd87305e --- /dev/null +++ b/contracts/carrot-app/tests/strategy.rs @@ -0,0 +1,152 @@ +use std::collections::HashMap; + +use carrot_app::yield_sources::yield_type::YieldType; +use cosmwasm_std::{coin, Decimal}; + +use carrot_app::state::compute_total_value; +use carrot_app::yield_sources::{BalanceStrategy, BalanceStrategyElement, YieldSource}; + +pub const LUNA: &str = "uluna"; +pub const OSMOSIS: &str = "uosmo"; +pub const STARGAZE: &str = "ustars"; +pub const NEUTRON: &str = "untrn"; +pub const USD: &str = "usd"; + +pub fn mock_strategy() -> BalanceStrategy { + BalanceStrategy(vec![ + BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: vec![ + (LUNA.to_string(), Decimal::percent(30)), + (OSMOSIS.to_string(), Decimal::percent(10)), + (STARGAZE.to_string(), Decimal::percent(60)), + ], + ty: YieldType::Mars("usdc".to_string()), + }, + share: Decimal::percent(33), + }, + BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: vec![(NEUTRON.to_string(), Decimal::percent(100))], + ty: YieldType::Mars("usdc".to_string()), + }, + share: Decimal::percent(67), + }, + ]) +} + +#[test] +fn bad_strategy_check_empty() -> cw_orch::anyhow::Result<()> { + let strategy = BalanceStrategy(vec![ + BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: vec![], + ty: YieldType::Mars("usdc".to_string()), + }, + share: Decimal::percent(33), + }, + BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: vec![], + ty: YieldType::Mars("usdc".to_string()), + }, + share: Decimal::percent(67), + }, + ]); + + strategy.check().unwrap_err(); + + Ok(()) +} + +#[test] +fn bad_strategy_check_sum() -> cw_orch::anyhow::Result<()> { + let strategy = BalanceStrategy(vec![ + BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: vec![(NEUTRON.to_string(), Decimal::percent(100))], + ty: YieldType::Mars("usdc".to_string()), + }, + share: Decimal::percent(33), + }, + BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: vec![(NEUTRON.to_string(), Decimal::percent(100))], + ty: YieldType::Mars("usdc".to_string()), + }, + share: Decimal::percent(66), + }, + ]); + + strategy.check().unwrap_err(); + + Ok(()) +} + +#[test] +fn bad_strategy_check_sum_inner() -> cw_orch::anyhow::Result<()> { + let strategy = BalanceStrategy(vec![ + BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: vec![ + (NEUTRON.to_string(), Decimal::percent(33)), + (NEUTRON.to_string(), Decimal::percent(66)), + ], + ty: YieldType::Mars("usdc".to_string()), + }, + share: Decimal::percent(33), + }, + BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: vec![(NEUTRON.to_string(), Decimal::percent(100))], + ty: YieldType::Mars("usdc".to_string()), + }, + share: Decimal::percent(67), + }, + ]); + + strategy.check().unwrap_err(); + + Ok(()) +} + +#[test] +fn check_strategy() -> cw_orch::anyhow::Result<()> { + let strategy = mock_strategy(); + + strategy.check()?; + + Ok(()) +} + +#[test] +fn value_fill_strategy() -> cw_orch::anyhow::Result<()> { + let strategy = mock_strategy(); + + let exchange_rates: HashMap = [ + (LUNA.to_string(), Decimal::percent(150)), + (USD.to_string(), Decimal::percent(100)), + (NEUTRON.to_string(), Decimal::percent(75)), + (OSMOSIS.to_string(), Decimal::percent(10)), + (STARGAZE.to_string(), Decimal::percent(35)), + ] + .into_iter() + .collect(); + + let funds = vec![ + coin(1_000_000_000, LUNA), + coin(2_000_000_000, USD), + coin(25_000_000, NEUTRON), + ]; + println!( + "total value : {:?}", + compute_total_value(&funds, &exchange_rates) + ); + + let fill_result = strategy.fill_all(funds, &exchange_rates)?; + + assert_eq!(fill_result.len(), 2); + assert_eq!(fill_result[0].0.len(), 3); + assert_eq!(fill_result[1].0.len(), 1); + Ok(()) +} From 3f28a971455a2243d2698076f0f0b3f740f48e7e Mon Sep 17 00:00:00 2001 From: Kayanski Date: Fri, 22 Mar 2024 18:07:56 +0000 Subject: [PATCH 02/42] Restored --- Cargo.toml | 4 ++-- contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0a21eb50..deb155a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,5 +11,5 @@ abstract-dex-adapter = { git = "https://github.com/abstractsdk/abstract.git", ta abstract-client = { version = "0.21.0" } -[patch.crates-io] -osmosis-test-tube = { path = "../test-tube/packages/osmosis-test-tube" } +# [patch.crates-io] +# osmosis-test-tube = { path = "../test-tube/packages/osmosis-test-tube" } diff --git a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs index 74f9aa8d..278a96c4 100644 --- a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs +++ b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs @@ -46,8 +46,8 @@ fn create_position( // 2. Create a position let tokens = cosmwasm_to_proto_coins(funds); - let msg = app.executor(deps).execute_with_reply( - vec![AccountAction::from_vec(vec![MsgCreatePosition { + let msg = app.executor(deps).execute_with_reply_and_data( + MsgCreatePosition { pool_id: params.pool_id, sender: proxy_addr.to_string(), lower_tick: params.lower_tick, @@ -55,7 +55,8 @@ fn create_position( tokens_provided: tokens, token_min_amount0: "0".to_string(), token_min_amount1: "0".to_string(), - }])], + } + .into(), ReplyOn::Success, OSMOSIS_CREATE_POSITION_REPLY_ID, )?; From 023f3f0d10356658b21d34d738e207186fbade86 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Tue, 26 Mar 2024 11:31:22 +0000 Subject: [PATCH 03/42] Handle multiple osmosis pool positions --- Cargo.toml | 16 ++- bot/Cargo.toml | 3 +- contracts/carrot-app/Cargo.toml | 14 ++- contracts/carrot-app/src/error.rs | 3 + contracts/carrot-app/src/handlers/execute.rs | 29 +++-- contracts/carrot-app/src/handlers/internal.rs | 26 ++++- contracts/carrot-app/src/msg.rs | 6 +- .../carrot-app/src/replies/after_swaps.rs | 7 +- .../src/replies/osmosis/add_to_position.rs | 23 ++-- .../src/replies/osmosis/create_position.rs | 22 +++- contracts/carrot-app/src/state.rs | 9 +- contracts/carrot-app/src/yield_sources.rs | 8 +- .../src/yield_sources/osmosis_cl_pool.rs | 101 ++++++++++++------ .../src/yield_sources/yield_type.rs | 18 ++-- contracts/carrot-app/tests/common.rs | 2 +- .../carrot-app/tests/deposit_withdraw.rs | 70 ++++++++---- 16 files changed, 254 insertions(+), 103 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index deb155a6..57789e94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,11 +5,23 @@ resolver = "2" [workspace.dependencies] cw-orch = "0.20.1" + abstract-app = { version = "0.21.0" } abstract-interface = { version = "0.21.0" } abstract-dex-adapter = { git = "https://github.com/abstractsdk/abstract.git", tag = "v0.21.0" } abstract-client = { version = "0.21.0" } +abstract-testing = { version = "0.21.0" } +abstract-sdk = { version = "0.21.0", features = ["stargate"] } + + +[patch.crates-io] +osmosis-test-tube = { path = "../test-tube/packages/osmosis-test-tube" } -# [patch.crates-io] -# osmosis-test-tube = { path = "../test-tube/packages/osmosis-test-tube" } +# This was added to account for the fix data forwaring in the proxy contract +abstract-app = { git = "https://github.com/abstractsdk/abstract.git" } +abstract-interface = { git = "https://github.com/abstractsdk/abstract.git" } +abstract-client = { git = "https://github.com/abstractsdk/abstract.git" } +abstract-testing = { git = "https://github.com/abstractsdk/abstract.git" } +abstract-core = { git = "https://github.com/abstractsdk/abstract.git" } +abstract-sdk = { git = "https://github.com/abstractsdk/abstract.git" } diff --git a/bot/Cargo.toml b/bot/Cargo.toml index 356765be..d8e154eb 100644 --- a/bot/Cargo.toml +++ b/bot/Cargo.toml @@ -15,7 +15,8 @@ abstract-client = { workspace = true } osmosis-std = { version = "0.21.0" } cosmos-sdk-proto = { version = "0.20.0" } dotenv = "0.15.0" -env_logger = "0.11.2" +# For cw-optimizoor +env_logger = { version = "0.11.3", default-features = false } log = "0.4.20" tonic = "0.10.0" carrot-app = { path = "../contracts/carrot-app", features = ["interface"] } diff --git a/contracts/carrot-app/Cargo.toml b/contracts/carrot-app/Cargo.toml index cb8e91b4..c90f14cd 100644 --- a/contracts/carrot-app/Cargo.toml +++ b/contracts/carrot-app/Cargo.toml @@ -48,7 +48,8 @@ schemars = "0.8" cw-asset = { version = "3.0" } abstract-app = { workspace = true } -abstract-sdk = { version = "0.21.0", features = ["stargate"] } +abstract-sdk = { workspace = true } + # Dependencies for interface abstract-dex-adapter = { workspace = true, features = ["osmosis"] } cw-orch = { workspace = true, optional = true } @@ -63,12 +64,15 @@ prost = { version = "0.12.3" } prost-types = { version = "0.12.3" } log = { version = "0.4.20" } carrot-app = { path = ".", features = ["interface"] } -abstract-testing = { version = "0.21.0" } -abstract-client = { version = "0.21.0" } -abstract-sdk = { version = "0.21.0", features = ["test-utils"] } +abstract-testing = { workspace = true } +abstract-client = { workspace = true } +abstract-sdk = { workspace = true, features = ["test-utils"] } + +# For cw-optimizoor +env_logger = { version = "0.11.3", default-features = false } + speculoos = "0.11.0" semver = "1.0" dotenv = "0.15.0" -env_logger = "0.10.0" cw-orch = { workspace = true, features = ["osmosis-test-tube"] } clap = { version = "4.3.7", features = ["derive"] } diff --git a/contracts/carrot-app/src/error.rs b/contracts/carrot-app/src/error.rs index 782530fe..22b98c02 100644 --- a/contracts/carrot-app/src/error.rs +++ b/contracts/carrot-app/src/error.rs @@ -88,4 +88,7 @@ pub enum AppError { #[error("Deposited total value is zero")] NoDeposit {}, + + #[error("Wrong yield type when executing internal operations")] + WrongYieldType {}, } diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index a9ff24a6..04c98829 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -13,7 +13,7 @@ use crate::{ }; use abstract_app::{abstract_sdk::features::AbstractResponse, objects::AnsAsset}; use abstract_dex_adapter::DexInterface; -use abstract_sdk::features::AbstractNameService; +use abstract_sdk::{features::AbstractNameService, AccountAction, Execution, ExecutorMsg}; use cosmwasm_std::{ to_json_binary, wasm_execute, Coin, Coins, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, StdError, SubMsg, Uint128, WasmMsg, @@ -50,15 +50,17 @@ pub fn execute_handler( AppExecuteMsg::DepositOneStrategy { swap_strategy, yield_type, - } => deposit_one_strategy(deps, env, info, swap_strategy, yield_type, app), + yield_index, + } => deposit_one_strategy(deps, env, info, swap_strategy, yield_index, yield_type, app), AppExecuteMsg::ExecuteOneDepositSwapStep { asset_in, denom_out, expected_amount, } => execute_one_deposit_step(deps, env, info, asset_in, denom_out, expected_amount, app), - AppExecuteMsg::FinalizeDeposit { yield_type } => { - execute_finalize_deposit(deps, env, info, yield_type, app) - } + AppExecuteMsg::FinalizeDeposit { + yield_type, + yield_index, + } => execute_finalize_deposit(deps, env, info, yield_type, yield_index, app), } } @@ -221,7 +223,8 @@ pub fn _inner_deposit( .iter() .map(|s| s.yield_source.ty.clone()), ) - .map(|(strategy, yield_type)| strategy.deposit_msgs(env, yield_type)) + .enumerate() + .map(|(index, (strategy, yield_type))| strategy.deposit_msgs(env, index, yield_type)) .collect::, _>>()?; Ok(deposit_msgs) @@ -232,7 +235,7 @@ fn _inner_withdraw( _env: &Env, value: Option, app: &App, -) -> AppResult> { +) -> AppResult> { // We need to select the share of each investment that needs to be withdrawn let withdraw_share = value .map(|value| { @@ -268,13 +271,19 @@ fn _inner_withdraw( Ok::<_, AppError>(this_withdraw_amount) }) .transpose()?; - s.yield_source + let raw_msg = s + .yield_source .ty - .withdraw(deps.as_ref(), this_withdraw_amount, app) + .withdraw(deps.as_ref(), this_withdraw_amount, app)?; + + Ok::<_, AppError>( + app.executor(deps.as_ref()) + .execute(vec![AccountAction::from_vec(raw_msg)])?, + ) }) .collect::, _>>()?; - Ok(withdraw_msgs.into_iter().flatten().collect()) + Ok(withdraw_msgs.into_iter().collect()) } // /// Sends autocompound rewards to the executor. diff --git a/contracts/carrot-app/src/handlers/internal.rs b/contracts/carrot-app/src/handlers/internal.rs index 962b69b7..95307e45 100644 --- a/contracts/carrot-app/src/handlers/internal.rs +++ b/contracts/carrot-app/src/handlers/internal.rs @@ -4,13 +4,15 @@ use crate::{ helpers::{add_funds, get_proxy_balance}, msg::{AppExecuteMsg, ExecuteMsg}, replies::REPLY_AFTER_SWAPS_STEP, - state::{CONFIG, TEMP_CURRENT_COIN, TEMP_DEPOSIT_COINS, TEMP_EXPECTED_SWAP_COIN}, - yield_sources::{yield_type::YieldType, DepositStep, OneDepositStrategy}, + state::{ + CONFIG, TEMP_CURRENT_COIN, TEMP_CURRENT_YIELD, TEMP_DEPOSIT_COINS, TEMP_EXPECTED_SWAP_COIN, + }, + yield_sources::{yield_type::YieldType, BalanceStrategy, DepositStep, OneDepositStrategy}, }; use abstract_app::{abstract_sdk::features::AbstractResponse, objects::AnsAsset}; use abstract_dex_adapter::DexInterface; use abstract_sdk::features::AbstractNameService; -use cosmwasm_std::{wasm_execute, Coin, DepsMut, Env, MessageInfo, SubMsg, Uint128}; +use cosmwasm_std::{wasm_execute, Coin, DepsMut, Env, MessageInfo, StdError, SubMsg, Uint128}; use cw_asset::AssetInfo; use super::query::query_exchange_rate; @@ -20,6 +22,7 @@ pub fn deposit_one_strategy( env: Env, info: MessageInfo, strategy: OneDepositStrategy, + yield_index: usize, yield_type: YieldType, app: App, ) -> AppResult { @@ -66,7 +69,10 @@ pub fn deposit_one_strategy( // Finalize and execute the deposit let last_step = wasm_execute( env.contract.address.clone(), - &ExecuteMsg::Module(AppExecuteMsg::FinalizeDeposit { yield_type }), + &ExecuteMsg::Module(AppExecuteMsg::FinalizeDeposit { + yield_type, + yield_index, + }), vec![], )?; @@ -122,6 +128,7 @@ pub fn execute_finalize_deposit( env: Env, info: MessageInfo, yield_type: YieldType, + yield_index: usize, app: App, ) -> AppResult { if info.sender != env.contract.address { @@ -129,7 +136,18 @@ pub fn execute_finalize_deposit( } let available_deposit_coins = TEMP_DEPOSIT_COINS.load(deps.storage)?; + TEMP_CURRENT_YIELD.save(deps.storage, &yield_index)?; + let msgs = yield_type.deposit(deps.as_ref(), &env, available_deposit_coins, &app)?; Ok(app.response("one-deposit-step").add_submessages(msgs)) } + +pub fn save_strategy(deps: DepsMut, strategy: BalanceStrategy) -> AppResult<()> { + CONFIG.update(deps.storage, |mut config| { + config.balance_strategy = strategy; + Ok::<_, StdError>(config) + })?; + + Ok(()) +} diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index 1d86d91c..e68d4048 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -47,6 +47,7 @@ pub enum AppExecuteMsg { DepositOneStrategy { swap_strategy: OneDepositStrategy, yield_type: YieldType, + yield_index: usize, }, /// Execute one Deposit Swap Step ExecuteOneDepositSwapStep { @@ -55,7 +56,10 @@ pub enum AppExecuteMsg { expected_amount: Uint128, }, /// Finalize the deposit after all swaps are executed - FinalizeDeposit { yield_type: YieldType }, + FinalizeDeposit { + yield_type: YieldType, + yield_index: usize, + }, } /// App query messages diff --git a/contracts/carrot-app/src/replies/after_swaps.rs b/contracts/carrot-app/src/replies/after_swaps.rs index 0ef2b648..39f2f84c 100644 --- a/contracts/carrot-app/src/replies/after_swaps.rs +++ b/contracts/carrot-app/src/replies/after_swaps.rs @@ -9,8 +9,11 @@ use crate::{ pub fn after_swap_reply(deps: DepsMut, _env: Env, app: App, _reply: Reply) -> AppResult { let coins_before = TEMP_CURRENT_COIN.load(deps.storage)?; - let current_coins = get_proxy_balance(deps.as_ref(), &app, coins_before.denom)?; + let current_coins = get_proxy_balance(deps.as_ref(), &app, coins_before.denom.clone())?; + // We just update the coins to deposit after the swap + deps.api + .debug(&format!("{:?}-{:?}", coins_before, current_coins)); if current_coins.amount > coins_before.amount { TEMP_DEPOSIT_COINS.update(deps.storage, |f| { add_funds( @@ -23,6 +26,8 @@ pub fn after_swap_reply(deps: DepsMut, _env: Env, app: App, _reply: Reply) -> Ap })?; } deps.api.debug("Swap reply over"); + deps.api + .debug(&format!("-{:?}", TEMP_DEPOSIT_COINS.load(deps.storage)?)); Ok(app.response("after_swap_reply")) } diff --git a/contracts/carrot-app/src/replies/osmosis/add_to_position.rs b/contracts/carrot-app/src/replies/osmosis/add_to_position.rs index 3e6ba43e..4a322c88 100644 --- a/contracts/carrot-app/src/replies/osmosis/add_to_position.rs +++ b/contracts/carrot-app/src/replies/osmosis/add_to_position.rs @@ -5,8 +5,9 @@ use osmosis_std::types::osmosis::concentratedliquidity::v1beta1::MsgAddToPositio use crate::{ contract::{App, AppResult}, error::AppError, - state::OSMOSIS_POSITION, - yield_sources::osmosis_cl_pool::OsmosisPosition, + handlers::{internal::save_strategy, query::query_strategy}, + state::TEMP_CURRENT_YIELD, + yield_sources::{osmosis_cl_pool::OsmosisPosition, yield_type::YieldType}, }; pub fn add_to_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) -> AppResult { @@ -16,17 +17,27 @@ pub fn add_to_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) - ))); }; - // Parse the msg exec response from the reply + // Parse the msg exec response from the reply. This is because this reply is generated by calling the proxy contract let parsed = cw_utils::parse_execute_response_data(&b)?; // Parse the position response from the message let response: MsgAddToPositionResponse = parsed.data.unwrap_or_default().try_into()?; // We update the position - let position = OsmosisPosition { - position_id: response.position_id, + let current_position_index = TEMP_CURRENT_YIELD.load(deps.storage)?; + let mut strategy = query_strategy(deps.as_ref())?; + + let current_yield = strategy.strategy.0.get_mut(current_position_index).unwrap(); + + current_yield.yield_source.ty = match current_yield.yield_source.ty.clone() { + YieldType::ConcentratedLiquidityPool(mut position) => { + position.position_id = Some(response.position_id); + YieldType::ConcentratedLiquidityPool(position) + } + YieldType::Mars(_) => return Err(AppError::WrongYieldType {}), }; - OSMOSIS_POSITION.save(deps.storage, &position)?; + + save_strategy(deps, strategy.strategy)?; Ok(app .response("create_position_reply") diff --git a/contracts/carrot-app/src/replies/osmosis/create_position.rs b/contracts/carrot-app/src/replies/osmosis/create_position.rs index 1f0004d0..b753257c 100644 --- a/contracts/carrot-app/src/replies/osmosis/create_position.rs +++ b/contracts/carrot-app/src/replies/osmosis/create_position.rs @@ -5,8 +5,9 @@ use osmosis_std::types::osmosis::concentratedliquidity::v1beta1::MsgCreatePositi use crate::{ contract::{App, AppResult}, error::AppError, - state::OSMOSIS_POSITION, - yield_sources::osmosis_cl_pool::OsmosisPosition, + handlers::{internal::save_strategy, query::query_strategy}, + state::TEMP_CURRENT_YIELD, + yield_sources::{osmosis_cl_pool::OsmosisPosition, yield_type::YieldType}, }; pub fn create_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) -> AppResult { @@ -15,6 +16,7 @@ pub fn create_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) - "Failed to create position", ))); }; + deps.api .debug(&format!("Inside create position reply : {:x?}", b)); @@ -24,10 +26,20 @@ pub fn create_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) - let response: MsgCreatePositionResponse = parsed.data.clone().unwrap_or_default().try_into()?; // We save the position - let position = OsmosisPosition { - position_id: response.position_id, + let current_position_index = TEMP_CURRENT_YIELD.load(deps.storage)?; + let mut strategy = query_strategy(deps.as_ref())?; + + let current_yield = strategy.strategy.0.get_mut(current_position_index).unwrap(); + + current_yield.yield_source.ty = match current_yield.yield_source.ty.clone() { + YieldType::ConcentratedLiquidityPool(mut position) => { + position.position_id = Some(response.position_id); + YieldType::ConcentratedLiquidityPool(position.clone()) + } + YieldType::Mars(_) => return Err(AppError::WrongYieldType {}), }; - OSMOSIS_POSITION.save(deps.storage, &position)?; + + save_strategy(deps, strategy.strategy)?; Ok(app .response("create_position_reply") diff --git a/contracts/carrot-app/src/state.rs b/contracts/carrot-app/src/state.rs index d00b5769..5707596c 100644 --- a/contracts/carrot-app/src/state.rs +++ b/contracts/carrot-app/src/state.rs @@ -7,11 +7,8 @@ use cosmwasm_std::{ ensure, Addr, Coin, Decimal, Deps, Env, MessageInfo, Storage, Timestamp, Uint128, Uint64, }; use cw_storage_plus::Item; -use osmosis_std::types::osmosis::concentratedliquidity::v1beta1::{ - ConcentratedliquidityQuerier, FullPositionBreakdown, -}; -use crate::yield_sources::osmosis_cl_pool::OsmosisPosition; +use crate::yield_sources::yield_type::YieldType; use crate::yield_sources::BalanceStrategy; use crate::{contract::AppResult, error::AppError, msg::CompoundStatus}; @@ -23,9 +20,7 @@ pub const CURRENT_EXECUTOR: Item = Item::new("executor"); pub const TEMP_CURRENT_COIN: Item = Item::new("temp_current_coins"); pub const TEMP_EXPECTED_SWAP_COIN: Item = Item::new("temp_expected_swap_coin"); pub const TEMP_DEPOSIT_COINS: Item> = Item::new("temp_deposit_coins"); - -// Storage for each yield source -pub const OSMOSIS_POSITION: Item = Item::new("osmosis_cl_position"); +pub const TEMP_CURRENT_YIELD: Item = Item::new("temp_current_yield_type"); #[cw_serde] pub struct Config { diff --git a/contracts/carrot-app/src/yield_sources.rs b/contracts/carrot-app/src/yield_sources.rs index 6b648323..ec3d4fef 100644 --- a/contracts/carrot-app/src/yield_sources.rs +++ b/contracts/carrot-app/src/yield_sources.rs @@ -245,13 +245,19 @@ impl From>> for OneDepositStrategy { } impl OneDepositStrategy { - pub fn deposit_msgs(&self, env: &Env, yield_type: YieldType) -> AppResult { + pub fn deposit_msgs( + &self, + env: &Env, + yield_index: usize, + yield_type: YieldType, + ) -> AppResult { // For each strategy, we send a message on the contract to execute it Ok(wasm_execute( env.contract.address.clone(), &ExecuteMsg::Module(AppExecuteMsg::DepositOneStrategy { swap_strategy: self.clone(), yield_type, + yield_index, }), vec![], )? diff --git a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs index 278a96c4..f70486ac 100644 --- a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs +++ b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs @@ -3,8 +3,8 @@ use std::str::FromStr; use crate::{ contract::{App, AppResult}, error::AppError, + handlers::query::query_strategy, replies::{OSMOSIS_ADD_TO_POSITION_REPLY_ID, OSMOSIS_CREATE_POSITION_REPLY_ID}, - state::OSMOSIS_POSITION, }; use abstract_app::traits::AccountIdentification; use abstract_sdk::{AccountAction, Execution}; @@ -66,17 +66,35 @@ fn create_position( Ok(vec![msg]) } -fn raw_deposit(deps: Deps, funds: Vec, app: &App) -> AppResult> { - let pool = get_osmosis_position(deps)?; +fn raw_deposit( + deps: Deps, + funds: Vec, + app: &App, + position_id: u64, +) -> AppResult> { + let pool = get_osmosis_position_by_id(deps, position_id)?; let position = pool.position.unwrap(); let proxy_addr = app.account_base(deps)?.proxy; + + // We need to make sure the amounts are in the right order + // We assume the funds vector has 2 coins associated + let (amount0, amount1) = match pool + .asset0 + .map(|c| c.denom == funds[0].denom) + .or(pool.asset1.map(|c| c.denom == funds[1].denom)) + { + Some(true) => (funds[0].amount, funds[1].amount), // we already had the right order + Some(false) => (funds[1].amount, funds[0].amount), // we had the wrong order + None => return Err(AppError::NoPosition {}), // A position has to exist in order to execute this function. This should be unreachable + }; + let deposit_msg = app.executor(deps).execute_with_reply_and_data( MsgAddToPosition { position_id: position.position_id, sender: proxy_addr.to_string(), - amount0: funds[0].amount.to_string(), - amount1: funds[1].amount.to_string(), + amount0: amount0.to_string(), + amount1: amount1.to_string(), token_min_amount0: "0".to_string(), token_min_amount1: "0".to_string(), } @@ -85,29 +103,42 @@ fn raw_deposit(deps: Deps, funds: Vec, app: &App) -> AppResult OSMOSIS_ADD_TO_POSITION_REPLY_ID, )?; + deps.api + .debug(&format!("Add to position message {:?}", funds)); + Ok(vec![deposit_msg]) } pub fn deposit( deps: Deps, - env: &Env, + _env: &Env, params: ConcentratedPoolParams, funds: Vec, app: &App, ) -> AppResult> { // We verify there is a position stored - let osmosis_position = OSMOSIS_POSITION.may_load(deps.storage)?; - if let Some(position) = osmosis_position { + + let osmosis_position = params + .position_id + .map(|position_id| get_osmosis_position_by_id(deps, position_id)); + + if let Some(Ok(_)) = osmosis_position { // We just deposit - raw_deposit(deps, funds, app) + raw_deposit(deps, funds, app, params.position_id.unwrap()) } else { // We need to create a position create_position(deps, params, funds, app) } } -pub fn withdraw(deps: Deps, amount: Option, app: &App) -> AppResult> { - let position = get_osmosis_position(deps)?; +pub fn withdraw( + deps: Deps, + amount: Option, + app: &App, + params: ConcentratedPoolParams, +) -> AppResult> { + let position = + get_osmosis_position_by_id(deps, params.position_id.ok_or(AppError::NoPosition {})?)?; let position_details = position.position.unwrap(); let total_liquidity = position_details.liquidity.replace('.', ""); @@ -129,8 +160,13 @@ pub fn withdraw(deps: Deps, amount: Option, app: &App) -> AppResult AppResult> { - let position = get_osmosis_position(deps)?; +pub fn user_deposit( + deps: Deps, + _app: &App, + params: ConcentratedPoolParams, +) -> AppResult> { + let position = + get_osmosis_position_by_id(deps, params.position_id.ok_or(AppError::NoPosition {})?)?; Ok([ try_proto_to_cosmwasm_coins(position.asset0)?, @@ -142,22 +178,32 @@ pub fn user_deposit(deps: Deps, _app: &App) -> AppResult> { } /// Returns an amount representing a user's liquidity -pub fn user_liquidity(deps: Deps, _app: &App) -> AppResult { - let position = get_osmosis_position(deps)?; +pub fn user_liquidity( + deps: Deps, + _app: &App, + params: ConcentratedPoolParams, +) -> AppResult { + let position = + get_osmosis_position_by_id(deps, params.position_id.ok_or(AppError::NoPosition {})?)?; let total_liquidity = position.position.unwrap().liquidity.replace('.', ""); Ok(Uint128::from_str(&total_liquidity)?) } -pub fn user_rewards(deps: Deps, _app: &App) -> AppResult> { - let pool = get_osmosis_position(deps)?; +pub fn user_rewards( + deps: Deps, + _app: &App, + params: ConcentratedPoolParams, +) -> AppResult> { + let position = + get_osmosis_position_by_id(deps, params.position_id.ok_or(AppError::NoPosition {})?)?; let mut rewards = cosmwasm_std::Coins::default(); - for coin in try_proto_to_cosmwasm_coins(pool.claimable_incentives)? { + for coin in try_proto_to_cosmwasm_coins(position.claimable_incentives)? { rewards.add(coin)?; } - for coin in try_proto_to_cosmwasm_coins(pool.claimable_spread_rewards)? { + for coin in try_proto_to_cosmwasm_coins(position.claimable_spread_rewards)? { rewards.add(coin)?; } @@ -224,18 +270,13 @@ pub struct OsmosisPosition { pub position_id: u64, } -pub fn get_position(deps: Deps) -> AppResult { - OSMOSIS_POSITION - .load(deps.storage) - .map_err(|_| AppError::NoPosition {}) -} - -pub fn get_osmosis_position(deps: Deps) -> AppResult { - let position = get_position(deps)?; - +pub fn get_osmosis_position_by_id( + deps: Deps, + position_id: u64, +) -> AppResult { ConcentratedliquidityQuerier::new(&deps.querier) - .position_by_id(position.position_id) - .map_err(|e| AppError::UnableToQueryPosition(position.position_id, e))? + .position_by_id(position_id) + .map_err(|e| AppError::UnableToQueryPosition(position_id, e))? .position .ok_or(AppError::NoPosition {}) } diff --git a/contracts/carrot-app/src/yield_sources/yield_type.rs b/contracts/carrot-app/src/yield_sources/yield_type.rs index 699a104a..661936d0 100644 --- a/contracts/carrot-app/src/yield_sources/yield_type.rs +++ b/contracts/carrot-app/src/yield_sources/yield_type.rs @@ -37,8 +37,8 @@ impl YieldType { app: &App, ) -> AppResult> { match self { - YieldType::ConcentratedLiquidityPool(_params) => { - osmosis_cl_pool::withdraw(deps, amount, app) + YieldType::ConcentratedLiquidityPool(params) => { + osmosis_cl_pool::withdraw(deps, amount, app, params) } YieldType::Mars(denom) => mars::withdraw(deps, denom, amount, app), } @@ -46,8 +46,8 @@ impl YieldType { pub fn user_deposit(&self, deps: Deps, app: &App) -> AppResult> { match self { - YieldType::ConcentratedLiquidityPool(_params) => { - osmosis_cl_pool::user_deposit(deps, app) + YieldType::ConcentratedLiquidityPool(params) => { + osmosis_cl_pool::user_deposit(deps, app, params.clone()) } YieldType::Mars(denom) => Ok(coins( mars::user_deposit(deps, denom.clone(), app)?.into(), @@ -58,8 +58,8 @@ impl YieldType { pub fn user_rewards(&self, deps: Deps, app: &App) -> AppResult> { match self { - YieldType::ConcentratedLiquidityPool(_params) => { - osmosis_cl_pool::user_rewards(deps, app) + YieldType::ConcentratedLiquidityPool(params) => { + osmosis_cl_pool::user_rewards(deps, app, params.clone()) } YieldType::Mars(denom) => mars::user_rewards(deps, denom.clone(), app), } @@ -67,8 +67,8 @@ impl YieldType { pub fn user_liquidity(&self, deps: Deps, app: &App) -> AppResult { match self { - YieldType::ConcentratedLiquidityPool(_params) => { - osmosis_cl_pool::user_liquidity(deps, app) + YieldType::ConcentratedLiquidityPool(params) => { + osmosis_cl_pool::user_liquidity(deps, app, params.clone()) } YieldType::Mars(denom) => mars::user_liquidity(deps, denom.clone(), app), } @@ -80,5 +80,5 @@ pub struct ConcentratedPoolParams { pub pool_id: u64, pub lower_tick: i64, pub upper_tick: i64, - pub position_id: u64, + pub position_id: Option, } diff --git a/contracts/carrot-app/tests/common.rs b/contracts/carrot-app/tests/common.rs index 028eff15..86e3084b 100644 --- a/contracts/carrot-app/tests/common.rs +++ b/contracts/carrot-app/tests/common.rs @@ -138,7 +138,7 @@ pub fn deploy( pool_id, lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, - position_id: 0, + position_id: None, }), }, share: Decimal::one(), diff --git a/contracts/carrot-app/tests/deposit_withdraw.rs b/contracts/carrot-app/tests/deposit_withdraw.rs index 8bd10c66..c9e07b7d 100644 --- a/contracts/carrot-app/tests/deposit_withdraw.rs +++ b/contracts/carrot-app/tests/deposit_withdraw.rs @@ -1,9 +1,12 @@ mod common; use crate::common::{setup_test_tube, USDC, USDT}; +use abstract_client::Application; +use abstract_interface::Abstract; use carrot_app::{ msg::{AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse}, yield_sources::osmosis_cl_pool::OsmosisPosition, + AppInterface, }; use cosmwasm_std::{coin, coins, Decimal, Uint128}; use cw_orch::{ @@ -16,6 +19,21 @@ use cw_orch::{ }; use osmosis_std::types::osmosis::concentratedliquidity::v1beta1::PositionByIdRequest; +fn query_balances( + carrot_app: &Application>, +) -> anyhow::Result { + let balance = carrot_app.balance(); + if balance.is_err() { + return Ok(Uint128::zero()); + } + let sum = balance? + .balances + .iter() + .fold(Uint128::zero(), |acc, e| acc + e.amount); + + Ok(sum) +} + #[test] fn deposit_lands() -> anyhow::Result<()> { let (_, carrot_app) = setup_test_tube(false)?; @@ -26,6 +44,8 @@ fn deposit_lands() -> anyhow::Result<()> { // We should add funds to the account proxy let deposit_coins = coins(deposit_amount, USDT.to_owned()); let mut chain = carrot_app.get_chain().clone(); + + let balances_before = query_balances(&carrot_app)?; chain.add_balance( carrot_app.account().proxy()?.to_string(), deposit_coins.clone(), @@ -34,22 +54,23 @@ fn deposit_lands() -> anyhow::Result<()> { // Do the deposit carrot_app.deposit(deposit_coins.clone())?; // Check almost everything landed - let balance: AssetsBalanceResponse = carrot_app.balance()?; - let sum = balance - .balances - .iter() - .fold(Uint128::zero(), |acc, e| acc + e.amount); - assert!(sum.u128() > (deposit_amount - max_fee.u128()) * 2); + let balances_after = query_balances(&carrot_app)?; + assert!(balances_before < balances_after); + // Add some more funds + chain.add_balance( + carrot_app.account().proxy()?.to_string(), + deposit_coins.clone(), + )?; // Do the second deposit - carrot_app.deposit(vec![coin(deposit_amount, USDT.to_owned())])?; + let response = carrot_app.deposit(vec![coin(deposit_amount, USDT.to_owned())])?; // Check almost everything landed - let balance: AssetsBalanceResponse = carrot_app.balance()?; - let sum = balance - .balances - .iter() - .fold(Uint128::zero(), |acc, e| acc + e.amount); - assert!(sum.u128() > (deposit_amount - max_fee.u128()) * 3); + let balances_after_second = query_balances(&carrot_app)?; + assert!(balances_after < balances_after_second); + + // We assert the deposit response is an add to position and not a create position + response.event_attr_value("add_to_position", "new_position_id")?; + Ok(()) } @@ -57,19 +78,24 @@ fn deposit_lands() -> anyhow::Result<()> { fn withdraw_position() -> anyhow::Result<()> { let (_, carrot_app) = setup_test_tube(false)?; - let chain = carrot_app.get_chain().clone(); + let mut chain = carrot_app.get_chain().clone(); - carrot_app.deposit(coins(10_000, USDT.to_owned()))?; + // Add some more funds + let deposit_amount = 10_000; + let deposit_coins = coins(deposit_amount, USDT.to_owned()); + let proxy_addr = carrot_app.account().proxy()?; + chain.add_balance(proxy_addr.to_string(), deposit_coins.clone())?; + carrot_app.deposit(deposit_coins)?; let balance: AssetsBalanceResponse = carrot_app.balance()?; let balance_usdc_before_withdraw = chain .bank_querier() - .balance(chain.sender(), Some(USDT.to_owned()))? + .balance(&proxy_addr, Some(USDT.to_owned()))? .pop() .unwrap(); let balance_usdt_before_withdraw = chain .bank_querier() - .balance(chain.sender(), Some(USDC.to_owned()))? + .balance(&proxy_addr, Some(USDC.to_owned()))? .pop() .unwrap(); @@ -80,12 +106,12 @@ fn withdraw_position() -> anyhow::Result<()> { let balance_usdc_after_half_withdraw = chain .bank_querier() - .balance(chain.sender(), Some(USDT.to_owned()))? + .balance(&proxy_addr, Some(USDT.to_owned()))? .pop() .unwrap(); let balance_usdt_after_half_withdraw = chain .bank_querier() - .balance(chain.sender(), Some(USDC.to_owned()))? + .balance(&proxy_addr, Some(USDC.to_owned()))? .pop() .unwrap(); @@ -114,7 +140,11 @@ fn withdraw_position() -> anyhow::Result<()> { fn deposit_multiple_assets() -> anyhow::Result<()> { let (_, carrot_app) = setup_test_tube(false)?; - carrot_app.deposit(vec![coin(258, USDT.to_owned()), coin(234, USDC.to_owned())])?; + let mut chain = carrot_app.get_chain().clone(); + let proxy_addr = carrot_app.account().proxy()?; + let deposit_coins = vec![coin(234, USDC.to_owned()), coin(258, USDT.to_owned())]; + chain.add_balance(proxy_addr.to_string(), deposit_coins.clone())?; + carrot_app.deposit(deposit_coins)?; Ok(()) } From fb126173cb9afcdcef703206ad01e2dd414ba516 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Tue, 26 Mar 2024 13:36:50 +0000 Subject: [PATCH 04/42] Add autocompound' --- contracts/carrot-app/src/handlers/execute.rs | 335 ++++++++---------- contracts/carrot-app/src/handlers/mod.rs | 2 +- .../carrot-app/src/handlers/swap_helpers.rs | 274 +++++++------- .../carrot-app/src/yield_sources/mars.rs | 9 + .../src/yield_sources/osmosis_cl_pool.rs | 52 ++- .../src/yield_sources/yield_type.rs | 9 + contracts/carrot-app/tests/config.rs | 104 ++++++ .../carrot-app/tests/deposit_withdraw.rs | 69 +++- 8 files changed, 525 insertions(+), 329 deletions(-) create mode 100644 contracts/carrot-app/tests/config.rs diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index 04c98829..cbe701ac 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -2,35 +2,23 @@ use crate::{ contract::{App, AppResult}, error::AppError, handlers::query::query_balance, - helpers::{add_funds, get_balance, get_proxy_balance}, msg::{AppExecuteMsg, ExecuteMsg}, - replies::REPLY_AFTER_SWAPS_STEP, - state::{ - assert_contract, Config, CONFIG, TEMP_CURRENT_COIN, TEMP_DEPOSIT_COINS, - TEMP_EXPECTED_SWAP_COIN, - }, - yield_sources::{yield_type::YieldType, DepositStep, OneDepositStrategy}, + state::{assert_contract, get_autocompound_status, Config, CONFIG}, + yield_sources::BalanceStrategy, }; use abstract_app::{abstract_sdk::features::AbstractResponse, objects::AnsAsset}; use abstract_dex_adapter::DexInterface; -use abstract_sdk::{features::AbstractNameService, AccountAction, Execution, ExecutorMsg}; +use abstract_sdk::{AccountAction, Execution, ExecutorMsg, TransferInterface}; use cosmwasm_std::{ - to_json_binary, wasm_execute, Coin, Coins, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, - StdError, SubMsg, Uint128, WasmMsg, + to_json_binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, StdError, SubMsg, + Uint128, WasmMsg, }; -use cw_asset::{Asset, AssetInfo}; -use osmosis_std::{ - cosmwasm_to_proto_coins, try_proto_to_cosmwasm_coins, - types::osmosis::concentratedliquidity::v1beta1::{ - MsgAddToPosition, MsgCollectIncentives, MsgCollectSpreadRewards, MsgCreatePosition, - MsgWithdrawPosition, - }, -}; -use std::{collections::HashMap, str::FromStr}; +use std::collections::HashMap; use super::{ internal::{deposit_one_strategy, execute_finalize_deposit, execute_one_deposit_step}, query::{query_exchange_rate, query_strategy}, + swap_helpers::swap_msg, }; pub fn execute_handler( @@ -43,8 +31,8 @@ pub fn execute_handler( match msg { AppExecuteMsg::Deposit { funds } => deposit(deps, env, info, funds, app), AppExecuteMsg::Withdraw { amount } => withdraw(deps, env, info, amount, app), - AppExecuteMsg::Autocompound {} => todo!(), - AppExecuteMsg::Rebalance { strategy } => todo!(), + AppExecuteMsg::Autocompound {} => autocompound(deps, env, info, app), + AppExecuteMsg::Rebalance { strategy } => rebalance(deps, env, info, strategy, app), // Endpoints called by the contract directly AppExecuteMsg::DepositOneStrategy { @@ -65,8 +53,10 @@ pub fn execute_handler( } fn deposit(deps: DepsMut, env: Env, info: MessageInfo, funds: Vec, app: App) -> AppResult { - // Only the admin (manager contracts or account owner) can deposit - app.admin.assert_admin(deps.as_ref(), &info.sender)?; + // Only the admin (manager contracts or account owner) can deposit as well as the contract itself + app.admin + .assert_admin(deps.as_ref(), &info.sender) + .or(assert_contract(&info, &env))?; let deposit_msgs = _inner_deposit(deps.as_ref(), &env, funds, &app)?; deps.api @@ -90,94 +80,94 @@ fn withdraw( Ok(app.response("withdraw").add_messages(msgs)) } +fn rebalance( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + strategy: BalanceStrategy, + app: App, +) -> AppResult { + // We load it raw because we're changing the strategy + let mut config = CONFIG.load(deps.storage)?; + let old_strategy = config.balance_strategy; + strategy.check()?; + + // We execute operations to rebalance the funds between the strategies + // TODO + config.balance_strategy = strategy; + CONFIG.save(deps.storage, &config)?; + + Ok(app.response("rebalance")) +} + // /// Auto-compound the position with earned fees and incentives. -// fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResult { -// // Everyone can autocompound - -// let position = get_osmosis_position(deps.as_ref())?; -// let position_details = position.position.unwrap(); - -// let mut rewards = cosmwasm_std::Coins::default(); -// let mut collect_rewards_msgs = vec![]; - -// // Get app's user and set up authz. -// let user = get_user(deps.as_ref(), &app)?; -// let authz = app.auth_z(deps.as_ref(), Some(user.clone()))?; - -// // If there are external incentives, claim them. -// if !position.claimable_incentives.is_empty() { -// for coin in try_proto_to_cosmwasm_coins(position.claimable_incentives)? { -// rewards.add(coin)?; -// } -// collect_rewards_msgs.push(authz.execute( -// &env.contract.address, -// MsgCollectIncentives { -// position_ids: vec![position_details.position_id], -// sender: user.to_string(), -// }, -// )); -// } - -// // If there is income from swap fees, claim them. -// if !position.claimable_spread_rewards.is_empty() { -// for coin in try_proto_to_cosmwasm_coins(position.claimable_spread_rewards)? { -// rewards.add(coin)?; -// } -// collect_rewards_msgs.push(authz.execute( -// &env.contract.address, -// MsgCollectSpreadRewards { -// position_ids: vec![position_details.position_id], -// sender: position_details.address.clone(), -// }, -// )) -// } - -// // If there are no rewards, we can't do anything -// if rewards.is_empty() { -// return Err(crate::error::AppError::NoRewards {}); -// } - -// // Finally we deposit of all rewarded tokens into the position -// let msg_deposit = CosmosMsg::Wasm(WasmMsg::Execute { -// contract_addr: env.contract.address.to_string(), -// msg: to_json_binary(&ExecuteMsg::Module(AppExecuteMsg::Deposit { -// funds: rewards.into(), -// max_spread: None, -// belief_price0: None, -// belief_price1: None, -// }))?, -// funds: vec![], -// }); - -// let mut response = app -// .response("auto-compound") -// .add_messages(collect_rewards_msgs) -// .add_message(msg_deposit); - -// // If called by non-admin and reward cooldown has ended, send rewards to the contract caller. -// let config = CONFIG.load(deps.storage)?; -// if !app.admin.is_admin(deps.as_ref(), &info.sender)? -// && get_position_status( -// deps.storage, -// &env, -// config.autocompound_cooldown_seconds.u64(), -// )? -// .is_ready() -// { -// let executor_reward_messages = autocompound_executor_rewards( -// deps.as_ref(), -// &env, -// info.sender.into_string(), -// &app, -// config, -// )?; - -// response = response.add_messages(executor_reward_messages); -// } - -// Ok(response) -// } +fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResult { + // Everyone can autocompound + + // We withdraw all rewards from protocols + let (rewards, collect_rewards_msgs): (Vec>, Vec) = + query_strategy(deps.as_ref())? + .strategy + .0 + .into_iter() + .map(|s| { + let (rewards, raw_msgs) = + s.yield_source.ty.withdraw_rewards(deps.as_ref(), &app)?; + + Ok::<_, AppError>(( + rewards, + app.executor(deps.as_ref()) + .execute(vec![AccountAction::from_vec(raw_msgs)])?, + )) + }) + .collect::, _>>()? + .into_iter() + .unzip(); + + let all_rewards: Vec = rewards.into_iter().flatten().collect(); + // If there are no rewards, we can't do anything + if all_rewards.is_empty() { + return Err(crate::error::AppError::NoRewards {}); + } + + // Finally we deposit of all rewarded tokens into the position + let msg_deposit = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_json_binary(&ExecuteMsg::Module(AppExecuteMsg::Deposit { + funds: all_rewards, + }))?, + funds: vec![], + }); + + let mut response = app + .response("auto-compound") + .add_messages(collect_rewards_msgs) + .add_message(msg_deposit); + + // If called by non-admin and reward cooldown has ended, send rewards to the contract caller. + let config = CONFIG.load(deps.storage)?; + if !app.admin.is_admin(deps.as_ref(), &info.sender)? + && get_autocompound_status( + deps.storage, + &env, + config.autocompound_config.cooldown_seconds.u64(), + )? + .is_ready() + { + let executor_reward_messages = autocompound_executor_rewards( + deps.as_ref(), + &env, + info.sender.into_string(), + &app, + config, + )?; + + response = response.add_messages(executor_reward_messages); + } + + Ok(response) +} pub fn _inner_deposit( deps: Deps, @@ -286,72 +276,61 @@ fn _inner_withdraw( Ok(withdraw_msgs.into_iter().collect()) } -// /// Sends autocompound rewards to the executor. -// /// In case user does not have not enough gas token the contract will swap some -// /// tokens for gas tokens. -// pub fn autocompound_executor_rewards( -// deps: Deps, -// env: &Env, -// executor: String, -// app: &App, -// config: Config, -// ) -> AppResult> { -// let rewards_config = config.autocompound_rewards_config; -// let position = get_position(deps)?; -// let user = position.owner; - -// // Get user balance of gas denom -// let gas_denom = rewards_config -// .gas_asset -// .resolve(&deps.querier, &app.ans_host(deps)?)?; -// let user_gas_balance = gas_denom.query_balance(&deps.querier, user.clone())?; - -// let mut rewards_messages = vec![]; - -// // If not enough gas coins - swap for some amount -// if user_gas_balance < rewards_config.min_gas_balance { -// // Get asset entries -// let dex = app.ans_dex(deps, OSMOSIS.to_string()); - -// // Do reverse swap to find approximate amount we need to swap -// let need_gas_coins = rewards_config.max_gas_balance - user_gas_balance; -// let simulate_swap_response = dex.simulate_swap( -// AnsAsset::new(rewards_config.gas_asset.clone(), need_gas_coins), -// rewards_config.swap_asset.clone(), -// )?; - -// // Get user balance of swap denom -// let user_swap_balance = -// get_balance(rewards_config.swap_asset.clone(), deps, user.clone(), app)?; - -// // Swap as much as available if not enough for max_gas_balance -// let swap_amount = simulate_swap_response.return_amount.min(user_swap_balance); - -// let msgs = swap_msg( -// deps, -// env, -// AnsAsset::new(rewards_config.swap_asset, swap_amount), -// rewards_config.gas_asset, -// app, -// )?; -// rewards_messages.extend(msgs); -// } - -// let reward_asset = Asset::new(gas_denom, rewards_config.reward); -// let msg_send = reward_asset.transfer_msg(env.contract.address.to_string())?; - -// // To avoid giving general `MsgSend` authorization to any address we do 2 sends here -// // 1) From user to the contract -// // 2) From contract to the executor -// // That way we can limit the `MsgSend` authorization to the contract address only. -// let send_reward_to_contract_msg = app -// .auth_z(deps, Some(cosmwasm_std::Addr::unchecked(user)))? -// .execute(&env.contract.address, msg_send); -// rewards_messages.push(send_reward_to_contract_msg); - -// let send_reward_to_executor_msg = reward_asset.transfer_msg(executor)?; - -// rewards_messages.push(send_reward_to_executor_msg); - -// Ok(rewards_messages) -// } +/// Sends autocompound rewards to the executor. +/// In case user does not have not enough gas token the contract will swap some +/// tokens for gas tokens. +pub fn autocompound_executor_rewards( + deps: Deps, + env: &Env, + executor: String, + app: &App, + config: Config, +) -> AppResult> { + let rewards_config = config.autocompound_config.rewards; + + // Get user balance of gas denom + let user_gas_balance = app.bank(deps).balance(&rewards_config.gas_asset)?.amount; + + let mut rewards_messages = vec![]; + + // If not enough gas coins - swap for some amount + if user_gas_balance < rewards_config.min_gas_balance { + // Get asset entries + let dex = app.ans_dex(deps, config.dex.to_string()); + + // Do reverse swap to find approximate amount we need to swap + let need_gas_coins = rewards_config.max_gas_balance - user_gas_balance; + let simulate_swap_response = dex.simulate_swap( + AnsAsset::new(rewards_config.gas_asset.clone(), need_gas_coins), + rewards_config.swap_asset.clone(), + )?; + + // Get user balance of swap denom + let user_swap_balance = app.bank(deps).balance(&rewards_config.swap_asset)?.amount; + + // Swap as much as available if not enough for max_gas_balance + let swap_amount = simulate_swap_response.return_amount.min(user_swap_balance); + + let msgs = swap_msg( + deps, + env, + AnsAsset::new(rewards_config.swap_asset, swap_amount), + rewards_config.gas_asset.clone(), + app, + )?; + rewards_messages.extend(msgs); + } + + // We send their reward to the executor + let msg_send = app.bank(deps).transfer( + vec![AnsAsset::new( + rewards_config.gas_asset, + rewards_config.reward, + )], + &deps.api.addr_validate(&executor)?, + )?; + + rewards_messages.push(app.executor(deps).execute(vec![msg_send])?.into()); + + Ok(rewards_messages) +} diff --git a/contracts/carrot-app/src/handlers/mod.rs b/contracts/carrot-app/src/handlers/mod.rs index dc4aa2da..5d877c7b 100644 --- a/contracts/carrot-app/src/handlers/mod.rs +++ b/contracts/carrot-app/src/handlers/mod.rs @@ -3,7 +3,7 @@ pub mod instantiate; pub mod internal; // pub mod migrate; pub mod query; -// pub mod swap_helpers; +pub mod swap_helpers; pub use crate::handlers::{ execute::execute_handler, instantiate::instantiate_handler, // migrate::migrate_handler, diff --git a/contracts/carrot-app/src/handlers/swap_helpers.rs b/contracts/carrot-app/src/handlers/swap_helpers.rs index c14030d7..a5dc6cc3 100644 --- a/contracts/carrot-app/src/handlers/swap_helpers.rs +++ b/contracts/carrot-app/src/handlers/swap_helpers.rs @@ -7,11 +7,10 @@ pub const DEFAULT_SLIPPAGE: Decimal = Decimal::permille(5); use crate::{ contract::{App, AppResult, OSMOSIS}, - helpers::get_user, state::CONFIG, }; -use super::query::query_price; +// use super::query::query_price; pub(crate) fn swap_msg( deps: Deps, @@ -19,149 +18,145 @@ pub(crate) fn swap_msg( offer_asset: AnsAsset, ask_asset: AssetEntry, app: &App, -) -> AppResult> { +) -> AppResult> { // Don't swap if not required if offer_asset.amount.is_zero() { - return Ok(vec![]); + return Ok(None); } - let sender = get_user(deps, app)?; let dex = app.ans_dex(deps, OSMOSIS.to_string()); - let trigger_swap_msg: GenerateMessagesResponse = dex.generate_swap_messages( + let swap_msg = dex.swap( offer_asset, ask_asset, Some(Decimal::percent(MAX_SPREAD_PERCENT)), None, - sender.clone(), )?; - let authz = app.auth_z(deps, Some(sender))?; - Ok(trigger_swap_msg - .messages - .into_iter() - .map(|m| authz.execute(&env.contract.address, m)) - .collect()) + Ok(Some(swap_msg)) } -pub(crate) fn tokens_to_swap( - deps: Deps, - amount_to_swap: Vec, - asset0: Coin, // Represents the amount of Coin 0 we would like the position to handle - asset1: Coin, // Represents the amount of Coin 1 we would like the position to handle, - price: Decimal, // Relative price (when swapping amount0 for amount1, equals amount0/amount1) -) -> AppResult<(AnsAsset, AssetEntry, Vec)> { - let config = CONFIG.load(deps.storage)?; - - let x0 = amount_to_swap - .iter() - .find(|c| c.denom == asset0.denom) - .cloned() - .unwrap_or(Coin { - denom: asset0.denom, - amount: Uint128::zero(), - }); - let x1 = amount_to_swap - .iter() - .find(|c| c.denom == asset1.denom) - .cloned() - .unwrap_or(Coin { - denom: asset1.denom, - amount: Uint128::zero(), - }); - - // We will swap on the pool to get the right coin ratio - - // We have x0 and x1 to deposit. Let p (or price) be the price of asset1 (the number of asset0 you get for 1 unit of asset1) - // In order to deposit, you need to have X0 and X1 such that X0/X1 = A0/A1 where A0 and A1 are the current liquidity inside the position - // That is equivalent to X0*A1 = X1*A0 - // We need to find how much to swap. - // If x0*A1 < x1*A0, we need to have more x0 to balance the swap --> so we need to send some of x1 to swap (lets say we wend y1 to swap) - // So X1 = x1-y1 - // X0 = x0 + price*y1 - // Therefore, the following equation needs to be true - // (x0 + price*y1)*A1 = (x1-y1)*A0 or y1 = (x1*a0 - x0*a1)/(a0 + p*a1) - // If x0*A1 > x1*A0, we need to have more x1 to balance the swap --> so we need to send some of x0 to swap (lets say we wend y0 to swap) - // So X0 = x0-y0 - // X1 = x1 + y0/price - // Therefore, the following equation needs to be true - // (x0-y0)*A1 = (x1 + y0/price)*A0 or y0 = (x0*a1 - x1*a0)/(a1 + a0/p) - - let x0_a1 = x0.amount * asset1.amount; - let x1_a0 = x1.amount * asset0.amount; - - let (offer_asset, ask_asset, mut resulting_balance) = if x0_a1 < x1_a0 { - let numerator = x1_a0 - x0_a1; - let denominator = asset0.amount + price * asset1.amount; - let y1 = numerator / denominator; - - ( - AnsAsset::new(config.pool_config.asset1, y1), - config.pool_config.asset0, - vec![ - Coin { - amount: x0.amount + price * y1, - denom: x0.denom, - }, - Coin { - amount: x1.amount - y1, - denom: x1.denom, - }, - ], - ) - } else { - let numerator = x0_a1 - x1_a0; - let denominator = - asset1.amount + Decimal::from_ratio(asset0.amount, 1u128) / price * Uint128::one(); - let y0 = numerator / denominator; - - ( - AnsAsset::new(config.pool_config.asset0, numerator / denominator), - config.pool_config.asset1, - vec![ - Coin { - amount: x0.amount - y0, - denom: x0.denom, - }, - Coin { - amount: x1.amount + Decimal::from_ratio(y0, 1u128) / price * Uint128::one(), - denom: x1.denom, - }, - ], - ) - }; - - resulting_balance.sort_by(|a, b| a.denom.cmp(&b.denom)); - // TODO, compute the resulting balance to be able to deposit back into the pool - Ok((offer_asset, ask_asset, resulting_balance)) -} - -#[allow(clippy::too_many_arguments)] -pub fn swap_to_enter_position( - deps: Deps, - env: &Env, - funds: Vec, - app: &App, - asset0: Coin, - asset1: Coin, - max_spread: Option, - belief_price0: Option, - belief_price1: Option, -) -> AppResult<(Vec, Vec)> { - let price = query_price(deps, &funds, app, max_spread, belief_price0, belief_price1)?; - let (offer_asset, ask_asset, resulting_assets) = - tokens_to_swap(deps, funds, asset0, asset1, price)?; - - Ok(( - swap_msg(deps, env, offer_asset, ask_asset, app)?, - resulting_assets, - )) -} +// pub(crate) fn tokens_to_swap( +// deps: Deps, +// amount_to_swap: Vec, +// asset0: Coin, // Represents the amount of Coin 0 we would like the position to handle +// asset1: Coin, // Represents the amount of Coin 1 we would like the position to handle, +// price: Decimal, // Relative price (when swapping amount0 for amount1, equals amount0/amount1) +// ) -> AppResult<(AnsAsset, AssetEntry, Vec)> { +// let config = CONFIG.load(deps.storage)?; + +// let x0 = amount_to_swap +// .iter() +// .find(|c| c.denom == asset0.denom) +// .cloned() +// .unwrap_or(Coin { +// denom: asset0.denom, +// amount: Uint128::zero(), +// }); +// let x1 = amount_to_swap +// .iter() +// .find(|c| c.denom == asset1.denom) +// .cloned() +// .unwrap_or(Coin { +// denom: asset1.denom, +// amount: Uint128::zero(), +// }); + +// // We will swap on the pool to get the right coin ratio + +// // We have x0 and x1 to deposit. Let p (or price) be the price of asset1 (the number of asset0 you get for 1 unit of asset1) +// // In order to deposit, you need to have X0 and X1 such that X0/X1 = A0/A1 where A0 and A1 are the current liquidity inside the position +// // That is equivalent to X0*A1 = X1*A0 +// // We need to find how much to swap. +// // If x0*A1 < x1*A0, we need to have more x0 to balance the swap --> so we need to send some of x1 to swap (lets say we wend y1 to swap) +// // So X1 = x1-y1 +// // X0 = x0 + price*y1 +// // Therefore, the following equation needs to be true +// // (x0 + price*y1)*A1 = (x1-y1)*A0 or y1 = (x1*a0 - x0*a1)/(a0 + p*a1) +// // If x0*A1 > x1*A0, we need to have more x1 to balance the swap --> so we need to send some of x0 to swap (lets say we wend y0 to swap) +// // So X0 = x0-y0 +// // X1 = x1 + y0/price +// // Therefore, the following equation needs to be true +// // (x0-y0)*A1 = (x1 + y0/price)*A0 or y0 = (x0*a1 - x1*a0)/(a1 + a0/p) + +// let x0_a1 = x0.amount * asset1.amount; +// let x1_a0 = x1.amount * asset0.amount; + +// let (offer_asset, ask_asset, mut resulting_balance) = if x0_a1 < x1_a0 { +// let numerator = x1_a0 - x0_a1; +// let denominator = asset0.amount + price * asset1.amount; +// let y1 = numerator / denominator; + +// ( +// AnsAsset::new(config.pool_config.asset1, y1), +// config.pool_config.asset0, +// vec![ +// Coin { +// amount: x0.amount + price * y1, +// denom: x0.denom, +// }, +// Coin { +// amount: x1.amount - y1, +// denom: x1.denom, +// }, +// ], +// ) +// } else { +// let numerator = x0_a1 - x1_a0; +// let denominator = +// asset1.amount + Decimal::from_ratio(asset0.amount, 1u128) / price * Uint128::one(); +// let y0 = numerator / denominator; + +// ( +// AnsAsset::new(config.pool_config.asset0, numerator / denominator), +// config.pool_config.asset1, +// vec![ +// Coin { +// amount: x0.amount - y0, +// denom: x0.denom, +// }, +// Coin { +// amount: x1.amount + Decimal::from_ratio(y0, 1u128) / price * Uint128::one(), +// denom: x1.denom, +// }, +// ], +// ) +// }; + +// resulting_balance.sort_by(|a, b| a.denom.cmp(&b.denom)); +// // TODO, compute the resulting balance to be able to deposit back into the pool +// Ok((offer_asset, ask_asset, resulting_balance)) +// } + +// #[allow(clippy::too_many_arguments)] +// pub fn swap_to_enter_position( +// deps: Deps, +// env: &Env, +// funds: Vec, +// app: &App, +// asset0: Coin, +// asset1: Coin, +// max_spread: Option, +// belief_price0: Option, +// belief_price1: Option, +// ) -> AppResult<(Vec, Vec)> { +// let price = query_price(deps, &funds, app, max_spread, belief_price0, belief_price1)?; +// let (offer_asset, ask_asset, resulting_assets) = +// tokens_to_swap(deps, funds, asset0, asset1, price)?; + +// Ok(( +// swap_msg(deps, env, offer_asset, ask_asset, app)?, +// resulting_assets, +// )) +// } #[cfg(test)] mod tests { use super::*; - use crate::state::{AutocompoundRewardsConfig, Config, PoolConfig}; + use crate::{ + state::{AutocompoundRewardsConfig, Config, PoolConfig}, + yield_sources::BalanceStrategy, + }; use cosmwasm_std::{coin, coins, testing::mock_dependencies, DepsMut, Uint64}; pub const DEPOSIT_TOKEN: &str = "USDC"; pub const TOKEN0: &str = "USDT"; @@ -180,20 +175,17 @@ mod tests { CONFIG.save( deps.storage, &Config { - pool_config: PoolConfig { - pool_id: 45, - token0: TOKEN0.to_string(), - token1: TOKEN1.to_string(), - asset0: AssetEntry::new(TOKEN0), - asset1: AssetEntry::new(TOKEN1), - }, - autocompound_cooldown_seconds: Uint64::zero(), - autocompound_rewards_config: AutocompoundRewardsConfig { - gas_asset: "foo".into(), - swap_asset: "bar".into(), - reward: Uint128::zero(), - min_gas_balance: Uint128::zero(), - max_gas_balance: Uint128::new(1), + dex: OSMOSIS.to_string(), + balance_strategy: BalanceStrategy(vec![]), + autocompound_config: crate::state::AutocompoundConfig { + cooldown_seconds: Uint64::zero(), + rewards: AutocompoundRewardsConfig { + gas_asset: "foo".into(), + swap_asset: "bar".into(), + reward: Uint128::zero(), + min_gas_balance: Uint128::zero(), + max_gas_balance: Uint128::new(1), + }, }, }, )?; diff --git a/contracts/carrot-app/src/yield_sources/mars.rs b/contracts/carrot-app/src/yield_sources/mars.rs index bcbb921f..a2725103 100644 --- a/contracts/carrot-app/src/yield_sources/mars.rs +++ b/contracts/carrot-app/src/yield_sources/mars.rs @@ -45,6 +45,15 @@ pub fn withdraw( Ok(vec![]) } +pub fn withdraw_rewards( + deps: Deps, + denom: String, + app: &App, +) -> AppResult<(Vec, Vec)> { + // Mars doesn't have rewards, it's automatically auto-compounded + Ok((vec![], vec![])) +} + pub fn user_deposit(deps: Deps, denom: String, app: &App) -> AppResult { let ans = app.name_service(deps); let ans_fund = ans.query(&AssetInfo::native(denom))?; diff --git a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs index f70486ac..9048b685 100644 --- a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs +++ b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs @@ -3,18 +3,17 @@ use std::str::FromStr; use crate::{ contract::{App, AppResult}, error::AppError, - handlers::query::query_strategy, replies::{OSMOSIS_ADD_TO_POSITION_REPLY_ID, OSMOSIS_CREATE_POSITION_REPLY_ID}, }; use abstract_app::traits::AccountIdentification; -use abstract_sdk::{AccountAction, Execution}; +use abstract_sdk::Execution; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Coin, CosmosMsg, Deps, Env, ReplyOn, SubMsg, Uint128}; +use cosmwasm_std::{Coin, Coins, CosmosMsg, Deps, Env, ReplyOn, SubMsg, Uint128}; use osmosis_std::{ cosmwasm_to_proto_coins, try_proto_to_cosmwasm_coins, types::osmosis::concentratedliquidity::v1beta1::{ - ConcentratedliquidityQuerier, FullPositionBreakdown, MsgAddToPosition, MsgCreatePosition, - MsgWithdrawPosition, + ConcentratedliquidityQuerier, FullPositionBreakdown, MsgAddToPosition, + MsgCollectIncentives, MsgCollectSpreadRewards, MsgCreatePosition, MsgWithdrawPosition, }, }; @@ -160,6 +159,49 @@ pub fn withdraw( .into()]) } +pub fn withdraw_rewards( + deps: Deps, + params: ConcentratedPoolParams, + app: &App, +) -> AppResult<(Vec, Vec)> { + let position = + get_osmosis_position_by_id(deps, params.position_id.ok_or(AppError::NoPosition {})?)?; + let position_details = position.position.unwrap(); + + let user = app.account_base(deps)?.proxy; + let mut rewards = Coins::default(); + let mut msgs: Vec = vec![]; + // If there are external incentives, claim them. + if !position.claimable_incentives.is_empty() { + for coin in try_proto_to_cosmwasm_coins(position.claimable_incentives)? { + rewards.add(coin)?; + } + msgs.push( + MsgCollectIncentives { + position_ids: vec![position_details.position_id], + sender: user.to_string(), + } + .into(), + ); + } + + // If there is income from swap fees, claim them. + if !position.claimable_spread_rewards.is_empty() { + for coin in try_proto_to_cosmwasm_coins(position.claimable_spread_rewards)? { + rewards.add(coin)?; + } + msgs.push( + MsgCollectSpreadRewards { + position_ids: vec![position_details.position_id], + sender: position_details.address.clone(), + } + .into(), + ) + } + + Ok((rewards.to_vec(), msgs)) +} + pub fn user_deposit( deps: Deps, _app: &App, diff --git a/contracts/carrot-app/src/yield_sources/yield_type.rs b/contracts/carrot-app/src/yield_sources/yield_type.rs index 661936d0..e5fd54b9 100644 --- a/contracts/carrot-app/src/yield_sources/yield_type.rs +++ b/contracts/carrot-app/src/yield_sources/yield_type.rs @@ -44,6 +44,15 @@ impl YieldType { } } + pub fn withdraw_rewards(self, deps: Deps, app: &App) -> AppResult<(Vec, Vec)> { + match self { + YieldType::ConcentratedLiquidityPool(params) => { + osmosis_cl_pool::withdraw_rewards(deps, params, app) + } + YieldType::Mars(denom) => mars::withdraw_rewards(deps, denom, app), + } + } + pub fn user_deposit(&self, deps: Deps, app: &App) -> AppResult> { match self { YieldType::ConcentratedLiquidityPool(params) => { diff --git a/contracts/carrot-app/tests/config.rs b/contracts/carrot-app/tests/config.rs new file mode 100644 index 00000000..d0ada8c0 --- /dev/null +++ b/contracts/carrot-app/tests/config.rs @@ -0,0 +1,104 @@ +mod common; + +use crate::common::{setup_test_tube, USDC, USDT}; +use carrot_app::{ + msg::{AppExecuteMsgFns, AppQueryMsgFns}, + yield_sources::{ + yield_type::{ConcentratedPoolParams, YieldType}, + BalanceStrategy, BalanceStrategyElement, YieldSource, + }, +}; +use common::{INITIAL_LOWER_TICK, INITIAL_UPPER_TICK}; +use cosmwasm_std::Decimal; +use cw_orch::{anyhow, prelude::*}; + +#[test] +fn rebalance_fails() -> anyhow::Result<()> { + let (_, carrot_app) = setup_test_tube(false)?; + + carrot_app + .rebalance(BalanceStrategy(vec![ + BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: vec![ + (USDT.to_string(), Decimal::percent(50)), + (USDC.to_string(), Decimal::percent(50)), + ], + ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + pool_id: 7, + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + }), + }, + share: Decimal::one(), + }, + BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: vec![ + (USDT.to_string(), Decimal::percent(50)), + (USDC.to_string(), Decimal::percent(50)), + ], + ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + pool_id: 7, + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + }), + }, + share: Decimal::one(), + }, + ])) + .unwrap_err(); + + // We query the nex strategy + + Ok(()) +} + +#[test] +fn rebalance_success() -> anyhow::Result<()> { + let (_, carrot_app) = setup_test_tube(false)?; + + let new_strat = BalanceStrategy(vec![ + BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: vec![ + (USDT.to_string(), Decimal::percent(50)), + (USDC.to_string(), Decimal::percent(50)), + ], + ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + pool_id: 7, + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + }), + }, + share: Decimal::percent(50), + }, + BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: vec![ + (USDT.to_string(), Decimal::percent(50)), + (USDC.to_string(), Decimal::percent(50)), + ], + ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + pool_id: 7, + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + }), + }, + share: Decimal::percent(50), + }, + ]); + carrot_app.rebalance(new_strat.clone())?; + + let strategy = carrot_app.strategy()?; + + assert_eq!(strategy.strategy, new_strat); + + // We query the nex strategy + + Ok(()) +} diff --git a/contracts/carrot-app/tests/deposit_withdraw.rs b/contracts/carrot-app/tests/deposit_withdraw.rs index c9e07b7d..4090dc70 100644 --- a/contracts/carrot-app/tests/deposit_withdraw.rs +++ b/contracts/carrot-app/tests/deposit_withdraw.rs @@ -5,9 +5,14 @@ use abstract_client::Application; use abstract_interface::Abstract; use carrot_app::{ msg::{AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse}, - yield_sources::osmosis_cl_pool::OsmosisPosition, + yield_sources::{ + osmosis_cl_pool::OsmosisPosition, + yield_type::{ConcentratedPoolParams, YieldType}, + BalanceStrategy, BalanceStrategyElement, YieldSource, + }, AppInterface, }; +use common::{INITIAL_LOWER_TICK, INITIAL_UPPER_TICK}; use cosmwasm_std::{coin, coins, Decimal, Uint128}; use cw_orch::{ anyhow, @@ -38,10 +43,8 @@ fn query_balances( fn deposit_lands() -> anyhow::Result<()> { let (_, carrot_app) = setup_test_tube(false)?; - let deposit_amount = 5_000; - let max_fee = Uint128::new(deposit_amount).mul_floor(Decimal::percent(3)); - // We should add funds to the account proxy + let deposit_amount = 5_000; let deposit_coins = coins(deposit_amount, USDT.to_owned()); let mut chain = carrot_app.get_chain().clone(); @@ -149,6 +152,64 @@ fn deposit_multiple_assets() -> anyhow::Result<()> { Ok(()) } +#[test] +fn deposit_multiple_positions() -> anyhow::Result<()> { + let (pool_id, carrot_app) = setup_test_tube(false)?; + + let new_strat = BalanceStrategy(vec![ + BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: vec![ + (USDT.to_string(), Decimal::percent(50)), + (USDC.to_string(), Decimal::percent(50)), + ], + ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + pool_id, + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + }), + }, + share: Decimal::percent(50), + }, + BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: vec![ + (USDT.to_string(), Decimal::percent(50)), + (USDC.to_string(), Decimal::percent(50)), + ], + ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + pool_id, + lower_tick: 2 * INITIAL_LOWER_TICK, + upper_tick: 2 * INITIAL_UPPER_TICK, + position_id: None, + }), + }, + share: Decimal::percent(50), + }, + ]); + carrot_app.rebalance(new_strat.clone())?; + + let deposit_amount = 5_000; + let deposit_coins = coins(deposit_amount, USDT.to_owned()); + let mut chain = carrot_app.get_chain().clone(); + + let balances_before = query_balances(&carrot_app)?; + chain.add_balance( + carrot_app.account().proxy()?.to_string(), + deposit_coins.clone(), + )?; + carrot_app.deposit(deposit_coins)?; + let balances_after = query_balances(&carrot_app)?; + + let slippage = Decimal::percent(4); + assert!( + balances_after + > balances_before + (Uint128::from(deposit_amount) * (Decimal::one() - slippage)) + ); + Ok(()) +} + // #[test] // fn create_position_on_instantiation() -> anyhow::Result<()> { // let (_, carrot_app) = setup_test_tube(true)?; From d1cfce13129529e93b7caea0cc75d2d38c0b2d37 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Tue, 26 Mar 2024 13:46:12 +0000 Subject: [PATCH 05/42] Rationalized exchange rate --- contracts/carrot-app/src/handlers/execute.rs | 38 ++++++++------------ contracts/carrot-app/src/handlers/query.rs | 14 ++++++++ 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index cbe701ac..ec0e7b34 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -10,14 +10,12 @@ use abstract_app::{abstract_sdk::features::AbstractResponse, objects::AnsAsset}; use abstract_dex_adapter::DexInterface; use abstract_sdk::{AccountAction, Execution, ExecutorMsg, TransferInterface}; use cosmwasm_std::{ - to_json_binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, StdError, SubMsg, - Uint128, WasmMsg, + to_json_binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Uint128, WasmMsg, }; -use std::collections::HashMap; use super::{ internal::{deposit_one_strategy, execute_finalize_deposit, execute_one_deposit_step}, - query::{query_exchange_rate, query_strategy}, + query::{query_all_exchange_rates, query_exchange_rate, query_strategy}, swap_helpers::swap_msg, }; @@ -177,27 +175,21 @@ pub fn _inner_deposit( ) -> AppResult> { // We determine the value of all the tokens that were received with USD - let all_strategy_exchange_rates = query_strategy(deps)?.strategy.0.into_iter().flat_map(|s| { - s.yield_source - .expected_tokens + let exchange_rates = query_all_exchange_rates( + deps, + query_strategy(deps)? + .strategy + .0 .into_iter() - .map(|(denom, _)| { - Ok::<_, AppError>(( - denom.clone(), - query_exchange_rate(deps, denom.clone(), app)?, - )) + .flat_map(|s| { + s.yield_source + .expected_tokens + .into_iter() + .map(|(denom, _)| denom) }) - }); - let exchange_rates = funds - .iter() - .map(|f| { - Ok::<_, AppError>(( - f.denom.clone(), - query_exchange_rate(deps, f.denom.clone(), app)?, - )) - }) - .chain(all_strategy_exchange_rates) - .collect::, _>>()?; + .chain(funds.iter().map(|f| f.denom.clone())), + app, + )?; let deposit_strategies = query_strategy(deps)? .strategy diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index 94e94558..66df7645 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use abstract_app::traits::AccountIdentification; use abstract_app::{ abstract_core::objects::AnsAsset, @@ -128,3 +130,15 @@ pub fn query_exchange_rate(_deps: Deps, _denom: String, _app: &App) -> AppResult // In the first iteration, all deposited tokens are assumed to be equal to 1 Ok(Decimal::one()) } + +// Returns a hashmap with all request exchange rates +pub fn query_all_exchange_rates( + deps: Deps, + denoms: impl Iterator, + app: &App, +) -> AppResult> { + denoms + .into_iter() + .map(|denom| Ok((denom.clone(), query_exchange_rate(deps, denom, app)?))) + .collect() +} From 3b8b374fec0b5b7b605b5ca6d22e6172a39fa8ff Mon Sep 17 00:00:00 2001 From: Kayanski Date: Tue, 26 Mar 2024 14:02:17 +0000 Subject: [PATCH 06/42] nits --- contracts/carrot-app/src/handlers/execute.rs | 3 +- .../carrot-app/src/handlers/swap_helpers.rs | 4 +- .../src/replies/osmosis/add_to_position.rs | 2 +- .../src/replies/osmosis/create_position.rs | 2 +- .../src/yield_sources/osmosis_cl_pool.rs | 106 ++++++++---------- 5 files changed, 50 insertions(+), 67 deletions(-) diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index ec0e7b34..b7602e62 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -173,8 +173,7 @@ pub fn _inner_deposit( funds: Vec, app: &App, ) -> AppResult> { - // We determine the value of all the tokens that were received with USD - + // We determine the value of all tokens that will be used inside this function let exchange_rates = query_all_exchange_rates( deps, query_strategy(deps)? diff --git a/contracts/carrot-app/src/handlers/swap_helpers.rs b/contracts/carrot-app/src/handlers/swap_helpers.rs index a5dc6cc3..c3336d48 100644 --- a/contracts/carrot-app/src/handlers/swap_helpers.rs +++ b/contracts/carrot-app/src/handlers/swap_helpers.rs @@ -10,8 +10,6 @@ use crate::{ state::CONFIG, }; -// use super::query::query_price; - pub(crate) fn swap_msg( deps: Deps, env: &Env, @@ -139,7 +137,7 @@ pub(crate) fn swap_msg( // belief_price0: Option, // belief_price1: Option, // ) -> AppResult<(Vec, Vec)> { -// let price = query_price(deps, &funds, app, max_spread, belief_price0, belief_price1)?; +// let price = query_swap_price(deps, &funds, app, max_spread, belief_price0, belief_price1)?; // let (offer_asset, ask_asset, resulting_assets) = // tokens_to_swap(deps, funds, asset0, asset1, price)?; diff --git a/contracts/carrot-app/src/replies/osmosis/add_to_position.rs b/contracts/carrot-app/src/replies/osmosis/add_to_position.rs index 4a322c88..90a0101b 100644 --- a/contracts/carrot-app/src/replies/osmosis/add_to_position.rs +++ b/contracts/carrot-app/src/replies/osmosis/add_to_position.rs @@ -7,7 +7,7 @@ use crate::{ error::AppError, handlers::{internal::save_strategy, query::query_strategy}, state::TEMP_CURRENT_YIELD, - yield_sources::{osmosis_cl_pool::OsmosisPosition, yield_type::YieldType}, + yield_sources::yield_type::YieldType, }; pub fn add_to_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) -> AppResult { diff --git a/contracts/carrot-app/src/replies/osmosis/create_position.rs b/contracts/carrot-app/src/replies/osmosis/create_position.rs index b753257c..3e45177f 100644 --- a/contracts/carrot-app/src/replies/osmosis/create_position.rs +++ b/contracts/carrot-app/src/replies/osmosis/create_position.rs @@ -7,7 +7,7 @@ use crate::{ error::AppError, handlers::{internal::save_strategy, query::query_strategy}, state::TEMP_CURRENT_YIELD, - yield_sources::{osmosis_cl_pool::OsmosisPosition, yield_type::YieldType}, + yield_sources::yield_type::YieldType, }; pub fn create_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) -> AppResult { diff --git a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs index 9048b685..30e5d72f 100644 --- a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs +++ b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs @@ -3,12 +3,15 @@ use std::str::FromStr; use crate::{ contract::{App, AppResult}, error::AppError, + handlers::swap_helpers::DEFAULT_SLIPPAGE, replies::{OSMOSIS_ADD_TO_POSITION_REPLY_ID, OSMOSIS_CREATE_POSITION_REPLY_ID}, + state::CONFIG, }; -use abstract_app::traits::AccountIdentification; +use abstract_app::{objects::AnsAsset, traits::AccountIdentification}; +use abstract_dex_adapter::DexInterface; use abstract_sdk::Execution; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Coin, Coins, CosmosMsg, Deps, Env, ReplyOn, SubMsg, Uint128}; +use cosmwasm_std::{ensure, Coin, Coins, CosmosMsg, Decimal, Deps, Env, ReplyOn, SubMsg, Uint128}; use osmosis_std::{ cosmwasm_to_proto_coins, try_proto_to_cosmwasm_coins, types::osmosis::concentratedliquidity::v1beta1::{ @@ -252,64 +255,47 @@ pub fn user_rewards( Ok(rewards.into()) } -// pub fn query_price( -// deps: Deps, -// funds: &[Coin], -// app: &App, -// max_spread: Option, -// belief_price0: Option, -// belief_price1: Option, -// ) -> AppResult { -// let config = CONFIG.load(deps.storage)?; - -// let amount0 = funds -// .iter() -// .find(|c| c.denom == config.pool_config.token0) -// .map(|c| c.amount) -// .unwrap_or_default(); -// let amount1 = funds -// .iter() -// .find(|c| c.denom == config.pool_config.token1) -// .map(|c| c.amount) -// .unwrap_or_default(); - -// // We take the biggest amount and simulate a swap for the corresponding asset -// let price = if amount0 > amount1 { -// let simulation_result = app.ans_dex(deps, OSMOSIS.to_string()).simulate_swap( -// AnsAsset::new(config.pool_config.asset0, amount0), -// config.pool_config.asset1, -// )?; - -// let price = Decimal::from_ratio(amount0, simulation_result.return_amount); -// if let Some(belief_price) = belief_price1 { -// ensure!( -// belief_price.abs_diff(price) <= max_spread.unwrap_or(DEFAULT_SLIPPAGE), -// AppError::MaxSpreadAssertion { price } -// ); -// } -// price -// } else { -// let simulation_result = app.ans_dex(deps, OSMOSIS.to_string()).simulate_swap( -// AnsAsset::new(config.pool_config.asset1, amount1), -// config.pool_config.asset0, -// )?; - -// let price = Decimal::from_ratio(simulation_result.return_amount, amount1); -// if let Some(belief_price) = belief_price0 { -// ensure!( -// belief_price.abs_diff(price) <= max_spread.unwrap_or(DEFAULT_SLIPPAGE), -// AppError::MaxSpreadAssertion { price } -// ); -// } -// price -// }; - -// Ok(price) -// } - -#[cw_serde] -pub struct OsmosisPosition { - pub position_id: u64, +pub fn query_swap_price( + deps: Deps, + app: &App, + max_spread: Option, + belief_price0: Option, + belief_price1: Option, + asset0: AnsAsset, + asset1: AnsAsset, +) -> AppResult { + let config = CONFIG.load(deps.storage)?; + + // We take the biggest amount and simulate a swap for the corresponding asset + let price = if asset0.amount > asset1.amount { + let simulation_result = app + .ans_dex(deps, config.dex.clone()) + .simulate_swap(asset0.clone(), asset1.name)?; + + let price = Decimal::from_ratio(asset0.amount, simulation_result.return_amount); + if let Some(belief_price) = belief_price1 { + ensure!( + belief_price.abs_diff(price) <= max_spread.unwrap_or(DEFAULT_SLIPPAGE), + AppError::MaxSpreadAssertion { price } + ); + } + price + } else { + let simulation_result = app + .ans_dex(deps, config.dex.clone()) + .simulate_swap(asset1.clone(), asset0.name)?; + + let price = Decimal::from_ratio(simulation_result.return_amount, asset1.amount); + if let Some(belief_price) = belief_price0 { + ensure!( + belief_price.abs_diff(price) <= max_spread.unwrap_or(DEFAULT_SLIPPAGE), + AppError::MaxSpreadAssertion { price } + ); + } + price + }; + + Ok(price) } pub fn get_osmosis_position_by_id( From 9c8d8f29a01fb0fd41db288bc99665f7e0da218e Mon Sep 17 00:00:00 2001 From: Kayanski Date: Tue, 26 Mar 2024 20:03:16 +0000 Subject: [PATCH 07/42] Added params to help deposit --- contracts/carrot-app/src/error.rs | 3 + contracts/carrot-app/src/handlers/execute.rs | 73 ++-- .../carrot-app/src/handlers/instantiate.rs | 4 +- contracts/carrot-app/src/handlers/query.rs | 8 +- .../carrot-app/src/handlers/swap_helpers.rs | 311 +----------------- contracts/carrot-app/src/msg.rs | 13 +- .../carrot-app/src/replies/after_swaps.rs | 5 - .../src/replies/osmosis/create_position.rs | 3 - contracts/carrot-app/src/state.rs | 1 - contracts/carrot-app/src/yield_sources.rs | 32 +- .../carrot-app/src/yield_sources/mars.rs | 13 +- .../src/yield_sources/osmosis_cl_pool.rs | 69 +++- contracts/carrot-app/tests/autocompound.rs | 187 +++++------ contracts/carrot-app/tests/common.rs | 14 +- contracts/carrot-app/tests/config.rs | 42 ++- .../carrot-app/tests/deposit_withdraw.rs | 33 +- contracts/carrot-app/tests/strategy.rs | 58 +++- 17 files changed, 382 insertions(+), 487 deletions(-) diff --git a/contracts/carrot-app/src/error.rs b/contracts/carrot-app/src/error.rs index 22b98c02..33ce1882 100644 --- a/contracts/carrot-app/src/error.rs +++ b/contracts/carrot-app/src/error.rs @@ -91,4 +91,7 @@ pub enum AppError { #[error("Wrong yield type when executing internal operations")] WrongYieldType {}, + + #[error("Invalid strategy format, check shares and parameters")] + InvalidStrategy {}, } diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index b7602e62..dedb3146 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -4,7 +4,7 @@ use crate::{ handlers::query::query_balance, msg::{AppExecuteMsg, ExecuteMsg}, state::{assert_contract, get_autocompound_status, Config, CONFIG}, - yield_sources::BalanceStrategy, + yield_sources::{BalanceStrategy, ExpectedToken}, }; use abstract_app::{abstract_sdk::features::AbstractResponse, objects::AnsAsset}; use abstract_dex_adapter::DexInterface; @@ -27,7 +27,10 @@ pub fn execute_handler( msg: AppExecuteMsg, ) -> AppResult { match msg { - AppExecuteMsg::Deposit { funds } => deposit(deps, env, info, funds, app), + AppExecuteMsg::Deposit { + funds, + yield_sources_params, + } => deposit(deps, env, info, funds, yield_sources_params, app), AppExecuteMsg::Withdraw { amount } => withdraw(deps, env, info, amount, app), AppExecuteMsg::Autocompound {} => autocompound(deps, env, info, app), AppExecuteMsg::Rebalance { strategy } => rebalance(deps, env, info, strategy, app), @@ -50,15 +53,23 @@ pub fn execute_handler( } } -fn deposit(deps: DepsMut, env: Env, info: MessageInfo, funds: Vec, app: App) -> AppResult { +fn deposit( + deps: DepsMut, + env: Env, + info: MessageInfo, + funds: Vec, + yield_source_params: Option>>>, + app: App, +) -> AppResult { // Only the admin (manager contracts or account owner) can deposit as well as the contract itself app.admin .assert_admin(deps.as_ref(), &info.sender) .or(assert_contract(&info, &env))?; - let deposit_msgs = _inner_deposit(deps.as_ref(), &env, funds, &app)?; - deps.api - .debug(&format!("All deposit messages {:?}", deposit_msgs)); + let yield_source_params = yield_source_params + .unwrap_or_else(|| vec![None; query_strategy(deps.as_ref()).unwrap().strategy.0.len()]); + + let deposit_msgs = _inner_deposit(deps.as_ref(), &env, funds, yield_source_params, &app)?; Ok(app.response("deposit").add_messages(deposit_msgs)) } @@ -103,25 +114,23 @@ fn rebalance( fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResult { // Everyone can autocompound + let strategy = query_strategy(deps.as_ref())?.strategy; // We withdraw all rewards from protocols - let (rewards, collect_rewards_msgs): (Vec>, Vec) = - query_strategy(deps.as_ref())? - .strategy - .0 - .into_iter() - .map(|s| { - let (rewards, raw_msgs) = - s.yield_source.ty.withdraw_rewards(deps.as_ref(), &app)?; - - Ok::<_, AppError>(( - rewards, - app.executor(deps.as_ref()) - .execute(vec![AccountAction::from_vec(raw_msgs)])?, - )) - }) - .collect::, _>>()? - .into_iter() - .unzip(); + let (rewards, collect_rewards_msgs): (Vec>, Vec) = strategy + .0 + .into_iter() + .map(|s| { + let (rewards, raw_msgs) = s.yield_source.ty.withdraw_rewards(deps.as_ref(), &app)?; + + Ok::<_, AppError>(( + rewards, + app.executor(deps.as_ref()) + .execute(vec![AccountAction::from_vec(raw_msgs)])?, + )) + }) + .collect::, _>>()? + .into_iter() + .unzip(); let all_rewards: Vec = rewards.into_iter().flatten().collect(); // If there are no rewards, we can't do anything @@ -134,6 +143,7 @@ fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResu contract_addr: env.contract.address.to_string(), msg: to_json_binary(&ExecuteMsg::Module(AppExecuteMsg::Deposit { funds: all_rewards, + yield_sources_params: None, }))?, funds: vec![], }); @@ -171,6 +181,7 @@ pub fn _inner_deposit( deps: Deps, env: &Env, funds: Vec, + yield_source_params: Vec>>, app: &App, ) -> AppResult> { // We determine the value of all tokens that will be used inside this function @@ -184,12 +195,24 @@ pub fn _inner_deposit( s.yield_source .expected_tokens .into_iter() - .map(|(denom, _)| denom) + .map(|ExpectedToken { denom, share: _ }| denom) }) .chain(funds.iter().map(|f| f.denom.clone())), app, )?; + // We correct the strategy if specified in parameters + let mut current_strategy_status = query_strategy(deps)?.strategy; + current_strategy_status + .0 + .iter_mut() + .zip(yield_source_params) + .for_each(|(strategy, params)| { + if let Some(param) = params { + strategy.yield_source.expected_tokens = param; + } + }); + let deposit_strategies = query_strategy(deps)? .strategy .fill_all(funds, &exchange_rates)?; diff --git a/contracts/carrot-app/src/handlers/instantiate.rs b/contracts/carrot-app/src/handlers/instantiate.rs index 12e80130..d4cb59cd 100644 --- a/contracts/carrot-app/src/handlers/instantiate.rs +++ b/contracts/carrot-app/src/handlers/instantiate.rs @@ -28,6 +28,7 @@ pub fn instantiate_handler( .rewards .check(deps.as_ref(), &msg.dex, ans.host())?; + let strategy_len = msg.balance_strategy.0.len(); let config: Config = Config { dex: msg.dex, balance_strategy: msg.balance_strategy, @@ -39,7 +40,8 @@ pub fn instantiate_handler( // If provided - do an initial deposit if let Some(funds) = msg.deposit { - let deposit_msgs = _inner_deposit(deps.as_ref(), &env, funds, &app)?; + let deposit_msgs = + _inner_deposit(deps.as_ref(), &env, funds, vec![None; strategy_len], &app)?; response = response.add_messages(deposit_msgs); } diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index 66df7645..cd8db1a7 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -6,7 +6,7 @@ use abstract_app::{ traits::{AbstractNameService, Resolve}, }; use abstract_dex_adapter::DexInterface; -use cosmwasm_std::{to_json_binary, Binary, Coins, Decimal, Deps, Env}; +use cosmwasm_std::{to_json_binary, Binary, Coins, Decimal, Deps, Env, Uint128}; use cw_asset::Asset; use crate::{ @@ -96,16 +96,20 @@ fn query_config(deps: Deps) -> AppResult { pub fn query_balance(deps: Deps, app: &App) -> AppResult { let mut funds = Coins::default(); + let mut total_value = Uint128::zero(); query_strategy(deps)?.strategy.0.iter().try_for_each(|s| { let deposit_value = s.yield_source.ty.user_deposit(deps, app)?; for fund in deposit_value { - funds.add(fund)?; + let exchange_rate = query_exchange_rate(deps, fund.denom.clone(), app)?; + funds.add(fund.clone())?; + total_value += fund.amount * exchange_rate; } Ok::<_, AppError>(()) })?; Ok(AssetsBalanceResponse { balances: funds.into(), + total_value, }) } diff --git a/contracts/carrot-app/src/handlers/swap_helpers.rs b/contracts/carrot-app/src/handlers/swap_helpers.rs index c3336d48..b18c75b3 100644 --- a/contracts/carrot-app/src/handlers/swap_helpers.rs +++ b/contracts/carrot-app/src/handlers/swap_helpers.rs @@ -1,18 +1,14 @@ use abstract_app::objects::{AnsAsset, AssetEntry}; -use abstract_dex_adapter::{msg::GenerateMessagesResponse, DexInterface}; -use abstract_sdk::AuthZInterface; -use cosmwasm_std::{Coin, CosmosMsg, Decimal, Deps, Env, Uint128}; +use abstract_dex_adapter::DexInterface; +use cosmwasm_std::{CosmosMsg, Decimal, Deps, Env}; const MAX_SPREAD_PERCENT: u64 = 20; pub const DEFAULT_SLIPPAGE: Decimal = Decimal::permille(5); -use crate::{ - contract::{App, AppResult, OSMOSIS}, - state::CONFIG, -}; +use crate::contract::{App, AppResult, OSMOSIS}; pub(crate) fn swap_msg( deps: Deps, - env: &Env, + _env: &Env, offer_asset: AnsAsset, ask_asset: AssetEntry, app: &App, @@ -32,302 +28,3 @@ pub(crate) fn swap_msg( Ok(Some(swap_msg)) } - -// pub(crate) fn tokens_to_swap( -// deps: Deps, -// amount_to_swap: Vec, -// asset0: Coin, // Represents the amount of Coin 0 we would like the position to handle -// asset1: Coin, // Represents the amount of Coin 1 we would like the position to handle, -// price: Decimal, // Relative price (when swapping amount0 for amount1, equals amount0/amount1) -// ) -> AppResult<(AnsAsset, AssetEntry, Vec)> { -// let config = CONFIG.load(deps.storage)?; - -// let x0 = amount_to_swap -// .iter() -// .find(|c| c.denom == asset0.denom) -// .cloned() -// .unwrap_or(Coin { -// denom: asset0.denom, -// amount: Uint128::zero(), -// }); -// let x1 = amount_to_swap -// .iter() -// .find(|c| c.denom == asset1.denom) -// .cloned() -// .unwrap_or(Coin { -// denom: asset1.denom, -// amount: Uint128::zero(), -// }); - -// // We will swap on the pool to get the right coin ratio - -// // We have x0 and x1 to deposit. Let p (or price) be the price of asset1 (the number of asset0 you get for 1 unit of asset1) -// // In order to deposit, you need to have X0 and X1 such that X0/X1 = A0/A1 where A0 and A1 are the current liquidity inside the position -// // That is equivalent to X0*A1 = X1*A0 -// // We need to find how much to swap. -// // If x0*A1 < x1*A0, we need to have more x0 to balance the swap --> so we need to send some of x1 to swap (lets say we wend y1 to swap) -// // So X1 = x1-y1 -// // X0 = x0 + price*y1 -// // Therefore, the following equation needs to be true -// // (x0 + price*y1)*A1 = (x1-y1)*A0 or y1 = (x1*a0 - x0*a1)/(a0 + p*a1) -// // If x0*A1 > x1*A0, we need to have more x1 to balance the swap --> so we need to send some of x0 to swap (lets say we wend y0 to swap) -// // So X0 = x0-y0 -// // X1 = x1 + y0/price -// // Therefore, the following equation needs to be true -// // (x0-y0)*A1 = (x1 + y0/price)*A0 or y0 = (x0*a1 - x1*a0)/(a1 + a0/p) - -// let x0_a1 = x0.amount * asset1.amount; -// let x1_a0 = x1.amount * asset0.amount; - -// let (offer_asset, ask_asset, mut resulting_balance) = if x0_a1 < x1_a0 { -// let numerator = x1_a0 - x0_a1; -// let denominator = asset0.amount + price * asset1.amount; -// let y1 = numerator / denominator; - -// ( -// AnsAsset::new(config.pool_config.asset1, y1), -// config.pool_config.asset0, -// vec![ -// Coin { -// amount: x0.amount + price * y1, -// denom: x0.denom, -// }, -// Coin { -// amount: x1.amount - y1, -// denom: x1.denom, -// }, -// ], -// ) -// } else { -// let numerator = x0_a1 - x1_a0; -// let denominator = -// asset1.amount + Decimal::from_ratio(asset0.amount, 1u128) / price * Uint128::one(); -// let y0 = numerator / denominator; - -// ( -// AnsAsset::new(config.pool_config.asset0, numerator / denominator), -// config.pool_config.asset1, -// vec![ -// Coin { -// amount: x0.amount - y0, -// denom: x0.denom, -// }, -// Coin { -// amount: x1.amount + Decimal::from_ratio(y0, 1u128) / price * Uint128::one(), -// denom: x1.denom, -// }, -// ], -// ) -// }; - -// resulting_balance.sort_by(|a, b| a.denom.cmp(&b.denom)); -// // TODO, compute the resulting balance to be able to deposit back into the pool -// Ok((offer_asset, ask_asset, resulting_balance)) -// } - -// #[allow(clippy::too_many_arguments)] -// pub fn swap_to_enter_position( -// deps: Deps, -// env: &Env, -// funds: Vec, -// app: &App, -// asset0: Coin, -// asset1: Coin, -// max_spread: Option, -// belief_price0: Option, -// belief_price1: Option, -// ) -> AppResult<(Vec, Vec)> { -// let price = query_swap_price(deps, &funds, app, max_spread, belief_price0, belief_price1)?; -// let (offer_asset, ask_asset, resulting_assets) = -// tokens_to_swap(deps, funds, asset0, asset1, price)?; - -// Ok(( -// swap_msg(deps, env, offer_asset, ask_asset, app)?, -// resulting_assets, -// )) -// } - -#[cfg(test)] -mod tests { - use super::*; - - use crate::{ - state::{AutocompoundRewardsConfig, Config, PoolConfig}, - yield_sources::BalanceStrategy, - }; - use cosmwasm_std::{coin, coins, testing::mock_dependencies, DepsMut, Uint64}; - pub const DEPOSIT_TOKEN: &str = "USDC"; - pub const TOKEN0: &str = "USDT"; - pub const TOKEN1: &str = DEPOSIT_TOKEN; - - fn assert_is_around(result: Uint128, expected: impl Into) { - let expected = expected.into().u128(); - let result = result.u128(); - - if expected < result - 1 || expected > result + 1 { - panic!("Results are not close enough") - } - } - - fn setup_config(deps: DepsMut) -> cw_orch::anyhow::Result<()> { - CONFIG.save( - deps.storage, - &Config { - dex: OSMOSIS.to_string(), - balance_strategy: BalanceStrategy(vec![]), - autocompound_config: crate::state::AutocompoundConfig { - cooldown_seconds: Uint64::zero(), - rewards: AutocompoundRewardsConfig { - gas_asset: "foo".into(), - swap_asset: "bar".into(), - reward: Uint128::zero(), - min_gas_balance: Uint128::zero(), - max_gas_balance: Uint128::new(1), - }, - }, - }, - )?; - Ok(()) - } - - // TODO: more tests on tokens_to_swap - #[test] - fn swap_for_ratio_one_to_one() { - let mut deps = mock_dependencies(); - setup_config(deps.as_mut()).unwrap(); - let (swap, ask_asset, _final_asset) = tokens_to_swap( - deps.as_ref(), - coins(5_000, DEPOSIT_TOKEN), - coin(100_000_000, TOKEN0), - coin(100_000_000, TOKEN1), - Decimal::one(), - ) - .unwrap(); - - assert_eq!( - swap, - AnsAsset { - name: AssetEntry::new("usdc"), - amount: Uint128::new(2500) - } - ); - assert_eq!(ask_asset, AssetEntry::new("usdt")); - } - - #[test] - fn swap_for_ratio_close_to_one() { - let mut deps = mock_dependencies(); - setup_config(deps.as_mut()).unwrap(); - let amount0 = 110_000_000; - let amount1 = 100_000_000; - - let (swap, ask_asset, _final_asset) = tokens_to_swap( - deps.as_ref(), - coins(5_000, DEPOSIT_TOKEN), - coin(amount0, TOKEN0), - coin(amount1, TOKEN1), - Decimal::one(), - ) - .unwrap(); - - assert_is_around(swap.amount, 5_000 - 5_000 * amount1 / (amount1 + amount0)); - assert_eq!(swap.name, AssetEntry::new(TOKEN1)); - assert_eq!(ask_asset, AssetEntry::new(TOKEN0)); - } - - #[test] - fn swap_for_ratio_far_from_one() { - let mut deps = mock_dependencies(); - setup_config(deps.as_mut()).unwrap(); - let amount0 = 90_000_000; - let amount1 = 10_000_000; - let (swap, ask_asset, _final_asset) = tokens_to_swap( - deps.as_ref(), - coins(5_000, DEPOSIT_TOKEN), - coin(amount0, TOKEN0), - coin(amount1, TOKEN1), - Decimal::one(), - ) - .unwrap(); - - assert_eq!( - swap, - AnsAsset { - name: AssetEntry::new(DEPOSIT_TOKEN), - amount: Uint128::new(5_000 - 5_000 * amount1 / (amount1 + amount0)) - } - ); - assert_eq!(ask_asset, AssetEntry::new(TOKEN0)); - } - - #[test] - fn swap_for_ratio_far_from_one_inverse() { - let mut deps = mock_dependencies(); - setup_config(deps.as_mut()).unwrap(); - let amount0 = 10_000_000; - let amount1 = 90_000_000; - let (swap, ask_asset, _final_asset) = tokens_to_swap( - deps.as_ref(), - coins(5_000, DEPOSIT_TOKEN), - coin(amount0, TOKEN0), - coin(amount1, TOKEN1), - Decimal::one(), - ) - .unwrap(); - - assert_is_around(swap.amount, 5_000 - 5_000 * amount1 / (amount1 + amount0)); - assert_eq!(swap.name, AssetEntry::new(TOKEN1)); - assert_eq!(ask_asset, AssetEntry::new(TOKEN0)); - } - - #[test] - fn swap_for_non_unit_price() { - let mut deps = mock_dependencies(); - setup_config(deps.as_mut()).unwrap(); - let amount0 = 10_000_000; - let amount1 = 90_000_000; - let price = Decimal::percent(150); - let (swap, ask_asset, _final_asset) = tokens_to_swap( - deps.as_ref(), - coins(5_000, DEPOSIT_TOKEN), - coin(amount0, TOKEN0), - coin(amount1, TOKEN1), - price, - ) - .unwrap(); - - assert_is_around( - swap.amount, - 5_000 - - 5_000 * amount1 - / (amount1 - + (Decimal::from_ratio(amount0, 1u128) / price * Uint128::one()).u128()), - ); - assert_eq!(swap.name, AssetEntry::new(TOKEN1)); - assert_eq!(ask_asset, AssetEntry::new(TOKEN0)); - } - - #[test] - fn swap_multiple_tokens_for_non_unit_price() { - let mut deps = mock_dependencies(); - setup_config(deps.as_mut()).unwrap(); - let amount0 = 10_000_000; - let amount1 = 10_000_000; - let price = Decimal::percent(150); - let (swap, ask_asset, _final_asset) = tokens_to_swap( - deps.as_ref(), - vec![coin(10_000, TOKEN0), coin(4_000, TOKEN1)], - coin(amount0, TOKEN0), - coin(amount1, TOKEN1), - price, - ) - .unwrap(); - - assert_eq!(swap.name, AssetEntry::new(TOKEN0)); - assert_eq!(ask_asset, AssetEntry::new(TOKEN1)); - assert_eq!( - 10_000 - swap.amount.u128(), - 4_000 + (Decimal::from_ratio(swap.amount, 1u128) / price * Uint128::one()).u128() - ); - } -} diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index e68d4048..831dd59c 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -5,7 +5,7 @@ use cw_asset::AssetBase; use crate::{ contract::App, state::AutocompoundConfig, - yield_sources::{yield_type::YieldType, BalanceStrategy, OneDepositStrategy}, + yield_sources::{yield_type::YieldType, BalanceStrategy, ExpectedToken, OneDepositStrategy}, }; // This is used for type safety and re-exporting the contract endpoint structs. @@ -34,7 +34,15 @@ pub enum AppExecuteMsg { /// Those funds will be distributed between yield sources according to the current strategy /// TODO : for now only send stable coins that have the same value as USD /// More tokens can be included when the oracle adapter is live - Deposit { funds: Vec }, + Deposit { + funds: Vec, + /// This is additional paramters used to change the funds repartition when doing an additional deposit + /// This is not used for a first deposit into a strategy that hasn't changed for instance + /// This is an options because this is not mandatory + /// The vector then has option inside of it because we might not want to change parameters for all strategies + /// We might not use a vector but use a (usize, Vec) instead to avoid having to pass a full vector everytime + yield_sources_params: Option>>>, + }, /// Partial withdraw of the funds available on the app /// If amount is omitted, withdraws everything that is on the app Withdraw { amount: Option }, @@ -105,6 +113,7 @@ pub struct AvailableRewardsResponse { #[cw_serde] pub struct AssetsBalanceResponse { pub balances: Vec, + pub total_value: Uint128, } #[cw_serde] diff --git a/contracts/carrot-app/src/replies/after_swaps.rs b/contracts/carrot-app/src/replies/after_swaps.rs index 39f2f84c..02a24976 100644 --- a/contracts/carrot-app/src/replies/after_swaps.rs +++ b/contracts/carrot-app/src/replies/after_swaps.rs @@ -12,8 +12,6 @@ pub fn after_swap_reply(deps: DepsMut, _env: Env, app: App, _reply: Reply) -> Ap let current_coins = get_proxy_balance(deps.as_ref(), &app, coins_before.denom.clone())?; // We just update the coins to deposit after the swap - deps.api - .debug(&format!("{:?}-{:?}", coins_before, current_coins)); if current_coins.amount > coins_before.amount { TEMP_DEPOSIT_COINS.update(deps.storage, |f| { add_funds( @@ -25,9 +23,6 @@ pub fn after_swap_reply(deps: DepsMut, _env: Env, app: App, _reply: Reply) -> Ap ) })?; } - deps.api.debug("Swap reply over"); - deps.api - .debug(&format!("-{:?}", TEMP_DEPOSIT_COINS.load(deps.storage)?)); Ok(app.response("after_swap_reply")) } diff --git a/contracts/carrot-app/src/replies/osmosis/create_position.rs b/contracts/carrot-app/src/replies/osmosis/create_position.rs index 3e45177f..46b7bde3 100644 --- a/contracts/carrot-app/src/replies/osmosis/create_position.rs +++ b/contracts/carrot-app/src/replies/osmosis/create_position.rs @@ -17,9 +17,6 @@ pub fn create_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) - ))); }; - deps.api - .debug(&format!("Inside create position reply : {:x?}", b)); - let parsed = cw_utils::parse_execute_response_data(&b)?; // Parse create position response diff --git a/contracts/carrot-app/src/state.rs b/contracts/carrot-app/src/state.rs index 5707596c..5ff0cc26 100644 --- a/contracts/carrot-app/src/state.rs +++ b/contracts/carrot-app/src/state.rs @@ -8,7 +8,6 @@ use cosmwasm_std::{ }; use cw_storage_plus::Item; -use crate::yield_sources::yield_type::YieldType; use crate::yield_sources::BalanceStrategy; use crate::{contract::AppResult, error::AppError, msg::CompoundStatus}; diff --git a/contracts/carrot-app/src/yield_sources.rs b/contracts/carrot-app/src/yield_sources.rs index ec3d4fef..03c674ec 100644 --- a/contracts/carrot-app/src/yield_sources.rs +++ b/contracts/carrot-app/src/yield_sources.rs @@ -23,14 +23,14 @@ use self::yield_type::YieldType; #[cw_serde] pub struct YieldSource { /// This id (denom, share) - pub expected_tokens: Vec<(String, Decimal)>, + pub expected_tokens: Vec, pub ty: YieldType, } impl YieldSource { pub fn check(&self) -> AppResult<()> { // First we check the share sums the 100 - let share_sum: Decimal = self.expected_tokens.iter().map(|e| e.1).sum(); + let share_sum: Decimal = self.expected_tokens.iter().map(|e| e.share).sum(); ensure_eq!( share_sum, Decimal::one(), @@ -42,10 +42,34 @@ impl YieldSource { ); // Then we check every yield strategy underneath + + match &self.ty { + YieldType::ConcentratedLiquidityPool(_) => { + // A valid CL pool strategy is for 2 assets + ensure_eq!(self.expected_tokens.len(), 2, AppError::InvalidStrategy {}); + } + YieldType::Mars(denom) => { + // We verify there is only one element in the shares vector + ensure_eq!(self.expected_tokens.len(), 1, AppError::InvalidStrategy {}); + // We verify the first element correspond to the mars deposit denom + ensure_eq!( + &self.expected_tokens[0].denom, + denom, + AppError::InvalidStrategy {} + ); + } + } + Ok(()) } } +#[cw_serde] +pub struct ExpectedToken { + pub denom: String, + pub share: Decimal, +} + // Related to balance strategies #[cw_serde] pub struct BalanceStrategy(pub Vec); @@ -102,7 +126,7 @@ impl BalanceStrategy { .yield_source .expected_tokens .iter() - .map(|(denom, share)| B { + .map(|ExpectedToken { denom, share }| B { denom: denom.clone(), raw_funds: Uint128::zero(), remaining_amount: share * source.share * total_value, @@ -121,7 +145,7 @@ impl BalanceStrategy { .expected_tokens .iter() .zip(status.iter_mut()) - .find(|((denom, _share), _status)| this_coin.denom.eq(denom)) + .find(|(ExpectedToken { denom, share: _ }, _status)| this_coin.denom.eq(denom)) .map(|(_, status)| status); if let Some(status) = this_denom_status { diff --git a/contracts/carrot-app/src/yield_sources/mars.rs b/contracts/carrot-app/src/yield_sources/mars.rs index a2725103..5f9434c6 100644 --- a/contracts/carrot-app/src/yield_sources/mars.rs +++ b/contracts/carrot-app/src/yield_sources/mars.rs @@ -1,10 +1,11 @@ use crate::contract::{App, AppResult}; +use crate::error::AppError; use abstract_app::traits::AccountIdentification; use abstract_app::{ objects::{AnsAsset, AssetEntry}, traits::AbstractNameService, }; -use cosmwasm_std::{Coin, CosmosMsg, Deps, SubMsg, Uint128}; +use cosmwasm_std::{ensure_eq, Coin, CosmosMsg, Decimal, Deps, SubMsg, Uint128}; use cw_asset::AssetInfo; pub fn deposit(deps: Deps, denom: String, amount: Uint128, app: &App) -> AppResult> { @@ -54,6 +55,16 @@ pub fn withdraw_rewards( Ok((vec![], vec![])) } +/// This computes the current shares between assets in the position +/// For mars, there is no share, the yield strategy is for 1 asset only +/// So we just return the given share (which should be valid) +pub fn current_share( + deps: Deps, + shares: Vec<(String, Decimal)>, +) -> AppResult> { + Ok(shares) +} + pub fn user_deposit(deps: Deps, denom: String, app: &App) -> AppResult { let ans = app.name_service(deps); let ans_fund = ans.query(&AssetInfo::native(denom))?; diff --git a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs index 30e5d72f..61541855 100644 --- a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs +++ b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs @@ -3,14 +3,13 @@ use std::str::FromStr; use crate::{ contract::{App, AppResult}, error::AppError, - handlers::swap_helpers::DEFAULT_SLIPPAGE, + handlers::{query::query_exchange_rate, swap_helpers::DEFAULT_SLIPPAGE}, replies::{OSMOSIS_ADD_TO_POSITION_REPLY_ID, OSMOSIS_CREATE_POSITION_REPLY_ID}, state::CONFIG, }; use abstract_app::{objects::AnsAsset, traits::AccountIdentification}; use abstract_dex_adapter::DexInterface; use abstract_sdk::Execution; -use cosmwasm_schema::cw_serde; use cosmwasm_std::{ensure, Coin, Coins, CosmosMsg, Decimal, Deps, Env, ReplyOn, SubMsg, Uint128}; use osmosis_std::{ cosmwasm_to_proto_coins, try_proto_to_cosmwasm_coins, @@ -63,8 +62,6 @@ fn create_position( OSMOSIS_CREATE_POSITION_REPLY_ID, )?; - deps.api.debug("Created position messages"); - Ok(vec![msg]) } @@ -105,9 +102,6 @@ fn raw_deposit( OSMOSIS_ADD_TO_POSITION_REPLY_ID, )?; - deps.api - .debug(&format!("Add to position message {:?}", funds)); - Ok(vec![deposit_msg]) } @@ -205,6 +199,67 @@ pub fn withdraw_rewards( Ok((rewards.to_vec(), msgs)) } +/// This computes the current shares between assets in the position +/// For osmosis, it fetches the position and returns the current asset value ratio between assets +/// This will be called everytime when analyzing the current strategy, even if the position doesn't exist +/// This function should not error if the position doesn't exist +pub fn current_share( + deps: Deps, + shares: Vec<(String, Decimal)>, + params: &ConcentratedPoolParams, + app: &App, +) -> AppResult> { + let position_id = if let Some(position_id) = params.position_id { + position_id + } else { + // No position ? --> We return the target strategy + return Ok(shares); + }; + + let position = if let Ok(position) = get_osmosis_position_by_id(deps, position_id) { + position + } else { + // No position ? --> We return the target strategy + return Ok(shares); + }; + + let (denom0, value0) = if let Some(asset) = position.asset0 { + let exchange_rate = query_exchange_rate(deps, asset.denom.clone(), app)?; + let value = Uint128::from_str(&asset.amount)? * exchange_rate; + (Some(asset.denom), value) + } else { + (None, Uint128::zero()) + }; + + let (denom1, value1) = if let Some(asset) = position.asset1 { + let exchange_rate = query_exchange_rate(deps, asset.denom.clone(), app)?; + let value = Uint128::from_str(&asset.amount)? * exchange_rate; + (Some(asset.denom), value) + } else { + (None, Uint128::zero()) + }; + + let total_value = value0 + value1; + // No value ? --> We return the target strategy + // This should be unreachable + if total_value.is_zero() { + return Ok(shares); + } + + if denom0.is_none() { + // If the first denom has no coins, all the value is in the second denom + Ok(vec![(denom1.unwrap(), Decimal::one())]) + } else if denom1.is_none() { + // If the second denom has no coins, all the value is in the first denom + Ok(vec![(denom0.unwrap(), Decimal::one())]) + } else { + Ok(vec![ + (denom0.unwrap(), Decimal::from_ratio(value0, total_value)), + (denom1.unwrap(), Decimal::from_ratio(value1, total_value)), + ]) + } +} + pub fn user_deposit( deps: Deps, _app: &App, diff --git a/contracts/carrot-app/tests/autocompound.rs b/contracts/carrot-app/tests/autocompound.rs index 9fc1fdcc..6f157016 100644 --- a/contracts/carrot-app/tests/autocompound.rs +++ b/contracts/carrot-app/tests/autocompound.rs @@ -1,96 +1,97 @@ -// mod common; - -// use crate::common::{ -// create_position, setup_test_tube, DEX_NAME, GAS_DENOM, LOTS, REWARD_DENOM, USDC, USDT, -// }; -// use abstract_app::abstract_interface::{Abstract, AbstractAccount}; -// use carrot_app::msg::{ -// AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse, AvailableRewardsResponse, -// CompoundStatus, CompoundStatusResponse, -// }; -// use cosmwasm_std::{coin, coins, Uint128}; -// use cw_asset::AssetBase; -// use cw_orch::osmosis_test_tube::osmosis_test_tube::Account; -// use cw_orch::{anyhow, prelude::*}; - -// #[test] -// fn check_autocompound() -> anyhow::Result<()> { -// let (_, carrot_app) = setup_test_tube(false)?; - -// let chain = carrot_app.get_chain().clone(); - -// // Create position -// create_position( -// &carrot_app, -// coins(100_000, USDT.to_owned()), -// coin(1_000_000, USDT.to_owned()), -// coin(1_000_000, USDC.to_owned()), -// )?; - -// // Do some swaps -// let dex: abstract_dex_adapter::interface::DexAdapter<_> = carrot_app.module()?; -// let abs = Abstract::load_from(chain.clone())?; -// let account_id = carrot_app.account().id()?; -// let account = AbstractAccount::new(&abs, account_id); -// chain.bank_send( -// account.proxy.addr_str()?, -// vec![ -// coin(200_000, USDC.to_owned()), -// coin(200_000, USDT.to_owned()), -// ], -// )?; -// for _ in 0..10 { -// dex.ans_swap((USDC, 50_000), USDT, DEX_NAME.to_string(), &account)?; -// dex.ans_swap((USDT, 50_000), USDC, DEX_NAME.to_string(), &account)?; -// } - -// // Check autocompound adds liquidity from the rewards and user balance remain unchanged - -// // Check it has some rewards to autocompound first -// let rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; -// assert!(!rewards.available_rewards.is_empty()); - -// // Save balances -// let balance_before_autocompound: AssetsBalanceResponse = carrot_app.balance()?; -// let balance_usdc_before_autocompound = chain -// .bank_querier() -// .balance(chain.sender(), Some(USDC.to_owned()))? -// .pop() -// .unwrap(); -// let balance_usdt_before_autocompound = chain -// .bank_querier() -// .balance(chain.sender(), Some(USDT.to_owned()))? -// .pop() -// .unwrap(); - -// // Autocompound -// chain.wait_seconds(300)?; -// carrot_app.autocompound()?; - -// // Save new balances -// let balance_after_autocompound: AssetsBalanceResponse = carrot_app.balance()?; -// let balance_usdc_after_autocompound = chain -// .bank_querier() -// .balance(chain.sender(), Some(USDC.to_owned()))? -// .pop() -// .unwrap(); -// let balance_usdt_after_autocompound = chain -// .bank_querier() -// .balance(chain.sender(), Some(USDT.to_owned()))? -// .pop() -// .unwrap(); - -// // Liquidity added -// assert!(balance_after_autocompound.liquidity > balance_before_autocompound.liquidity); -// // Only rewards went in there -// assert!(balance_usdc_after_autocompound.amount >= balance_usdc_before_autocompound.amount); -// assert!(balance_usdt_after_autocompound.amount >= balance_usdt_before_autocompound.amount,); -// // Check it used all of the rewards -// let rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; -// assert!(rewards.available_rewards.is_empty()); - -// Ok(()) -// } +mod common; + +use crate::common::{setup_test_tube, DEX_NAME, GAS_DENOM, LOTS, REWARD_DENOM, USDC, USDT}; +use abstract_app::abstract_interface::{Abstract, AbstractAccount}; +use carrot_app::msg::{ + AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse, AvailableRewardsResponse, + CompoundStatus, CompoundStatusResponse, +}; +use cosmwasm_std::{coin, coins, Uint128}; +use cw_asset::AssetBase; +use cw_orch::osmosis_test_tube::osmosis_test_tube::Account; +use cw_orch::{anyhow, prelude::*}; + +#[test] +fn check_autocompound() -> anyhow::Result<()> { + let (_, carrot_app) = setup_test_tube(false)?; + + let mut chain = carrot_app.get_chain().clone(); + + // Create position + let deposit_amount = 5_000; + let deposit_coins = coins(deposit_amount, USDT.to_owned()); + chain.add_balance( + carrot_app.account().proxy()?.to_string(), + deposit_coins.clone(), + )?; + + // Do the deposit + carrot_app.deposit(deposit_coins.clone(), None)?; + + // Do some swaps + let dex: abstract_dex_adapter::interface::DexAdapter<_> = carrot_app.module()?; + let abs = Abstract::load_from(chain.clone())?; + let account_id = carrot_app.account().id()?; + let account = AbstractAccount::new(&abs, account_id); + chain.bank_send( + account.proxy.addr_str()?, + vec![ + coin(200_000, USDC.to_owned()), + coin(200_000, USDT.to_owned()), + ], + )?; + for _ in 0..10 { + dex.ans_swap((USDC, 50_000), USDT, DEX_NAME.to_string(), &account)?; + dex.ans_swap((USDT, 50_000), USDC, DEX_NAME.to_string(), &account)?; + } + + // Check autocompound adds liquidity from the rewards and user balance remain unchanged + + // Check it has some rewards to autocompound first + let rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; + assert!(!rewards.available_rewards.is_empty()); + + // Save balances + let balance_before_autocompound: AssetsBalanceResponse = carrot_app.balance()?; + let balance_usdc_before_autocompound = chain + .bank_querier() + .balance(chain.sender(), Some(USDC.to_owned()))? + .pop() + .unwrap(); + let balance_usdt_before_autocompound = chain + .bank_querier() + .balance(chain.sender(), Some(USDT.to_owned()))? + .pop() + .unwrap(); + + // Autocompound + chain.wait_seconds(300)?; + carrot_app.autocompound()?; + + // Save new balances + let balance_after_autocompound: AssetsBalanceResponse = carrot_app.balance()?; + let balance_usdc_after_autocompound = chain + .bank_querier() + .balance(chain.sender(), Some(USDC.to_owned()))? + .pop() + .unwrap(); + let balance_usdt_after_autocompound = chain + .bank_querier() + .balance(chain.sender(), Some(USDT.to_owned()))? + .pop() + .unwrap(); + + // Liquidity added + assert!(balance_after_autocompound.total_value > balance_before_autocompound.total_value); + // Only rewards went in there + assert!(balance_usdc_after_autocompound.amount >= balance_usdc_before_autocompound.amount); + assert!(balance_usdt_after_autocompound.amount >= balance_usdt_before_autocompound.amount,); + // Check it used all of the rewards + let rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; + assert!(rewards.available_rewards.is_empty()); + + Ok(()) +} // #[test] // fn stranger_autocompound() -> anyhow::Result<()> { diff --git a/contracts/carrot-app/tests/common.rs b/contracts/carrot-app/tests/common.rs index 86e3084b..f1c105d9 100644 --- a/contracts/carrot-app/tests/common.rs +++ b/contracts/carrot-app/tests/common.rs @@ -11,7 +11,9 @@ use carrot_app::contract::{APP_ID, OSMOSIS}; use carrot_app::msg::AppInstantiateMsg; use carrot_app::state::{AutocompoundConfig, AutocompoundRewardsConfig}; use carrot_app::yield_sources::yield_type::{ConcentratedPoolParams, YieldType}; -use carrot_app::yield_sources::{BalanceStrategy, BalanceStrategyElement, YieldSource}; +use carrot_app::yield_sources::{ + BalanceStrategy, BalanceStrategyElement, ExpectedToken, YieldSource, +}; use cosmwasm_std::{coin, coins, to_json_binary, to_json_vec, Decimal, Uint128, Uint64}; use cw_asset::AssetInfoUnchecked; use cw_orch::osmosis_test_tube::osmosis_test_tube::Gamm; @@ -131,8 +133,14 @@ pub fn deploy( balance_strategy: BalanceStrategy(vec![BalanceStrategyElement { yield_source: YieldSource { expected_tokens: vec![ - (USDT.to_string(), Decimal::percent(50)), - (USDC.to_string(), Decimal::percent(50)), + ExpectedToken { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + ExpectedToken { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, ], ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { pool_id, diff --git a/contracts/carrot-app/tests/config.rs b/contracts/carrot-app/tests/config.rs index d0ada8c0..9915f022 100644 --- a/contracts/carrot-app/tests/config.rs +++ b/contracts/carrot-app/tests/config.rs @@ -5,7 +5,7 @@ use carrot_app::{ msg::{AppExecuteMsgFns, AppQueryMsgFns}, yield_sources::{ yield_type::{ConcentratedPoolParams, YieldType}, - BalanceStrategy, BalanceStrategyElement, YieldSource, + BalanceStrategy, BalanceStrategyElement, ExpectedToken, YieldSource, }, }; use common::{INITIAL_LOWER_TICK, INITIAL_UPPER_TICK}; @@ -21,8 +21,14 @@ fn rebalance_fails() -> anyhow::Result<()> { BalanceStrategyElement { yield_source: YieldSource { expected_tokens: vec![ - (USDT.to_string(), Decimal::percent(50)), - (USDC.to_string(), Decimal::percent(50)), + ExpectedToken { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + ExpectedToken { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, ], ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { pool_id: 7, @@ -36,8 +42,14 @@ fn rebalance_fails() -> anyhow::Result<()> { BalanceStrategyElement { yield_source: YieldSource { expected_tokens: vec![ - (USDT.to_string(), Decimal::percent(50)), - (USDC.to_string(), Decimal::percent(50)), + ExpectedToken { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + ExpectedToken { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, ], ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { pool_id: 7, @@ -64,8 +76,14 @@ fn rebalance_success() -> anyhow::Result<()> { BalanceStrategyElement { yield_source: YieldSource { expected_tokens: vec![ - (USDT.to_string(), Decimal::percent(50)), - (USDC.to_string(), Decimal::percent(50)), + ExpectedToken { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + ExpectedToken { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, ], ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { pool_id: 7, @@ -79,8 +97,14 @@ fn rebalance_success() -> anyhow::Result<()> { BalanceStrategyElement { yield_source: YieldSource { expected_tokens: vec![ - (USDT.to_string(), Decimal::percent(50)), - (USDC.to_string(), Decimal::percent(50)), + ExpectedToken { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + ExpectedToken { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, ], ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { pool_id: 7, diff --git a/contracts/carrot-app/tests/deposit_withdraw.rs b/contracts/carrot-app/tests/deposit_withdraw.rs index 4090dc70..fe509a96 100644 --- a/contracts/carrot-app/tests/deposit_withdraw.rs +++ b/contracts/carrot-app/tests/deposit_withdraw.rs @@ -6,9 +6,8 @@ use abstract_interface::Abstract; use carrot_app::{ msg::{AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse}, yield_sources::{ - osmosis_cl_pool::OsmosisPosition, yield_type::{ConcentratedPoolParams, YieldType}, - BalanceStrategy, BalanceStrategyElement, YieldSource, + BalanceStrategy, BalanceStrategyElement, ExpectedToken, YieldSource, }, AppInterface, }; @@ -55,7 +54,7 @@ fn deposit_lands() -> anyhow::Result<()> { )?; // Do the deposit - carrot_app.deposit(deposit_coins.clone())?; + carrot_app.deposit(deposit_coins.clone(), None)?; // Check almost everything landed let balances_after = query_balances(&carrot_app)?; assert!(balances_before < balances_after); @@ -66,7 +65,7 @@ fn deposit_lands() -> anyhow::Result<()> { deposit_coins.clone(), )?; // Do the second deposit - let response = carrot_app.deposit(vec![coin(deposit_amount, USDT.to_owned())])?; + let response = carrot_app.deposit(vec![coin(deposit_amount, USDT.to_owned())], None)?; // Check almost everything landed let balances_after_second = query_balances(&carrot_app)?; assert!(balances_after < balances_after_second); @@ -88,7 +87,7 @@ fn withdraw_position() -> anyhow::Result<()> { let deposit_coins = coins(deposit_amount, USDT.to_owned()); let proxy_addr = carrot_app.account().proxy()?; chain.add_balance(proxy_addr.to_string(), deposit_coins.clone())?; - carrot_app.deposit(deposit_coins)?; + carrot_app.deposit(deposit_coins, None)?; let balance: AssetsBalanceResponse = carrot_app.balance()?; let balance_usdc_before_withdraw = chain @@ -147,7 +146,7 @@ fn deposit_multiple_assets() -> anyhow::Result<()> { let proxy_addr = carrot_app.account().proxy()?; let deposit_coins = vec![coin(234, USDC.to_owned()), coin(258, USDT.to_owned())]; chain.add_balance(proxy_addr.to_string(), deposit_coins.clone())?; - carrot_app.deposit(deposit_coins)?; + carrot_app.deposit(deposit_coins, None)?; Ok(()) } @@ -160,8 +159,14 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { BalanceStrategyElement { yield_source: YieldSource { expected_tokens: vec![ - (USDT.to_string(), Decimal::percent(50)), - (USDC.to_string(), Decimal::percent(50)), + ExpectedToken { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + ExpectedToken { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, ], ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { pool_id, @@ -175,8 +180,14 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { BalanceStrategyElement { yield_source: YieldSource { expected_tokens: vec![ - (USDT.to_string(), Decimal::percent(50)), - (USDC.to_string(), Decimal::percent(50)), + ExpectedToken { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + ExpectedToken { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, ], ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { pool_id, @@ -199,7 +210,7 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { carrot_app.account().proxy()?.to_string(), deposit_coins.clone(), )?; - carrot_app.deposit(deposit_coins)?; + carrot_app.deposit(deposit_coins, None)?; let balances_after = query_balances(&carrot_app)?; let slippage = Decimal::percent(4); diff --git a/contracts/carrot-app/tests/strategy.rs b/contracts/carrot-app/tests/strategy.rs index bd87305e..120edc1f 100644 --- a/contracts/carrot-app/tests/strategy.rs +++ b/contracts/carrot-app/tests/strategy.rs @@ -1,33 +1,49 @@ use std::collections::HashMap; -use carrot_app::yield_sources::yield_type::YieldType; +use carrot_app::yield_sources::yield_type::{ConcentratedPoolParams, YieldType}; use cosmwasm_std::{coin, Decimal}; use carrot_app::state::compute_total_value; -use carrot_app::yield_sources::{BalanceStrategy, BalanceStrategyElement, YieldSource}; +use carrot_app::yield_sources::{ + BalanceStrategy, BalanceStrategyElement, ExpectedToken, YieldSource, +}; pub const LUNA: &str = "uluna"; pub const OSMOSIS: &str = "uosmo"; pub const STARGAZE: &str = "ustars"; pub const NEUTRON: &str = "untrn"; pub const USD: &str = "usd"; +pub const USDC: &str = "usdc"; pub fn mock_strategy() -> BalanceStrategy { BalanceStrategy(vec![ BalanceStrategyElement { yield_source: YieldSource { expected_tokens: vec![ - (LUNA.to_string(), Decimal::percent(30)), - (OSMOSIS.to_string(), Decimal::percent(10)), - (STARGAZE.to_string(), Decimal::percent(60)), + ExpectedToken { + denom: LUNA.to_string(), + share: Decimal::percent(90), + }, + ExpectedToken { + denom: OSMOSIS.to_string(), + share: Decimal::percent(10), + }, ], - ty: YieldType::Mars("usdc".to_string()), + ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + pool_id: 8, + lower_tick: 6, + upper_tick: -6, + position_id: None, + }), }, share: Decimal::percent(33), }, BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![(NEUTRON.to_string(), Decimal::percent(100))], + expected_tokens: vec![ExpectedToken { + denom: "usdc".to_string(), + share: Decimal::percent(100), + }], ty: YieldType::Mars("usdc".to_string()), }, share: Decimal::percent(67), @@ -64,14 +80,20 @@ fn bad_strategy_check_sum() -> cw_orch::anyhow::Result<()> { let strategy = BalanceStrategy(vec![ BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![(NEUTRON.to_string(), Decimal::percent(100))], + expected_tokens: vec![ExpectedToken { + denom: NEUTRON.to_string(), + share: Decimal::percent(100), + }], ty: YieldType::Mars("usdc".to_string()), }, share: Decimal::percent(33), }, BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![(NEUTRON.to_string(), Decimal::percent(100))], + expected_tokens: vec![ExpectedToken { + denom: NEUTRON.to_string(), + share: Decimal::percent(100), + }], ty: YieldType::Mars("usdc".to_string()), }, share: Decimal::percent(66), @@ -89,8 +111,14 @@ fn bad_strategy_check_sum_inner() -> cw_orch::anyhow::Result<()> { BalanceStrategyElement { yield_source: YieldSource { expected_tokens: vec![ - (NEUTRON.to_string(), Decimal::percent(33)), - (NEUTRON.to_string(), Decimal::percent(66)), + ExpectedToken { + denom: NEUTRON.to_string(), + share: Decimal::percent(33), + }, + ExpectedToken { + denom: NEUTRON.to_string(), + share: Decimal::percent(33), + }, ], ty: YieldType::Mars("usdc".to_string()), }, @@ -98,7 +126,10 @@ fn bad_strategy_check_sum_inner() -> cw_orch::anyhow::Result<()> { }, BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![(NEUTRON.to_string(), Decimal::percent(100))], + expected_tokens: vec![ExpectedToken { + denom: NEUTRON.to_string(), + share: Decimal::percent(100), + }], ty: YieldType::Mars("usdc".to_string()), }, share: Decimal::percent(67), @@ -129,6 +160,7 @@ fn value_fill_strategy() -> cw_orch::anyhow::Result<()> { (NEUTRON.to_string(), Decimal::percent(75)), (OSMOSIS.to_string(), Decimal::percent(10)), (STARGAZE.to_string(), Decimal::percent(35)), + (USDC.to_string(), Decimal::percent(101)), ] .into_iter() .collect(); @@ -146,7 +178,7 @@ fn value_fill_strategy() -> cw_orch::anyhow::Result<()> { let fill_result = strategy.fill_all(funds, &exchange_rates)?; assert_eq!(fill_result.len(), 2); - assert_eq!(fill_result[0].0.len(), 3); + assert_eq!(fill_result[0].0.len(), 2); assert_eq!(fill_result[1].0.len(), 1); Ok(()) } From 467433d63e98e2fb0b23ba43c0ca481cd18e9062 Mon Sep 17 00:00:00 2001 From: cyberhoward Date: Wed, 27 Mar 2024 12:50:59 +0200 Subject: [PATCH 08/42] nits --- bot/Cargo.toml | 1 - contracts/carrot-app/src/yield_sources/yield_type.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/Cargo.toml b/bot/Cargo.toml index d8e154eb..bb35d6d6 100644 --- a/bot/Cargo.toml +++ b/bot/Cargo.toml @@ -15,7 +15,6 @@ abstract-client = { workspace = true } osmosis-std = { version = "0.21.0" } cosmos-sdk-proto = { version = "0.20.0" } dotenv = "0.15.0" -# For cw-optimizoor env_logger = { version = "0.11.3", default-features = false } log = "0.4.20" tonic = "0.10.0" diff --git a/contracts/carrot-app/src/yield_sources/yield_type.rs b/contracts/carrot-app/src/yield_sources/yield_type.rs index e5fd54b9..a8170088 100644 --- a/contracts/carrot-app/src/yield_sources/yield_type.rs +++ b/contracts/carrot-app/src/yield_sources/yield_type.rs @@ -9,7 +9,7 @@ use super::{mars, osmosis_cl_pool}; pub enum YieldType { /// For osmosis CL Pools, you need a pool id to do your deposit, and that's all ConcentratedLiquidityPool(ConcentratedPoolParams), - /// For Mars CL Pools, you just need to deposit in the RedBank + /// For Mars, you just need to deposit in the RedBank /// You need to indicate the denom of the funds you want to deposit Mars(String), } From ac021d96f9f0573847eb9d6c0968b1ea369cf667 Mon Sep 17 00:00:00 2001 From: cyberhoward Date: Wed, 27 Mar 2024 12:53:16 +0200 Subject: [PATCH 09/42] rename expected_tokens to asset_distribution --- contracts/carrot-app/src/handlers/execute.rs | 2 +- contracts/carrot-app/src/yield_sources.rs | 10 +++++----- .../carrot-app/src/yield_sources/yield_type.rs | 5 ++++- contracts/carrot-app/tests/common.rs | 2 +- contracts/carrot-app/tests/config.rs | 8 ++++---- contracts/carrot-app/tests/deposit_withdraw.rs | 4 ++-- contracts/carrot-app/tests/strategy.rs | 16 ++++++++-------- 7 files changed, 25 insertions(+), 22 deletions(-) diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index b7602e62..6c2f98af 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -182,7 +182,7 @@ pub fn _inner_deposit( .into_iter() .flat_map(|s| { s.yield_source - .expected_tokens + .asset_distribution .into_iter() .map(|(denom, _)| denom) }) diff --git a/contracts/carrot-app/src/yield_sources.rs b/contracts/carrot-app/src/yield_sources.rs index ec3d4fef..7acd7be2 100644 --- a/contracts/carrot-app/src/yield_sources.rs +++ b/contracts/carrot-app/src/yield_sources.rs @@ -23,21 +23,21 @@ use self::yield_type::YieldType; #[cw_serde] pub struct YieldSource { /// This id (denom, share) - pub expected_tokens: Vec<(String, Decimal)>, + pub asset_distribution: Vec<(String, Decimal)>, pub ty: YieldType, } impl YieldSource { pub fn check(&self) -> AppResult<()> { // First we check the share sums the 100 - let share_sum: Decimal = self.expected_tokens.iter().map(|e| e.1).sum(); + let share_sum: Decimal = self.asset_distribution.iter().map(|e| e.1).sum(); ensure_eq!( share_sum, Decimal::one(), AppError::InvalidStrategySum { share_sum } ); ensure!( - !self.expected_tokens.is_empty(), + !self.asset_distribution.is_empty(), AppError::InvalidEmptyStrategy {} ); @@ -100,7 +100,7 @@ impl BalanceStrategy { .map(|source| { source .yield_source - .expected_tokens + .asset_distribution .iter() .map(|(denom, share)| B { denom: denom.clone(), @@ -118,7 +118,7 @@ impl BalanceStrategy { // Find the share for the specific denom inside the strategy let this_denom_status = strategy .yield_source - .expected_tokens + .asset_distribution .iter() .zip(status.iter_mut()) .find(|((denom, _share), _status)| this_coin.denom.eq(denom)) diff --git a/contracts/carrot-app/src/yield_sources/yield_type.rs b/contracts/carrot-app/src/yield_sources/yield_type.rs index a8170088..9f79c5ac 100644 --- a/contracts/carrot-app/src/yield_sources/yield_type.rs +++ b/contracts/carrot-app/src/yield_sources/yield_type.rs @@ -5,13 +5,16 @@ use crate::contract::{App, AppResult}; use super::{mars, osmosis_cl_pool}; +/// Denomination of a bank / token-factory / IBC token. +pub type Denom = String; + #[cw_serde] pub enum YieldType { /// For osmosis CL Pools, you need a pool id to do your deposit, and that's all ConcentratedLiquidityPool(ConcentratedPoolParams), /// For Mars, you just need to deposit in the RedBank /// You need to indicate the denom of the funds you want to deposit - Mars(String), + Mars(Denom), } impl YieldType { diff --git a/contracts/carrot-app/tests/common.rs b/contracts/carrot-app/tests/common.rs index 86e3084b..ffb663e7 100644 --- a/contracts/carrot-app/tests/common.rs +++ b/contracts/carrot-app/tests/common.rs @@ -130,7 +130,7 @@ pub fn deploy( }, balance_strategy: BalanceStrategy(vec![BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![ + asset_distribution: vec![ (USDT.to_string(), Decimal::percent(50)), (USDC.to_string(), Decimal::percent(50)), ], diff --git a/contracts/carrot-app/tests/config.rs b/contracts/carrot-app/tests/config.rs index d0ada8c0..9530765f 100644 --- a/contracts/carrot-app/tests/config.rs +++ b/contracts/carrot-app/tests/config.rs @@ -20,7 +20,7 @@ fn rebalance_fails() -> anyhow::Result<()> { .rebalance(BalanceStrategy(vec![ BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![ + asset_distribution: vec![ (USDT.to_string(), Decimal::percent(50)), (USDC.to_string(), Decimal::percent(50)), ], @@ -35,7 +35,7 @@ fn rebalance_fails() -> anyhow::Result<()> { }, BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![ + asset_distribution: vec![ (USDT.to_string(), Decimal::percent(50)), (USDC.to_string(), Decimal::percent(50)), ], @@ -63,7 +63,7 @@ fn rebalance_success() -> anyhow::Result<()> { let new_strat = BalanceStrategy(vec![ BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![ + asset_distribution: vec![ (USDT.to_string(), Decimal::percent(50)), (USDC.to_string(), Decimal::percent(50)), ], @@ -78,7 +78,7 @@ fn rebalance_success() -> anyhow::Result<()> { }, BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![ + asset_distribution: vec![ (USDT.to_string(), Decimal::percent(50)), (USDC.to_string(), Decimal::percent(50)), ], diff --git a/contracts/carrot-app/tests/deposit_withdraw.rs b/contracts/carrot-app/tests/deposit_withdraw.rs index 4090dc70..3713856c 100644 --- a/contracts/carrot-app/tests/deposit_withdraw.rs +++ b/contracts/carrot-app/tests/deposit_withdraw.rs @@ -159,7 +159,7 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { let new_strat = BalanceStrategy(vec![ BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![ + asset_distribution: vec![ (USDT.to_string(), Decimal::percent(50)), (USDC.to_string(), Decimal::percent(50)), ], @@ -174,7 +174,7 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { }, BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![ + asset_distribution: vec![ (USDT.to_string(), Decimal::percent(50)), (USDC.to_string(), Decimal::percent(50)), ], diff --git a/contracts/carrot-app/tests/strategy.rs b/contracts/carrot-app/tests/strategy.rs index bd87305e..918ee4cb 100644 --- a/contracts/carrot-app/tests/strategy.rs +++ b/contracts/carrot-app/tests/strategy.rs @@ -16,7 +16,7 @@ pub fn mock_strategy() -> BalanceStrategy { BalanceStrategy(vec![ BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![ + asset_distribution: vec![ (LUNA.to_string(), Decimal::percent(30)), (OSMOSIS.to_string(), Decimal::percent(10)), (STARGAZE.to_string(), Decimal::percent(60)), @@ -27,7 +27,7 @@ pub fn mock_strategy() -> BalanceStrategy { }, BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![(NEUTRON.to_string(), Decimal::percent(100))], + asset_distribution: vec![(NEUTRON.to_string(), Decimal::percent(100))], ty: YieldType::Mars("usdc".to_string()), }, share: Decimal::percent(67), @@ -40,14 +40,14 @@ fn bad_strategy_check_empty() -> cw_orch::anyhow::Result<()> { let strategy = BalanceStrategy(vec![ BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![], + asset_distribution: vec![], ty: YieldType::Mars("usdc".to_string()), }, share: Decimal::percent(33), }, BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![], + asset_distribution: vec![], ty: YieldType::Mars("usdc".to_string()), }, share: Decimal::percent(67), @@ -64,14 +64,14 @@ fn bad_strategy_check_sum() -> cw_orch::anyhow::Result<()> { let strategy = BalanceStrategy(vec![ BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![(NEUTRON.to_string(), Decimal::percent(100))], + asset_distribution: vec![(NEUTRON.to_string(), Decimal::percent(100))], ty: YieldType::Mars("usdc".to_string()), }, share: Decimal::percent(33), }, BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![(NEUTRON.to_string(), Decimal::percent(100))], + asset_distribution: vec![(NEUTRON.to_string(), Decimal::percent(100))], ty: YieldType::Mars("usdc".to_string()), }, share: Decimal::percent(66), @@ -88,7 +88,7 @@ fn bad_strategy_check_sum_inner() -> cw_orch::anyhow::Result<()> { let strategy = BalanceStrategy(vec![ BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![ + asset_distribution: vec![ (NEUTRON.to_string(), Decimal::percent(33)), (NEUTRON.to_string(), Decimal::percent(66)), ], @@ -98,7 +98,7 @@ fn bad_strategy_check_sum_inner() -> cw_orch::anyhow::Result<()> { }, BalanceStrategyElement { yield_source: YieldSource { - expected_tokens: vec![(NEUTRON.to_string(), Decimal::percent(100))], + asset_distribution: vec![(NEUTRON.to_string(), Decimal::percent(100))], ty: YieldType::Mars("usdc".to_string()), }, share: Decimal::percent(67), From 458cca4b73392b19def98fc378c0486af0fb2afc Mon Sep 17 00:00:00 2001 From: Kayanski Date: Wed, 27 Mar 2024 12:45:30 +0000 Subject: [PATCH 10/42] Added current status --- contracts/carrot-app/src/handlers/query.rs | 72 ++++++++++++++++++++++ contracts/carrot-app/src/helpers.rs | 38 +++++++++++- contracts/carrot-app/src/msg.rs | 6 +- contracts/carrot-app/src/yield_sources.rs | 11 ++-- contracts/carrot-app/tests/config.rs | 2 +- contracts/carrot-app/tests/query.rs | 50 +++++++++++++++ 6 files changed, 170 insertions(+), 9 deletions(-) create mode 100644 contracts/carrot-app/tests/query.rs diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index cd8db1a7..316e1a44 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -9,6 +9,7 @@ use abstract_dex_adapter::DexInterface; use cosmwasm_std::{to_json_binary, Binary, Coins, Decimal, Deps, Env, Uint128}; use cw_asset::Asset; +use crate::yield_sources::{BalanceStrategy, BalanceStrategyElement, ExpectedToken, YieldSource}; use crate::{ contract::{App, AppResult}, error::AppError, @@ -28,6 +29,7 @@ pub fn query_handler(deps: Deps, env: Env, app: &App, msg: AppQueryMsg) -> AppRe AppQueryMsg::Strategy {} => to_json_binary(&query_strategy(deps)?), AppQueryMsg::CompoundStatus {} => to_json_binary(&query_compound_status(deps, env, app)?), AppQueryMsg::RebalancePreview {} => todo!(), + AppQueryMsg::StrategyStatus {} => to_json_binary(&query_strategy_status(deps, app)?), } .map_err(Into::into) } @@ -90,6 +92,76 @@ pub fn query_strategy(deps: Deps) -> AppResult { }) } +pub fn query_strategy_status(deps: Deps, app: &App) -> AppResult { + let strategy = query_strategy(deps)?.strategy; + let exchange_rates = query_all_exchange_rates( + deps, + strategy.0.iter().flat_map(|s| { + s.yield_source + .expected_tokens + .iter() + .map(|ExpectedToken { denom, share: _ }| denom.clone()) + }), + app, + )?; + + // We get the value for each investment + let all_strategy_values = query_strategy(deps)? + .strategy + .0 + .iter() + .map(|s| { + let user_deposit = s.yield_source.ty.user_deposit(deps, app)?; + + // From this, we compute the shares within the investment + let each_value = user_deposit + .iter() + .map(|fund| { + let exchange_rate = exchange_rates + .get(&fund.denom) + .ok_or(AppError::NoExchangeRate(fund.denom.clone()))?; + + Ok::<_, AppError>((fund.denom.clone(), *exchange_rate * fund.amount)) + }) + .collect::, _>>()?; + + let total_value: Uint128 = each_value.iter().map(|(_denom, amount)| amount).sum(); + + let each_shares = each_value + .into_iter() + .map(|(denom, amount)| ExpectedToken { + denom, + share: Decimal::from_ratio(amount, total_value), + }) + .collect::>(); + + Ok::<_, AppError>((total_value, each_shares)) + }) + .collect::, _>>()?; + + let all_strategies_value: Uint128 = all_strategy_values.iter().map(|(value, _)| value).sum(); + + // Finally, we dispatch the total_value to get investment shares + Ok(StrategyResponse { + strategy: BalanceStrategy( + strategy + .0 + .into_iter() + .zip(all_strategy_values) + .map( + |(original_strategy, (value, shares))| BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: shares, + ty: original_strategy.yield_source.ty, + }, + share: Decimal::from_ratio(value, all_strategies_value), + }, + ) + .collect(), + ), + }) +} + fn query_config(deps: Deps) -> AppResult { Ok(CONFIG.load(deps.storage)?) } diff --git a/contracts/carrot-app/src/helpers.rs b/contracts/carrot-app/src/helpers.rs index 25ab7326..2413c768 100644 --- a/contracts/carrot-app/src/helpers.rs +++ b/contracts/carrot-app/src/helpers.rs @@ -2,7 +2,7 @@ use crate::contract::{App, AppResult}; use abstract_app::traits::AccountIdentification; use abstract_app::{objects::AssetEntry, traits::AbstractNameService}; use abstract_sdk::Resolve; -use cosmwasm_std::{Addr, Coin, Coins, Deps, StdResult, Uint128}; +use cosmwasm_std::{Addr, Coin, Coins, Decimal, Deps, StdResult, Uint128}; pub fn get_balance(a: AssetEntry, deps: Deps, address: Addr, app: &App) -> AppResult { let denom = a.resolve(&deps.querier, &app.ans_host(deps)?)?; @@ -21,3 +21,39 @@ pub fn add_funds(funds: Vec, to_add: Coin) -> StdResult> { funds.add(to_add)?; Ok(funds.into()) } + +pub const CLOSE_PER_MILLE: u64 = 1; + +/// Returns wether actual is close to expected within CLOSE_PER_MILLE per mille +pub fn close_to(expected: Decimal, actual: Decimal) -> bool { + let close_coeff = expected * Decimal::permille(CLOSE_PER_MILLE); + + if expected == Decimal::zero() { + return actual < close_coeff; + } + + actual > expected * (Decimal::one() - close_coeff) + && actual < expected * (Decimal::one() + close_coeff) +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use cosmwasm_std::Decimal; + + use crate::helpers::close_to; + + #[test] + fn not_close_to() { + assert!(!close_to(Decimal::percent(99), Decimal::one())) + } + + #[test] + fn actually_close_to() { + assert!(close_to( + Decimal::from_str("0.99999").unwrap(), + Decimal::one() + )); + } +} diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index 831dd59c..483e5425 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -88,10 +88,14 @@ pub enum AppQueryMsg { /// Returns [`CompoundStatusResponse`] #[returns(CompoundStatusResponse)] CompoundStatus {}, - /// Returns the current strategy + /// Returns the current strategy as stored in the application /// Returns [`StrategyResponse`] #[returns(StrategyResponse)] Strategy {}, + /// Returns the current funds distribution between all the strategies + /// Returns [`StrategyResponse`] + #[returns(StrategyResponse)] + StrategyStatus {}, /// Returns a preview of the rebalance distribution /// Returns [`RebalancePreviewResponse`] #[returns(RebalancePreviewResponse)] diff --git a/contracts/carrot-app/src/yield_sources.rs b/contracts/carrot-app/src/yield_sources.rs index 03c674ec..e7098ffe 100644 --- a/contracts/carrot-app/src/yield_sources.rs +++ b/contracts/carrot-app/src/yield_sources.rs @@ -11,6 +11,7 @@ use std::collections::HashMap; use crate::{ contract::AppResult, error::AppError, + helpers::close_to, msg::{AppExecuteMsg, ExecuteMsg}, state::compute_total_value, }; @@ -31,9 +32,8 @@ impl YieldSource { pub fn check(&self) -> AppResult<()> { // First we check the share sums the 100 let share_sum: Decimal = self.expected_tokens.iter().map(|e| e.share).sum(); - ensure_eq!( - share_sum, - Decimal::one(), + ensure!( + close_to(Decimal::one(), share_sum), AppError::InvalidStrategySum { share_sum } ); ensure!( @@ -89,9 +89,8 @@ impl BalanceStrategy { pub fn check(&self) -> AppResult<()> { // First we check the share sums the 100 let share_sum: Decimal = self.0.iter().map(|e| e.share).sum(); - ensure_eq!( - share_sum, - Decimal::one(), + ensure!( + close_to(Decimal::one(), share_sum), AppError::InvalidStrategySum { share_sum } ); ensure!(!self.0.is_empty(), AppError::InvalidEmptyStrategy {}); diff --git a/contracts/carrot-app/tests/config.rs b/contracts/carrot-app/tests/config.rs index 9915f022..3bbe4357 100644 --- a/contracts/carrot-app/tests/config.rs +++ b/contracts/carrot-app/tests/config.rs @@ -10,7 +10,7 @@ use carrot_app::{ }; use common::{INITIAL_LOWER_TICK, INITIAL_UPPER_TICK}; use cosmwasm_std::Decimal; -use cw_orch::{anyhow, prelude::*}; +use cw_orch::anyhow; #[test] fn rebalance_fails() -> anyhow::Result<()> { diff --git a/contracts/carrot-app/tests/query.rs b/contracts/carrot-app/tests/query.rs new file mode 100644 index 00000000..12a56fca --- /dev/null +++ b/contracts/carrot-app/tests/query.rs @@ -0,0 +1,50 @@ +mod common; + +use crate::common::{setup_test_tube, USDT}; +use carrot_app::{ + helpers::close_to, + msg::{AppExecuteMsgFns, AppQueryMsgFns}, +}; +use cosmwasm_std::{coins, Decimal}; +use cw_orch::{anyhow, prelude::*}; + +#[test] +fn query_strategy_status() -> anyhow::Result<()> { + let (_, carrot_app) = setup_test_tube(false)?; + + // We should add funds to the account proxy + let deposit_amount = 5_000; + let deposit_coins = coins(deposit_amount, USDT.to_owned()); + let mut chain = carrot_app.get_chain().clone(); + + chain.add_balance( + carrot_app.account().proxy()?.to_string(), + deposit_coins.clone(), + )?; + + // Do the deposit + carrot_app.deposit(deposit_coins.clone(), None)?; + + let strategy = carrot_app.strategy_status()?.strategy; + + assert_eq!(strategy.0.len(), 1); + let single_strategy = strategy.0[0].clone(); + assert_eq!(single_strategy.share, Decimal::one()); + assert_eq!(single_strategy.yield_source.expected_tokens.len(), 2); + // The strategy shares are a little off 50% + assert_ne!( + single_strategy.yield_source.expected_tokens[0].share, + Decimal::percent(50) + ); + assert_ne!( + single_strategy.yield_source.expected_tokens[1].share, + Decimal::percent(50) + ); + assert!(close_to( + Decimal::one(), + single_strategy.yield_source.expected_tokens[0].share + + single_strategy.yield_source.expected_tokens[1].share + ),); + + Ok(()) +} From 37f877203bfa9c9be76d58831a39173d760d8fef Mon Sep 17 00:00:00 2001 From: Kayanski Date: Wed, 27 Mar 2024 13:24:35 +0000 Subject: [PATCH 11/42] Incorporated money market --- Cargo.toml | 7 +- contracts/carrot-app/Cargo.toml | 3 + contracts/carrot-app/src/contract.rs | 35 +++++++--- .../carrot-app/src/yield_sources/mars.rs | 55 +++++++--------- .../src/yield_sources/osmosis_cl_pool.rs | 64 +------------------ 5 files changed, 62 insertions(+), 102 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 57789e94..7e8c3f4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,17 +9,22 @@ cw-orch = "0.20.1" abstract-app = { version = "0.21.0" } abstract-interface = { version = "0.21.0" } abstract-dex-adapter = { git = "https://github.com/abstractsdk/abstract.git", tag = "v0.21.0" } +abstract-money-market-adapter = { git = "https://github.com/abstractsdk/abstract.git" } +abstract-money-market-standard = { git = "https://github.com/abstractsdk/abstract.git" } abstract-client = { version = "0.21.0" } abstract-testing = { version = "0.21.0" } abstract-sdk = { version = "0.21.0", features = ["stargate"] } [patch.crates-io] +# This is included to avoid recompinling too much osmosis-test-tube = { path = "../test-tube/packages/osmosis-test-tube" } -# This was added to account for the fix data forwaring in the proxy contract +# This was added to account for the fix data forwaring in the proxy contract + moneymarket adapter abstract-app = { git = "https://github.com/abstractsdk/abstract.git" } +abstract-adapter = { git = "https://github.com/abstractsdk/abstract.git" } +abstract-adapter-utils = { git = "https://github.com/abstractsdk/abstract.git" } abstract-interface = { git = "https://github.com/abstractsdk/abstract.git" } abstract-client = { git = "https://github.com/abstractsdk/abstract.git" } abstract-testing = { git = "https://github.com/abstractsdk/abstract.git" } diff --git a/contracts/carrot-app/Cargo.toml b/contracts/carrot-app/Cargo.toml index c90f14cd..c31eea8d 100644 --- a/contracts/carrot-app/Cargo.toml +++ b/contracts/carrot-app/Cargo.toml @@ -34,6 +34,7 @@ interface = [ "dep:cw-orch", "abstract-app/interface-macro", "abstract-dex-adapter/interface", + "abstract-money-market-adapter/interface", ] schema = ["abstract-app/schema"] @@ -52,7 +53,9 @@ abstract-sdk = { workspace = true } # Dependencies for interface abstract-dex-adapter = { workspace = true, features = ["osmosis"] } +abstract-money-market-adapter = { workspace = true, features = ["mars"] } cw-orch = { workspace = true, optional = true } +abstract-money-market-standard = { workspace = true } osmosis-std = { version = "0.21.0" } prost = { version = "0.12.3" } diff --git a/contracts/carrot-app/src/contract.rs b/contracts/carrot-app/src/contract.rs index d60fc116..3bde956b 100644 --- a/contracts/carrot-app/src/contract.rs +++ b/contracts/carrot-app/src/contract.rs @@ -65,14 +65,33 @@ impl abstract_app::abstract_interface::Depen as abstract_app::abstract_interface::DependencyCreation>::dependency_install_configs( cosmwasm_std::Empty {}, )?; + let moneymarket_dependency_install_configs: Vec = + as abstract_app::abstract_interface::DependencyCreation>::dependency_install_configs( + cosmwasm_std::Empty {}, + )?; - let adapter_install_config = abstract_app::abstract_core::manager::ModuleInstallConfig::new( - abstract_app::abstract_core::objects::module::ModuleInfo::from_id( - abstract_dex_adapter::DEX_ADAPTER_ID, - abstract_dex_adapter::contract::CONTRACT_VERSION.into(), - )?, - None, - ); - Ok([dex_dependency_install_configs, vec![adapter_install_config]].concat()) + let adapter_install_config = vec![ + abstract_app::abstract_core::manager::ModuleInstallConfig::new( + abstract_app::abstract_core::objects::module::ModuleInfo::from_id( + abstract_dex_adapter::DEX_ADAPTER_ID, + abstract_dex_adapter::contract::CONTRACT_VERSION.into(), + )?, + None, + ), + abstract_app::abstract_core::manager::ModuleInstallConfig::new( + abstract_app::abstract_core::objects::module::ModuleInfo::from_id( + abstract_money_market_adapter::MONEY_MARKET_ADAPTER_ID, + abstract_money_market_adapter::contract::CONTRACT_VERSION.into(), + )?, + None, + ), + ]; + + Ok([ + dex_dependency_install_configs, + moneymarket_dependency_install_configs, + adapter_install_config, + ] + .concat()) } } diff --git a/contracts/carrot-app/src/yield_sources/mars.rs b/contracts/carrot-app/src/yield_sources/mars.rs index 5f9434c6..403110bf 100644 --- a/contracts/carrot-app/src/yield_sources/mars.rs +++ b/contracts/carrot-app/src/yield_sources/mars.rs @@ -5,20 +5,23 @@ use abstract_app::{ objects::{AnsAsset, AssetEntry}, traits::AbstractNameService, }; +use abstract_money_market_adapter::msg::MoneyMarketQueryMsg; +use abstract_money_market_adapter::MoneyMarketInterface; use cosmwasm_std::{ensure_eq, Coin, CosmosMsg, Decimal, Deps, SubMsg, Uint128}; use cw_asset::AssetInfo; +use abstract_money_market_standard::query::MoneyMarketAnsQuery; + +pub const MARS_MONEY_MARKET: &str = "mars"; + pub fn deposit(deps: Deps, denom: String, amount: Uint128, app: &App) -> AppResult> { let ans = app.name_service(deps); let ans_fund = ans.query(&AssetInfo::native(denom))?; - // TODO after MM Adapter is merged - // Ok(vec![app - // .ans_money_market(deps)? - // .deposit(AnsAsset::new(ans_fund, amount))? - // .into()]) - - Ok(vec![]) + Ok(vec![SubMsg::new( + app.ans_money_market(deps, MARS_MONEY_MARKET.to_string()) + .deposit(AnsAsset::new(ans_fund, amount))?, + )]) } pub fn withdraw( @@ -37,13 +40,10 @@ pub fn withdraw( let ans_fund = ans.query(&AssetInfo::native(denom))?; - // TODO after MM Adapter is merged - // Ok(vec![app - // .ans_money_market(deps)? - // .withdraw(AnsAsset::new(ans_fund, amount))? - // .into()]) - - Ok(vec![]) + Ok(vec![app + .ans_money_market(deps, MARS_MONEY_MARKET.to_string()) + .withdraw(AnsAsset::new(ans_fund, amount))? + .into()]) } pub fn withdraw_rewards( @@ -55,27 +55,20 @@ pub fn withdraw_rewards( Ok((vec![], vec![])) } -/// This computes the current shares between assets in the position -/// For mars, there is no share, the yield strategy is for 1 asset only -/// So we just return the given share (which should be valid) -pub fn current_share( - deps: Deps, - shares: Vec<(String, Decimal)>, -) -> AppResult> { - Ok(shares) -} - pub fn user_deposit(deps: Deps, denom: String, app: &App) -> AppResult { let ans = app.name_service(deps); - let ans_fund = ans.query(&AssetInfo::native(denom))?; + let asset = ans.query(&AssetInfo::native(denom))?; let user = app.account_base(deps)?.proxy; - // TODO after MM Adapter is merged - // Ok(app - // .ans_money_market(deps)? - // .user_deposit(user, ans_fund)? - // .into()) - Ok(Uint128::zero()) + Ok(app + .ans_money_market(deps, MARS_MONEY_MARKET.to_string()) + .query(MoneyMarketQueryMsg::MoneyMarketAnsQuery { + query: MoneyMarketAnsQuery::UserDeposit { + user: user.to_string(), + asset, + }, + money_market: MARS_MONEY_MARKET.to_string(), + })?) } /// Returns an amount representing a user's liquidity diff --git a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs index 61541855..1acc31de 100644 --- a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs +++ b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use crate::{ contract::{App, AppResult}, error::AppError, - handlers::{query::query_exchange_rate, swap_helpers::DEFAULT_SLIPPAGE}, + handlers::swap_helpers::DEFAULT_SLIPPAGE, replies::{OSMOSIS_ADD_TO_POSITION_REPLY_ID, OSMOSIS_CREATE_POSITION_REPLY_ID}, state::CONFIG, }; @@ -199,67 +199,7 @@ pub fn withdraw_rewards( Ok((rewards.to_vec(), msgs)) } -/// This computes the current shares between assets in the position -/// For osmosis, it fetches the position and returns the current asset value ratio between assets -/// This will be called everytime when analyzing the current strategy, even if the position doesn't exist -/// This function should not error if the position doesn't exist -pub fn current_share( - deps: Deps, - shares: Vec<(String, Decimal)>, - params: &ConcentratedPoolParams, - app: &App, -) -> AppResult> { - let position_id = if let Some(position_id) = params.position_id { - position_id - } else { - // No position ? --> We return the target strategy - return Ok(shares); - }; - - let position = if let Ok(position) = get_osmosis_position_by_id(deps, position_id) { - position - } else { - // No position ? --> We return the target strategy - return Ok(shares); - }; - - let (denom0, value0) = if let Some(asset) = position.asset0 { - let exchange_rate = query_exchange_rate(deps, asset.denom.clone(), app)?; - let value = Uint128::from_str(&asset.amount)? * exchange_rate; - (Some(asset.denom), value) - } else { - (None, Uint128::zero()) - }; - - let (denom1, value1) = if let Some(asset) = position.asset1 { - let exchange_rate = query_exchange_rate(deps, asset.denom.clone(), app)?; - let value = Uint128::from_str(&asset.amount)? * exchange_rate; - (Some(asset.denom), value) - } else { - (None, Uint128::zero()) - }; - - let total_value = value0 + value1; - // No value ? --> We return the target strategy - // This should be unreachable - if total_value.is_zero() { - return Ok(shares); - } - - if denom0.is_none() { - // If the first denom has no coins, all the value is in the second denom - Ok(vec![(denom1.unwrap(), Decimal::one())]) - } else if denom1.is_none() { - // If the second denom has no coins, all the value is in the first denom - Ok(vec![(denom0.unwrap(), Decimal::one())]) - } else { - Ok(vec![ - (denom0.unwrap(), Decimal::from_ratio(value0, total_value)), - (denom1.unwrap(), Decimal::from_ratio(value1, total_value)), - ]) - } -} - +/// This may return 0, 1 or 2 elements depending on the position's status pub fn user_deposit( deps: Deps, _app: &App, From fa554c76ab3c444e3cff1e7c22079ebde938eb04 Mon Sep 17 00:00:00 2001 From: cyberhoward Date: Thu, 28 Mar 2024 12:40:37 +0200 Subject: [PATCH 12/42] optimize strategy query --- contracts/carrot-app/src/handlers/execute.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index 6c2f98af..09a8b166 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -173,12 +173,13 @@ pub fn _inner_deposit( funds: Vec, app: &App, ) -> AppResult> { + let strategy = query_strategy(deps)?.strategy; + // We determine the value of all tokens that will be used inside this function let exchange_rates = query_all_exchange_rates( deps, - query_strategy(deps)? - .strategy - .0 + strategy + .0.clone() .into_iter() .flat_map(|s| { s.yield_source @@ -190,16 +191,14 @@ pub fn _inner_deposit( app, )?; - let deposit_strategies = query_strategy(deps)? - .strategy + let deposit_strategies = strategy .fill_all(funds, &exchange_rates)?; // We select the target shares depending on the strategy selected let deposit_msgs = deposit_strategies .iter() .zip( - query_strategy(deps)? - .strategy + strategy .0 .iter() .map(|s| s.yield_source.ty.clone()), From fb38a0981f3eb03fe08d4ee63a7eb84eb4bc288d Mon Sep 17 00:00:00 2001 From: Kayanski Date: Thu, 28 Mar 2024 10:45:37 +0000 Subject: [PATCH 13/42] Added internal messages, and better strategy checks --- contracts/carrot-app/src/contract.rs | 24 +-- contracts/carrot-app/src/error.rs | 3 + contracts/carrot-app/src/handlers/execute.rs | 96 ++++++--- .../carrot-app/src/handlers/instantiate.rs | 2 +- contracts/carrot-app/src/handlers/internal.rs | 45 ++--- contracts/carrot-app/src/handlers/query.rs | 117 +++++++---- contracts/carrot-app/src/msg.rs | 19 +- contracts/carrot-app/src/yield_sources.rs | 53 +++-- .../carrot-app/src/yield_sources/mars.rs | 23 +-- .../src/yield_sources/yield_type.rs | 35 +++- contracts/carrot-app/tests/common.rs | 24 ++- .../carrot-app/tests/deposit_withdraw.rs | 80 ++++++++ contracts/carrot-app/tests/strategy.rs | 184 ------------------ 13 files changed, 387 insertions(+), 318 deletions(-) delete mode 100644 contracts/carrot-app/tests/strategy.rs diff --git a/contracts/carrot-app/src/contract.rs b/contracts/carrot-app/src/contract.rs index 3bde956b..e2f010a2 100644 --- a/contracts/carrot-app/src/contract.rs +++ b/contracts/carrot-app/src/contract.rs @@ -65,10 +65,10 @@ impl abstract_app::abstract_interface::Depen as abstract_app::abstract_interface::DependencyCreation>::dependency_install_configs( cosmwasm_std::Empty {}, )?; - let moneymarket_dependency_install_configs: Vec = - as abstract_app::abstract_interface::DependencyCreation>::dependency_install_configs( - cosmwasm_std::Empty {}, - )?; + // let moneymarket_dependency_install_configs: Vec = + // as abstract_app::abstract_interface::DependencyCreation>::dependency_install_configs( + // cosmwasm_std::Empty {}, + // )?; let adapter_install_config = vec![ abstract_app::abstract_core::manager::ModuleInstallConfig::new( @@ -78,18 +78,18 @@ impl abstract_app::abstract_interface::Depen )?, None, ), - abstract_app::abstract_core::manager::ModuleInstallConfig::new( - abstract_app::abstract_core::objects::module::ModuleInfo::from_id( - abstract_money_market_adapter::MONEY_MARKET_ADAPTER_ID, - abstract_money_market_adapter::contract::CONTRACT_VERSION.into(), - )?, - None, - ), + // abstract_app::abstract_core::manager::ModuleInstallConfig::new( + // abstract_app::abstract_core::objects::module::ModuleInfo::from_id( + // abstract_money_market_adapter::MONEY_MARKET_ADAPTER_ID, + // abstract_money_market_adapter::contract::CONTRACT_VERSION.into(), + // )?, + // None, + // ), ]; Ok([ dex_dependency_install_configs, - moneymarket_dependency_install_configs, + // moneymarket_dependency_install_configs, adapter_install_config, ] .concat()) diff --git a/contracts/carrot-app/src/error.rs b/contracts/carrot-app/src/error.rs index 33ce1882..e91821ab 100644 --- a/contracts/carrot-app/src/error.rs +++ b/contracts/carrot-app/src/error.rs @@ -51,6 +51,9 @@ pub enum AppError { #[error("No position registered in contract, please create a position !")] NoPosition {}, + #[error("Deposit pool was not found")] + PoolNotFound {}, + #[error("No swap fund to swap assets into each other")] NoSwapPossibility {}, diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index dedb3146..d2006147 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -2,7 +2,7 @@ use crate::{ contract::{App, AppResult}, error::AppError, handlers::query::query_balance, - msg::{AppExecuteMsg, ExecuteMsg}, + msg::{AppExecuteMsg, ExecuteMsg, InternalExecuteMsg}, state::{assert_contract, get_autocompound_status, Config, CONFIG}, yield_sources::{BalanceStrategy, ExpectedToken}, }; @@ -15,7 +15,7 @@ use cosmwasm_std::{ use super::{ internal::{deposit_one_strategy, execute_finalize_deposit, execute_one_deposit_step}, - query::{query_all_exchange_rates, query_exchange_rate, query_strategy}, + query::{query_all_exchange_rates, query_exchange_rate, query_strategy, query_strategy_target}, swap_helpers::swap_msg, }; @@ -34,22 +34,44 @@ pub fn execute_handler( AppExecuteMsg::Withdraw { amount } => withdraw(deps, env, info, amount, app), AppExecuteMsg::Autocompound {} => autocompound(deps, env, info, app), AppExecuteMsg::Rebalance { strategy } => rebalance(deps, env, info, strategy, app), - // Endpoints called by the contract directly - AppExecuteMsg::DepositOneStrategy { - swap_strategy, - yield_type, - yield_index, - } => deposit_one_strategy(deps, env, info, swap_strategy, yield_index, yield_type, app), - AppExecuteMsg::ExecuteOneDepositSwapStep { - asset_in, - denom_out, - expected_amount, - } => execute_one_deposit_step(deps, env, info, asset_in, denom_out, expected_amount, app), - AppExecuteMsg::FinalizeDeposit { - yield_type, - yield_index, - } => execute_finalize_deposit(deps, env, info, yield_type, yield_index, app), + AppExecuteMsg::Internal(internal_msg) => { + if info.sender != env.contract.address { + return Err(AppError::Unauthorized {}); + } + match internal_msg { + InternalExecuteMsg::DepositOneStrategy { + swap_strategy, + yield_type, + yield_index, + } => deposit_one_strategy( + deps, + env, + info, + swap_strategy, + yield_index, + yield_type, + app, + ), + InternalExecuteMsg::ExecuteOneDepositSwapStep { + asset_in, + denom_out, + expected_amount, + } => execute_one_deposit_step( + deps, + env, + info, + asset_in, + denom_out, + expected_amount, + app, + ), + InternalExecuteMsg::FinalizeDeposit { + yield_type, + yield_index, + } => execute_finalize_deposit(deps, env, info, yield_type, yield_index, app), + } + } } } @@ -99,7 +121,8 @@ fn rebalance( // We load it raw because we're changing the strategy let mut config = CONFIG.load(deps.storage)?; let old_strategy = config.balance_strategy; - strategy.check()?; + + strategy.check(deps.as_ref(), &app)?; // We execute operations to rebalance the funds between the strategies // TODO @@ -177,6 +200,33 @@ fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResu Ok(response) } +/// The deposit process goes through the following steps +/// 1. We query the target strategy in storage +/// 2. We correct the expected token shares of each strategy, in case there are corrections passed to the function +/// 3. We deposit funds according to that strategy +/// +/// This approach is not perfect. TO show the flaws, take an example where you allocate 50% into mars, 50% into osmosis and both give similar rewards. +/// Assume we deposited 2x inside the app. +/// When an auto-compounding happens, they both get y as rewards, mars is already auto-compounding and osmosis' rewards are redeposited inside the pool +/// Step | Mars | Osmosis | Rewards| +/// Deposit | x | x | 0 | +/// Withdraw Rewards | x + y | x| y | +/// Re-deposit | x + y + y/2 | x + y/2 | 0 | +/// The final ratio is not the 50/50 ratio we target +/// +/// PROPOSITION : We could also have this kind of deposit flow +/// 1a. We query the target strategy in storage (target strategy) +/// 1b. We query the current status of the strategy (current strategy) +/// 1c. We create a temporary strategy object to allocate the funds from this deposit into the various strategies +/// --> the goal of those 3 steps is to correct the funds allocation faster towards the target strategy +/// 2. We correct the expected token shares of each strategy, in case there are corrections passed to the function +/// 3. We deposit funds according to that strategy +/// This time : +/// Step | Mars | Osmosis | Rewards| +/// Deposit | x | x | 0 | +/// Withdraw Rewards | x + y | x| y | +/// Re-deposit | x + y | x + y | 0 | +/// pub fn _inner_deposit( deps: Deps, env: &Env, @@ -201,8 +251,9 @@ pub fn _inner_deposit( app, )?; + // We query the target strategy depending on the existing deposits + let mut current_strategy_status = query_strategy_target(deps, app)?.strategy; // We correct the strategy if specified in parameters - let mut current_strategy_status = query_strategy(deps)?.strategy; current_strategy_status .0 .iter_mut() @@ -213,16 +264,13 @@ pub fn _inner_deposit( } }); - let deposit_strategies = query_strategy(deps)? - .strategy - .fill_all(funds, &exchange_rates)?; + let deposit_strategies = current_strategy_status.fill_all(funds, &exchange_rates)?; // We select the target shares depending on the strategy selected let deposit_msgs = deposit_strategies .iter() .zip( - query_strategy(deps)? - .strategy + current_strategy_status .0 .iter() .map(|s| s.yield_source.ty.clone()), diff --git a/contracts/carrot-app/src/handlers/instantiate.rs b/contracts/carrot-app/src/handlers/instantiate.rs index d4cb59cd..960b93b2 100644 --- a/contracts/carrot-app/src/handlers/instantiate.rs +++ b/contracts/carrot-app/src/handlers/instantiate.rs @@ -16,7 +16,7 @@ pub fn instantiate_handler( msg: AppInstantiateMsg, ) -> AppResult { // We check the balance strategy is valid - msg.balance_strategy.check()?; + msg.balance_strategy.check(deps.as_ref(), &app)?; // We don't check the dex on instantiation diff --git a/contracts/carrot-app/src/handlers/internal.rs b/contracts/carrot-app/src/handlers/internal.rs index 95307e45..8048fb53 100644 --- a/contracts/carrot-app/src/handlers/internal.rs +++ b/contracts/carrot-app/src/handlers/internal.rs @@ -1,8 +1,7 @@ use crate::{ contract::{App, AppResult}, - error::AppError, helpers::{add_funds, get_proxy_balance}, - msg::{AppExecuteMsg, ExecuteMsg}, + msg::{AppExecuteMsg, ExecuteMsg, InternalExecuteMsg}, replies::REPLY_AFTER_SWAPS_STEP, state::{ CONFIG, TEMP_CURRENT_COIN, TEMP_CURRENT_YIELD, TEMP_DEPOSIT_COINS, TEMP_EXPECTED_SWAP_COIN, @@ -20,15 +19,14 @@ use super::query::query_exchange_rate; pub fn deposit_one_strategy( deps: DepsMut, env: Env, - info: MessageInfo, + _info: MessageInfo, strategy: OneDepositStrategy, yield_index: usize, yield_type: YieldType, app: App, ) -> AppResult { - if info.sender != env.contract.address { - return Err(AppError::Unauthorized {}); - } + deps.api + .debug(&format!("We're depositing {:?}-{:?}", strategy, yield_type)); TEMP_DEPOSIT_COINS.save(deps.storage, &vec![])?; @@ -46,11 +44,13 @@ pub fn deposit_one_strategy( expected_amount, } => wasm_execute( env.contract.address.clone(), - &ExecuteMsg::Module(AppExecuteMsg::ExecuteOneDepositSwapStep { - asset_in, - denom_out, - expected_amount, - }), + &ExecuteMsg::Module(AppExecuteMsg::Internal( + InternalExecuteMsg::ExecuteOneDepositSwapStep { + asset_in, + denom_out, + expected_amount, + }, + )), vec![], ) .map(|msg| Some(SubMsg::reply_on_success(msg, REPLY_AFTER_SWAPS_STEP))), @@ -69,10 +69,12 @@ pub fn deposit_one_strategy( // Finalize and execute the deposit let last_step = wasm_execute( env.contract.address.clone(), - &ExecuteMsg::Module(AppExecuteMsg::FinalizeDeposit { - yield_type, - yield_index, - }), + &ExecuteMsg::Module(AppExecuteMsg::Internal( + InternalExecuteMsg::FinalizeDeposit { + yield_type, + yield_index, + }, + )), vec![], )?; @@ -84,17 +86,13 @@ pub fn deposit_one_strategy( pub fn execute_one_deposit_step( deps: DepsMut, - env: Env, - info: MessageInfo, + _env: Env, + _info: MessageInfo, asset_in: Coin, denom_out: String, expected_amount: Uint128, app: App, ) -> AppResult { - if info.sender != env.contract.address { - return Err(AppError::Unauthorized {}); - } - let config = CONFIG.load(deps.storage)?; let exchange_rate_in = query_exchange_rate(deps.as_ref(), asset_in.denom.clone(), &app)?; @@ -126,14 +124,11 @@ pub fn execute_one_deposit_step( pub fn execute_finalize_deposit( deps: DepsMut, env: Env, - info: MessageInfo, + _info: MessageInfo, yield_type: YieldType, yield_index: usize, app: App, ) -> AppResult { - if info.sender != env.contract.address { - return Err(AppError::Unauthorized {}); - } let available_deposit_coins = TEMP_DEPOSIT_COINS.load(deps.storage)?; TEMP_CURRENT_YIELD.save(deps.storage, &yield_index)?; diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index 316e1a44..b1abbb3a 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -92,51 +92,48 @@ pub fn query_strategy(deps: Deps) -> AppResult { }) } +// Returns the target strategy for strategies +// This includes querying the dynamic strategy if specified in the strategy options +// This allows querying what actually needs to be deposited inside the strategy +pub fn query_strategy_target(deps: Deps, app: &App) -> AppResult { + let strategy = query_strategy(deps)?.strategy; + + Ok(StrategyResponse { + strategy: BalanceStrategy( + strategy + .0 + .into_iter() + .map(|mut yield_source| { + let shares = match yield_source.yield_source.ty.share_type() { + crate::yield_sources::ShareType::Dynamic => { + let (_total_value, shares) = + query_dynamic_source_value(deps, &yield_source, app)?; + shares + } + crate::yield_sources::ShareType::Fixed => { + yield_source.yield_source.expected_tokens + } + }; + + yield_source.yield_source.expected_tokens = shares; + + Ok::<_, AppError>(yield_source) + }) + .collect::, _>>()?, + ), + }) +} + +/// Returns the current status of the full strategy. It returns shares reflecting the underlying positions pub fn query_strategy_status(deps: Deps, app: &App) -> AppResult { let strategy = query_strategy(deps)?.strategy; - let exchange_rates = query_all_exchange_rates( - deps, - strategy.0.iter().flat_map(|s| { - s.yield_source - .expected_tokens - .iter() - .map(|ExpectedToken { denom, share: _ }| denom.clone()) - }), - app, - )?; - // We get the value for each investment + // We get the value for each investment and the shares within that investment let all_strategy_values = query_strategy(deps)? .strategy .0 .iter() - .map(|s| { - let user_deposit = s.yield_source.ty.user_deposit(deps, app)?; - - // From this, we compute the shares within the investment - let each_value = user_deposit - .iter() - .map(|fund| { - let exchange_rate = exchange_rates - .get(&fund.denom) - .ok_or(AppError::NoExchangeRate(fund.denom.clone()))?; - - Ok::<_, AppError>((fund.denom.clone(), *exchange_rate * fund.amount)) - }) - .collect::, _>>()?; - - let total_value: Uint128 = each_value.iter().map(|(_denom, amount)| amount).sum(); - - let each_shares = each_value - .into_iter() - .map(|(denom, amount)| ExpectedToken { - denom, - share: Decimal::from_ratio(amount, total_value), - }) - .collect::>(); - - Ok::<_, AppError>((total_value, each_shares)) - }) + .map(|s| query_dynamic_source_value(deps, s, app)) .collect::, _>>()?; let all_strategies_value: Uint128 = all_strategy_values.iter().map(|(value, _)| value).sum(); @@ -162,6 +159,44 @@ pub fn query_strategy_status(deps: Deps, app: &App) -> AppResult AppResult<(Uint128, Vec)> { + // If there is no deposit + let user_deposit = match yield_source.yield_source.ty.user_deposit(deps, app) { + Ok(deposit) => deposit, + Err(_) => { + return Ok(( + Uint128::zero(), + yield_source.yield_source.expected_tokens.clone(), + )) + } + }; + + // From this, we compute the shares within the investment + let each_value = user_deposit + .iter() + .map(|fund| { + let exchange_rate = query_exchange_rate(deps, fund.denom.clone(), app)?; + + Ok::<_, AppError>((fund.denom.clone(), exchange_rate * fund.amount)) + }) + .collect::, _>>()?; + + let total_value: Uint128 = each_value.iter().map(|(_denom, amount)| amount).sum(); + + let each_shares = each_value + .into_iter() + .map(|(denom, amount)| ExpectedToken { + denom, + share: Decimal::from_ratio(amount, total_value), + }) + .collect::>(); + Ok((total_value, each_shares)) +} + fn query_config(deps: Deps) -> AppResult { Ok(CONFIG.load(deps.storage)?) } @@ -170,7 +205,11 @@ pub fn query_balance(deps: Deps, app: &App) -> AppResult let mut funds = Coins::default(); let mut total_value = Uint128::zero(); query_strategy(deps)?.strategy.0.iter().try_for_each(|s| { - let deposit_value = s.yield_source.ty.user_deposit(deps, app)?; + let deposit_value = s + .yield_source + .ty + .user_deposit(deps, app) + .unwrap_or_default(); for fund in deposit_value { let exchange_rate = query_exchange_rate(deps, fund.denom.clone(), app)?; funds.add(fund.clone())?; diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index 483e5425..4d8ba348 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -51,7 +51,14 @@ pub enum AppExecuteMsg { /// Rebalances all investments according to a new balance strategy Rebalance { strategy: BalanceStrategy }, - /// Only called by the contract internally + /// Only called by the contract internally + Internal(InternalExecuteMsg), +} + +#[cw_serde] +#[cfg_attr(feature = "interface", derive(cw_orch::ExecuteFns))] +#[cfg_attr(feature = "interface", impl_into(ExecuteMsg))] +pub enum InternalExecuteMsg { DepositOneStrategy { swap_strategy: OneDepositStrategy, yield_type: YieldType, @@ -69,6 +76,16 @@ pub enum AppExecuteMsg { yield_index: usize, }, } +impl From + for abstract_app::abstract_core::base::ExecuteMsg< + abstract_app::abstract_core::app::BaseExecuteMsg, + AppExecuteMsg, + > +{ + fn from(value: InternalExecuteMsg) -> Self { + Self::Module(AppExecuteMsg::Internal(value)) + } +} /// App query messages #[cosmwasm_schema::cw_serde] diff --git a/contracts/carrot-app/src/yield_sources.rs b/contracts/carrot-app/src/yield_sources.rs index e7098ffe..0b2b4f35 100644 --- a/contracts/carrot-app/src/yield_sources.rs +++ b/contracts/carrot-app/src/yield_sources.rs @@ -4,17 +4,19 @@ pub mod yield_type; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - coin, ensure, ensure_eq, wasm_execute, Coin, Coins, CosmosMsg, Decimal, Env, Uint128, + coin, ensure, ensure_eq, wasm_execute, Coin, Coins, CosmosMsg, Decimal, Deps, Env, Uint128, }; +use cw_asset::AssetInfo; use std::collections::HashMap; use crate::{ - contract::AppResult, + contract::{App, AppResult}, error::AppError, helpers::close_to, - msg::{AppExecuteMsg, ExecuteMsg}, + msg::{AppExecuteMsg, ExecuteMsg, InternalExecuteMsg}, state::compute_total_value, }; +use abstract_app::traits::AbstractNameService; use self::yield_type::YieldType; @@ -29,7 +31,7 @@ pub struct YieldSource { } impl YieldSource { - pub fn check(&self) -> AppResult<()> { + pub fn check(&self, deps: Deps, app: &App) -> AppResult<()> { // First we check the share sums the 100 let share_sum: Decimal = self.expected_tokens.iter().map(|e| e.share).sum(); ensure!( @@ -41,12 +43,23 @@ impl YieldSource { AppError::InvalidEmptyStrategy {} ); - // Then we check every yield strategy underneath + // We ensure all deposited tokens exist in ANS + let ans = app.name_service(deps); + ans.host().query_assets_reverse( + &deps.querier, + &self + .expected_tokens + .iter() + .map(|e| AssetInfo::native(e.denom.clone())) + .collect::>(), + )?; + // Then we check every yield strategy underneath match &self.ty { - YieldType::ConcentratedLiquidityPool(_) => { + YieldType::ConcentratedLiquidityPool(params) => { // A valid CL pool strategy is for 2 assets ensure_eq!(self.expected_tokens.len(), 2, AppError::InvalidStrategy {}); + params.check(deps)?; } YieldType::Mars(denom) => { // We verify there is only one element in the shares vector @@ -70,6 +83,14 @@ pub struct ExpectedToken { pub share: Decimal, } +#[cw_serde] +pub enum ShareType { + /// This allows using the current distribution of tokens inside the position to compute the distribution on deposit + Dynamic, + /// This forces the position to use the target distribution of tokens when depositing + Fixed, +} + // Related to balance strategies #[cw_serde] pub struct BalanceStrategy(pub Vec); @@ -80,13 +101,13 @@ pub struct BalanceStrategyElement { pub share: Decimal, } impl BalanceStrategyElement { - pub fn check(&self) -> AppResult<()> { - self.yield_source.check() + pub fn check(&self, deps: Deps, app: &App) -> AppResult<()> { + self.yield_source.check(deps, app) } } impl BalanceStrategy { - pub fn check(&self) -> AppResult<()> { + pub fn check(&self, deps: Deps, app: &App) -> AppResult<()> { // First we check the share sums the 100 let share_sum: Decimal = self.0.iter().map(|e| e.share).sum(); ensure!( @@ -97,7 +118,7 @@ impl BalanceStrategy { // Then we check every yield strategy underneath for yield_source in &self.0 { - yield_source.check()?; + yield_source.check(deps, app)?; } Ok(()) @@ -277,11 +298,13 @@ impl OneDepositStrategy { // For each strategy, we send a message on the contract to execute it Ok(wasm_execute( env.contract.address.clone(), - &ExecuteMsg::Module(AppExecuteMsg::DepositOneStrategy { - swap_strategy: self.clone(), - yield_type, - yield_index, - }), + &ExecuteMsg::Module(AppExecuteMsg::Internal( + InternalExecuteMsg::DepositOneStrategy { + swap_strategy: self.clone(), + yield_type, + yield_index, + }, + )), vec![], )? .into()) diff --git a/contracts/carrot-app/src/yield_sources/mars.rs b/contracts/carrot-app/src/yield_sources/mars.rs index 403110bf..1a0ab82f 100644 --- a/contracts/carrot-app/src/yield_sources/mars.rs +++ b/contracts/carrot-app/src/yield_sources/mars.rs @@ -1,13 +1,9 @@ use crate::contract::{App, AppResult}; -use crate::error::AppError; use abstract_app::traits::AccountIdentification; -use abstract_app::{ - objects::{AnsAsset, AssetEntry}, - traits::AbstractNameService, -}; +use abstract_app::{objects::AnsAsset, traits::AbstractNameService}; use abstract_money_market_adapter::msg::MoneyMarketQueryMsg; use abstract_money_market_adapter::MoneyMarketInterface; -use cosmwasm_std::{ensure_eq, Coin, CosmosMsg, Decimal, Deps, SubMsg, Uint128}; +use cosmwasm_std::{Coin, CosmosMsg, Deps, SubMsg, Uint128}; use cw_asset::AssetInfo; use abstract_money_market_standard::query::MoneyMarketAnsQuery; @@ -35,21 +31,20 @@ pub fn withdraw( let amount = if let Some(amount) = amount { amount } else { - user_deposit(deps, denom.clone(), &app)? + user_deposit(deps, denom.clone(), app)? }; let ans_fund = ans.query(&AssetInfo::native(denom))?; Ok(vec![app .ans_money_market(deps, MARS_MONEY_MARKET.to_string()) - .withdraw(AnsAsset::new(ans_fund, amount))? - .into()]) + .withdraw(AnsAsset::new(ans_fund, amount))?]) } pub fn withdraw_rewards( - deps: Deps, - denom: String, - app: &App, + _deps: Deps, + _denom: String, + _app: &App, ) -> AppResult<(Vec, Vec)> { // Mars doesn't have rewards, it's automatically auto-compounded Ok((vec![], vec![])) @@ -76,8 +71,8 @@ pub fn user_liquidity(deps: Deps, denom: String, app: &App) -> AppResult AppResult> { - // No rewards, because mars is self-auto-compounding +pub fn user_rewards(_deps: Deps, _denom: String, _app: &App) -> AppResult> { + // No rewards, because mars is already auto-compounding Ok(vec![]) } diff --git a/contracts/carrot-app/src/yield_sources/yield_type.rs b/contracts/carrot-app/src/yield_sources/yield_type.rs index e5fd54b9..c8bfadf4 100644 --- a/contracts/carrot-app/src/yield_sources/yield_type.rs +++ b/contracts/carrot-app/src/yield_sources/yield_type.rs @@ -1,9 +1,15 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{coins, Coin, CosmosMsg, Deps, Env, SubMsg, Uint128}; +use osmosis_std::types::osmosis::{ + concentratedliquidity::v1beta1::Pool, poolmanager::v1beta1::PoolmanagerQuerier, +}; -use crate::contract::{App, AppResult}; +use crate::{ + contract::{App, AppResult}, + error::AppError, +}; -use super::{mars, osmosis_cl_pool}; +use super::{mars, osmosis_cl_pool, ShareType}; #[cw_serde] pub enum YieldType { @@ -22,6 +28,9 @@ impl YieldType { funds: Vec, app: &App, ) -> AppResult> { + if funds.is_empty() { + return Ok(vec![]); + } match self { YieldType::ConcentratedLiquidityPool(params) => { osmosis_cl_pool::deposit(deps, env, params, funds, app) @@ -82,6 +91,17 @@ impl YieldType { YieldType::Mars(denom) => mars::user_liquidity(deps, denom.clone(), app), } } + + /// Indicate the default funds allocation + /// This is specifically useful for auto-compound as we're not able to input target amounts + /// CL pools use that to know the best funds deposit ratio + /// Mars doesn't use that, because the share is fixed to 1 + pub fn share_type(&self) -> ShareType { + match self { + YieldType::ConcentratedLiquidityPool(_) => ShareType::Dynamic, + YieldType::Mars(_) => ShareType::Fixed, + } + } } #[cw_serde] @@ -91,3 +111,14 @@ pub struct ConcentratedPoolParams { pub upper_tick: i64, pub position_id: Option, } + +impl ConcentratedPoolParams { + pub fn check(&self, deps: Deps) -> AppResult<()> { + let _pool: Pool = PoolmanagerQuerier::new(&deps.querier) + .pool(self.pool_id)? + .pool + .ok_or(AppError::PoolNotFound {})? + .try_into()?; + Ok(()) + } +} diff --git a/contracts/carrot-app/tests/common.rs b/contracts/carrot-app/tests/common.rs index f1c105d9..dbf91226 100644 --- a/contracts/carrot-app/tests/common.rs +++ b/contracts/carrot-app/tests/common.rs @@ -12,7 +12,7 @@ use carrot_app::msg::AppInstantiateMsg; use carrot_app::state::{AutocompoundConfig, AutocompoundRewardsConfig}; use carrot_app::yield_sources::yield_type::{ConcentratedPoolParams, YieldType}; use carrot_app::yield_sources::{ - BalanceStrategy, BalanceStrategyElement, ExpectedToken, YieldSource, + BalanceStrategy, BalanceStrategyElement, ExpectedToken, ShareType, YieldSource, }; use cosmwasm_std::{coin, coins, to_json_binary, to_json_vec, Decimal, Uint128, Uint64}; use cw_asset::AssetInfoUnchecked; @@ -115,6 +115,16 @@ pub fn deploy( recipient_account: 0, }, )?; + // // The moneymarket adapter + // let money_market_adapter = publisher + // .publish_adapter::<_, abstract_money_market_adapter::interface::MoneyMarketAdapter< + // Chain, + // >>( + // abstract_money_market_adapter::msg::MoneyMarketInstantiateMsg { + // fee: Decimal::percent(2), + // recipient_account: 0, + // }, + // )?; // The savings app publisher.publish_app::>()?; @@ -177,6 +187,18 @@ pub fn deploy( ), None, )?; + // money_market_adapter.execute( + // &abstract_money_market_adapter::msg::ExecuteMsg::Base( + // abstract_app::abstract_core::adapter::BaseExecuteMsg { + // proxy_address: Some(carrot_app.account().proxy()?.to_string()), + // msg: abstract_app::abstract_core::adapter::AdapterBaseMsg::UpdateAuthorizedAddresses { + // to_add: vec![carrot_app.addr_str()?], + // to_remove: vec![], + // }, + // }, + // ), + // None, + // )?; Ok(carrot_app) } diff --git a/contracts/carrot-app/tests/deposit_withdraw.rs b/contracts/carrot-app/tests/deposit_withdraw.rs index fe509a96..7819958e 100644 --- a/contracts/carrot-app/tests/deposit_withdraw.rs +++ b/contracts/carrot-app/tests/deposit_withdraw.rs @@ -221,6 +221,86 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { Ok(()) } +#[test] +fn deposit_multiple_positions_with_empty() -> anyhow::Result<()> { + let (pool_id, carrot_app) = setup_test_tube(false)?; + + let new_strat = BalanceStrategy(vec![ + BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: vec![ + ExpectedToken { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + ExpectedToken { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, + ], + ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + pool_id, + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + }), + }, + share: Decimal::percent(50), + }, + BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: vec![ + ExpectedToken { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + ExpectedToken { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, + ], + ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + pool_id, + lower_tick: 2 * INITIAL_LOWER_TICK, + upper_tick: 2 * INITIAL_UPPER_TICK, + position_id: None, + }), + }, + share: Decimal::percent(50), + }, + BalanceStrategyElement { + yield_source: YieldSource { + expected_tokens: vec![ExpectedToken { + denom: USDT.to_string(), + share: Decimal::percent(100), + }], + ty: YieldType::Mars(USDT.to_string()), + }, + share: Decimal::percent(0), + }, + ]); + carrot_app.rebalance(new_strat.clone())?; + + let deposit_amount = 5_000; + let deposit_coins = coins(deposit_amount, USDT.to_owned()); + let mut chain = carrot_app.get_chain().clone(); + + let balances_before = query_balances(&carrot_app)?; + chain.add_balance( + carrot_app.account().proxy()?.to_string(), + deposit_coins.clone(), + )?; + carrot_app.deposit(deposit_coins, None)?; + let balances_after = query_balances(&carrot_app)?; + + println!("{balances_before} --> {balances_after}"); + let slippage = Decimal::percent(4); + assert!( + balances_after + > balances_before + (Uint128::from(deposit_amount) * (Decimal::one() - slippage)) + ); + Ok(()) +} // #[test] // fn create_position_on_instantiation() -> anyhow::Result<()> { // let (_, carrot_app) = setup_test_tube(true)?; diff --git a/contracts/carrot-app/tests/strategy.rs b/contracts/carrot-app/tests/strategy.rs deleted file mode 100644 index 120edc1f..00000000 --- a/contracts/carrot-app/tests/strategy.rs +++ /dev/null @@ -1,184 +0,0 @@ -use std::collections::HashMap; - -use carrot_app::yield_sources::yield_type::{ConcentratedPoolParams, YieldType}; -use cosmwasm_std::{coin, Decimal}; - -use carrot_app::state::compute_total_value; -use carrot_app::yield_sources::{ - BalanceStrategy, BalanceStrategyElement, ExpectedToken, YieldSource, -}; - -pub const LUNA: &str = "uluna"; -pub const OSMOSIS: &str = "uosmo"; -pub const STARGAZE: &str = "ustars"; -pub const NEUTRON: &str = "untrn"; -pub const USD: &str = "usd"; -pub const USDC: &str = "usdc"; - -pub fn mock_strategy() -> BalanceStrategy { - BalanceStrategy(vec![ - BalanceStrategyElement { - yield_source: YieldSource { - expected_tokens: vec![ - ExpectedToken { - denom: LUNA.to_string(), - share: Decimal::percent(90), - }, - ExpectedToken { - denom: OSMOSIS.to_string(), - share: Decimal::percent(10), - }, - ], - ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { - pool_id: 8, - lower_tick: 6, - upper_tick: -6, - position_id: None, - }), - }, - share: Decimal::percent(33), - }, - BalanceStrategyElement { - yield_source: YieldSource { - expected_tokens: vec![ExpectedToken { - denom: "usdc".to_string(), - share: Decimal::percent(100), - }], - ty: YieldType::Mars("usdc".to_string()), - }, - share: Decimal::percent(67), - }, - ]) -} - -#[test] -fn bad_strategy_check_empty() -> cw_orch::anyhow::Result<()> { - let strategy = BalanceStrategy(vec![ - BalanceStrategyElement { - yield_source: YieldSource { - expected_tokens: vec![], - ty: YieldType::Mars("usdc".to_string()), - }, - share: Decimal::percent(33), - }, - BalanceStrategyElement { - yield_source: YieldSource { - expected_tokens: vec![], - ty: YieldType::Mars("usdc".to_string()), - }, - share: Decimal::percent(67), - }, - ]); - - strategy.check().unwrap_err(); - - Ok(()) -} - -#[test] -fn bad_strategy_check_sum() -> cw_orch::anyhow::Result<()> { - let strategy = BalanceStrategy(vec![ - BalanceStrategyElement { - yield_source: YieldSource { - expected_tokens: vec![ExpectedToken { - denom: NEUTRON.to_string(), - share: Decimal::percent(100), - }], - ty: YieldType::Mars("usdc".to_string()), - }, - share: Decimal::percent(33), - }, - BalanceStrategyElement { - yield_source: YieldSource { - expected_tokens: vec![ExpectedToken { - denom: NEUTRON.to_string(), - share: Decimal::percent(100), - }], - ty: YieldType::Mars("usdc".to_string()), - }, - share: Decimal::percent(66), - }, - ]); - - strategy.check().unwrap_err(); - - Ok(()) -} - -#[test] -fn bad_strategy_check_sum_inner() -> cw_orch::anyhow::Result<()> { - let strategy = BalanceStrategy(vec![ - BalanceStrategyElement { - yield_source: YieldSource { - expected_tokens: vec![ - ExpectedToken { - denom: NEUTRON.to_string(), - share: Decimal::percent(33), - }, - ExpectedToken { - denom: NEUTRON.to_string(), - share: Decimal::percent(33), - }, - ], - ty: YieldType::Mars("usdc".to_string()), - }, - share: Decimal::percent(33), - }, - BalanceStrategyElement { - yield_source: YieldSource { - expected_tokens: vec![ExpectedToken { - denom: NEUTRON.to_string(), - share: Decimal::percent(100), - }], - ty: YieldType::Mars("usdc".to_string()), - }, - share: Decimal::percent(67), - }, - ]); - - strategy.check().unwrap_err(); - - Ok(()) -} - -#[test] -fn check_strategy() -> cw_orch::anyhow::Result<()> { - let strategy = mock_strategy(); - - strategy.check()?; - - Ok(()) -} - -#[test] -fn value_fill_strategy() -> cw_orch::anyhow::Result<()> { - let strategy = mock_strategy(); - - let exchange_rates: HashMap = [ - (LUNA.to_string(), Decimal::percent(150)), - (USD.to_string(), Decimal::percent(100)), - (NEUTRON.to_string(), Decimal::percent(75)), - (OSMOSIS.to_string(), Decimal::percent(10)), - (STARGAZE.to_string(), Decimal::percent(35)), - (USDC.to_string(), Decimal::percent(101)), - ] - .into_iter() - .collect(); - - let funds = vec![ - coin(1_000_000_000, LUNA), - coin(2_000_000_000, USD), - coin(25_000_000, NEUTRON), - ]; - println!( - "total value : {:?}", - compute_total_value(&funds, &exchange_rates) - ); - - let fill_result = strategy.fill_all(funds, &exchange_rates)?; - - assert_eq!(fill_result.len(), 2); - assert_eq!(fill_result[0].0.len(), 2); - assert_eq!(fill_result[1].0.len(), 1); - Ok(()) -} From 97e65bc0911450ccd309e71b21de105a4d4fbddf Mon Sep 17 00:00:00 2001 From: Kayanski Date: Thu, 28 Mar 2024 10:52:57 +0000 Subject: [PATCH 14/42] Check --- contracts/carrot-app/src/handlers/execute.rs | 4 +-- contracts/carrot-app/src/handlers/query.rs | 8 ++--- contracts/carrot-app/tests/common.rs | 34 +++++-------------- .../carrot-app/tests/deposit_withdraw.rs | 11 +----- contracts/carrot-app/tests/query.rs | 10 +++--- 5 files changed, 20 insertions(+), 47 deletions(-) diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index 6fe004cc..f1688181 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -247,8 +247,8 @@ pub fn _inner_deposit( .flat_map(|s| { s.yield_source .asset_distribution - .into_iter() - .map(|ExpectedToken { denom, share: _ }| denom) + .iter() + .map(|ExpectedToken { denom, share: _ }| denom.clone()) }) .chain(funds.iter().map(|f| f.denom.clone())), app, diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index b1abbb3a..64fbd8cf 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -111,11 +111,11 @@ pub fn query_strategy_target(deps: Deps, app: &App) -> AppResult { - yield_source.yield_source.expected_tokens + yield_source.yield_source.asset_distribution } }; - yield_source.yield_source.expected_tokens = shares; + yield_source.yield_source.asset_distribution = shares; Ok::<_, AppError>(yield_source) }) @@ -148,7 +148,7 @@ pub fn query_strategy_status(deps: Deps, app: &App) -> AppResult { return Ok(( Uint128::zero(), - yield_source.yield_source.expected_tokens.clone(), + yield_source.yield_source.asset_distribution.clone(), )) } }; diff --git a/contracts/carrot-app/tests/common.rs b/contracts/carrot-app/tests/common.rs index 8f588ef5..8c6a5e8c 100644 --- a/contracts/carrot-app/tests/common.rs +++ b/contracts/carrot-app/tests/common.rs @@ -1,50 +1,32 @@ -use std::iter; - use abstract_app::abstract_core::objects::{ - pool_id::PoolAddressBase, AccountId, AssetEntry, PoolMetadata, PoolType, + pool_id::PoolAddressBase, AssetEntry, PoolMetadata, PoolType, }; -use abstract_app::objects::module::ModuleInfo; -use abstract_client::{AbstractClient, Application, Environment, Namespace}; -use abstract_dex_adapter::DEX_ADAPTER_ID; -use abstract_sdk::core::manager::{self, ModuleInstallConfig}; -use carrot_app::contract::{APP_ID, OSMOSIS}; +use abstract_client::{AbstractClient, Application, Namespace}; +use carrot_app::contract::OSMOSIS; use carrot_app::msg::AppInstantiateMsg; use carrot_app::state::{AutocompoundConfig, AutocompoundRewardsConfig}; use carrot_app::yield_sources::yield_type::{ConcentratedPoolParams, YieldType}; use carrot_app::yield_sources::{ - BalanceStrategy, BalanceStrategyElement, ExpectedToken, ShareType, YieldSource, + BalanceStrategy, BalanceStrategyElement, ExpectedToken, YieldSource, }; -use cosmwasm_std::{coin, coins, to_json_binary, to_json_vec, Decimal, Uint128, Uint64}; +use cosmwasm_std::{coin, coins, Decimal, Uint128, Uint64}; use cw_asset::AssetInfoUnchecked; use cw_orch::osmosis_test_tube::osmosis_test_tube::Gamm; use cw_orch::{ anyhow, osmosis_test_tube::osmosis_test_tube::{ osmosis_std::types::{ - cosmos::{ - authz::v1beta1::{GenericAuthorization, Grant, MsgGrant, MsgGrantResponse}, - base::v1beta1, - }, - osmosis::{ - concentratedliquidity::v1beta1::{ - MsgCreatePosition, MsgWithdrawPosition, Pool, PoolsRequest, - }, - gamm::v1beta1::MsgSwapExactAmountIn, - }, + cosmos::base::v1beta1, + osmosis::concentratedliquidity::v1beta1::{MsgCreatePosition, Pool, PoolsRequest}, }, ConcentratedLiquidity, GovWithAppAccess, Module, }, prelude::*, }; -use osmosis_std::types::cosmos::bank::v1beta1::SendAuthorization; -use osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContract; use osmosis_std::types::osmosis::concentratedliquidity::v1beta1::{ - CreateConcentratedLiquidityPoolsProposal, MsgAddToPosition, MsgCollectIncentives, - MsgCollectSpreadRewards, PoolRecord, + CreateConcentratedLiquidityPoolsProposal, PoolRecord, }; use prost::Message; -use prost_types::Any; - pub const LOTS: u128 = 100_000_000_000_000; // Asset 0 diff --git a/contracts/carrot-app/tests/deposit_withdraw.rs b/contracts/carrot-app/tests/deposit_withdraw.rs index 1a41a4b4..0043dc2f 100644 --- a/contracts/carrot-app/tests/deposit_withdraw.rs +++ b/contracts/carrot-app/tests/deposit_withdraw.rs @@ -2,7 +2,6 @@ mod common; use crate::common::{setup_test_tube, USDC, USDT}; use abstract_client::Application; -use abstract_interface::Abstract; use carrot_app::{ msg::{AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse}, yield_sources::{ @@ -13,15 +12,7 @@ use carrot_app::{ }; use common::{INITIAL_LOWER_TICK, INITIAL_UPPER_TICK}; use cosmwasm_std::{coin, coins, Decimal, Uint128}; -use cw_orch::{ - anyhow, - osmosis_test_tube::osmosis_test_tube::{ - osmosis_std::types::osmosis::concentratedliquidity::v1beta1::MsgWithdrawPosition, - ConcentratedLiquidity, Module, - }, - prelude::*, -}; -use osmosis_std::types::osmosis::concentratedliquidity::v1beta1::PositionByIdRequest; +use cw_orch::{anyhow, prelude::*}; fn query_balances( carrot_app: &Application>, diff --git a/contracts/carrot-app/tests/query.rs b/contracts/carrot-app/tests/query.rs index 12a56fca..a6132fb7 100644 --- a/contracts/carrot-app/tests/query.rs +++ b/contracts/carrot-app/tests/query.rs @@ -30,20 +30,20 @@ fn query_strategy_status() -> anyhow::Result<()> { assert_eq!(strategy.0.len(), 1); let single_strategy = strategy.0[0].clone(); assert_eq!(single_strategy.share, Decimal::one()); - assert_eq!(single_strategy.yield_source.expected_tokens.len(), 2); + assert_eq!(single_strategy.yield_source.asset_distribution.len(), 2); // The strategy shares are a little off 50% assert_ne!( - single_strategy.yield_source.expected_tokens[0].share, + single_strategy.yield_source.asset_distribution[0].share, Decimal::percent(50) ); assert_ne!( - single_strategy.yield_source.expected_tokens[1].share, + single_strategy.yield_source.asset_distribution[1].share, Decimal::percent(50) ); assert!(close_to( Decimal::one(), - single_strategy.yield_source.expected_tokens[0].share - + single_strategy.yield_source.expected_tokens[1].share + single_strategy.yield_source.asset_distribution[0].share + + single_strategy.yield_source.asset_distribution[1].share ),); Ok(()) From 8ff3e5e15275b9175ace5b61c2c2e86f2d3e4b1d Mon Sep 17 00:00:00 2001 From: Kayanski Date: Thu, 28 Mar 2024 11:17:10 +0000 Subject: [PATCH 15/42] Deposit refactor --- .../carrot-app/src/distribution/deposit.rs | 236 ++++++++++++++++++ contracts/carrot-app/src/distribution/mod.rs | 4 + contracts/carrot-app/src/handlers/execute.rs | 48 +--- .../carrot-app/src/handlers/instantiate.rs | 4 +- contracts/carrot-app/src/handlers/internal.rs | 3 +- contracts/carrot-app/src/helpers.rs | 28 ++- contracts/carrot-app/src/lib.rs | 1 + contracts/carrot-app/src/msg.rs | 3 +- contracts/carrot-app/src/state.rs | 29 +-- contracts/carrot-app/src/yield_sources.rs | 200 +-------------- 10 files changed, 289 insertions(+), 267 deletions(-) create mode 100644 contracts/carrot-app/src/distribution/deposit.rs create mode 100644 contracts/carrot-app/src/distribution/mod.rs diff --git a/contracts/carrot-app/src/distribution/deposit.rs b/contracts/carrot-app/src/distribution/deposit.rs new file mode 100644 index 00000000..b6d193d4 --- /dev/null +++ b/contracts/carrot-app/src/distribution/deposit.rs @@ -0,0 +1,236 @@ +use std::collections::HashMap; + +use cosmwasm_std::{coin, Coin, Coins, Decimal, Uint128}; + +use crate::{ + contract::AppResult, + helpers::compute_total_value, + yield_sources::{yield_type::YieldType, BalanceStrategy, ExpectedToken}, +}; + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{wasm_execute, CosmosMsg, Env}; + +use crate::{ + error::AppError, + msg::{AppExecuteMsg, ExecuteMsg, InternalExecuteMsg}, +}; + +impl BalanceStrategy { + // We dispatch the available funds directly into the Strategies + // This returns : + // 0 : Funds that are used for specific strategies. And remaining amounts to fill those strategies + // 1 : Funds that are still available to fill those strategies + // This is the algorithm that is implemented here + pub fn fill_sources( + &self, + funds: Vec, + exchange_rates: &HashMap, + ) -> AppResult<(StrategyStatus, Coins)> { + let total_value = compute_total_value(&funds, exchange_rates)?; + let mut remaining_funds = Coins::default(); + + // We create the vector that holds the funds information + let mut yield_source_status = self + .0 + .iter() + .map(|source| { + source + .yield_source + .asset_distribution + .iter() + .map(|ExpectedToken { denom, share }| StrategyStatusElement { + denom: denom.clone(), + raw_funds: Uint128::zero(), + remaining_amount: share * source.share * total_value, + }) + .collect::>() + }) + .collect::>(); + + for this_coin in funds { + let mut remaining_amount = this_coin.amount; + // We distribute those funds in to the accepting strategies + for (strategy, status) in self.0.iter().zip(yield_source_status.iter_mut()) { + // Find the share for the specific denom inside the strategy + let this_denom_status = strategy + .yield_source + .asset_distribution + .iter() + .zip(status.iter_mut()) + .find(|(ExpectedToken { denom, share: _ }, _status)| this_coin.denom.eq(denom)) + .map(|(_, status)| status); + + if let Some(status) = this_denom_status { + // We fill the needed value with the remaining_amount + let funds_to_use_here = remaining_amount.min(status.remaining_amount); + + // Those funds are not available for other yield sources + remaining_amount -= funds_to_use_here; + + status.raw_funds += funds_to_use_here; + status.remaining_amount -= funds_to_use_here; + } + } + remaining_funds.add(coin(remaining_amount.into(), this_coin.denom))?; + } + + Ok((yield_source_status.into(), remaining_funds)) + } + + pub fn fill_all( + &self, + funds: Vec, + exchange_rates: &HashMap, + ) -> AppResult> { + let (status, remaining_funds) = self.fill_sources(funds, exchange_rates)?; + status.fill_with_remaining_funds(remaining_funds, exchange_rates) + } + + /// Gets the deposit messages from a given strategy by filling all strategies with the associated funds + pub fn fill_all_and_get_messages( + &self, + env: &Env, + funds: Vec, + exchange_rates: &HashMap, + ) -> AppResult> { + let deposit_strategies = self.fill_all(funds, exchange_rates)?; + deposit_strategies + .iter() + .zip(self.0.iter().map(|s| s.yield_source.ty.clone())) + .enumerate() + .map(|(index, (strategy, yield_type))| strategy.deposit_msgs(env, index, yield_type)) + .collect() + } + + /// Corrects the current strategy with some parameters passed by the user + pub fn correct_with(&mut self, params: Option>>>) { + // We correct the strategy if specified in parameters + let params = params.unwrap_or_else(|| vec![None; self.0.len()]); + + self.0 + .iter_mut() + .zip(params) + .for_each(|(strategy, params)| { + if let Some(param) = params { + strategy.yield_source.asset_distribution = param; + } + }) + } +} + +#[cw_serde] +pub struct StrategyStatusElement { + pub denom: String, + pub raw_funds: Uint128, + pub remaining_amount: Uint128, +} + +/// This contains information about the strategy status +/// AFTER filling with unrelated coins +/// Before filling with related coins +#[cw_serde] +pub struct StrategyStatus(pub Vec>); + +impl From>> for StrategyStatus { + fn from(value: Vec>) -> Self { + Self(value) + } +} + +impl StrategyStatus { + pub fn fill_with_remaining_funds( + &self, + mut funds: Coins, + exchange_rates: &HashMap, + ) -> AppResult> { + self.0 + .iter() + .map(|f| { + f.clone() + .iter_mut() + .map(|status| { + let mut swaps = vec![]; + for fund in funds.to_vec() { + let direct_e_r = exchange_rates + .get(&fund.denom) + .ok_or(AppError::NoExchangeRate(fund.denom.clone()))? + / exchange_rates + .get(&status.denom) + .ok_or(AppError::NoExchangeRate(status.denom.clone()))?; + let available_coin_in_destination_amount = fund.amount * direct_e_r; + + let fill_amount = + available_coin_in_destination_amount.min(status.remaining_amount); + + let swap_in_amount = fill_amount * (Decimal::one() / direct_e_r); + + if swap_in_amount != Uint128::zero() { + status.remaining_amount -= fill_amount; + let swap_funds = coin(swap_in_amount.into(), fund.denom); + funds.sub(swap_funds.clone())?; + swaps.push(DepositStep::Swap { + asset_in: swap_funds, + denom_out: status.denom.clone(), + expected_amount: fill_amount, + }); + } + } + if !status.raw_funds.is_zero() { + swaps.push(DepositStep::UseFunds { + asset: coin(status.raw_funds.into(), status.denom.clone()), + }) + } + + Ok::<_, AppError>(swaps) + }) + .collect::, _>>() + .map(Into::into) + }) + .collect::, _>>() + } +} + +#[cw_serde] +pub enum DepositStep { + Swap { + asset_in: Coin, + denom_out: String, + expected_amount: Uint128, + }, + UseFunds { + asset: Coin, + }, +} + +#[cw_serde] +pub struct OneDepositStrategy(pub Vec>); + +impl From>> for OneDepositStrategy { + fn from(value: Vec>) -> Self { + Self(value) + } +} + +impl OneDepositStrategy { + pub fn deposit_msgs( + &self, + env: &Env, + yield_index: usize, + yield_type: YieldType, + ) -> AppResult { + // For each strategy, we send a message on the contract to execute it + Ok(wasm_execute( + env.contract.address.clone(), + &ExecuteMsg::Module(AppExecuteMsg::Internal( + InternalExecuteMsg::DepositOneStrategy { + swap_strategy: self.clone(), + yield_type, + yield_index, + }, + )), + vec![], + )? + .into()) + } +} diff --git a/contracts/carrot-app/src/distribution/mod.rs b/contracts/carrot-app/src/distribution/mod.rs new file mode 100644 index 00000000..70705118 --- /dev/null +++ b/contracts/carrot-app/src/distribution/mod.rs @@ -0,0 +1,4 @@ +/// This module handles the strategy distribution. It handles the following cases +/// 1. A user deposits some funds. +/// This modules dispatches the funds into the different strategies according to current status and target strats +pub mod deposit; diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index f1688181..dfee55ed 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -2,8 +2,9 @@ use crate::{ contract::{App, AppResult}, error::AppError, handlers::query::query_balance, + helpers::assert_contract, msg::{AppExecuteMsg, ExecuteMsg, InternalExecuteMsg}, - state::{assert_contract, get_autocompound_status, Config, CONFIG}, + state::{get_autocompound_status, Config, CONFIG}, yield_sources::{BalanceStrategy, ExpectedToken}, }; use abstract_app::{abstract_sdk::features::AbstractResponse, objects::AnsAsset}; @@ -88,9 +89,6 @@ fn deposit( .assert_admin(deps.as_ref(), &info.sender) .or(assert_contract(&info, &env))?; - let yield_source_params = yield_source_params - .unwrap_or_else(|| vec![None; query_strategy(deps.as_ref()).unwrap().strategy.0.len()]); - let deposit_msgs = _inner_deposit(deps.as_ref(), &env, funds, yield_source_params, &app)?; Ok(app.response("deposit").add_messages(deposit_msgs)) @@ -226,12 +224,11 @@ fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResu /// Deposit | x | x | 0 | /// Withdraw Rewards | x + y | x| y | /// Re-deposit | x + y | x + y | 0 | -/// pub fn _inner_deposit( deps: Deps, env: &Env, funds: Vec, - yield_source_params: Vec>>, + yield_source_params: Option>>>, app: &App, ) -> AppResult> { // We query the target strategy depending on the existing deposits @@ -241,46 +238,15 @@ pub fn _inner_deposit( let exchange_rates = query_all_exchange_rates( deps, current_strategy_status - .0 - .clone() - .iter() - .flat_map(|s| { - s.yield_source - .asset_distribution - .iter() - .map(|ExpectedToken { denom, share: _ }| denom.clone()) - }) + .get_all_tokens() + .into_iter() .chain(funds.iter().map(|f| f.denom.clone())), app, )?; - // We correct the strategy if specified in parameters - current_strategy_status - .0 - .iter_mut() - .zip(yield_source_params) - .for_each(|(strategy, params)| { - if let Some(param) = params { - strategy.yield_source.asset_distribution = param; - } - }); - - let deposit_strategies = current_strategy_status.fill_all(funds, &exchange_rates)?; - - // We select the target shares depending on the strategy selected - let deposit_msgs = deposit_strategies - .iter() - .zip( - current_strategy_status - .0 - .iter() - .map(|s| s.yield_source.ty.clone()), - ) - .enumerate() - .map(|(index, (strategy, yield_type))| strategy.deposit_msgs(env, index, yield_type)) - .collect::, _>>()?; + current_strategy_status.correct_with(yield_source_params); - Ok(deposit_msgs) + current_strategy_status.fill_all_and_get_messages(env, funds, &exchange_rates) } fn _inner_withdraw( diff --git a/contracts/carrot-app/src/handlers/instantiate.rs b/contracts/carrot-app/src/handlers/instantiate.rs index 960b93b2..67b1596b 100644 --- a/contracts/carrot-app/src/handlers/instantiate.rs +++ b/contracts/carrot-app/src/handlers/instantiate.rs @@ -28,7 +28,6 @@ pub fn instantiate_handler( .rewards .check(deps.as_ref(), &msg.dex, ans.host())?; - let strategy_len = msg.balance_strategy.0.len(); let config: Config = Config { dex: msg.dex, balance_strategy: msg.balance_strategy, @@ -40,8 +39,7 @@ pub fn instantiate_handler( // If provided - do an initial deposit if let Some(funds) = msg.deposit { - let deposit_msgs = - _inner_deposit(deps.as_ref(), &env, funds, vec![None; strategy_len], &app)?; + let deposit_msgs = _inner_deposit(deps.as_ref(), &env, funds, None, &app)?; response = response.add_messages(deposit_msgs); } diff --git a/contracts/carrot-app/src/handlers/internal.rs b/contracts/carrot-app/src/handlers/internal.rs index 8048fb53..26fa3c9f 100644 --- a/contracts/carrot-app/src/handlers/internal.rs +++ b/contracts/carrot-app/src/handlers/internal.rs @@ -1,12 +1,13 @@ use crate::{ contract::{App, AppResult}, + distribution::deposit::{DepositStep, OneDepositStrategy}, helpers::{add_funds, get_proxy_balance}, msg::{AppExecuteMsg, ExecuteMsg, InternalExecuteMsg}, replies::REPLY_AFTER_SWAPS_STEP, state::{ CONFIG, TEMP_CURRENT_COIN, TEMP_CURRENT_YIELD, TEMP_DEPOSIT_COINS, TEMP_EXPECTED_SWAP_COIN, }, - yield_sources::{yield_type::YieldType, BalanceStrategy, DepositStep, OneDepositStrategy}, + yield_sources::{yield_type::YieldType, BalanceStrategy}, }; use abstract_app::{abstract_sdk::features::AbstractResponse, objects::AnsAsset}; use abstract_dex_adapter::DexInterface; diff --git a/contracts/carrot-app/src/helpers.rs b/contracts/carrot-app/src/helpers.rs index 2413c768..9ab1ed2c 100644 --- a/contracts/carrot-app/src/helpers.rs +++ b/contracts/carrot-app/src/helpers.rs @@ -1,8 +1,19 @@ +use std::collections::HashMap; + use crate::contract::{App, AppResult}; +use crate::error::AppError; use abstract_app::traits::AccountIdentification; use abstract_app::{objects::AssetEntry, traits::AbstractNameService}; use abstract_sdk::Resolve; -use cosmwasm_std::{Addr, Coin, Coins, Decimal, Deps, StdResult, Uint128}; +use cosmwasm_std::{Addr, Coin, Coins, Decimal, Deps, Env, MessageInfo, StdResult, Uint128}; + +pub fn assert_contract(info: &MessageInfo, env: &Env) -> AppResult<()> { + if info.sender == env.contract.address { + Ok(()) + } else { + Err(AppError::Unauthorized {}) + } +} pub fn get_balance(a: AssetEntry, deps: Deps, address: Addr, app: &App) -> AppResult { let denom = a.resolve(&deps.querier, &app.ans_host(deps)?)?; @@ -36,6 +47,21 @@ pub fn close_to(expected: Decimal, actual: Decimal) -> bool { && actual < expected * (Decimal::one() + close_coeff) } +pub fn compute_total_value( + funds: &[Coin], + exchange_rates: &HashMap, +) -> AppResult { + funds + .iter() + .map(|c| { + let exchange_rate = exchange_rates + .get(&c.denom) + .ok_or(AppError::NoExchangeRate(c.denom.clone()))?; + Ok(c.amount * *exchange_rate) + }) + .sum() +} + #[cfg(test)] mod test { use std::str::FromStr; diff --git a/contracts/carrot-app/src/lib.rs b/contracts/carrot-app/src/lib.rs index feba3e76..048056c6 100644 --- a/contracts/carrot-app/src/lib.rs +++ b/contracts/carrot-app/src/lib.rs @@ -1,4 +1,5 @@ pub mod contract; +pub mod distribution; pub mod error; mod handlers; pub mod helpers; diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index 4d8ba348..360848e5 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -4,8 +4,9 @@ use cw_asset::AssetBase; use crate::{ contract::App, + distribution::deposit::OneDepositStrategy, state::AutocompoundConfig, - yield_sources::{yield_type::YieldType, BalanceStrategy, ExpectedToken, OneDepositStrategy}, + yield_sources::{yield_type::YieldType, BalanceStrategy, ExpectedToken}, }; // This is used for type safety and re-exporting the contract endpoint structs. diff --git a/contracts/carrot-app/src/state.rs b/contracts/carrot-app/src/state.rs index 5ff0cc26..bdddfa42 100644 --- a/contracts/carrot-app/src/state.rs +++ b/contracts/carrot-app/src/state.rs @@ -1,11 +1,7 @@ -use std::collections::HashMap; - use abstract_app::abstract_sdk::{feature_objects::AnsHost, Resolve}; use abstract_app::{abstract_core::objects::AssetEntry, objects::DexAssetPairing}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ - ensure, Addr, Coin, Decimal, Deps, Env, MessageInfo, Storage, Timestamp, Uint128, Uint64, -}; +use cosmwasm_std::{ensure, Addr, Coin, Deps, Env, Storage, Timestamp, Uint128, Uint64}; use cw_storage_plus::Item; use crate::yield_sources::BalanceStrategy; @@ -87,29 +83,6 @@ pub struct PoolConfig { pub asset1: AssetEntry, } -pub fn compute_total_value( - funds: &[Coin], - exchange_rates: &HashMap, -) -> AppResult { - funds - .iter() - .map(|c| { - let exchange_rate = exchange_rates - .get(&c.denom) - .ok_or(AppError::NoExchangeRate(c.denom.clone()))?; - Ok(c.amount * *exchange_rate) - }) - .sum() -} - -pub fn assert_contract(info: &MessageInfo, env: &Env) -> AppResult<()> { - if info.sender == env.contract.address { - Ok(()) - } else { - Err(AppError::Unauthorized {}) - } -} - #[cw_serde] pub struct AutocompoundState { pub last_compound: Timestamp, diff --git a/contracts/carrot-app/src/yield_sources.rs b/contracts/carrot-app/src/yield_sources.rs index 46fe74a4..79d8d90a 100644 --- a/contracts/carrot-app/src/yield_sources.rs +++ b/contracts/carrot-app/src/yield_sources.rs @@ -3,18 +3,13 @@ pub mod osmosis_cl_pool; pub mod yield_type; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ - coin, ensure, ensure_eq, wasm_execute, Coin, Coins, CosmosMsg, Decimal, Deps, Env, Uint128, -}; +use cosmwasm_std::{ensure, ensure_eq, Decimal, Deps}; use cw_asset::AssetInfo; -use std::collections::HashMap; use crate::{ contract::{App, AppResult}, error::AppError, helpers::close_to, - msg::{AppExecuteMsg, ExecuteMsg, InternalExecuteMsg}, - state::compute_total_value, }; use abstract_app::traits::AbstractNameService; @@ -132,195 +127,16 @@ impl BalanceStrategy { Ok(()) } - // We dispatch the available funds directly into the Strategies - // This returns : - // 0 : Funds that are used for specific strategies. And remaining amounts to fill those strategies - // 1 : Funds that are still available to fill those strategies - // This is the algorithm that is implemented here - pub fn fill_sources( - &self, - funds: Vec, - exchange_rates: &HashMap, - ) -> AppResult<(StrategyStatus, Coins)> { - let total_value = compute_total_value(&funds, exchange_rates)?; - let mut remaining_funds = Coins::default(); - - // We create the vector that holds the funds information - let mut yield_source_status = self - .0 + pub fn get_all_tokens(&self) -> Vec { + self.0 + .clone() .iter() - .map(|source| { - source - .yield_source + .flat_map(|s| { + s.yield_source .asset_distribution .iter() - .map(|ExpectedToken { denom, share }| B { - denom: denom.clone(), - raw_funds: Uint128::zero(), - remaining_amount: share * source.share * total_value, - }) - .collect::>() + .map(|ExpectedToken { denom, share: _ }| denom.clone()) }) - .collect::>(); - - for this_coin in funds { - let mut remaining_amount = this_coin.amount; - // We distribute those funds in to the accepting strategies - for (strategy, status) in self.0.iter().zip(yield_source_status.iter_mut()) { - // Find the share for the specific denom inside the strategy - let this_denom_status = strategy - .yield_source - .asset_distribution - .iter() - .zip(status.iter_mut()) - .find(|(ExpectedToken { denom, share: _ }, _status)| this_coin.denom.eq(denom)) - .map(|(_, status)| status); - - if let Some(status) = this_denom_status { - // We fill the needed value with the remaining_amount - let funds_to_use_here = remaining_amount.min(status.remaining_amount); - - // Those funds are not available for other yield sources - remaining_amount -= funds_to_use_here; - - status.raw_funds += funds_to_use_here; - status.remaining_amount -= funds_to_use_here; - } - } - remaining_funds.add(coin(remaining_amount.into(), this_coin.denom))?; - } - - Ok((yield_source_status.into(), remaining_funds)) + .collect() } - - pub fn fill_all( - &self, - funds: Vec, - exchange_rates: &HashMap, - ) -> AppResult> { - let (status, remaining_funds) = self.fill_sources(funds, exchange_rates)?; - status.fill_with_remaining_funds(remaining_funds, exchange_rates) - } -} - -#[cw_serde] -pub struct B { - pub denom: String, - pub raw_funds: Uint128, - pub remaining_amount: Uint128, -} - -/// This contains information about the strategy status -/// AFTER filling with unrelated coins -/// Before filling with related coins -#[cw_serde] -pub struct StrategyStatus(pub Vec>); - -impl From>> for StrategyStatus { - fn from(value: Vec>) -> Self { - Self(value) - } -} - -impl StrategyStatus { - pub fn fill_with_remaining_funds( - &self, - mut funds: Coins, - exchange_rates: &HashMap, - ) -> AppResult> { - self.0 - .iter() - .map(|f| { - f.clone() - .iter_mut() - .map(|status| { - let mut swaps = vec![]; - for fund in funds.to_vec() { - let direct_e_r = exchange_rates - .get(&fund.denom) - .ok_or(AppError::NoExchangeRate(fund.denom.clone()))? - / exchange_rates - .get(&status.denom) - .ok_or(AppError::NoExchangeRate(status.denom.clone()))?; - let available_coin_in_destination_amount = fund.amount * direct_e_r; - - let fill_amount = - available_coin_in_destination_amount.min(status.remaining_amount); - - let swap_in_amount = fill_amount * (Decimal::one() / direct_e_r); - - if swap_in_amount != Uint128::zero() { - status.remaining_amount -= fill_amount; - let swap_funds = coin(swap_in_amount.into(), fund.denom); - funds.sub(swap_funds.clone())?; - swaps.push(DepositStep::Swap { - asset_in: swap_funds, - denom_out: status.denom.clone(), - expected_amount: fill_amount, - }); - } - } - if !status.raw_funds.is_zero() { - swaps.push(DepositStep::UseFunds { - asset: coin(status.raw_funds.into(), status.denom.clone()), - }) - } - - Ok::<_, AppError>(swaps) - }) - .collect::, _>>() - .map(Into::into) - }) - .collect::, _>>() - } -} - -#[cw_serde] -pub enum DepositStep { - Swap { - asset_in: Coin, - denom_out: String, - expected_amount: Uint128, - }, - UseFunds { - asset: Coin, - }, -} - -#[cw_serde] -pub struct OneDepositStrategy(pub Vec>); - -impl From>> for OneDepositStrategy { - fn from(value: Vec>) -> Self { - Self(value) - } -} - -impl OneDepositStrategy { - pub fn deposit_msgs( - &self, - env: &Env, - yield_index: usize, - yield_type: YieldType, - ) -> AppResult { - // For each strategy, we send a message on the contract to execute it - Ok(wasm_execute( - env.contract.address.clone(), - &ExecuteMsg::Module(AppExecuteMsg::Internal( - InternalExecuteMsg::DepositOneStrategy { - swap_strategy: self.clone(), - yield_type, - yield_index, - }, - )), - vec![], - )? - .into()) - } -} - -#[cw_serde] -pub enum DepositStepResult { - Todo(DepositStep), - Done { amount: Coin }, } From 54a0cb64ab96c98ecac6195fb5de40254a967467 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Thu, 28 Mar 2024 12:04:12 +0000 Subject: [PATCH 16/42] Fix auto-compounding and test checks --- .../carrot-app/src/distribution/deposit.rs | 25 ++- contracts/carrot-app/src/distribution/mod.rs | 12 ++ .../carrot-app/src/distribution/query.rs | 21 +++ .../carrot-app/src/distribution/rewards.rs | 34 ++++ .../carrot-app/src/distribution/withdraw.rs | 40 ++++ contracts/carrot-app/src/error.rs | 3 + contracts/carrot-app/src/handlers/execute.rs | 176 +++--------------- .../carrot-app/src/handlers/instantiate.rs | 8 +- contracts/carrot-app/src/handlers/query.rs | 6 +- contracts/carrot-app/src/msg.rs | 2 +- contracts/carrot-app/src/state.rs | 94 +++++++++- contracts/carrot-app/src/yield_sources.rs | 35 ++-- .../src/yield_sources/yield_type.rs | 3 +- contracts/carrot-app/tests/config.rs | 14 +- .../carrot-app/tests/deposit_withdraw.rs | 4 +- 15 files changed, 288 insertions(+), 189 deletions(-) create mode 100644 contracts/carrot-app/src/distribution/query.rs create mode 100644 contracts/carrot-app/src/distribution/rewards.rs create mode 100644 contracts/carrot-app/src/distribution/withdraw.rs diff --git a/contracts/carrot-app/src/distribution/deposit.rs b/contracts/carrot-app/src/distribution/deposit.rs index b6d193d4..ffdb4656 100644 --- a/contracts/carrot-app/src/distribution/deposit.rs +++ b/contracts/carrot-app/src/distribution/deposit.rs @@ -1,9 +1,10 @@ use std::collections::HashMap; -use cosmwasm_std::{coin, Coin, Coins, Decimal, Uint128}; +use cosmwasm_std::{coin, Coin, Coins, Decimal, Deps, Uint128}; use crate::{ - contract::AppResult, + contract::{App, AppResult}, + handlers::query::query_all_exchange_rates, helpers::compute_total_value, yield_sources::{yield_type::YieldType, BalanceStrategy, ExpectedToken}, }; @@ -80,21 +81,31 @@ impl BalanceStrategy { pub fn fill_all( &self, + deps: Deps, funds: Vec, - exchange_rates: &HashMap, + app: &App, ) -> AppResult> { - let (status, remaining_funds) = self.fill_sources(funds, exchange_rates)?; - status.fill_with_remaining_funds(remaining_funds, exchange_rates) + // We determine the value of all tokens that will be used inside this function + let exchange_rates = query_all_exchange_rates( + deps, + self.all_denoms() + .into_iter() + .chain(funds.iter().map(|f| f.denom.clone())), + app, + )?; + let (status, remaining_funds) = self.fill_sources(funds, &exchange_rates)?; + status.fill_with_remaining_funds(remaining_funds, &exchange_rates) } /// Gets the deposit messages from a given strategy by filling all strategies with the associated funds pub fn fill_all_and_get_messages( &self, + deps: Deps, env: &Env, funds: Vec, - exchange_rates: &HashMap, + app: &App, ) -> AppResult> { - let deposit_strategies = self.fill_all(funds, exchange_rates)?; + let deposit_strategies = self.fill_all(deps, funds, app)?; deposit_strategies .iter() .zip(self.0.iter().map(|s| s.yield_source.ty.clone())) diff --git a/contracts/carrot-app/src/distribution/mod.rs b/contracts/carrot-app/src/distribution/mod.rs index 70705118..430cbc29 100644 --- a/contracts/carrot-app/src/distribution/mod.rs +++ b/contracts/carrot-app/src/distribution/mod.rs @@ -2,3 +2,15 @@ /// 1. A user deposits some funds. /// This modules dispatches the funds into the different strategies according to current status and target strats pub mod deposit; + +/// 2. A user want to withdraw some funds +/// This module withdraws a share of the funds deposited inside the registered strategies +pub mod withdraw; + +/// 3. A user wants to claim their rewards and autocompound +/// This module compute the available rewards and withdraws the rewards from the registered strategies +pub mod rewards; + +/// 4. Some queries are needed on certain structures for abstraction purposes +/// +pub mod query; diff --git a/contracts/carrot-app/src/distribution/query.rs b/contracts/carrot-app/src/distribution/query.rs new file mode 100644 index 00000000..24e887f8 --- /dev/null +++ b/contracts/carrot-app/src/distribution/query.rs @@ -0,0 +1,21 @@ +use cosmwasm_std::{Deps, Uint128}; + +use crate::{ + contract::{App, AppResult}, + error::AppError, + handlers::query::query_exchange_rate, + msg::AssetsBalanceResponse, +}; + +impl AssetsBalanceResponse { + pub fn value(&self, deps: Deps, app: &App) -> AppResult { + self.balances + .iter() + .map(|balance| { + let exchange_rate = query_exchange_rate(deps, &balance.denom, app)?; + + Ok::<_, AppError>(exchange_rate * balance.amount) + }) + .sum() + } +} diff --git a/contracts/carrot-app/src/distribution/rewards.rs b/contracts/carrot-app/src/distribution/rewards.rs new file mode 100644 index 00000000..1a450d9d --- /dev/null +++ b/contracts/carrot-app/src/distribution/rewards.rs @@ -0,0 +1,34 @@ +use abstract_sdk::{AccountAction, Execution, ExecutorMsg}; +use cosmwasm_std::{Coin, Deps}; + +use crate::{ + contract::{App, AppResult}, + error::AppError, + yield_sources::BalanceStrategy, +}; + +impl BalanceStrategy { + pub fn withdraw_rewards( + self, + deps: Deps, + app: &App, + ) -> AppResult<(Vec, Vec)> { + let (rewards, msgs): (Vec>, _) = self + .0 + .into_iter() + .map(|s| { + let (rewards, raw_msgs) = s.yield_source.ty.withdraw_rewards(deps, app)?; + + Ok::<_, AppError>(( + rewards, + app.executor(deps) + .execute(vec![AccountAction::from_vec(raw_msgs)])?, + )) + }) + .collect::, _>>()? + .into_iter() + .unzip(); + + Ok((rewards.into_iter().flatten().collect(), msgs)) + } +} diff --git a/contracts/carrot-app/src/distribution/withdraw.rs b/contracts/carrot-app/src/distribution/withdraw.rs new file mode 100644 index 00000000..428d5b03 --- /dev/null +++ b/contracts/carrot-app/src/distribution/withdraw.rs @@ -0,0 +1,40 @@ +use abstract_sdk::{AccountAction, Execution, ExecutorMsg}; +use cosmwasm_std::{Decimal, Deps}; + +use crate::{ + contract::{App, AppResult}, + error::AppError, + yield_sources::BalanceStrategy, +}; + +impl BalanceStrategy { + pub fn withdraw( + self, + deps: Deps, + withdraw_share: Option, + app: &App, + ) -> AppResult> { + self.0 + .into_iter() + .map(|s| { + let this_withdraw_amount = withdraw_share + .map(|share| { + let this_amount = s.yield_source.ty.user_liquidity(deps, app)?; + let this_withdraw_amount = share * this_amount; + + Ok::<_, AppError>(this_withdraw_amount) + }) + .transpose()?; + let raw_msg = s + .yield_source + .ty + .withdraw(deps, this_withdraw_amount, app)?; + + Ok::<_, AppError>( + app.executor(deps) + .execute(vec![AccountAction::from_vec(raw_msg)])?, + ) + }) + .collect() + } +} diff --git a/contracts/carrot-app/src/error.rs b/contracts/carrot-app/src/error.rs index e91821ab..010e1307 100644 --- a/contracts/carrot-app/src/error.rs +++ b/contracts/carrot-app/src/error.rs @@ -54,6 +54,9 @@ pub enum AppError { #[error("Deposit pool was not found")] PoolNotFound {}, + #[error("Deposit assets were not found in Abstract ANS : {0:?}")] + AssetsNotRegistered(Vec), + #[error("No swap fund to swap assets into each other")] NoSwapPossibility {}, diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index dfee55ed..5a8b768c 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -4,20 +4,18 @@ use crate::{ handlers::query::query_balance, helpers::assert_contract, msg::{AppExecuteMsg, ExecuteMsg, InternalExecuteMsg}, - state::{get_autocompound_status, Config, CONFIG}, + state::{AUTOCOMPOUND_STATE, CONFIG}, yield_sources::{BalanceStrategy, ExpectedToken}, }; -use abstract_app::{abstract_sdk::features::AbstractResponse, objects::AnsAsset}; -use abstract_dex_adapter::DexInterface; -use abstract_sdk::{AccountAction, Execution, ExecutorMsg, TransferInterface}; +use abstract_app::abstract_sdk::features::AbstractResponse; +use abstract_sdk::ExecutorMsg; use cosmwasm_std::{ to_json_binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Uint128, WasmMsg, }; use super::{ internal::{deposit_one_strategy, execute_finalize_deposit, execute_one_deposit_step}, - query::{query_all_exchange_rates, query_exchange_rate, query_strategy, query_strategy_target}, - swap_helpers::swap_msg, + query::{query_strategy, query_strategy_target}, }; pub fn execute_handler( @@ -34,7 +32,9 @@ pub fn execute_handler( } => deposit(deps, env, info, funds, yield_sources_params, app), AppExecuteMsg::Withdraw { amount } => withdraw(deps, env, info, amount, app), AppExecuteMsg::Autocompound {} => autocompound(deps, env, info, app), - AppExecuteMsg::Rebalance { strategy } => rebalance(deps, env, info, strategy, app), + AppExecuteMsg::UpdateStrategy { strategy } => { + update_strategy(deps, env, info, strategy, app) + } // Endpoints called by the contract directly AppExecuteMsg::Internal(internal_msg) => { if info.sender != env.contract.address { @@ -109,7 +109,7 @@ fn withdraw( Ok(app.response("withdraw").add_messages(msgs)) } -fn rebalance( +fn update_strategy( deps: DepsMut, _env: Env, _info: MessageInfo, @@ -134,26 +134,11 @@ fn rebalance( fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResult { // Everyone can autocompound - let strategy = query_strategy(deps.as_ref())?.strategy; - // We withdraw all rewards from protocols - let (rewards, collect_rewards_msgs): (Vec>, Vec) = strategy - .0 - .into_iter() - .map(|s| { - let (rewards, raw_msgs) = s.yield_source.ty.withdraw_rewards(deps.as_ref(), &app)?; - Ok::<_, AppError>(( - rewards, - app.executor(deps.as_ref()) - .execute(vec![AccountAction::from_vec(raw_msgs)])?, - )) - }) - .collect::, _>>()? - .into_iter() - .unzip(); + // We withdraw all rewards from protocols + let (all_rewards, collect_rewards_msgs) = strategy.withdraw_rewards(deps.as_ref(), &app)?; - let all_rewards: Vec = rewards.into_iter().flatten().collect(); // If there are no rewards, we can't do anything if all_rewards.is_empty() { return Err(crate::error::AppError::NoRewards {}); @@ -169,33 +154,21 @@ fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResu funds: vec![], }); - let mut response = app + let response = app .response("auto-compound") .add_messages(collect_rewards_msgs) .add_message(msg_deposit); - // If called by non-admin and reward cooldown has ended, send rewards to the contract caller. let config = CONFIG.load(deps.storage)?; - if !app.admin.is_admin(deps.as_ref(), &info.sender)? - && get_autocompound_status( - deps.storage, - &env, - config.autocompound_config.cooldown_seconds.u64(), - )? - .is_ready() - { - let executor_reward_messages = autocompound_executor_rewards( - deps.as_ref(), - &env, - info.sender.into_string(), - &app, - config, - )?; + let executor_reward_messages = + config.get_executor_reward_messages(deps.as_ref(), &env, info, &app)?; - response = response.add_messages(executor_reward_messages); - } + AUTOCOMPOUND_STATE.update(deps.storage, |mut state| { + state.last_compound = env.block.time; + Ok::<_, AppError>(state) + })?; - Ok(response) + Ok(response.add_messages(executor_reward_messages)) } /// The deposit process goes through the following steps @@ -234,19 +207,11 @@ pub fn _inner_deposit( // We query the target strategy depending on the existing deposits let mut current_strategy_status = query_strategy_target(deps, app)?.strategy; - // We determine the value of all tokens that will be used inside this function - let exchange_rates = query_all_exchange_rates( - deps, - current_strategy_status - .get_all_tokens() - .into_iter() - .chain(funds.iter().map(|f| f.denom.clone())), - app, - )?; - + // We correct it if the user asked to correct the share parameters of each strategy current_strategy_status.correct_with(yield_source_params); - current_strategy_status.fill_all_and_get_messages(env, funds, &exchange_rates) + // We fill the strategies with the current deposited funds and get messages to execute those deposits + current_strategy_status.fill_all_and_get_messages(deps, env, funds, app) } fn _inner_withdraw( @@ -259,15 +224,7 @@ fn _inner_withdraw( let withdraw_share = value .map(|value| { let total_deposit = query_balance(deps.as_ref(), app)?; - let total_value = total_deposit - .balances - .into_iter() - .map(|balance| { - let exchange_rate = query_exchange_rate(deps.as_ref(), balance.denom, app)?; - - Ok::<_, AppError>(exchange_rate * balance.amount) - }) - .sum::>()?; + let total_value = total_deposit.value(deps.as_ref(), app)?; if total_value.is_zero() { return Err(AppError::NoDeposit {}); @@ -276,90 +233,11 @@ fn _inner_withdraw( }) .transpose()?; - // We withdraw the necessary share from all investments - let withdraw_msgs = query_strategy(deps.as_ref())? - .strategy - .0 - .into_iter() - .map(|s| { - let this_withdraw_amount = withdraw_share - .map(|share| { - let this_amount = s.yield_source.ty.user_liquidity(deps.as_ref(), app)?; - let this_withdraw_amount = share * this_amount; - - Ok::<_, AppError>(this_withdraw_amount) - }) - .transpose()?; - let raw_msg = s - .yield_source - .ty - .withdraw(deps.as_ref(), this_withdraw_amount, app)?; - - Ok::<_, AppError>( - app.executor(deps.as_ref()) - .execute(vec![AccountAction::from_vec(raw_msg)])?, - ) - }) - .collect::, _>>()?; + // We withdraw the necessary share from all registered investments + let withdraw_msgs = + query_strategy(deps.as_ref())? + .strategy + .withdraw(deps.as_ref(), withdraw_share, app)?; Ok(withdraw_msgs.into_iter().collect()) } - -/// Sends autocompound rewards to the executor. -/// In case user does not have not enough gas token the contract will swap some -/// tokens for gas tokens. -pub fn autocompound_executor_rewards( - deps: Deps, - env: &Env, - executor: String, - app: &App, - config: Config, -) -> AppResult> { - let rewards_config = config.autocompound_config.rewards; - - // Get user balance of gas denom - let user_gas_balance = app.bank(deps).balance(&rewards_config.gas_asset)?.amount; - - let mut rewards_messages = vec![]; - - // If not enough gas coins - swap for some amount - if user_gas_balance < rewards_config.min_gas_balance { - // Get asset entries - let dex = app.ans_dex(deps, config.dex.to_string()); - - // Do reverse swap to find approximate amount we need to swap - let need_gas_coins = rewards_config.max_gas_balance - user_gas_balance; - let simulate_swap_response = dex.simulate_swap( - AnsAsset::new(rewards_config.gas_asset.clone(), need_gas_coins), - rewards_config.swap_asset.clone(), - )?; - - // Get user balance of swap denom - let user_swap_balance = app.bank(deps).balance(&rewards_config.swap_asset)?.amount; - - // Swap as much as available if not enough for max_gas_balance - let swap_amount = simulate_swap_response.return_amount.min(user_swap_balance); - - let msgs = swap_msg( - deps, - env, - AnsAsset::new(rewards_config.swap_asset, swap_amount), - rewards_config.gas_asset.clone(), - app, - )?; - rewards_messages.extend(msgs); - } - - // We send their reward to the executor - let msg_send = app.bank(deps).transfer( - vec![AnsAsset::new( - rewards_config.gas_asset, - rewards_config.reward, - )], - &deps.api.addr_validate(&executor)?, - )?; - - rewards_messages.push(app.executor(deps).execute(vec![msg_send])?.into()); - - Ok(rewards_messages) -} diff --git a/contracts/carrot-app/src/handlers/instantiate.rs b/contracts/carrot-app/src/handlers/instantiate.rs index 67b1596b..2ad842ac 100644 --- a/contracts/carrot-app/src/handlers/instantiate.rs +++ b/contracts/carrot-app/src/handlers/instantiate.rs @@ -1,7 +1,7 @@ use crate::{ contract::{App, AppResult}, msg::AppInstantiateMsg, - state::{Config, CONFIG}, + state::{AutocompoundState, Config, AUTOCOMPOUND_STATE, CONFIG}, }; use abstract_app::abstract_sdk::{features::AbstractNameService, AbstractResponse}; use cosmwasm_std::{DepsMut, Env, MessageInfo}; @@ -34,6 +34,12 @@ pub fn instantiate_handler( autocompound_config: msg.autocompound_config, }; CONFIG.save(deps.storage, &config)?; + AUTOCOMPOUND_STATE.save( + deps.storage, + &AutocompoundState { + last_compound: env.block.time, + }, + )?; let mut response = app.response("instantiate_savings_app"); diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index 64fbd8cf..50a0be46 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -241,7 +241,11 @@ fn query_rewards(deps: Deps, app: &App) -> AppResult { }) } -pub fn query_exchange_rate(_deps: Deps, _denom: String, _app: &App) -> AppResult { +pub fn query_exchange_rate( + _deps: Deps, + _denom: impl Into, + _app: &App, +) -> AppResult { // In the first iteration, all deposited tokens are assumed to be equal to 1 Ok(Decimal::one()) } diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index 360848e5..a773c97b 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -50,7 +50,7 @@ pub enum AppExecuteMsg { /// Auto-compounds the pool rewards into the pool Autocompound {}, /// Rebalances all investments according to a new balance strategy - Rebalance { strategy: BalanceStrategy }, + UpdateStrategy { strategy: BalanceStrategy }, /// Only called by the contract internally Internal(InternalExecuteMsg), diff --git a/contracts/carrot-app/src/state.rs b/contracts/carrot-app/src/state.rs index bdddfa42..648b744e 100644 --- a/contracts/carrot-app/src/state.rs +++ b/contracts/carrot-app/src/state.rs @@ -1,14 +1,21 @@ use abstract_app::abstract_sdk::{feature_objects::AnsHost, Resolve}; +use abstract_app::objects::AnsAsset; use abstract_app::{abstract_core::objects::AssetEntry, objects::DexAssetPairing}; +use abstract_dex_adapter::DexInterface; +use abstract_sdk::{Execution, TransferInterface}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ensure, Addr, Coin, Deps, Env, Storage, Timestamp, Uint128, Uint64}; +use cosmwasm_std::{ + ensure, Addr, Coin, CosmosMsg, Deps, Env, MessageInfo, Storage, Timestamp, Uint128, Uint64, +}; use cw_storage_plus::Item; +use crate::contract::App; +use crate::handlers::swap_helpers::swap_msg; use crate::yield_sources::BalanceStrategy; use crate::{contract::AppResult, error::AppError, msg::CompoundStatus}; pub const CONFIG: Item = Item::new("config"); -pub const POSITION: Item = Item::new("position"); +pub const AUTOCOMPOUND_STATE: Item = Item::new("position"); pub const CURRENT_EXECUTOR: Item = Item::new("executor"); // TEMP VARIABLES FOR DEPOSITING INTO ONE STRATEGY @@ -35,6 +42,87 @@ pub struct AutocompoundConfig { pub rewards: AutocompoundRewardsConfig, } +impl Config { + pub fn get_executor_reward_messages( + &self, + deps: Deps, + env: &Env, + info: MessageInfo, + app: &App, + ) -> AppResult> { + Ok( + // If called by non-admin and reward cooldown has ended, send rewards to the contract caller. + if !app.admin.is_admin(deps, &info.sender)? + && get_autocompound_status( + deps.storage, + env, + self.autocompound_config.cooldown_seconds.u64(), + )? + .is_ready() + { + self.autocompound_executor_rewards(deps, env, &info.sender, app)? + } else { + vec![] + }, + ) + } + pub fn autocompound_executor_rewards( + &self, + deps: Deps, + env: &Env, + executor: &Addr, + app: &App, + ) -> AppResult> { + let rewards_config = self.autocompound_config.rewards.clone(); + + // Get user balance of gas denom + let user_gas_balance = app.bank(deps).balance(&rewards_config.gas_asset)?.amount; + + let mut rewards_messages = vec![]; + + // If not enough gas coins - swap for some amount + if user_gas_balance < rewards_config.min_gas_balance { + // Get asset entries + let dex = app.ans_dex(deps, self.dex.to_string()); + + // Do reverse swap to find approximate amount we need to swap + let need_gas_coins = rewards_config.max_gas_balance - user_gas_balance; + let simulate_swap_response = dex.simulate_swap( + AnsAsset::new(rewards_config.gas_asset.clone(), need_gas_coins), + rewards_config.swap_asset.clone(), + )?; + + // Get user balance of swap denom + let user_swap_balance = app.bank(deps).balance(&rewards_config.swap_asset)?.amount; + + // Swap as much as available if not enough for max_gas_balance + let swap_amount = simulate_swap_response.return_amount.min(user_swap_balance); + + let msgs = swap_msg( + deps, + env, + AnsAsset::new(rewards_config.swap_asset, swap_amount), + rewards_config.gas_asset.clone(), + app, + )?; + rewards_messages.extend(msgs); + } + + // We send their reward to the executor + let msg_send = app.bank(deps).transfer( + vec![AnsAsset::new( + rewards_config.gas_asset, + rewards_config.reward, + )], + executor, + )?; + + rewards_messages.push(app.executor(deps).execute(vec![msg_send])?.into()); + + Ok(rewards_messages) + } +} + /// Configuration on how rewards should be distributed /// to the address who helped to execute autocompound #[cw_serde] @@ -93,7 +181,7 @@ pub fn get_autocompound_status( env: &Env, cooldown_seconds: u64, ) -> AppResult { - let position = POSITION.may_load(storage)?; + let position = AUTOCOMPOUND_STATE.may_load(storage)?; let status = match position { Some(position) => { let ready_on = position.last_compound.plus_seconds(cooldown_seconds); diff --git a/contracts/carrot-app/src/yield_sources.rs b/contracts/carrot-app/src/yield_sources.rs index 79d8d90a..2192c141 100644 --- a/contracts/carrot-app/src/yield_sources.rs +++ b/contracts/carrot-app/src/yield_sources.rs @@ -37,17 +37,18 @@ impl YieldSource { !self.asset_distribution.is_empty(), AppError::InvalidEmptyStrategy {} ); - // We ensure all deposited tokens exist in ANS + let all_denoms = self.all_denoms(); let ans = app.name_service(deps); - ans.host().query_assets_reverse( - &deps.querier, - &self - .asset_distribution - .iter() - .map(|e| AssetInfo::native(e.denom.clone())) - .collect::>(), - )?; + ans.host() + .query_assets_reverse( + &deps.querier, + &all_denoms + .iter() + .map(|denom| AssetInfo::native(denom.clone())) + .collect::>(), + ) + .map_err(|_| AppError::AssetsNotRegistered(all_denoms))?; // Then we check every yield strategy underneath match &self.ty { @@ -78,6 +79,13 @@ impl YieldSource { Ok(()) } + + pub fn all_denoms(&self) -> Vec { + self.asset_distribution + .iter() + .map(|e| e.denom.clone()) + .collect() + } } #[cw_serde] @@ -127,16 +135,11 @@ impl BalanceStrategy { Ok(()) } - pub fn get_all_tokens(&self) -> Vec { + pub fn all_denoms(&self) -> Vec { self.0 .clone() .iter() - .flat_map(|s| { - s.yield_source - .asset_distribution - .iter() - .map(|ExpectedToken { denom, share: _ }| denom.clone()) - }) + .flat_map(|s| s.yield_source.all_denoms()) .collect() } } diff --git a/contracts/carrot-app/src/yield_sources/yield_type.rs b/contracts/carrot-app/src/yield_sources/yield_type.rs index e8c88b1e..6eaddca9 100644 --- a/contracts/carrot-app/src/yield_sources/yield_type.rs +++ b/contracts/carrot-app/src/yield_sources/yield_type.rs @@ -118,7 +118,8 @@ pub struct ConcentratedPoolParams { impl ConcentratedPoolParams { pub fn check(&self, deps: Deps) -> AppResult<()> { let _pool: Pool = PoolmanagerQuerier::new(&deps.querier) - .pool(self.pool_id)? + .pool(self.pool_id) + .map_err(|_| AppError::PoolNotFound {})? .pool .ok_or(AppError::PoolNotFound {})? .try_into()?; diff --git a/contracts/carrot-app/tests/config.rs b/contracts/carrot-app/tests/config.rs index cbb78ee0..0d67ee08 100644 --- a/contracts/carrot-app/tests/config.rs +++ b/contracts/carrot-app/tests/config.rs @@ -17,7 +17,7 @@ fn rebalance_fails() -> anyhow::Result<()> { let (_, carrot_app) = setup_test_tube(false)?; carrot_app - .rebalance(BalanceStrategy(vec![ + .update_strategy(BalanceStrategy(vec![ BalanceStrategyElement { yield_source: YieldSource { asset_distribution: vec![ @@ -70,7 +70,7 @@ fn rebalance_fails() -> anyhow::Result<()> { #[test] fn rebalance_success() -> anyhow::Result<()> { - let (_, carrot_app) = setup_test_tube(false)?; + let (pool_id, carrot_app) = setup_test_tube(false)?; let new_strat = BalanceStrategy(vec![ BalanceStrategyElement { @@ -86,7 +86,7 @@ fn rebalance_success() -> anyhow::Result<()> { }, ], ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { - pool_id: 7, + pool_id, // Pool Id needs to exist lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, position_id: None, @@ -107,7 +107,7 @@ fn rebalance_success() -> anyhow::Result<()> { }, ], ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { - pool_id: 7, + pool_id, // Pool Id needs to exist lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, position_id: None, @@ -116,13 +116,11 @@ fn rebalance_success() -> anyhow::Result<()> { share: Decimal::percent(50), }, ]); - carrot_app.rebalance(new_strat.clone())?; + carrot_app.update_strategy(new_strat.clone())?; + // We query the new strategy let strategy = carrot_app.strategy()?; - assert_eq!(strategy.strategy, new_strat); - // We query the nex strategy - Ok(()) } diff --git a/contracts/carrot-app/tests/deposit_withdraw.rs b/contracts/carrot-app/tests/deposit_withdraw.rs index 0043dc2f..e97ed170 100644 --- a/contracts/carrot-app/tests/deposit_withdraw.rs +++ b/contracts/carrot-app/tests/deposit_withdraw.rs @@ -190,7 +190,7 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { share: Decimal::percent(50), }, ]); - carrot_app.rebalance(new_strat.clone())?; + carrot_app.update_strategy(new_strat.clone())?; let deposit_amount = 5_000; let deposit_coins = coins(deposit_amount, USDT.to_owned()); @@ -270,7 +270,7 @@ fn deposit_multiple_positions_with_empty() -> anyhow::Result<()> { share: Decimal::percent(0), }, ]); - carrot_app.rebalance(new_strat.clone())?; + carrot_app.update_strategy(new_strat.clone())?; let deposit_amount = 5_000; let deposit_coins = coins(deposit_amount, USDT.to_owned()); From 619e8c1f1f575973f95db2cad9e5d780ff80fc18 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Thu, 28 Mar 2024 17:32:42 +0000 Subject: [PATCH 17/42] Added smart deposit --- .../examples/install_savings_app.rs | 2 +- contracts/carrot-app/src/autocompound.rs | 172 +++++++ .../carrot-app/src/distribution/deposit.rs | 16 +- contracts/carrot-app/src/distribution/mod.rs | 4 +- .../carrot-app/src/distribution/query.rs | 131 +++++- .../carrot-app/src/distribution/rebalance.rs | 8 + .../carrot-app/src/distribution/withdraw.rs | 72 ++- contracts/carrot-app/src/handlers/execute.rs | 139 ++++-- .../carrot-app/src/handlers/instantiate.rs | 3 +- contracts/carrot-app/src/handlers/internal.rs | 8 +- contracts/carrot-app/src/handlers/query.rs | 104 +---- contracts/carrot-app/src/helpers.rs | 11 + contracts/carrot-app/src/lib.rs | 1 + contracts/carrot-app/src/msg.rs | 8 +- contracts/carrot-app/src/state.rs | 178 +------- contracts/carrot-app/src/yield_sources.rs | 46 +- .../carrot-app/src/yield_sources/mars.rs | 128 +++--- .../src/yield_sources/osmosis_cl_pool.rs | 422 +++++++++--------- .../src/yield_sources/yield_type.rs | 106 ++--- contracts/carrot-app/tests/autocompound.rs | 7 +- contracts/carrot-app/tests/common.rs | 13 +- contracts/carrot-app/tests/config.rs | 20 +- .../carrot-app/tests/deposit_withdraw.rs | 26 +- 23 files changed, 878 insertions(+), 747 deletions(-) create mode 100644 contracts/carrot-app/src/autocompound.rs create mode 100644 contracts/carrot-app/src/distribution/rebalance.rs diff --git a/contracts/carrot-app/examples/install_savings_app.rs b/contracts/carrot-app/examples/install_savings_app.rs index bdb84469..cf147462 100644 --- a/contracts/carrot-app/examples/install_savings_app.rs +++ b/contracts/carrot-app/examples/install_savings_app.rs @@ -11,9 +11,9 @@ use cw_orch::{ use dotenv::dotenv; use carrot_app::{ + autocompound::{AutocompoundConfig, AutocompoundRewardsConfig}, contract::OSMOSIS, msg::AppInstantiateMsg, - state::{AutocompoundConfig, AutocompoundRewardsConfig}, yield_sources::BalanceStrategy, }; use osmosis_std::types::cosmos::authz::v1beta1::MsgGrantResponse; diff --git a/contracts/carrot-app/src/autocompound.rs b/contracts/carrot-app/src/autocompound.rs new file mode 100644 index 00000000..4a526b32 --- /dev/null +++ b/contracts/carrot-app/src/autocompound.rs @@ -0,0 +1,172 @@ +use abstract_app::abstract_sdk::{feature_objects::AnsHost, Resolve}; +use abstract_app::objects::AnsAsset; +use abstract_app::{abstract_core::objects::AssetEntry, objects::DexAssetPairing}; +use abstract_dex_adapter::DexInterface; +use abstract_sdk::{Execution, TransferInterface}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ + ensure, Addr, CosmosMsg, Deps, Env, MessageInfo, Storage, Timestamp, Uint128, Uint64, +}; + +use crate::contract::App; +use crate::handlers::swap_helpers::swap_msg; +use crate::msg::CompoundStatus; +use crate::state::{Config, AUTOCOMPOUND_STATE}; +use crate::{contract::AppResult, error::AppError}; + +/// General auto-compound parameters. +/// Includes the cool down and the technical funds config +#[cw_serde] +pub struct AutocompoundConfig { + /// Seconds to wait before autocompound is incentivized. + /// Allows the user to configure when the auto-compound happens + pub cooldown_seconds: Uint64, + /// Configuration of rewards to the address who helped to execute autocompound + pub rewards: AutocompoundRewardsConfig, +} + +/// Configuration on how rewards should be distributed +/// to the address who helped to execute autocompound +#[cw_serde] +pub struct AutocompoundRewardsConfig { + /// Gas denominator for this chain + pub gas_asset: AssetEntry, + /// Denominator of the asset that will be used for swap to the gas asset + pub swap_asset: AssetEntry, + /// Reward amount + pub reward: Uint128, + /// If gas token balance falls below this bound a swap will be generated + pub min_gas_balance: Uint128, + /// Upper bound of gas tokens expected after the swap + pub max_gas_balance: Uint128, +} + +impl AutocompoundRewardsConfig { + pub fn check(&self, deps: Deps, dex_name: &str, ans_host: &AnsHost) -> AppResult<()> { + ensure!( + self.reward <= self.min_gas_balance, + AppError::RewardConfigError( + "reward should be lower or equal to the min_gas_balance".to_owned() + ) + ); + ensure!( + self.max_gas_balance > self.min_gas_balance, + AppError::RewardConfigError( + "max_gas_balance has to be bigger than min_gas_balance".to_owned() + ) + ); + + // Check swap asset has pairing into gas asset + DexAssetPairing::new(self.gas_asset.clone(), self.swap_asset.clone(), dex_name) + .resolve(&deps.querier, ans_host)?; + + Ok(()) + } +} + +/// Autocompound related methods +impl Config { + pub fn get_executor_reward_messages( + &self, + deps: Deps, + env: &Env, + info: MessageInfo, + app: &App, + ) -> AppResult> { + Ok( + // If called by non-admin and reward cooldown has ended, send rewards to the contract caller. + if !app.admin.is_admin(deps, &info.sender)? + && get_autocompound_status( + deps.storage, + env, + self.autocompound_config.cooldown_seconds.u64(), + )? + .is_ready() + { + self.autocompound_executor_rewards(deps, env, &info.sender, app)? + } else { + vec![] + }, + ) + } + pub fn autocompound_executor_rewards( + &self, + deps: Deps, + env: &Env, + executor: &Addr, + app: &App, + ) -> AppResult> { + let rewards_config = self.autocompound_config.rewards.clone(); + + // Get user balance of gas denom + let user_gas_balance = app.bank(deps).balance(&rewards_config.gas_asset)?.amount; + + let mut rewards_messages = vec![]; + + // If not enough gas coins - swap for some amount + if user_gas_balance < rewards_config.min_gas_balance { + // Get asset entries + let dex = app.ans_dex(deps, self.dex.to_string()); + + // Do reverse swap to find approximate amount we need to swap + let need_gas_coins = rewards_config.max_gas_balance - user_gas_balance; + let simulate_swap_response = dex.simulate_swap( + AnsAsset::new(rewards_config.gas_asset.clone(), need_gas_coins), + rewards_config.swap_asset.clone(), + )?; + + // Get user balance of swap denom + let user_swap_balance = app.bank(deps).balance(&rewards_config.swap_asset)?.amount; + + // Swap as much as available if not enough for max_gas_balance + let swap_amount = simulate_swap_response.return_amount.min(user_swap_balance); + + let msgs = swap_msg( + deps, + env, + AnsAsset::new(rewards_config.swap_asset, swap_amount), + rewards_config.gas_asset.clone(), + app, + )?; + rewards_messages.extend(msgs); + } + + // We send their reward to the executor + let msg_send = app.bank(deps).transfer( + vec![AnsAsset::new( + rewards_config.gas_asset, + rewards_config.reward, + )], + executor, + )?; + + rewards_messages.push(app.executor(deps).execute(vec![msg_send])?.into()); + + Ok(rewards_messages) + } +} + +#[cw_serde] +pub struct AutocompoundState { + pub last_compound: Timestamp, +} + +pub fn get_autocompound_status( + storage: &dyn Storage, + env: &Env, + cooldown_seconds: u64, +) -> AppResult { + let position = AUTOCOMPOUND_STATE.may_load(storage)?; + let status = match position { + Some(position) => { + let ready_on = position.last_compound.plus_seconds(cooldown_seconds); + if env.block.time >= ready_on { + CompoundStatus::Ready {} + } else { + CompoundStatus::Cooldown((env.block.time.seconds() - ready_on.seconds()).into()) + } + } + None => CompoundStatus::NoPosition {}, + }; + Ok(status) +} diff --git a/contracts/carrot-app/src/distribution/deposit.rs b/contracts/carrot-app/src/distribution/deposit.rs index ffdb4656..ed57b464 100644 --- a/contracts/carrot-app/src/distribution/deposit.rs +++ b/contracts/carrot-app/src/distribution/deposit.rs @@ -6,7 +6,7 @@ use crate::{ contract::{App, AppResult}, handlers::query::query_all_exchange_rates, helpers::compute_total_value, - yield_sources::{yield_type::YieldType, BalanceStrategy, ExpectedToken}, + yield_sources::{yield_type::YieldType, AssetShare, BalanceStrategy}, }; use cosmwasm_schema::cw_serde; @@ -23,7 +23,7 @@ impl BalanceStrategy { // 0 : Funds that are used for specific strategies. And remaining amounts to fill those strategies // 1 : Funds that are still available to fill those strategies // This is the algorithm that is implemented here - pub fn fill_sources( + fn fill_sources( &self, funds: Vec, exchange_rates: &HashMap, @@ -40,7 +40,7 @@ impl BalanceStrategy { .yield_source .asset_distribution .iter() - .map(|ExpectedToken { denom, share }| StrategyStatusElement { + .map(|AssetShare { denom, share }| StrategyStatusElement { denom: denom.clone(), raw_funds: Uint128::zero(), remaining_amount: share * source.share * total_value, @@ -59,7 +59,7 @@ impl BalanceStrategy { .asset_distribution .iter() .zip(status.iter_mut()) - .find(|(ExpectedToken { denom, share: _ }, _status)| this_coin.denom.eq(denom)) + .find(|(AssetShare { denom, share: _ }, _status)| this_coin.denom.eq(denom)) .map(|(_, status)| status); if let Some(status) = this_denom_status { @@ -79,7 +79,7 @@ impl BalanceStrategy { Ok((yield_source_status.into(), remaining_funds)) } - pub fn fill_all( + fn fill_all( &self, deps: Deps, funds: Vec, @@ -115,7 +115,7 @@ impl BalanceStrategy { } /// Corrects the current strategy with some parameters passed by the user - pub fn correct_with(&mut self, params: Option>>>) { + pub fn correct_with(&mut self, params: Option>>>) { // We correct the strategy if specified in parameters let params = params.unwrap_or_else(|| vec![None; self.0.len()]); @@ -131,7 +131,7 @@ impl BalanceStrategy { } #[cw_serde] -pub struct StrategyStatusElement { +struct StrategyStatusElement { pub denom: String, pub raw_funds: Uint128, pub remaining_amount: Uint128, @@ -141,7 +141,7 @@ pub struct StrategyStatusElement { /// AFTER filling with unrelated coins /// Before filling with related coins #[cw_serde] -pub struct StrategyStatus(pub Vec>); +struct StrategyStatus(pub Vec>); impl From>> for StrategyStatus { fn from(value: Vec>) -> Self { diff --git a/contracts/carrot-app/src/distribution/mod.rs b/contracts/carrot-app/src/distribution/mod.rs index 430cbc29..f3e36243 100644 --- a/contracts/carrot-app/src/distribution/mod.rs +++ b/contracts/carrot-app/src/distribution/mod.rs @@ -12,5 +12,7 @@ pub mod withdraw; pub mod rewards; /// 4. Some queries are needed on certain structures for abstraction purposes -/// pub mod query; + +/// 4. Some queries are needed on certain structures for abstraction purposes +pub mod rebalance; diff --git a/contracts/carrot-app/src/distribution/query.rs b/contracts/carrot-app/src/distribution/query.rs index 24e887f8..1eb6a350 100644 --- a/contracts/carrot-app/src/distribution/query.rs +++ b/contracts/carrot-app/src/distribution/query.rs @@ -1,21 +1,136 @@ -use cosmwasm_std::{Deps, Uint128}; +use cosmwasm_std::{Coins, Decimal, Deps, Uint128}; use crate::{ contract::{App, AppResult}, error::AppError, handlers::query::query_exchange_rate, msg::AssetsBalanceResponse, + yield_sources::{AssetShare, BalanceStrategy, BalanceStrategyElement, YieldSource}, }; -impl AssetsBalanceResponse { - pub fn value(&self, deps: Deps, app: &App) -> AppResult { - self.balances +impl BalanceStrategy { + // Returns the total balance + pub fn current_balance(&self, deps: Deps, app: &App) -> AppResult { + let mut funds = Coins::default(); + let mut total_value = Uint128::zero(); + self.0.iter().try_for_each(|s| { + let deposit_value = s + .yield_source + .ty + .user_deposit(deps, app) + .unwrap_or_default(); + for fund in deposit_value { + let exchange_rate = query_exchange_rate(deps, fund.denom.clone(), app)?; + funds.add(fund.clone())?; + total_value += fund.amount * exchange_rate; + } + Ok::<_, AppError>(()) + })?; + + Ok(AssetsBalanceResponse { + balances: funds.into(), + total_value, + }) + } + + /// Returns the current status of the full strategy. It returns shares reflecting the underlying positions + pub fn query_current_status(&self, deps: Deps, app: &App) -> AppResult { + let all_strategy_values = self + .0 + .iter() + .map(|s| s.query_current_value(deps, app)) + .collect::, _>>()?; + + let all_strategies_value: Uint128 = + all_strategy_values.iter().map(|(value, _)| value).sum(); + + // If there is no value, the current status is the stored strategy + if all_strategies_value.is_zero() { + return Ok(self.clone()); + } + + // Finally, we dispatch the total_value to get investment shares + Ok(BalanceStrategy( + self.0 + .iter() + .zip(all_strategy_values) + .map( + |(original_strategy, (value, shares))| BalanceStrategyElement { + yield_source: YieldSource { + asset_distribution: shares, + ty: original_strategy.yield_source.ty.clone(), + }, + share: Decimal::from_ratio(value, all_strategies_value), + }, + ) + .collect(), + )) + } + + /// This function applies the underlying shares inside yield sources to each yield source depending on the current strategy state + pub fn apply_current_strategy_shares(&mut self, deps: Deps, app: &App) -> AppResult<()> { + self.0.iter_mut().try_for_each(|yield_source| { + match yield_source.yield_source.ty.share_type() { + crate::yield_sources::ShareType::Dynamic => { + let (_total_value, shares) = yield_source.query_current_value(deps, app)?; + yield_source.yield_source.asset_distribution = shares; + } + crate::yield_sources::ShareType::Fixed => {} + }; + + Ok::<_, AppError>(()) + })?; + Ok(()) + } +} + +impl BalanceStrategyElement { + /// Queries the current value distribution of a registered strategy + /// If there is no deposit or the query for the user deposit value fails + /// the function returns 0 value with the registered asset distribution + pub fn query_current_value( + &self, + deps: Deps, + app: &App, + ) -> AppResult<(Uint128, Vec)> { + // If there is no deposit + let user_deposit = match self.yield_source.ty.user_deposit(deps, app) { + Ok(deposit) => deposit, + Err(_) => { + return Ok(( + Uint128::zero(), + self.yield_source.asset_distribution.clone(), + )) + } + }; + + // From this, we compute the shares within the investment + let each_value = user_deposit .iter() - .map(|balance| { - let exchange_rate = query_exchange_rate(deps, &balance.denom, app)?; + .map(|fund| { + let exchange_rate = query_exchange_rate(deps, fund.denom.clone(), app)?; + + Ok::<_, AppError>((fund.denom.clone(), exchange_rate * fund.amount)) + }) + .collect::, _>>()?; + + let total_value: Uint128 = each_value.iter().map(|(_denom, amount)| amount).sum(); + + // If there is no value, the current status is the stored strategy + if total_value.is_zero() { + return Ok(( + Uint128::zero(), + self.yield_source.asset_distribution.clone(), + )); + } - Ok::<_, AppError>(exchange_rate * balance.amount) + let each_shares = each_value + .into_iter() + .map(|(denom, amount)| AssetShare { + denom, + share: Decimal::from_ratio(amount, total_value), }) - .sum() + .collect(); + Ok((total_value, each_shares)) } } diff --git a/contracts/carrot-app/src/distribution/rebalance.rs b/contracts/carrot-app/src/distribution/rebalance.rs new file mode 100644 index 00000000..08f0ab9d --- /dev/null +++ b/contracts/carrot-app/src/distribution/rebalance.rs @@ -0,0 +1,8 @@ +use crate::yield_sources::BalanceStrategy; + +/// In order to re-balance the strategies, we need in order : +/// 1. Withdraw from the strategies that will be deleted +/// 2. Compute the total value that should land in each strategy +/// 3. Withdraw from strategies that have too much value +/// 4. Deposit all the withdrawn funds into the strategies to match the target. +impl BalanceStrategy {} diff --git a/contracts/carrot-app/src/distribution/withdraw.rs b/contracts/carrot-app/src/distribution/withdraw.rs index 428d5b03..3e3e854b 100644 --- a/contracts/carrot-app/src/distribution/withdraw.rs +++ b/contracts/carrot-app/src/distribution/withdraw.rs @@ -1,10 +1,13 @@ use abstract_sdk::{AccountAction, Execution, ExecutorMsg}; -use cosmwasm_std::{Decimal, Deps}; +use cosmwasm_std::{Coin, Decimal, Deps}; use crate::{ contract::{App, AppResult}, error::AppError, - yield_sources::BalanceStrategy, + handlers::query::query_all_exchange_rates, + helpers::compute_total_value, + msg::AssetsBalanceResponse, + yield_sources::{BalanceStrategy, BalanceStrategyElement}, }; impl BalanceStrategy { @@ -16,25 +19,56 @@ impl BalanceStrategy { ) -> AppResult> { self.0 .into_iter() - .map(|s| { - let this_withdraw_amount = withdraw_share - .map(|share| { - let this_amount = s.yield_source.ty.user_liquidity(deps, app)?; - let this_withdraw_amount = share * this_amount; + .map(|s| s.withdraw(deps, withdraw_share, app)) + .collect() + } +} - Ok::<_, AppError>(this_withdraw_amount) - }) - .transpose()?; - let raw_msg = s - .yield_source - .ty - .withdraw(deps, this_withdraw_amount, app)?; +impl BalanceStrategyElement { + pub fn withdraw( + self, + deps: Deps, + withdraw_share: Option, + app: &App, + ) -> AppResult { + let this_withdraw_amount = withdraw_share + .map(|share| { + let this_amount = self.yield_source.ty.user_liquidity(deps, app)?; + let this_withdraw_amount = share * this_amount; - Ok::<_, AppError>( - app.executor(deps) - .execute(vec![AccountAction::from_vec(raw_msg)])?, - ) + Ok::<_, AppError>(this_withdraw_amount) }) - .collect() + .transpose()?; + let raw_msg = self + .yield_source + .ty + .withdraw(deps, this_withdraw_amount, app)?; + + Ok::<_, AppError>( + app.executor(deps) + .execute(vec![AccountAction::from_vec(raw_msg)])?, + ) + } + + pub fn withdraw_preview( + &self, + deps: Deps, + withdraw_share: Option, + app: &App, + ) -> AppResult> { + let current_deposit = self.yield_source.ty.user_deposit(deps, app)?; + let exchange_rates = + query_all_exchange_rates(deps, current_deposit.iter().map(|f| f.denom.clone()), app)?; + if let Some(share) = withdraw_share { + Ok(current_deposit + .into_iter() + .map(|funds| Coin { + denom: funds.denom, + amount: funds.amount * share, + }) + .collect()) + } else { + Ok(current_deposit) + } } } diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index 5a8b768c..8277c142 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -2,20 +2,21 @@ use crate::{ contract::{App, AppResult}, error::AppError, handlers::query::query_balance, - helpers::assert_contract, + helpers::{assert_contract, compute_value}, msg::{AppExecuteMsg, ExecuteMsg, InternalExecuteMsg}, state::{AUTOCOMPOUND_STATE, CONFIG}, - yield_sources::{BalanceStrategy, ExpectedToken}, + yield_sources::{AssetShare, BalanceStrategy, BalanceStrategyElement}, }; use abstract_app::abstract_sdk::features::AbstractResponse; use abstract_sdk::ExecutorMsg; use cosmwasm_std::{ - to_json_binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Uint128, WasmMsg, + to_json_binary, Coin, Coins, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Uint128, + WasmMsg, }; use super::{ internal::{deposit_one_strategy, execute_finalize_deposit, execute_one_deposit_step}, - query::{query_strategy, query_strategy_target}, + query::query_strategy, }; pub fn execute_handler( @@ -45,32 +46,16 @@ pub fn execute_handler( swap_strategy, yield_type, yield_index, - } => deposit_one_strategy( - deps, - env, - info, - swap_strategy, - yield_index, - yield_type, - app, - ), + } => deposit_one_strategy(deps, env, swap_strategy, yield_index, yield_type, app), InternalExecuteMsg::ExecuteOneDepositSwapStep { asset_in, denom_out, expected_amount, - } => execute_one_deposit_step( - deps, - env, - info, - asset_in, - denom_out, - expected_amount, - app, - ), + } => execute_one_deposit_step(deps, env, asset_in, denom_out, expected_amount, app), InternalExecuteMsg::FinalizeDeposit { yield_type, yield_index, - } => execute_finalize_deposit(deps, env, info, yield_type, yield_index, app), + } => execute_finalize_deposit(deps, yield_type, yield_index, app), } } } @@ -81,7 +66,7 @@ fn deposit( env: Env, info: MessageInfo, funds: Vec, - yield_source_params: Option>>>, + yield_source_params: Option>>>, app: App, ) -> AppResult { // Only the admin (manager contracts or account owner) can deposit as well as the contract itself @@ -89,7 +74,9 @@ fn deposit( .assert_admin(deps.as_ref(), &info.sender) .or(assert_contract(&info, &env))?; - let deposit_msgs = _inner_deposit(deps.as_ref(), &env, funds, yield_source_params, &app)?; + // let deposit_msgs = _inner_deposit(deps.as_ref(), &env, funds, yield_source_params, &app)?; + let deposit_msgs = + _inner_advanced_deposit(deps.as_ref(), &env, funds, yield_source_params, &app)?; Ok(app.response("deposit").add_messages(deposit_msgs)) } @@ -201,11 +188,12 @@ pub fn _inner_deposit( deps: Deps, env: &Env, funds: Vec, - yield_source_params: Option>>>, + yield_source_params: Option>>>, app: &App, ) -> AppResult> { // We query the target strategy depending on the existing deposits - let mut current_strategy_status = query_strategy_target(deps, app)?.strategy; + let mut current_strategy_status = query_strategy(deps)?.strategy; + current_strategy_status.apply_current_strategy_shares(deps, app)?; // We correct it if the user asked to correct the share parameters of each strategy current_strategy_status.correct_with(yield_source_params); @@ -214,6 +202,98 @@ pub fn _inner_deposit( current_strategy_status.fill_all_and_get_messages(deps, env, funds, app) } +pub fn _inner_advanced_deposit( + deps: Deps, + env: &Env, + mut funds: Vec, + yield_source_params: Option>>>, + app: &App, +) -> AppResult> { + // This is the storage strategy for all assets + let target_strategy = query_strategy(deps)?.strategy; + + // This is the current distribution of funds inside the strategies + let current_distribution = target_strategy.query_current_status(deps, app)?; + let total_value = target_strategy.current_balance(deps, app)?.total_value; + let deposit_value = compute_value(deps, &funds, app)?; + + if deposit_value.is_zero() { + // We are trying to deposit no value, so we just don't do anything + return Ok(vec![]); + } + + // We create the strategy so that he final distribution is as close to the target strategy as possible + // 1. For all strategies, we withdraw some if its value is too high above target_strategy + let mut withdraw_funds = Coins::default(); + let mut withdraw_value = Uint128::zero(); + let mut withdraw_msgs = vec![]; + + // All strategies have to be reviewed + // EITHER of those are true : + // - The yield source has too much funds deposited and some should be withdrawn + // OR + // - Some funds need to be deposited into the strategy + let mut this_deposit_strategy: BalanceStrategy = target_strategy + .0 + .iter() + .zip(current_distribution.0) + .map(|(target, current)| { + // We need to take into account the total value added by the current shares + + let value_now = current.share * total_value; + let target_value = target.share * (total_value + deposit_value); + + // If value now is greater than the target value, we need to withdraw some funds from the protocol + if target_value < value_now { + let this_withdraw_value = target_value - value_now; + // In the following line, total_value can't be zero, otherwise the if condition wouldn't be met + let this_withdraw_share = Decimal::from_ratio(withdraw_value, total_value); + let this_withdraw_funds = + current.withdraw_preview(deps, Some(this_withdraw_share), app)?; + withdraw_value += this_withdraw_value; + for fund in this_withdraw_funds { + withdraw_funds.add(fund)?; + } + withdraw_msgs.push( + current + .withdraw(deps, Some(this_withdraw_share), app)? + .into(), + ); + + // In case there is a withdraw from the strategy, we don't need to deposit into this strategy after ! + Ok::<_, AppError>(None) + } else { + // In case we don't withdraw anything, it means we might deposit. + // Total should sum to one ! + let share = Decimal::from_ratio(target_value - value_now, deposit_value); + + Ok(Some(BalanceStrategyElement { + yield_source: target.yield_source.clone(), + share, + })) + } + }) + .collect::, _>>()? + .into_iter() + .flatten() + .collect::>() + .into(); + + // We add the withdrawn funds to the deposit funds + funds.extend(withdraw_funds); + + // We query the yield source shares + this_deposit_strategy.apply_current_strategy_shares(deps, app)?; + + // We correct it if the user asked to correct the share parameters of each strategy + this_deposit_strategy.correct_with(yield_source_params); + + // We fill the strategies with the current deposited funds and get messages to execute those deposits + let deposit_msgs = this_deposit_strategy.fill_all_and_get_messages(deps, env, funds, app)?; + + Ok([withdraw_msgs, deposit_msgs].concat()) +} + fn _inner_withdraw( deps: DepsMut, _env: &Env, @@ -224,12 +304,11 @@ fn _inner_withdraw( let withdraw_share = value .map(|value| { let total_deposit = query_balance(deps.as_ref(), app)?; - let total_value = total_deposit.value(deps.as_ref(), app)?; - if total_value.is_zero() { + if total_deposit.total_value.is_zero() { return Err(AppError::NoDeposit {}); } - Ok(Decimal::from_ratio(value, total_value)) + Ok(Decimal::from_ratio(value, total_deposit.total_value)) }) .transpose()?; diff --git a/contracts/carrot-app/src/handlers/instantiate.rs b/contracts/carrot-app/src/handlers/instantiate.rs index 2ad842ac..ae3a97c9 100644 --- a/contracts/carrot-app/src/handlers/instantiate.rs +++ b/contracts/carrot-app/src/handlers/instantiate.rs @@ -1,7 +1,8 @@ use crate::{ + autocompound::AutocompoundState, contract::{App, AppResult}, msg::AppInstantiateMsg, - state::{AutocompoundState, Config, AUTOCOMPOUND_STATE, CONFIG}, + state::{Config, AUTOCOMPOUND_STATE, CONFIG}, }; use abstract_app::abstract_sdk::{features::AbstractNameService, AbstractResponse}; use cosmwasm_std::{DepsMut, Env, MessageInfo}; diff --git a/contracts/carrot-app/src/handlers/internal.rs b/contracts/carrot-app/src/handlers/internal.rs index 26fa3c9f..e5a85783 100644 --- a/contracts/carrot-app/src/handlers/internal.rs +++ b/contracts/carrot-app/src/handlers/internal.rs @@ -12,7 +12,7 @@ use crate::{ use abstract_app::{abstract_sdk::features::AbstractResponse, objects::AnsAsset}; use abstract_dex_adapter::DexInterface; use abstract_sdk::features::AbstractNameService; -use cosmwasm_std::{wasm_execute, Coin, DepsMut, Env, MessageInfo, StdError, SubMsg, Uint128}; +use cosmwasm_std::{wasm_execute, Coin, DepsMut, Env, StdError, SubMsg, Uint128}; use cw_asset::AssetInfo; use super::query::query_exchange_rate; @@ -20,7 +20,6 @@ use super::query::query_exchange_rate; pub fn deposit_one_strategy( deps: DepsMut, env: Env, - _info: MessageInfo, strategy: OneDepositStrategy, yield_index: usize, yield_type: YieldType, @@ -88,7 +87,6 @@ pub fn deposit_one_strategy( pub fn execute_one_deposit_step( deps: DepsMut, _env: Env, - _info: MessageInfo, asset_in: Coin, denom_out: String, expected_amount: Uint128, @@ -124,8 +122,6 @@ pub fn execute_one_deposit_step( pub fn execute_finalize_deposit( deps: DepsMut, - env: Env, - _info: MessageInfo, yield_type: YieldType, yield_index: usize, app: App, @@ -134,7 +130,7 @@ pub fn execute_finalize_deposit( TEMP_CURRENT_YIELD.save(deps.storage, &yield_index)?; - let msgs = yield_type.deposit(deps.as_ref(), &env, available_deposit_coins, &app)?; + let msgs = yield_type.deposit(deps.as_ref(), available_deposit_coins, &app)?; Ok(app.response("one-deposit-step").add_submessages(msgs)) } diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index 50a0be46..1c8ac4da 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -9,7 +9,8 @@ use abstract_dex_adapter::DexInterface; use cosmwasm_std::{to_json_binary, Binary, Coins, Decimal, Deps, Env, Uint128}; use cw_asset::Asset; -use crate::yield_sources::{BalanceStrategy, BalanceStrategyElement, ExpectedToken, YieldSource}; +use crate::autocompound::get_autocompound_status; +use crate::yield_sources::BalanceStrategy; use crate::{ contract::{App, AppResult}, error::AppError, @@ -18,7 +19,7 @@ use crate::{ AppQueryMsg, AssetsBalanceResponse, AvailableRewardsResponse, CompoundStatusResponse, StrategyResponse, }, - state::{get_autocompound_status, Config, CONFIG}, + state::{Config, CONFIG}, }; pub fn query_handler(deps: Deps, env: Env, app: &App, msg: AppQueryMsg) -> AppResult { @@ -92,111 +93,14 @@ pub fn query_strategy(deps: Deps) -> AppResult { }) } -// Returns the target strategy for strategies -// This includes querying the dynamic strategy if specified in the strategy options -// This allows querying what actually needs to be deposited inside the strategy -pub fn query_strategy_target(deps: Deps, app: &App) -> AppResult { - let strategy = query_strategy(deps)?.strategy; - - Ok(StrategyResponse { - strategy: BalanceStrategy( - strategy - .0 - .into_iter() - .map(|mut yield_source| { - let shares = match yield_source.yield_source.ty.share_type() { - crate::yield_sources::ShareType::Dynamic => { - let (_total_value, shares) = - query_dynamic_source_value(deps, &yield_source, app)?; - shares - } - crate::yield_sources::ShareType::Fixed => { - yield_source.yield_source.asset_distribution - } - }; - - yield_source.yield_source.asset_distribution = shares; - - Ok::<_, AppError>(yield_source) - }) - .collect::, _>>()?, - ), - }) -} - -/// Returns the current status of the full strategy. It returns shares reflecting the underlying positions pub fn query_strategy_status(deps: Deps, app: &App) -> AppResult { let strategy = query_strategy(deps)?.strategy; - // We get the value for each investment and the shares within that investment - let all_strategy_values = query_strategy(deps)? - .strategy - .0 - .iter() - .map(|s| query_dynamic_source_value(deps, s, app)) - .collect::, _>>()?; - - let all_strategies_value: Uint128 = all_strategy_values.iter().map(|(value, _)| value).sum(); - - // Finally, we dispatch the total_value to get investment shares Ok(StrategyResponse { - strategy: BalanceStrategy( - strategy - .0 - .into_iter() - .zip(all_strategy_values) - .map( - |(original_strategy, (value, shares))| BalanceStrategyElement { - yield_source: YieldSource { - asset_distribution: shares, - ty: original_strategy.yield_source.ty, - }, - share: Decimal::from_ratio(value, all_strategies_value), - }, - ) - .collect(), - ), + strategy: strategy.query_current_status(deps, app)?, }) } -fn query_dynamic_source_value( - deps: Deps, - yield_source: &BalanceStrategyElement, - app: &App, -) -> AppResult<(Uint128, Vec)> { - // If there is no deposit - let user_deposit = match yield_source.yield_source.ty.user_deposit(deps, app) { - Ok(deposit) => deposit, - Err(_) => { - return Ok(( - Uint128::zero(), - yield_source.yield_source.asset_distribution.clone(), - )) - } - }; - - // From this, we compute the shares within the investment - let each_value = user_deposit - .iter() - .map(|fund| { - let exchange_rate = query_exchange_rate(deps, fund.denom.clone(), app)?; - - Ok::<_, AppError>((fund.denom.clone(), exchange_rate * fund.amount)) - }) - .collect::, _>>()?; - - let total_value: Uint128 = each_value.iter().map(|(_denom, amount)| amount).sum(); - - let each_shares = each_value - .into_iter() - .map(|(denom, amount)| ExpectedToken { - denom, - share: Decimal::from_ratio(amount, total_value), - }) - .collect::>(); - Ok((total_value, each_shares)) -} - fn query_config(deps: Deps) -> AppResult { Ok(CONFIG.load(deps.storage)?) } diff --git a/contracts/carrot-app/src/helpers.rs b/contracts/carrot-app/src/helpers.rs index 9ab1ed2c..99250513 100644 --- a/contracts/carrot-app/src/helpers.rs +++ b/contracts/carrot-app/src/helpers.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use crate::contract::{App, AppResult}; use crate::error::AppError; +use crate::handlers::query::query_exchange_rate; use abstract_app::traits::AccountIdentification; use abstract_app::{objects::AssetEntry, traits::AbstractNameService}; use abstract_sdk::Resolve; @@ -62,6 +63,16 @@ pub fn compute_total_value( .sum() } +pub fn compute_value(deps: Deps, funds: &[Coin], app: &App) -> AppResult { + funds + .iter() + .map(|c| { + let exchange_rate = query_exchange_rate(deps, c.denom.clone(), app)?; + Ok(c.amount * exchange_rate) + }) + .sum() +} + #[cfg(test)] mod test { use std::str::FromStr; diff --git a/contracts/carrot-app/src/lib.rs b/contracts/carrot-app/src/lib.rs index 048056c6..06d7b3d6 100644 --- a/contracts/carrot-app/src/lib.rs +++ b/contracts/carrot-app/src/lib.rs @@ -1,3 +1,4 @@ +pub mod autocompound; pub mod contract; pub mod distribution; pub mod error; diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index a773c97b..c90c8d06 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -3,10 +3,10 @@ use cosmwasm_std::{Coin, Uint128, Uint64}; use cw_asset::AssetBase; use crate::{ + autocompound::AutocompoundConfig, contract::App, distribution::deposit::OneDepositStrategy, - state::AutocompoundConfig, - yield_sources::{yield_type::YieldType, BalanceStrategy, ExpectedToken}, + yield_sources::{yield_type::YieldType, AssetShare, BalanceStrategy}, }; // This is used for type safety and re-exporting the contract endpoint structs. @@ -41,8 +41,8 @@ pub enum AppExecuteMsg { /// This is not used for a first deposit into a strategy that hasn't changed for instance /// This is an options because this is not mandatory /// The vector then has option inside of it because we might not want to change parameters for all strategies - /// We might not use a vector but use a (usize, Vec) instead to avoid having to pass a full vector everytime - yield_sources_params: Option>>>, + /// We might not use a vector but use a (usize, Vec) instead to avoid having to pass a full vector everytime + yield_sources_params: Option>>>, }, /// Partial withdraw of the funds available on the app /// If amount is omitted, withdraws everything that is on the app diff --git a/contracts/carrot-app/src/state.rs b/contracts/carrot-app/src/state.rs index 648b744e..0aea9329 100644 --- a/contracts/carrot-app/src/state.rs +++ b/contracts/carrot-app/src/state.rs @@ -1,18 +1,9 @@ -use abstract_app::abstract_sdk::{feature_objects::AnsHost, Resolve}; -use abstract_app::objects::AnsAsset; -use abstract_app::{abstract_core::objects::AssetEntry, objects::DexAssetPairing}; -use abstract_dex_adapter::DexInterface; -use abstract_sdk::{Execution, TransferInterface}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ - ensure, Addr, Coin, CosmosMsg, Deps, Env, MessageInfo, Storage, Timestamp, Uint128, Uint64, -}; +use cosmwasm_std::{Addr, Coin, Uint128}; use cw_storage_plus::Item; -use crate::contract::App; -use crate::handlers::swap_helpers::swap_msg; +use crate::autocompound::{AutocompoundConfig, AutocompoundState}; use crate::yield_sources::BalanceStrategy; -use crate::{contract::AppResult, error::AppError, msg::CompoundStatus}; pub const CONFIG: Item = Item::new("config"); pub const AUTOCOMPOUND_STATE: Item = Item::new("position"); @@ -30,168 +21,3 @@ pub struct Config { pub autocompound_config: AutocompoundConfig, pub dex: String, } - -/// General auto-compound parameters. -/// Includes the cool down and the technical funds config -#[cw_serde] -pub struct AutocompoundConfig { - /// Seconds to wait before autocompound is incentivized. - /// Allows the user to configure when the auto-compound happens - pub cooldown_seconds: Uint64, - /// Configuration of rewards to the address who helped to execute autocompound - pub rewards: AutocompoundRewardsConfig, -} - -impl Config { - pub fn get_executor_reward_messages( - &self, - deps: Deps, - env: &Env, - info: MessageInfo, - app: &App, - ) -> AppResult> { - Ok( - // If called by non-admin and reward cooldown has ended, send rewards to the contract caller. - if !app.admin.is_admin(deps, &info.sender)? - && get_autocompound_status( - deps.storage, - env, - self.autocompound_config.cooldown_seconds.u64(), - )? - .is_ready() - { - self.autocompound_executor_rewards(deps, env, &info.sender, app)? - } else { - vec![] - }, - ) - } - pub fn autocompound_executor_rewards( - &self, - deps: Deps, - env: &Env, - executor: &Addr, - app: &App, - ) -> AppResult> { - let rewards_config = self.autocompound_config.rewards.clone(); - - // Get user balance of gas denom - let user_gas_balance = app.bank(deps).balance(&rewards_config.gas_asset)?.amount; - - let mut rewards_messages = vec![]; - - // If not enough gas coins - swap for some amount - if user_gas_balance < rewards_config.min_gas_balance { - // Get asset entries - let dex = app.ans_dex(deps, self.dex.to_string()); - - // Do reverse swap to find approximate amount we need to swap - let need_gas_coins = rewards_config.max_gas_balance - user_gas_balance; - let simulate_swap_response = dex.simulate_swap( - AnsAsset::new(rewards_config.gas_asset.clone(), need_gas_coins), - rewards_config.swap_asset.clone(), - )?; - - // Get user balance of swap denom - let user_swap_balance = app.bank(deps).balance(&rewards_config.swap_asset)?.amount; - - // Swap as much as available if not enough for max_gas_balance - let swap_amount = simulate_swap_response.return_amount.min(user_swap_balance); - - let msgs = swap_msg( - deps, - env, - AnsAsset::new(rewards_config.swap_asset, swap_amount), - rewards_config.gas_asset.clone(), - app, - )?; - rewards_messages.extend(msgs); - } - - // We send their reward to the executor - let msg_send = app.bank(deps).transfer( - vec![AnsAsset::new( - rewards_config.gas_asset, - rewards_config.reward, - )], - executor, - )?; - - rewards_messages.push(app.executor(deps).execute(vec![msg_send])?.into()); - - Ok(rewards_messages) - } -} - -/// Configuration on how rewards should be distributed -/// to the address who helped to execute autocompound -#[cw_serde] -pub struct AutocompoundRewardsConfig { - /// Gas denominator for this chain - pub gas_asset: AssetEntry, - /// Denominator of the asset that will be used for swap to the gas asset - pub swap_asset: AssetEntry, - /// Reward amount - pub reward: Uint128, - /// If gas token balance falls below this bound a swap will be generated - pub min_gas_balance: Uint128, - /// Upper bound of gas tokens expected after the swap - pub max_gas_balance: Uint128, -} - -impl AutocompoundRewardsConfig { - pub fn check(&self, deps: Deps, dex_name: &str, ans_host: &AnsHost) -> AppResult<()> { - ensure!( - self.reward <= self.min_gas_balance, - AppError::RewardConfigError( - "reward should be lower or equal to the min_gas_balance".to_owned() - ) - ); - ensure!( - self.max_gas_balance > self.min_gas_balance, - AppError::RewardConfigError( - "max_gas_balance has to be bigger than min_gas_balance".to_owned() - ) - ); - - // Check swap asset has pairing into gas asset - DexAssetPairing::new(self.gas_asset.clone(), self.swap_asset.clone(), dex_name) - .resolve(&deps.querier, ans_host)?; - - Ok(()) - } -} - -#[cw_serde] -pub struct PoolConfig { - pub pool_id: u64, - pub token0: String, - pub token1: String, - pub asset0: AssetEntry, - pub asset1: AssetEntry, -} - -#[cw_serde] -pub struct AutocompoundState { - pub last_compound: Timestamp, -} - -pub fn get_autocompound_status( - storage: &dyn Storage, - env: &Env, - cooldown_seconds: u64, -) -> AppResult { - let position = AUTOCOMPOUND_STATE.may_load(storage)?; - let status = match position { - Some(position) => { - let ready_on = position.last_compound.plus_seconds(cooldown_seconds); - if env.block.time >= ready_on { - CompoundStatus::Ready {} - } else { - CompoundStatus::Cooldown((env.block.time.seconds() - ready_on.seconds()).into()) - } - } - None => CompoundStatus::NoPosition {}, - }; - Ok(status) -} diff --git a/contracts/carrot-app/src/yield_sources.rs b/contracts/carrot-app/src/yield_sources.rs index 2192c141..8c76727a 100644 --- a/contracts/carrot-app/src/yield_sources.rs +++ b/contracts/carrot-app/src/yield_sources.rs @@ -10,6 +10,7 @@ use crate::{ contract::{App, AppResult}, error::AppError, helpers::close_to, + yield_sources::yield_type::YieldTypeImplementation, }; use abstract_app::traits::AbstractNameService; @@ -20,8 +21,7 @@ use self::yield_type::YieldType; /// A type that allows routing to the right smart-contract integration internally #[cw_serde] pub struct YieldSource { - /// This id (denom, share) - pub asset_distribution: Vec, + pub asset_distribution: Vec, pub ty: YieldType, } @@ -33,6 +33,7 @@ impl YieldSource { close_to(Decimal::one(), share_sum), AppError::InvalidStrategySum { share_sum } ); + // We make sure that assets are associated with this strategy ensure!( !self.asset_distribution.is_empty(), AppError::InvalidEmptyStrategy {} @@ -61,7 +62,7 @@ impl YieldSource { ); params.check(deps)?; } - YieldType::Mars(denom) => { + YieldType::Mars(params) => { // We verify there is only one element in the shares vector ensure_eq!( self.asset_distribution.len(), @@ -70,10 +71,11 @@ impl YieldSource { ); // We verify the first element correspond to the mars deposit denom ensure_eq!( - &self.asset_distribution[0].denom, - denom, + self.asset_distribution[0].denom, + params.denom, AppError::InvalidStrategy {} ); + params.check(deps)?; } } @@ -88,8 +90,9 @@ impl YieldSource { } } +/// This is used to express a share of tokens inside a strategy #[cw_serde] -pub struct ExpectedToken { +pub struct AssetShare { pub denom: String, pub share: Decimal, } @@ -102,21 +105,11 @@ pub enum ShareType { Fixed, } -// Related to balance strategies +// This represents a balance strategy +// This object is used for storing the current strategy, retrieving the actual strategy status or expressing a target strategy when depositing #[cw_serde] pub struct BalanceStrategy(pub Vec); -#[cw_serde] -pub struct BalanceStrategyElement { - pub yield_source: YieldSource, - pub share: Decimal, -} -impl BalanceStrategyElement { - pub fn check(&self, deps: Deps, app: &App) -> AppResult<()> { - self.yield_source.check(deps, app) - } -} - impl BalanceStrategy { pub fn check(&self, deps: Deps, app: &App) -> AppResult<()> { // First we check the share sums the 100 @@ -143,3 +136,20 @@ impl BalanceStrategy { .collect() } } + +impl From> for BalanceStrategy { + fn from(value: Vec) -> Self { + Self(value) + } +} + +#[cw_serde] +pub struct BalanceStrategyElement { + pub yield_source: YieldSource, + pub share: Decimal, +} +impl BalanceStrategyElement { + pub fn check(&self, deps: Deps, app: &App) -> AppResult<()> { + self.yield_source.check(deps, app) + } +} diff --git a/contracts/carrot-app/src/yield_sources/mars.rs b/contracts/carrot-app/src/yield_sources/mars.rs index 1a0ab82f..ccba3b74 100644 --- a/contracts/carrot-app/src/yield_sources/mars.rs +++ b/contracts/carrot-app/src/yield_sources/mars.rs @@ -3,76 +3,86 @@ use abstract_app::traits::AccountIdentification; use abstract_app::{objects::AnsAsset, traits::AbstractNameService}; use abstract_money_market_adapter::msg::MoneyMarketQueryMsg; use abstract_money_market_adapter::MoneyMarketInterface; -use cosmwasm_std::{Coin, CosmosMsg, Deps, SubMsg, Uint128}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{coins, Coin, CosmosMsg, Deps, SubMsg, Uint128}; use cw_asset::AssetInfo; use abstract_money_market_standard::query::MoneyMarketAnsQuery; -pub const MARS_MONEY_MARKET: &str = "mars"; +use super::yield_type::YieldTypeImplementation; +use super::ShareType; -pub fn deposit(deps: Deps, denom: String, amount: Uint128, app: &App) -> AppResult> { - let ans = app.name_service(deps); - let ans_fund = ans.query(&AssetInfo::native(denom))?; +pub const MARS_MONEY_MARKET: &str = "mars"; - Ok(vec![SubMsg::new( - app.ans_money_market(deps, MARS_MONEY_MARKET.to_string()) - .deposit(AnsAsset::new(ans_fund, amount))?, - )]) +#[cw_serde] +pub struct MarsDepositParams { + pub denom: String, } +impl YieldTypeImplementation for MarsDepositParams { + fn deposit(self, deps: Deps, funds: Vec, app: &App) -> AppResult> { + let ans = app.name_service(deps); + let ans_fund = ans.query(&AssetInfo::native(self.denom))?; -pub fn withdraw( - deps: Deps, - denom: String, - amount: Option, - app: &App, -) -> AppResult> { - let ans = app.name_service(deps); - - let amount = if let Some(amount) = amount { - amount - } else { - user_deposit(deps, denom.clone(), app)? - }; - - let ans_fund = ans.query(&AssetInfo::native(denom))?; - - Ok(vec![app - .ans_money_market(deps, MARS_MONEY_MARKET.to_string()) - .withdraw(AnsAsset::new(ans_fund, amount))?]) -} + Ok(vec![SubMsg::new( + app.ans_money_market(deps, MARS_MONEY_MARKET.to_string()) + .deposit(AnsAsset::new(ans_fund, funds[0].amount))?, + )]) + } -pub fn withdraw_rewards( - _deps: Deps, - _denom: String, - _app: &App, -) -> AppResult<(Vec, Vec)> { - // Mars doesn't have rewards, it's automatically auto-compounded - Ok((vec![], vec![])) -} + fn withdraw(self, deps: Deps, amount: Option, app: &App) -> AppResult> { + let ans = app.name_service(deps); -pub fn user_deposit(deps: Deps, denom: String, app: &App) -> AppResult { - let ans = app.name_service(deps); - let asset = ans.query(&AssetInfo::native(denom))?; - let user = app.account_base(deps)?.proxy; - - Ok(app - .ans_money_market(deps, MARS_MONEY_MARKET.to_string()) - .query(MoneyMarketQueryMsg::MoneyMarketAnsQuery { - query: MoneyMarketAnsQuery::UserDeposit { - user: user.to_string(), - asset, - }, - money_market: MARS_MONEY_MARKET.to_string(), - })?) -} + let amount = if let Some(amount) = amount { + amount + } else { + self.user_deposit(deps, app)?[0].amount + }; -/// Returns an amount representing a user's liquidity -pub fn user_liquidity(deps: Deps, denom: String, app: &App) -> AppResult { - user_deposit(deps, denom, app) -} + let ans_fund = ans.query(&AssetInfo::native(self.denom))?; + + Ok(vec![app + .ans_money_market(deps, MARS_MONEY_MARKET.to_string()) + .withdraw(AnsAsset::new(ans_fund, amount))?]) + } + + fn withdraw_rewards(self, _deps: Deps, _app: &App) -> AppResult<(Vec, Vec)> { + // Mars doesn't have rewards, it's automatically auto-compounded + Ok((vec![], vec![])) + } + + fn user_deposit(&self, deps: Deps, app: &App) -> AppResult> { + let ans = app.name_service(deps); + let asset = ans.query(&AssetInfo::native(self.denom.clone()))?; + let user = app.account_base(deps)?.proxy; + + let deposit: Uint128 = app + .ans_money_market(deps, MARS_MONEY_MARKET.to_string()) + .query(MoneyMarketQueryMsg::MoneyMarketAnsQuery { + query: MoneyMarketAnsQuery::UserDeposit { + user: user.to_string(), + asset, + }, + money_market: MARS_MONEY_MARKET.to_string(), + })?; + + Ok(coins(deposit.u128(), self.denom.clone())) + } + + fn user_rewards(&self, _deps: Deps, _app: &App) -> AppResult> { + // No rewards, because mars is already auto-compounding + + Ok(vec![]) + } + + fn user_liquidity(&self, deps: Deps, app: &App) -> AppResult { + Ok(self.user_deposit(deps, app)?[0].amount) + } -pub fn user_rewards(_deps: Deps, _denom: String, _app: &App) -> AppResult> { - // No rewards, because mars is already auto-compounding + fn share_type(&self) -> super::ShareType { + ShareType::Fixed + } - Ok(vec![]) + fn check(&self, _deps: Deps) -> AppResult<()> { + Ok(()) + } } diff --git a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs index 1acc31de..a5d9f526 100644 --- a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs +++ b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs @@ -10,244 +10,247 @@ use crate::{ use abstract_app::{objects::AnsAsset, traits::AccountIdentification}; use abstract_dex_adapter::DexInterface; use abstract_sdk::Execution; -use cosmwasm_std::{ensure, Coin, Coins, CosmosMsg, Decimal, Deps, Env, ReplyOn, SubMsg, Uint128}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ensure, Coin, Coins, CosmosMsg, Decimal, Deps, ReplyOn, SubMsg, Uint128}; use osmosis_std::{ cosmwasm_to_proto_coins, try_proto_to_cosmwasm_coins, - types::osmosis::concentratedliquidity::v1beta1::{ - ConcentratedliquidityQuerier, FullPositionBreakdown, MsgAddToPosition, - MsgCollectIncentives, MsgCollectSpreadRewards, MsgCreatePosition, MsgWithdrawPosition, + types::osmosis::{ + concentratedliquidity::v1beta1::{ + ConcentratedliquidityQuerier, FullPositionBreakdown, MsgAddToPosition, + MsgCollectIncentives, MsgCollectSpreadRewards, MsgCreatePosition, MsgWithdrawPosition, + Pool, + }, + poolmanager::v1beta1::PoolmanagerQuerier, }, }; -use super::yield_type::ConcentratedPoolParams; - -/// This function creates a position for the user, -/// 1. Swap the indicated funds to match the asset0/asset1 ratio and deposit as much as possible in the pool for the given parameters -/// 2. Create a new position -/// 3. Store position id from create position response -/// -/// * `lower_tick` - Concentrated liquidity pool parameter -/// * `upper_tick` - Concentrated liquidity pool parameter -/// * `funds` - Funds that will be deposited from the user wallet directly into the pool. DO NOT SEND FUNDS TO THIS ENDPOINT -/// * `asset0` - The target amount of asset0.denom that the user will deposit inside the pool -/// * `asset1` - The target amount of asset1.denom that the user will deposit inside the pool -/// -/// asset0 and asset1 are only used in a ratio to each other. They are there to make sure that the deposited funds will ALL land inside the pool. -/// We don't use an asset ratio because either one of the amounts can be zero -/// See https://docs.osmosis.zone/osmosis-core/modules/concentrated-liquidity for more details -/// -fn create_position( - deps: Deps, - params: ConcentratedPoolParams, - funds: Vec, - app: &App, - // create_position_msg: CreatePositionMessage, -) -> AppResult> { - let proxy_addr = app.account_base(deps)?.proxy; - - // 2. Create a position - let tokens = cosmwasm_to_proto_coins(funds); - let msg = app.executor(deps).execute_with_reply_and_data( - MsgCreatePosition { - pool_id: params.pool_id, - sender: proxy_addr.to_string(), - lower_tick: params.lower_tick, - upper_tick: params.upper_tick, - tokens_provided: tokens, - token_min_amount0: "0".to_string(), - token_min_amount1: "0".to_string(), - } - .into(), - ReplyOn::Success, - OSMOSIS_CREATE_POSITION_REPLY_ID, - )?; - - Ok(vec![msg]) +use super::{yield_type::YieldTypeImplementation, ShareType}; + +#[cw_serde] +pub struct ConcentratedPoolParams { + // This is part of the pool parameters + pub pool_id: u64, + // This is part of the pool parameters + pub lower_tick: i64, + // This is part of the pool parameters + pub upper_tick: i64, + // This is something that is filled after position creation + // This is not actually a parameter but rather state + // This can be used as a parameter for existing positions + pub position_id: Option, } -fn raw_deposit( - deps: Deps, - funds: Vec, - app: &App, - position_id: u64, -) -> AppResult> { - let pool = get_osmosis_position_by_id(deps, position_id)?; - let position = pool.position.unwrap(); - - let proxy_addr = app.account_base(deps)?.proxy; - - // We need to make sure the amounts are in the right order - // We assume the funds vector has 2 coins associated - let (amount0, amount1) = match pool - .asset0 - .map(|c| c.denom == funds[0].denom) - .or(pool.asset1.map(|c| c.denom == funds[1].denom)) - { - Some(true) => (funds[0].amount, funds[1].amount), // we already had the right order - Some(false) => (funds[1].amount, funds[0].amount), // we had the wrong order - None => return Err(AppError::NoPosition {}), // A position has to exist in order to execute this function. This should be unreachable - }; +impl YieldTypeImplementation for ConcentratedPoolParams { + fn check(&self, deps: Deps) -> AppResult<()> { + let _pool: Pool = PoolmanagerQuerier::new(&deps.querier) + .pool(self.pool_id) + .map_err(|_| AppError::PoolNotFound {})? + .pool + .ok_or(AppError::PoolNotFound {})? + .try_into()?; + Ok(()) + } - let deposit_msg = app.executor(deps).execute_with_reply_and_data( - MsgAddToPosition { - position_id: position.position_id, - sender: proxy_addr.to_string(), - amount0: amount0.to_string(), - amount1: amount1.to_string(), - token_min_amount0: "0".to_string(), - token_min_amount1: "0".to_string(), + fn deposit(self, deps: Deps, funds: Vec, app: &App) -> AppResult> { + // We verify there is a position stored + if self.position(deps).is_ok() { + // We just deposit + self.raw_deposit(deps, funds, app) + } else { + // We need to create a position + self.create_position(deps, funds, app) } - .into(), - cosmwasm_std::ReplyOn::Success, - OSMOSIS_ADD_TO_POSITION_REPLY_ID, - )?; - - Ok(vec![deposit_msg]) -} - -pub fn deposit( - deps: Deps, - _env: &Env, - params: ConcentratedPoolParams, - funds: Vec, - app: &App, -) -> AppResult> { - // We verify there is a position stored - - let osmosis_position = params - .position_id - .map(|position_id| get_osmosis_position_by_id(deps, position_id)); + } - if let Some(Ok(_)) = osmosis_position { - // We just deposit - raw_deposit(deps, funds, app, params.position_id.unwrap()) - } else { - // We need to create a position - create_position(deps, params, funds, app) + fn withdraw(self, deps: Deps, amount: Option, app: &App) -> AppResult> { + let position = self.position(deps)?; + let position_details = position.position.unwrap(); + + let total_liquidity = position_details.liquidity.replace('.', ""); + + let liquidity_amount = if let Some(amount) = amount { + amount.to_string() + } else { + // TODO: it's decimals inside contracts + total_liquidity.clone() + }; + let user = app.account_base(deps)?.proxy; + + // We need to execute withdraw on the user's behalf + Ok(vec![MsgWithdrawPosition { + position_id: position_details.position_id, + sender: user.to_string(), + liquidity_amount: liquidity_amount.clone(), + } + .into()]) } -} -pub fn withdraw( - deps: Deps, - amount: Option, - app: &App, - params: ConcentratedPoolParams, -) -> AppResult> { - let position = - get_osmosis_position_by_id(deps, params.position_id.ok_or(AppError::NoPosition {})?)?; - let position_details = position.position.unwrap(); + fn withdraw_rewards(self, deps: Deps, app: &App) -> AppResult<(Vec, Vec)> { + let position = self.position(deps)?; + let position_details = position.position.unwrap(); + + let user = app.account_base(deps)?.proxy; + let mut rewards = Coins::default(); + let mut msgs: Vec = vec![]; + // If there are external incentives, claim them. + if !position.claimable_incentives.is_empty() { + for coin in try_proto_to_cosmwasm_coins(position.claimable_incentives)? { + rewards.add(coin)?; + } + msgs.push( + MsgCollectIncentives { + position_ids: vec![position_details.position_id], + sender: user.to_string(), + } + .into(), + ); + } - let total_liquidity = position_details.liquidity.replace('.', ""); + // If there is income from swap fees, claim them. + if !position.claimable_spread_rewards.is_empty() { + for coin in try_proto_to_cosmwasm_coins(position.claimable_spread_rewards)? { + rewards.add(coin)?; + } + msgs.push( + MsgCollectSpreadRewards { + position_ids: vec![position_details.position_id], + sender: position_details.address.clone(), + } + .into(), + ) + } - let liquidity_amount = if let Some(amount) = amount { - amount.to_string() - } else { - // TODO: it's decimals inside contracts - total_liquidity.clone() - }; - let user = app.account_base(deps)?.proxy; + Ok((rewards.to_vec(), msgs)) + } - // We need to execute withdraw on the user's behalf - Ok(vec![MsgWithdrawPosition { - position_id: position_details.position_id, - sender: user.to_string(), - liquidity_amount: liquidity_amount.clone(), + /// This may return 0, 1 or 2 elements depending on the position's status + fn user_deposit(&self, deps: Deps, _app: &App) -> AppResult> { + let position = self.position(deps)?; + + Ok([ + try_proto_to_cosmwasm_coins(position.asset0)?, + try_proto_to_cosmwasm_coins(position.asset1)?, + ] + .into_iter() + .flatten() + .collect()) } - .into()]) -} -pub fn withdraw_rewards( - deps: Deps, - params: ConcentratedPoolParams, - app: &App, -) -> AppResult<(Vec, Vec)> { - let position = - get_osmosis_position_by_id(deps, params.position_id.ok_or(AppError::NoPosition {})?)?; - let position_details = position.position.unwrap(); - - let user = app.account_base(deps)?.proxy; - let mut rewards = Coins::default(); - let mut msgs: Vec = vec![]; - // If there are external incentives, claim them. - if !position.claimable_incentives.is_empty() { + fn user_rewards(&self, deps: Deps, _app: &App) -> AppResult> { + let position = self.position(deps)?; + + let mut rewards = cosmwasm_std::Coins::default(); for coin in try_proto_to_cosmwasm_coins(position.claimable_incentives)? { rewards.add(coin)?; } - msgs.push( - MsgCollectIncentives { - position_ids: vec![position_details.position_id], - sender: user.to_string(), - } - .into(), - ); - } - // If there is income from swap fees, claim them. - if !position.claimable_spread_rewards.is_empty() { for coin in try_proto_to_cosmwasm_coins(position.claimable_spread_rewards)? { rewards.add(coin)?; } - msgs.push( - MsgCollectSpreadRewards { - position_ids: vec![position_details.position_id], - sender: position_details.address.clone(), - } - .into(), - ) + + Ok(rewards.into()) } - Ok((rewards.to_vec(), msgs)) -} + fn user_liquidity(&self, deps: Deps, _app: &App) -> AppResult { + let position = self.position(deps)?; + let total_liquidity = position.position.unwrap().liquidity.replace('.', ""); -/// This may return 0, 1 or 2 elements depending on the position's status -pub fn user_deposit( - deps: Deps, - _app: &App, - params: ConcentratedPoolParams, -) -> AppResult> { - let position = - get_osmosis_position_by_id(deps, params.position_id.ok_or(AppError::NoPosition {})?)?; - - Ok([ - try_proto_to_cosmwasm_coins(position.asset0)?, - try_proto_to_cosmwasm_coins(position.asset1)?, - ] - .into_iter() - .flatten() - .collect()) -} + Ok(Uint128::from_str(&total_liquidity)?) + } -/// Returns an amount representing a user's liquidity -pub fn user_liquidity( - deps: Deps, - _app: &App, - params: ConcentratedPoolParams, -) -> AppResult { - let position = - get_osmosis_position_by_id(deps, params.position_id.ok_or(AppError::NoPosition {})?)?; - let total_liquidity = position.position.unwrap().liquidity.replace('.', ""); - - Ok(Uint128::from_str(&total_liquidity)?) + fn share_type(&self) -> super::ShareType { + ShareType::Dynamic + } } -pub fn user_rewards( - deps: Deps, - _app: &App, - params: ConcentratedPoolParams, -) -> AppResult> { - let position = - get_osmosis_position_by_id(deps, params.position_id.ok_or(AppError::NoPosition {})?)?; - - let mut rewards = cosmwasm_std::Coins::default(); - for coin in try_proto_to_cosmwasm_coins(position.claimable_incentives)? { - rewards.add(coin)?; +impl ConcentratedPoolParams { + /// This function creates a position for the user, + /// 1. Swap the indicated funds to match the asset0/asset1 ratio and deposit as much as possible in the pool for the given parameters + /// 2. Create a new position + /// 3. Store position id from create position response + /// + /// * `lower_tick` - Concentrated liquidity pool parameter + /// * `upper_tick` - Concentrated liquidity pool parameter + /// * `funds` - Funds that will be deposited from the user wallet directly into the pool. DO NOT SEND FUNDS TO THIS ENDPOINT + /// * `asset0` - The target amount of asset0.denom that the user will deposit inside the pool + /// * `asset1` - The target amount of asset1.denom that the user will deposit inside the pool + /// + /// asset0 and asset1 are only used in a ratio to each other. They are there to make sure that the deposited funds will ALL land inside the pool. + /// We don't use an asset ratio because either one of the amounts can be zero + /// See https://docs.osmosis.zone/osmosis-core/modules/concentrated-liquidity for more details + /// + fn create_position( + &self, + deps: Deps, + funds: Vec, + app: &App, + // create_position_msg: CreatePositionMessage, + ) -> AppResult> { + let proxy_addr = app.account_base(deps)?.proxy; + + // 2. Create a position + let tokens = cosmwasm_to_proto_coins(funds); + let msg = app.executor(deps).execute_with_reply_and_data( + MsgCreatePosition { + pool_id: self.pool_id, + sender: proxy_addr.to_string(), + lower_tick: self.lower_tick, + upper_tick: self.upper_tick, + tokens_provided: tokens, + token_min_amount0: "0".to_string(), + token_min_amount1: "0".to_string(), + } + .into(), + ReplyOn::Success, + OSMOSIS_CREATE_POSITION_REPLY_ID, + )?; + + Ok(vec![msg]) } - for coin in try_proto_to_cosmwasm_coins(position.claimable_spread_rewards)? { - rewards.add(coin)?; + fn raw_deposit(&self, deps: Deps, funds: Vec, app: &App) -> AppResult> { + let pool = self.position(deps)?; + let position = pool.position.unwrap(); + + let proxy_addr = app.account_base(deps)?.proxy; + + // We need to make sure the amounts are in the right order + // We assume the funds vector has 2 coins associated + let (amount0, amount1) = match pool + .asset0 + .map(|c| c.denom == funds[0].denom) + .or(pool.asset1.map(|c| c.denom == funds[1].denom)) + { + Some(true) => (funds[0].amount, funds[1].amount), // we already had the right order + Some(false) => (funds[1].amount, funds[0].amount), // we had the wrong order + None => return Err(AppError::NoPosition {}), // A position has to exist in order to execute this function. This should be unreachable + }; + + let deposit_msg = app.executor(deps).execute_with_reply_and_data( + MsgAddToPosition { + position_id: position.position_id, + sender: proxy_addr.to_string(), + amount0: amount0.to_string(), + amount1: amount1.to_string(), + token_min_amount0: "0".to_string(), + token_min_amount1: "0".to_string(), + } + .into(), + cosmwasm_std::ReplyOn::Success, + OSMOSIS_ADD_TO_POSITION_REPLY_ID, + )?; + + Ok(vec![deposit_msg]) } - Ok(rewards.into()) + fn position(&self, deps: Deps) -> AppResult { + let position_id = self.position_id.ok_or(AppError::NoPosition {})?; + ConcentratedliquidityQuerier::new(&deps.querier) + .position_by_id(position_id) + .map_err(|e| AppError::UnableToQueryPosition(position_id, e))? + .position + .ok_or(AppError::NoPosition {}) + } } pub fn query_swap_price( @@ -292,14 +295,3 @@ pub fn query_swap_price( Ok(price) } - -pub fn get_osmosis_position_by_id( - deps: Deps, - position_id: u64, -) -> AppResult { - ConcentratedliquidityQuerier::new(&deps.querier) - .position_by_id(position_id) - .map_err(|e| AppError::UnableToQueryPosition(position_id, e))? - .position - .ok_or(AppError::NoPosition {}) -} diff --git a/contracts/carrot-app/src/yield_sources/yield_type.rs b/contracts/carrot-app/src/yield_sources/yield_type.rs index 6eaddca9..d3888515 100644 --- a/contracts/carrot-app/src/yield_sources/yield_type.rs +++ b/contracts/carrot-app/src/yield_sources/yield_type.rs @@ -1,44 +1,26 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{coins, Coin, CosmosMsg, Deps, Env, SubMsg, Uint128}; -use osmosis_std::types::osmosis::{ - concentratedliquidity::v1beta1::Pool, poolmanager::v1beta1::PoolmanagerQuerier, -}; +use cosmwasm_std::{Coin, CosmosMsg, Deps, SubMsg, Uint128}; -use crate::{ - contract::{App, AppResult}, - error::AppError, -}; +use crate::contract::{App, AppResult}; -use super::{mars, osmosis_cl_pool, ShareType}; - -/// Denomination of a bank / token-factory / IBC token. -pub type Denom = String; +use super::{mars::MarsDepositParams, osmosis_cl_pool::ConcentratedPoolParams, ShareType}; #[cw_serde] pub enum YieldType { - /// For osmosis CL Pools, you need a pool id to do your deposit, and that's all ConcentratedLiquidityPool(ConcentratedPoolParams), /// For Mars, you just need to deposit in the RedBank /// You need to indicate the denom of the funds you want to deposit - Mars(Denom), + Mars(MarsDepositParams), } impl YieldType { - pub fn deposit( - self, - deps: Deps, - env: &Env, - funds: Vec, - app: &App, - ) -> AppResult> { + pub fn deposit(self, deps: Deps, funds: Vec, app: &App) -> AppResult> { if funds.is_empty() { return Ok(vec![]); } match self { - YieldType::ConcentratedLiquidityPool(params) => { - osmosis_cl_pool::deposit(deps, env, params, funds, app) - } - YieldType::Mars(denom) => mars::deposit(deps, denom, funds[0].amount, app), + YieldType::ConcentratedLiquidityPool(params) => params.deposit(deps, funds, app), + YieldType::Mars(params) => params.deposit(deps, funds, app), } } @@ -49,49 +31,36 @@ impl YieldType { app: &App, ) -> AppResult> { match self { - YieldType::ConcentratedLiquidityPool(params) => { - osmosis_cl_pool::withdraw(deps, amount, app, params) - } - YieldType::Mars(denom) => mars::withdraw(deps, denom, amount, app), + YieldType::ConcentratedLiquidityPool(params) => params.withdraw(deps, amount, app), + YieldType::Mars(params) => params.withdraw(deps, amount, app), } } pub fn withdraw_rewards(self, deps: Deps, app: &App) -> AppResult<(Vec, Vec)> { match self { - YieldType::ConcentratedLiquidityPool(params) => { - osmosis_cl_pool::withdraw_rewards(deps, params, app) - } - YieldType::Mars(denom) => mars::withdraw_rewards(deps, denom, app), + YieldType::ConcentratedLiquidityPool(params) => params.withdraw_rewards(deps, app), + YieldType::Mars(params) => params.withdraw_rewards(deps, app), } } pub fn user_deposit(&self, deps: Deps, app: &App) -> AppResult> { match self { - YieldType::ConcentratedLiquidityPool(params) => { - osmosis_cl_pool::user_deposit(deps, app, params.clone()) - } - YieldType::Mars(denom) => Ok(coins( - mars::user_deposit(deps, denom.clone(), app)?.into(), - denom, - )), + YieldType::ConcentratedLiquidityPool(params) => params.user_deposit(deps, app), + YieldType::Mars(params) => params.user_deposit(deps, app), } } pub fn user_rewards(&self, deps: Deps, app: &App) -> AppResult> { match self { - YieldType::ConcentratedLiquidityPool(params) => { - osmosis_cl_pool::user_rewards(deps, app, params.clone()) - } - YieldType::Mars(denom) => mars::user_rewards(deps, denom.clone(), app), + YieldType::ConcentratedLiquidityPool(params) => params.user_rewards(deps, app), + YieldType::Mars(params) => params.user_rewards(deps, app), } } pub fn user_liquidity(&self, deps: Deps, app: &App) -> AppResult { match self { - YieldType::ConcentratedLiquidityPool(params) => { - osmosis_cl_pool::user_liquidity(deps, app, params.clone()) - } - YieldType::Mars(denom) => mars::user_liquidity(deps, denom.clone(), app), + YieldType::ConcentratedLiquidityPool(params) => params.user_liquidity(deps, app), + YieldType::Mars(params) => params.user_liquidity(deps, app), } } @@ -101,28 +70,31 @@ impl YieldType { /// Mars doesn't use that, because the share is fixed to 1 pub fn share_type(&self) -> ShareType { match self { - YieldType::ConcentratedLiquidityPool(_) => ShareType::Dynamic, - YieldType::Mars(_) => ShareType::Fixed, + YieldType::ConcentratedLiquidityPool(params) => params.share_type(), + YieldType::Mars(params) => params.share_type(), } } } -#[cw_serde] -pub struct ConcentratedPoolParams { - pub pool_id: u64, - pub lower_tick: i64, - pub upper_tick: i64, - pub position_id: Option, -} +pub trait YieldTypeImplementation { + fn deposit(self, deps: Deps, funds: Vec, app: &App) -> AppResult>; -impl ConcentratedPoolParams { - pub fn check(&self, deps: Deps) -> AppResult<()> { - let _pool: Pool = PoolmanagerQuerier::new(&deps.querier) - .pool(self.pool_id) - .map_err(|_| AppError::PoolNotFound {})? - .pool - .ok_or(AppError::PoolNotFound {})? - .try_into()?; - Ok(()) - } + fn withdraw(self, deps: Deps, amount: Option, app: &App) -> AppResult>; + + fn withdraw_rewards(self, deps: Deps, app: &App) -> AppResult<(Vec, Vec)>; + + fn user_deposit(&self, deps: Deps, app: &App) -> AppResult>; + + fn user_rewards(&self, deps: Deps, app: &App) -> AppResult>; + + fn user_liquidity(&self, deps: Deps, app: &App) -> AppResult; + + /// Indicate the default funds allocation + /// This is specifically useful for auto-compound as we're not able to input target amounts + /// CL pools use that to know the best funds deposit ratio + /// Mars doesn't use that, because the share is fixed to 1 + fn share_type(&self) -> ShareType; + + /// Verifies the yield type is valid + fn check(&self, deps: Deps) -> AppResult<()>; } diff --git a/contracts/carrot-app/tests/autocompound.rs b/contracts/carrot-app/tests/autocompound.rs index 6f157016..1a69f77f 100644 --- a/contracts/carrot-app/tests/autocompound.rs +++ b/contracts/carrot-app/tests/autocompound.rs @@ -1,14 +1,11 @@ mod common; -use crate::common::{setup_test_tube, DEX_NAME, GAS_DENOM, LOTS, REWARD_DENOM, USDC, USDT}; +use crate::common::{setup_test_tube, DEX_NAME, USDC, USDT}; use abstract_app::abstract_interface::{Abstract, AbstractAccount}; use carrot_app::msg::{ AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse, AvailableRewardsResponse, - CompoundStatus, CompoundStatusResponse, }; -use cosmwasm_std::{coin, coins, Uint128}; -use cw_asset::AssetBase; -use cw_orch::osmosis_test_tube::osmosis_test_tube::Account; +use cosmwasm_std::{coin, coins}; use cw_orch::{anyhow, prelude::*}; #[test] diff --git a/contracts/carrot-app/tests/common.rs b/contracts/carrot-app/tests/common.rs index 8c6a5e8c..45c5d0b6 100644 --- a/contracts/carrot-app/tests/common.rs +++ b/contracts/carrot-app/tests/common.rs @@ -2,13 +2,12 @@ use abstract_app::abstract_core::objects::{ pool_id::PoolAddressBase, AssetEntry, PoolMetadata, PoolType, }; use abstract_client::{AbstractClient, Application, Namespace}; +use carrot_app::autocompound::{AutocompoundConfig, AutocompoundRewardsConfig}; use carrot_app::contract::OSMOSIS; use carrot_app::msg::AppInstantiateMsg; -use carrot_app::state::{AutocompoundConfig, AutocompoundRewardsConfig}; -use carrot_app::yield_sources::yield_type::{ConcentratedPoolParams, YieldType}; -use carrot_app::yield_sources::{ - BalanceStrategy, BalanceStrategyElement, ExpectedToken, YieldSource, -}; +use carrot_app::yield_sources::osmosis_cl_pool::ConcentratedPoolParams; +use carrot_app::yield_sources::yield_type::YieldType; +use carrot_app::yield_sources::{AssetShare, BalanceStrategy, BalanceStrategyElement, YieldSource}; use cosmwasm_std::{coin, coins, Decimal, Uint128, Uint64}; use cw_asset::AssetInfoUnchecked; use cw_orch::osmosis_test_tube::osmosis_test_tube::Gamm; @@ -125,11 +124,11 @@ pub fn deploy( balance_strategy: BalanceStrategy(vec![BalanceStrategyElement { yield_source: YieldSource { asset_distribution: vec![ - ExpectedToken { + AssetShare { denom: USDT.to_string(), share: Decimal::percent(50), }, - ExpectedToken { + AssetShare { denom: USDC.to_string(), share: Decimal::percent(50), }, diff --git a/contracts/carrot-app/tests/config.rs b/contracts/carrot-app/tests/config.rs index 0d67ee08..d0569efa 100644 --- a/contracts/carrot-app/tests/config.rs +++ b/contracts/carrot-app/tests/config.rs @@ -4,8 +4,8 @@ use crate::common::{setup_test_tube, USDC, USDT}; use carrot_app::{ msg::{AppExecuteMsgFns, AppQueryMsgFns}, yield_sources::{ - yield_type::{ConcentratedPoolParams, YieldType}, - BalanceStrategy, BalanceStrategyElement, ExpectedToken, YieldSource, + osmosis_cl_pool::ConcentratedPoolParams, yield_type::YieldType, AssetShare, + BalanceStrategy, BalanceStrategyElement, YieldSource, }, }; use common::{INITIAL_LOWER_TICK, INITIAL_UPPER_TICK}; @@ -21,11 +21,11 @@ fn rebalance_fails() -> anyhow::Result<()> { BalanceStrategyElement { yield_source: YieldSource { asset_distribution: vec![ - ExpectedToken { + AssetShare { denom: USDT.to_string(), share: Decimal::percent(50), }, - ExpectedToken { + AssetShare { denom: USDC.to_string(), share: Decimal::percent(50), }, @@ -42,11 +42,11 @@ fn rebalance_fails() -> anyhow::Result<()> { BalanceStrategyElement { yield_source: YieldSource { asset_distribution: vec![ - ExpectedToken { + AssetShare { denom: USDT.to_string(), share: Decimal::percent(50), }, - ExpectedToken { + AssetShare { denom: USDC.to_string(), share: Decimal::percent(50), }, @@ -76,11 +76,11 @@ fn rebalance_success() -> anyhow::Result<()> { BalanceStrategyElement { yield_source: YieldSource { asset_distribution: vec![ - ExpectedToken { + AssetShare { denom: USDT.to_string(), share: Decimal::percent(50), }, - ExpectedToken { + AssetShare { denom: USDC.to_string(), share: Decimal::percent(50), }, @@ -97,11 +97,11 @@ fn rebalance_success() -> anyhow::Result<()> { BalanceStrategyElement { yield_source: YieldSource { asset_distribution: vec![ - ExpectedToken { + AssetShare { denom: USDT.to_string(), share: Decimal::percent(50), }, - ExpectedToken { + AssetShare { denom: USDC.to_string(), share: Decimal::percent(50), }, diff --git a/contracts/carrot-app/tests/deposit_withdraw.rs b/contracts/carrot-app/tests/deposit_withdraw.rs index e97ed170..eff42c28 100644 --- a/contracts/carrot-app/tests/deposit_withdraw.rs +++ b/contracts/carrot-app/tests/deposit_withdraw.rs @@ -5,8 +5,8 @@ use abstract_client::Application; use carrot_app::{ msg::{AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse}, yield_sources::{ - yield_type::{ConcentratedPoolParams, YieldType}, - BalanceStrategy, BalanceStrategyElement, ExpectedToken, YieldSource, + mars::MarsDepositParams, osmosis_cl_pool::ConcentratedPoolParams, yield_type::YieldType, + AssetShare, BalanceStrategy, BalanceStrategyElement, YieldSource, }, AppInterface, }; @@ -150,11 +150,11 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { BalanceStrategyElement { yield_source: YieldSource { asset_distribution: vec![ - ExpectedToken { + AssetShare { denom: USDT.to_string(), share: Decimal::percent(50), }, - ExpectedToken { + AssetShare { denom: USDC.to_string(), share: Decimal::percent(50), }, @@ -171,11 +171,11 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { BalanceStrategyElement { yield_source: YieldSource { asset_distribution: vec![ - ExpectedToken { + AssetShare { denom: USDT.to_string(), share: Decimal::percent(50), }, - ExpectedToken { + AssetShare { denom: USDC.to_string(), share: Decimal::percent(50), }, @@ -220,11 +220,11 @@ fn deposit_multiple_positions_with_empty() -> anyhow::Result<()> { BalanceStrategyElement { yield_source: YieldSource { asset_distribution: vec![ - ExpectedToken { + AssetShare { denom: USDT.to_string(), share: Decimal::percent(50), }, - ExpectedToken { + AssetShare { denom: USDC.to_string(), share: Decimal::percent(50), }, @@ -241,11 +241,11 @@ fn deposit_multiple_positions_with_empty() -> anyhow::Result<()> { BalanceStrategyElement { yield_source: YieldSource { asset_distribution: vec![ - ExpectedToken { + AssetShare { denom: USDT.to_string(), share: Decimal::percent(50), }, - ExpectedToken { + AssetShare { denom: USDC.to_string(), share: Decimal::percent(50), }, @@ -261,11 +261,13 @@ fn deposit_multiple_positions_with_empty() -> anyhow::Result<()> { }, BalanceStrategyElement { yield_source: YieldSource { - asset_distribution: vec![ExpectedToken { + asset_distribution: vec![AssetShare { denom: USDT.to_string(), share: Decimal::percent(100), }], - ty: YieldType::Mars(USDT.to_string()), + ty: YieldType::Mars(MarsDepositParams { + denom: USDT.to_string(), + }), }, share: Decimal::percent(0), }, From e5ff15bb0d0944705cee151e5b407f5bdb83d24d Mon Sep 17 00:00:00 2001 From: Kayanski Date: Fri, 29 Mar 2024 08:18:04 +0000 Subject: [PATCH 18/42] More structure --- .../carrot-app/src/distribution/deposit.rs | 80 +++++++++++++++- .../carrot-app/src/distribution/withdraw.rs | 6 +- contracts/carrot-app/src/handlers/execute.rs | 92 ++++--------------- contracts/carrot-app/src/handlers/query.rs | 1 - 4 files changed, 99 insertions(+), 80 deletions(-) diff --git a/contracts/carrot-app/src/distribution/deposit.rs b/contracts/carrot-app/src/distribution/deposit.rs index ed57b464..f52c9d17 100644 --- a/contracts/carrot-app/src/distribution/deposit.rs +++ b/contracts/carrot-app/src/distribution/deposit.rs @@ -5,8 +5,8 @@ use cosmwasm_std::{coin, Coin, Coins, Decimal, Deps, Uint128}; use crate::{ contract::{App, AppResult}, handlers::query::query_all_exchange_rates, - helpers::compute_total_value, - yield_sources::{yield_type::YieldType, AssetShare, BalanceStrategy}, + helpers::{compute_total_value, compute_value}, + yield_sources::{yield_type::YieldType, AssetShare, BalanceStrategy, BalanceStrategyElement}, }; use cosmwasm_schema::cw_serde; @@ -18,6 +18,82 @@ use crate::{ }; impl BalanceStrategy { + // We determine the best balance strategy depending on the current deposits and the target strategy. + // This method needs to be called on the stored strategy + pub fn current_deposit_strategy( + &self, + deps: Deps, + funds: &mut Coins, + current_strategy_status: Self, + app: &App, + ) -> AppResult<(Vec, Option)> { + let total_value = self.current_balance(deps, app)?.total_value; + let deposit_value = compute_value(deps, &funds.to_vec(), app)?; + + if deposit_value.is_zero() { + // We are trying to deposit no value, so we just don't do anything + return Ok((vec![], None)); + } + + // We create the strategy so that he final distribution is as close to the target strategy as possible + // 1. For all strategies, we withdraw some if its value is too high above target_strategy + let mut withdraw_value = Uint128::zero(); + let mut withdraw_msgs = vec![]; + + // All strategies have to be reviewed + // EITHER of those are true : + // - The yield source has too much funds deposited and some should be withdrawn + // OR + // - Some funds need to be deposited into the strategy + let this_deposit_strategy: BalanceStrategy = current_strategy_status + .0 + .into_iter() + .zip(self.0.clone()) + .map(|(target, current)| { + // We need to take into account the total value added by the current shares + + let value_now = current.share * total_value; + let target_value = target.share * (total_value + deposit_value); + + // If value now is greater than the target value, we need to withdraw some funds from the protocol + if target_value < value_now { + let this_withdraw_value = target_value - value_now; + // In the following line, total_value can't be zero, otherwise the if condition wouldn't be met + let this_withdraw_share = Decimal::from_ratio(withdraw_value, total_value); + let this_withdraw_funds = + current.withdraw_preview(deps, Some(this_withdraw_share), app)?; + withdraw_value += this_withdraw_value; + for fund in this_withdraw_funds { + funds.add(fund)?; + } + withdraw_msgs.push( + current + .withdraw(deps, Some(this_withdraw_share), app)? + .into(), + ); + + // In case there is a withdraw from the strategy, we don't need to deposit into this strategy after ! + Ok::<_, AppError>(None) + } else { + // In case we don't withdraw anything, it means we might deposit. + // Total should sum to one ! + let share = Decimal::from_ratio(target_value - value_now, deposit_value); + + Ok(Some(BalanceStrategyElement { + yield_source: target.yield_source.clone(), + share, + })) + } + }) + .collect::, _>>()? + .into_iter() + .flatten() + .collect::>() + .into(); + + Ok((withdraw_msgs, Some(this_deposit_strategy))) + } + // We dispatch the available funds directly into the Strategies // This returns : // 0 : Funds that are used for specific strategies. And remaining amounts to fill those strategies diff --git a/contracts/carrot-app/src/distribution/withdraw.rs b/contracts/carrot-app/src/distribution/withdraw.rs index 3e3e854b..021e8309 100644 --- a/contracts/carrot-app/src/distribution/withdraw.rs +++ b/contracts/carrot-app/src/distribution/withdraw.rs @@ -4,9 +4,6 @@ use cosmwasm_std::{Coin, Decimal, Deps}; use crate::{ contract::{App, AppResult}, error::AppError, - handlers::query::query_all_exchange_rates, - helpers::compute_total_value, - msg::AssetsBalanceResponse, yield_sources::{BalanceStrategy, BalanceStrategyElement}, }; @@ -57,8 +54,7 @@ impl BalanceStrategyElement { app: &App, ) -> AppResult> { let current_deposit = self.yield_source.ty.user_deposit(deps, app)?; - let exchange_rates = - query_all_exchange_rates(deps, current_deposit.iter().map(|f| f.denom.clone()), app)?; + if let Some(share) = withdraw_share { Ok(current_deposit .into_iter() diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index 8277c142..482bd225 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -2,10 +2,10 @@ use crate::{ contract::{App, AppResult}, error::AppError, handlers::query::query_balance, - helpers::{assert_contract, compute_value}, + helpers::assert_contract, msg::{AppExecuteMsg, ExecuteMsg, InternalExecuteMsg}, state::{AUTOCOMPOUND_STATE, CONFIG}, - yield_sources::{AssetShare, BalanceStrategy, BalanceStrategyElement}, + yield_sources::{AssetShare, BalanceStrategy}, }; use abstract_app::abstract_sdk::features::AbstractResponse; use abstract_sdk::ExecutorMsg; @@ -205,7 +205,7 @@ pub fn _inner_deposit( pub fn _inner_advanced_deposit( deps: Deps, env: &Env, - mut funds: Vec, + funds: Vec, yield_source_params: Option>>>, app: &App, ) -> AppResult> { @@ -213,74 +213,21 @@ pub fn _inner_advanced_deposit( let target_strategy = query_strategy(deps)?.strategy; // This is the current distribution of funds inside the strategies - let current_distribution = target_strategy.query_current_status(deps, app)?; - let total_value = target_strategy.current_balance(deps, app)?.total_value; - let deposit_value = compute_value(deps, &funds, app)?; - - if deposit_value.is_zero() { - // We are trying to deposit no value, so we just don't do anything - return Ok(vec![]); - } - - // We create the strategy so that he final distribution is as close to the target strategy as possible - // 1. For all strategies, we withdraw some if its value is too high above target_strategy - let mut withdraw_funds = Coins::default(); - let mut withdraw_value = Uint128::zero(); - let mut withdraw_msgs = vec![]; - - // All strategies have to be reviewed - // EITHER of those are true : - // - The yield source has too much funds deposited and some should be withdrawn - // OR - // - Some funds need to be deposited into the strategy - let mut this_deposit_strategy: BalanceStrategy = target_strategy - .0 - .iter() - .zip(current_distribution.0) - .map(|(target, current)| { - // We need to take into account the total value added by the current shares - - let value_now = current.share * total_value; - let target_value = target.share * (total_value + deposit_value); - - // If value now is greater than the target value, we need to withdraw some funds from the protocol - if target_value < value_now { - let this_withdraw_value = target_value - value_now; - // In the following line, total_value can't be zero, otherwise the if condition wouldn't be met - let this_withdraw_share = Decimal::from_ratio(withdraw_value, total_value); - let this_withdraw_funds = - current.withdraw_preview(deps, Some(this_withdraw_share), app)?; - withdraw_value += this_withdraw_value; - for fund in this_withdraw_funds { - withdraw_funds.add(fund)?; - } - withdraw_msgs.push( - current - .withdraw(deps, Some(this_withdraw_share), app)? - .into(), - ); - - // In case there is a withdraw from the strategy, we don't need to deposit into this strategy after ! - Ok::<_, AppError>(None) - } else { - // In case we don't withdraw anything, it means we might deposit. - // Total should sum to one ! - let share = Decimal::from_ratio(target_value - value_now, deposit_value); - - Ok(Some(BalanceStrategyElement { - yield_source: target.yield_source.clone(), - share, - })) - } - }) - .collect::, _>>()? - .into_iter() - .flatten() - .collect::>() - .into(); - - // We add the withdrawn funds to the deposit funds - funds.extend(withdraw_funds); + let current_strategy_status = target_strategy.query_current_status(deps, app)?; + + let mut usable_funds: Coins = funds.try_into()?; + let (withdraw_msgs, this_deposit_strategy) = target_strategy.current_deposit_strategy( + deps, + &mut usable_funds, + current_strategy_status, + app, + )?; + + let mut this_deposit_strategy = if let Some(this_deposit_strategy) = this_deposit_strategy { + this_deposit_strategy + } else { + return Ok(withdraw_msgs); + }; // We query the yield source shares this_deposit_strategy.apply_current_strategy_shares(deps, app)?; @@ -289,7 +236,8 @@ pub fn _inner_advanced_deposit( this_deposit_strategy.correct_with(yield_source_params); // We fill the strategies with the current deposited funds and get messages to execute those deposits - let deposit_msgs = this_deposit_strategy.fill_all_and_get_messages(deps, env, funds, app)?; + let deposit_msgs = + this_deposit_strategy.fill_all_and_get_messages(deps, env, usable_funds.into(), app)?; Ok([withdraw_msgs, deposit_msgs].concat()) } diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index 1c8ac4da..ff36e05a 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -10,7 +10,6 @@ use cosmwasm_std::{to_json_binary, Binary, Coins, Decimal, Deps, Env, Uint128}; use cw_asset::Asset; use crate::autocompound::get_autocompound_status; -use crate::yield_sources::BalanceStrategy; use crate::{ contract::{App, AppResult}, error::AppError, From a266725098b5368e678906371dbfbd95a8ac357e Mon Sep 17 00:00:00 2001 From: Kayanski Date: Fri, 29 Mar 2024 08:46:50 +0000 Subject: [PATCH 19/42] Added inbalance test --- .../carrot-app/src/distribution/deposit.rs | 2 +- .../carrot-app/src/distribution/query.rs | 2 +- contracts/carrot-app/src/exchange_rate.rs | 26 +++++ contracts/carrot-app/src/handlers/internal.rs | 2 +- contracts/carrot-app/src/handlers/query.rs | 44 ++++----- contracts/carrot-app/src/helpers.rs | 2 +- contracts/carrot-app/src/lib.rs | 1 + contracts/carrot-app/src/msg.rs | 14 +++ contracts/carrot-app/tests/pool_inbalance.rs | 98 +++++++++++++++++++ 9 files changed, 165 insertions(+), 26 deletions(-) create mode 100644 contracts/carrot-app/src/exchange_rate.rs create mode 100644 contracts/carrot-app/tests/pool_inbalance.rs diff --git a/contracts/carrot-app/src/distribution/deposit.rs b/contracts/carrot-app/src/distribution/deposit.rs index f52c9d17..815f2bef 100644 --- a/contracts/carrot-app/src/distribution/deposit.rs +++ b/contracts/carrot-app/src/distribution/deposit.rs @@ -4,7 +4,7 @@ use cosmwasm_std::{coin, Coin, Coins, Decimal, Deps, Uint128}; use crate::{ contract::{App, AppResult}, - handlers::query::query_all_exchange_rates, + exchange_rate::query_all_exchange_rates, helpers::{compute_total_value, compute_value}, yield_sources::{yield_type::YieldType, AssetShare, BalanceStrategy, BalanceStrategyElement}, }; diff --git a/contracts/carrot-app/src/distribution/query.rs b/contracts/carrot-app/src/distribution/query.rs index 1eb6a350..ea238247 100644 --- a/contracts/carrot-app/src/distribution/query.rs +++ b/contracts/carrot-app/src/distribution/query.rs @@ -3,7 +3,7 @@ use cosmwasm_std::{Coins, Decimal, Deps, Uint128}; use crate::{ contract::{App, AppResult}, error::AppError, - handlers::query::query_exchange_rate, + exchange_rate::query_exchange_rate, msg::AssetsBalanceResponse, yield_sources::{AssetShare, BalanceStrategy, BalanceStrategyElement, YieldSource}, }; diff --git a/contracts/carrot-app/src/exchange_rate.rs b/contracts/carrot-app/src/exchange_rate.rs new file mode 100644 index 00000000..649f61f6 --- /dev/null +++ b/contracts/carrot-app/src/exchange_rate.rs @@ -0,0 +1,26 @@ +use std::collections::HashMap; + +use cosmwasm_std::{Decimal, Deps}; + +use crate::contract::{App, AppResult}; + +pub fn query_exchange_rate( + _deps: Deps, + _denom: impl Into, + _app: &App, +) -> AppResult { + // In the first iteration, all deposited tokens are assumed to be equal to 1 + Ok(Decimal::one()) +} + +// Returns a hashmap with all request exchange rates +pub fn query_all_exchange_rates( + deps: Deps, + denoms: impl Iterator, + app: &App, +) -> AppResult> { + denoms + .into_iter() + .map(|denom| Ok((denom.clone(), query_exchange_rate(deps, denom, app)?))) + .collect() +} diff --git a/contracts/carrot-app/src/handlers/internal.rs b/contracts/carrot-app/src/handlers/internal.rs index e5a85783..08b48411 100644 --- a/contracts/carrot-app/src/handlers/internal.rs +++ b/contracts/carrot-app/src/handlers/internal.rs @@ -15,7 +15,7 @@ use abstract_sdk::features::AbstractNameService; use cosmwasm_std::{wasm_execute, Coin, DepsMut, Env, StdError, SubMsg, Uint128}; use cw_asset::AssetInfo; -use super::query::query_exchange_rate; +use crate::exchange_rate::query_exchange_rate; pub fn deposit_one_strategy( deps: DepsMut, diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index ff36e05a..eedf371b 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -1,15 +1,15 @@ -use std::collections::HashMap; - use abstract_app::traits::AccountIdentification; use abstract_app::{ abstract_core::objects::AnsAsset, traits::{AbstractNameService, Resolve}, }; use abstract_dex_adapter::DexInterface; -use cosmwasm_std::{to_json_binary, Binary, Coins, Decimal, Deps, Env, Uint128}; +use cosmwasm_std::{to_json_binary, Binary, Coins, Deps, Env, Uint128}; use cw_asset::Asset; use crate::autocompound::get_autocompound_status; +use crate::exchange_rate::query_exchange_rate; +use crate::msg::{PositionResponse, PositionsResponse}; use crate::{ contract::{App, AppResult}, error::AppError, @@ -30,6 +30,7 @@ pub fn query_handler(deps: Deps, env: Env, app: &App, msg: AppQueryMsg) -> AppRe AppQueryMsg::CompoundStatus {} => to_json_binary(&query_compound_status(deps, env, app)?), AppQueryMsg::RebalancePreview {} => todo!(), AppQueryMsg::StrategyStatus {} => to_json_binary(&query_strategy_status(deps, app)?), + AppQueryMsg::Positions {} => to_json_binary(&query_positions(deps, app)?), } .map_err(Into::into) } @@ -144,23 +145,22 @@ fn query_rewards(deps: Deps, app: &App) -> AppResult { }) } -pub fn query_exchange_rate( - _deps: Deps, - _denom: impl Into, - _app: &App, -) -> AppResult { - // In the first iteration, all deposited tokens are assumed to be equal to 1 - Ok(Decimal::one()) -} - -// Returns a hashmap with all request exchange rates -pub fn query_all_exchange_rates( - deps: Deps, - denoms: impl Iterator, - app: &App, -) -> AppResult> { - denoms - .into_iter() - .map(|denom| Ok((denom.clone(), query_exchange_rate(deps, denom, app)?))) - .collect() +pub fn query_positions(deps: Deps, app: &App) -> AppResult { + Ok(PositionsResponse { + positions: query_strategy(deps)? + .strategy + .0 + .into_iter() + .map(|s| { + let balance = s.yield_source.ty.user_deposit(deps, app)?; + let liquidity = s.yield_source.ty.user_liquidity(deps, app)?; + + Ok::<_, AppError>(PositionResponse { + ty: s.yield_source.ty, + balance, + liquidity, + }) + }) + .collect::>()?, + }) } diff --git a/contracts/carrot-app/src/helpers.rs b/contracts/carrot-app/src/helpers.rs index 99250513..c353856f 100644 --- a/contracts/carrot-app/src/helpers.rs +++ b/contracts/carrot-app/src/helpers.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use crate::contract::{App, AppResult}; use crate::error::AppError; -use crate::handlers::query::query_exchange_rate; +use crate::exchange_rate::query_exchange_rate; use abstract_app::traits::AccountIdentification; use abstract_app::{objects::AssetEntry, traits::AbstractNameService}; use abstract_sdk::Resolve; diff --git a/contracts/carrot-app/src/lib.rs b/contracts/carrot-app/src/lib.rs index 06d7b3d6..ce732ba5 100644 --- a/contracts/carrot-app/src/lib.rs +++ b/contracts/carrot-app/src/lib.rs @@ -2,6 +2,7 @@ pub mod autocompound; pub mod contract; pub mod distribution; pub mod error; +pub mod exchange_rate; mod handlers; pub mod helpers; pub mod msg; diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index c90c8d06..c9528992 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -98,6 +98,8 @@ pub enum AppQueryMsg { Config {}, #[returns(AssetsBalanceResponse)] Balance {}, + #[returns(PositionsResponse)] + Positions {}, /// Get the claimable rewards that the position has accumulated. /// Returns [`AvailableRewardsResponse`] #[returns(AvailableRewardsResponse)] @@ -143,6 +145,18 @@ pub struct StrategyResponse { pub strategy: BalanceStrategy, } +#[cw_serde] +pub struct PositionsResponse { + pub positions: Vec, +} + +#[cw_serde] +pub struct PositionResponse { + pub ty: YieldType, + pub balance: Vec, + pub liquidity: Uint128, +} + #[cw_serde] pub struct CompoundStatusResponse { pub status: CompoundStatus, diff --git a/contracts/carrot-app/tests/pool_inbalance.rs b/contracts/carrot-app/tests/pool_inbalance.rs new file mode 100644 index 00000000..69a2e613 --- /dev/null +++ b/contracts/carrot-app/tests/pool_inbalance.rs @@ -0,0 +1,98 @@ +mod common; + +use crate::common::{setup_test_tube, LOTS, USDC, USDT}; +use abstract_client::Application; +use carrot_app::{ + msg::{AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse}, + yield_sources::{ + mars::MarsDepositParams, osmosis_cl_pool::ConcentratedPoolParams, yield_type::YieldType, + AssetShare, BalanceStrategy, BalanceStrategyElement, YieldSource, + }, + AppInterface, +}; +use common::{INITIAL_LOWER_TICK, INITIAL_UPPER_TICK}; +use cosmwasm_std::{coin, coins, Decimal, Uint128}; +use cw_orch::{anyhow, prelude::*}; +use osmosis_std::types::osmosis::{ + gamm::v1beta1::{MsgSwapExactAmountIn, MsgSwapExactAmountInResponse}, + poolmanager::v1beta1::SwapAmountInRoute, +}; +use prost_types::Any; + +fn query_balances( + carrot_app: &Application>, +) -> anyhow::Result { + let balance = carrot_app.balance(); + if balance.is_err() { + return Ok(Uint128::zero()); + } + let sum = balance? + .balances + .iter() + .fold(Uint128::zero(), |acc, e| acc + e.amount); + + Ok(sum) +} + +#[test] +fn deposit_after_inbalance_works() -> anyhow::Result<()> { + let (pool_id, carrot_app) = setup_test_tube(false)?; + + // We should add funds to the account proxy + let deposit_amount = 5_000; + let deposit_coins = coins(deposit_amount, USDT.to_owned()); + let mut chain = carrot_app.get_chain().clone(); + let proxy = carrot_app.account().proxy()?; + chain.add_balance(proxy.to_string(), deposit_coins.clone())?; + + // Do the deposit + carrot_app.deposit(deposit_coins.clone(), None)?; + + // Create a pool inbalance by swapping a lot deposit amount from one to the other. + // All the positions in the pool are centered, so the price doesn't change, just the funds ratio inside the position + + let swap_msg = MsgSwapExactAmountIn { + sender: chain.sender().to_string(), + token_in: Some(osmosis_std::types::cosmos::base::v1beta1::Coin { + denom: USDT.to_string(), + amount: "10_000".to_string(), + }), + token_out_min_amount: "1".to_string(), + routes: vec![SwapAmountInRoute { + pool_id, + token_out_denom: USDC.to_string(), + }], + } + .to_any(); + let resp = chain.commit_any::( + vec![Any { + type_url: swap_msg.type_url, + value: swap_msg.value, + }], + None, + )?; + + let proxy_balance_before_second = chain + .bank_querier() + .balance(&proxy, Some(USDT.to_string()))?[0] + .amount; + // Add some more funds + chain.add_balance(proxy.to_string(), deposit_coins.clone())?; + + // // Do the second deposit + let response = carrot_app.deposit(vec![coin(deposit_amount, USDT.to_owned())], None)?; + // Check almost everything landed + let proxy_balance_after_second = chain + .bank_querier() + .balance(&proxy, Some(USDT.to_string()))?[0] + .amount; + + println!( + "balances : {:?}, {:?}", + proxy_balance_before_second, proxy_balance_after_second + ); + + panic!(); + + Ok(()) +} From 904e0c47567f7db4b421cc691f39e1fd5c115e94 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Wed, 3 Apr 2024 12:36:34 +0000 Subject: [PATCH 20/42] Added rebalance and tests --- .../carrot-app/src/distribution/deposit.rs | 27 +- contracts/carrot-app/src/handlers/execute.rs | 61 ++- contracts/carrot-app/src/handlers/internal.rs | 8 + contracts/carrot-app/src/handlers/query.rs | 13 +- contracts/carrot-app/src/msg.rs | 7 +- .../src/yield_sources/osmosis_cl_pool.rs | 8 +- .../src/yield_sources/yield_type.rs | 15 +- contracts/carrot-app/tests/common.rs | 7 +- contracts/carrot-app/tests/config.rs | 360 +++++++++++++++--- .../carrot-app/tests/deposit_withdraw.rs | 4 +- contracts/carrot-app/tests/pool_inbalance.rs | 42 +- 11 files changed, 443 insertions(+), 109 deletions(-) diff --git a/contracts/carrot-app/src/distribution/deposit.rs b/contracts/carrot-app/src/distribution/deposit.rs index 815f2bef..6e6fe530 100644 --- a/contracts/carrot-app/src/distribution/deposit.rs +++ b/contracts/carrot-app/src/distribution/deposit.rs @@ -116,14 +116,22 @@ impl BalanceStrategy { .yield_source .asset_distribution .iter() - .map(|AssetShare { denom, share }| StrategyStatusElement { - denom: denom.clone(), - raw_funds: Uint128::zero(), - remaining_amount: share * source.share * total_value, + .map(|AssetShare { denom, share }| { + // Amount to fill this denom completely is value / exchange_rate + // Value we want to put here is share * source.share * total_value + Ok::<_, AppError>(StrategyStatusElement { + denom: denom.clone(), + raw_funds: Uint128::zero(), + remaining_amount: (share * source.share + / exchange_rates + .get(denom) + .ok_or(AppError::NoExchangeRate(denom.clone()))?) + * total_value, + }) }) - .collect::>() + .collect::, _>>() }) - .collect::>(); + .collect::, _>>()?; for this_coin in funds { let mut remaining_amount = this_coin.amount; @@ -164,9 +172,10 @@ impl BalanceStrategy { // We determine the value of all tokens that will be used inside this function let exchange_rates = query_all_exchange_rates( deps, - self.all_denoms() - .into_iter() - .chain(funds.iter().map(|f| f.denom.clone())), + funds + .iter() + .map(|f| f.denom.clone()) + .chain(self.all_denoms()), app, )?; let (status, remaining_funds) = self.fill_sources(funds, &exchange_rates)?; diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index 482bd225..eafb2215 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -18,6 +18,7 @@ use super::{ internal::{deposit_one_strategy, execute_finalize_deposit, execute_one_deposit_step}, query::query_strategy, }; +use abstract_app::traits::AccountIdentification; pub fn execute_handler( deps: DepsMut, @@ -33,8 +34,8 @@ pub fn execute_handler( } => deposit(deps, env, info, funds, yield_sources_params, app), AppExecuteMsg::Withdraw { amount } => withdraw(deps, env, info, amount, app), AppExecuteMsg::Autocompound {} => autocompound(deps, env, info, app), - AppExecuteMsg::UpdateStrategy { strategy } => { - update_strategy(deps, env, info, strategy, app) + AppExecuteMsg::UpdateStrategy { strategy, funds } => { + update_strategy(deps, env, info, strategy, funds, app) } // Endpoints called by the contract directly AppExecuteMsg::Internal(internal_msg) => { @@ -98,23 +99,73 @@ fn withdraw( fn update_strategy( deps: DepsMut, - _env: Env, + env: Env, _info: MessageInfo, strategy: BalanceStrategy, + funds: Vec, app: App, ) -> AppResult { // We load it raw because we're changing the strategy let mut config = CONFIG.load(deps.storage)?; let old_strategy = config.balance_strategy; + // We check the new strategy strategy.check(deps.as_ref(), &app)?; + deps.api.debug("After strategy check"); // We execute operations to rebalance the funds between the strategies - // TODO + let mut available_funds: Coins = funds.try_into()?; + // 1. We withdraw all yield_sources that are not included in the new strategies + let all_stale_sources: Vec<_> = old_strategy + .0 + .into_iter() + .filter(|x| !strategy.0.contains(x)) + .collect(); + + deps.api.debug("After stale sources"); + let (withdrawn_funds, withdraw_msgs): (Vec>, Vec>) = + all_stale_sources + .into_iter() + .map(|s| { + Ok::<_, AppError>(( + s.withdraw_preview(deps.as_ref(), None, &app) + .unwrap_or_default(), + s.withdraw(deps.as_ref(), None, &app).ok(), + )) + }) + .collect::, _>>()? + .into_iter() + .unzip(); + + deps.api + .debug(&format!("After withdraw messages : {:?}", withdrawn_funds)); + withdrawn_funds + .into_iter() + .try_for_each(|f| f.into_iter().try_for_each(|f| available_funds.add(f)))?; + + // 2. We replace the strategy with the new strategy config.balance_strategy = strategy; CONFIG.save(deps.storage, &config)?; - Ok(app.response("rebalance")) + // 3. We deposit the funds into the new strategy + let deposit_msgs = _inner_deposit(deps.as_ref(), &env, available_funds.into(), None, &app)?; + + deps.api.debug(&format!( + "Proxy balance before withdraw : {:?}", + deps.querier + .query_all_balances(app.account_base(deps.as_ref())?.proxy)? + )); + + deps.api.debug("After deposit msgs"); + Ok(app + .response("rebalance") + .add_messages( + withdraw_msgs + .into_iter() + .flatten() + .collect::>(), + ) + .add_messages(deposit_msgs)) } // /// Auto-compound the position with earned fees and incentives. diff --git a/contracts/carrot-app/src/handlers/internal.rs b/contracts/carrot-app/src/handlers/internal.rs index 08b48411..d1a1a7f3 100644 --- a/contracts/carrot-app/src/handlers/internal.rs +++ b/contracts/carrot-app/src/handlers/internal.rs @@ -16,6 +16,7 @@ use cosmwasm_std::{wasm_execute, Coin, DepsMut, Env, StdError, SubMsg, Uint128}; use cw_asset::AssetInfo; use crate::exchange_rate::query_exchange_rate; +use abstract_app::traits::AccountIdentification; pub fn deposit_one_strategy( deps: DepsMut, @@ -27,6 +28,11 @@ pub fn deposit_one_strategy( ) -> AppResult { deps.api .debug(&format!("We're depositing {:?}-{:?}", strategy, yield_type)); + deps.api.debug(&format!( + "Proxy balance after withdraw : {:?}", + deps.querier + .query_all_balances(app.account_base(deps.as_ref())?.proxy)? + )); TEMP_DEPOSIT_COINS.save(deps.storage, &vec![])?; @@ -93,6 +99,8 @@ pub fn execute_one_deposit_step( app: App, ) -> AppResult { let config = CONFIG.load(deps.storage)?; + deps.api + .debug(&format!("Deposit step swap : {:?}", asset_in)); let exchange_rate_in = query_exchange_rate(deps.as_ref(), asset_in.denom.clone(), &app)?; let exchange_rate_out = query_exchange_rate(deps.as_ref(), denom_out.clone(), &app)?; diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index eedf371b..f33fe8e8 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -155,9 +155,20 @@ pub fn query_positions(deps: Deps, app: &App) -> AppResult { let balance = s.yield_source.ty.user_deposit(deps, app)?; let liquidity = s.yield_source.ty.user_liquidity(deps, app)?; + let total_value = balance + .iter() + .map(|fund| { + let exchange_rate = query_exchange_rate(deps, fund.denom.clone(), app)?; + Ok(fund.amount * exchange_rate) + }) + .sum::>()?; + Ok::<_, AppError>(PositionResponse { ty: s.yield_source.ty, - balance, + balance: AssetsBalanceResponse { + balances: balance, + total_value, + }, liquidity, }) }) diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index c9528992..35222396 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -50,7 +50,10 @@ pub enum AppExecuteMsg { /// Auto-compounds the pool rewards into the pool Autocompound {}, /// Rebalances all investments according to a new balance strategy - UpdateStrategy { strategy: BalanceStrategy }, + UpdateStrategy { + strategy: BalanceStrategy, + funds: Vec, + }, /// Only called by the contract internally Internal(InternalExecuteMsg), @@ -153,7 +156,7 @@ pub struct PositionsResponse { #[cw_serde] pub struct PositionResponse { pub ty: YieldType, - pub balance: Vec, + pub balance: AssetsBalanceResponse, pub liquidity: Uint128, } diff --git a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs index a5d9f526..52ca259c 100644 --- a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs +++ b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs @@ -133,6 +133,11 @@ impl YieldTypeImplementation for ConcentratedPoolParams { ] .into_iter() .flatten() + .map(|mut fund| { + // This is used because osmosis seems to charge 1 amount for withdrawals on all positions + fund.amount -= Uint128::one(); + fund + }) .collect()) } @@ -187,7 +192,8 @@ impl ConcentratedPoolParams { // create_position_msg: CreatePositionMessage, ) -> AppResult> { let proxy_addr = app.account_base(deps)?.proxy; - + deps.api + .debug(&format!("coins to be deposited : {:?}", funds)); // 2. Create a position let tokens = cosmwasm_to_proto_coins(funds); let msg = app.executor(deps).execute_with_reply_and_data( diff --git a/contracts/carrot-app/src/yield_sources/yield_type.rs b/contracts/carrot-app/src/yield_sources/yield_type.rs index d3888515..9f914cc3 100644 --- a/contracts/carrot-app/src/yield_sources/yield_type.rs +++ b/contracts/carrot-app/src/yield_sources/yield_type.rs @@ -44,24 +44,27 @@ impl YieldType { } pub fn user_deposit(&self, deps: Deps, app: &App) -> AppResult> { - match self { + let user_deposit_result = match self { YieldType::ConcentratedLiquidityPool(params) => params.user_deposit(deps, app), YieldType::Mars(params) => params.user_deposit(deps, app), - } + }; + Ok(user_deposit_result.unwrap_or_default()) } pub fn user_rewards(&self, deps: Deps, app: &App) -> AppResult> { - match self { + let user_deposit_result = match self { YieldType::ConcentratedLiquidityPool(params) => params.user_rewards(deps, app), YieldType::Mars(params) => params.user_rewards(deps, app), - } + }; + Ok(user_deposit_result.unwrap_or_default()) } pub fn user_liquidity(&self, deps: Deps, app: &App) -> AppResult { - match self { + let user_deposit_result = match self { YieldType::ConcentratedLiquidityPool(params) => params.user_liquidity(deps, app), YieldType::Mars(params) => params.user_liquidity(deps, app), - } + }; + Ok(user_deposit_result.unwrap_or_default()) } /// Indicate the default funds allocation diff --git a/contracts/carrot-app/tests/common.rs b/contracts/carrot-app/tests/common.rs index 45c5d0b6..c31ecfd6 100644 --- a/contracts/carrot-app/tests/common.rs +++ b/contracts/carrot-app/tests/common.rs @@ -92,7 +92,7 @@ pub fn deploy( let dex_adapter = publisher .publish_adapter::<_, abstract_dex_adapter::interface::DexAdapter>( abstract_dex_adapter::msg::DexInstantiateMsg { - swap_fee: Decimal::percent(2), + swap_fee: Decimal::permille(2), recipient_account: 0, }, )?; @@ -205,7 +205,7 @@ pub fn create_pool(mut chain: OsmosisTestTube) -> anyhow::Result<(u64, u64)> { // }], // None, // )?; - let _proposal_response = GovWithAppAccess::new(&chain.app.borrow()) + GovWithAppAccess::new(&chain.app.borrow()) .propose_and_execute( CreateConcentratedLiquidityPoolsProposal::TYPE_URL.to_string(), CreateConcentratedLiquidityPoolsProposal { @@ -223,12 +223,13 @@ pub fn create_pool(mut chain: OsmosisTestTube) -> anyhow::Result<(u64, u64)> { &chain.sender, ) .unwrap(); + let test_tube = chain.app.borrow(); let cl = ConcentratedLiquidity::new(&*test_tube); let pools = cl.query_pools(&PoolsRequest { pagination: None }).unwrap(); - let pool = Pool::decode(pools.pools[0].value.as_slice()).unwrap(); + let pool = Pool::decode(pools.pools.last().unwrap().value.as_slice()).unwrap(); let _response = cl .create_position( MsgCreatePosition { diff --git a/contracts/carrot-app/tests/config.rs b/contracts/carrot-app/tests/config.rs index d0569efa..35508300 100644 --- a/contracts/carrot-app/tests/config.rs +++ b/contracts/carrot-app/tests/config.rs @@ -1,6 +1,6 @@ mod common; -use crate::common::{setup_test_tube, USDC, USDT}; +use crate::common::{create_pool, setup_test_tube, USDC, USDT}; use carrot_app::{ msg::{AppExecuteMsgFns, AppQueryMsgFns}, yield_sources::{ @@ -9,58 +9,63 @@ use carrot_app::{ }, }; use common::{INITIAL_LOWER_TICK, INITIAL_UPPER_TICK}; -use cosmwasm_std::Decimal; +use cosmwasm_std::{coins, Decimal, Uint128}; use cw_orch::anyhow; +use cw_orch::prelude::BankSetter; +use cw_orch::prelude::ContractInstance; #[test] fn rebalance_fails() -> anyhow::Result<()> { let (_, carrot_app) = setup_test_tube(false)?; carrot_app - .update_strategy(BalanceStrategy(vec![ - BalanceStrategyElement { - yield_source: YieldSource { - asset_distribution: vec![ - AssetShare { - denom: USDT.to_string(), - share: Decimal::percent(50), - }, - AssetShare { - denom: USDC.to_string(), - share: Decimal::percent(50), - }, - ], - ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { - pool_id: 7, - lower_tick: INITIAL_LOWER_TICK, - upper_tick: INITIAL_UPPER_TICK, - position_id: None, - }), + .update_strategy( + vec![], + BalanceStrategy(vec![ + BalanceStrategyElement { + yield_source: YieldSource { + asset_distribution: vec![ + AssetShare { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + AssetShare { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, + ], + ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + pool_id: 7, + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + }), + }, + share: Decimal::one(), }, - share: Decimal::one(), - }, - BalanceStrategyElement { - yield_source: YieldSource { - asset_distribution: vec![ - AssetShare { - denom: USDT.to_string(), - share: Decimal::percent(50), - }, - AssetShare { - denom: USDC.to_string(), - share: Decimal::percent(50), - }, - ], - ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { - pool_id: 7, - lower_tick: INITIAL_LOWER_TICK, - upper_tick: INITIAL_UPPER_TICK, - position_id: None, - }), + BalanceStrategyElement { + yield_source: YieldSource { + asset_distribution: vec![ + AssetShare { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + AssetShare { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, + ], + ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + pool_id: 7, + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + }), + }, + share: Decimal::one(), }, - share: Decimal::one(), - }, - ])) + ]), + ) .unwrap_err(); // We query the nex strategy @@ -116,7 +121,9 @@ fn rebalance_success() -> anyhow::Result<()> { share: Decimal::percent(50), }, ]); - carrot_app.update_strategy(new_strat.clone())?; + let strategy = carrot_app.strategy()?; + assert_ne!(strategy.strategy, new_strat); + carrot_app.update_strategy(vec![], new_strat.clone())?; // We query the new strategy let strategy = carrot_app.strategy()?; @@ -124,3 +131,266 @@ fn rebalance_success() -> anyhow::Result<()> { Ok(()) } + +#[test] +fn rebalance_with_new_pool_success() -> anyhow::Result<()> { + let (pool_id, carrot_app) = setup_test_tube(false)?; + let mut chain = carrot_app.get_chain().clone(); + let (new_pool_id, _) = create_pool(chain.clone())?; + + let deposit_amount = 10_000; + let deposit_coins = coins(deposit_amount, USDT); + + chain.add_balance( + carrot_app.account().proxy()?.to_string(), + deposit_coins.clone(), + )?; + + let new_strat = BalanceStrategy(vec![ + BalanceStrategyElement { + yield_source: YieldSource { + asset_distribution: vec![ + AssetShare { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + AssetShare { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, + ], + ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + pool_id, // Pool Id needs to exist + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + }), + }, + share: Decimal::percent(50), + }, + BalanceStrategyElement { + yield_source: YieldSource { + asset_distribution: vec![ + AssetShare { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + AssetShare { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, + ], + ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + pool_id: new_pool_id, + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + }), + }, + share: Decimal::percent(50), + }, + ]); + carrot_app.update_strategy(deposit_coins.clone(), new_strat.clone())?; + + carrot_app.strategy()?; + + // We query the balance + let balance = carrot_app.balance()?; + assert!(balance.total_value > Uint128::from(deposit_amount) * Decimal::percent(2)); + + let distribution = carrot_app.positions()?; + + // We make sure the total values are close between the 2 positions + let balance0 = distribution.positions[0].balance.total_value; + let balance1 = distribution.positions[1].balance.total_value; + let balance_diff = balance0 + .checked_sub(balance1) + .or(balance1.checked_sub(balance0))?; + assert!(balance_diff < Uint128::from(deposit_amount) * Decimal::permille(5)); + + Ok(()) +} + +#[test] +fn rebalance_with_stale_strategy_success() -> anyhow::Result<()> { + let (pool_id, carrot_app) = setup_test_tube(false)?; + let mut chain = carrot_app.get_chain().clone(); + let (new_pool_id, _) = create_pool(chain.clone())?; + + let deposit_amount = 10_000; + let deposit_coins = coins(deposit_amount, USDT); + + chain.add_balance( + carrot_app.account().proxy()?.to_string(), + deposit_coins.clone(), + )?; + let common_yield_source = YieldSource { + asset_distribution: vec![ + AssetShare { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + AssetShare { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, + ], + ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + pool_id, // Pool Id needs to exist + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + }), + }; + + let strat = BalanceStrategy(vec![ + BalanceStrategyElement { + yield_source: common_yield_source.clone(), + share: Decimal::percent(50), + }, + BalanceStrategyElement { + yield_source: YieldSource { + asset_distribution: vec![ + AssetShare { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + AssetShare { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, + ], + ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + pool_id: new_pool_id, + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + }), + }, + share: Decimal::percent(50), + }, + ]); + + carrot_app.update_strategy(deposit_coins.clone(), strat.clone())?; + + let new_strat = BalanceStrategy(vec![BalanceStrategyElement { + yield_source: common_yield_source.clone(), + share: Decimal::percent(100), + }]); + let total_value_before = carrot_app.balance()?.total_value; + + // No additional deposit + carrot_app.update_strategy(vec![], new_strat.clone())?; + + carrot_app.strategy()?; + + // We query the balance + let balance = carrot_app.balance()?; + // Make sure the deposit went almost all in + assert!(balance.total_value > Uint128::from(deposit_amount) * Decimal::percent(98)); + println!( + "Before :{}, after: {}", + total_value_before, balance.total_value + ); + // Make sure the total value has almost not changed when updating the strategy + assert!(balance.total_value > total_value_before * Decimal::permille(999)); + + let distribution = carrot_app.positions()?; + + // We make sure the total values are close between the 2 positions + assert_eq!(distribution.positions.len(), 1); + + Ok(()) +} + +#[test] +fn rebalance_with_current_and_stale_strategy_success() -> anyhow::Result<()> { + let (pool_id, carrot_app) = setup_test_tube(false)?; + let mut chain = carrot_app.get_chain().clone(); + let (new_pool_id, _) = create_pool(chain.clone())?; + + let deposit_amount = 10_000; + let deposit_coins = coins(deposit_amount, USDT); + + chain.add_balance( + carrot_app.account().proxy()?.to_string(), + deposit_coins.clone(), + )?; + let moving_strategy = YieldSource { + asset_distribution: vec![ + AssetShare { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + AssetShare { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, + ], + ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + pool_id: new_pool_id, + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + }), + }; + + let strat = BalanceStrategy(vec![ + BalanceStrategyElement { + yield_source: YieldSource { + asset_distribution: vec![ + AssetShare { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + AssetShare { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, + ], + ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + pool_id, // Pool Id needs to exist + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + }), + }, + share: Decimal::percent(50), + }, + BalanceStrategyElement { + yield_source: moving_strategy.clone(), + share: Decimal::percent(50), + }, + ]); + + carrot_app.update_strategy(deposit_coins.clone(), strat.clone())?; + + let mut strategies = carrot_app.strategy()?.strategy; + + strategies.0[1].yield_source = moving_strategy; + + let total_value_before = carrot_app.balance()?.total_value; + + // No additional deposit + carrot_app.update_strategy(vec![], strategies.clone())?; + + carrot_app.strategy()?; + + // We query the balance + let balance = carrot_app.balance()?; + // Make sure the deposit went almost all in + assert!(balance.total_value > Uint128::from(deposit_amount) * Decimal::percent(98)); + println!( + "Before :{}, after: {}", + total_value_before, balance.total_value + ); + // Make sure the total value has almost not changed when updating the strategy + assert!(balance.total_value > total_value_before * Decimal::permille(998)); + + let distribution = carrot_app.positions()?; + + // We make sure the total values are close between the 2 positions + assert_eq!(distribution.positions.len(), 2); + + Ok(()) +} diff --git a/contracts/carrot-app/tests/deposit_withdraw.rs b/contracts/carrot-app/tests/deposit_withdraw.rs index eff42c28..ff60d556 100644 --- a/contracts/carrot-app/tests/deposit_withdraw.rs +++ b/contracts/carrot-app/tests/deposit_withdraw.rs @@ -190,7 +190,7 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { share: Decimal::percent(50), }, ]); - carrot_app.update_strategy(new_strat.clone())?; + carrot_app.update_strategy(vec![], new_strat.clone())?; let deposit_amount = 5_000; let deposit_coins = coins(deposit_amount, USDT.to_owned()); @@ -272,7 +272,7 @@ fn deposit_multiple_positions_with_empty() -> anyhow::Result<()> { share: Decimal::percent(0), }, ]); - carrot_app.update_strategy(new_strat.clone())?; + carrot_app.update_strategy(vec![], new_strat.clone())?; let deposit_amount = 5_000; let deposit_coins = coins(deposit_amount, USDT.to_owned()); diff --git a/contracts/carrot-app/tests/pool_inbalance.rs b/contracts/carrot-app/tests/pool_inbalance.rs index 69a2e613..deebf02f 100644 --- a/contracts/carrot-app/tests/pool_inbalance.rs +++ b/contracts/carrot-app/tests/pool_inbalance.rs @@ -1,17 +1,8 @@ mod common; -use crate::common::{setup_test_tube, LOTS, USDC, USDT}; -use abstract_client::Application; -use carrot_app::{ - msg::{AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse}, - yield_sources::{ - mars::MarsDepositParams, osmosis_cl_pool::ConcentratedPoolParams, yield_type::YieldType, - AssetShare, BalanceStrategy, BalanceStrategyElement, YieldSource, - }, - AppInterface, -}; -use common::{INITIAL_LOWER_TICK, INITIAL_UPPER_TICK}; -use cosmwasm_std::{coin, coins, Decimal, Uint128}; +use crate::common::{setup_test_tube, USDC, USDT}; +use carrot_app::msg::AppExecuteMsgFns; +use cosmwasm_std::{coin, coins}; use cw_orch::{anyhow, prelude::*}; use osmosis_std::types::osmosis::{ gamm::v1beta1::{MsgSwapExactAmountIn, MsgSwapExactAmountInResponse}, @@ -19,21 +10,6 @@ use osmosis_std::types::osmosis::{ }; use prost_types::Any; -fn query_balances( - carrot_app: &Application>, -) -> anyhow::Result { - let balance = carrot_app.balance(); - if balance.is_err() { - return Ok(Uint128::zero()); - } - let sum = balance? - .balances - .iter() - .fold(Uint128::zero(), |acc, e| acc + e.amount); - - Ok(sum) -} - #[test] fn deposit_after_inbalance_works() -> anyhow::Result<()> { let (pool_id, carrot_app) = setup_test_tube(false)?; @@ -64,7 +40,7 @@ fn deposit_after_inbalance_works() -> anyhow::Result<()> { }], } .to_any(); - let resp = chain.commit_any::( + chain.commit_any::( vec![Any { type_url: swap_msg.type_url, value: swap_msg.value, @@ -80,19 +56,15 @@ fn deposit_after_inbalance_works() -> anyhow::Result<()> { chain.add_balance(proxy.to_string(), deposit_coins.clone())?; // // Do the second deposit - let response = carrot_app.deposit(vec![coin(deposit_amount, USDT.to_owned())], None)?; + carrot_app.deposit(vec![coin(deposit_amount, USDT.to_owned())], None)?; // Check almost everything landed let proxy_balance_after_second = chain .bank_querier() .balance(&proxy, Some(USDT.to_string()))?[0] .amount; - println!( - "balances : {:?}, {:?}", - proxy_balance_before_second, proxy_balance_after_second - ); - - panic!(); + // Assert second deposit is more efficient than the first one + assert!(proxy_balance_after_second - proxy_balance_before_second < proxy_balance_before_second); Ok(()) } From 615842ec21a0c85ee0a9f9530d2b8a11a300f6b4 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Thu, 4 Apr 2024 22:46:42 +0000 Subject: [PATCH 21/42] Added checked and unchecked features --- .../examples/install_savings_app.rs | 4 +- .../carrot-app/src/distribution/deposit.rs | 2 +- .../carrot-app/src/distribution/query.rs | 28 ++--- contracts/carrot-app/src/handlers/execute.rs | 23 ++-- .../carrot-app/src/handlers/instantiate.rs | 6 +- contracts/carrot-app/src/handlers/query.rs | 22 ++-- contracts/carrot-app/src/helpers.rs | 2 +- contracts/carrot-app/src/msg.rs | 13 +- .../src/replies/osmosis/add_to_position.rs | 10 +- .../src/replies/osmosis/create_position.rs | 10 +- contracts/carrot-app/src/yield_sources.rs | 115 +++++++++++++----- .../carrot-app/src/yield_sources/mars.rs | 15 ++- .../src/yield_sources/osmosis_cl_pool.rs | 25 +++- .../src/yield_sources/yield_type.rs | 33 ++++- contracts/carrot-app/tests/common.rs | 39 ++++-- contracts/carrot-app/tests/config.rs | 86 +++++++------ .../carrot-app/tests/deposit_withdraw.rs | 62 ++++++---- 17 files changed, 314 insertions(+), 181 deletions(-) diff --git a/contracts/carrot-app/examples/install_savings_app.rs b/contracts/carrot-app/examples/install_savings_app.rs index cf147462..1172454c 100644 --- a/contracts/carrot-app/examples/install_savings_app.rs +++ b/contracts/carrot-app/examples/install_savings_app.rs @@ -14,7 +14,7 @@ use carrot_app::{ autocompound::{AutocompoundConfig, AutocompoundRewardsConfig}, contract::OSMOSIS, msg::AppInstantiateMsg, - yield_sources::BalanceStrategy, + yield_sources::{BalanceStrategy, BalanceStrategyBase}, }; use osmosis_std::types::cosmos::authz::v1beta1::MsgGrantResponse; @@ -68,7 +68,7 @@ fn main() -> anyhow::Result<()> { max_gas_balance: Uint128::new(3000000), }, }, - balance_strategy: BalanceStrategy(vec![]), + balance_strategy: BalanceStrategyBase(vec![]), deposit: Some(coins(100, "usdc")), dex: OSMOSIS.to_string(), }; diff --git a/contracts/carrot-app/src/distribution/deposit.rs b/contracts/carrot-app/src/distribution/deposit.rs index 6e6fe530..f822946b 100644 --- a/contracts/carrot-app/src/distribution/deposit.rs +++ b/contracts/carrot-app/src/distribution/deposit.rs @@ -57,7 +57,7 @@ impl BalanceStrategy { // If value now is greater than the target value, we need to withdraw some funds from the protocol if target_value < value_now { - let this_withdraw_value = target_value - value_now; + let this_withdraw_value = value_now - target_value; // In the following line, total_value can't be zero, otherwise the if condition wouldn't be met let this_withdraw_share = Decimal::from_ratio(withdraw_value, total_value); let this_withdraw_funds = diff --git a/contracts/carrot-app/src/distribution/query.rs b/contracts/carrot-app/src/distribution/query.rs index ea238247..7be2f954 100644 --- a/contracts/carrot-app/src/distribution/query.rs +++ b/contracts/carrot-app/src/distribution/query.rs @@ -50,21 +50,21 @@ impl BalanceStrategy { } // Finally, we dispatch the total_value to get investment shares - Ok(BalanceStrategy( - self.0 - .iter() - .zip(all_strategy_values) - .map( - |(original_strategy, (value, shares))| BalanceStrategyElement { - yield_source: YieldSource { - asset_distribution: shares, - ty: original_strategy.yield_source.ty.clone(), - }, - share: Decimal::from_ratio(value, all_strategies_value), + Ok(self + .0 + .iter() + .zip(all_strategy_values) + .map( + |(original_strategy, (value, shares))| BalanceStrategyElement { + yield_source: YieldSource { + asset_distribution: shares, + ty: original_strategy.yield_source.ty.clone(), }, - ) - .collect(), - )) + share: Decimal::from_ratio(value, all_strategies_value), + }, + ) + .collect::>() + .into()) } /// This function applies the underlying shares inside yield sources to each yield source depending on the current strategy state diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index eafb2215..16c4a62f 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -5,7 +5,7 @@ use crate::{ helpers::assert_contract, msg::{AppExecuteMsg, ExecuteMsg, InternalExecuteMsg}, state::{AUTOCOMPOUND_STATE, CONFIG}, - yield_sources::{AssetShare, BalanceStrategy}, + yield_sources::{AssetShare, BalanceStrategyUnchecked, Checkable}, }; use abstract_app::abstract_sdk::features::AbstractResponse; use abstract_sdk::ExecutorMsg; @@ -14,10 +14,7 @@ use cosmwasm_std::{ WasmMsg, }; -use super::{ - internal::{deposit_one_strategy, execute_finalize_deposit, execute_one_deposit_step}, - query::query_strategy, -}; +use super::internal::{deposit_one_strategy, execute_finalize_deposit, execute_one_deposit_step}; use abstract_app::traits::AccountIdentification; pub fn execute_handler( @@ -101,7 +98,7 @@ fn update_strategy( deps: DepsMut, env: Env, _info: MessageInfo, - strategy: BalanceStrategy, + strategy: BalanceStrategyUnchecked, funds: Vec, app: App, ) -> AppResult { @@ -110,8 +107,7 @@ fn update_strategy( let old_strategy = config.balance_strategy; // We check the new strategy - strategy.check(deps.as_ref(), &app)?; - deps.api.debug("After strategy check"); + let strategy = strategy.check(deps.as_ref(), &app)?; // We execute operations to rebalance the funds between the strategies let mut available_funds: Coins = funds.try_into()?; @@ -172,7 +168,7 @@ fn update_strategy( fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResult { // Everyone can autocompound - let strategy = query_strategy(deps.as_ref())?.strategy; + let strategy = CONFIG.load(deps.storage)?.balance_strategy; // We withdraw all rewards from protocols let (all_rewards, collect_rewards_msgs) = strategy.withdraw_rewards(deps.as_ref(), &app)?; @@ -243,7 +239,7 @@ pub fn _inner_deposit( app: &App, ) -> AppResult> { // We query the target strategy depending on the existing deposits - let mut current_strategy_status = query_strategy(deps)?.strategy; + let mut current_strategy_status = CONFIG.load(deps.storage)?.balance_strategy; current_strategy_status.apply_current_strategy_shares(deps, app)?; // We correct it if the user asked to correct the share parameters of each strategy @@ -261,7 +257,7 @@ pub fn _inner_advanced_deposit( app: &App, ) -> AppResult> { // This is the storage strategy for all assets - let target_strategy = query_strategy(deps)?.strategy; + let target_strategy = CONFIG.load(deps.storage)?.balance_strategy; // This is the current distribution of funds inside the strategies let current_strategy_status = target_strategy.query_current_status(deps, app)?; @@ -313,8 +309,9 @@ fn _inner_withdraw( // We withdraw the necessary share from all registered investments let withdraw_msgs = - query_strategy(deps.as_ref())? - .strategy + CONFIG + .load(deps.storage)? + .balance_strategy .withdraw(deps.as_ref(), withdraw_share, app)?; Ok(withdraw_msgs.into_iter().collect()) diff --git a/contracts/carrot-app/src/handlers/instantiate.rs b/contracts/carrot-app/src/handlers/instantiate.rs index ae3a97c9..5a90011c 100644 --- a/contracts/carrot-app/src/handlers/instantiate.rs +++ b/contracts/carrot-app/src/handlers/instantiate.rs @@ -3,6 +3,7 @@ use crate::{ contract::{App, AppResult}, msg::AppInstantiateMsg, state::{Config, AUTOCOMPOUND_STATE, CONFIG}, + yield_sources::Checkable, }; use abstract_app::abstract_sdk::{features::AbstractNameService, AbstractResponse}; use cosmwasm_std::{DepsMut, Env, MessageInfo}; @@ -16,9 +17,6 @@ pub fn instantiate_handler( app: App, msg: AppInstantiateMsg, ) -> AppResult { - // We check the balance strategy is valid - msg.balance_strategy.check(deps.as_ref(), &app)?; - // We don't check the dex on instantiation // We query the ANS for useful information on the tokens and pool @@ -31,7 +29,7 @@ pub fn instantiate_handler( let config: Config = Config { dex: msg.dex, - balance_strategy: msg.balance_strategy, + balance_strategy: msg.balance_strategy.check(deps.as_ref(), &app)?, autocompound_config: msg.autocompound_config, }; CONFIG.save(deps.storage, &config)?; diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index f33fe8e8..2f4fae66 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -89,15 +89,18 @@ pub fn query_strategy(deps: Deps) -> AppResult { let config = CONFIG.load(deps.storage)?; Ok(StrategyResponse { - strategy: config.balance_strategy, + strategy: config.balance_strategy.into(), }) } pub fn query_strategy_status(deps: Deps, app: &App) -> AppResult { - let strategy = query_strategy(deps)?.strategy; + let config = CONFIG.load(deps.storage)?; Ok(StrategyResponse { - strategy: strategy.query_current_status(deps, app)?, + strategy: config + .balance_strategy + .query_current_status(deps, app)? + .into(), }) } @@ -108,7 +111,9 @@ fn query_config(deps: Deps) -> AppResult { pub fn query_balance(deps: Deps, app: &App) -> AppResult { let mut funds = Coins::default(); let mut total_value = Uint128::zero(); - query_strategy(deps)?.strategy.0.iter().try_for_each(|s| { + + let config = CONFIG.load(deps.storage)?; + config.balance_strategy.0.iter().try_for_each(|s| { let deposit_value = s .yield_source .ty @@ -129,7 +134,7 @@ pub fn query_balance(deps: Deps, app: &App) -> AppResult } fn query_rewards(deps: Deps, app: &App) -> AppResult { - let strategy = query_strategy(deps)?.strategy; + let strategy = CONFIG.load(deps.storage)?.balance_strategy; let mut rewards = Coins::default(); strategy.0.into_iter().try_for_each(|s| { @@ -147,8 +152,9 @@ fn query_rewards(deps: Deps, app: &App) -> AppResult { pub fn query_positions(deps: Deps, app: &App) -> AppResult { Ok(PositionsResponse { - positions: query_strategy(deps)? - .strategy + positions: CONFIG + .load(deps.storage)? + .balance_strategy .0 .into_iter() .map(|s| { @@ -164,7 +170,7 @@ pub fn query_positions(deps: Deps, app: &App) -> AppResult { .sum::>()?; Ok::<_, AppError>(PositionResponse { - ty: s.yield_source.ty, + ty: s.yield_source.ty.into(), balance: AssetsBalanceResponse { balances: balance, total_value, diff --git a/contracts/carrot-app/src/helpers.rs b/contracts/carrot-app/src/helpers.rs index c353856f..b579c145 100644 --- a/contracts/carrot-app/src/helpers.rs +++ b/contracts/carrot-app/src/helpers.rs @@ -38,7 +38,7 @@ pub const CLOSE_PER_MILLE: u64 = 1; /// Returns wether actual is close to expected within CLOSE_PER_MILLE per mille pub fn close_to(expected: Decimal, actual: Decimal) -> bool { - let close_coeff = expected * Decimal::permille(CLOSE_PER_MILLE); + let close_coeff = Decimal::permille(CLOSE_PER_MILLE); if expected == Decimal::zero() { return actual < close_coeff; diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index 35222396..e3cd3f00 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -6,7 +6,10 @@ use crate::{ autocompound::AutocompoundConfig, contract::App, distribution::deposit::OneDepositStrategy, - yield_sources::{yield_type::YieldType, AssetShare, BalanceStrategy}, + yield_sources::{ + yield_type::{YieldType, YieldTypeUnchecked}, + AssetShare, BalanceStrategyUnchecked, + }, }; // This is used for type safety and re-exporting the contract endpoint structs. @@ -16,7 +19,7 @@ abstract_app::app_msg_types!(App, AppExecuteMsg, AppQueryMsg); #[cosmwasm_schema::cw_serde] pub struct AppInstantiateMsg { /// Strategy to use to dispatch the deposited funds - pub balance_strategy: BalanceStrategy, + pub balance_strategy: BalanceStrategyUnchecked, /// Configuration of the aut-compounding procedure pub autocompound_config: AutocompoundConfig, /// Target dex to swap things on @@ -51,7 +54,7 @@ pub enum AppExecuteMsg { Autocompound {}, /// Rebalances all investments according to a new balance strategy UpdateStrategy { - strategy: BalanceStrategy, + strategy: BalanceStrategyUnchecked, funds: Vec, }, @@ -145,7 +148,7 @@ pub struct AssetsBalanceResponse { #[cw_serde] pub struct StrategyResponse { - pub strategy: BalanceStrategy, + pub strategy: BalanceStrategyUnchecked, } #[cw_serde] @@ -155,7 +158,7 @@ pub struct PositionsResponse { #[cw_serde] pub struct PositionResponse { - pub ty: YieldType, + pub ty: YieldTypeUnchecked, pub balance: AssetsBalanceResponse, pub liquidity: Uint128, } diff --git a/contracts/carrot-app/src/replies/osmosis/add_to_position.rs b/contracts/carrot-app/src/replies/osmosis/add_to_position.rs index 90a0101b..5b3646fa 100644 --- a/contracts/carrot-app/src/replies/osmosis/add_to_position.rs +++ b/contracts/carrot-app/src/replies/osmosis/add_to_position.rs @@ -5,8 +5,8 @@ use osmosis_std::types::osmosis::concentratedliquidity::v1beta1::MsgAddToPositio use crate::{ contract::{App, AppResult}, error::AppError, - handlers::{internal::save_strategy, query::query_strategy}, - state::TEMP_CURRENT_YIELD, + handlers::internal::save_strategy, + state::{CONFIG, TEMP_CURRENT_YIELD}, yield_sources::yield_type::YieldType, }; @@ -25,9 +25,9 @@ pub fn add_to_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) - // We update the position let current_position_index = TEMP_CURRENT_YIELD.load(deps.storage)?; - let mut strategy = query_strategy(deps.as_ref())?; + let mut strategy = CONFIG.load(deps.storage)?.balance_strategy; - let current_yield = strategy.strategy.0.get_mut(current_position_index).unwrap(); + let current_yield = strategy.0.get_mut(current_position_index).unwrap(); current_yield.yield_source.ty = match current_yield.yield_source.ty.clone() { YieldType::ConcentratedLiquidityPool(mut position) => { @@ -37,7 +37,7 @@ pub fn add_to_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) - YieldType::Mars(_) => return Err(AppError::WrongYieldType {}), }; - save_strategy(deps, strategy.strategy)?; + save_strategy(deps, strategy)?; Ok(app .response("create_position_reply") diff --git a/contracts/carrot-app/src/replies/osmosis/create_position.rs b/contracts/carrot-app/src/replies/osmosis/create_position.rs index 46b7bde3..b3b9e2dc 100644 --- a/contracts/carrot-app/src/replies/osmosis/create_position.rs +++ b/contracts/carrot-app/src/replies/osmosis/create_position.rs @@ -5,8 +5,8 @@ use osmosis_std::types::osmosis::concentratedliquidity::v1beta1::MsgCreatePositi use crate::{ contract::{App, AppResult}, error::AppError, - handlers::{internal::save_strategy, query::query_strategy}, - state::TEMP_CURRENT_YIELD, + handlers::internal::save_strategy, + state::{CONFIG, TEMP_CURRENT_YIELD}, yield_sources::yield_type::YieldType, }; @@ -24,9 +24,9 @@ pub fn create_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) - // We save the position let current_position_index = TEMP_CURRENT_YIELD.load(deps.storage)?; - let mut strategy = query_strategy(deps.as_ref())?; + let mut strategy = CONFIG.load(deps.storage)?.balance_strategy; - let current_yield = strategy.strategy.0.get_mut(current_position_index).unwrap(); + let current_yield = strategy.0.get_mut(current_position_index).unwrap(); current_yield.yield_source.ty = match current_yield.yield_source.ty.clone() { YieldType::ConcentratedLiquidityPool(mut position) => { @@ -36,7 +36,7 @@ pub fn create_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) - YieldType::Mars(_) => return Err(AppError::WrongYieldType {}), }; - save_strategy(deps, strategy.strategy)?; + save_strategy(deps, strategy)?; Ok(app .response("create_position_reply") diff --git a/contracts/carrot-app/src/yield_sources.rs b/contracts/carrot-app/src/yield_sources.rs index 8c76727a..66eef6fa 100644 --- a/contracts/carrot-app/src/yield_sources.rs +++ b/contracts/carrot-app/src/yield_sources.rs @@ -10,23 +10,34 @@ use crate::{ contract::{App, AppResult}, error::AppError, helpers::close_to, - yield_sources::yield_type::YieldTypeImplementation, + yield_sources::yield_type::YieldTypeBase, }; use abstract_app::traits::AbstractNameService; -use self::yield_type::YieldType; - /// A yield sources has the following elements /// A vector of tokens that NEED to be deposited inside the yield source with a repartition of tokens /// A type that allows routing to the right smart-contract integration internally #[cw_serde] -pub struct YieldSource { +pub struct YieldSourceBase { pub asset_distribution: Vec, - pub ty: YieldType, + pub ty: YieldTypeBase, } -impl YieldSource { - pub fn check(&self, deps: Deps, app: &App) -> AppResult<()> { +pub type YieldSourceUnchecked = YieldSourceBase; +pub type YieldSource = YieldSourceBase; + +impl From for YieldSourceUnchecked { + fn from(value: YieldSource) -> Self { + Self { + asset_distribution: value.asset_distribution, + ty: value.ty.into(), + } + } +} + +impl Checkable for YieldSourceUnchecked { + type CheckOutput = YieldSource; + fn check(self, deps: Deps, app: &App) -> AppResult { // First we check the share sums the 100 let share_sum: Decimal = self.asset_distribution.iter().map(|e| e.share).sum(); ensure!( @@ -51,18 +62,17 @@ impl YieldSource { ) .map_err(|_| AppError::AssetsNotRegistered(all_denoms))?; - // Then we check every yield strategy underneath - match &self.ty { - YieldType::ConcentratedLiquidityPool(params) => { + let ty = match self.ty { + YieldTypeBase::ConcentratedLiquidityPool(params) => { // A valid CL pool strategy is for 2 assets ensure_eq!( self.asset_distribution.len(), 2, AppError::InvalidStrategy {} ); - params.check(deps)?; + YieldTypeBase::ConcentratedLiquidityPool(params.check(deps, app)?) } - YieldType::Mars(params) => { + YieldTypeBase::Mars(params) => { // We verify there is only one element in the shares vector ensure_eq!( self.asset_distribution.len(), @@ -75,13 +85,18 @@ impl YieldSource { params.denom, AppError::InvalidStrategy {} ); - params.check(deps)?; + YieldTypeBase::Mars(params.check(deps, app)?) } - } + }; - Ok(()) + Ok(YieldSource { + asset_distribution: self.asset_distribution, + ty, + }) } +} +impl YieldSourceBase { pub fn all_denoms(&self) -> Vec { self.asset_distribution .iter() @@ -105,13 +120,33 @@ pub enum ShareType { Fixed, } +#[cw_serde] +pub struct Checked; +#[cw_serde] +pub struct Unchecked; + +pub trait Checkable { + type CheckOutput; + fn check(self, deps: Deps, app: &App) -> AppResult; +} + // This represents a balance strategy // This object is used for storing the current strategy, retrieving the actual strategy status or expressing a target strategy when depositing #[cw_serde] -pub struct BalanceStrategy(pub Vec); +pub struct BalanceStrategyBase(pub Vec>); -impl BalanceStrategy { - pub fn check(&self, deps: Deps, app: &App) -> AppResult<()> { +pub type BalanceStrategyUnchecked = BalanceStrategyBase; +pub type BalanceStrategy = BalanceStrategyBase; + +impl From for BalanceStrategyUnchecked { + fn from(value: BalanceStrategy) -> Self { + Self(value.0.into_iter().map(Into::into).collect()) + } +} + +impl Checkable for BalanceStrategyUnchecked { + type CheckOutput = BalanceStrategy; + fn check(self, deps: Deps, app: &App) -> AppResult { // First we check the share sums the 100 let share_sum: Decimal = self.0.iter().map(|e| e.share).sum(); ensure!( @@ -121,13 +156,18 @@ impl BalanceStrategy { ensure!(!self.0.is_empty(), AppError::InvalidEmptyStrategy {}); // Then we check every yield strategy underneath - for yield_source in &self.0 { - yield_source.check(deps, app)?; - } - Ok(()) + let checked = self + .0 + .into_iter() + .map(|yield_source| yield_source.check(deps, app)) + .collect::, _>>()?; + + Ok(checked.into()) } +} +impl BalanceStrategy { pub fn all_denoms(&self) -> Vec { self.0 .clone() @@ -137,19 +177,36 @@ impl BalanceStrategy { } } -impl From> for BalanceStrategy { - fn from(value: Vec) -> Self { +impl From>> for BalanceStrategyBase { + fn from(value: Vec>) -> Self { Self(value) } } #[cw_serde] -pub struct BalanceStrategyElement { - pub yield_source: YieldSource, +pub struct BalanceStrategyElementBase { + pub yield_source: YieldSourceBase, pub share: Decimal, } -impl BalanceStrategyElement { - pub fn check(&self, deps: Deps, app: &App) -> AppResult<()> { - self.yield_source.check(deps, app) + +pub type BalanceStrategyElementUnchecked = BalanceStrategyElementBase; +pub type BalanceStrategyElement = BalanceStrategyElementBase; + +impl From for BalanceStrategyElementUnchecked { + fn from(value: BalanceStrategyElement) -> Self { + Self { + yield_source: value.yield_source.into(), + share: value.share, + } + } +} +impl Checkable for BalanceStrategyElementUnchecked { + type CheckOutput = BalanceStrategyElement; + fn check(self, deps: Deps, app: &App) -> AppResult { + let yield_source = self.yield_source.check(deps, app)?; + Ok(BalanceStrategyElement { + yield_source, + share: self.share, + }) } } diff --git a/contracts/carrot-app/src/yield_sources/mars.rs b/contracts/carrot-app/src/yield_sources/mars.rs index ccba3b74..f3d0654b 100644 --- a/contracts/carrot-app/src/yield_sources/mars.rs +++ b/contracts/carrot-app/src/yield_sources/mars.rs @@ -10,7 +10,7 @@ use cw_asset::AssetInfo; use abstract_money_market_standard::query::MoneyMarketAnsQuery; use super::yield_type::YieldTypeImplementation; -use super::ShareType; +use super::{Checkable, ShareType}; pub const MARS_MONEY_MARKET: &str = "mars"; @@ -18,6 +18,15 @@ pub const MARS_MONEY_MARKET: &str = "mars"; pub struct MarsDepositParams { pub denom: String, } + +impl Checkable for MarsDepositParams { + type CheckOutput = MarsDepositParams; + + fn check(self, _deps: Deps, _app: &App) -> AppResult { + Ok(self) + } +} + impl YieldTypeImplementation for MarsDepositParams { fn deposit(self, deps: Deps, funds: Vec, app: &App) -> AppResult> { let ans = app.name_service(deps); @@ -81,8 +90,4 @@ impl YieldTypeImplementation for MarsDepositParams { fn share_type(&self) -> super::ShareType { ShareType::Fixed } - - fn check(&self, _deps: Deps) -> AppResult<()> { - Ok(()) - } } diff --git a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs index 52ca259c..7d2f0e2e 100644 --- a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs +++ b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs @@ -1,4 +1,4 @@ -use std::str::FromStr; +use std::{marker::PhantomData, str::FromStr}; use crate::{ contract::{App, AppResult}, @@ -24,10 +24,10 @@ use osmosis_std::{ }, }; -use super::{yield_type::YieldTypeImplementation, ShareType}; +use super::{yield_type::YieldTypeImplementation, Checkable, Checked, ShareType, Unchecked}; #[cw_serde] -pub struct ConcentratedPoolParams { +pub struct ConcentratedPoolParamsBase { // This is part of the pool parameters pub pool_id: u64, // This is part of the pool parameters @@ -38,19 +38,32 @@ pub struct ConcentratedPoolParams { // This is not actually a parameter but rather state // This can be used as a parameter for existing positions pub position_id: Option, + pub _phantom: PhantomData, } -impl YieldTypeImplementation for ConcentratedPoolParams { - fn check(&self, deps: Deps) -> AppResult<()> { +pub type ConcentratedPoolParamsUnchecked = ConcentratedPoolParamsBase; +pub type ConcentratedPoolParams = ConcentratedPoolParamsBase; + +impl Checkable for ConcentratedPoolParamsUnchecked { + type CheckOutput = ConcentratedPoolParams; + fn check(self, deps: Deps, _app: &App) -> AppResult { let _pool: Pool = PoolmanagerQuerier::new(&deps.querier) .pool(self.pool_id) .map_err(|_| AppError::PoolNotFound {})? .pool .ok_or(AppError::PoolNotFound {})? .try_into()?; - Ok(()) + Ok(ConcentratedPoolParams { + pool_id: self.pool_id, + lower_tick: self.lower_tick, + upper_tick: self.upper_tick, + position_id: self.position_id, + _phantom: PhantomData, + }) } +} +impl YieldTypeImplementation for ConcentratedPoolParams { fn deposit(self, deps: Deps, funds: Vec, app: &App) -> AppResult> { // We verify there is a position stored if self.position(deps).is_ok() { diff --git a/contracts/carrot-app/src/yield_sources/yield_type.rs b/contracts/carrot-app/src/yield_sources/yield_type.rs index 9f914cc3..82b7b5cf 100644 --- a/contracts/carrot-app/src/yield_sources/yield_type.rs +++ b/contracts/carrot-app/src/yield_sources/yield_type.rs @@ -3,16 +3,40 @@ use cosmwasm_std::{Coin, CosmosMsg, Deps, SubMsg, Uint128}; use crate::contract::{App, AppResult}; -use super::{mars::MarsDepositParams, osmosis_cl_pool::ConcentratedPoolParams, ShareType}; +use super::{ + mars::MarsDepositParams, osmosis_cl_pool::ConcentratedPoolParamsBase, Checked, ShareType, + Unchecked, +}; +// This however is not checkable by itself, because the check also depends on the asset share distribution #[cw_serde] -pub enum YieldType { - ConcentratedLiquidityPool(ConcentratedPoolParams), +pub enum YieldTypeBase { + ConcentratedLiquidityPool(ConcentratedPoolParamsBase), /// For Mars, you just need to deposit in the RedBank /// You need to indicate the denom of the funds you want to deposit Mars(MarsDepositParams), } +pub type YieldTypeUnchecked = YieldTypeBase; +pub type YieldType = YieldTypeBase; + +impl From for YieldTypeUnchecked { + fn from(value: YieldType) -> Self { + match value { + YieldTypeBase::ConcentratedLiquidityPool(params) => { + YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + pool_id: params.pool_id, + lower_tick: params.lower_tick, + upper_tick: params.upper_tick, + position_id: params.position_id, + _phantom: std::marker::PhantomData, + }) + } + YieldTypeBase::Mars(params) => YieldTypeBase::Mars(params), + } + } +} + impl YieldType { pub fn deposit(self, deps: Deps, funds: Vec, app: &App) -> AppResult> { if funds.is_empty() { @@ -97,7 +121,4 @@ pub trait YieldTypeImplementation { /// CL pools use that to know the best funds deposit ratio /// Mars doesn't use that, because the share is fixed to 1 fn share_type(&self) -> ShareType; - - /// Verifies the yield type is valid - fn check(&self, deps: Deps) -> AppResult<()>; } diff --git a/contracts/carrot-app/tests/common.rs b/contracts/carrot-app/tests/common.rs index c31ecfd6..e25a69a0 100644 --- a/contracts/carrot-app/tests/common.rs +++ b/contracts/carrot-app/tests/common.rs @@ -5,11 +5,14 @@ use abstract_client::{AbstractClient, Application, Namespace}; use carrot_app::autocompound::{AutocompoundConfig, AutocompoundRewardsConfig}; use carrot_app::contract::OSMOSIS; use carrot_app::msg::AppInstantiateMsg; -use carrot_app::yield_sources::osmosis_cl_pool::ConcentratedPoolParams; -use carrot_app::yield_sources::yield_type::YieldType; -use carrot_app::yield_sources::{AssetShare, BalanceStrategy, BalanceStrategyElement, YieldSource}; -use cosmwasm_std::{coin, coins, Decimal, Uint128, Uint64}; +use carrot_app::yield_sources::osmosis_cl_pool::ConcentratedPoolParamsBase; +use carrot_app::yield_sources::yield_type::YieldTypeBase; +use carrot_app::yield_sources::{ + AssetShare, BalanceStrategyBase, BalanceStrategyElementBase, YieldSourceBase, +}; +use cosmwasm_std::{coin, coins, Coins, Decimal, Uint128, Uint64}; use cw_asset::AssetInfoUnchecked; +use cw_orch::environment::MutCwEnv; use cw_orch::osmosis_test_tube::osmosis_test_tube::Gamm; use cw_orch::{ anyhow, @@ -45,8 +48,8 @@ pub const SPREAD_FACTOR: u64 = 1; pub const INITIAL_LOWER_TICK: i64 = -100000; pub const INITIAL_UPPER_TICK: i64 = 10000; // Deploys abstract and other contracts -pub fn deploy( - chain: Chain, +pub fn deploy( + mut chain: Chain, pool_id: u64, gas_pool_id: u64, initial_deposit: Option>, @@ -87,6 +90,7 @@ pub fn deploy( // We deploy the carrot_app let publisher = client .publisher_builder(Namespace::new("abstract")?) + .install_on_sub_account(false) .build()?; // The dex adapter let dex_adapter = publisher @@ -109,6 +113,10 @@ pub fn deploy( // The savings app publisher.publish_app::>()?; + if let Some(deposit) = &initial_deposit { + chain.add_balance(publisher.account().proxy()?.to_string(), deposit.clone())?; + } + let init_msg = AppInstantiateMsg { // 5 mins autocompound_config: AutocompoundConfig { @@ -121,8 +129,8 @@ pub fn deploy( max_gas_balance: Uint128::new(10000), }, }, - balance_strategy: BalanceStrategy(vec![BalanceStrategyElement { - yield_source: YieldSource { + balance_strategy: BalanceStrategyBase(vec![BalanceStrategyElementBase { + yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { denom: USDT.to_string(), @@ -133,11 +141,12 @@ pub fn deploy( share: Decimal::percent(50), }, ], - ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, position_id: None, + _phantom: std::marker::PhantomData, }), }, share: Decimal::one(), @@ -284,9 +293,15 @@ pub fn setup_test_tube( // We create a usdt-usdc pool let (pool_id, gas_pool_id) = create_pool(chain.clone())?; - let initial_deposit = create_position.then(|| - // TODO: Requires instantiate2 to test it (we need to give authz authorization before instantiating) - vec![coin(1_000_000, USDT),coin(1_000_000, USDC)]); + let initial_deposit: Option> = create_position + .then(|| { + // TODO: Requires instantiate2 to test it (we need to give authz authorization before instantiating) + let mut initial_coins = Coins::default(); + initial_coins.add(coin(10_000, USDT))?; + initial_coins.add(coin(10_000, USDC))?; + Ok::<_, anyhow::Error>(initial_coins.into()) + }) + .transpose()?; let carrot_app = deploy(chain.clone(), pool_id, gas_pool_id, initial_deposit)?; Ok((pool_id, carrot_app)) diff --git a/contracts/carrot-app/tests/config.rs b/contracts/carrot-app/tests/config.rs index 35508300..6d7bf6a7 100644 --- a/contracts/carrot-app/tests/config.rs +++ b/contracts/carrot-app/tests/config.rs @@ -4,8 +4,8 @@ use crate::common::{create_pool, setup_test_tube, USDC, USDT}; use carrot_app::{ msg::{AppExecuteMsgFns, AppQueryMsgFns}, yield_sources::{ - osmosis_cl_pool::ConcentratedPoolParams, yield_type::YieldType, AssetShare, - BalanceStrategy, BalanceStrategyElement, YieldSource, + osmosis_cl_pool::ConcentratedPoolParamsBase, yield_type::YieldTypeBase, AssetShare, + BalanceStrategyBase, BalanceStrategyElementBase, YieldSourceBase, }, }; use common::{INITIAL_LOWER_TICK, INITIAL_UPPER_TICK}; @@ -21,9 +21,9 @@ fn rebalance_fails() -> anyhow::Result<()> { carrot_app .update_strategy( vec![], - BalanceStrategy(vec![ - BalanceStrategyElement { - yield_source: YieldSource { + BalanceStrategyBase(vec![ + BalanceStrategyElementBase { + yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { denom: USDT.to_string(), @@ -34,17 +34,18 @@ fn rebalance_fails() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id: 7, lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, position_id: None, + _phantom: std::marker::PhantomData, }), }, share: Decimal::one(), }, - BalanceStrategyElement { - yield_source: YieldSource { + BalanceStrategyElementBase { + yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { denom: USDT.to_string(), @@ -55,11 +56,12 @@ fn rebalance_fails() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id: 7, lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, position_id: None, + _phantom: std::marker::PhantomData, }), }, share: Decimal::one(), @@ -77,9 +79,9 @@ fn rebalance_fails() -> anyhow::Result<()> { fn rebalance_success() -> anyhow::Result<()> { let (pool_id, carrot_app) = setup_test_tube(false)?; - let new_strat = BalanceStrategy(vec![ - BalanceStrategyElement { - yield_source: YieldSource { + let new_strat = BalanceStrategyBase(vec![ + BalanceStrategyElementBase { + yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { denom: USDT.to_string(), @@ -90,17 +92,18 @@ fn rebalance_success() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, // Pool Id needs to exist lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, position_id: None, + _phantom: std::marker::PhantomData, }), }, share: Decimal::percent(50), }, - BalanceStrategyElement { - yield_source: YieldSource { + BalanceStrategyElementBase { + yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { denom: USDT.to_string(), @@ -111,11 +114,12 @@ fn rebalance_success() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, // Pool Id needs to exist lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, position_id: None, + _phantom: std::marker::PhantomData, }), }, share: Decimal::percent(50), @@ -146,9 +150,9 @@ fn rebalance_with_new_pool_success() -> anyhow::Result<()> { deposit_coins.clone(), )?; - let new_strat = BalanceStrategy(vec![ - BalanceStrategyElement { - yield_source: YieldSource { + let new_strat = BalanceStrategyBase(vec![ + BalanceStrategyElementBase { + yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { denom: USDT.to_string(), @@ -159,17 +163,18 @@ fn rebalance_with_new_pool_success() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, // Pool Id needs to exist lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, position_id: None, + _phantom: std::marker::PhantomData, }), }, share: Decimal::percent(50), }, - BalanceStrategyElement { - yield_source: YieldSource { + BalanceStrategyElementBase { + yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { denom: USDT.to_string(), @@ -180,11 +185,12 @@ fn rebalance_with_new_pool_success() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id: new_pool_id, lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, position_id: None, + _phantom: std::marker::PhantomData, }), }, share: Decimal::percent(50), @@ -224,7 +230,7 @@ fn rebalance_with_stale_strategy_success() -> anyhow::Result<()> { carrot_app.account().proxy()?.to_string(), deposit_coins.clone(), )?; - let common_yield_source = YieldSource { + let common_yield_source = YieldSourceBase { asset_distribution: vec![ AssetShare { denom: USDT.to_string(), @@ -235,21 +241,22 @@ fn rebalance_with_stale_strategy_success() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, // Pool Id needs to exist lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, position_id: None, + _phantom: std::marker::PhantomData, }), }; - let strat = BalanceStrategy(vec![ - BalanceStrategyElement { + let strat = BalanceStrategyBase(vec![ + BalanceStrategyElementBase { yield_source: common_yield_source.clone(), share: Decimal::percent(50), }, - BalanceStrategyElement { - yield_source: YieldSource { + BalanceStrategyElementBase { + yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { denom: USDT.to_string(), @@ -260,11 +267,12 @@ fn rebalance_with_stale_strategy_success() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id: new_pool_id, lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, position_id: None, + _phantom: std::marker::PhantomData, }), }, share: Decimal::percent(50), @@ -273,7 +281,7 @@ fn rebalance_with_stale_strategy_success() -> anyhow::Result<()> { carrot_app.update_strategy(deposit_coins.clone(), strat.clone())?; - let new_strat = BalanceStrategy(vec![BalanceStrategyElement { + let new_strat = BalanceStrategyBase(vec![BalanceStrategyElementBase { yield_source: common_yield_source.clone(), share: Decimal::percent(100), }]); @@ -316,7 +324,7 @@ fn rebalance_with_current_and_stale_strategy_success() -> anyhow::Result<()> { carrot_app.account().proxy()?.to_string(), deposit_coins.clone(), )?; - let moving_strategy = YieldSource { + let moving_strategy = YieldSourceBase { asset_distribution: vec![ AssetShare { denom: USDT.to_string(), @@ -327,17 +335,18 @@ fn rebalance_with_current_and_stale_strategy_success() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id: new_pool_id, lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, position_id: None, + _phantom: std::marker::PhantomData, }), }; - let strat = BalanceStrategy(vec![ - BalanceStrategyElement { - yield_source: YieldSource { + let strat = BalanceStrategyBase(vec![ + BalanceStrategyElementBase { + yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { denom: USDT.to_string(), @@ -348,16 +357,17 @@ fn rebalance_with_current_and_stale_strategy_success() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, // Pool Id needs to exist lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, position_id: None, + _phantom: std::marker::PhantomData, }), }, share: Decimal::percent(50), }, - BalanceStrategyElement { + BalanceStrategyElementBase { yield_source: moving_strategy.clone(), share: Decimal::percent(50), }, diff --git a/contracts/carrot-app/tests/deposit_withdraw.rs b/contracts/carrot-app/tests/deposit_withdraw.rs index ff60d556..18f54981 100644 --- a/contracts/carrot-app/tests/deposit_withdraw.rs +++ b/contracts/carrot-app/tests/deposit_withdraw.rs @@ -5,8 +5,9 @@ use abstract_client::Application; use carrot_app::{ msg::{AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse}, yield_sources::{ - mars::MarsDepositParams, osmosis_cl_pool::ConcentratedPoolParams, yield_type::YieldType, - AssetShare, BalanceStrategy, BalanceStrategyElement, YieldSource, + mars::MarsDepositParams, osmosis_cl_pool::ConcentratedPoolParamsBase, + yield_type::YieldTypeBase, AssetShare, BalanceStrategyBase, BalanceStrategyElementBase, + YieldSourceBase, }, AppInterface, }; @@ -146,9 +147,9 @@ fn deposit_multiple_assets() -> anyhow::Result<()> { fn deposit_multiple_positions() -> anyhow::Result<()> { let (pool_id, carrot_app) = setup_test_tube(false)?; - let new_strat = BalanceStrategy(vec![ - BalanceStrategyElement { - yield_source: YieldSource { + let new_strat = BalanceStrategyBase(vec![ + BalanceStrategyElementBase { + yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { denom: USDT.to_string(), @@ -159,17 +160,18 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, position_id: None, + _phantom: std::marker::PhantomData, }), }, share: Decimal::percent(50), }, - BalanceStrategyElement { - yield_source: YieldSource { + BalanceStrategyElementBase { + yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { denom: USDT.to_string(), @@ -180,11 +182,12 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, lower_tick: 2 * INITIAL_LOWER_TICK, upper_tick: 2 * INITIAL_UPPER_TICK, position_id: None, + _phantom: std::marker::PhantomData, }), }, share: Decimal::percent(50), @@ -216,9 +219,9 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { fn deposit_multiple_positions_with_empty() -> anyhow::Result<()> { let (pool_id, carrot_app) = setup_test_tube(false)?; - let new_strat = BalanceStrategy(vec![ - BalanceStrategyElement { - yield_source: YieldSource { + let new_strat = BalanceStrategyBase(vec![ + BalanceStrategyElementBase { + yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { denom: USDT.to_string(), @@ -229,17 +232,18 @@ fn deposit_multiple_positions_with_empty() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, position_id: None, + _phantom: std::marker::PhantomData, }), }, share: Decimal::percent(50), }, - BalanceStrategyElement { - yield_source: YieldSource { + BalanceStrategyElementBase { + yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { denom: USDT.to_string(), @@ -250,22 +254,23 @@ fn deposit_multiple_positions_with_empty() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldType::ConcentratedLiquidityPool(ConcentratedPoolParams { + ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, lower_tick: 2 * INITIAL_LOWER_TICK, upper_tick: 2 * INITIAL_UPPER_TICK, position_id: None, + _phantom: std::marker::PhantomData, }), }, share: Decimal::percent(50), }, - BalanceStrategyElement { - yield_source: YieldSource { + BalanceStrategyElementBase { + yield_source: YieldSourceBase { asset_distribution: vec![AssetShare { denom: USDT.to_string(), share: Decimal::percent(100), }], - ty: YieldType::Mars(MarsDepositParams { + ty: YieldTypeBase::Mars(MarsDepositParams { denom: USDT.to_string(), }), }, @@ -294,15 +299,18 @@ fn deposit_multiple_positions_with_empty() -> anyhow::Result<()> { ); Ok(()) } -// #[test] -// fn create_position_on_instantiation() -> anyhow::Result<()> { -// let (_, carrot_app) = setup_test_tube(true)?; -// carrot_app.deposit(vec![coin(258, USDT.to_owned()), coin(234, USDC.to_owned())])?; -// let position: OsmosisPositionResponse = carrot_app.position()?; -// assert!(position.position.is_some()); -// Ok(()) -// } +#[test] +fn create_position_on_instantiation() -> anyhow::Result<()> { + let (_, carrot_app) = setup_test_tube(true)?; + + let position = carrot_app.positions()?; + assert!(!position.positions.is_empty()); + + let balance = carrot_app.balance()?; + assert!(balance.total_value > Uint128::from(20_000u128) * Decimal::percent(99)); + Ok(()) +} // #[test] // fn withdraw_after_user_withdraw_liquidity_manually() -> anyhow::Result<()> { From 1babe2f6f6268fdc9c92762772e1d5ac75e8aec8 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Thu, 4 Apr 2024 23:04:39 +0000 Subject: [PATCH 22/42] Checks on config --- .../examples/install_savings_app.rs | 29 +++++--- contracts/carrot-app/src/autocompound.rs | 63 ++++++++++++++--- .../carrot-app/src/handlers/instantiate.rs | 20 ++---- contracts/carrot-app/src/msg.rs | 12 ++-- contracts/carrot-app/src/state.rs | 45 ++++++++++-- contracts/carrot-app/src/yield_sources.rs | 2 +- contracts/carrot-app/tests/common.rs | 70 ++++++++++--------- 7 files changed, 158 insertions(+), 83 deletions(-) diff --git a/contracts/carrot-app/examples/install_savings_app.rs b/contracts/carrot-app/examples/install_savings_app.rs index 1172454c..f13c2190 100644 --- a/contracts/carrot-app/examples/install_savings_app.rs +++ b/contracts/carrot-app/examples/install_savings_app.rs @@ -11,9 +11,13 @@ use cw_orch::{ use dotenv::dotenv; use carrot_app::{ - autocompound::{AutocompoundConfig, AutocompoundRewardsConfig}, + autocompound::{ + AutocompoundConfig, AutocompoundConfigBase, AutocompoundRewardsConfig, + AutocompoundRewardsConfigBase, + }, contract::OSMOSIS, msg::AppInstantiateMsg, + state::ConfigBase, yield_sources::{BalanceStrategy, BalanceStrategyBase}, }; use osmosis_std::types::cosmos::authz::v1beta1::MsgGrantResponse; @@ -58,19 +62,22 @@ fn main() -> anyhow::Result<()> { let mut msgs = vec![]; let init_msg = AppInstantiateMsg { - autocompound_config: AutocompoundConfig { - cooldown_seconds: Uint64::new(AUTOCOMPOUND_COOLDOWN_SECONDS), - rewards: AutocompoundRewardsConfig { - gas_asset: AssetEntry::new(utils::REWARD_ASSET), - swap_asset: app_data.swap_asset, - reward: Uint128::new(50_000), - min_gas_balance: Uint128::new(1000000), - max_gas_balance: Uint128::new(3000000), + config: ConfigBase { + autocompound_config: AutocompoundConfigBase { + cooldown_seconds: Uint64::new(AUTOCOMPOUND_COOLDOWN_SECONDS), + rewards: AutocompoundRewardsConfigBase { + gas_asset: AssetEntry::new(utils::REWARD_ASSET), + swap_asset: app_data.swap_asset, + reward: Uint128::new(50_000), + min_gas_balance: Uint128::new(1000000), + max_gas_balance: Uint128::new(3000000), + _phantom: std::marker::PhantomData, + }, }, + balance_strategy: BalanceStrategyBase(vec![]), + dex: OSMOSIS.to_string(), }, - balance_strategy: BalanceStrategyBase(vec![]), deposit: Some(coins(100, "usdc")), - dex: OSMOSIS.to_string(), }; let create_sub_account_message = utils::create_account_message(&client, init_msg)?; diff --git a/contracts/carrot-app/src/autocompound.rs b/contracts/carrot-app/src/autocompound.rs index 4a526b32..d6ffa934 100644 --- a/contracts/carrot-app/src/autocompound.rs +++ b/contracts/carrot-app/src/autocompound.rs @@ -1,5 +1,8 @@ -use abstract_app::abstract_sdk::{feature_objects::AnsHost, Resolve}; +use std::marker::PhantomData; + +use abstract_app::abstract_sdk::Resolve; use abstract_app::objects::AnsAsset; +use abstract_app::traits::AbstractNameService; use abstract_app::{abstract_core::objects::AssetEntry, objects::DexAssetPairing}; use abstract_dex_adapter::DexInterface; use abstract_sdk::{Execution, TransferInterface}; @@ -12,23 +15,36 @@ use crate::contract::App; use crate::handlers::swap_helpers::swap_msg; use crate::msg::CompoundStatus; use crate::state::{Config, AUTOCOMPOUND_STATE}; +use crate::yield_sources::{Checked, Unchecked}; use crate::{contract::AppResult, error::AppError}; +pub type AutocompoundConfig = AutocompoundConfigBase; +pub type AutocompoundConfigUnchecked = AutocompoundConfigBase; + /// General auto-compound parameters. /// Includes the cool down and the technical funds config #[cw_serde] -pub struct AutocompoundConfig { +pub struct AutocompoundConfigBase { /// Seconds to wait before autocompound is incentivized. /// Allows the user to configure when the auto-compound happens pub cooldown_seconds: Uint64, /// Configuration of rewards to the address who helped to execute autocompound - pub rewards: AutocompoundRewardsConfig, + pub rewards: AutocompoundRewardsConfigBase, +} + +impl From for AutocompoundConfigUnchecked { + fn from(value: AutocompoundConfig) -> Self { + Self { + cooldown_seconds: value.cooldown_seconds, + rewards: value.rewards.into(), + } + } } /// Configuration on how rewards should be distributed /// to the address who helped to execute autocompound #[cw_serde] -pub struct AutocompoundRewardsConfig { +pub struct AutocompoundRewardsConfigBase { /// Gas denominator for this chain pub gas_asset: AssetEntry, /// Denominator of the asset that will be used for swap to the gas asset @@ -39,10 +55,32 @@ pub struct AutocompoundRewardsConfig { pub min_gas_balance: Uint128, /// Upper bound of gas tokens expected after the swap pub max_gas_balance: Uint128, + pub _phantom: PhantomData, } -impl AutocompoundRewardsConfig { - pub fn check(&self, deps: Deps, dex_name: &str, ans_host: &AnsHost) -> AppResult<()> { +pub type AutocompoundRewardsConfigUnchecked = AutocompoundRewardsConfigBase; +pub type AutocompoundRewardsConfig = AutocompoundRewardsConfigBase; + +impl From for AutocompoundRewardsConfigUnchecked { + fn from(value: AutocompoundRewardsConfig) -> Self { + Self { + gas_asset: value.gas_asset, + swap_asset: value.swap_asset, + reward: value.reward, + min_gas_balance: value.min_gas_balance, + max_gas_balance: value.max_gas_balance, + _phantom: PhantomData, + } + } +} + +impl AutocompoundRewardsConfigUnchecked { + pub fn check( + self, + deps: Deps, + app: &App, + dex_name: &str, + ) -> AppResult { ensure!( self.reward <= self.min_gas_balance, AppError::RewardConfigError( @@ -58,9 +96,16 @@ impl AutocompoundRewardsConfig { // Check swap asset has pairing into gas asset DexAssetPairing::new(self.gas_asset.clone(), self.swap_asset.clone(), dex_name) - .resolve(&deps.querier, ans_host)?; - - Ok(()) + .resolve(&deps.querier, app.name_service(deps).host())?; + + Ok(AutocompoundRewardsConfig { + gas_asset: self.gas_asset, + swap_asset: self.swap_asset, + reward: self.reward, + min_gas_balance: self.min_gas_balance, + max_gas_balance: self.max_gas_balance, + _phantom: PhantomData, + }) } } diff --git a/contracts/carrot-app/src/handlers/instantiate.rs b/contracts/carrot-app/src/handlers/instantiate.rs index 5a90011c..cb9acda2 100644 --- a/contracts/carrot-app/src/handlers/instantiate.rs +++ b/contracts/carrot-app/src/handlers/instantiate.rs @@ -2,10 +2,10 @@ use crate::{ autocompound::AutocompoundState, contract::{App, AppResult}, msg::AppInstantiateMsg, - state::{Config, AUTOCOMPOUND_STATE, CONFIG}, + state::{AUTOCOMPOUND_STATE, CONFIG}, yield_sources::Checkable, }; -use abstract_app::abstract_sdk::{features::AbstractNameService, AbstractResponse}; +use abstract_app::abstract_sdk::AbstractResponse; use cosmwasm_std::{DepsMut, Env, MessageInfo}; use super::execute::_inner_deposit; @@ -17,21 +17,9 @@ pub fn instantiate_handler( app: App, msg: AppInstantiateMsg, ) -> AppResult { - // We don't check the dex on instantiation + // Check validity of registered config + let config = msg.config.check(deps.as_ref(), &app)?; - // We query the ANS for useful information on the tokens and pool - let ans = app.name_service(deps.as_ref()); - - // Check validity of autocompound rewards - msg.autocompound_config - .rewards - .check(deps.as_ref(), &msg.dex, ans.host())?; - - let config: Config = Config { - dex: msg.dex, - balance_strategy: msg.balance_strategy.check(deps.as_ref(), &app)?, - autocompound_config: msg.autocompound_config, - }; CONFIG.save(deps.storage, &config)?; AUTOCOMPOUND_STATE.save( deps.storage, diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index e3cd3f00..4949dd0d 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -3,9 +3,9 @@ use cosmwasm_std::{Coin, Uint128, Uint64}; use cw_asset::AssetBase; use crate::{ - autocompound::AutocompoundConfig, contract::App, distribution::deposit::OneDepositStrategy, + state::ConfigUnchecked, yield_sources::{ yield_type::{YieldType, YieldTypeUnchecked}, AssetShare, BalanceStrategyUnchecked, @@ -18,12 +18,8 @@ abstract_app::app_msg_types!(App, AppExecuteMsg, AppQueryMsg); /// App instantiate message #[cosmwasm_schema::cw_serde] pub struct AppInstantiateMsg { - /// Strategy to use to dispatch the deposited funds - pub balance_strategy: BalanceStrategyUnchecked, - /// Configuration of the aut-compounding procedure - pub autocompound_config: AutocompoundConfig, - /// Target dex to swap things on - pub dex: String, + /// Future app configuration + pub config: ConfigUnchecked, /// Create position with instantiation. /// Will not create position if omitted pub deposit: Option>, @@ -100,7 +96,7 @@ impl From #[cfg_attr(feature = "interface", impl_into(QueryMsg))] #[derive(QueryResponses)] pub enum AppQueryMsg { - #[returns(crate::state::Config)] + #[returns(ConfigUnchecked)] Config {}, #[returns(AssetsBalanceResponse)] Balance {}, diff --git a/contracts/carrot-app/src/state.rs b/contracts/carrot-app/src/state.rs index 0aea9329..20072cd9 100644 --- a/contracts/carrot-app/src/state.rs +++ b/contracts/carrot-app/src/state.rs @@ -2,8 +2,8 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin, Uint128}; use cw_storage_plus::Item; -use crate::autocompound::{AutocompoundConfig, AutocompoundState}; -use crate::yield_sources::BalanceStrategy; +use crate::autocompound::{AutocompoundConfigBase, AutocompoundState}; +use crate::yield_sources::{BalanceStrategyBase, Checkable, Checked, Unchecked}; pub const CONFIG: Item = Item::new("config"); pub const AUTOCOMPOUND_STATE: Item = Item::new("position"); @@ -15,9 +15,44 @@ pub const TEMP_EXPECTED_SWAP_COIN: Item = Item::new("temp_expected_swap pub const TEMP_DEPOSIT_COINS: Item> = Item::new("temp_deposit_coins"); pub const TEMP_CURRENT_YIELD: Item = Item::new("temp_current_yield_type"); +pub type Config = ConfigBase; +pub type ConfigUnchecked = ConfigBase; + +impl From for ConfigUnchecked { + fn from(value: Config) -> Self { + Self { + balance_strategy: value.balance_strategy.into(), + autocompound_config: value.autocompound_config.into(), + dex: value.dex, + } + } +} + #[cw_serde] -pub struct Config { - pub balance_strategy: BalanceStrategy, - pub autocompound_config: AutocompoundConfig, +pub struct ConfigBase { + pub balance_strategy: BalanceStrategyBase, + pub autocompound_config: AutocompoundConfigBase, pub dex: String, } + +impl Checkable for ConfigUnchecked { + type CheckOutput = Config; + + fn check( + self, + deps: cosmwasm_std::Deps, + app: &crate::contract::App, + ) -> crate::contract::AppResult { + Ok(Config { + balance_strategy: self.balance_strategy.check(deps, app)?, + autocompound_config: AutocompoundConfigBase { + cooldown_seconds: self.autocompound_config.cooldown_seconds, + rewards: self + .autocompound_config + .rewards + .check(deps, app, &self.dex)?, + }, + dex: self.dex, + }) + } +} diff --git a/contracts/carrot-app/src/yield_sources.rs b/contracts/carrot-app/src/yield_sources.rs index 66eef6fa..f93edb1e 100644 --- a/contracts/carrot-app/src/yield_sources.rs +++ b/contracts/carrot-app/src/yield_sources.rs @@ -133,7 +133,7 @@ pub trait Checkable { // This represents a balance strategy // This object is used for storing the current strategy, retrieving the actual strategy status or expressing a target strategy when depositing #[cw_serde] -pub struct BalanceStrategyBase(pub Vec>); +pub struct BalanceStrategyBase(pub Vec>); pub type BalanceStrategyUnchecked = BalanceStrategyBase; pub type BalanceStrategy = BalanceStrategyBase; diff --git a/contracts/carrot-app/tests/common.rs b/contracts/carrot-app/tests/common.rs index e25a69a0..8d693473 100644 --- a/contracts/carrot-app/tests/common.rs +++ b/contracts/carrot-app/tests/common.rs @@ -2,9 +2,10 @@ use abstract_app::abstract_core::objects::{ pool_id::PoolAddressBase, AssetEntry, PoolMetadata, PoolType, }; use abstract_client::{AbstractClient, Application, Namespace}; -use carrot_app::autocompound::{AutocompoundConfig, AutocompoundRewardsConfig}; +use carrot_app::autocompound::{AutocompoundConfigBase, AutocompoundRewardsConfigBase}; use carrot_app::contract::OSMOSIS; use carrot_app::msg::AppInstantiateMsg; +use carrot_app::state::ConfigBase; use carrot_app::yield_sources::osmosis_cl_pool::ConcentratedPoolParamsBase; use carrot_app::yield_sources::yield_type::YieldTypeBase; use carrot_app::yield_sources::{ @@ -118,41 +119,44 @@ pub fn deploy( } let init_msg = AppInstantiateMsg { - // 5 mins - autocompound_config: AutocompoundConfig { - cooldown_seconds: Uint64::new(300), - rewards: AutocompoundRewardsConfig { - gas_asset: AssetEntry::new(REWARD_ASSET), - swap_asset: AssetEntry::new(USDC), - reward: Uint128::new(1000), - min_gas_balance: Uint128::new(2000), - max_gas_balance: Uint128::new(10000), - }, - }, - balance_strategy: BalanceStrategyBase(vec![BalanceStrategyElementBase { - yield_source: YieldSourceBase { - asset_distribution: vec![ - AssetShare { - denom: USDT.to_string(), - share: Decimal::percent(50), - }, - AssetShare { - denom: USDC.to_string(), - share: Decimal::percent(50), - }, - ], - ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { - pool_id, - lower_tick: INITIAL_LOWER_TICK, - upper_tick: INITIAL_UPPER_TICK, - position_id: None, + config: ConfigBase { + // 5 mins + autocompound_config: AutocompoundConfigBase { + cooldown_seconds: Uint64::new(300), + rewards: AutocompoundRewardsConfigBase { + gas_asset: AssetEntry::new(REWARD_ASSET), + swap_asset: AssetEntry::new(USDC), + reward: Uint128::new(1000), + min_gas_balance: Uint128::new(2000), + max_gas_balance: Uint128::new(10000), _phantom: std::marker::PhantomData, - }), + }, }, - share: Decimal::one(), - }]), + balance_strategy: BalanceStrategyBase(vec![BalanceStrategyElementBase { + yield_source: YieldSourceBase { + asset_distribution: vec![ + AssetShare { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + AssetShare { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, + ], + ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + pool_id, + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + _phantom: std::marker::PhantomData, + }), + }, + share: Decimal::one(), + }]), + dex: OSMOSIS.to_string(), + }, deposit: initial_deposit, - dex: OSMOSIS.to_string(), }; // We install the carrot-app From a142f88a9ceea60dcf23dc0e836ccfa6ab3f7822 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Fri, 5 Apr 2024 08:19:31 +0000 Subject: [PATCH 23/42] Moved checks --- contracts/carrot-app/src/autocompound.rs | 60 +--- contracts/carrot-app/src/check.rs | 317 ++++++++++++++++++ contracts/carrot-app/src/handlers/execute.rs | 3 +- .../carrot-app/src/handlers/instantiate.rs | 2 +- contracts/carrot-app/src/lib.rs | 1 + contracts/carrot-app/src/state.rs | 35 +- contracts/carrot-app/src/yield_sources.rs | 212 ------------ .../carrot-app/src/yield_sources/mars.rs | 10 +- contracts/carrot-app/src/yield_sources/mod.rs | 79 +++++ .../src/yield_sources/osmosis_cl_pool.rs | 32 +- .../src/yield_sources/yield_type.rs | 27 +- 11 files changed, 417 insertions(+), 361 deletions(-) create mode 100644 contracts/carrot-app/src/check.rs delete mode 100644 contracts/carrot-app/src/yield_sources.rs create mode 100644 contracts/carrot-app/src/yield_sources/mod.rs diff --git a/contracts/carrot-app/src/autocompound.rs b/contracts/carrot-app/src/autocompound.rs index d6ffa934..a49c338d 100644 --- a/contracts/carrot-app/src/autocompound.rs +++ b/contracts/carrot-app/src/autocompound.rs @@ -1,22 +1,18 @@ use std::marker::PhantomData; -use abstract_app::abstract_sdk::Resolve; +use abstract_app::abstract_core::objects::AssetEntry; use abstract_app::objects::AnsAsset; -use abstract_app::traits::AbstractNameService; -use abstract_app::{abstract_core::objects::AssetEntry, objects::DexAssetPairing}; use abstract_dex_adapter::DexInterface; use abstract_sdk::{Execution, TransferInterface}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ - ensure, Addr, CosmosMsg, Deps, Env, MessageInfo, Storage, Timestamp, Uint128, Uint64, -}; +use cosmwasm_std::{Addr, CosmosMsg, Deps, Env, MessageInfo, Storage, Timestamp, Uint128, Uint64}; +use crate::check::{Checked, Unchecked}; use crate::contract::App; +use crate::contract::AppResult; use crate::handlers::swap_helpers::swap_msg; use crate::msg::CompoundStatus; use crate::state::{Config, AUTOCOMPOUND_STATE}; -use crate::yield_sources::{Checked, Unchecked}; -use crate::{contract::AppResult, error::AppError}; pub type AutocompoundConfig = AutocompoundConfigBase; pub type AutocompoundConfigUnchecked = AutocompoundConfigBase; @@ -61,54 +57,6 @@ pub struct AutocompoundRewardsConfigBase { pub type AutocompoundRewardsConfigUnchecked = AutocompoundRewardsConfigBase; pub type AutocompoundRewardsConfig = AutocompoundRewardsConfigBase; -impl From for AutocompoundRewardsConfigUnchecked { - fn from(value: AutocompoundRewardsConfig) -> Self { - Self { - gas_asset: value.gas_asset, - swap_asset: value.swap_asset, - reward: value.reward, - min_gas_balance: value.min_gas_balance, - max_gas_balance: value.max_gas_balance, - _phantom: PhantomData, - } - } -} - -impl AutocompoundRewardsConfigUnchecked { - pub fn check( - self, - deps: Deps, - app: &App, - dex_name: &str, - ) -> AppResult { - ensure!( - self.reward <= self.min_gas_balance, - AppError::RewardConfigError( - "reward should be lower or equal to the min_gas_balance".to_owned() - ) - ); - ensure!( - self.max_gas_balance > self.min_gas_balance, - AppError::RewardConfigError( - "max_gas_balance has to be bigger than min_gas_balance".to_owned() - ) - ); - - // Check swap asset has pairing into gas asset - DexAssetPairing::new(self.gas_asset.clone(), self.swap_asset.clone(), dex_name) - .resolve(&deps.querier, app.name_service(deps).host())?; - - Ok(AutocompoundRewardsConfig { - gas_asset: self.gas_asset, - swap_asset: self.swap_asset, - reward: self.reward, - min_gas_balance: self.min_gas_balance, - max_gas_balance: self.max_gas_balance, - _phantom: PhantomData, - }) - } -} - /// Autocompound related methods impl Config { pub fn get_executor_reward_messages( diff --git a/contracts/carrot-app/src/check.rs b/contracts/carrot-app/src/check.rs new file mode 100644 index 00000000..e5123616 --- /dev/null +++ b/contracts/carrot-app/src/check.rs @@ -0,0 +1,317 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Deps; + +use crate::contract::{App, AppResult}; + +#[cw_serde] +pub struct Checked; +#[cw_serde] +pub struct Unchecked; + +pub trait Checkable { + type CheckOutput; + fn check(self, deps: Deps, app: &App) -> AppResult; +} + +mod config { + use std::marker::PhantomData; + + use abstract_app::{ + abstract_sdk::Resolve, objects::DexAssetPairing, traits::AbstractNameService, + }; + use cosmwasm_std::{ensure, Deps}; + + use crate::{ + autocompound::{ + AutocompoundConfigBase, AutocompoundRewardsConfig, AutocompoundRewardsConfigUnchecked, + }, + contract::{App, AppResult}, + error::AppError, + state::{Config, ConfigUnchecked}, + }; + + use super::Checkable; + impl From for AutocompoundRewardsConfigUnchecked { + fn from(value: AutocompoundRewardsConfig) -> Self { + Self { + gas_asset: value.gas_asset, + swap_asset: value.swap_asset, + reward: value.reward, + min_gas_balance: value.min_gas_balance, + max_gas_balance: value.max_gas_balance, + _phantom: PhantomData, + } + } + } + + impl AutocompoundRewardsConfigUnchecked { + pub fn check( + self, + deps: Deps, + app: &App, + dex_name: &str, + ) -> AppResult { + ensure!( + self.reward <= self.min_gas_balance, + AppError::RewardConfigError( + "reward should be lower or equal to the min_gas_balance".to_owned() + ) + ); + ensure!( + self.max_gas_balance > self.min_gas_balance, + AppError::RewardConfigError( + "max_gas_balance has to be bigger than min_gas_balance".to_owned() + ) + ); + + // Check swap asset has pairing into gas asset + DexAssetPairing::new(self.gas_asset.clone(), self.swap_asset.clone(), dex_name) + .resolve(&deps.querier, app.name_service(deps).host())?; + + Ok(AutocompoundRewardsConfig { + gas_asset: self.gas_asset, + swap_asset: self.swap_asset, + reward: self.reward, + min_gas_balance: self.min_gas_balance, + max_gas_balance: self.max_gas_balance, + _phantom: PhantomData, + }) + } + } + + impl From for ConfigUnchecked { + fn from(value: Config) -> Self { + Self { + balance_strategy: value.balance_strategy.into(), + autocompound_config: value.autocompound_config.into(), + dex: value.dex, + } + } + } + + impl Checkable for ConfigUnchecked { + type CheckOutput = Config; + + fn check( + self, + deps: cosmwasm_std::Deps, + app: &crate::contract::App, + ) -> crate::contract::AppResult { + Ok(Config { + balance_strategy: self.balance_strategy.check(deps, app)?, + autocompound_config: AutocompoundConfigBase { + cooldown_seconds: self.autocompound_config.cooldown_seconds, + rewards: self + .autocompound_config + .rewards + .check(deps, app, &self.dex)?, + }, + dex: self.dex, + }) + } + } +} + +mod yield_sources { + use std::marker::PhantomData; + + use cosmwasm_std::{ensure, ensure_eq, Decimal, Deps}; + use cw_asset::AssetInfo; + use osmosis_std::types::osmosis::{ + concentratedliquidity::v1beta1::Pool, poolmanager::v1beta1::PoolmanagerQuerier, + }; + + use crate::{ + contract::{App, AppResult}, + error::AppError, + helpers::close_to, + yield_sources::{ + osmosis_cl_pool::{ + ConcentratedPoolParams, ConcentratedPoolParamsBase, ConcentratedPoolParamsUnchecked, + }, + yield_type::{YieldType, YieldTypeBase, YieldTypeUnchecked}, + BalanceStrategy, BalanceStrategyElement, BalanceStrategyElementUnchecked, + BalanceStrategyUnchecked, YieldSource, YieldSourceUnchecked, + }, + }; + + use super::Checkable; + + mod params { + use crate::yield_sources::mars::MarsDepositParams; + + use super::*; + impl Checkable for ConcentratedPoolParamsUnchecked { + type CheckOutput = ConcentratedPoolParams; + fn check(self, deps: Deps, _app: &App) -> AppResult { + let _pool: Pool = PoolmanagerQuerier::new(&deps.querier) + .pool(self.pool_id) + .map_err(|_| AppError::PoolNotFound {})? + .pool + .ok_or(AppError::PoolNotFound {})? + .try_into()?; + Ok(ConcentratedPoolParams { + pool_id: self.pool_id, + lower_tick: self.lower_tick, + upper_tick: self.upper_tick, + position_id: self.position_id, + _phantom: PhantomData, + }) + } + } + + impl Checkable for MarsDepositParams { + type CheckOutput = MarsDepositParams; + + fn check(self, _deps: Deps, _app: &App) -> AppResult { + Ok(self) + } + } + } + mod yield_type { + use super::*; + + impl From for YieldTypeUnchecked { + fn from(value: YieldType) -> Self { + match value { + YieldTypeBase::ConcentratedLiquidityPool(params) => { + YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + pool_id: params.pool_id, + lower_tick: params.lower_tick, + upper_tick: params.upper_tick, + position_id: params.position_id, + _phantom: std::marker::PhantomData, + }) + } + YieldTypeBase::Mars(params) => YieldTypeBase::Mars(params), + } + } + } + } + mod yield_source { + use super::*; + use abstract_app::traits::AbstractNameService; + + impl From for YieldSourceUnchecked { + fn from(value: YieldSource) -> Self { + Self { + asset_distribution: value.asset_distribution, + ty: value.ty.into(), + } + } + } + + impl Checkable for YieldSourceUnchecked { + type CheckOutput = YieldSource; + fn check(self, deps: Deps, app: &App) -> AppResult { + // First we check the share sums the 100 + let share_sum: Decimal = self.asset_distribution.iter().map(|e| e.share).sum(); + ensure!( + close_to(Decimal::one(), share_sum), + AppError::InvalidStrategySum { share_sum } + ); + // We make sure that assets are associated with this strategy + ensure!( + !self.asset_distribution.is_empty(), + AppError::InvalidEmptyStrategy {} + ); + // We ensure all deposited tokens exist in ANS + let all_denoms = self.all_denoms(); + let ans = app.name_service(deps); + ans.host() + .query_assets_reverse( + &deps.querier, + &all_denoms + .iter() + .map(|denom| AssetInfo::native(denom.clone())) + .collect::>(), + ) + .map_err(|_| AppError::AssetsNotRegistered(all_denoms))?; + + let ty = match self.ty { + YieldTypeBase::ConcentratedLiquidityPool(params) => { + // A valid CL pool strategy is for 2 assets + ensure_eq!( + self.asset_distribution.len(), + 2, + AppError::InvalidStrategy {} + ); + YieldTypeBase::ConcentratedLiquidityPool(params.check(deps, app)?) + } + YieldTypeBase::Mars(params) => { + // We verify there is only one element in the shares vector + ensure_eq!( + self.asset_distribution.len(), + 1, + AppError::InvalidStrategy {} + ); + // We verify the first element correspond to the mars deposit denom + ensure_eq!( + self.asset_distribution[0].denom, + params.denom, + AppError::InvalidStrategy {} + ); + YieldTypeBase::Mars(params.check(deps, app)?) + } + }; + + Ok(YieldSource { + asset_distribution: self.asset_distribution, + ty, + }) + } + } + } + + mod balance_strategy { + use super::*; + + impl From for BalanceStrategyElementUnchecked { + fn from(value: BalanceStrategyElement) -> Self { + Self { + yield_source: value.yield_source.into(), + share: value.share, + } + } + } + impl Checkable for BalanceStrategyElementUnchecked { + type CheckOutput = BalanceStrategyElement; + fn check(self, deps: Deps, app: &App) -> AppResult { + let yield_source = self.yield_source.check(deps, app)?; + Ok(BalanceStrategyElement { + yield_source, + share: self.share, + }) + } + } + + impl From for BalanceStrategyUnchecked { + fn from(value: BalanceStrategy) -> Self { + Self(value.0.into_iter().map(Into::into).collect()) + } + } + + impl Checkable for BalanceStrategyUnchecked { + type CheckOutput = BalanceStrategy; + fn check(self, deps: Deps, app: &App) -> AppResult { + // First we check the share sums the 100 + let share_sum: Decimal = self.0.iter().map(|e| e.share).sum(); + ensure!( + close_to(Decimal::one(), share_sum), + AppError::InvalidStrategySum { share_sum } + ); + ensure!(!self.0.is_empty(), AppError::InvalidEmptyStrategy {}); + + // Then we check every yield strategy underneath + + let checked = self + .0 + .into_iter() + .map(|yield_source| yield_source.check(deps, app)) + .collect::, _>>()?; + + Ok(checked.into()) + } + } + } +} diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index 16c4a62f..35887219 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -1,11 +1,12 @@ use crate::{ + check::Checkable, contract::{App, AppResult}, error::AppError, handlers::query::query_balance, helpers::assert_contract, msg::{AppExecuteMsg, ExecuteMsg, InternalExecuteMsg}, state::{AUTOCOMPOUND_STATE, CONFIG}, - yield_sources::{AssetShare, BalanceStrategyUnchecked, Checkable}, + yield_sources::{AssetShare, BalanceStrategyUnchecked}, }; use abstract_app::abstract_sdk::features::AbstractResponse; use abstract_sdk::ExecutorMsg; diff --git a/contracts/carrot-app/src/handlers/instantiate.rs b/contracts/carrot-app/src/handlers/instantiate.rs index cb9acda2..290aa3ea 100644 --- a/contracts/carrot-app/src/handlers/instantiate.rs +++ b/contracts/carrot-app/src/handlers/instantiate.rs @@ -1,9 +1,9 @@ use crate::{ autocompound::AutocompoundState, + check::Checkable, contract::{App, AppResult}, msg::AppInstantiateMsg, state::{AUTOCOMPOUND_STATE, CONFIG}, - yield_sources::Checkable, }; use abstract_app::abstract_sdk::AbstractResponse; use cosmwasm_std::{DepsMut, Env, MessageInfo}; diff --git a/contracts/carrot-app/src/lib.rs b/contracts/carrot-app/src/lib.rs index ce732ba5..90580176 100644 --- a/contracts/carrot-app/src/lib.rs +++ b/contracts/carrot-app/src/lib.rs @@ -1,4 +1,5 @@ pub mod autocompound; +pub mod check; pub mod contract; pub mod distribution; pub mod error; diff --git a/contracts/carrot-app/src/state.rs b/contracts/carrot-app/src/state.rs index 20072cd9..f8e09afa 100644 --- a/contracts/carrot-app/src/state.rs +++ b/contracts/carrot-app/src/state.rs @@ -3,7 +3,8 @@ use cosmwasm_std::{Addr, Coin, Uint128}; use cw_storage_plus::Item; use crate::autocompound::{AutocompoundConfigBase, AutocompoundState}; -use crate::yield_sources::{BalanceStrategyBase, Checkable, Checked, Unchecked}; +use crate::check::{Checked, Unchecked}; +use crate::yield_sources::BalanceStrategyBase; pub const CONFIG: Item = Item::new("config"); pub const AUTOCOMPOUND_STATE: Item = Item::new("position"); @@ -18,41 +19,9 @@ pub const TEMP_CURRENT_YIELD: Item = Item::new("temp_current_yield_type") pub type Config = ConfigBase; pub type ConfigUnchecked = ConfigBase; -impl From for ConfigUnchecked { - fn from(value: Config) -> Self { - Self { - balance_strategy: value.balance_strategy.into(), - autocompound_config: value.autocompound_config.into(), - dex: value.dex, - } - } -} - #[cw_serde] pub struct ConfigBase { pub balance_strategy: BalanceStrategyBase, pub autocompound_config: AutocompoundConfigBase, pub dex: String, } - -impl Checkable for ConfigUnchecked { - type CheckOutput = Config; - - fn check( - self, - deps: cosmwasm_std::Deps, - app: &crate::contract::App, - ) -> crate::contract::AppResult { - Ok(Config { - balance_strategy: self.balance_strategy.check(deps, app)?, - autocompound_config: AutocompoundConfigBase { - cooldown_seconds: self.autocompound_config.cooldown_seconds, - rewards: self - .autocompound_config - .rewards - .check(deps, app, &self.dex)?, - }, - dex: self.dex, - }) - } -} diff --git a/contracts/carrot-app/src/yield_sources.rs b/contracts/carrot-app/src/yield_sources.rs deleted file mode 100644 index f93edb1e..00000000 --- a/contracts/carrot-app/src/yield_sources.rs +++ /dev/null @@ -1,212 +0,0 @@ -pub mod mars; -pub mod osmosis_cl_pool; -pub mod yield_type; - -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ensure, ensure_eq, Decimal, Deps}; -use cw_asset::AssetInfo; - -use crate::{ - contract::{App, AppResult}, - error::AppError, - helpers::close_to, - yield_sources::yield_type::YieldTypeBase, -}; -use abstract_app::traits::AbstractNameService; - -/// A yield sources has the following elements -/// A vector of tokens that NEED to be deposited inside the yield source with a repartition of tokens -/// A type that allows routing to the right smart-contract integration internally -#[cw_serde] -pub struct YieldSourceBase { - pub asset_distribution: Vec, - pub ty: YieldTypeBase, -} - -pub type YieldSourceUnchecked = YieldSourceBase; -pub type YieldSource = YieldSourceBase; - -impl From for YieldSourceUnchecked { - fn from(value: YieldSource) -> Self { - Self { - asset_distribution: value.asset_distribution, - ty: value.ty.into(), - } - } -} - -impl Checkable for YieldSourceUnchecked { - type CheckOutput = YieldSource; - fn check(self, deps: Deps, app: &App) -> AppResult { - // First we check the share sums the 100 - let share_sum: Decimal = self.asset_distribution.iter().map(|e| e.share).sum(); - ensure!( - close_to(Decimal::one(), share_sum), - AppError::InvalidStrategySum { share_sum } - ); - // We make sure that assets are associated with this strategy - ensure!( - !self.asset_distribution.is_empty(), - AppError::InvalidEmptyStrategy {} - ); - // We ensure all deposited tokens exist in ANS - let all_denoms = self.all_denoms(); - let ans = app.name_service(deps); - ans.host() - .query_assets_reverse( - &deps.querier, - &all_denoms - .iter() - .map(|denom| AssetInfo::native(denom.clone())) - .collect::>(), - ) - .map_err(|_| AppError::AssetsNotRegistered(all_denoms))?; - - let ty = match self.ty { - YieldTypeBase::ConcentratedLiquidityPool(params) => { - // A valid CL pool strategy is for 2 assets - ensure_eq!( - self.asset_distribution.len(), - 2, - AppError::InvalidStrategy {} - ); - YieldTypeBase::ConcentratedLiquidityPool(params.check(deps, app)?) - } - YieldTypeBase::Mars(params) => { - // We verify there is only one element in the shares vector - ensure_eq!( - self.asset_distribution.len(), - 1, - AppError::InvalidStrategy {} - ); - // We verify the first element correspond to the mars deposit denom - ensure_eq!( - self.asset_distribution[0].denom, - params.denom, - AppError::InvalidStrategy {} - ); - YieldTypeBase::Mars(params.check(deps, app)?) - } - }; - - Ok(YieldSource { - asset_distribution: self.asset_distribution, - ty, - }) - } -} - -impl YieldSourceBase { - pub fn all_denoms(&self) -> Vec { - self.asset_distribution - .iter() - .map(|e| e.denom.clone()) - .collect() - } -} - -/// This is used to express a share of tokens inside a strategy -#[cw_serde] -pub struct AssetShare { - pub denom: String, - pub share: Decimal, -} - -#[cw_serde] -pub enum ShareType { - /// This allows using the current distribution of tokens inside the position to compute the distribution on deposit - Dynamic, - /// This forces the position to use the target distribution of tokens when depositing - Fixed, -} - -#[cw_serde] -pub struct Checked; -#[cw_serde] -pub struct Unchecked; - -pub trait Checkable { - type CheckOutput; - fn check(self, deps: Deps, app: &App) -> AppResult; -} - -// This represents a balance strategy -// This object is used for storing the current strategy, retrieving the actual strategy status or expressing a target strategy when depositing -#[cw_serde] -pub struct BalanceStrategyBase(pub Vec>); - -pub type BalanceStrategyUnchecked = BalanceStrategyBase; -pub type BalanceStrategy = BalanceStrategyBase; - -impl From for BalanceStrategyUnchecked { - fn from(value: BalanceStrategy) -> Self { - Self(value.0.into_iter().map(Into::into).collect()) - } -} - -impl Checkable for BalanceStrategyUnchecked { - type CheckOutput = BalanceStrategy; - fn check(self, deps: Deps, app: &App) -> AppResult { - // First we check the share sums the 100 - let share_sum: Decimal = self.0.iter().map(|e| e.share).sum(); - ensure!( - close_to(Decimal::one(), share_sum), - AppError::InvalidStrategySum { share_sum } - ); - ensure!(!self.0.is_empty(), AppError::InvalidEmptyStrategy {}); - - // Then we check every yield strategy underneath - - let checked = self - .0 - .into_iter() - .map(|yield_source| yield_source.check(deps, app)) - .collect::, _>>()?; - - Ok(checked.into()) - } -} - -impl BalanceStrategy { - pub fn all_denoms(&self) -> Vec { - self.0 - .clone() - .iter() - .flat_map(|s| s.yield_source.all_denoms()) - .collect() - } -} - -impl From>> for BalanceStrategyBase { - fn from(value: Vec>) -> Self { - Self(value) - } -} - -#[cw_serde] -pub struct BalanceStrategyElementBase { - pub yield_source: YieldSourceBase, - pub share: Decimal, -} - -pub type BalanceStrategyElementUnchecked = BalanceStrategyElementBase; -pub type BalanceStrategyElement = BalanceStrategyElementBase; - -impl From for BalanceStrategyElementUnchecked { - fn from(value: BalanceStrategyElement) -> Self { - Self { - yield_source: value.yield_source.into(), - share: value.share, - } - } -} -impl Checkable for BalanceStrategyElementUnchecked { - type CheckOutput = BalanceStrategyElement; - fn check(self, deps: Deps, app: &App) -> AppResult { - let yield_source = self.yield_source.check(deps, app)?; - Ok(BalanceStrategyElement { - yield_source, - share: self.share, - }) - } -} diff --git a/contracts/carrot-app/src/yield_sources/mars.rs b/contracts/carrot-app/src/yield_sources/mars.rs index f3d0654b..ca8a8d26 100644 --- a/contracts/carrot-app/src/yield_sources/mars.rs +++ b/contracts/carrot-app/src/yield_sources/mars.rs @@ -10,7 +10,7 @@ use cw_asset::AssetInfo; use abstract_money_market_standard::query::MoneyMarketAnsQuery; use super::yield_type::YieldTypeImplementation; -use super::{Checkable, ShareType}; +use super::ShareType; pub const MARS_MONEY_MARKET: &str = "mars"; @@ -19,14 +19,6 @@ pub struct MarsDepositParams { pub denom: String, } -impl Checkable for MarsDepositParams { - type CheckOutput = MarsDepositParams; - - fn check(self, _deps: Deps, _app: &App) -> AppResult { - Ok(self) - } -} - impl YieldTypeImplementation for MarsDepositParams { fn deposit(self, deps: Deps, funds: Vec, app: &App) -> AppResult> { let ans = app.name_service(deps); diff --git a/contracts/carrot-app/src/yield_sources/mod.rs b/contracts/carrot-app/src/yield_sources/mod.rs new file mode 100644 index 00000000..99c25f03 --- /dev/null +++ b/contracts/carrot-app/src/yield_sources/mod.rs @@ -0,0 +1,79 @@ +pub mod mars; +pub mod osmosis_cl_pool; +pub mod yield_type; + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Decimal; + +use crate::{ + check::{Checked, Unchecked}, + yield_sources::yield_type::YieldTypeBase, +}; + +/// A yield sources has the following elements +/// A vector of tokens that NEED to be deposited inside the yield source with a repartition of tokens +/// A type that allows routing to the right smart-contract integration internally +#[cw_serde] +pub struct YieldSourceBase { + pub asset_distribution: Vec, + pub ty: YieldTypeBase, +} + +pub type YieldSourceUnchecked = YieldSourceBase; +pub type YieldSource = YieldSourceBase; + +impl YieldSourceBase { + pub fn all_denoms(&self) -> Vec { + self.asset_distribution + .iter() + .map(|e| e.denom.clone()) + .collect() + } +} + +/// This is used to express a share of tokens inside a strategy +#[cw_serde] +pub struct AssetShare { + pub denom: String, + pub share: Decimal, +} + +#[cw_serde] +pub enum ShareType { + /// This allows using the current distribution of tokens inside the position to compute the distribution on deposit + Dynamic, + /// This forces the position to use the target distribution of tokens when depositing + Fixed, +} + +// This represents a balance strategy +// This object is used for storing the current strategy, retrieving the actual strategy status or expressing a target strategy when depositing +#[cw_serde] +pub struct BalanceStrategyBase(pub Vec>); + +pub type BalanceStrategyUnchecked = BalanceStrategyBase; +pub type BalanceStrategy = BalanceStrategyBase; + +impl BalanceStrategy { + pub fn all_denoms(&self) -> Vec { + self.0 + .clone() + .iter() + .flat_map(|s| s.yield_source.all_denoms()) + .collect() + } +} + +#[cw_serde] +pub struct BalanceStrategyElementBase { + pub yield_source: YieldSourceBase, + pub share: Decimal, +} +impl From>> for BalanceStrategyBase { + fn from(value: Vec>) -> Self { + Self(value) + } +} + +pub type BalanceStrategyElementUnchecked = BalanceStrategyElementBase; +pub type BalanceStrategyElement = BalanceStrategyElementBase; diff --git a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs index 7d2f0e2e..713bd81c 100644 --- a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs +++ b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs @@ -1,6 +1,7 @@ use std::{marker::PhantomData, str::FromStr}; use crate::{ + check::{Checked, Unchecked}, contract::{App, AppResult}, error::AppError, handlers::swap_helpers::DEFAULT_SLIPPAGE, @@ -14,17 +15,13 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{ensure, Coin, Coins, CosmosMsg, Decimal, Deps, ReplyOn, SubMsg, Uint128}; use osmosis_std::{ cosmwasm_to_proto_coins, try_proto_to_cosmwasm_coins, - types::osmosis::{ - concentratedliquidity::v1beta1::{ - ConcentratedliquidityQuerier, FullPositionBreakdown, MsgAddToPosition, - MsgCollectIncentives, MsgCollectSpreadRewards, MsgCreatePosition, MsgWithdrawPosition, - Pool, - }, - poolmanager::v1beta1::PoolmanagerQuerier, + types::osmosis::concentratedliquidity::v1beta1::{ + ConcentratedliquidityQuerier, FullPositionBreakdown, MsgAddToPosition, + MsgCollectIncentives, MsgCollectSpreadRewards, MsgCreatePosition, MsgWithdrawPosition, }, }; -use super::{yield_type::YieldTypeImplementation, Checkable, Checked, ShareType, Unchecked}; +use super::{yield_type::YieldTypeImplementation, ShareType}; #[cw_serde] pub struct ConcentratedPoolParamsBase { @@ -44,25 +41,6 @@ pub struct ConcentratedPoolParamsBase { pub type ConcentratedPoolParamsUnchecked = ConcentratedPoolParamsBase; pub type ConcentratedPoolParams = ConcentratedPoolParamsBase; -impl Checkable for ConcentratedPoolParamsUnchecked { - type CheckOutput = ConcentratedPoolParams; - fn check(self, deps: Deps, _app: &App) -> AppResult { - let _pool: Pool = PoolmanagerQuerier::new(&deps.querier) - .pool(self.pool_id) - .map_err(|_| AppError::PoolNotFound {})? - .pool - .ok_or(AppError::PoolNotFound {})? - .try_into()?; - Ok(ConcentratedPoolParams { - pool_id: self.pool_id, - lower_tick: self.lower_tick, - upper_tick: self.upper_tick, - position_id: self.position_id, - _phantom: PhantomData, - }) - } -} - impl YieldTypeImplementation for ConcentratedPoolParams { fn deposit(self, deps: Deps, funds: Vec, app: &App) -> AppResult> { // We verify there is a position stored diff --git a/contracts/carrot-app/src/yield_sources/yield_type.rs b/contracts/carrot-app/src/yield_sources/yield_type.rs index 82b7b5cf..ae83af91 100644 --- a/contracts/carrot-app/src/yield_sources/yield_type.rs +++ b/contracts/carrot-app/src/yield_sources/yield_type.rs @@ -1,13 +1,13 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Coin, CosmosMsg, Deps, SubMsg, Uint128}; -use crate::contract::{App, AppResult}; - -use super::{ - mars::MarsDepositParams, osmosis_cl_pool::ConcentratedPoolParamsBase, Checked, ShareType, - Unchecked, +use crate::{ + check::{Checked, Unchecked}, + contract::{App, AppResult}, }; +use super::{mars::MarsDepositParams, osmosis_cl_pool::ConcentratedPoolParamsBase, ShareType}; + // This however is not checkable by itself, because the check also depends on the asset share distribution #[cw_serde] pub enum YieldTypeBase { @@ -20,23 +20,6 @@ pub enum YieldTypeBase { pub type YieldTypeUnchecked = YieldTypeBase; pub type YieldType = YieldTypeBase; -impl From for YieldTypeUnchecked { - fn from(value: YieldType) -> Self { - match value { - YieldTypeBase::ConcentratedLiquidityPool(params) => { - YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { - pool_id: params.pool_id, - lower_tick: params.lower_tick, - upper_tick: params.upper_tick, - position_id: params.position_id, - _phantom: std::marker::PhantomData, - }) - } - YieldTypeBase::Mars(params) => YieldTypeBase::Mars(params), - } - } -} - impl YieldType { pub fn deposit(self, deps: Deps, funds: Vec, app: &App) -> AppResult> { if funds.is_empty() { From 656ad28d02e99a0349c59456ca18328fbc6a4826 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Fri, 5 Apr 2024 15:10:19 +0000 Subject: [PATCH 24/42] Added deposit preview --- contracts/carrot-app/src/contract.rs | 2 +- .../carrot-app/src/distribution/deposit.rs | 149 ++++++++++++------ contracts/carrot-app/src/handlers/execute.rs | 136 +++++++--------- contracts/carrot-app/src/handlers/mod.rs | 7 +- contracts/carrot-app/src/handlers/preview.rs | 42 +++++ contracts/carrot-app/src/handlers/query.rs | 13 +- contracts/carrot-app/src/msg.rs | 53 +++++-- contracts/carrot-app/tests/config.rs | 11 +- 8 files changed, 268 insertions(+), 145 deletions(-) create mode 100644 contracts/carrot-app/src/handlers/preview.rs diff --git a/contracts/carrot-app/src/contract.rs b/contracts/carrot-app/src/contract.rs index e2f010a2..dae1f9c1 100644 --- a/contracts/carrot-app/src/contract.rs +++ b/contracts/carrot-app/src/contract.rs @@ -34,7 +34,7 @@ const APP: App = App::new(APP_ID, APP_VERSION, None) .with_instantiate(handlers::instantiate_handler) .with_execute(handlers::execute_handler) .with_query(handlers::query_handler) - // .with_migrate(handlers::migrate_handler) + .with_migrate(handlers::migrate_handler) .with_replies(&[ (OSMOSIS_CREATE_POSITION_REPLY_ID, create_position_reply), (OSMOSIS_ADD_TO_POSITION_REPLY_ID, add_to_position_reply), diff --git a/contracts/carrot-app/src/distribution/deposit.rs b/contracts/carrot-app/src/distribution/deposit.rs index f822946b..6451b466 100644 --- a/contracts/carrot-app/src/distribution/deposit.rs +++ b/contracts/carrot-app/src/distribution/deposit.rs @@ -6,52 +6,85 @@ use crate::{ contract::{App, AppResult}, exchange_rate::query_all_exchange_rates, helpers::{compute_total_value, compute_value}, + state::CONFIG, yield_sources::{yield_type::YieldType, AssetShare, BalanceStrategy, BalanceStrategyElement}, }; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{wasm_execute, CosmosMsg, Env}; -use crate::{ - error::AppError, - msg::{AppExecuteMsg, ExecuteMsg, InternalExecuteMsg}, -}; +use crate::{error::AppError, msg::InternalExecuteMsg}; + +pub fn generate_deposit_strategy( + deps: Deps, + funds: Vec, + yield_source_params: Option>>>, + app: &App, +) -> AppResult<( + Vec<(BalanceStrategyElement, Decimal)>, + Vec, +)> { + // This is the storage strategy for all assets + let target_strategy = CONFIG.load(deps.storage)?.balance_strategy; + + // This is the current distribution of funds inside the strategies + let current_strategy_status = target_strategy.query_current_status(deps, app)?; + + let mut usable_funds: Coins = funds.try_into()?; + let (withdraw_strategy, mut this_deposit_strategy) = target_strategy.current_deposit_strategy( + deps, + &mut usable_funds, + current_strategy_status, + app, + )?; + + // We query the yield source shares + this_deposit_strategy.apply_current_strategy_shares(deps, app)?; + + // We correct it if the user asked to correct the share parameters of each strategy + this_deposit_strategy.correct_with(yield_source_params); + + // We fill the strategies with the current deposited funds and get messages to execute those deposits + let deposit_msgs = + this_deposit_strategy.fill_all_and_get_messages(deps, usable_funds.into(), app)?; + + Ok((withdraw_strategy, deposit_msgs)) +} impl BalanceStrategy { // We determine the best balance strategy depending on the current deposits and the target strategy. // This method needs to be called on the stored strategy + // We error if deposit value is non-zero here pub fn current_deposit_strategy( &self, deps: Deps, funds: &mut Coins, current_strategy_status: Self, app: &App, - ) -> AppResult<(Vec, Option)> { + ) -> AppResult<(Vec<(BalanceStrategyElement, Decimal)>, Self)> { let total_value = self.current_balance(deps, app)?.total_value; let deposit_value = compute_value(deps, &funds.to_vec(), app)?; - if deposit_value.is_zero() { - // We are trying to deposit no value, so we just don't do anything - return Ok((vec![], None)); + if (total_value + deposit_value).is_zero() { + return Err(AppError::NoDeposit {}); } // We create the strategy so that he final distribution is as close to the target strategy as possible // 1. For all strategies, we withdraw some if its value is too high above target_strategy let mut withdraw_value = Uint128::zero(); - let mut withdraw_msgs = vec![]; // All strategies have to be reviewed // EITHER of those are true : // - The yield source has too much funds deposited and some should be withdrawn // OR // - Some funds need to be deposited into the strategy - let this_deposit_strategy: BalanceStrategy = current_strategy_status + + // First we generate the messages for withdrawing strategies that have too much funds + let withdraw_strategy: Vec<(BalanceStrategyElement, Decimal)> = current_strategy_status .0 - .into_iter() + .iter() .zip(self.0.clone()) .map(|(target, current)| { // We need to take into account the total value added by the current shares - let value_now = current.share * total_value; let target_value = target.share * (total_value + deposit_value); @@ -59,39 +92,66 @@ impl BalanceStrategy { if target_value < value_now { let this_withdraw_value = value_now - target_value; // In the following line, total_value can't be zero, otherwise the if condition wouldn't be met - let this_withdraw_share = Decimal::from_ratio(withdraw_value, total_value); + let this_withdraw_share = Decimal::from_ratio(this_withdraw_value, total_value); let this_withdraw_funds = current.withdraw_preview(deps, Some(this_withdraw_share), app)?; withdraw_value += this_withdraw_value; for fund in this_withdraw_funds { funds.add(fund)?; } - withdraw_msgs.push( - current - .withdraw(deps, Some(this_withdraw_share), app)? - .into(), - ); // In case there is a withdraw from the strategy, we don't need to deposit into this strategy after ! - Ok::<_, AppError>(None) + Ok::<_, AppError>(Some((current, this_withdraw_share))) + } else { + Ok(None) + } + }) + .collect::, _>>()? + .into_iter() + .flatten() + .collect::>(); + + let available_value = withdraw_value + deposit_value; + + let this_deposit_strategy: BalanceStrategy = current_strategy_status + .0 + .into_iter() + .zip(self.0.clone()) + .flat_map(|(target, current)| { + // We need to take into account the total value added by the current shares + let value_now = current.share * total_value; + let target_value = target.share * (total_value + deposit_value); + + // If value now is smaller than the target value, we need to deposit some funds into the protocol + if target_value < value_now { + None } else { // In case we don't withdraw anything, it means we might deposit. - // Total should sum to one ! - let share = Decimal::from_ratio(target_value - value_now, deposit_value); + let share = if available_value.is_zero() { + Decimal::zero() + } else { + Decimal::from_ratio(target_value - value_now, available_value) + }; - Ok(Some(BalanceStrategyElement { + Some(BalanceStrategyElement { yield_source: target.yield_source.clone(), share, - })) + }) } }) - .collect::, _>>()? - .into_iter() - .flatten() .collect::>() .into(); - Ok((withdraw_msgs, Some(this_deposit_strategy))) + // // Then we create the deposit elements to generate the deposits + // + // }) + // .collect::, _>>()? + // .into_iter() + // .flatten() + // .collect::>() + // .into(); + + Ok((withdraw_strategy, this_deposit_strategy)) } // We dispatch the available funds directly into the Strategies @@ -186,17 +246,16 @@ impl BalanceStrategy { pub fn fill_all_and_get_messages( &self, deps: Deps, - env: &Env, funds: Vec, app: &App, - ) -> AppResult> { + ) -> AppResult> { let deposit_strategies = self.fill_all(deps, funds, app)?; - deposit_strategies + Ok(deposit_strategies .iter() .zip(self.0.iter().map(|s| s.yield_source.ty.clone())) .enumerate() - .map(|(index, (strategy, yield_type))| strategy.deposit_msgs(env, index, yield_type)) - .collect() + .map(|(index, (strategy, yield_type))| strategy.deposit_msgs(index, yield_type)) + .collect()) } /// Corrects the current strategy with some parameters passed by the user @@ -309,24 +368,12 @@ impl From>> for OneDepositStrategy { } impl OneDepositStrategy { - pub fn deposit_msgs( - &self, - env: &Env, - yield_index: usize, - yield_type: YieldType, - ) -> AppResult { + pub fn deposit_msgs(&self, yield_index: usize, yield_type: YieldType) -> InternalExecuteMsg { // For each strategy, we send a message on the contract to execute it - Ok(wasm_execute( - env.contract.address.clone(), - &ExecuteMsg::Module(AppExecuteMsg::Internal( - InternalExecuteMsg::DepositOneStrategy { - swap_strategy: self.clone(), - yield_type, - yield_index, - }, - )), - vec![], - )? - .into()) + InternalExecuteMsg::DepositOneStrategy { + swap_strategy: self.clone(), + yield_type, + yield_index, + } } } diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index 35887219..2711a9f3 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -1,6 +1,7 @@ use crate::{ check::Checkable, contract::{App, AppResult}, + distribution::deposit::generate_deposit_strategy, error::AppError, handlers::query::query_balance, helpers::assert_contract, @@ -73,10 +74,7 @@ fn deposit( .assert_admin(deps.as_ref(), &info.sender) .or(assert_contract(&info, &env))?; - // let deposit_msgs = _inner_deposit(deps.as_ref(), &env, funds, yield_source_params, &app)?; - let deposit_msgs = - _inner_advanced_deposit(deps.as_ref(), &env, funds, yield_source_params, &app)?; - + let deposit_msgs = _inner_deposit(deps.as_ref(), &env, funds, yield_source_params, &app)?; Ok(app.response("deposit").add_messages(deposit_msgs)) } @@ -206,88 +204,70 @@ fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResu Ok(response.add_messages(executor_reward_messages)) } -/// The deposit process goes through the following steps -/// 1. We query the target strategy in storage -/// 2. We correct the expected token shares of each strategy, in case there are corrections passed to the function -/// 3. We deposit funds according to that strategy -/// -/// This approach is not perfect. TO show the flaws, take an example where you allocate 50% into mars, 50% into osmosis and both give similar rewards. -/// Assume we deposited 2x inside the app. -/// When an auto-compounding happens, they both get y as rewards, mars is already auto-compounding and osmosis' rewards are redeposited inside the pool -/// Step | Mars | Osmosis | Rewards| -/// Deposit | x | x | 0 | -/// Withdraw Rewards | x + y | x| y | -/// Re-deposit | x + y + y/2 | x + y/2 | 0 | -/// The final ratio is not the 50/50 ratio we target -/// -/// PROPOSITION : We could also have this kind of deposit flow -/// 1a. We query the target strategy in storage (target strategy) -/// 1b. We query the current status of the strategy (current strategy) -/// 1c. We create a temporary strategy object to allocate the funds from this deposit into the various strategies -/// --> the goal of those 3 steps is to correct the funds allocation faster towards the target strategy -/// 2. We correct the expected token shares of each strategy, in case there are corrections passed to the function -/// 3. We deposit funds according to that strategy -/// This time : -/// Step | Mars | Osmosis | Rewards| -/// Deposit | x | x | 0 | -/// Withdraw Rewards | x + y | x| y | -/// Re-deposit | x + y | x + y | 0 | -pub fn _inner_deposit( - deps: Deps, - env: &Env, - funds: Vec, - yield_source_params: Option>>>, - app: &App, -) -> AppResult> { - // We query the target strategy depending on the existing deposits - let mut current_strategy_status = CONFIG.load(deps.storage)?.balance_strategy; - current_strategy_status.apply_current_strategy_shares(deps, app)?; - - // We correct it if the user asked to correct the share parameters of each strategy - current_strategy_status.correct_with(yield_source_params); - - // We fill the strategies with the current deposited funds and get messages to execute those deposits - current_strategy_status.fill_all_and_get_messages(deps, env, funds, app) -} +// /// UNUSED FOR NOW, replaces by _inner_advanced_deposit +// /// The deposit process goes through the following steps +// /// 1. We query the target strategy in storage +// /// 2. We correct the expected token shares of each strategy, in case there are corrections passed to the function +// /// 3. We deposit funds according to that strategy +// /// +// /// This approach is not perfect. TO show the flaws, take an example where you allocate 50% into mars, 50% into osmosis and both give similar rewards. +// /// Assume we deposited 2x inside the app. +// /// When an auto-compounding happens, they both get y as rewards, mars is already auto-compounding and osmosis' rewards are redeposited inside the pool +// /// Step | Mars | Osmosis | Rewards| +// /// Deposit | x | x | 0 | +// /// Withdraw Rewards | x + y | x| y | +// /// Re-deposit | x + y + y/2 | x + y/2 | 0 | +// /// The final ratio is not the 50/50 ratio we target +// /// +// /// PROPOSITION : We could also have this kind of deposit flow +// /// 1a. We query the target strategy in storage (target strategy) +// /// 1b. We query the current status of the strategy (current strategy) +// /// 1c. We create a temporary strategy object to allocate the funds from this deposit into the various strategies +// /// --> the goal of those 3 steps is to correct the funds allocation faster towards the target strategy +// /// 2. We correct the expected token shares of each strategy, in case there are corrections passed to the function +// /// 3. We deposit funds according to that strategy +// /// This time : +// /// Step | Mars | Osmosis | Rewards| +// /// Deposit | x | x | 0 | +// /// Withdraw Rewards | x + y | x| y | +// /// Re-deposit | x + y | x + y | 0 | +// pub fn _inner_deposit( +// deps: Deps, +// env: &Env, +// funds: Vec, +// yield_source_params: Option>>>, +// app: &App, +// ) -> AppResult> { +// // We query the target strategy depending on the existing deposits +// let mut current_strategy_status = CONFIG.load(deps.storage)?.balance_strategy; +// current_strategy_status.apply_current_strategy_shares(deps, app)?; + +// // We correct it if the user asked to correct the share parameters of each strategy +// current_strategy_status.correct_with(yield_source_params); + +// // We fill the strategies with the current deposited funds and get messages to execute those deposits +// current_strategy_status.fill_all_and_get_messages(deps, env, funds, app) +// } -pub fn _inner_advanced_deposit( +pub fn _inner_deposit( deps: Deps, env: &Env, funds: Vec, yield_source_params: Option>>>, app: &App, ) -> AppResult> { - // This is the storage strategy for all assets - let target_strategy = CONFIG.load(deps.storage)?.balance_strategy; - - // This is the current distribution of funds inside the strategies - let current_strategy_status = target_strategy.query_current_status(deps, app)?; - - let mut usable_funds: Coins = funds.try_into()?; - let (withdraw_msgs, this_deposit_strategy) = target_strategy.current_deposit_strategy( - deps, - &mut usable_funds, - current_strategy_status, - app, - )?; - - let mut this_deposit_strategy = if let Some(this_deposit_strategy) = this_deposit_strategy { - this_deposit_strategy - } else { - return Ok(withdraw_msgs); - }; - - // We query the yield source shares - this_deposit_strategy.apply_current_strategy_shares(deps, app)?; - - // We correct it if the user asked to correct the share parameters of each strategy - this_deposit_strategy.correct_with(yield_source_params); - - // We fill the strategies with the current deposited funds and get messages to execute those deposits - let deposit_msgs = - this_deposit_strategy.fill_all_and_get_messages(deps, env, usable_funds.into(), app)?; + let (withdraw_strategy, deposit_msgs) = + generate_deposit_strategy(deps, funds, yield_source_params, app)?; + let deposit_withdraw_msgs = withdraw_strategy + .into_iter() + .map(|(el, share)| el.withdraw(deps, Some(share), app).map(Into::into)) + .collect::, _>>()?; + let deposit_msgs = deposit_msgs + .into_iter() + .map(|msg| msg.to_cosmos_msg(env)) + .collect::, _>>()?; - Ok([withdraw_msgs, deposit_msgs].concat()) + Ok([deposit_withdraw_msgs, deposit_msgs].concat()) } fn _inner_withdraw( diff --git a/contracts/carrot-app/src/handlers/mod.rs b/contracts/carrot-app/src/handlers/mod.rs index 5d877c7b..6ba1876a 100644 --- a/contracts/carrot-app/src/handlers/mod.rs +++ b/contracts/carrot-app/src/handlers/mod.rs @@ -1,11 +1,12 @@ pub mod execute; pub mod instantiate; pub mod internal; -// pub mod migrate; +pub mod migrate; +/// Allows to preview the usual operations before executing them +pub mod preview; pub mod query; pub mod swap_helpers; pub use crate::handlers::{ - execute::execute_handler, - instantiate::instantiate_handler, // migrate::migrate_handler, + execute::execute_handler, instantiate::instantiate_handler, migrate::migrate_handler, query::query_handler, }; diff --git a/contracts/carrot-app/src/handlers/preview.rs b/contracts/carrot-app/src/handlers/preview.rs new file mode 100644 index 00000000..057bee1d --- /dev/null +++ b/contracts/carrot-app/src/handlers/preview.rs @@ -0,0 +1,42 @@ +use cosmwasm_std::{Coin, Deps, Uint128}; + +use crate::{ + contract::{App, AppResult}, + distribution::deposit::generate_deposit_strategy, + msg::{DepositPreviewResponse, UpdateStrategyPreviewResponse, WithdrawPreviewResponse}, + yield_sources::{AssetShare, BalanceStrategyUnchecked}, +}; + +pub fn deposit_preview( + deps: Deps, + funds: Vec, + yield_source_params: Option>>>, + app: &App, +) -> AppResult { + let (withdraw_strategy, deposit_strategy) = + generate_deposit_strategy(deps, funds, yield_source_params, app)?; + + Ok(DepositPreviewResponse { + withdraw: withdraw_strategy + .into_iter() + .map(|(el, share)| (el.into(), share)) + .collect(), + deposit: deposit_strategy, + }) +} + +pub fn withdraw_preview( + deps: Deps, + amount: Option, + app: &App, +) -> AppResult { + Ok(WithdrawPreviewResponse {}) +} +pub fn update_strategy_preview( + deps: Deps, + funds: Vec, + strategy: BalanceStrategyUnchecked, + app: &App, +) -> AppResult { + Ok(UpdateStrategyPreviewResponse {}) +} diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index 2f4fae66..1f674f78 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -21,6 +21,8 @@ use crate::{ state::{Config, CONFIG}, }; +use super::preview::{deposit_preview, update_strategy_preview, withdraw_preview}; + pub fn query_handler(deps: Deps, env: Env, app: &App, msg: AppQueryMsg) -> AppResult { match msg { AppQueryMsg::Balance {} => to_json_binary(&query_balance(deps, app)?), @@ -28,9 +30,18 @@ pub fn query_handler(deps: Deps, env: Env, app: &App, msg: AppQueryMsg) -> AppRe AppQueryMsg::Config {} => to_json_binary(&query_config(deps)?), AppQueryMsg::Strategy {} => to_json_binary(&query_strategy(deps)?), AppQueryMsg::CompoundStatus {} => to_json_binary(&query_compound_status(deps, env, app)?), - AppQueryMsg::RebalancePreview {} => todo!(), AppQueryMsg::StrategyStatus {} => to_json_binary(&query_strategy_status(deps, app)?), AppQueryMsg::Positions {} => to_json_binary(&query_positions(deps, app)?), + AppQueryMsg::DepositPreview { + funds, + yield_sources_params, + } => to_json_binary(&deposit_preview(deps, funds, yield_sources_params, app)?), + AppQueryMsg::WithdrawPreview { amount } => { + to_json_binary(&withdraw_preview(deps, amount, app)?) + } + AppQueryMsg::UpdateStrategyPreview { strategy, funds } => { + to_json_binary(&update_strategy_preview(deps, funds, strategy, app)?) + } } .map_err(Into::into) } diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index 4949dd0d..1a2568eb 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -1,14 +1,14 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Coin, Uint128, Uint64}; +use cosmwasm_std::{wasm_execute, Coin, CosmosMsg, Decimal, Env, Uint128, Uint64}; use cw_asset::AssetBase; use crate::{ - contract::App, + contract::{App, AppResult}, distribution::deposit::OneDepositStrategy, state::ConfigUnchecked, yield_sources::{ yield_type::{YieldType, YieldTypeUnchecked}, - AssetShare, BalanceStrategyUnchecked, + AssetShare, BalanceStrategyElementUnchecked, BalanceStrategyUnchecked, }, }; @@ -50,8 +50,8 @@ pub enum AppExecuteMsg { Autocompound {}, /// Rebalances all investments according to a new balance strategy UpdateStrategy { - strategy: BalanceStrategyUnchecked, funds: Vec, + strategy: BalanceStrategyUnchecked, }, /// Only called by the contract internally @@ -64,8 +64,8 @@ pub enum AppExecuteMsg { pub enum InternalExecuteMsg { DepositOneStrategy { swap_strategy: OneDepositStrategy, - yield_type: YieldType, yield_index: usize, + yield_type: YieldType, }, /// Execute one Deposit Swap Step ExecuteOneDepositSwapStep { @@ -75,8 +75,8 @@ pub enum InternalExecuteMsg { }, /// Finalize the deposit after all swaps are executed FinalizeDeposit { - yield_type: YieldType, yield_index: usize, + yield_type: YieldType, }, } impl From @@ -90,6 +90,17 @@ impl From } } +impl InternalExecuteMsg { + pub fn to_cosmos_msg(&self, env: &Env) -> AppResult { + Ok(wasm_execute( + env.contract.address.clone(), + &ExecuteMsg::Module(AppExecuteMsg::Internal(self.clone())), + vec![], + )? + .into()) + } +} + /// App query messages #[cosmwasm_schema::cw_serde] #[cfg_attr(feature = "interface", derive(cw_orch::QueryFns))] @@ -118,10 +129,25 @@ pub enum AppQueryMsg { /// Returns [`StrategyResponse`] #[returns(StrategyResponse)] StrategyStatus {}, + + // **** Simulation Endpoints *****/ + // **** These allow to preview what will happen under the hood for each operation inside the Carrot App *****/ + // Their arguments match the arguments of the corresponding Execute Endpoint + #[returns(DepositPreviewResponse)] + DepositPreview { + funds: Vec, + yield_sources_params: Option>>>, + }, + #[returns(WithdrawPreviewResponse)] + WithdrawPreview { amount: Option }, + /// Returns a preview of the rebalance distribution /// Returns [`RebalancePreviewResponse`] - #[returns(RebalancePreviewResponse)] - RebalancePreview {}, + #[returns(UpdateStrategyPreviewResponse)] + UpdateStrategyPreview { + funds: Vec, + strategy: BalanceStrategyUnchecked, + }, } #[cosmwasm_schema::cw_serde] @@ -185,4 +211,13 @@ impl CompoundStatus { } #[cw_serde] -pub struct RebalancePreviewResponse {} +pub struct DepositPreviewResponse { + pub withdraw: Vec<(BalanceStrategyElementUnchecked, Decimal)>, + pub deposit: Vec, +} + +#[cw_serde] +pub struct WithdrawPreviewResponse {} + +#[cw_serde] +pub struct UpdateStrategyPreviewResponse {} diff --git a/contracts/carrot-app/tests/config.rs b/contracts/carrot-app/tests/config.rs index 6d7bf6a7..3cc1ff83 100644 --- a/contracts/carrot-app/tests/config.rs +++ b/contracts/carrot-app/tests/config.rs @@ -78,6 +78,7 @@ fn rebalance_fails() -> anyhow::Result<()> { #[test] fn rebalance_success() -> anyhow::Result<()> { let (pool_id, carrot_app) = setup_test_tube(false)?; + let mut chain = carrot_app.get_chain().clone(); let new_strat = BalanceStrategyBase(vec![ BalanceStrategyElementBase { @@ -127,11 +128,17 @@ fn rebalance_success() -> anyhow::Result<()> { ]); let strategy = carrot_app.strategy()?; assert_ne!(strategy.strategy, new_strat); - carrot_app.update_strategy(vec![], new_strat.clone())?; + let deposit_coins = coins(10, USDC); + chain.add_balance( + carrot_app.account().proxy()?.to_string(), + deposit_coins.clone(), + )?; + + carrot_app.update_strategy(deposit_coins, new_strat.clone())?; // We query the new strategy let strategy = carrot_app.strategy()?; - assert_eq!(strategy.strategy, new_strat); + assert_eq!(strategy.strategy.0.len(), 2); Ok(()) } From 7fe855f85c1879243c05c745a075ea34542d143a Mon Sep 17 00:00:00 2001 From: Kayanski Date: Mon, 8 Apr 2024 17:03:41 +0000 Subject: [PATCH 25/42] Added release profile --- Cargo.toml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 7e8c3f4c..245db0ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,3 +30,15 @@ abstract-client = { git = "https://github.com/abstractsdk/abstract.git" } abstract-testing = { git = "https://github.com/abstractsdk/abstract.git" } abstract-core = { git = "https://github.com/abstractsdk/abstract.git" } abstract-sdk = { git = "https://github.com/abstractsdk/abstract.git" } + + +[profile.release] +rpath = false +lto = true +overflow-checks = true +opt-level = 3 +debug = false +debug-assertions = false +codegen-units = 1 +panic = 'abort' +incremental = false From 7f1f75b973df210a88026fcf95f635b321d64c25 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Mon, 8 Apr 2024 17:37:36 +0000 Subject: [PATCH 26/42] Nots --- .../examples/install_savings_app.rs | 4 +- contracts/carrot-app/src/check.rs | 30 ++++----- .../carrot-app/src/distribution/deposit.rs | 21 +++--- .../carrot-app/src/distribution/query.rs | 22 +++---- .../carrot-app/src/distribution/rebalance.rs | 4 +- .../carrot-app/src/distribution/rewards.rs | 4 +- .../carrot-app/src/distribution/withdraw.rs | 6 +- contracts/carrot-app/src/handlers/execute.rs | 64 ++++++++----------- .../carrot-app/src/handlers/instantiate.rs | 9 +-- contracts/carrot-app/src/handlers/internal.rs | 50 +++++++++++---- contracts/carrot-app/src/handlers/preview.rs | 4 +- contracts/carrot-app/src/handlers/query.rs | 21 +++--- contracts/carrot-app/src/helpers.rs | 10 ++- contracts/carrot-app/src/msg.rs | 12 ++-- .../src/replies/osmosis/add_to_position.rs | 4 +- .../src/replies/osmosis/create_position.rs | 4 +- contracts/carrot-app/src/state.rs | 4 +- contracts/carrot-app/src/yield_sources/mod.rs | 18 +++--- contracts/carrot-app/tests/common.rs | 48 +++++++------- contracts/carrot-app/tests/config.rs | 34 +++++----- .../carrot-app/tests/deposit_withdraw.rs | 17 +++-- 21 files changed, 192 insertions(+), 198 deletions(-) diff --git a/contracts/carrot-app/examples/install_savings_app.rs b/contracts/carrot-app/examples/install_savings_app.rs index f13c2190..dd3ffd10 100644 --- a/contracts/carrot-app/examples/install_savings_app.rs +++ b/contracts/carrot-app/examples/install_savings_app.rs @@ -18,7 +18,7 @@ use carrot_app::{ contract::OSMOSIS, msg::AppInstantiateMsg, state::ConfigBase, - yield_sources::{BalanceStrategy, BalanceStrategyBase}, + yield_sources::{Strategy, StrategyBase}, }; use osmosis_std::types::cosmos::authz::v1beta1::MsgGrantResponse; @@ -74,9 +74,9 @@ fn main() -> anyhow::Result<()> { _phantom: std::marker::PhantomData, }, }, - balance_strategy: BalanceStrategyBase(vec![]), dex: OSMOSIS.to_string(), }, + strategy: StrategyBase(vec![]), deposit: Some(coins(100, "usdc")), }; let create_sub_account_message = utils::create_account_message(&client, init_msg)?; diff --git a/contracts/carrot-app/src/check.rs b/contracts/carrot-app/src/check.rs index e5123616..3b427117 100644 --- a/contracts/carrot-app/src/check.rs +++ b/contracts/carrot-app/src/check.rs @@ -82,7 +82,6 @@ mod config { impl From for ConfigUnchecked { fn from(value: Config) -> Self { Self { - balance_strategy: value.balance_strategy.into(), autocompound_config: value.autocompound_config.into(), dex: value.dex, } @@ -98,7 +97,6 @@ mod config { app: &crate::contract::App, ) -> crate::contract::AppResult { Ok(Config { - balance_strategy: self.balance_strategy.check(deps, app)?, autocompound_config: AutocompoundConfigBase { cooldown_seconds: self.autocompound_config.cooldown_seconds, rewards: self @@ -130,8 +128,8 @@ mod yield_sources { ConcentratedPoolParams, ConcentratedPoolParamsBase, ConcentratedPoolParamsUnchecked, }, yield_type::{YieldType, YieldTypeBase, YieldTypeUnchecked}, - BalanceStrategy, BalanceStrategyElement, BalanceStrategyElementUnchecked, - BalanceStrategyUnchecked, YieldSource, YieldSourceUnchecked, + Strategy, StrategyElement, StrategyElementUnchecked, StrategyUnchecked, YieldSource, + YieldSourceUnchecked, }, }; @@ -263,37 +261,37 @@ mod yield_sources { } } - mod balance_strategy { + mod strategy { use super::*; - impl From for BalanceStrategyElementUnchecked { - fn from(value: BalanceStrategyElement) -> Self { + impl From for StrategyElementUnchecked { + fn from(value: StrategyElement) -> Self { Self { yield_source: value.yield_source.into(), share: value.share, } } } - impl Checkable for BalanceStrategyElementUnchecked { - type CheckOutput = BalanceStrategyElement; - fn check(self, deps: Deps, app: &App) -> AppResult { + impl Checkable for StrategyElementUnchecked { + type CheckOutput = StrategyElement; + fn check(self, deps: Deps, app: &App) -> AppResult { let yield_source = self.yield_source.check(deps, app)?; - Ok(BalanceStrategyElement { + Ok(StrategyElement { yield_source, share: self.share, }) } } - impl From for BalanceStrategyUnchecked { - fn from(value: BalanceStrategy) -> Self { + impl From for StrategyUnchecked { + fn from(value: Strategy) -> Self { Self(value.0.into_iter().map(Into::into).collect()) } } - impl Checkable for BalanceStrategyUnchecked { - type CheckOutput = BalanceStrategy; - fn check(self, deps: Deps, app: &App) -> AppResult { + impl Checkable for StrategyUnchecked { + type CheckOutput = Strategy; + fn check(self, deps: Deps, app: &App) -> AppResult { // First we check the share sums the 100 let share_sum: Decimal = self.0.iter().map(|e| e.share).sum(); ensure!( diff --git a/contracts/carrot-app/src/distribution/deposit.rs b/contracts/carrot-app/src/distribution/deposit.rs index 6451b466..8aeed063 100644 --- a/contracts/carrot-app/src/distribution/deposit.rs +++ b/contracts/carrot-app/src/distribution/deposit.rs @@ -6,8 +6,8 @@ use crate::{ contract::{App, AppResult}, exchange_rate::query_all_exchange_rates, helpers::{compute_total_value, compute_value}, - state::CONFIG, - yield_sources::{yield_type::YieldType, AssetShare, BalanceStrategy, BalanceStrategyElement}, + state::STRATEGY_CONFIG, + yield_sources::{yield_type::YieldType, AssetShare, Strategy, StrategyElement}, }; use cosmwasm_schema::cw_serde; @@ -19,12 +19,9 @@ pub fn generate_deposit_strategy( funds: Vec, yield_source_params: Option>>>, app: &App, -) -> AppResult<( - Vec<(BalanceStrategyElement, Decimal)>, - Vec, -)> { +) -> AppResult<(Vec<(StrategyElement, Decimal)>, Vec)> { // This is the storage strategy for all assets - let target_strategy = CONFIG.load(deps.storage)?.balance_strategy; + let target_strategy = STRATEGY_CONFIG.load(deps.storage)?; // This is the current distribution of funds inside the strategies let current_strategy_status = target_strategy.query_current_status(deps, app)?; @@ -50,7 +47,7 @@ pub fn generate_deposit_strategy( Ok((withdraw_strategy, deposit_msgs)) } -impl BalanceStrategy { +impl Strategy { // We determine the best balance strategy depending on the current deposits and the target strategy. // This method needs to be called on the stored strategy // We error if deposit value is non-zero here @@ -60,7 +57,7 @@ impl BalanceStrategy { funds: &mut Coins, current_strategy_status: Self, app: &App, - ) -> AppResult<(Vec<(BalanceStrategyElement, Decimal)>, Self)> { + ) -> AppResult<(Vec<(StrategyElement, Decimal)>, Self)> { let total_value = self.current_balance(deps, app)?.total_value; let deposit_value = compute_value(deps, &funds.to_vec(), app)?; @@ -79,7 +76,7 @@ impl BalanceStrategy { // - Some funds need to be deposited into the strategy // First we generate the messages for withdrawing strategies that have too much funds - let withdraw_strategy: Vec<(BalanceStrategyElement, Decimal)> = current_strategy_status + let withdraw_strategy: Vec<(StrategyElement, Decimal)> = current_strategy_status .0 .iter() .zip(self.0.clone()) @@ -113,7 +110,7 @@ impl BalanceStrategy { let available_value = withdraw_value + deposit_value; - let this_deposit_strategy: BalanceStrategy = current_strategy_status + let this_deposit_strategy: Strategy = current_strategy_status .0 .into_iter() .zip(self.0.clone()) @@ -133,7 +130,7 @@ impl BalanceStrategy { Decimal::from_ratio(target_value - value_now, available_value) }; - Some(BalanceStrategyElement { + Some(StrategyElement { yield_source: target.yield_source.clone(), share, }) diff --git a/contracts/carrot-app/src/distribution/query.rs b/contracts/carrot-app/src/distribution/query.rs index 7be2f954..ca80f78f 100644 --- a/contracts/carrot-app/src/distribution/query.rs +++ b/contracts/carrot-app/src/distribution/query.rs @@ -5,10 +5,10 @@ use crate::{ error::AppError, exchange_rate::query_exchange_rate, msg::AssetsBalanceResponse, - yield_sources::{AssetShare, BalanceStrategy, BalanceStrategyElement, YieldSource}, + yield_sources::{AssetShare, Strategy, StrategyElement, YieldSource}, }; -impl BalanceStrategy { +impl Strategy { // Returns the total balance pub fn current_balance(&self, deps: Deps, app: &App) -> AppResult { let mut funds = Coins::default(); @@ -34,7 +34,7 @@ impl BalanceStrategy { } /// Returns the current status of the full strategy. It returns shares reflecting the underlying positions - pub fn query_current_status(&self, deps: Deps, app: &App) -> AppResult { + pub fn query_current_status(&self, deps: Deps, app: &App) -> AppResult { let all_strategy_values = self .0 .iter() @@ -54,15 +54,13 @@ impl BalanceStrategy { .0 .iter() .zip(all_strategy_values) - .map( - |(original_strategy, (value, shares))| BalanceStrategyElement { - yield_source: YieldSource { - asset_distribution: shares, - ty: original_strategy.yield_source.ty.clone(), - }, - share: Decimal::from_ratio(value, all_strategies_value), + .map(|(original_strategy, (value, shares))| StrategyElement { + yield_source: YieldSource { + asset_distribution: shares, + ty: original_strategy.yield_source.ty.clone(), }, - ) + share: Decimal::from_ratio(value, all_strategies_value), + }) .collect::>() .into()) } @@ -84,7 +82,7 @@ impl BalanceStrategy { } } -impl BalanceStrategyElement { +impl StrategyElement { /// Queries the current value distribution of a registered strategy /// If there is no deposit or the query for the user deposit value fails /// the function returns 0 value with the registered asset distribution diff --git a/contracts/carrot-app/src/distribution/rebalance.rs b/contracts/carrot-app/src/distribution/rebalance.rs index 08f0ab9d..a0f5ea6d 100644 --- a/contracts/carrot-app/src/distribution/rebalance.rs +++ b/contracts/carrot-app/src/distribution/rebalance.rs @@ -1,8 +1,8 @@ -use crate::yield_sources::BalanceStrategy; +use crate::yield_sources::Strategy; /// In order to re-balance the strategies, we need in order : /// 1. Withdraw from the strategies that will be deleted /// 2. Compute the total value that should land in each strategy /// 3. Withdraw from strategies that have too much value /// 4. Deposit all the withdrawn funds into the strategies to match the target. -impl BalanceStrategy {} +impl Strategy {} diff --git a/contracts/carrot-app/src/distribution/rewards.rs b/contracts/carrot-app/src/distribution/rewards.rs index 1a450d9d..35fb8236 100644 --- a/contracts/carrot-app/src/distribution/rewards.rs +++ b/contracts/carrot-app/src/distribution/rewards.rs @@ -4,10 +4,10 @@ use cosmwasm_std::{Coin, Deps}; use crate::{ contract::{App, AppResult}, error::AppError, - yield_sources::BalanceStrategy, + yield_sources::Strategy, }; -impl BalanceStrategy { +impl Strategy { pub fn withdraw_rewards( self, deps: Deps, diff --git a/contracts/carrot-app/src/distribution/withdraw.rs b/contracts/carrot-app/src/distribution/withdraw.rs index 021e8309..97959df2 100644 --- a/contracts/carrot-app/src/distribution/withdraw.rs +++ b/contracts/carrot-app/src/distribution/withdraw.rs @@ -4,10 +4,10 @@ use cosmwasm_std::{Coin, Decimal, Deps}; use crate::{ contract::{App, AppResult}, error::AppError, - yield_sources::{BalanceStrategy, BalanceStrategyElement}, + yield_sources::{Strategy, StrategyElement}, }; -impl BalanceStrategy { +impl Strategy { pub fn withdraw( self, deps: Deps, @@ -21,7 +21,7 @@ impl BalanceStrategy { } } -impl BalanceStrategyElement { +impl StrategyElement { pub fn withdraw( self, deps: Deps, diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index 2711a9f3..88aa16ea 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -1,13 +1,14 @@ use crate::{ + autocompound::AutocompoundState, check::Checkable, contract::{App, AppResult}, distribution::deposit::generate_deposit_strategy, error::AppError, handlers::query::query_balance, helpers::assert_contract, - msg::{AppExecuteMsg, ExecuteMsg, InternalExecuteMsg}, - state::{AUTOCOMPOUND_STATE, CONFIG}, - yield_sources::{AssetShare, BalanceStrategyUnchecked}, + msg::{AppExecuteMsg, ExecuteMsg}, + state::{AUTOCOMPOUND_STATE, CONFIG, STRATEGY_CONFIG}, + yield_sources::{AssetShare, StrategyUnchecked}, }; use abstract_app::abstract_sdk::features::AbstractResponse; use abstract_sdk::ExecutorMsg; @@ -16,9 +17,10 @@ use cosmwasm_std::{ WasmMsg, }; -use super::internal::{deposit_one_strategy, execute_finalize_deposit, execute_one_deposit_step}; use abstract_app::traits::AccountIdentification; +use super::internal::execute_internal_action; + pub fn execute_handler( deps: DepsMut, env: Env, @@ -38,25 +40,8 @@ pub fn execute_handler( } // Endpoints called by the contract directly AppExecuteMsg::Internal(internal_msg) => { - if info.sender != env.contract.address { - return Err(AppError::Unauthorized {}); - } - match internal_msg { - InternalExecuteMsg::DepositOneStrategy { - swap_strategy, - yield_type, - yield_index, - } => deposit_one_strategy(deps, env, swap_strategy, yield_index, yield_type, app), - InternalExecuteMsg::ExecuteOneDepositSwapStep { - asset_in, - denom_out, - expected_amount, - } => execute_one_deposit_step(deps, env, asset_in, denom_out, expected_amount, app), - InternalExecuteMsg::FinalizeDeposit { - yield_type, - yield_index, - } => execute_finalize_deposit(deps, yield_type, yield_index, app), - } + assert_contract(&info, &env)?; + execute_internal_action(deps, env, internal_msg, app) } } } @@ -75,6 +60,14 @@ fn deposit( .or(assert_contract(&info, &env))?; let deposit_msgs = _inner_deposit(deps.as_ref(), &env, funds, yield_source_params, &app)?; + + AUTOCOMPOUND_STATE.save( + deps.storage, + &AutocompoundState { + last_compound: env.block.time, + }, + )?; + Ok(app.response("deposit").add_messages(deposit_msgs)) } @@ -97,13 +90,12 @@ fn update_strategy( deps: DepsMut, env: Env, _info: MessageInfo, - strategy: BalanceStrategyUnchecked, + strategy: StrategyUnchecked, funds: Vec, app: App, ) -> AppResult { // We load it raw because we're changing the strategy - let mut config = CONFIG.load(deps.storage)?; - let old_strategy = config.balance_strategy; + let old_strategy = STRATEGY_CONFIG.load(deps.storage)?; // We check the new strategy let strategy = strategy.check(deps.as_ref(), &app)?; @@ -139,8 +131,7 @@ fn update_strategy( .try_for_each(|f| f.into_iter().try_for_each(|f| available_funds.add(f)))?; // 2. We replace the strategy with the new strategy - config.balance_strategy = strategy; - CONFIG.save(deps.storage, &config)?; + STRATEGY_CONFIG.save(deps.storage, &strategy)?; // 3. We deposit the funds into the new strategy let deposit_msgs = _inner_deposit(deps.as_ref(), &env, available_funds.into(), None, &app)?; @@ -167,7 +158,7 @@ fn update_strategy( fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResult { // Everyone can autocompound - let strategy = CONFIG.load(deps.storage)?.balance_strategy; + let strategy = STRATEGY_CONFIG.load(deps.storage)?; // We withdraw all rewards from protocols let (all_rewards, collect_rewards_msgs) = strategy.withdraw_rewards(deps.as_ref(), &app)?; @@ -196,10 +187,12 @@ fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResu let executor_reward_messages = config.get_executor_reward_messages(deps.as_ref(), &env, info, &app)?; - AUTOCOMPOUND_STATE.update(deps.storage, |mut state| { - state.last_compound = env.block.time; - Ok::<_, AppError>(state) - })?; + AUTOCOMPOUND_STATE.save( + deps.storage, + &AutocompoundState { + last_compound: env.block.time, + }, + )?; Ok(response.add_messages(executor_reward_messages)) } @@ -239,7 +232,7 @@ fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResu // app: &App, // ) -> AppResult> { // // We query the target strategy depending on the existing deposits -// let mut current_strategy_status = CONFIG.load(deps.storage)?.balance_strategy; +// let mut current_strategy_status = CONFIG.load(deps.storage)?.strategy; // current_strategy_status.apply_current_strategy_shares(deps, app)?; // // We correct it if the user asked to correct the share parameters of each strategy @@ -290,9 +283,8 @@ fn _inner_withdraw( // We withdraw the necessary share from all registered investments let withdraw_msgs = - CONFIG + STRATEGY_CONFIG .load(deps.storage)? - .balance_strategy .withdraw(deps.as_ref(), withdraw_share, app)?; Ok(withdraw_msgs.into_iter().collect()) diff --git a/contracts/carrot-app/src/handlers/instantiate.rs b/contracts/carrot-app/src/handlers/instantiate.rs index 290aa3ea..01d4f758 100644 --- a/contracts/carrot-app/src/handlers/instantiate.rs +++ b/contracts/carrot-app/src/handlers/instantiate.rs @@ -1,9 +1,8 @@ use crate::{ - autocompound::AutocompoundState, check::Checkable, contract::{App, AppResult}, msg::AppInstantiateMsg, - state::{AUTOCOMPOUND_STATE, CONFIG}, + state::CONFIG, }; use abstract_app::abstract_sdk::AbstractResponse; use cosmwasm_std::{DepsMut, Env, MessageInfo}; @@ -21,12 +20,6 @@ pub fn instantiate_handler( let config = msg.config.check(deps.as_ref(), &app)?; CONFIG.save(deps.storage, &config)?; - AUTOCOMPOUND_STATE.save( - deps.storage, - &AutocompoundState { - last_compound: env.block.time, - }, - )?; let mut response = app.response("instantiate_savings_app"); diff --git a/contracts/carrot-app/src/handlers/internal.rs b/contracts/carrot-app/src/handlers/internal.rs index d1a1a7f3..e637abac 100644 --- a/contracts/carrot-app/src/handlers/internal.rs +++ b/contracts/carrot-app/src/handlers/internal.rs @@ -1,24 +1,49 @@ use crate::{ contract::{App, AppResult}, distribution::deposit::{DepositStep, OneDepositStrategy}, - helpers::{add_funds, get_proxy_balance}, + helpers::get_proxy_balance, msg::{AppExecuteMsg, ExecuteMsg, InternalExecuteMsg}, replies::REPLY_AFTER_SWAPS_STEP, state::{ - CONFIG, TEMP_CURRENT_COIN, TEMP_CURRENT_YIELD, TEMP_DEPOSIT_COINS, TEMP_EXPECTED_SWAP_COIN, + CONFIG, STRATEGY_CONFIG, TEMP_CURRENT_COIN, TEMP_CURRENT_YIELD, TEMP_DEPOSIT_COINS, + TEMP_EXPECTED_SWAP_COIN, }, - yield_sources::{yield_type::YieldType, BalanceStrategy}, + yield_sources::{yield_type::YieldType, Strategy}, }; use abstract_app::{abstract_sdk::features::AbstractResponse, objects::AnsAsset}; use abstract_dex_adapter::DexInterface; use abstract_sdk::features::AbstractNameService; -use cosmwasm_std::{wasm_execute, Coin, DepsMut, Env, StdError, SubMsg, Uint128}; +use cosmwasm_std::{wasm_execute, Coin, Coins, DepsMut, Env, SubMsg, Uint128}; use cw_asset::AssetInfo; use crate::exchange_rate::query_exchange_rate; use abstract_app::traits::AccountIdentification; -pub fn deposit_one_strategy( +pub fn execute_internal_action( + deps: DepsMut, + env: Env, + internal_msg: InternalExecuteMsg, + app: App, +) -> AppResult { + match internal_msg { + InternalExecuteMsg::DepositOneStrategy { + swap_strategy, + yield_type, + yield_index, + } => deposit_one_strategy(deps, env, swap_strategy, yield_index, yield_type, app), + InternalExecuteMsg::ExecuteOneDepositSwapStep { + asset_in, + denom_out, + expected_amount, + } => execute_one_deposit_step(deps, env, asset_in, denom_out, expected_amount, app), + InternalExecuteMsg::FinalizeDeposit { + yield_type, + yield_index, + } => execute_finalize_deposit(deps, yield_type, yield_index, app), + } +} + +fn deposit_one_strategy( deps: DepsMut, env: Env, strategy: OneDepositStrategy, @@ -33,8 +58,7 @@ pub fn deposit_one_strategy( deps.querier .query_all_balances(app.account_base(deps.as_ref())?.proxy)? )); - - TEMP_DEPOSIT_COINS.save(deps.storage, &vec![])?; + let mut temp_deposit_coins = Coins::default(); // We go through all deposit steps. // If the step is a swap, we execute with a reply to catch the amount change and get the exact deposit amount @@ -62,7 +86,7 @@ pub fn deposit_one_strategy( .map(|msg| Some(SubMsg::reply_on_success(msg, REPLY_AFTER_SWAPS_STEP))), DepositStep::UseFunds { asset } => { - TEMP_DEPOSIT_COINS.update(deps.storage, |funds| add_funds(funds, asset))?; + temp_deposit_coins.add(asset)?; Ok(None) } }) @@ -70,6 +94,8 @@ pub fn deposit_one_strategy( }) .collect::, _>>()?; + TEMP_DEPOSIT_COINS.save(deps.storage, &temp_deposit_coins.into())?; + let msgs = msg.into_iter().flatten().flatten().collect::>(); // Finalize and execute the deposit @@ -143,11 +169,7 @@ pub fn execute_finalize_deposit( Ok(app.response("one-deposit-step").add_submessages(msgs)) } -pub fn save_strategy(deps: DepsMut, strategy: BalanceStrategy) -> AppResult<()> { - CONFIG.update(deps.storage, |mut config| { - config.balance_strategy = strategy; - Ok::<_, StdError>(config) - })?; - +pub fn save_strategy(deps: DepsMut, strategy: Strategy) -> AppResult<()> { + STRATEGY_CONFIG.save(deps.storage, &strategy)?; Ok(()) } diff --git a/contracts/carrot-app/src/handlers/preview.rs b/contracts/carrot-app/src/handlers/preview.rs index 057bee1d..18664a56 100644 --- a/contracts/carrot-app/src/handlers/preview.rs +++ b/contracts/carrot-app/src/handlers/preview.rs @@ -4,7 +4,7 @@ use crate::{ contract::{App, AppResult}, distribution::deposit::generate_deposit_strategy, msg::{DepositPreviewResponse, UpdateStrategyPreviewResponse, WithdrawPreviewResponse}, - yield_sources::{AssetShare, BalanceStrategyUnchecked}, + yield_sources::{AssetShare, StrategyUnchecked}, }; pub fn deposit_preview( @@ -35,7 +35,7 @@ pub fn withdraw_preview( pub fn update_strategy_preview( deps: Deps, funds: Vec, - strategy: BalanceStrategyUnchecked, + strategy: StrategyUnchecked, app: &App, ) -> AppResult { Ok(UpdateStrategyPreviewResponse {}) diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index 1f674f78..edaef535 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -10,6 +10,7 @@ use cw_asset::Asset; use crate::autocompound::get_autocompound_status; use crate::exchange_rate::query_exchange_rate; use crate::msg::{PositionResponse, PositionsResponse}; +use crate::state::STRATEGY_CONFIG; use crate::{ contract::{App, AppResult}, error::AppError, @@ -97,21 +98,18 @@ fn query_compound_status(deps: Deps, env: Env, app: &App) -> AppResult AppResult { - let config = CONFIG.load(deps.storage)?; + let strategy = STRATEGY_CONFIG.load(deps.storage)?; Ok(StrategyResponse { - strategy: config.balance_strategy.into(), + strategy: strategy.into(), }) } pub fn query_strategy_status(deps: Deps, app: &App) -> AppResult { - let config = CONFIG.load(deps.storage)?; + let strategy = STRATEGY_CONFIG.load(deps.storage)?; Ok(StrategyResponse { - strategy: config - .balance_strategy - .query_current_status(deps, app)? - .into(), + strategy: strategy.query_current_status(deps, app)?.into(), }) } @@ -123,8 +121,8 @@ pub fn query_balance(deps: Deps, app: &App) -> AppResult let mut funds = Coins::default(); let mut total_value = Uint128::zero(); - let config = CONFIG.load(deps.storage)?; - config.balance_strategy.0.iter().try_for_each(|s| { + let strategy = STRATEGY_CONFIG.load(deps.storage)?; + strategy.0.iter().try_for_each(|s| { let deposit_value = s .yield_source .ty @@ -145,7 +143,7 @@ pub fn query_balance(deps: Deps, app: &App) -> AppResult } fn query_rewards(deps: Deps, app: &App) -> AppResult { - let strategy = CONFIG.load(deps.storage)?.balance_strategy; + let strategy = STRATEGY_CONFIG.load(deps.storage)?; let mut rewards = Coins::default(); strategy.0.into_iter().try_for_each(|s| { @@ -163,9 +161,8 @@ fn query_rewards(deps: Deps, app: &App) -> AppResult { pub fn query_positions(deps: Deps, app: &App) -> AppResult { Ok(PositionsResponse { - positions: CONFIG + positions: STRATEGY_CONFIG .load(deps.storage)? - .balance_strategy .0 .into_iter() .map(|s| { diff --git a/contracts/carrot-app/src/helpers.rs b/contracts/carrot-app/src/helpers.rs index b579c145..18752e4c 100644 --- a/contracts/carrot-app/src/helpers.rs +++ b/contracts/carrot-app/src/helpers.rs @@ -34,18 +34,16 @@ pub fn add_funds(funds: Vec, to_add: Coin) -> StdResult> { Ok(funds.into()) } -pub const CLOSE_PER_MILLE: u64 = 1; +pub const CLOSE_COEFF: Decimal = Decimal::permille(1); /// Returns wether actual is close to expected within CLOSE_PER_MILLE per mille pub fn close_to(expected: Decimal, actual: Decimal) -> bool { - let close_coeff = Decimal::permille(CLOSE_PER_MILLE); - if expected == Decimal::zero() { - return actual < close_coeff; + return actual < CLOSE_COEFF; } - actual > expected * (Decimal::one() - close_coeff) - && actual < expected * (Decimal::one() + close_coeff) + actual > expected * (Decimal::one() - CLOSE_COEFF) + && actual < expected * (Decimal::one() + CLOSE_COEFF) } pub fn compute_total_value( diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index 1a2568eb..a726a8ad 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -8,7 +8,7 @@ use crate::{ state::ConfigUnchecked, yield_sources::{ yield_type::{YieldType, YieldTypeUnchecked}, - AssetShare, BalanceStrategyElementUnchecked, BalanceStrategyUnchecked, + AssetShare, StrategyElementUnchecked, StrategyUnchecked, }, }; @@ -20,6 +20,8 @@ abstract_app::app_msg_types!(App, AppExecuteMsg, AppQueryMsg); pub struct AppInstantiateMsg { /// Future app configuration pub config: ConfigUnchecked, + /// Future app strategy + pub strategy: StrategyUnchecked, /// Create position with instantiation. /// Will not create position if omitted pub deposit: Option>, @@ -51,7 +53,7 @@ pub enum AppExecuteMsg { /// Rebalances all investments according to a new balance strategy UpdateStrategy { funds: Vec, - strategy: BalanceStrategyUnchecked, + strategy: StrategyUnchecked, }, /// Only called by the contract internally @@ -146,7 +148,7 @@ pub enum AppQueryMsg { #[returns(UpdateStrategyPreviewResponse)] UpdateStrategyPreview { funds: Vec, - strategy: BalanceStrategyUnchecked, + strategy: StrategyUnchecked, }, } @@ -170,7 +172,7 @@ pub struct AssetsBalanceResponse { #[cw_serde] pub struct StrategyResponse { - pub strategy: BalanceStrategyUnchecked, + pub strategy: StrategyUnchecked, } #[cw_serde] @@ -212,7 +214,7 @@ impl CompoundStatus { #[cw_serde] pub struct DepositPreviewResponse { - pub withdraw: Vec<(BalanceStrategyElementUnchecked, Decimal)>, + pub withdraw: Vec<(StrategyElementUnchecked, Decimal)>, pub deposit: Vec, } diff --git a/contracts/carrot-app/src/replies/osmosis/add_to_position.rs b/contracts/carrot-app/src/replies/osmosis/add_to_position.rs index 5b3646fa..6e13cccf 100644 --- a/contracts/carrot-app/src/replies/osmosis/add_to_position.rs +++ b/contracts/carrot-app/src/replies/osmosis/add_to_position.rs @@ -6,7 +6,7 @@ use crate::{ contract::{App, AppResult}, error::AppError, handlers::internal::save_strategy, - state::{CONFIG, TEMP_CURRENT_YIELD}, + state::{STRATEGY_CONFIG, TEMP_CURRENT_YIELD}, yield_sources::yield_type::YieldType, }; @@ -25,7 +25,7 @@ pub fn add_to_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) - // We update the position let current_position_index = TEMP_CURRENT_YIELD.load(deps.storage)?; - let mut strategy = CONFIG.load(deps.storage)?.balance_strategy; + let mut strategy = STRATEGY_CONFIG.load(deps.storage)?; let current_yield = strategy.0.get_mut(current_position_index).unwrap(); diff --git a/contracts/carrot-app/src/replies/osmosis/create_position.rs b/contracts/carrot-app/src/replies/osmosis/create_position.rs index b3b9e2dc..d34f2412 100644 --- a/contracts/carrot-app/src/replies/osmosis/create_position.rs +++ b/contracts/carrot-app/src/replies/osmosis/create_position.rs @@ -6,7 +6,7 @@ use crate::{ contract::{App, AppResult}, error::AppError, handlers::internal::save_strategy, - state::{CONFIG, TEMP_CURRENT_YIELD}, + state::{STRATEGY_CONFIG, TEMP_CURRENT_YIELD}, yield_sources::yield_type::YieldType, }; @@ -24,7 +24,7 @@ pub fn create_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) - // We save the position let current_position_index = TEMP_CURRENT_YIELD.load(deps.storage)?; - let mut strategy = CONFIG.load(deps.storage)?.balance_strategy; + let mut strategy = STRATEGY_CONFIG.load(deps.storage)?; let current_yield = strategy.0.get_mut(current_position_index).unwrap(); diff --git a/contracts/carrot-app/src/state.rs b/contracts/carrot-app/src/state.rs index f8e09afa..6b432a7f 100644 --- a/contracts/carrot-app/src/state.rs +++ b/contracts/carrot-app/src/state.rs @@ -4,9 +4,10 @@ use cw_storage_plus::Item; use crate::autocompound::{AutocompoundConfigBase, AutocompoundState}; use crate::check::{Checked, Unchecked}; -use crate::yield_sources::BalanceStrategyBase; +use crate::yield_sources::Strategy; pub const CONFIG: Item = Item::new("config"); +pub const STRATEGY_CONFIG: Item = Item::new("strategy_config"); pub const AUTOCOMPOUND_STATE: Item = Item::new("position"); pub const CURRENT_EXECUTOR: Item = Item::new("executor"); @@ -21,7 +22,6 @@ pub type ConfigUnchecked = ConfigBase; #[cw_serde] pub struct ConfigBase { - pub balance_strategy: BalanceStrategyBase, pub autocompound_config: AutocompoundConfigBase, pub dex: String, } diff --git a/contracts/carrot-app/src/yield_sources/mod.rs b/contracts/carrot-app/src/yield_sources/mod.rs index 99c25f03..00b4c7ca 100644 --- a/contracts/carrot-app/src/yield_sources/mod.rs +++ b/contracts/carrot-app/src/yield_sources/mod.rs @@ -49,12 +49,12 @@ pub enum ShareType { // This represents a balance strategy // This object is used for storing the current strategy, retrieving the actual strategy status or expressing a target strategy when depositing #[cw_serde] -pub struct BalanceStrategyBase(pub Vec>); +pub struct StrategyBase(pub Vec>); -pub type BalanceStrategyUnchecked = BalanceStrategyBase; -pub type BalanceStrategy = BalanceStrategyBase; +pub type StrategyUnchecked = StrategyBase; +pub type Strategy = StrategyBase; -impl BalanceStrategy { +impl Strategy { pub fn all_denoms(&self) -> Vec { self.0 .clone() @@ -65,15 +65,15 @@ impl BalanceStrategy { } #[cw_serde] -pub struct BalanceStrategyElementBase { +pub struct StrategyElementBase { pub yield_source: YieldSourceBase, pub share: Decimal, } -impl From>> for BalanceStrategyBase { - fn from(value: Vec>) -> Self { +impl From>> for StrategyBase { + fn from(value: Vec>) -> Self { Self(value) } } -pub type BalanceStrategyElementUnchecked = BalanceStrategyElementBase; -pub type BalanceStrategyElement = BalanceStrategyElementBase; +pub type StrategyElementUnchecked = StrategyElementBase; +pub type StrategyElement = StrategyElementBase; diff --git a/contracts/carrot-app/tests/common.rs b/contracts/carrot-app/tests/common.rs index 8d693473..62e4045f 100644 --- a/contracts/carrot-app/tests/common.rs +++ b/contracts/carrot-app/tests/common.rs @@ -8,9 +8,7 @@ use carrot_app::msg::AppInstantiateMsg; use carrot_app::state::ConfigBase; use carrot_app::yield_sources::osmosis_cl_pool::ConcentratedPoolParamsBase; use carrot_app::yield_sources::yield_type::YieldTypeBase; -use carrot_app::yield_sources::{ - AssetShare, BalanceStrategyBase, BalanceStrategyElementBase, YieldSourceBase, -}; +use carrot_app::yield_sources::{AssetShare, StrategyBase, StrategyElementBase, YieldSourceBase}; use cosmwasm_std::{coin, coins, Coins, Decimal, Uint128, Uint64}; use cw_asset::AssetInfoUnchecked; use cw_orch::environment::MutCwEnv; @@ -132,30 +130,30 @@ pub fn deploy( _phantom: std::marker::PhantomData, }, }, - balance_strategy: BalanceStrategyBase(vec![BalanceStrategyElementBase { - yield_source: YieldSourceBase { - asset_distribution: vec![ - AssetShare { - denom: USDT.to_string(), - share: Decimal::percent(50), - }, - AssetShare { - denom: USDC.to_string(), - share: Decimal::percent(50), - }, - ], - ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { - pool_id, - lower_tick: INITIAL_LOWER_TICK, - upper_tick: INITIAL_UPPER_TICK, - position_id: None, - _phantom: std::marker::PhantomData, - }), - }, - share: Decimal::one(), - }]), dex: OSMOSIS.to_string(), }, + strategy: StrategyBase(vec![StrategyElementBase { + yield_source: YieldSourceBase { + asset_distribution: vec![ + AssetShare { + denom: USDT.to_string(), + share: Decimal::percent(50), + }, + AssetShare { + denom: USDC.to_string(), + share: Decimal::percent(50), + }, + ], + ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + pool_id, + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + _phantom: std::marker::PhantomData, + }), + }, + share: Decimal::one(), + }]), deposit: initial_deposit, }; diff --git a/contracts/carrot-app/tests/config.rs b/contracts/carrot-app/tests/config.rs index 3cc1ff83..afdb38ad 100644 --- a/contracts/carrot-app/tests/config.rs +++ b/contracts/carrot-app/tests/config.rs @@ -5,7 +5,7 @@ use carrot_app::{ msg::{AppExecuteMsgFns, AppQueryMsgFns}, yield_sources::{ osmosis_cl_pool::ConcentratedPoolParamsBase, yield_type::YieldTypeBase, AssetShare, - BalanceStrategyBase, BalanceStrategyElementBase, YieldSourceBase, + StrategyBase, StrategyElementBase, YieldSourceBase, }, }; use common::{INITIAL_LOWER_TICK, INITIAL_UPPER_TICK}; @@ -21,8 +21,8 @@ fn rebalance_fails() -> anyhow::Result<()> { carrot_app .update_strategy( vec![], - BalanceStrategyBase(vec![ - BalanceStrategyElementBase { + StrategyBase(vec![ + StrategyElementBase { yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { @@ -44,7 +44,7 @@ fn rebalance_fails() -> anyhow::Result<()> { }, share: Decimal::one(), }, - BalanceStrategyElementBase { + StrategyElementBase { yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { @@ -80,8 +80,8 @@ fn rebalance_success() -> anyhow::Result<()> { let (pool_id, carrot_app) = setup_test_tube(false)?; let mut chain = carrot_app.get_chain().clone(); - let new_strat = BalanceStrategyBase(vec![ - BalanceStrategyElementBase { + let new_strat = StrategyBase(vec![ + StrategyElementBase { yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { @@ -103,7 +103,7 @@ fn rebalance_success() -> anyhow::Result<()> { }, share: Decimal::percent(50), }, - BalanceStrategyElementBase { + StrategyElementBase { yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { @@ -157,8 +157,8 @@ fn rebalance_with_new_pool_success() -> anyhow::Result<()> { deposit_coins.clone(), )?; - let new_strat = BalanceStrategyBase(vec![ - BalanceStrategyElementBase { + let new_strat = StrategyBase(vec![ + StrategyElementBase { yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { @@ -180,7 +180,7 @@ fn rebalance_with_new_pool_success() -> anyhow::Result<()> { }, share: Decimal::percent(50), }, - BalanceStrategyElementBase { + StrategyElementBase { yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { @@ -257,12 +257,12 @@ fn rebalance_with_stale_strategy_success() -> anyhow::Result<()> { }), }; - let strat = BalanceStrategyBase(vec![ - BalanceStrategyElementBase { + let strat = StrategyBase(vec![ + StrategyElementBase { yield_source: common_yield_source.clone(), share: Decimal::percent(50), }, - BalanceStrategyElementBase { + StrategyElementBase { yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { @@ -288,7 +288,7 @@ fn rebalance_with_stale_strategy_success() -> anyhow::Result<()> { carrot_app.update_strategy(deposit_coins.clone(), strat.clone())?; - let new_strat = BalanceStrategyBase(vec![BalanceStrategyElementBase { + let new_strat = StrategyBase(vec![StrategyElementBase { yield_source: common_yield_source.clone(), share: Decimal::percent(100), }]); @@ -351,8 +351,8 @@ fn rebalance_with_current_and_stale_strategy_success() -> anyhow::Result<()> { }), }; - let strat = BalanceStrategyBase(vec![ - BalanceStrategyElementBase { + let strat = StrategyBase(vec![ + StrategyElementBase { yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { @@ -374,7 +374,7 @@ fn rebalance_with_current_and_stale_strategy_success() -> anyhow::Result<()> { }, share: Decimal::percent(50), }, - BalanceStrategyElementBase { + StrategyElementBase { yield_source: moving_strategy.clone(), share: Decimal::percent(50), }, diff --git a/contracts/carrot-app/tests/deposit_withdraw.rs b/contracts/carrot-app/tests/deposit_withdraw.rs index 18f54981..17e1b5ac 100644 --- a/contracts/carrot-app/tests/deposit_withdraw.rs +++ b/contracts/carrot-app/tests/deposit_withdraw.rs @@ -6,8 +6,7 @@ use carrot_app::{ msg::{AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse}, yield_sources::{ mars::MarsDepositParams, osmosis_cl_pool::ConcentratedPoolParamsBase, - yield_type::YieldTypeBase, AssetShare, BalanceStrategyBase, BalanceStrategyElementBase, - YieldSourceBase, + yield_type::YieldTypeBase, AssetShare, StrategyBase, StrategyElementBase, YieldSourceBase, }, AppInterface, }; @@ -147,8 +146,8 @@ fn deposit_multiple_assets() -> anyhow::Result<()> { fn deposit_multiple_positions() -> anyhow::Result<()> { let (pool_id, carrot_app) = setup_test_tube(false)?; - let new_strat = BalanceStrategyBase(vec![ - BalanceStrategyElementBase { + let new_strat = StrategyBase(vec![ + StrategyElementBase { yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { @@ -170,7 +169,7 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { }, share: Decimal::percent(50), }, - BalanceStrategyElementBase { + StrategyElementBase { yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { @@ -219,8 +218,8 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { fn deposit_multiple_positions_with_empty() -> anyhow::Result<()> { let (pool_id, carrot_app) = setup_test_tube(false)?; - let new_strat = BalanceStrategyBase(vec![ - BalanceStrategyElementBase { + let new_strat = StrategyBase(vec![ + StrategyElementBase { yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { @@ -242,7 +241,7 @@ fn deposit_multiple_positions_with_empty() -> anyhow::Result<()> { }, share: Decimal::percent(50), }, - BalanceStrategyElementBase { + StrategyElementBase { yield_source: YieldSourceBase { asset_distribution: vec![ AssetShare { @@ -264,7 +263,7 @@ fn deposit_multiple_positions_with_empty() -> anyhow::Result<()> { }, share: Decimal::percent(50), }, - BalanceStrategyElementBase { + StrategyElementBase { yield_source: YieldSourceBase { asset_distribution: vec![AssetShare { denom: USDT.to_string(), From eb326a394e387bb98a33a59ea9b624f8e2efc1fc Mon Sep 17 00:00:00 2001 From: Kayanski Date: Mon, 8 Apr 2024 17:39:09 +0000 Subject: [PATCH 27/42] error rename --- contracts/carrot-app/src/error.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/carrot-app/src/error.rs b/contracts/carrot-app/src/error.rs index 010e1307..7232c2f4 100644 --- a/contracts/carrot-app/src/error.rs +++ b/contracts/carrot-app/src/error.rs @@ -42,6 +42,9 @@ pub enum AppError { #[error("Unauthorized")] Unauthorized {}, + #[error("Sender not contract. Only the contract can execute internal calls")] + SenderNotContract {}, + #[error("Wrong denom deposited, expected exactly {expected}, got {got:?}")] DepositError { expected: AssetInfo, got: Vec }, From 2085369e1ed1fb44c50260d2b01c89eb3830a0bb Mon Sep 17 00:00:00 2001 From: Kayanski Date: Mon, 8 Apr 2024 18:44:26 +0000 Subject: [PATCH 28/42] Migrate Msg to struct --- contracts/carrot-app/src/msg.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index a726a8ad..41a728d3 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -153,7 +153,7 @@ pub enum AppQueryMsg { } #[cosmwasm_schema::cw_serde] -pub enum AppMigrateMsg {} +pub struct AppMigrateMsg {} #[cosmwasm_schema::cw_serde] pub struct BalanceResponse { From 6903c1a90a75504e7838629c1fb91d3931f86fed Mon Sep 17 00:00:00 2001 From: Kayanski Date: Thu, 11 Apr 2024 13:15:46 +0000 Subject: [PATCH 29/42] Fixed instantiate and dependencies --- contracts/carrot-app/Cargo.toml | 2 +- contracts/carrot-app/src/handlers/instantiate.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/carrot-app/Cargo.toml b/contracts/carrot-app/Cargo.toml index c31eea8d..286b6688 100644 --- a/contracts/carrot-app/Cargo.toml +++ b/contracts/carrot-app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "carrot-app" -version = "0.1.0" +version = "1.0.1" authors = [ "CyberHoward ", "Adair ", diff --git a/contracts/carrot-app/src/handlers/instantiate.rs b/contracts/carrot-app/src/handlers/instantiate.rs index 01d4f758..e4c93852 100644 --- a/contracts/carrot-app/src/handlers/instantiate.rs +++ b/contracts/carrot-app/src/handlers/instantiate.rs @@ -2,7 +2,7 @@ use crate::{ check::Checkable, contract::{App, AppResult}, msg::AppInstantiateMsg, - state::CONFIG, + state::{CONFIG, STRATEGY_CONFIG}, }; use abstract_app::abstract_sdk::AbstractResponse; use cosmwasm_std::{DepsMut, Env, MessageInfo}; @@ -20,6 +20,8 @@ pub fn instantiate_handler( let config = msg.config.check(deps.as_ref(), &app)?; CONFIG.save(deps.storage, &config)?; + let strategy = msg.strategy.check(deps.as_ref(), &app)?; + STRATEGY_CONFIG.save(deps.storage, &strategy)?; let mut response = app.response("instantiate_savings_app"); From 2414e216be248ef9479edc77480f126fb218006f Mon Sep 17 00:00:00 2001 From: Kayanski Date: Thu, 11 Apr 2024 13:18:55 +0000 Subject: [PATCH 30/42] Added localnet tests --- .../carrot-app/examples/localnet_deploy.rs | 157 ++++++++++++++++++ .../carrot-app/examples/localnet_install.rs | 131 +++++++++++++++ .../carrot-app/examples/localnet_test.rs | 81 +++++++++ 3 files changed, 369 insertions(+) create mode 100644 contracts/carrot-app/examples/localnet_deploy.rs create mode 100644 contracts/carrot-app/examples/localnet_install.rs create mode 100644 contracts/carrot-app/examples/localnet_test.rs diff --git a/contracts/carrot-app/examples/localnet_deploy.rs b/contracts/carrot-app/examples/localnet_deploy.rs new file mode 100644 index 00000000..991804dc --- /dev/null +++ b/contracts/carrot-app/examples/localnet_deploy.rs @@ -0,0 +1,157 @@ +use abstract_app::objects::{ + namespace::ABSTRACT_NAMESPACE, pool_id::PoolAddressBase, AssetEntry, PoolMetadata, PoolType, +}; +use abstract_client::{AbstractClient, Application, Namespace}; +use abstract_interface::Abstract; +use abstract_sdk::core::ans_host::QueryMsgFns; +use cosmwasm_std::Decimal; +use cw_asset::AssetInfoUnchecked; +use cw_orch::{ + anyhow, + contract::Deploy, + daemon::{ + networks::{LOCAL_OSMO, OSMOSIS_1, OSMO_5}, + Daemon, DaemonBuilder, + }, + prelude::*, + tokio::runtime::Runtime, +}; +use dotenv::dotenv; + +use carrot_app::{contract::APP_ID, AppInterface}; +use cw_orch::osmosis_test_tube::osmosis_test_tube::cosmrs::proto::traits::Message; +use osmosis_std::types::{ + cosmos::base::v1beta1, + osmosis::concentratedliquidity::{ + poolmodel::concentrated::v1beta1::{ + MsgCreateConcentratedPool, MsgCreateConcentratedPoolResponse, + }, + v1beta1::{MsgCreatePosition, MsgCreatePositionResponse}, + }, +}; +use prost_types::Any; + +pub const ION: &str = "uion"; +pub const OSMO: &str = "uosmo"; + +pub const TICK_SPACING: u64 = 100; +pub const SPREAD_FACTOR: u64 = 0; + +pub const INITIAL_LOWER_TICK: i64 = -100000; +pub const INITIAL_UPPER_TICK: i64 = 10000; + +fn main() -> anyhow::Result<()> { + dotenv().ok(); + env_logger::init(); + let mut chain = LOCAL_OSMO; + chain.grpc_urls = &["http://localhost:9090"]; + chain.chain_id = "osmosis-1"; + + let rt = Runtime::new()?; + let daemon = DaemonBuilder::default() + .chain(chain) + .handle(rt.handle()) + .build()?; + + // We create a CL pool + let pool_id = create_pool(daemon.clone())?; + // We register the ans entries of ion and osmosis balances + register_ans(daemon.clone(), pool_id)?; + + deploy_app(daemon.clone())?; + Ok(()) +} + +pub fn create_pool(chain: Chain) -> anyhow::Result { + let response = chain.commit_any::( + vec![Any { + value: MsgCreateConcentratedPool { + sender: chain.sender().to_string(), + denom0: ION.to_owned(), + denom1: OSMO.to_owned(), + tick_spacing: TICK_SPACING, + spread_factor: SPREAD_FACTOR.to_string(), + } + .encode_to_vec(), + type_url: MsgCreateConcentratedPool::TYPE_URL.to_string(), + }], + None, + )?; + + let pool_id = response + .event_attr_value("pool_created", "pool_id")? + .parse()?; + // Provide liquidity + + chain.commit_any::( + vec![Any { + type_url: MsgCreatePosition::TYPE_URL.to_string(), + value: MsgCreatePosition { + pool_id, + sender: chain.sender().to_string(), + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + tokens_provided: vec![ + v1beta1::Coin { + denom: ION.to_string(), + amount: "1_000_000".to_owned(), + }, + v1beta1::Coin { + denom: OSMO.to_string(), + amount: "1_000_000".to_owned(), + }, + ], + token_min_amount0: "0".to_string(), + token_min_amount1: "0".to_string(), + } + .encode_to_vec(), + }], + None, + )?; + Ok(pool_id) +} + +pub fn register_ans(chain: Chain, pool_id: u64) -> anyhow::Result<()> { + let asset0 = ION.to_owned(); + let asset1 = OSMO.to_owned(); + // We register the pool inside the Abstract ANS + let client = AbstractClient::builder(chain.clone()) + .dex("osmosis") + .assets(vec![ + (ION.to_string(), AssetInfoUnchecked::Native(asset0.clone())), + (OSMO.to_string(), AssetInfoUnchecked::Native(asset1.clone())), + ]) + .pools(vec![( + PoolAddressBase::Id(pool_id), + PoolMetadata { + dex: "osmosis".to_owned(), + pool_type: PoolType::ConcentratedLiquidity, + assets: vec![AssetEntry::new(ION), AssetEntry::new(OSMO)], + }, + )]) + .build()?; + + Ok(()) +} + +pub fn deploy_app(chain: Chain) -> anyhow::Result<()> { + let client = abstract_client::AbstractClient::new(chain.clone())?; + // We deploy the carrot_app + let publisher = client + .publisher_builder(Namespace::new(ABSTRACT_NAMESPACE)?) + .install_on_sub_account(false) + .build()?; + + // The dex adapter + publisher.publish_adapter::<_, abstract_dex_adapter::interface::DexAdapter>( + abstract_dex_adapter::msg::DexInstantiateMsg { + swap_fee: Decimal::permille(2), + recipient_account: 0, + }, + )?; + + // The savings app + publisher.publish_app::>()?; + + Ok(()) +} diff --git a/contracts/carrot-app/examples/localnet_install.rs b/contracts/carrot-app/examples/localnet_install.rs new file mode 100644 index 00000000..87d7a766 --- /dev/null +++ b/contracts/carrot-app/examples/localnet_install.rs @@ -0,0 +1,131 @@ +use abstract_app::objects::{ + module::ModuleInfo, namespace::ABSTRACT_NAMESPACE, AccountId, AssetEntry, +}; +use abstract_client::{Application, Namespace}; +use abstract_dex_adapter::{interface::DexAdapter, DEX_ADAPTER_ID}; +use abstract_interface::{Abstract, VCQueryFns}; +use abstract_sdk::core::{ans_host::QueryMsgFns, app::BaseMigrateMsg}; +use cosmwasm_std::{Decimal, Uint128, Uint64}; +use cw_orch::{ + anyhow, + contract::Deploy, + daemon::{ + networks::{LOCAL_OSMO, OSMOSIS_1, OSMO_5}, + Daemon, DaemonBuilder, + }, + prelude::*, + tokio::runtime::Runtime, +}; +use dotenv::dotenv; + +use carrot_app::{ + autocompound::{AutocompoundConfigBase, AutocompoundRewardsConfigBase}, + contract::APP_ID, + msg::{AppInstantiateMsg, AppMigrateMsg, MigrateMsg}, + state::ConfigBase, + yield_sources::{ + osmosis_cl_pool::ConcentratedPoolParamsBase, yield_type::YieldTypeBase, AssetShare, + StrategyBase, StrategyElementBase, YieldSourceBase, + }, + AppInterface, +}; + +pub const ION: &str = "uion"; +pub const OSMO: &str = "uosmo"; + +pub const TICK_SPACING: u64 = 100; +pub const SPREAD_FACTOR: u64 = 0; + +pub const INITIAL_LOWER_TICK: i64 = -100000; +pub const INITIAL_UPPER_TICK: i64 = 10000; + +pub const POOL_ID: u64 = 1; +pub const USER_NAMESPACE: &str = "usernamespace"; + +fn main() -> anyhow::Result<()> { + dotenv().ok(); + env_logger::init(); + let mut chain = LOCAL_OSMO; + chain.grpc_urls = &["http://localhost:9090"]; + chain.chain_id = "osmosis-1"; + + let rt = Runtime::new()?; + let daemon = DaemonBuilder::default() + .chain(chain) + .handle(rt.handle()) + .build()?; + + let client = abstract_client::AbstractClient::new(daemon.clone())?; + + // Verify modules exist + let account = client + .account_builder() + .install_on_sub_account(false) + .namespace(USER_NAMESPACE.try_into()?) + .build()?; + + let init_msg = AppInstantiateMsg { + config: ConfigBase { + // 5 mins + autocompound_config: AutocompoundConfigBase { + cooldown_seconds: Uint64::new(300), + rewards: AutocompoundRewardsConfigBase { + gas_asset: AssetEntry::new(OSMO), + swap_asset: AssetEntry::new(ION), + reward: Uint128::new(1000), + min_gas_balance: Uint128::new(2000), + max_gas_balance: Uint128::new(10000), + _phantom: std::marker::PhantomData, + }, + }, + dex: "osmosis".to_string(), + }, + strategy: StrategyBase(vec![StrategyElementBase { + yield_source: YieldSourceBase { + asset_distribution: vec![ + AssetShare { + denom: ION.to_string(), + share: Decimal::percent(50), + }, + AssetShare { + denom: OSMO.to_string(), + share: Decimal::percent(50), + }, + ], + ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + pool_id: POOL_ID, + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + _phantom: std::marker::PhantomData, + }), + }, + share: Decimal::one(), + }]), + deposit: None, + }; + + let carrot_app = account + .install_app_with_dependencies::>( + &init_msg, + Empty {}, + &[], + )?; + + // We update authorized addresses on the adapter for the app + let dex_adapter: Application> = account.application()?; + dex_adapter.execute( + &abstract_dex_adapter::msg::ExecuteMsg::Base( + abstract_app::abstract_core::adapter::BaseExecuteMsg { + proxy_address: Some(carrot_app.account().proxy()?.to_string()), + msg: abstract_app::abstract_core::adapter::AdapterBaseMsg::UpdateAuthorizedAddresses { + to_add: vec![carrot_app.addr_str()?], + to_remove: vec![], + }, + }, + ), + None, + )?; + + Ok(()) +} diff --git a/contracts/carrot-app/examples/localnet_test.rs b/contracts/carrot-app/examples/localnet_test.rs new file mode 100644 index 00000000..cd3ee48a --- /dev/null +++ b/contracts/carrot-app/examples/localnet_test.rs @@ -0,0 +1,81 @@ +use abstract_app::objects::{ + module::ModuleInfo, namespace::ABSTRACT_NAMESPACE, AccountId, AssetEntry, +}; +use abstract_client::{Application, Namespace}; +use abstract_dex_adapter::{interface::DexAdapter, DEX_ADAPTER_ID}; +use abstract_interface::{Abstract, VCQueryFns}; +use abstract_sdk::core::ans_host::QueryMsgFns; +use cosmwasm_std::{coins, Decimal, Uint128, Uint64}; +use cw_orch::{ + anyhow, + contract::Deploy, + daemon::{ + networks::{LOCAL_OSMO, OSMOSIS_1, OSMO_5}, + Daemon, DaemonBuilder, + }, + prelude::*, + tokio::runtime::Runtime, +}; +use dotenv::dotenv; + +use carrot_app::{ + autocompound::{AutocompoundConfigBase, AutocompoundRewardsConfigBase}, + contract::APP_ID, + msg::AppInstantiateMsg, + state::ConfigBase, + yield_sources::{ + osmosis_cl_pool::ConcentratedPoolParamsBase, yield_type::YieldTypeBase, AssetShare, + StrategyBase, StrategyElementBase, YieldSourceBase, + }, + AppExecuteMsgFns, AppInterface, +}; + +pub const ION: &str = "uion"; +pub const OSMO: &str = "uosmo"; + +pub const TICK_SPACING: u64 = 100; +pub const SPREAD_FACTOR: u64 = 0; + +pub const INITIAL_LOWER_TICK: i64 = -100000; +pub const INITIAL_UPPER_TICK: i64 = 10000; + +pub const POOL_ID: u64 = 2; +pub const USER_NAMESPACE: &str = "usernamespace"; + +fn main() -> anyhow::Result<()> { + dotenv().ok(); + env_logger::init(); + let mut chain = LOCAL_OSMO; + chain.grpc_urls = &["http://localhost:9090"]; + chain.chain_id = "osmosis-1"; + + let rt = Runtime::new()?; + let daemon = DaemonBuilder::default() + .chain(chain) + .handle(rt.handle()) + .build()?; + + let client = abstract_client::AbstractClient::new(daemon.clone())?; + + let block_info = daemon.block_info()?; + + // Verify modules exist + let account = client + .account_builder() + .install_on_sub_account(false) + .namespace(USER_NAMESPACE.try_into()?) + .build()?; + + let carrot: Application> = account.application()?; + + daemon.rt_handle.block_on( + daemon + .daemon + .sender + .bank_send(account.proxy()?.as_str(), coins(10_000, "uosmo")), + )?; + + carrot.deposit(coins(10_000, "uosmo"), None)?; + + Ok(()) +} From 3de860ba4a3e04198944456bed6c144493e50819 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Thu, 11 Apr 2024 17:09:35 +0000 Subject: [PATCH 31/42] Gas optimizations --- Cargo.toml | 4 +-- contracts/carrot-app/examples/gas-usage.md | 12 +++++++++ .../carrot-app/examples/localnet_test.rs | 3 +++ .../carrot-app/src/distribution/deposit.rs | 11 +++----- contracts/carrot-app/src/handlers/execute.rs | 10 ------- contracts/carrot-app/src/handlers/internal.rs | 15 ++++------- .../src/replies/osmosis/add_to_position.rs | 1 + .../src/replies/osmosis/create_position.rs | 1 + .../src/yield_sources/osmosis_cl_pool.rs | 26 +++++++++++-------- 9 files changed, 43 insertions(+), 40 deletions(-) create mode 100644 contracts/carrot-app/examples/gas-usage.md diff --git a/Cargo.toml b/Cargo.toml index 245db0ec..66125e42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ cw-orch = "0.20.1" abstract-app = { version = "0.21.0" } abstract-interface = { version = "0.21.0" } -abstract-dex-adapter = { git = "https://github.com/abstractsdk/abstract.git", tag = "v0.21.0" } +abstract-dex-adapter = { git = "https://github.com/abstractsdk/abstract.git" } abstract-money-market-adapter = { git = "https://github.com/abstractsdk/abstract.git" } abstract-money-market-standard = { git = "https://github.com/abstractsdk/abstract.git" } abstract-client = { version = "0.21.0" } @@ -17,7 +17,7 @@ abstract-sdk = { version = "0.21.0", features = ["stargate"] } [patch.crates-io] -# This is included to avoid recompinling too much +# This is included to avoid recompling too much osmosis-test-tube = { path = "../test-tube/packages/osmosis-test-tube" } diff --git a/contracts/carrot-app/examples/gas-usage.md b/contracts/carrot-app/examples/gas-usage.md new file mode 100644 index 00000000..2be208f2 --- /dev/null +++ b/contracts/carrot-app/examples/gas-usage.md @@ -0,0 +1,12 @@ +Gas usage tests : +1. Create position on carrot app +V1 : 882193 +V2 : 1238259 + +2. Add to position +V1 : 1774990 +V2 : 3172677 + +3. Withdraw all +V1 : 1100482 +V2 : 1189022 \ No newline at end of file diff --git a/contracts/carrot-app/examples/localnet_test.rs b/contracts/carrot-app/examples/localnet_test.rs index cd3ee48a..8d44f046 100644 --- a/contracts/carrot-app/examples/localnet_test.rs +++ b/contracts/carrot-app/examples/localnet_test.rs @@ -75,7 +75,10 @@ fn main() -> anyhow::Result<()> { .bank_send(account.proxy()?.as_str(), coins(10_000, "uosmo")), )?; + // carrot.deposit(coins(10_000, "uosmo"), None)?; carrot.deposit(coins(10_000, "uosmo"), None)?; + // carrot.withdraw(None)?; + Ok(()) } diff --git a/contracts/carrot-app/src/distribution/deposit.rs b/contracts/carrot-app/src/distribution/deposit.rs index 8aeed063..82ba46a7 100644 --- a/contracts/carrot-app/src/distribution/deposit.rs +++ b/contracts/carrot-app/src/distribution/deposit.rs @@ -258,16 +258,13 @@ impl Strategy { /// Corrects the current strategy with some parameters passed by the user pub fn correct_with(&mut self, params: Option>>>) { // We correct the strategy if specified in parameters - let params = params.unwrap_or_else(|| vec![None; self.0.len()]); - - self.0 - .iter_mut() - .zip(params) - .for_each(|(strategy, params)| { - if let Some(param) = params { + if let Some(params) = params { + self.0.iter_mut().zip(params).for_each(|(strategy, param)| { + if let Some(param) = param { strategy.yield_source.asset_distribution = param; } }) + } } } diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index 88aa16ea..74be03c9 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -109,7 +109,6 @@ fn update_strategy( .filter(|x| !strategy.0.contains(x)) .collect(); - deps.api.debug("After stale sources"); let (withdrawn_funds, withdraw_msgs): (Vec>, Vec>) = all_stale_sources .into_iter() @@ -124,8 +123,6 @@ fn update_strategy( .into_iter() .unzip(); - deps.api - .debug(&format!("After withdraw messages : {:?}", withdrawn_funds)); withdrawn_funds .into_iter() .try_for_each(|f| f.into_iter().try_for_each(|f| available_funds.add(f)))?; @@ -136,13 +133,6 @@ fn update_strategy( // 3. We deposit the funds into the new strategy let deposit_msgs = _inner_deposit(deps.as_ref(), &env, available_funds.into(), None, &app)?; - deps.api.debug(&format!( - "Proxy balance before withdraw : {:?}", - deps.querier - .query_all_balances(app.account_base(deps.as_ref())?.proxy)? - )); - - deps.api.debug("After deposit msgs"); Ok(app .response("rebalance") .add_messages( diff --git a/contracts/carrot-app/src/handlers/internal.rs b/contracts/carrot-app/src/handlers/internal.rs index e637abac..90c33948 100644 --- a/contracts/carrot-app/src/handlers/internal.rs +++ b/contracts/carrot-app/src/handlers/internal.rs @@ -51,13 +51,7 @@ fn deposit_one_strategy( yield_type: YieldType, app: App, ) -> AppResult { - deps.api - .debug(&format!("We're depositing {:?}-{:?}", strategy, yield_type)); - deps.api.debug(&format!( - "Proxy balance after withdraw : {:?}", - deps.querier - .query_all_balances(app.account_base(deps.as_ref())?.proxy)? - )); + deps.api.debug("Start deposit one strategy"); let mut temp_deposit_coins = Coins::default(); // We go through all deposit steps. @@ -125,8 +119,7 @@ pub fn execute_one_deposit_step( app: App, ) -> AppResult { let config = CONFIG.load(deps.storage)?; - deps.api - .debug(&format!("Deposit step swap : {:?}", asset_in)); + deps.api.debug("Start onde deposit step"); let exchange_rate_in = query_exchange_rate(deps.as_ref(), asset_in.denom.clone(), &app)?; let exchange_rate_out = query_exchange_rate(deps.as_ref(), denom_out.clone(), &app)?; @@ -160,13 +153,15 @@ pub fn execute_finalize_deposit( yield_index: usize, app: App, ) -> AppResult { + deps.api.debug("Start finalize deposit"); let available_deposit_coins = TEMP_DEPOSIT_COINS.load(deps.storage)?; TEMP_CURRENT_YIELD.save(deps.storage, &yield_index)?; let msgs = yield_type.deposit(deps.as_ref(), available_deposit_coins, &app)?; - Ok(app.response("one-deposit-step").add_submessages(msgs)) + deps.api.debug("End finalize deposit"); + Ok(app.response("finalize-deposit").add_submessages(msgs)) } pub fn save_strategy(deps: DepsMut, strategy: Strategy) -> AppResult<()> { diff --git a/contracts/carrot-app/src/replies/osmosis/add_to_position.rs b/contracts/carrot-app/src/replies/osmosis/add_to_position.rs index 6e13cccf..8b49658e 100644 --- a/contracts/carrot-app/src/replies/osmosis/add_to_position.rs +++ b/contracts/carrot-app/src/replies/osmosis/add_to_position.rs @@ -36,6 +36,7 @@ pub fn add_to_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) - } YieldType::Mars(_) => return Err(AppError::WrongYieldType {}), }; + deps.api.debug("after add"); save_strategy(deps, strategy)?; diff --git a/contracts/carrot-app/src/replies/osmosis/create_position.rs b/contracts/carrot-app/src/replies/osmosis/create_position.rs index d34f2412..9cfe5ebf 100644 --- a/contracts/carrot-app/src/replies/osmosis/create_position.rs +++ b/contracts/carrot-app/src/replies/osmosis/create_position.rs @@ -36,6 +36,7 @@ pub fn create_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) - YieldType::Mars(_) => return Err(AppError::WrongYieldType {}), }; + deps.api.debug("after create"); save_strategy(deps, strategy)?; Ok(app diff --git a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs index 713bd81c..3c6233fb 100644 --- a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs +++ b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs @@ -44,9 +44,8 @@ pub type ConcentratedPoolParams = ConcentratedPoolParamsBase; impl YieldTypeImplementation for ConcentratedPoolParams { fn deposit(self, deps: Deps, funds: Vec, app: &App) -> AppResult> { // We verify there is a position stored - if self.position(deps).is_ok() { - // We just deposit - self.raw_deposit(deps, funds, app) + if let Ok(position) = self.position(deps) { + self.raw_deposit(deps, funds, app, position) } else { // We need to create a position self.create_position(deps, funds, app) @@ -183,8 +182,6 @@ impl ConcentratedPoolParams { // create_position_msg: CreatePositionMessage, ) -> AppResult> { let proxy_addr = app.account_base(deps)?.proxy; - deps.api - .debug(&format!("coins to be deposited : {:?}", funds)); // 2. Create a position let tokens = cosmwasm_to_proto_coins(funds); let msg = app.executor(deps).execute_with_reply_and_data( @@ -205,27 +202,33 @@ impl ConcentratedPoolParams { Ok(vec![msg]) } - fn raw_deposit(&self, deps: Deps, funds: Vec, app: &App) -> AppResult> { - let pool = self.position(deps)?; - let position = pool.position.unwrap(); + fn raw_deposit( + &self, + deps: Deps, + funds: Vec, + app: &App, + position: FullPositionBreakdown, + ) -> AppResult> { + let position_id = position.position.unwrap().position_id; let proxy_addr = app.account_base(deps)?.proxy; // We need to make sure the amounts are in the right order // We assume the funds vector has 2 coins associated - let (amount0, amount1) = match pool + let (amount0, amount1) = match position .asset0 .map(|c| c.denom == funds[0].denom) - .or(pool.asset1.map(|c| c.denom == funds[1].denom)) + .or(position.asset1.map(|c| c.denom == funds[1].denom)) { Some(true) => (funds[0].amount, funds[1].amount), // we already had the right order Some(false) => (funds[1].amount, funds[0].amount), // we had the wrong order None => return Err(AppError::NoPosition {}), // A position has to exist in order to execute this function. This should be unreachable }; + deps.api.debug("After amounts"); let deposit_msg = app.executor(deps).execute_with_reply_and_data( MsgAddToPosition { - position_id: position.position_id, + position_id, sender: proxy_addr.to_string(), amount0: amount0.to_string(), amount1: amount1.to_string(), @@ -236,6 +239,7 @@ impl ConcentratedPoolParams { cosmwasm_std::ReplyOn::Success, OSMOSIS_ADD_TO_POSITION_REPLY_ID, )?; + deps.api.debug("After messages"); Ok(vec![deposit_msg]) } From 24990257f869d4d3614780c1ed75dc34fa74e964 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Thu, 11 Apr 2024 22:22:50 +0000 Subject: [PATCH 32/42] Simplify further yield source impls --- .../carrot-app/src/distribution/query.rs | 4 +- .../carrot-app/src/distribution/rewards.rs | 2 +- .../carrot-app/src/distribution/withdraw.rs | 2 +- contracts/carrot-app/src/handlers/execute.rs | 3 - contracts/carrot-app/src/handlers/internal.rs | 6 +- contracts/carrot-app/src/handlers/query.rs | 1 + .../carrot-app/src/yield_sources/mars.rs | 15 ++-- .../src/yield_sources/osmosis_cl_pool.rs | 11 ++- .../src/yield_sources/yield_type.rs | 68 ++++++++----------- 9 files changed, 55 insertions(+), 57 deletions(-) diff --git a/contracts/carrot-app/src/distribution/query.rs b/contracts/carrot-app/src/distribution/query.rs index ca80f78f..b374f38b 100644 --- a/contracts/carrot-app/src/distribution/query.rs +++ b/contracts/carrot-app/src/distribution/query.rs @@ -5,7 +5,9 @@ use crate::{ error::AppError, exchange_rate::query_exchange_rate, msg::AssetsBalanceResponse, - yield_sources::{AssetShare, Strategy, StrategyElement, YieldSource}, + yield_sources::{ + yield_type::YieldTypeImplementation, AssetShare, Strategy, StrategyElement, YieldSource, + }, }; impl Strategy { diff --git a/contracts/carrot-app/src/distribution/rewards.rs b/contracts/carrot-app/src/distribution/rewards.rs index 35fb8236..401ab29a 100644 --- a/contracts/carrot-app/src/distribution/rewards.rs +++ b/contracts/carrot-app/src/distribution/rewards.rs @@ -4,7 +4,7 @@ use cosmwasm_std::{Coin, Deps}; use crate::{ contract::{App, AppResult}, error::AppError, - yield_sources::Strategy, + yield_sources::{yield_type::YieldTypeImplementation, Strategy}, }; impl Strategy { diff --git a/contracts/carrot-app/src/distribution/withdraw.rs b/contracts/carrot-app/src/distribution/withdraw.rs index 97959df2..c8a9b751 100644 --- a/contracts/carrot-app/src/distribution/withdraw.rs +++ b/contracts/carrot-app/src/distribution/withdraw.rs @@ -4,7 +4,7 @@ use cosmwasm_std::{Coin, Decimal, Deps}; use crate::{ contract::{App, AppResult}, error::AppError, - yield_sources::{Strategy, StrategyElement}, + yield_sources::{yield_type::YieldTypeImplementation, Strategy, StrategyElement}, }; impl Strategy { diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index 74be03c9..395dabad 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -16,9 +16,6 @@ use cosmwasm_std::{ to_json_binary, Coin, Coins, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Uint128, WasmMsg, }; - -use abstract_app::traits::AccountIdentification; - use super::internal::execute_internal_action; pub fn execute_handler( diff --git a/contracts/carrot-app/src/handlers/internal.rs b/contracts/carrot-app/src/handlers/internal.rs index 90c33948..68f9477c 100644 --- a/contracts/carrot-app/src/handlers/internal.rs +++ b/contracts/carrot-app/src/handlers/internal.rs @@ -8,7 +8,10 @@ use crate::{ CONFIG, STRATEGY_CONFIG, TEMP_CURRENT_COIN, TEMP_CURRENT_YIELD, TEMP_DEPOSIT_COINS, TEMP_EXPECTED_SWAP_COIN, }, - yield_sources::{yield_type::YieldType, Strategy}, + yield_sources::{ + yield_type::{YieldType, YieldTypeImplementation}, + Strategy, + }, }; use abstract_app::{abstract_sdk::features::AbstractResponse, objects::AnsAsset}; use abstract_dex_adapter::DexInterface; @@ -17,7 +20,6 @@ use cosmwasm_std::{wasm_execute, Coin, Coins, DepsMut, Env, SubMsg, Uint128}; use cw_asset::AssetInfo; use crate::exchange_rate::query_exchange_rate; -use abstract_app::traits::AccountIdentification; pub fn execute_internal_action( deps: DepsMut, diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index edaef535..65eee5a7 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -11,6 +11,7 @@ use crate::autocompound::get_autocompound_status; use crate::exchange_rate::query_exchange_rate; use crate::msg::{PositionResponse, PositionsResponse}; use crate::state::STRATEGY_CONFIG; +use crate::yield_sources::yield_type::YieldTypeImplementation; use crate::{ contract::{App, AppResult}, error::AppError, diff --git a/contracts/carrot-app/src/yield_sources/mars.rs b/contracts/carrot-app/src/yield_sources/mars.rs index ca8a8d26..48ccd22c 100644 --- a/contracts/carrot-app/src/yield_sources/mars.rs +++ b/contracts/carrot-app/src/yield_sources/mars.rs @@ -20,9 +20,9 @@ pub struct MarsDepositParams { } impl YieldTypeImplementation for MarsDepositParams { - fn deposit(self, deps: Deps, funds: Vec, app: &App) -> AppResult> { + fn deposit(&self, deps: Deps, funds: Vec, app: &App) -> AppResult> { let ans = app.name_service(deps); - let ans_fund = ans.query(&AssetInfo::native(self.denom))?; + let ans_fund = ans.query(&AssetInfo::native(self.denom.clone()))?; Ok(vec![SubMsg::new( app.ans_money_market(deps, MARS_MONEY_MARKET.to_string()) @@ -30,7 +30,12 @@ impl YieldTypeImplementation for MarsDepositParams { )]) } - fn withdraw(self, deps: Deps, amount: Option, app: &App) -> AppResult> { + fn withdraw( + &self, + deps: Deps, + amount: Option, + app: &App, + ) -> AppResult> { let ans = app.name_service(deps); let amount = if let Some(amount) = amount { @@ -39,14 +44,14 @@ impl YieldTypeImplementation for MarsDepositParams { self.user_deposit(deps, app)?[0].amount }; - let ans_fund = ans.query(&AssetInfo::native(self.denom))?; + let ans_fund = ans.query(&AssetInfo::native(self.denom.clone()))?; Ok(vec![app .ans_money_market(deps, MARS_MONEY_MARKET.to_string()) .withdraw(AnsAsset::new(ans_fund, amount))?]) } - fn withdraw_rewards(self, _deps: Deps, _app: &App) -> AppResult<(Vec, Vec)> { + fn withdraw_rewards(&self, _deps: Deps, _app: &App) -> AppResult<(Vec, Vec)> { // Mars doesn't have rewards, it's automatically auto-compounded Ok((vec![], vec![])) } diff --git a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs index 3c6233fb..446a85bc 100644 --- a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs +++ b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs @@ -42,7 +42,7 @@ pub type ConcentratedPoolParamsUnchecked = ConcentratedPoolParamsBase pub type ConcentratedPoolParams = ConcentratedPoolParamsBase; impl YieldTypeImplementation for ConcentratedPoolParams { - fn deposit(self, deps: Deps, funds: Vec, app: &App) -> AppResult> { + fn deposit(&self, deps: Deps, funds: Vec, app: &App) -> AppResult> { // We verify there is a position stored if let Ok(position) = self.position(deps) { self.raw_deposit(deps, funds, app, position) @@ -52,7 +52,12 @@ impl YieldTypeImplementation for ConcentratedPoolParams { } } - fn withdraw(self, deps: Deps, amount: Option, app: &App) -> AppResult> { + fn withdraw( + &self, + deps: Deps, + amount: Option, + app: &App, + ) -> AppResult> { let position = self.position(deps)?; let position_details = position.position.unwrap(); @@ -75,7 +80,7 @@ impl YieldTypeImplementation for ConcentratedPoolParams { .into()]) } - fn withdraw_rewards(self, deps: Deps, app: &App) -> AppResult<(Vec, Vec)> { + fn withdraw_rewards(&self, deps: Deps, app: &App) -> AppResult<(Vec, Vec)> { let position = self.position(deps)?; let position_details = position.position.unwrap(); diff --git a/contracts/carrot-app/src/yield_sources/yield_type.rs b/contracts/carrot-app/src/yield_sources/yield_type.rs index ae83af91..88176ac1 100644 --- a/contracts/carrot-app/src/yield_sources/yield_type.rs +++ b/contracts/carrot-app/src/yield_sources/yield_type.rs @@ -20,78 +20,64 @@ pub enum YieldTypeBase { pub type YieldTypeUnchecked = YieldTypeBase; pub type YieldType = YieldTypeBase; -impl YieldType { - pub fn deposit(self, deps: Deps, funds: Vec, app: &App) -> AppResult> { +impl YieldTypeImplementation for YieldType { + fn deposit(&self, deps: Deps, funds: Vec, app: &App) -> AppResult> { if funds.is_empty() { return Ok(vec![]); } - match self { - YieldType::ConcentratedLiquidityPool(params) => params.deposit(deps, funds, app), - YieldType::Mars(params) => params.deposit(deps, funds, app), - } + self.inner().deposit(deps, funds, app) } - pub fn withdraw( - self, + fn withdraw( + &self, deps: Deps, amount: Option, app: &App, ) -> AppResult> { - match self { - YieldType::ConcentratedLiquidityPool(params) => params.withdraw(deps, amount, app), - YieldType::Mars(params) => params.withdraw(deps, amount, app), - } + self.inner().withdraw(deps, amount, app) } - pub fn withdraw_rewards(self, deps: Deps, app: &App) -> AppResult<(Vec, Vec)> { - match self { - YieldType::ConcentratedLiquidityPool(params) => params.withdraw_rewards(deps, app), - YieldType::Mars(params) => params.withdraw_rewards(deps, app), - } + fn withdraw_rewards(&self, deps: Deps, app: &App) -> AppResult<(Vec, Vec)> { + self.inner().withdraw_rewards(deps, app) } - pub fn user_deposit(&self, deps: Deps, app: &App) -> AppResult> { - let user_deposit_result = match self { - YieldType::ConcentratedLiquidityPool(params) => params.user_deposit(deps, app), - YieldType::Mars(params) => params.user_deposit(deps, app), - }; - Ok(user_deposit_result.unwrap_or_default()) + fn user_deposit(&self, deps: Deps, app: &App) -> AppResult> { + Ok(self.inner().user_deposit(deps, app).unwrap_or_default()) } - pub fn user_rewards(&self, deps: Deps, app: &App) -> AppResult> { - let user_deposit_result = match self { - YieldType::ConcentratedLiquidityPool(params) => params.user_rewards(deps, app), - YieldType::Mars(params) => params.user_rewards(deps, app), - }; - Ok(user_deposit_result.unwrap_or_default()) + fn user_rewards(&self, deps: Deps, app: &App) -> AppResult> { + Ok(self.inner().user_rewards(deps, app).unwrap_or_default()) } - pub fn user_liquidity(&self, deps: Deps, app: &App) -> AppResult { - let user_deposit_result = match self { - YieldType::ConcentratedLiquidityPool(params) => params.user_liquidity(deps, app), - YieldType::Mars(params) => params.user_liquidity(deps, app), - }; - Ok(user_deposit_result.unwrap_or_default()) + fn user_liquidity(&self, deps: Deps, app: &App) -> AppResult { + Ok(self.inner().user_liquidity(deps, app).unwrap_or_default()) } /// Indicate the default funds allocation /// This is specifically useful for auto-compound as we're not able to input target amounts /// CL pools use that to know the best funds deposit ratio /// Mars doesn't use that, because the share is fixed to 1 - pub fn share_type(&self) -> ShareType { + fn share_type(&self) -> ShareType { + self.inner().share_type() + } +} + +impl YieldType { + fn inner(&self) -> &dyn YieldTypeImplementation { match self { - YieldType::ConcentratedLiquidityPool(params) => params.share_type(), - YieldType::Mars(params) => params.share_type(), + YieldType::ConcentratedLiquidityPool(params) => params, + YieldType::Mars(params) => params, } } } pub trait YieldTypeImplementation { - fn deposit(self, deps: Deps, funds: Vec, app: &App) -> AppResult>; + fn deposit(&self, deps: Deps, funds: Vec, app: &App) -> AppResult>; - fn withdraw(self, deps: Deps, amount: Option, app: &App) -> AppResult>; + fn withdraw(&self, deps: Deps, amount: Option, app: &App) + -> AppResult>; - fn withdraw_rewards(self, deps: Deps, app: &App) -> AppResult<(Vec, Vec)>; + fn withdraw_rewards(&self, deps: Deps, app: &App) -> AppResult<(Vec, Vec)>; fn user_deposit(&self, deps: Deps, app: &App) -> AppResult>; From 2f716d20bae1aa0975237e1944a88b6b2a541da2 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Thu, 11 Apr 2024 22:34:28 +0000 Subject: [PATCH 33/42] Rename yield type --- .../carrot-app/examples/localnet_install.rs | 4 +- .../carrot-app/examples/localnet_test.rs | 2 +- contracts/carrot-app/src/check.rs | 22 ++++---- .../carrot-app/src/distribution/deposit.rs | 2 +- .../carrot-app/src/distribution/query.rs | 8 +-- .../carrot-app/src/distribution/rewards.rs | 2 +- .../carrot-app/src/distribution/withdraw.rs | 6 +-- contracts/carrot-app/src/handlers/query.rs | 10 ++-- contracts/carrot-app/src/msg.rs | 2 +- .../src/replies/osmosis/add_to_position.rs | 2 +- .../src/replies/osmosis/create_position.rs | 2 +- contracts/carrot-app/src/yield_sources/mod.rs | 4 +- .../src/yield_sources/yield_type.rs | 6 +-- contracts/carrot-app/tests/common.rs | 4 +- contracts/carrot-app/tests/config.rs | 50 ++++++++++--------- .../carrot-app/tests/deposit_withdraw.rs | 13 ++--- 16 files changed, 72 insertions(+), 67 deletions(-) diff --git a/contracts/carrot-app/examples/localnet_install.rs b/contracts/carrot-app/examples/localnet_install.rs index 87d7a766..e7e87565 100644 --- a/contracts/carrot-app/examples/localnet_install.rs +++ b/contracts/carrot-app/examples/localnet_install.rs @@ -24,7 +24,7 @@ use carrot_app::{ msg::{AppInstantiateMsg, AppMigrateMsg, MigrateMsg}, state::ConfigBase, yield_sources::{ - osmosis_cl_pool::ConcentratedPoolParamsBase, yield_type::YieldTypeBase, AssetShare, + osmosis_cl_pool::ConcentratedPoolParamsBase, yield_type::YieldParamsBase, AssetShare, StrategyBase, StrategyElementBase, YieldSourceBase, }, AppInterface, @@ -92,7 +92,7 @@ fn main() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + params: YieldParamsBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id: POOL_ID, lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, diff --git a/contracts/carrot-app/examples/localnet_test.rs b/contracts/carrot-app/examples/localnet_test.rs index 8d44f046..bebfebff 100644 --- a/contracts/carrot-app/examples/localnet_test.rs +++ b/contracts/carrot-app/examples/localnet_test.rs @@ -24,7 +24,7 @@ use carrot_app::{ msg::AppInstantiateMsg, state::ConfigBase, yield_sources::{ - osmosis_cl_pool::ConcentratedPoolParamsBase, yield_type::YieldTypeBase, AssetShare, + osmosis_cl_pool::ConcentratedPoolParamsBase, yield_type::YieldParamsBase, AssetShare, StrategyBase, StrategyElementBase, YieldSourceBase, }, AppExecuteMsgFns, AppInterface, diff --git a/contracts/carrot-app/src/check.rs b/contracts/carrot-app/src/check.rs index 3b427117..6761ae93 100644 --- a/contracts/carrot-app/src/check.rs +++ b/contracts/carrot-app/src/check.rs @@ -127,7 +127,7 @@ mod yield_sources { osmosis_cl_pool::{ ConcentratedPoolParams, ConcentratedPoolParamsBase, ConcentratedPoolParamsUnchecked, }, - yield_type::{YieldType, YieldTypeBase, YieldTypeUnchecked}, + yield_type::{YieldParamsBase, YieldType, YieldTypeUnchecked}, Strategy, StrategyElement, StrategyElementUnchecked, StrategyUnchecked, YieldSource, YieldSourceUnchecked, }, @@ -172,8 +172,8 @@ mod yield_sources { impl From for YieldTypeUnchecked { fn from(value: YieldType) -> Self { match value { - YieldTypeBase::ConcentratedLiquidityPool(params) => { - YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + YieldParamsBase::ConcentratedLiquidityPool(params) => { + YieldParamsBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id: params.pool_id, lower_tick: params.lower_tick, upper_tick: params.upper_tick, @@ -181,7 +181,7 @@ mod yield_sources { _phantom: std::marker::PhantomData, }) } - YieldTypeBase::Mars(params) => YieldTypeBase::Mars(params), + YieldParamsBase::Mars(params) => YieldParamsBase::Mars(params), } } } @@ -194,7 +194,7 @@ mod yield_sources { fn from(value: YieldSource) -> Self { Self { asset_distribution: value.asset_distribution, - ty: value.ty.into(), + params: value.params.into(), } } } @@ -226,17 +226,17 @@ mod yield_sources { ) .map_err(|_| AppError::AssetsNotRegistered(all_denoms))?; - let ty = match self.ty { - YieldTypeBase::ConcentratedLiquidityPool(params) => { + let params = match self.params { + YieldParamsBase::ConcentratedLiquidityPool(params) => { // A valid CL pool strategy is for 2 assets ensure_eq!( self.asset_distribution.len(), 2, AppError::InvalidStrategy {} ); - YieldTypeBase::ConcentratedLiquidityPool(params.check(deps, app)?) + YieldParamsBase::ConcentratedLiquidityPool(params.check(deps, app)?) } - YieldTypeBase::Mars(params) => { + YieldParamsBase::Mars(params) => { // We verify there is only one element in the shares vector ensure_eq!( self.asset_distribution.len(), @@ -249,13 +249,13 @@ mod yield_sources { params.denom, AppError::InvalidStrategy {} ); - YieldTypeBase::Mars(params.check(deps, app)?) + YieldParamsBase::Mars(params.check(deps, app)?) } }; Ok(YieldSource { asset_distribution: self.asset_distribution, - ty, + params, }) } } diff --git a/contracts/carrot-app/src/distribution/deposit.rs b/contracts/carrot-app/src/distribution/deposit.rs index 82ba46a7..f9519a72 100644 --- a/contracts/carrot-app/src/distribution/deposit.rs +++ b/contracts/carrot-app/src/distribution/deposit.rs @@ -249,7 +249,7 @@ impl Strategy { let deposit_strategies = self.fill_all(deps, funds, app)?; Ok(deposit_strategies .iter() - .zip(self.0.iter().map(|s| s.yield_source.ty.clone())) + .zip(self.0.iter().map(|s| s.yield_source.params.clone())) .enumerate() .map(|(index, (strategy, yield_type))| strategy.deposit_msgs(index, yield_type)) .collect()) diff --git a/contracts/carrot-app/src/distribution/query.rs b/contracts/carrot-app/src/distribution/query.rs index b374f38b..03757cf7 100644 --- a/contracts/carrot-app/src/distribution/query.rs +++ b/contracts/carrot-app/src/distribution/query.rs @@ -18,7 +18,7 @@ impl Strategy { self.0.iter().try_for_each(|s| { let deposit_value = s .yield_source - .ty + .params .user_deposit(deps, app) .unwrap_or_default(); for fund in deposit_value { @@ -59,7 +59,7 @@ impl Strategy { .map(|(original_strategy, (value, shares))| StrategyElement { yield_source: YieldSource { asset_distribution: shares, - ty: original_strategy.yield_source.ty.clone(), + params: original_strategy.yield_source.params.clone(), }, share: Decimal::from_ratio(value, all_strategies_value), }) @@ -70,7 +70,7 @@ impl Strategy { /// This function applies the underlying shares inside yield sources to each yield source depending on the current strategy state pub fn apply_current_strategy_shares(&mut self, deps: Deps, app: &App) -> AppResult<()> { self.0.iter_mut().try_for_each(|yield_source| { - match yield_source.yield_source.ty.share_type() { + match yield_source.yield_source.params.share_type() { crate::yield_sources::ShareType::Dynamic => { let (_total_value, shares) = yield_source.query_current_value(deps, app)?; yield_source.yield_source.asset_distribution = shares; @@ -94,7 +94,7 @@ impl StrategyElement { app: &App, ) -> AppResult<(Uint128, Vec)> { // If there is no deposit - let user_deposit = match self.yield_source.ty.user_deposit(deps, app) { + let user_deposit = match self.yield_source.params.user_deposit(deps, app) { Ok(deposit) => deposit, Err(_) => { return Ok(( diff --git a/contracts/carrot-app/src/distribution/rewards.rs b/contracts/carrot-app/src/distribution/rewards.rs index 401ab29a..b416c582 100644 --- a/contracts/carrot-app/src/distribution/rewards.rs +++ b/contracts/carrot-app/src/distribution/rewards.rs @@ -17,7 +17,7 @@ impl Strategy { .0 .into_iter() .map(|s| { - let (rewards, raw_msgs) = s.yield_source.ty.withdraw_rewards(deps, app)?; + let (rewards, raw_msgs) = s.yield_source.params.withdraw_rewards(deps, app)?; Ok::<_, AppError>(( rewards, diff --git a/contracts/carrot-app/src/distribution/withdraw.rs b/contracts/carrot-app/src/distribution/withdraw.rs index c8a9b751..c69029d7 100644 --- a/contracts/carrot-app/src/distribution/withdraw.rs +++ b/contracts/carrot-app/src/distribution/withdraw.rs @@ -30,7 +30,7 @@ impl StrategyElement { ) -> AppResult { let this_withdraw_amount = withdraw_share .map(|share| { - let this_amount = self.yield_source.ty.user_liquidity(deps, app)?; + let this_amount = self.yield_source.params.user_liquidity(deps, app)?; let this_withdraw_amount = share * this_amount; Ok::<_, AppError>(this_withdraw_amount) @@ -38,7 +38,7 @@ impl StrategyElement { .transpose()?; let raw_msg = self .yield_source - .ty + .params .withdraw(deps, this_withdraw_amount, app)?; Ok::<_, AppError>( @@ -53,7 +53,7 @@ impl StrategyElement { withdraw_share: Option, app: &App, ) -> AppResult> { - let current_deposit = self.yield_source.ty.user_deposit(deps, app)?; + let current_deposit = self.yield_source.params.user_deposit(deps, app)?; if let Some(share) = withdraw_share { Ok(current_deposit diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index 65eee5a7..d5e36a79 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -126,7 +126,7 @@ pub fn query_balance(deps: Deps, app: &App) -> AppResult strategy.0.iter().try_for_each(|s| { let deposit_value = s .yield_source - .ty + .params .user_deposit(deps, app) .unwrap_or_default(); for fund in deposit_value { @@ -148,7 +148,7 @@ fn query_rewards(deps: Deps, app: &App) -> AppResult { let mut rewards = Coins::default(); strategy.0.into_iter().try_for_each(|s| { - let this_rewards = s.yield_source.ty.user_rewards(deps, app)?; + let this_rewards = s.yield_source.params.user_rewards(deps, app)?; for fund in this_rewards { rewards.add(fund)?; } @@ -167,8 +167,8 @@ pub fn query_positions(deps: Deps, app: &App) -> AppResult { .0 .into_iter() .map(|s| { - let balance = s.yield_source.ty.user_deposit(deps, app)?; - let liquidity = s.yield_source.ty.user_liquidity(deps, app)?; + let balance = s.yield_source.params.user_deposit(deps, app)?; + let liquidity = s.yield_source.params.user_liquidity(deps, app)?; let total_value = balance .iter() @@ -179,7 +179,7 @@ pub fn query_positions(deps: Deps, app: &App) -> AppResult { .sum::>()?; Ok::<_, AppError>(PositionResponse { - ty: s.yield_source.ty.into(), + params: s.yield_source.params.into(), balance: AssetsBalanceResponse { balances: balance, total_value, diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index 41a728d3..58f99aef 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -182,7 +182,7 @@ pub struct PositionsResponse { #[cw_serde] pub struct PositionResponse { - pub ty: YieldTypeUnchecked, + pub params: YieldTypeUnchecked, pub balance: AssetsBalanceResponse, pub liquidity: Uint128, } diff --git a/contracts/carrot-app/src/replies/osmosis/add_to_position.rs b/contracts/carrot-app/src/replies/osmosis/add_to_position.rs index 8b49658e..9251472b 100644 --- a/contracts/carrot-app/src/replies/osmosis/add_to_position.rs +++ b/contracts/carrot-app/src/replies/osmosis/add_to_position.rs @@ -29,7 +29,7 @@ pub fn add_to_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) - let current_yield = strategy.0.get_mut(current_position_index).unwrap(); - current_yield.yield_source.ty = match current_yield.yield_source.ty.clone() { + current_yield.yield_source.params = match current_yield.yield_source.params.clone() { YieldType::ConcentratedLiquidityPool(mut position) => { position.position_id = Some(response.position_id); YieldType::ConcentratedLiquidityPool(position) diff --git a/contracts/carrot-app/src/replies/osmosis/create_position.rs b/contracts/carrot-app/src/replies/osmosis/create_position.rs index 9cfe5ebf..76be4352 100644 --- a/contracts/carrot-app/src/replies/osmosis/create_position.rs +++ b/contracts/carrot-app/src/replies/osmosis/create_position.rs @@ -28,7 +28,7 @@ pub fn create_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) - let current_yield = strategy.0.get_mut(current_position_index).unwrap(); - current_yield.yield_source.ty = match current_yield.yield_source.ty.clone() { + current_yield.yield_source.params = match current_yield.yield_source.params.clone() { YieldType::ConcentratedLiquidityPool(mut position) => { position.position_id = Some(response.position_id); YieldType::ConcentratedLiquidityPool(position.clone()) diff --git a/contracts/carrot-app/src/yield_sources/mod.rs b/contracts/carrot-app/src/yield_sources/mod.rs index 00b4c7ca..33d9bea7 100644 --- a/contracts/carrot-app/src/yield_sources/mod.rs +++ b/contracts/carrot-app/src/yield_sources/mod.rs @@ -7,7 +7,7 @@ use cosmwasm_std::Decimal; use crate::{ check::{Checked, Unchecked}, - yield_sources::yield_type::YieldTypeBase, + yield_sources::yield_type::YieldParamsBase, }; /// A yield sources has the following elements @@ -16,7 +16,7 @@ use crate::{ #[cw_serde] pub struct YieldSourceBase { pub asset_distribution: Vec, - pub ty: YieldTypeBase, + pub params: YieldParamsBase, } pub type YieldSourceUnchecked = YieldSourceBase; diff --git a/contracts/carrot-app/src/yield_sources/yield_type.rs b/contracts/carrot-app/src/yield_sources/yield_type.rs index 88176ac1..eca0bcec 100644 --- a/contracts/carrot-app/src/yield_sources/yield_type.rs +++ b/contracts/carrot-app/src/yield_sources/yield_type.rs @@ -10,15 +10,15 @@ use super::{mars::MarsDepositParams, osmosis_cl_pool::ConcentratedPoolParamsBase // This however is not checkable by itself, because the check also depends on the asset share distribution #[cw_serde] -pub enum YieldTypeBase { +pub enum YieldParamsBase { ConcentratedLiquidityPool(ConcentratedPoolParamsBase), /// For Mars, you just need to deposit in the RedBank /// You need to indicate the denom of the funds you want to deposit Mars(MarsDepositParams), } -pub type YieldTypeUnchecked = YieldTypeBase; -pub type YieldType = YieldTypeBase; +pub type YieldTypeUnchecked = YieldParamsBase; +pub type YieldType = YieldParamsBase; impl YieldTypeImplementation for YieldType { fn deposit(&self, deps: Deps, funds: Vec, app: &App) -> AppResult> { diff --git a/contracts/carrot-app/tests/common.rs b/contracts/carrot-app/tests/common.rs index 62e4045f..503cc9dd 100644 --- a/contracts/carrot-app/tests/common.rs +++ b/contracts/carrot-app/tests/common.rs @@ -7,7 +7,7 @@ use carrot_app::contract::OSMOSIS; use carrot_app::msg::AppInstantiateMsg; use carrot_app::state::ConfigBase; use carrot_app::yield_sources::osmosis_cl_pool::ConcentratedPoolParamsBase; -use carrot_app::yield_sources::yield_type::YieldTypeBase; +use carrot_app::yield_sources::yield_type::YieldParamsBase; use carrot_app::yield_sources::{AssetShare, StrategyBase, StrategyElementBase, YieldSourceBase}; use cosmwasm_std::{coin, coins, Coins, Decimal, Uint128, Uint64}; use cw_asset::AssetInfoUnchecked; @@ -144,7 +144,7 @@ pub fn deploy( share: Decimal::percent(50), }, ], - ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + params: YieldParamsBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, diff --git a/contracts/carrot-app/tests/config.rs b/contracts/carrot-app/tests/config.rs index afdb38ad..0a2ce4b1 100644 --- a/contracts/carrot-app/tests/config.rs +++ b/contracts/carrot-app/tests/config.rs @@ -4,7 +4,7 @@ use crate::common::{create_pool, setup_test_tube, USDC, USDT}; use carrot_app::{ msg::{AppExecuteMsgFns, AppQueryMsgFns}, yield_sources::{ - osmosis_cl_pool::ConcentratedPoolParamsBase, yield_type::YieldTypeBase, AssetShare, + osmosis_cl_pool::ConcentratedPoolParamsBase, yield_type::YieldParamsBase, AssetShare, StrategyBase, StrategyElementBase, YieldSourceBase, }, }; @@ -34,13 +34,15 @@ fn rebalance_fails() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { - pool_id: 7, - lower_tick: INITIAL_LOWER_TICK, - upper_tick: INITIAL_UPPER_TICK, - position_id: None, - _phantom: std::marker::PhantomData, - }), + params: YieldParamsBase::ConcentratedLiquidityPool( + ConcentratedPoolParamsBase { + pool_id: 7, + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + _phantom: std::marker::PhantomData, + }, + ), }, share: Decimal::one(), }, @@ -56,13 +58,15 @@ fn rebalance_fails() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { - pool_id: 7, - lower_tick: INITIAL_LOWER_TICK, - upper_tick: INITIAL_UPPER_TICK, - position_id: None, - _phantom: std::marker::PhantomData, - }), + params: YieldParamsBase::ConcentratedLiquidityPool( + ConcentratedPoolParamsBase { + pool_id: 7, + lower_tick: INITIAL_LOWER_TICK, + upper_tick: INITIAL_UPPER_TICK, + position_id: None, + _phantom: std::marker::PhantomData, + }, + ), }, share: Decimal::one(), }, @@ -93,7 +97,7 @@ fn rebalance_success() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + params: YieldParamsBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, // Pool Id needs to exist lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, @@ -115,7 +119,7 @@ fn rebalance_success() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + params: YieldParamsBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, // Pool Id needs to exist lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, @@ -170,7 +174,7 @@ fn rebalance_with_new_pool_success() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + params: YieldParamsBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, // Pool Id needs to exist lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, @@ -192,7 +196,7 @@ fn rebalance_with_new_pool_success() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + params: YieldParamsBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id: new_pool_id, lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, @@ -248,7 +252,7 @@ fn rebalance_with_stale_strategy_success() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + params: YieldParamsBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, // Pool Id needs to exist lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, @@ -274,7 +278,7 @@ fn rebalance_with_stale_strategy_success() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + params: YieldParamsBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id: new_pool_id, lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, @@ -342,7 +346,7 @@ fn rebalance_with_current_and_stale_strategy_success() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + params: YieldParamsBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id: new_pool_id, lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, @@ -364,7 +368,7 @@ fn rebalance_with_current_and_stale_strategy_success() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + params: YieldParamsBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, // Pool Id needs to exist lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, diff --git a/contracts/carrot-app/tests/deposit_withdraw.rs b/contracts/carrot-app/tests/deposit_withdraw.rs index 17e1b5ac..fbb3ed44 100644 --- a/contracts/carrot-app/tests/deposit_withdraw.rs +++ b/contracts/carrot-app/tests/deposit_withdraw.rs @@ -6,7 +6,8 @@ use carrot_app::{ msg::{AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse}, yield_sources::{ mars::MarsDepositParams, osmosis_cl_pool::ConcentratedPoolParamsBase, - yield_type::YieldTypeBase, AssetShare, StrategyBase, StrategyElementBase, YieldSourceBase, + yield_type::YieldParamsBase, AssetShare, StrategyBase, StrategyElementBase, + YieldSourceBase, }, AppInterface, }; @@ -159,7 +160,7 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + params: YieldParamsBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, @@ -181,7 +182,7 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + params: YieldParamsBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, lower_tick: 2 * INITIAL_LOWER_TICK, upper_tick: 2 * INITIAL_UPPER_TICK, @@ -231,7 +232,7 @@ fn deposit_multiple_positions_with_empty() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + params: YieldParamsBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, lower_tick: INITIAL_LOWER_TICK, upper_tick: INITIAL_UPPER_TICK, @@ -253,7 +254,7 @@ fn deposit_multiple_positions_with_empty() -> anyhow::Result<()> { share: Decimal::percent(50), }, ], - ty: YieldTypeBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + params: YieldParamsBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { pool_id, lower_tick: 2 * INITIAL_LOWER_TICK, upper_tick: 2 * INITIAL_UPPER_TICK, @@ -269,7 +270,7 @@ fn deposit_multiple_positions_with_empty() -> anyhow::Result<()> { denom: USDT.to_string(), share: Decimal::percent(100), }], - ty: YieldTypeBase::Mars(MarsDepositParams { + params: YieldParamsBase::Mars(MarsDepositParams { denom: USDT.to_string(), }), }, From b21d38e48a568c86ceb4db2046b86608aea7552c Mon Sep 17 00:00:00 2001 From: Kayanski Date: Mon, 15 Apr 2024 08:22:00 +0000 Subject: [PATCH 34/42] Extend gas tests --- contracts/carrot-app/examples/gas-usage.md | 18 +++- .../carrot-app/examples/localnet_install.rs | 93 ++++++++++++++----- .../carrot-app/examples/localnet_test.rs | 6 +- 3 files changed, 92 insertions(+), 25 deletions(-) diff --git a/contracts/carrot-app/examples/gas-usage.md b/contracts/carrot-app/examples/gas-usage.md index 2be208f2..255e6949 100644 --- a/contracts/carrot-app/examples/gas-usage.md +++ b/contracts/carrot-app/examples/gas-usage.md @@ -9,4 +9,20 @@ V2 : 3172677 3. Withdraw all V1 : 1100482 -V2 : 1189022 \ No newline at end of file +V2 : 1189022 + + + +2 : +Create : 2342438 +Add to position : 5705186 + +3 : +Create : 3497234 +Add : 8516095 + +4 : +Create : 4673461 +Add: 11344713 + +5: \ No newline at end of file diff --git a/contracts/carrot-app/examples/localnet_install.rs b/contracts/carrot-app/examples/localnet_install.rs index e7e87565..94179127 100644 --- a/contracts/carrot-app/examples/localnet_install.rs +++ b/contracts/carrot-app/examples/localnet_install.rs @@ -25,7 +25,8 @@ use carrot_app::{ state::ConfigBase, yield_sources::{ osmosis_cl_pool::ConcentratedPoolParamsBase, yield_type::YieldParamsBase, AssetShare, - StrategyBase, StrategyElementBase, YieldSourceBase, + StrategyBase, StrategyElementBase, StrategyElementUnchecked, StrategyUnchecked, + YieldSourceBase, }, AppInterface, }; @@ -80,28 +81,7 @@ fn main() -> anyhow::Result<()> { }, dex: "osmosis".to_string(), }, - strategy: StrategyBase(vec![StrategyElementBase { - yield_source: YieldSourceBase { - asset_distribution: vec![ - AssetShare { - denom: ION.to_string(), - share: Decimal::percent(50), - }, - AssetShare { - denom: OSMO.to_string(), - share: Decimal::percent(50), - }, - ], - params: YieldParamsBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { - pool_id: POOL_ID, - lower_tick: INITIAL_LOWER_TICK, - upper_tick: INITIAL_UPPER_TICK, - position_id: None, - _phantom: std::marker::PhantomData, - }), - }, - share: Decimal::one(), - }]), + strategy: two_strategy(), deposit: None, }; @@ -129,3 +109,70 @@ fn main() -> anyhow::Result<()> { Ok(()) } + +fn one_element(upper_tick: i64, lower_tick: i64, share: Decimal) -> StrategyElementUnchecked { + StrategyElementBase { + yield_source: YieldSourceBase { + asset_distribution: vec![ + AssetShare { + denom: ION.to_string(), + share: Decimal::percent(50), + }, + AssetShare { + denom: OSMO.to_string(), + share: Decimal::percent(50), + }, + ], + params: YieldParamsBase::ConcentratedLiquidityPool(ConcentratedPoolParamsBase { + pool_id: POOL_ID, + lower_tick, + upper_tick, + position_id: None, + _phantom: std::marker::PhantomData, + }), + }, + share, + } +} + +pub fn single_strategy() -> StrategyUnchecked { + StrategyBase(vec![one_element( + INITIAL_UPPER_TICK, + INITIAL_LOWER_TICK, + Decimal::one(), + )]) +} + +pub fn two_strategy() -> StrategyUnchecked { + StrategyBase(vec![ + one_element(INITIAL_UPPER_TICK, INITIAL_LOWER_TICK, Decimal::percent(50)), + one_element(5000, -5000, Decimal::percent(50)), + ]) +} + +pub fn three_strategy() -> StrategyUnchecked { + StrategyBase(vec![ + one_element(INITIAL_UPPER_TICK, INITIAL_LOWER_TICK, Decimal::percent(33)), + one_element(5000, -5000, Decimal::percent(33)), + one_element(1000, -1000, Decimal::percent(34)), + ]) +} + +pub fn four_strategy() -> StrategyUnchecked { + StrategyBase(vec![ + one_element(INITIAL_UPPER_TICK, INITIAL_LOWER_TICK, Decimal::percent(25)), + one_element(5000, -5000, Decimal::percent(25)), + one_element(1000, -1000, Decimal::percent(25)), + one_element(100, -100, Decimal::percent(25)), + ]) +} + +pub fn five_strategy() -> StrategyUnchecked { + StrategyBase(vec![ + one_element(INITIAL_UPPER_TICK, INITIAL_LOWER_TICK, Decimal::percent(20)), + one_element(5000, -5000, Decimal::percent(20)), + one_element(1000, -1000, Decimal::percent(20)), + one_element(100, -100, Decimal::percent(20)), + one_element(600, -600, Decimal::percent(20)), + ]) +} diff --git a/contracts/carrot-app/examples/localnet_test.rs b/contracts/carrot-app/examples/localnet_test.rs index bebfebff..036415d9 100644 --- a/contracts/carrot-app/examples/localnet_test.rs +++ b/contracts/carrot-app/examples/localnet_test.rs @@ -29,6 +29,9 @@ use carrot_app::{ }, AppExecuteMsgFns, AppInterface, }; +use localnet_install::{five_strategy, four_strategy, three_strategy}; + +mod localnet_install; pub const ION: &str = "uion"; pub const OSMO: &str = "uosmo"; @@ -75,10 +78,11 @@ fn main() -> anyhow::Result<()> { .bank_send(account.proxy()?.as_str(), coins(10_000, "uosmo")), )?; - // carrot.deposit(coins(10_000, "uosmo"), None)?; carrot.deposit(coins(10_000, "uosmo"), None)?; + // carrot.update_strategy(vec![], five_strategy())?; // carrot.withdraw(None)?; + // carrot.deposit(coins(10_000, "uosmo"), None)?; Ok(()) } From 1c3a970b152bcad80132dbcfe33212ab6bec5287 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Mon, 15 Apr 2024 08:23:05 +0000 Subject: [PATCH 35/42] UPdated estimates --- contracts/carrot-app/examples/gas-usage.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/contracts/carrot-app/examples/gas-usage.md b/contracts/carrot-app/examples/gas-usage.md index 255e6949..21b928d3 100644 --- a/contracts/carrot-app/examples/gas-usage.md +++ b/contracts/carrot-app/examples/gas-usage.md @@ -12,17 +12,20 @@ V1 : 1100482 V2 : 1189022 +For multiple positions : -2 : +2. Create : 2342438 Add to position : 5705186 -3 : +3. Create : 3497234 Add : 8516095 -4 : +4. Create : 4673461 Add: 11344713 -5: \ No newline at end of file +5. +Create : 5869472 +Add: 14187958 \ No newline at end of file From b9b508c0c493661812e393879d19869702b5c4b9 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Mon, 15 Apr 2024 09:01:24 +0000 Subject: [PATCH 36/42] Added withdraw preview and removed useless deposit operation --- .../carrot-app/examples/localnet_test.rs | 2 +- .../carrot-app/src/distribution/deposit.rs | 9 ++-- .../carrot-app/src/distribution/query.rs | 2 +- .../carrot-app/src/distribution/withdraw.rs | 16 ++++++- contracts/carrot-app/src/handlers/execute.rs | 47 +------------------ contracts/carrot-app/src/handlers/preview.rs | 21 ++++++++- contracts/carrot-app/src/handlers/query.rs | 18 ++++++- contracts/carrot-app/src/msg.rs | 7 ++- 8 files changed, 66 insertions(+), 56 deletions(-) diff --git a/contracts/carrot-app/examples/localnet_test.rs b/contracts/carrot-app/examples/localnet_test.rs index 036415d9..ab07b5c1 100644 --- a/contracts/carrot-app/examples/localnet_test.rs +++ b/contracts/carrot-app/examples/localnet_test.rs @@ -80,7 +80,7 @@ fn main() -> anyhow::Result<()> { carrot.deposit(coins(10_000, "uosmo"), None)?; - // carrot.update_strategy(vec![], five_strategy())?; + // carrot.update_strategy(coins(10_000, "uosmo"), five_strategy())?; // carrot.withdraw(None)?; // carrot.deposit(coins(10_000, "uosmo"), None)?; diff --git a/contracts/carrot-app/src/distribution/deposit.rs b/contracts/carrot-app/src/distribution/deposit.rs index f9519a72..ba8500f2 100644 --- a/contracts/carrot-app/src/distribution/deposit.rs +++ b/contracts/carrot-app/src/distribution/deposit.rs @@ -14,6 +14,12 @@ use cosmwasm_schema::cw_serde; use crate::{error::AppError, msg::InternalExecuteMsg}; +/// This functions creates the current deposit strategy +// /// 1. We query the target strategy in storage (target strategy) +// /// 2. We query the current status of the strategy (current strategy) from all deposits (external queries) +// /// 3. We create a temporary strategy object that allocates the funds from this deposit into the various strategies +// /// 4. We correct the expected token shares of each strategy, in case there are corrections passed to the function +// /// 5. We deposit funds according to that strategy pub fn generate_deposit_strategy( deps: Deps, funds: Vec, @@ -34,9 +40,6 @@ pub fn generate_deposit_strategy( app, )?; - // We query the yield source shares - this_deposit_strategy.apply_current_strategy_shares(deps, app)?; - // We correct it if the user asked to correct the share parameters of each strategy this_deposit_strategy.correct_with(yield_source_params); diff --git a/contracts/carrot-app/src/distribution/query.rs b/contracts/carrot-app/src/distribution/query.rs index 03757cf7..67260fb6 100644 --- a/contracts/carrot-app/src/distribution/query.rs +++ b/contracts/carrot-app/src/distribution/query.rs @@ -85,7 +85,7 @@ impl Strategy { } impl StrategyElement { - /// Queries the current value distribution of a registered strategy + /// Queries the current value distribution of a registered strategy. /// If there is no deposit or the query for the user deposit value fails /// the function returns 0 value with the registered asset distribution pub fn query_current_value( diff --git a/contracts/carrot-app/src/distribution/withdraw.rs b/contracts/carrot-app/src/distribution/withdraw.rs index c69029d7..f68eb9c0 100644 --- a/contracts/carrot-app/src/distribution/withdraw.rs +++ b/contracts/carrot-app/src/distribution/withdraw.rs @@ -1,5 +1,5 @@ use abstract_sdk::{AccountAction, Execution, ExecutorMsg}; -use cosmwasm_std::{Coin, Decimal, Deps}; +use cosmwasm_std::{Coin, Coins, Decimal, Deps}; use crate::{ contract::{App, AppResult}, @@ -19,6 +19,20 @@ impl Strategy { .map(|s| s.withdraw(deps, withdraw_share, app)) .collect() } + pub fn withdraw_preview( + self, + deps: Deps, + withdraw_share: Option, + app: &App, + ) -> AppResult> { + let mut withdraw_result = Coins::default(); + self.0.into_iter().try_for_each(|s| { + let funds = s.withdraw_preview(deps, withdraw_share, app)?; + funds.into_iter().try_for_each(|f| withdraw_result.add(f))?; + Ok::<_, AppError>(()) + })?; + Ok(withdraw_result.into()) + } } impl StrategyElement { diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index 395dabad..a4d825aa 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -1,3 +1,4 @@ +use super::internal::execute_internal_action; use crate::{ autocompound::AutocompoundState, check::Checkable, @@ -16,7 +17,6 @@ use cosmwasm_std::{ to_json_binary, Coin, Coins, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Uint128, WasmMsg, }; -use super::internal::execute_internal_action; pub fn execute_handler( deps: DepsMut, @@ -184,51 +184,6 @@ fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResu Ok(response.add_messages(executor_reward_messages)) } -// /// UNUSED FOR NOW, replaces by _inner_advanced_deposit -// /// The deposit process goes through the following steps -// /// 1. We query the target strategy in storage -// /// 2. We correct the expected token shares of each strategy, in case there are corrections passed to the function -// /// 3. We deposit funds according to that strategy -// /// -// /// This approach is not perfect. TO show the flaws, take an example where you allocate 50% into mars, 50% into osmosis and both give similar rewards. -// /// Assume we deposited 2x inside the app. -// /// When an auto-compounding happens, they both get y as rewards, mars is already auto-compounding and osmosis' rewards are redeposited inside the pool -// /// Step | Mars | Osmosis | Rewards| -// /// Deposit | x | x | 0 | -// /// Withdraw Rewards | x + y | x| y | -// /// Re-deposit | x + y + y/2 | x + y/2 | 0 | -// /// The final ratio is not the 50/50 ratio we target -// /// -// /// PROPOSITION : We could also have this kind of deposit flow -// /// 1a. We query the target strategy in storage (target strategy) -// /// 1b. We query the current status of the strategy (current strategy) -// /// 1c. We create a temporary strategy object to allocate the funds from this deposit into the various strategies -// /// --> the goal of those 3 steps is to correct the funds allocation faster towards the target strategy -// /// 2. We correct the expected token shares of each strategy, in case there are corrections passed to the function -// /// 3. We deposit funds according to that strategy -// /// This time : -// /// Step | Mars | Osmosis | Rewards| -// /// Deposit | x | x | 0 | -// /// Withdraw Rewards | x + y | x| y | -// /// Re-deposit | x + y | x + y | 0 | -// pub fn _inner_deposit( -// deps: Deps, -// env: &Env, -// funds: Vec, -// yield_source_params: Option>>>, -// app: &App, -// ) -> AppResult> { -// // We query the target strategy depending on the existing deposits -// let mut current_strategy_status = CONFIG.load(deps.storage)?.strategy; -// current_strategy_status.apply_current_strategy_shares(deps, app)?; - -// // We correct it if the user asked to correct the share parameters of each strategy -// current_strategy_status.correct_with(yield_source_params); - -// // We fill the strategies with the current deposited funds and get messages to execute those deposits -// current_strategy_status.fill_all_and_get_messages(deps, env, funds, app) -// } - pub fn _inner_deposit( deps: Deps, env: &Env, diff --git a/contracts/carrot-app/src/handlers/preview.rs b/contracts/carrot-app/src/handlers/preview.rs index 18664a56..a5980ce1 100644 --- a/contracts/carrot-app/src/handlers/preview.rs +++ b/contracts/carrot-app/src/handlers/preview.rs @@ -1,12 +1,15 @@ -use cosmwasm_std::{Coin, Deps, Uint128}; +use cosmwasm_std::{Coin, Decimal, Deps, Uint128}; use crate::{ contract::{App, AppResult}, distribution::deposit::generate_deposit_strategy, msg::{DepositPreviewResponse, UpdateStrategyPreviewResponse, WithdrawPreviewResponse}, + state::STRATEGY_CONFIG, yield_sources::{AssetShare, StrategyUnchecked}, }; +use super::query::withdraw_share; + pub fn deposit_preview( deps: Deps, funds: Vec, @@ -30,8 +33,22 @@ pub fn withdraw_preview( amount: Option, app: &App, ) -> AppResult { - Ok(WithdrawPreviewResponse {}) + let withdraw_share = withdraw_share(deps, amount, app)?; + let funds = STRATEGY_CONFIG + .load(deps.storage)? + .withdraw_preview(deps, withdraw_share, app)?; + + let msgs = STRATEGY_CONFIG + .load(deps.storage)? + .withdraw(deps, withdraw_share, app)?; + + Ok(WithdrawPreviewResponse { + share: withdraw_share.unwrap_or(Decimal::one()), + funds, + msgs: msgs.into_iter().map(Into::into).collect(), + }) } + pub fn update_strategy_preview( deps: Deps, funds: Vec, diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index d5e36a79..3b505147 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -4,7 +4,7 @@ use abstract_app::{ traits::{AbstractNameService, Resolve}, }; use abstract_dex_adapter::DexInterface; -use cosmwasm_std::{to_json_binary, Binary, Coins, Deps, Env, Uint128}; +use cosmwasm_std::{to_json_binary, Binary, Coins, Decimal, Deps, Env, Uint128}; use cw_asset::Asset; use crate::autocompound::get_autocompound_status; @@ -190,3 +190,19 @@ pub fn query_positions(deps: Deps, app: &App) -> AppResult { .collect::>()?, }) } +pub fn withdraw_share( + deps: Deps, + amount: Option, + app: &App, +) -> AppResult> { + amount + .map(|value| { + let total_deposit = query_balance(deps, app)?; + + if total_deposit.total_value.is_zero() { + return Err(AppError::NoDeposit {}); + } + Ok(Decimal::from_ratio(value, total_deposit.total_value)) + }) + .transpose() +} diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index 58f99aef..93d8426b 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -219,7 +219,12 @@ pub struct DepositPreviewResponse { } #[cw_serde] -pub struct WithdrawPreviewResponse {} +pub struct WithdrawPreviewResponse { + /// Share of the total deposit that will be withdrawn from the app + pub share: Decimal, + pub funds: Vec, + pub msgs: Vec, +} #[cw_serde] pub struct UpdateStrategyPreviewResponse {} From 13c1c76d29b9a00055f01fc20e3a6b98201ef3ce Mon Sep 17 00:00:00 2001 From: Kayanski Date: Mon, 15 Apr 2024 09:16:06 +0000 Subject: [PATCH 37/42] Added update strategy preview --- .../carrot-app/src/distribution/deposit.rs | 5 +- contracts/carrot-app/src/handlers/execute.rs | 24 +++++-- .../carrot-app/src/handlers/instantiate.rs | 2 +- contracts/carrot-app/src/handlers/preview.rs | 63 ++++++++++++++++++- contracts/carrot-app/src/msg.rs | 5 +- 5 files changed, 86 insertions(+), 13 deletions(-) diff --git a/contracts/carrot-app/src/distribution/deposit.rs b/contracts/carrot-app/src/distribution/deposit.rs index ba8500f2..c938bfad 100644 --- a/contracts/carrot-app/src/distribution/deposit.rs +++ b/contracts/carrot-app/src/distribution/deposit.rs @@ -6,7 +6,6 @@ use crate::{ contract::{App, AppResult}, exchange_rate::query_all_exchange_rates, helpers::{compute_total_value, compute_value}, - state::STRATEGY_CONFIG, yield_sources::{yield_type::YieldType, AssetShare, Strategy, StrategyElement}, }; @@ -23,12 +22,10 @@ use crate::{error::AppError, msg::InternalExecuteMsg}; pub fn generate_deposit_strategy( deps: Deps, funds: Vec, + target_strategy: Strategy, yield_source_params: Option>>>, app: &App, ) -> AppResult<(Vec<(StrategyElement, Decimal)>, Vec)> { - // This is the storage strategy for all assets - let target_strategy = STRATEGY_CONFIG.load(deps.storage)?; - // This is the current distribution of funds inside the strategies let current_strategy_status = target_strategy.query_current_status(deps, app)?; diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index a4d825aa..40026c9f 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -9,7 +9,7 @@ use crate::{ helpers::assert_contract, msg::{AppExecuteMsg, ExecuteMsg}, state::{AUTOCOMPOUND_STATE, CONFIG, STRATEGY_CONFIG}, - yield_sources::{AssetShare, StrategyUnchecked}, + yield_sources::{AssetShare, Strategy, StrategyUnchecked}, }; use abstract_app::abstract_sdk::features::AbstractResponse; use abstract_sdk::ExecutorMsg; @@ -56,7 +56,15 @@ fn deposit( .assert_admin(deps.as_ref(), &info.sender) .or(assert_contract(&info, &env))?; - let deposit_msgs = _inner_deposit(deps.as_ref(), &env, funds, yield_source_params, &app)?; + let target_strategy = STRATEGY_CONFIG.load(deps.storage)?; + let deposit_msgs = _inner_deposit( + deps.as_ref(), + &env, + funds, + target_strategy, + yield_source_params, + &app, + )?; AUTOCOMPOUND_STATE.save( deps.storage, @@ -128,7 +136,14 @@ fn update_strategy( STRATEGY_CONFIG.save(deps.storage, &strategy)?; // 3. We deposit the funds into the new strategy - let deposit_msgs = _inner_deposit(deps.as_ref(), &env, available_funds.into(), None, &app)?; + let deposit_msgs = _inner_deposit( + deps.as_ref(), + &env, + available_funds.into(), + strategy, + None, + &app, + )?; Ok(app .response("rebalance") @@ -188,11 +203,12 @@ pub fn _inner_deposit( deps: Deps, env: &Env, funds: Vec, + target_strategy: Strategy, yield_source_params: Option>>>, app: &App, ) -> AppResult> { let (withdraw_strategy, deposit_msgs) = - generate_deposit_strategy(deps, funds, yield_source_params, app)?; + generate_deposit_strategy(deps, funds, target_strategy, yield_source_params, app)?; let deposit_withdraw_msgs = withdraw_strategy .into_iter() .map(|(el, share)| el.withdraw(deps, Some(share), app).map(Into::into)) diff --git a/contracts/carrot-app/src/handlers/instantiate.rs b/contracts/carrot-app/src/handlers/instantiate.rs index e4c93852..73dd9d00 100644 --- a/contracts/carrot-app/src/handlers/instantiate.rs +++ b/contracts/carrot-app/src/handlers/instantiate.rs @@ -27,7 +27,7 @@ pub fn instantiate_handler( // If provided - do an initial deposit if let Some(funds) = msg.deposit { - let deposit_msgs = _inner_deposit(deps.as_ref(), &env, funds, None, &app)?; + let deposit_msgs = _inner_deposit(deps.as_ref(), &env, funds, strategy, None, &app)?; response = response.add_messages(deposit_msgs); } diff --git a/contracts/carrot-app/src/handlers/preview.rs b/contracts/carrot-app/src/handlers/preview.rs index a5980ce1..8f26660d 100644 --- a/contracts/carrot-app/src/handlers/preview.rs +++ b/contracts/carrot-app/src/handlers/preview.rs @@ -1,8 +1,11 @@ -use cosmwasm_std::{Coin, Decimal, Deps, Uint128}; +use abstract_sdk::ExecutorMsg; +use cosmwasm_std::{Coin, Coins, Decimal, Deps, Uint128}; use crate::{ + check::Checkable, contract::{App, AppResult}, distribution::deposit::generate_deposit_strategy, + error::AppError, msg::{DepositPreviewResponse, UpdateStrategyPreviewResponse, WithdrawPreviewResponse}, state::STRATEGY_CONFIG, yield_sources::{AssetShare, StrategyUnchecked}, @@ -16,8 +19,9 @@ pub fn deposit_preview( yield_source_params: Option>>>, app: &App, ) -> AppResult { + let target_strategy = STRATEGY_CONFIG.load(deps.storage)?; let (withdraw_strategy, deposit_strategy) = - generate_deposit_strategy(deps, funds, yield_source_params, app)?; + generate_deposit_strategy(deps, funds, target_strategy, yield_source_params, app)?; Ok(DepositPreviewResponse { withdraw: withdraw_strategy @@ -55,5 +59,58 @@ pub fn update_strategy_preview( strategy: StrategyUnchecked, app: &App, ) -> AppResult { - Ok(UpdateStrategyPreviewResponse {}) + // We withdraw outstanding strategies + + let old_strategy = STRATEGY_CONFIG.load(deps.storage)?; + + // We check the new strategy + let strategy = strategy.check(deps, app)?; + + // We execute operations to rebalance the funds between the strategies + let mut available_funds: Coins = funds.try_into()?; + // 1. We withdraw all yield_sources that are not included in the new strategies + let all_stale_sources: Vec<_> = old_strategy + .0 + .into_iter() + .filter(|x| !strategy.0.contains(x)) + .collect(); + + let (withdrawn_funds, _withdraw_msgs): (Vec>, Vec>) = + all_stale_sources + .clone() + .into_iter() + .map(|s| { + Ok::<_, AppError>(( + s.withdraw_preview(deps, None, app).unwrap_or_default(), + s.withdraw(deps, None, app).ok(), + )) + }) + .collect::, _>>()? + .into_iter() + .unzip(); + + withdrawn_funds + .into_iter() + .try_for_each(|f| f.into_iter().try_for_each(|f| available_funds.add(f)))?; + + // 3. We deposit the funds into the new strategy + let (withdraw_strategy, deposit_strategy) = + generate_deposit_strategy(deps, available_funds.into(), strategy, None, app)?; + + let withdraw_strategy = [ + all_stale_sources + .into_iter() + .map(|s| (s, Decimal::one())) + .collect(), + withdraw_strategy, + ] + .concat(); + + Ok(UpdateStrategyPreviewResponse { + withdraw: withdraw_strategy + .into_iter() + .map(|(el, share)| (el.into(), share)) + .collect(), + deposit: deposit_strategy, + }) } diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index 93d8426b..44944fc0 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -227,4 +227,7 @@ pub struct WithdrawPreviewResponse { } #[cw_serde] -pub struct UpdateStrategyPreviewResponse {} +pub struct UpdateStrategyPreviewResponse { + pub withdraw: Vec<(StrategyElementUnchecked, Decimal)>, + pub deposit: Vec, +} From adca153485e8d858edae1391e1fe005255e62b02 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Mon, 15 Apr 2024 09:54:34 +0000 Subject: [PATCH 38/42] Removed usless import --- .../carrot-app/examples/localnet_deploy.rs | 15 ++---- .../carrot-app/examples/localnet_install.rs | 23 ++------- .../carrot-app/examples/localnet_test.rs | 51 +++---------------- 3 files changed, 17 insertions(+), 72 deletions(-) diff --git a/contracts/carrot-app/examples/localnet_deploy.rs b/contracts/carrot-app/examples/localnet_deploy.rs index 991804dc..e8b10f39 100644 --- a/contracts/carrot-app/examples/localnet_deploy.rs +++ b/contracts/carrot-app/examples/localnet_deploy.rs @@ -1,24 +1,17 @@ use abstract_app::objects::{ namespace::ABSTRACT_NAMESPACE, pool_id::PoolAddressBase, AssetEntry, PoolMetadata, PoolType, }; -use abstract_client::{AbstractClient, Application, Namespace}; -use abstract_interface::Abstract; -use abstract_sdk::core::ans_host::QueryMsgFns; +use abstract_client::{AbstractClient, Namespace}; use cosmwasm_std::Decimal; use cw_asset::AssetInfoUnchecked; use cw_orch::{ anyhow, - contract::Deploy, - daemon::{ - networks::{LOCAL_OSMO, OSMOSIS_1, OSMO_5}, - Daemon, DaemonBuilder, - }, + daemon::{networks::LOCAL_OSMO, DaemonBuilder}, prelude::*, tokio::runtime::Runtime, }; use dotenv::dotenv; -use carrot_app::{contract::APP_ID, AppInterface}; use cw_orch::osmosis_test_tube::osmosis_test_tube::cosmrs::proto::traits::Message; use osmosis_std::types::{ cosmos::base::v1beta1, @@ -40,7 +33,7 @@ pub const SPREAD_FACTOR: u64 = 0; pub const INITIAL_LOWER_TICK: i64 = -100000; pub const INITIAL_UPPER_TICK: i64 = 10000; -fn main() -> anyhow::Result<()> { +pub fn main() -> anyhow::Result<()> { dotenv().ok(); env_logger::init(); let mut chain = LOCAL_OSMO; @@ -115,7 +108,7 @@ pub fn register_ans(chain: Chain, pool_id: u64) -> anyhow::Result< let asset0 = ION.to_owned(); let asset1 = OSMO.to_owned(); // We register the pool inside the Abstract ANS - let client = AbstractClient::builder(chain.clone()) + let _client = AbstractClient::builder(chain.clone()) .dex("osmosis") .assets(vec![ (ION.to_string(), AssetInfoUnchecked::Native(asset0.clone())), diff --git a/contracts/carrot-app/examples/localnet_install.rs b/contracts/carrot-app/examples/localnet_install.rs index 94179127..3869620e 100644 --- a/contracts/carrot-app/examples/localnet_install.rs +++ b/contracts/carrot-app/examples/localnet_install.rs @@ -1,18 +1,10 @@ -use abstract_app::objects::{ - module::ModuleInfo, namespace::ABSTRACT_NAMESPACE, AccountId, AssetEntry, -}; -use abstract_client::{Application, Namespace}; -use abstract_dex_adapter::{interface::DexAdapter, DEX_ADAPTER_ID}; -use abstract_interface::{Abstract, VCQueryFns}; -use abstract_sdk::core::{ans_host::QueryMsgFns, app::BaseMigrateMsg}; +use abstract_app::objects::AssetEntry; +use abstract_client::Application; +use abstract_dex_adapter::interface::DexAdapter; use cosmwasm_std::{Decimal, Uint128, Uint64}; use cw_orch::{ anyhow, - contract::Deploy, - daemon::{ - networks::{LOCAL_OSMO, OSMOSIS_1, OSMO_5}, - Daemon, DaemonBuilder, - }, + daemon::{networks::LOCAL_OSMO, Daemon, DaemonBuilder}, prelude::*, tokio::runtime::Runtime, }; @@ -20,23 +12,18 @@ use dotenv::dotenv; use carrot_app::{ autocompound::{AutocompoundConfigBase, AutocompoundRewardsConfigBase}, - contract::APP_ID, - msg::{AppInstantiateMsg, AppMigrateMsg, MigrateMsg}, + msg::AppInstantiateMsg, state::ConfigBase, yield_sources::{ osmosis_cl_pool::ConcentratedPoolParamsBase, yield_type::YieldParamsBase, AssetShare, StrategyBase, StrategyElementBase, StrategyElementUnchecked, StrategyUnchecked, YieldSourceBase, }, - AppInterface, }; pub const ION: &str = "uion"; pub const OSMO: &str = "uosmo"; -pub const TICK_SPACING: u64 = 100; -pub const SPREAD_FACTOR: u64 = 0; - pub const INITIAL_LOWER_TICK: i64 = -100000; pub const INITIAL_UPPER_TICK: i64 = 10000; diff --git a/contracts/carrot-app/examples/localnet_test.rs b/contracts/carrot-app/examples/localnet_test.rs index ab07b5c1..76c9b314 100644 --- a/contracts/carrot-app/examples/localnet_test.rs +++ b/contracts/carrot-app/examples/localnet_test.rs @@ -1,50 +1,17 @@ -use abstract_app::objects::{ - module::ModuleInfo, namespace::ABSTRACT_NAMESPACE, AccountId, AssetEntry, -}; -use abstract_client::{Application, Namespace}; -use abstract_dex_adapter::{interface::DexAdapter, DEX_ADAPTER_ID}; -use abstract_interface::{Abstract, VCQueryFns}; -use abstract_sdk::core::ans_host::QueryMsgFns; -use cosmwasm_std::{coins, Decimal, Uint128, Uint64}; +use abstract_client::Application; +use cosmwasm_std::coins; use cw_orch::{ anyhow, - contract::Deploy, - daemon::{ - networks::{LOCAL_OSMO, OSMOSIS_1, OSMO_5}, - Daemon, DaemonBuilder, - }, - prelude::*, + daemon::{networks::LOCAL_OSMO, Daemon, DaemonBuilder}, tokio::runtime::Runtime, }; use dotenv::dotenv; -use carrot_app::{ - autocompound::{AutocompoundConfigBase, AutocompoundRewardsConfigBase}, - contract::APP_ID, - msg::AppInstantiateMsg, - state::ConfigBase, - yield_sources::{ - osmosis_cl_pool::ConcentratedPoolParamsBase, yield_type::YieldParamsBase, AssetShare, - StrategyBase, StrategyElementBase, YieldSourceBase, - }, - AppExecuteMsgFns, AppInterface, -}; -use localnet_install::{five_strategy, four_strategy, three_strategy}; +use carrot_app::AppExecuteMsgFns; +use localnet_install::{five_strategy, four_strategy, three_strategy, USER_NAMESPACE}; mod localnet_install; -pub const ION: &str = "uion"; -pub const OSMO: &str = "uosmo"; - -pub const TICK_SPACING: u64 = 100; -pub const SPREAD_FACTOR: u64 = 0; - -pub const INITIAL_LOWER_TICK: i64 = -100000; -pub const INITIAL_UPPER_TICK: i64 = 10000; - -pub const POOL_ID: u64 = 2; -pub const USER_NAMESPACE: &str = "usernamespace"; - fn main() -> anyhow::Result<()> { dotenv().ok(); env_logger::init(); @@ -60,8 +27,6 @@ fn main() -> anyhow::Result<()> { let client = abstract_client::AbstractClient::new(daemon.clone())?; - let block_info = daemon.block_info()?; - // Verify modules exist let account = client .account_builder() @@ -80,9 +45,9 @@ fn main() -> anyhow::Result<()> { carrot.deposit(coins(10_000, "uosmo"), None)?; - // carrot.update_strategy(coins(10_000, "uosmo"), five_strategy())?; - // carrot.withdraw(None)?; - // carrot.deposit(coins(10_000, "uosmo"), None)?; + carrot.update_strategy(coins(10_000, "uosmo"), five_strategy())?; + carrot.withdraw(None)?; + carrot.deposit(coins(10_000, "uosmo"), None)?; Ok(()) } From 6e750654079b439dd63b8e778630676666650a84 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Wed, 17 Apr 2024 13:43:49 +0000 Subject: [PATCH 39/42] Corrected tests --- .../carrot-app/src/distribution/deposit.rs | 32 +++++++------------ contracts/carrot-app/src/handlers/execute.rs | 1 + contracts/carrot-app/src/handlers/internal.rs | 4 --- .../src/replies/osmosis/add_to_position.rs | 1 - .../src/replies/osmosis/create_position.rs | 1 - .../src/yield_sources/osmosis_cl_pool.rs | 11 ++++--- contracts/carrot-app/tests/autocompound.rs | 8 ++--- contracts/carrot-app/tests/common.rs | 16 ++++++---- contracts/carrot-app/tests/config.rs | 14 ++++---- .../carrot-app/tests/deposit_withdraw.rs | 25 +++++++++++---- 10 files changed, 59 insertions(+), 54 deletions(-) diff --git a/contracts/carrot-app/src/distribution/deposit.rs b/contracts/carrot-app/src/distribution/deposit.rs index c938bfad..5c949278 100644 --- a/contracts/carrot-app/src/distribution/deposit.rs +++ b/contracts/carrot-app/src/distribution/deposit.rs @@ -80,7 +80,7 @@ impl Strategy { .0 .iter() .zip(self.0.clone()) - .map(|(target, current)| { + .map(|(current, target)| { // We need to take into account the total value added by the current shares let value_now = current.share * total_value; let target_value = target.share * (total_value + deposit_value); @@ -98,7 +98,7 @@ impl Strategy { } // In case there is a withdraw from the strategy, we don't need to deposit into this strategy after ! - Ok::<_, AppError>(Some((current, this_withdraw_share))) + Ok::<_, AppError>(Some((current.clone(), this_withdraw_share))) } else { Ok(None) } @@ -114,40 +114,30 @@ impl Strategy { .0 .into_iter() .zip(self.0.clone()) - .flat_map(|(target, current)| { + .map(|(current, target)| { // We need to take into account the total value added by the current shares let value_now = current.share * total_value; let target_value = target.share * (total_value + deposit_value); // If value now is smaller than the target value, we need to deposit some funds into the protocol - if target_value < value_now { - None + let share = if target_value < value_now { + Decimal::zero() } else { // In case we don't withdraw anything, it means we might deposit. - let share = if available_value.is_zero() { + if available_value.is_zero() { Decimal::zero() } else { Decimal::from_ratio(target_value - value_now, available_value) - }; - - Some(StrategyElement { - yield_source: target.yield_source.clone(), - share, - }) + } + }; + StrategyElement { + yield_source: current.yield_source.clone(), + share, } }) .collect::>() .into(); - // // Then we create the deposit elements to generate the deposits - // - // }) - // .collect::, _>>()? - // .into_iter() - // .flatten() - // .collect::>() - // .into(); - Ok((withdraw_strategy, this_deposit_strategy)) } diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index 40026c9f..4af43b64 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -209,6 +209,7 @@ pub fn _inner_deposit( ) -> AppResult> { let (withdraw_strategy, deposit_msgs) = generate_deposit_strategy(deps, funds, target_strategy, yield_source_params, app)?; + let deposit_withdraw_msgs = withdraw_strategy .into_iter() .map(|(el, share)| el.withdraw(deps, Some(share), app).map(Into::into)) diff --git a/contracts/carrot-app/src/handlers/internal.rs b/contracts/carrot-app/src/handlers/internal.rs index 68f9477c..95625cf4 100644 --- a/contracts/carrot-app/src/handlers/internal.rs +++ b/contracts/carrot-app/src/handlers/internal.rs @@ -53,7 +53,6 @@ fn deposit_one_strategy( yield_type: YieldType, app: App, ) -> AppResult { - deps.api.debug("Start deposit one strategy"); let mut temp_deposit_coins = Coins::default(); // We go through all deposit steps. @@ -121,7 +120,6 @@ pub fn execute_one_deposit_step( app: App, ) -> AppResult { let config = CONFIG.load(deps.storage)?; - deps.api.debug("Start onde deposit step"); let exchange_rate_in = query_exchange_rate(deps.as_ref(), asset_in.denom.clone(), &app)?; let exchange_rate_out = query_exchange_rate(deps.as_ref(), denom_out.clone(), &app)?; @@ -155,14 +153,12 @@ pub fn execute_finalize_deposit( yield_index: usize, app: App, ) -> AppResult { - deps.api.debug("Start finalize deposit"); let available_deposit_coins = TEMP_DEPOSIT_COINS.load(deps.storage)?; TEMP_CURRENT_YIELD.save(deps.storage, &yield_index)?; let msgs = yield_type.deposit(deps.as_ref(), available_deposit_coins, &app)?; - deps.api.debug("End finalize deposit"); Ok(app.response("finalize-deposit").add_submessages(msgs)) } diff --git a/contracts/carrot-app/src/replies/osmosis/add_to_position.rs b/contracts/carrot-app/src/replies/osmosis/add_to_position.rs index 9251472b..e65a6d4e 100644 --- a/contracts/carrot-app/src/replies/osmosis/add_to_position.rs +++ b/contracts/carrot-app/src/replies/osmosis/add_to_position.rs @@ -36,7 +36,6 @@ pub fn add_to_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) - } YieldType::Mars(_) => return Err(AppError::WrongYieldType {}), }; - deps.api.debug("after add"); save_strategy(deps, strategy)?; diff --git a/contracts/carrot-app/src/replies/osmosis/create_position.rs b/contracts/carrot-app/src/replies/osmosis/create_position.rs index 76be4352..e9ba404c 100644 --- a/contracts/carrot-app/src/replies/osmosis/create_position.rs +++ b/contracts/carrot-app/src/replies/osmosis/create_position.rs @@ -36,7 +36,6 @@ pub fn create_position_reply(deps: DepsMut, _env: Env, app: App, reply: Reply) - YieldType::Mars(_) => return Err(AppError::WrongYieldType {}), }; - deps.api.debug("after create"); save_strategy(deps, strategy)?; Ok(app diff --git a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs index 446a85bc..caa9421a 100644 --- a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs +++ b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs @@ -214,7 +214,7 @@ impl ConcentratedPoolParams { app: &App, position: FullPositionBreakdown, ) -> AppResult> { - let position_id = position.position.unwrap().position_id; + let position_id = position.position.clone().unwrap().position_id; let proxy_addr = app.account_base(deps)?.proxy; @@ -222,14 +222,16 @@ impl ConcentratedPoolParams { // We assume the funds vector has 2 coins associated let (amount0, amount1) = match position .asset0 + .clone() .map(|c| c.denom == funds[0].denom) - .or(position.asset1.map(|c| c.denom == funds[1].denom)) + .or(position.asset1.clone().map(|c| c.denom == funds[1].denom)) { Some(true) => (funds[0].amount, funds[1].amount), // we already had the right order Some(false) => (funds[1].amount, funds[0].amount), // we had the wrong order - None => return Err(AppError::NoPosition {}), // A position has to exist in order to execute this function. This should be unreachable + None => { + return Err(AppError::NoPosition {}); + } // A position has to exist in order to execute this function. This should be unreachable }; - deps.api.debug("After amounts"); let deposit_msg = app.executor(deps).execute_with_reply_and_data( MsgAddToPosition { @@ -244,7 +246,6 @@ impl ConcentratedPoolParams { cosmwasm_std::ReplyOn::Success, OSMOSIS_ADD_TO_POSITION_REPLY_ID, )?; - deps.api.debug("After messages"); Ok(vec![deposit_msg]) } diff --git a/contracts/carrot-app/tests/autocompound.rs b/contracts/carrot-app/tests/autocompound.rs index 1a69f77f..8dd5c70a 100644 --- a/contracts/carrot-app/tests/autocompound.rs +++ b/contracts/carrot-app/tests/autocompound.rs @@ -33,13 +33,13 @@ fn check_autocompound() -> anyhow::Result<()> { chain.bank_send( account.proxy.addr_str()?, vec![ - coin(200_000, USDC.to_owned()), - coin(200_000, USDT.to_owned()), + coin(2_000_000, USDC.to_owned()), + coin(2_000_000, USDT.to_owned()), ], )?; for _ in 0..10 { - dex.ans_swap((USDC, 50_000), USDT, DEX_NAME.to_string(), &account)?; - dex.ans_swap((USDT, 50_000), USDC, DEX_NAME.to_string(), &account)?; + dex.ans_swap((USDC, 500_000), USDT, DEX_NAME.to_string(), &account)?; + dex.ans_swap((USDT, 500_000), USDC, DEX_NAME.to_string(), &account)?; } // Check autocompound adds liquidity from the rewards and user balance remain unchanged diff --git a/contracts/carrot-app/tests/common.rs b/contracts/carrot-app/tests/common.rs index 503cc9dd..5bedc582 100644 --- a/contracts/carrot-app/tests/common.rs +++ b/contracts/carrot-app/tests/common.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use abstract_app::abstract_core::objects::{ pool_id::PoolAddressBase, AssetEntry, PoolMetadata, PoolType, }; @@ -29,6 +31,7 @@ use osmosis_std::types::osmosis::concentratedliquidity::v1beta1::{ }; use prost::Message; pub const LOTS: u128 = 100_000_000_000_000; +pub const LOTS_PROVIDE: u128 = 5_000_000; // Asset 0 pub const USDT: &str = "ibc/4ABBEF4C8926DDDB320AE5188CFD63267ABBCEFC0583E4AE05D6E5AA2401DDAB"; @@ -42,10 +45,11 @@ pub const GAS_DENOM: &str = "uosmo"; pub const DEX_NAME: &str = "osmosis"; pub const TICK_SPACING: u64 = 100; -pub const SPREAD_FACTOR: u64 = 1; +pub const SPREAD_FACTOR: &str = "0.01"; pub const INITIAL_LOWER_TICK: i64 = -100000; pub const INITIAL_UPPER_TICK: i64 = 10000; + // Deploys abstract and other contracts pub fn deploy( mut chain: Chain, @@ -196,8 +200,8 @@ pub fn deploy( } pub fn create_pool(mut chain: OsmosisTestTube) -> anyhow::Result<(u64, u64)> { - chain.add_balance(chain.sender(), coins(LOTS, USDC))?; - chain.add_balance(chain.sender(), coins(LOTS, USDT))?; + chain.add_balance(chain.sender(), coins(LOTS_PROVIDE, USDC))?; + chain.add_balance(chain.sender(), coins(LOTS_PROVIDE, USDT))?; let asset0 = USDT.to_owned(); let asset1 = USDC.to_owned(); @@ -227,7 +231,7 @@ pub fn create_pool(mut chain: OsmosisTestTube) -> anyhow::Result<(u64, u64)> { denom0: USDT.to_owned(), denom1: USDC.to_owned(), tick_spacing: TICK_SPACING, - spread_factor: Decimal::percent(SPREAD_FACTOR).atomics().to_string(), + spread_factor: Decimal::from_str(SPREAD_FACTOR)?.atomics().to_string(), }], }, chain.sender().to_string(), @@ -251,11 +255,11 @@ pub fn create_pool(mut chain: OsmosisTestTube) -> anyhow::Result<(u64, u64)> { tokens_provided: vec![ v1beta1::Coin { denom: asset1, - amount: "1_000_000".to_owned(), + amount: (LOTS_PROVIDE / 2).to_string(), }, v1beta1::Coin { denom: asset0.clone(), - amount: "1_000_000".to_owned(), + amount: (LOTS_PROVIDE / 2).to_string(), }, ], token_min_amount0: "0".to_string(), diff --git a/contracts/carrot-app/tests/config.rs b/contracts/carrot-app/tests/config.rs index 0a2ce4b1..ce9f54c0 100644 --- a/contracts/carrot-app/tests/config.rs +++ b/contracts/carrot-app/tests/config.rs @@ -305,14 +305,15 @@ fn rebalance_with_stale_strategy_success() -> anyhow::Result<()> { // We query the balance let balance = carrot_app.balance()?; - // Make sure the deposit went almost all in - assert!(balance.total_value > Uint128::from(deposit_amount) * Decimal::percent(98)); println!( "Before :{}, after: {}", total_value_before, balance.total_value ); + // Make sure the deposit went almost all in + assert!(balance.total_value > Uint128::from(deposit_amount) * Decimal::percent(98)); + // Make sure the total value has almost not changed when updating the strategy - assert!(balance.total_value > total_value_before * Decimal::permille(999)); + assert!(balance.total_value > total_value_before * Decimal::percent(99)); let distribution = carrot_app.positions()?; @@ -395,18 +396,19 @@ fn rebalance_with_current_and_stale_strategy_success() -> anyhow::Result<()> { // No additional deposit carrot_app.update_strategy(vec![], strategies.clone())?; - carrot_app.strategy()?; + assert_eq!(carrot_app.strategy()?.strategy.0.len(), 2); // We query the balance let balance = carrot_app.balance()?; // Make sure the deposit went almost all in - assert!(balance.total_value > Uint128::from(deposit_amount) * Decimal::percent(98)); println!( "Before :{}, after: {}", total_value_before, balance.total_value ); + assert!(balance.total_value > Uint128::from(deposit_amount) * Decimal::percent(98)); + // Make sure the total value has almost not changed when updating the strategy - assert!(balance.total_value > total_value_before * Decimal::permille(998)); + assert!(balance.total_value > total_value_before * Decimal::permille(997)); let distribution = carrot_app.positions()?; diff --git a/contracts/carrot-app/tests/deposit_withdraw.rs b/contracts/carrot-app/tests/deposit_withdraw.rs index fbb3ed44..09a8a378 100644 --- a/contracts/carrot-app/tests/deposit_withdraw.rs +++ b/contracts/carrot-app/tests/deposit_withdraw.rs @@ -49,7 +49,14 @@ fn deposit_lands() -> anyhow::Result<()> { carrot_app.deposit(deposit_coins.clone(), None)?; // Check almost everything landed let balances_after = query_balances(&carrot_app)?; - assert!(balances_before < balances_after); + println!( + "Expected deposit amount {}, actual deposit {}, remaining", + deposit_amount, + balances_after - balances_before, + ); + assert!( + balances_after > balances_before + Uint128::from(deposit_amount) * Decimal::percent(98) + ); // Add some more funds chain.add_balance( @@ -60,7 +67,15 @@ fn deposit_lands() -> anyhow::Result<()> { let response = carrot_app.deposit(vec![coin(deposit_amount, USDT.to_owned())], None)?; // Check almost everything landed let balances_after_second = query_balances(&carrot_app)?; - assert!(balances_after < balances_after_second); + println!( + "Expected deposit amount {}, actual deposit {}, remaining", + deposit_amount, + balances_after_second - balances_after, + ); + assert!( + balances_after_second + > balances_after + Uint128::from(deposit_amount) * Decimal::percent(98) + ); // We assert the deposit response is an add to position and not a create position response.event_attr_value("add_to_position", "new_position_id")?; @@ -193,7 +208,6 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { share: Decimal::percent(50), }, ]); - carrot_app.update_strategy(vec![], new_strat.clone())?; let deposit_amount = 5_000; let deposit_coins = coins(deposit_amount, USDT.to_owned()); @@ -204,7 +218,7 @@ fn deposit_multiple_positions() -> anyhow::Result<()> { carrot_app.account().proxy()?.to_string(), deposit_coins.clone(), )?; - carrot_app.deposit(deposit_coins, None)?; + carrot_app.update_strategy(deposit_coins, new_strat.clone())?; let balances_after = query_balances(&carrot_app)?; let slippage = Decimal::percent(4); @@ -277,7 +291,6 @@ fn deposit_multiple_positions_with_empty() -> anyhow::Result<()> { share: Decimal::percent(0), }, ]); - carrot_app.update_strategy(vec![], new_strat.clone())?; let deposit_amount = 5_000; let deposit_coins = coins(deposit_amount, USDT.to_owned()); @@ -288,7 +301,7 @@ fn deposit_multiple_positions_with_empty() -> anyhow::Result<()> { carrot_app.account().proxy()?.to_string(), deposit_coins.clone(), )?; - carrot_app.deposit(deposit_coins, None)?; + carrot_app.update_strategy(deposit_coins, new_strat.clone())?; let balances_after = query_balances(&carrot_app)?; println!("{balances_before} --> {balances_after}"); From ea38f9cf5eae1fb7f55b6e6db1694cc885c034d2 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Fri, 19 Apr 2024 12:41:11 +0000 Subject: [PATCH 40/42] Updated autocompound process --- bot/src/bot.rs | 4 +- .../examples/install_savings_app.rs | 8 +- .../carrot-app/examples/localnet_install.rs | 9 +- contracts/carrot-app/src/autocompound.rs | 106 ++++-------- contracts/carrot-app/src/check.rs | 40 +---- contracts/carrot-app/src/handlers/execute.rs | 19 ++- contracts/carrot-app/src/handlers/mod.rs | 1 - contracts/carrot-app/src/handlers/query.rs | 49 +----- .../carrot-app/src/handlers/swap_helpers.rs | 30 ---- contracts/carrot-app/src/msg.rs | 4 - .../src/yield_sources/osmosis_cl_pool.rs | 50 +----- contracts/carrot-app/tests/autocompound.rs | 152 +++++++++--------- contracts/carrot-app/tests/common.rs | 10 +- 13 files changed, 146 insertions(+), 336 deletions(-) delete mode 100644 contracts/carrot-app/src/handlers/swap_helpers.rs diff --git a/bot/src/bot.rs b/bot/src/bot.rs index 97f5a24e..1982f630 100644 --- a/bot/src/bot.rs +++ b/bot/src/bot.rs @@ -211,9 +211,7 @@ fn autocompound_instance(daemon: &Daemon, instance: (&str, &Addr)) -> anyhow::Re let resp: CompoundStatusResponse = app.compound_status()?; // TODO: ensure rewards > tx fee - // To discuss if we really need it? - - if resp.rewards_available { + if resp.status.is_ready() { // Execute autocompound let daemon = daemon.rebuild().authz_granter(address).build()?; daemon.execute( diff --git a/contracts/carrot-app/examples/install_savings_app.rs b/contracts/carrot-app/examples/install_savings_app.rs index dd3ffd10..6d879a70 100644 --- a/contracts/carrot-app/examples/install_savings_app.rs +++ b/contracts/carrot-app/examples/install_savings_app.rs @@ -1,7 +1,7 @@ #![allow(unused)] use abstract_app::objects::{AccountId, AssetEntry}; use abstract_client::AbstractClient; -use cosmwasm_std::{coins, Coin, Uint128, Uint256, Uint64}; +use cosmwasm_std::{coins, Coin, Decimal, Uint128, Uint256, Uint64}; use cw_orch::{ anyhow, daemon::{networks::OSMOSIS_1, Daemon, DaemonBuilder}, @@ -66,11 +66,7 @@ fn main() -> anyhow::Result<()> { autocompound_config: AutocompoundConfigBase { cooldown_seconds: Uint64::new(AUTOCOMPOUND_COOLDOWN_SECONDS), rewards: AutocompoundRewardsConfigBase { - gas_asset: AssetEntry::new(utils::REWARD_ASSET), - swap_asset: app_data.swap_asset, - reward: Uint128::new(50_000), - min_gas_balance: Uint128::new(1000000), - max_gas_balance: Uint128::new(3000000), + reward_percent: Decimal::percent(10), _phantom: std::marker::PhantomData, }, }, diff --git a/contracts/carrot-app/examples/localnet_install.rs b/contracts/carrot-app/examples/localnet_install.rs index 3869620e..e3a824f8 100644 --- a/contracts/carrot-app/examples/localnet_install.rs +++ b/contracts/carrot-app/examples/localnet_install.rs @@ -1,7 +1,6 @@ -use abstract_app::objects::AssetEntry; use abstract_client::Application; use abstract_dex_adapter::interface::DexAdapter; -use cosmwasm_std::{Decimal, Uint128, Uint64}; +use cosmwasm_std::{Decimal, Uint64}; use cw_orch::{ anyhow, daemon::{networks::LOCAL_OSMO, Daemon, DaemonBuilder}, @@ -58,11 +57,7 @@ fn main() -> anyhow::Result<()> { autocompound_config: AutocompoundConfigBase { cooldown_seconds: Uint64::new(300), rewards: AutocompoundRewardsConfigBase { - gas_asset: AssetEntry::new(OSMO), - swap_asset: AssetEntry::new(ION), - reward: Uint128::new(1000), - min_gas_balance: Uint128::new(2000), - max_gas_balance: Uint128::new(10000), + reward_percent: Decimal::percent(10), _phantom: std::marker::PhantomData, }, }, diff --git a/contracts/carrot-app/src/autocompound.rs b/contracts/carrot-app/src/autocompound.rs index a49c338d..b02af912 100644 --- a/contracts/carrot-app/src/autocompound.rs +++ b/contracts/carrot-app/src/autocompound.rs @@ -1,18 +1,14 @@ use std::marker::PhantomData; -use abstract_app::abstract_core::objects::AssetEntry; -use abstract_app::objects::AnsAsset; -use abstract_dex_adapter::DexInterface; -use abstract_sdk::{Execution, TransferInterface}; +use abstract_sdk::{Execution, ExecutorMsg, TransferInterface}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, CosmosMsg, Deps, Env, MessageInfo, Storage, Timestamp, Uint128, Uint64}; +use cosmwasm_std::{Coin, Decimal, Deps, Env, MessageInfo, Storage, Timestamp, Uint64}; use crate::check::{Checked, Unchecked}; use crate::contract::App; use crate::contract::AppResult; -use crate::handlers::swap_helpers::swap_msg; use crate::msg::CompoundStatus; -use crate::state::{Config, AUTOCOMPOUND_STATE}; +use crate::state::{Config, AUTOCOMPOUND_STATE, CONFIG}; pub type AutocompoundConfig = AutocompoundConfigBase; pub type AutocompoundConfigUnchecked = AutocompoundConfigBase; @@ -41,16 +37,8 @@ impl From for AutocompoundConfigUnchecked { /// to the address who helped to execute autocompound #[cw_serde] pub struct AutocompoundRewardsConfigBase { - /// Gas denominator for this chain - pub gas_asset: AssetEntry, - /// Denominator of the asset that will be used for swap to the gas asset - pub swap_asset: AssetEntry, - /// Reward amount - pub reward: Uint128, - /// If gas token balance falls below this bound a swap will be generated - pub min_gas_balance: Uint128, - /// Upper bound of gas tokens expected after the swap - pub max_gas_balance: Uint128, + /// Percentage of the withdraw, rewards that will be sent to the auto-compounder + pub reward_percent: Decimal, pub _phantom: PhantomData, } @@ -64,8 +52,10 @@ impl Config { deps: Deps, env: &Env, info: MessageInfo, + rewards: &[Coin], app: &App, - ) -> AppResult> { + ) -> AppResult { + let config = CONFIG.load(deps.storage)?; Ok( // If called by non-admin and reward cooldown has ended, send rewards to the contract caller. if !app.admin.is_admin(deps, &info.sender)? @@ -76,67 +66,35 @@ impl Config { )? .is_ready() { - self.autocompound_executor_rewards(deps, env, &info.sender, app)? + let funds: Vec = rewards + .iter() + .flat_map(|a| { + let reward_amount = + a.amount * config.autocompound_config.rewards.reward_percent; + + Some(Coin::new(reward_amount.into(), a.denom.clone())) + }) + .collect(); + ExecutorRewards { + funds: funds.clone(), + msg: Some( + app.executor(deps) + .execute(vec![app.bank(deps).transfer(funds, &info.sender)?])?, + ), + } } else { - vec![] + ExecutorRewards { + funds: vec![], + msg: None, + } }, ) } - pub fn autocompound_executor_rewards( - &self, - deps: Deps, - env: &Env, - executor: &Addr, - app: &App, - ) -> AppResult> { - let rewards_config = self.autocompound_config.rewards.clone(); - - // Get user balance of gas denom - let user_gas_balance = app.bank(deps).balance(&rewards_config.gas_asset)?.amount; - - let mut rewards_messages = vec![]; - - // If not enough gas coins - swap for some amount - if user_gas_balance < rewards_config.min_gas_balance { - // Get asset entries - let dex = app.ans_dex(deps, self.dex.to_string()); - - // Do reverse swap to find approximate amount we need to swap - let need_gas_coins = rewards_config.max_gas_balance - user_gas_balance; - let simulate_swap_response = dex.simulate_swap( - AnsAsset::new(rewards_config.gas_asset.clone(), need_gas_coins), - rewards_config.swap_asset.clone(), - )?; - - // Get user balance of swap denom - let user_swap_balance = app.bank(deps).balance(&rewards_config.swap_asset)?.amount; - - // Swap as much as available if not enough for max_gas_balance - let swap_amount = simulate_swap_response.return_amount.min(user_swap_balance); - - let msgs = swap_msg( - deps, - env, - AnsAsset::new(rewards_config.swap_asset, swap_amount), - rewards_config.gas_asset.clone(), - app, - )?; - rewards_messages.extend(msgs); - } - - // We send their reward to the executor - let msg_send = app.bank(deps).transfer( - vec![AnsAsset::new( - rewards_config.gas_asset, - rewards_config.reward, - )], - executor, - )?; - - rewards_messages.push(app.executor(deps).execute(vec![msg_send])?.into()); +} - Ok(rewards_messages) - } +pub struct ExecutorRewards { + pub funds: Vec, + pub msg: Option, } #[cw_serde] diff --git a/contracts/carrot-app/src/check.rs b/contracts/carrot-app/src/check.rs index 6761ae93..f8048304 100644 --- a/contracts/carrot-app/src/check.rs +++ b/contracts/carrot-app/src/check.rs @@ -16,10 +16,7 @@ pub trait Checkable { mod config { use std::marker::PhantomData; - use abstract_app::{ - abstract_sdk::Resolve, objects::DexAssetPairing, traits::AbstractNameService, - }; - use cosmwasm_std::{ensure, Deps}; + use cosmwasm_std::{ensure, Decimal, Deps}; use crate::{ autocompound::{ @@ -34,11 +31,7 @@ mod config { impl From for AutocompoundRewardsConfigUnchecked { fn from(value: AutocompoundRewardsConfig) -> Self { Self { - gas_asset: value.gas_asset, - swap_asset: value.swap_asset, - reward: value.reward, - min_gas_balance: value.min_gas_balance, - max_gas_balance: value.max_gas_balance, + reward_percent: value.reward_percent, _phantom: PhantomData, } } @@ -47,33 +40,16 @@ mod config { impl AutocompoundRewardsConfigUnchecked { pub fn check( self, - deps: Deps, - app: &App, - dex_name: &str, + _deps: Deps, + _app: &App, + _dex_name: &str, ) -> AppResult { ensure!( - self.reward <= self.min_gas_balance, - AppError::RewardConfigError( - "reward should be lower or equal to the min_gas_balance".to_owned() - ) - ); - ensure!( - self.max_gas_balance > self.min_gas_balance, - AppError::RewardConfigError( - "max_gas_balance has to be bigger than min_gas_balance".to_owned() - ) + self.reward_percent <= Decimal::one(), + AppError::RewardConfigError("reward percents should be lower than 100%".to_owned()) ); - - // Check swap asset has pairing into gas asset - DexAssetPairing::new(self.gas_asset.clone(), self.swap_asset.clone(), dex_name) - .resolve(&deps.querier, app.name_service(deps).host())?; - Ok(AutocompoundRewardsConfig { - gas_asset: self.gas_asset, - swap_asset: self.swap_asset, - reward: self.reward, - min_gas_balance: self.min_gas_balance, - max_gas_balance: self.max_gas_balance, + reward_percent: self.reward_percent, _phantom: PhantomData, }) } diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index 4af43b64..e05868af 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -159,6 +159,7 @@ fn update_strategy( // /// Auto-compound the position with earned fees and incentives. fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResult { + let config = CONFIG.load(deps.storage)?; // Everyone can autocompound let strategy = STRATEGY_CONFIG.load(deps.storage)?; @@ -170,11 +171,21 @@ fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResu return Err(crate::error::AppError::NoRewards {}); } + // We reward the caller of this endpoint with some funds + let executor_rewards = + config.get_executor_reward_messages(deps.as_ref(), &env, info, &all_rewards, &app)?; + + let mut all_rewards: Coins = all_rewards.try_into()?; + + for f in executor_rewards.funds { + all_rewards.sub(f)?; + } + // Finally we deposit of all rewarded tokens into the position let msg_deposit = CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: env.contract.address.to_string(), msg: to_json_binary(&ExecuteMsg::Module(AppExecuteMsg::Deposit { - funds: all_rewards, + funds: all_rewards.into(), yield_sources_params: None, }))?, funds: vec![], @@ -185,10 +196,6 @@ fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResu .add_messages(collect_rewards_msgs) .add_message(msg_deposit); - let config = CONFIG.load(deps.storage)?; - let executor_reward_messages = - config.get_executor_reward_messages(deps.as_ref(), &env, info, &app)?; - AUTOCOMPOUND_STATE.save( deps.storage, &AutocompoundState { @@ -196,7 +203,7 @@ fn autocompound(deps: DepsMut, env: Env, info: MessageInfo, app: App) -> AppResu }, )?; - Ok(response.add_messages(executor_reward_messages)) + Ok(response.add_messages(executor_rewards.msg)) } pub fn _inner_deposit( diff --git a/contracts/carrot-app/src/handlers/mod.rs b/contracts/carrot-app/src/handlers/mod.rs index 6ba1876a..fcbd3c7b 100644 --- a/contracts/carrot-app/src/handlers/mod.rs +++ b/contracts/carrot-app/src/handlers/mod.rs @@ -5,7 +5,6 @@ pub mod migrate; /// Allows to preview the usual operations before executing them pub mod preview; pub mod query; -pub mod swap_helpers; pub use crate::handlers::{ execute::execute_handler, instantiate::instantiate_handler, migrate::migrate_handler, query::query_handler, diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index 3b505147..2d8dc1a3 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -1,11 +1,4 @@ -use abstract_app::traits::AccountIdentification; -use abstract_app::{ - abstract_core::objects::AnsAsset, - traits::{AbstractNameService, Resolve}, -}; -use abstract_dex_adapter::DexInterface; use cosmwasm_std::{to_json_binary, Binary, Coins, Decimal, Deps, Env, Uint128}; -use cw_asset::Asset; use crate::autocompound::get_autocompound_status; use crate::exchange_rate::query_exchange_rate; @@ -15,7 +8,6 @@ use crate::yield_sources::yield_type::YieldTypeImplementation; use crate::{ contract::{App, AppResult}, error::AppError, - helpers::get_balance, msg::{ AppQueryMsg, AssetsBalanceResponse, AvailableRewardsResponse, CompoundStatusResponse, StrategyResponse, @@ -50,7 +42,7 @@ pub fn query_handler(deps: Deps, env: Env, app: &App, msg: AppQueryMsg) -> AppRe /// Gets the status of the compounding logic of the application /// Accounts for the user's ability to pay for the gas fees of executing the contract. -fn query_compound_status(deps: Deps, env: Env, app: &App) -> AppResult { +fn query_compound_status(deps: Deps, env: Env, _app: &App) -> AppResult { let config = CONFIG.load(deps.storage)?; let status = get_autocompound_status( deps.storage, @@ -58,44 +50,7 @@ fn query_compound_status(deps: Deps, env: Env, app: &App) -> AppResult= reward.amount { - true - } else { - // check if can swap - let rewards_config = config.autocompound_config.rewards; - let dex = app.ans_dex(deps, config.dex); - - // Reverse swap to see how many swap coins needed - let required_gas_coins = reward.amount - user_gas_balance; - let response = dex.simulate_swap( - AnsAsset::new(rewards_config.gas_asset, required_gas_coins), - rewards_config.swap_asset.clone(), - )?; - - // Check if user has enough of swap coins - let user_swap_balance = get_balance(rewards_config.swap_asset, deps, user, app)?; - let required_swap_amount = response.return_amount; - - user_swap_balance > required_swap_amount - }; - - Ok(CompoundStatusResponse { - status, - reward: reward.into(), - rewards_available, - }) + Ok(CompoundStatusResponse { status }) } pub fn query_strategy(deps: Deps) -> AppResult { diff --git a/contracts/carrot-app/src/handlers/swap_helpers.rs b/contracts/carrot-app/src/handlers/swap_helpers.rs deleted file mode 100644 index b18c75b3..00000000 --- a/contracts/carrot-app/src/handlers/swap_helpers.rs +++ /dev/null @@ -1,30 +0,0 @@ -use abstract_app::objects::{AnsAsset, AssetEntry}; -use abstract_dex_adapter::DexInterface; -use cosmwasm_std::{CosmosMsg, Decimal, Deps, Env}; -const MAX_SPREAD_PERCENT: u64 = 20; -pub const DEFAULT_SLIPPAGE: Decimal = Decimal::permille(5); - -use crate::contract::{App, AppResult, OSMOSIS}; - -pub(crate) fn swap_msg( - deps: Deps, - _env: &Env, - offer_asset: AnsAsset, - ask_asset: AssetEntry, - app: &App, -) -> AppResult> { - // Don't swap if not required - if offer_asset.amount.is_zero() { - return Ok(None); - } - - let dex = app.ans_dex(deps, OSMOSIS.to_string()); - let swap_msg = dex.swap( - offer_asset, - ask_asset, - Some(Decimal::percent(MAX_SPREAD_PERCENT)), - None, - )?; - - Ok(Some(swap_msg)) -} diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index 44944fc0..504ab8a1 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -1,6 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{wasm_execute, Coin, CosmosMsg, Decimal, Env, Uint128, Uint64}; -use cw_asset::AssetBase; use crate::{ contract::{App, AppResult}, @@ -190,9 +189,6 @@ pub struct PositionResponse { #[cw_serde] pub struct CompoundStatusResponse { pub status: CompoundStatus, - pub reward: AssetBase, - // Wether user have enough balance to reward or can swap - pub rewards_available: bool, } #[cw_serde] diff --git a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs index caa9421a..eba7a6d1 100644 --- a/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs +++ b/contracts/carrot-app/src/yield_sources/osmosis_cl_pool.rs @@ -4,15 +4,12 @@ use crate::{ check::{Checked, Unchecked}, contract::{App, AppResult}, error::AppError, - handlers::swap_helpers::DEFAULT_SLIPPAGE, replies::{OSMOSIS_ADD_TO_POSITION_REPLY_ID, OSMOSIS_CREATE_POSITION_REPLY_ID}, - state::CONFIG, }; -use abstract_app::{objects::AnsAsset, traits::AccountIdentification}; -use abstract_dex_adapter::DexInterface; +use abstract_app::traits::AccountIdentification; use abstract_sdk::Execution; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ensure, Coin, Coins, CosmosMsg, Decimal, Deps, ReplyOn, SubMsg, Uint128}; +use cosmwasm_std::{Coin, Coins, CosmosMsg, Deps, ReplyOn, SubMsg, Uint128}; use osmosis_std::{ cosmwasm_to_proto_coins, try_proto_to_cosmwasm_coins, types::osmosis::concentratedliquidity::v1beta1::{ @@ -259,46 +256,3 @@ impl ConcentratedPoolParams { .ok_or(AppError::NoPosition {}) } } - -pub fn query_swap_price( - deps: Deps, - app: &App, - max_spread: Option, - belief_price0: Option, - belief_price1: Option, - asset0: AnsAsset, - asset1: AnsAsset, -) -> AppResult { - let config = CONFIG.load(deps.storage)?; - - // We take the biggest amount and simulate a swap for the corresponding asset - let price = if asset0.amount > asset1.amount { - let simulation_result = app - .ans_dex(deps, config.dex.clone()) - .simulate_swap(asset0.clone(), asset1.name)?; - - let price = Decimal::from_ratio(asset0.amount, simulation_result.return_amount); - if let Some(belief_price) = belief_price1 { - ensure!( - belief_price.abs_diff(price) <= max_spread.unwrap_or(DEFAULT_SLIPPAGE), - AppError::MaxSpreadAssertion { price } - ); - } - price - } else { - let simulation_result = app - .ans_dex(deps, config.dex.clone()) - .simulate_swap(asset1.clone(), asset0.name)?; - - let price = Decimal::from_ratio(simulation_result.return_amount, asset1.amount); - if let Some(belief_price) = belief_price0 { - ensure!( - belief_price.abs_diff(price) <= max_spread.unwrap_or(DEFAULT_SLIPPAGE), - AppError::MaxSpreadAssertion { price } - ); - } - price - }; - - Ok(price) -} diff --git a/contracts/carrot-app/tests/autocompound.rs b/contracts/carrot-app/tests/autocompound.rs index 8dd5c70a..29455910 100644 --- a/contracts/carrot-app/tests/autocompound.rs +++ b/contracts/carrot-app/tests/autocompound.rs @@ -1,11 +1,13 @@ mod common; -use crate::common::{setup_test_tube, DEX_NAME, USDC, USDT}; +use crate::common::{setup_test_tube, DEX_NAME, EXECUTOR_REWARD, GAS_DENOM, LOTS, USDC, USDT}; use abstract_app::abstract_interface::{Abstract, AbstractAccount}; use carrot_app::msg::{ AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse, AvailableRewardsResponse, + CompoundStatus, CompoundStatusResponse, }; use cosmwasm_std::{coin, coins}; +use cw_orch::osmosis_test_tube::osmosis_test_tube::Account; use cw_orch::{anyhow, prelude::*}; #[test] @@ -90,74 +92,80 @@ fn check_autocompound() -> anyhow::Result<()> { Ok(()) } -// #[test] -// fn stranger_autocompound() -> anyhow::Result<()> { -// let (_, carrot_app) = setup_test_tube(false)?; - -// let mut chain = carrot_app.get_chain().clone(); -// let stranger = chain.init_account(coins(LOTS, GAS_DENOM))?; - -// // Create position -// create_position( -// &carrot_app, -// coins(100_000, USDT.to_owned()), -// coin(1_000_000, USDT.to_owned()), -// coin(1_000_000, USDC.to_owned()), -// )?; - -// // Do some swaps -// let dex: abstract_dex_adapter::interface::DexAdapter<_> = carrot_app.module()?; -// let abs = Abstract::load_from(chain.clone())?; -// let account_id = carrot_app.account().id()?; -// let account = AbstractAccount::new(&abs, account_id); -// chain.bank_send( -// account.proxy.addr_str()?, -// vec![ -// coin(200_000, USDC.to_owned()), -// coin(200_000, USDT.to_owned()), -// ], -// )?; -// for _ in 0..10 { -// dex.ans_swap((USDC, 50_000), USDT, DEX_NAME.to_string(), &account)?; -// dex.ans_swap((USDT, 50_000), USDC, DEX_NAME.to_string(), &account)?; -// } - -// // Check autocompound adds liquidity from the rewards, user balance remain unchanged -// // and rewards gets passed to the "stranger" - -// // Check it has some rewards to autocompound first -// let rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; -// assert!(!rewards.available_rewards.is_empty()); - -// // Save balances -// let balance_before_autocompound: AssetsBalanceResponse = carrot_app.balance()?; - -// // Autocompound by stranger -// chain.wait_seconds(300)?; -// // Check query is able to compute rewards, when swap is required -// let compound_status: CompoundStatusResponse = carrot_app.compound_status()?; -// assert_eq!( -// compound_status, -// CompoundStatusResponse { -// status: CompoundStatus::Ready {}, -// reward: AssetBase::native(REWARD_DENOM, 1000u128), -// rewards_available: true -// } -// ); -// carrot_app.call_as(&stranger).autocompound()?; - -// // Save new balances -// let balance_after_autocompound: AssetsBalanceResponse = carrot_app.balance()?; - -// // Liquidity added -// assert!(balance_after_autocompound.liquidity > balance_before_autocompound.liquidity); - -// // Check it used all of the rewards -// let rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; -// assert!(rewards.available_rewards.is_empty()); - -// // Check stranger gets rewarded -// let stranger_reward_balance = chain.query_balance(stranger.address().as_str(), REWARD_DENOM)?; -// assert_eq!(stranger_reward_balance, Uint128::new(1000)); -// Ok(()) -// } +#[test] +fn stranger_autocompound() -> anyhow::Result<()> { + let (_, carrot_app) = setup_test_tube(false)?; + + let mut chain = carrot_app.get_chain().clone(); + let stranger = chain.init_account(coins(LOTS, GAS_DENOM))?; + + // Create position + let deposit_amount = 5_000; + let deposit_coins = coins(deposit_amount, USDT.to_owned()); + chain.add_balance( + carrot_app.account().proxy()?.to_string(), + deposit_coins.clone(), + )?; + + // Do the deposit + carrot_app.deposit(deposit_coins.clone(), None)?; + + // Do some swaps + let dex: abstract_dex_adapter::interface::DexAdapter<_> = carrot_app.module()?; + let abs = Abstract::load_from(chain.clone())?; + let account_id = carrot_app.account().id()?; + let account = AbstractAccount::new(&abs, account_id); + chain.bank_send( + account.proxy.addr_str()?, + vec![ + coin(200_000, USDC.to_owned()), + coin(200_000, USDT.to_owned()), + ], + )?; + for _ in 0..10 { + dex.ans_swap((USDC, 50_000), USDT, DEX_NAME.to_string(), &account)?; + dex.ans_swap((USDT, 50_000), USDC, DEX_NAME.to_string(), &account)?; + } + + // Check autocompound adds liquidity from the rewards, user balance remain unchanged + // and rewards gets passed to the "stranger" + + // Check it has some rewards to autocompound first + let available_rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; + assert!(!available_rewards.available_rewards.is_empty()); + + // Save balances + let balance_before_autocompound: AssetsBalanceResponse = carrot_app.balance()?; + + // Autocompound by stranger + chain.wait_seconds(300)?; + // Check query is able to compute rewards, when swap is required + let compound_status = carrot_app.compound_status()?; + assert_eq!( + compound_status, + CompoundStatusResponse { + status: CompoundStatus::Ready {}, + } + ); + carrot_app.call_as(&stranger).autocompound()?; + + // Save new balances + let balance_after_autocompound: AssetsBalanceResponse = carrot_app.balance()?; + + // Liquidity added + assert!(balance_after_autocompound.total_value > balance_before_autocompound.total_value); + + // Check it used all of the rewards + let rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; + assert!(rewards.available_rewards.is_empty()); + + // Check stranger gets rewarded + + for reward in available_rewards.available_rewards { + let stranger_reward_balance = + chain.query_balance(stranger.address().as_str(), &reward.denom)?; + assert_eq!(stranger_reward_balance, reward.amount * EXECUTOR_REWARD); + } + + Ok(()) +} diff --git a/contracts/carrot-app/tests/common.rs b/contracts/carrot-app/tests/common.rs index 5bedc582..c8f4f7b0 100644 --- a/contracts/carrot-app/tests/common.rs +++ b/contracts/carrot-app/tests/common.rs @@ -11,7 +11,7 @@ use carrot_app::state::ConfigBase; use carrot_app::yield_sources::osmosis_cl_pool::ConcentratedPoolParamsBase; use carrot_app::yield_sources::yield_type::YieldParamsBase; use carrot_app::yield_sources::{AssetShare, StrategyBase, StrategyElementBase, YieldSourceBase}; -use cosmwasm_std::{coin, coins, Coins, Decimal, Uint128, Uint64}; +use cosmwasm_std::{coin, coins, Coins, Decimal, Uint64}; use cw_asset::AssetInfoUnchecked; use cw_orch::environment::MutCwEnv; use cw_orch::osmosis_test_tube::osmosis_test_tube::Gamm; @@ -50,6 +50,8 @@ pub const SPREAD_FACTOR: &str = "0.01"; pub const INITIAL_LOWER_TICK: i64 = -100000; pub const INITIAL_UPPER_TICK: i64 = 10000; +pub const EXECUTOR_REWARD: Decimal = Decimal::percent(30); + // Deploys abstract and other contracts pub fn deploy( mut chain: Chain, @@ -126,11 +128,7 @@ pub fn deploy( autocompound_config: AutocompoundConfigBase { cooldown_seconds: Uint64::new(300), rewards: AutocompoundRewardsConfigBase { - gas_asset: AssetEntry::new(REWARD_ASSET), - swap_asset: AssetEntry::new(USDC), - reward: Uint128::new(1000), - min_gas_balance: Uint128::new(2000), - max_gas_balance: Uint128::new(10000), + reward_percent: EXECUTOR_REWARD, _phantom: std::marker::PhantomData, }, }, From c300a4907325e0326a619d1e55fe23dc012a7fdb Mon Sep 17 00:00:00 2001 From: Kayanski Date: Fri, 19 Apr 2024 15:06:51 +0000 Subject: [PATCH 41/42] Fix autocompound test --- contracts/carrot-app/tests/autocompound.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/carrot-app/tests/autocompound.rs b/contracts/carrot-app/tests/autocompound.rs index 29455910..1d94ca1a 100644 --- a/contracts/carrot-app/tests/autocompound.rs +++ b/contracts/carrot-app/tests/autocompound.rs @@ -118,13 +118,13 @@ fn stranger_autocompound() -> anyhow::Result<()> { chain.bank_send( account.proxy.addr_str()?, vec![ - coin(200_000, USDC.to_owned()), - coin(200_000, USDT.to_owned()), + coin(2_000_000, USDC.to_owned()), + coin(2_000_000, USDT.to_owned()), ], )?; for _ in 0..10 { - dex.ans_swap((USDC, 50_000), USDT, DEX_NAME.to_string(), &account)?; - dex.ans_swap((USDT, 50_000), USDC, DEX_NAME.to_string(), &account)?; + dex.ans_swap((USDC, 500_000), USDT, DEX_NAME.to_string(), &account)?; + dex.ans_swap((USDT, 500_000), USDC, DEX_NAME.to_string(), &account)?; } // Check autocompound adds liquidity from the rewards, user balance remain unchanged From 923d49aec8385ab1f9cf276263cfe2a984b9e77e Mon Sep 17 00:00:00 2001 From: Kayanski Date: Fri, 19 Apr 2024 15:32:32 +0000 Subject: [PATCH 42/42] Changed autocompound strartegy on bot --- Cargo.toml | 2 + bot/Cargo.toml | 2 + bot/src/bot.rs | 43 ++++++++++++++----- contracts/carrot-app/Cargo.toml | 2 +- contracts/carrot-app/src/handlers/query.rs | 49 ++++++++++++++++++++-- contracts/carrot-app/src/msg.rs | 6 ++- contracts/carrot-app/tests/autocompound.rs | 21 ++++------ 7 files changed, 96 insertions(+), 29 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 66125e42..2c4ea407 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,8 @@ resolver = "2" [workspace.dependencies] cw-orch = "0.20.1" +cosmwasm-std = { version = "1.5" } + abstract-app = { version = "0.21.0" } abstract-interface = { version = "0.21.0" } abstract-dex-adapter = { git = "https://github.com/abstractsdk/abstract.git" } diff --git a/bot/Cargo.toml b/bot/Cargo.toml index bb35d6d6..26902ac9 100644 --- a/bot/Cargo.toml +++ b/bot/Cargo.toml @@ -24,3 +24,5 @@ humantime = "2.1.0" prometheus = "0.13.2" tokio = "1.26.0" warp = "0.3.6" +serde_json = "1.0.116" +cosmwasm-std = { workspace = true } diff --git a/bot/src/bot.rs b/bot/src/bot.rs index 1982f630..c4f88ee9 100644 --- a/bot/src/bot.rs +++ b/bot/src/bot.rs @@ -1,12 +1,16 @@ use abstract_client::{AbstractClient, AccountSource, Environment}; use carrot_app::{ - msg::{AppExecuteMsg, CompoundStatusResponse, ExecuteMsg}, + msg::{AppExecuteMsg, ExecuteMsg}, AppInterface, }; use cosmos_sdk_proto::{ - cosmwasm::wasm::v1::{query_client::QueryClient, QueryContractsByCodeRequest}, + cosmwasm::wasm::v1::{ + query_client::QueryClient, MsgExecuteContract, QueryContractsByCodeRequest, + }, traits::Message as _, + Any, }; +use cosmwasm_std::Uint128; use cw_orch::{ anyhow, daemon::{queriers::Authz, Daemon}, @@ -208,17 +212,36 @@ fn autocompound_instance(daemon: &Daemon, instance: (&str, &Addr)) -> anyhow::Re let app = AppInterface::new(id, daemon.clone()); app.set_address(address); use carrot_app::AppQueryMsgFns; - let resp: CompoundStatusResponse = app.compound_status()?; + let compound = app.compound_status()?; // TODO: ensure rewards > tx fee - if resp.status.is_ready() { - // Execute autocompound - let daemon = daemon.rebuild().authz_granter(address).build()?; - daemon.execute( - &ExecuteMsg::from(AppExecuteMsg::Autocompound {}), - &[], - address, + // Ensure that autocompound is allowed on the contract + if compound.status.is_ready() { + // We simulate the transaction + let msg = ExecuteMsg::from(AppExecuteMsg::Autocompound {}); + let exec_msg: MsgExecuteContract = MsgExecuteContract { + sender: daemon.sender().to_string(), + contract: address.as_str().parse()?, + msg: serde_json::to_vec(&msg)?, + funds: vec![], + }; + let tx_simulation = daemon.rt_handle.block_on( + daemon + .daemon + .sender + .simulate(vec![Any::from_msg(&exec_msg)?], None), )?; + let fee_value = app.funds_value(vec![tx_simulation.1])?; + let profit = compound + .execution_rewards + .total_value + .checked_sub(fee_value.total_value) + .unwrap_or_default(); + if profit > Uint128::zero() { + // If it's worth it, we autocompound + let daemon = daemon.rebuild().authz_granter(address).build()?; + daemon.execute(&msg, &[], address)?; + } } Ok(()) } diff --git a/contracts/carrot-app/Cargo.toml b/contracts/carrot-app/Cargo.toml index 286b6688..a017cc17 100644 --- a/contracts/carrot-app/Cargo.toml +++ b/contracts/carrot-app/Cargo.toml @@ -40,7 +40,7 @@ schema = ["abstract-app/schema"] [dependencies] cw-utils = { version = "1.0.3" } -cosmwasm-std = { version = "1.2" } +cosmwasm-std = { workspace = true } cosmwasm-schema = { version = "1.2" } cw-controllers = { version = "1.0.1" } cw-storage-plus = "1.2.0" diff --git a/contracts/carrot-app/src/handlers/query.rs b/contracts/carrot-app/src/handlers/query.rs index 2d8dc1a3..13348640 100644 --- a/contracts/carrot-app/src/handlers/query.rs +++ b/contracts/carrot-app/src/handlers/query.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{to_json_binary, Binary, Coins, Decimal, Deps, Env, Uint128}; +use cosmwasm_std::{to_json_binary, Binary, Coin, Coins, Decimal, Deps, Env, Uint128}; use crate::autocompound::get_autocompound_status; use crate::exchange_rate::query_exchange_rate; @@ -36,13 +36,14 @@ pub fn query_handler(deps: Deps, env: Env, app: &App, msg: AppQueryMsg) -> AppRe AppQueryMsg::UpdateStrategyPreview { strategy, funds } => { to_json_binary(&update_strategy_preview(deps, funds, strategy, app)?) } + AppQueryMsg::FundsValue { funds } => to_json_binary(&query_funds_value(deps, funds, app)?), } .map_err(Into::into) } /// Gets the status of the compounding logic of the application /// Accounts for the user's ability to pay for the gas fees of executing the contract. -fn query_compound_status(deps: Deps, env: Env, _app: &App) -> AppResult { +fn query_compound_status(deps: Deps, env: Env, app: &App) -> AppResult { let config = CONFIG.load(deps.storage)?; let status = get_autocompound_status( deps.storage, @@ -50,7 +51,23 @@ fn query_compound_status(deps: Deps, env: Env, _app: &App) -> AppResult = all_rewards + .iter() + .flat_map(|a| { + let reward_amount = a.amount * config.autocompound_config.rewards.reward_percent; + + Some(Coin::new(reward_amount.into(), a.denom.clone())) + }) + .collect(); + + Ok(CompoundStatusResponse { + status, + execution_rewards: query_funds_value(deps, funds, app)?, + }) } pub fn query_strategy(deps: Deps) -> AppResult { @@ -110,8 +127,14 @@ fn query_rewards(deps: Deps, app: &App) -> AppResult { Ok::<_, AppError>(()) })?; + let mut total_value = Uint128::zero(); + for fund in &rewards { + let exchange_rate = query_exchange_rate(deps, fund.denom.clone(), app)?; + total_value += fund.amount * exchange_rate; + } + Ok(AvailableRewardsResponse { - available_rewards: rewards.into(), + available_rewards: query_funds_value(deps, rewards.into(), app)?, }) } @@ -145,6 +168,24 @@ pub fn query_positions(deps: Deps, app: &App) -> AppResult { .collect::>()?, }) } + +pub fn query_funds_value( + deps: Deps, + funds: Vec, + app: &App, +) -> AppResult { + let mut total_value = Uint128::zero(); + for fund in &funds { + let exchange_rate = query_exchange_rate(deps, fund.denom.clone(), app)?; + total_value += fund.amount * exchange_rate; + } + + Ok(AssetsBalanceResponse { + balances: funds, + total_value, + }) +} + pub fn withdraw_share( deps: Deps, amount: Option, diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index 504ab8a1..1233cb17 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -149,6 +149,9 @@ pub enum AppQueryMsg { funds: Vec, strategy: StrategyUnchecked, }, + + #[returns(AssetsBalanceResponse)] + FundsValue { funds: Vec }, } #[cosmwasm_schema::cw_serde] @@ -160,7 +163,7 @@ pub struct BalanceResponse { } #[cosmwasm_schema::cw_serde] pub struct AvailableRewardsResponse { - pub available_rewards: Vec, + pub available_rewards: AssetsBalanceResponse, } #[cw_serde] @@ -189,6 +192,7 @@ pub struct PositionResponse { #[cw_serde] pub struct CompoundStatusResponse { pub status: CompoundStatus, + pub execution_rewards: AssetsBalanceResponse, } #[cw_serde] diff --git a/contracts/carrot-app/tests/autocompound.rs b/contracts/carrot-app/tests/autocompound.rs index 1d94ca1a..bae7e06b 100644 --- a/contracts/carrot-app/tests/autocompound.rs +++ b/contracts/carrot-app/tests/autocompound.rs @@ -4,7 +4,7 @@ use crate::common::{setup_test_tube, DEX_NAME, EXECUTOR_REWARD, GAS_DENOM, LOTS, use abstract_app::abstract_interface::{Abstract, AbstractAccount}; use carrot_app::msg::{ AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse, AvailableRewardsResponse, - CompoundStatus, CompoundStatusResponse, + CompoundStatus, }; use cosmwasm_std::{coin, coins}; use cw_orch::osmosis_test_tube::osmosis_test_tube::Account; @@ -47,8 +47,8 @@ fn check_autocompound() -> anyhow::Result<()> { // Check autocompound adds liquidity from the rewards and user balance remain unchanged // Check it has some rewards to autocompound first - let rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; - assert!(!rewards.available_rewards.is_empty()); + let rewards = carrot_app.available_rewards()?; + assert!(!rewards.available_rewards.balances.is_empty()); // Save balances let balance_before_autocompound: AssetsBalanceResponse = carrot_app.balance()?; @@ -87,7 +87,7 @@ fn check_autocompound() -> anyhow::Result<()> { assert!(balance_usdt_after_autocompound.amount >= balance_usdt_before_autocompound.amount,); // Check it used all of the rewards let rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; - assert!(rewards.available_rewards.is_empty()); + assert!(rewards.available_rewards.balances.is_empty()); Ok(()) } @@ -132,7 +132,7 @@ fn stranger_autocompound() -> anyhow::Result<()> { // Check it has some rewards to autocompound first let available_rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; - assert!(!available_rewards.available_rewards.is_empty()); + assert!(!available_rewards.available_rewards.balances.is_empty()); // Save balances let balance_before_autocompound: AssetsBalanceResponse = carrot_app.balance()?; @@ -141,12 +141,7 @@ fn stranger_autocompound() -> anyhow::Result<()> { chain.wait_seconds(300)?; // Check query is able to compute rewards, when swap is required let compound_status = carrot_app.compound_status()?; - assert_eq!( - compound_status, - CompoundStatusResponse { - status: CompoundStatus::Ready {}, - } - ); + assert_eq!(compound_status.status, CompoundStatus::Ready {},); carrot_app.call_as(&stranger).autocompound()?; // Save new balances @@ -157,11 +152,11 @@ fn stranger_autocompound() -> anyhow::Result<()> { // Check it used all of the rewards let rewards: AvailableRewardsResponse = carrot_app.available_rewards()?; - assert!(rewards.available_rewards.is_empty()); + assert!(rewards.available_rewards.balances.is_empty()); // Check stranger gets rewarded - for reward in available_rewards.available_rewards { + for reward in available_rewards.available_rewards.balances { let stranger_reward_balance = chain.query_balance(stranger.address().as_str(), &reward.denom)?; assert_eq!(stranger_reward_balance, reward.amount * EXECUTOR_REWARD);