diff --git a/contracts/carrot-app/examples/localnet_test.rs b/contracts/carrot-app/examples/localnet_test.rs index 76c9b314..e8acbd9f 100644 --- a/contracts/carrot-app/examples/localnet_test.rs +++ b/contracts/carrot-app/examples/localnet_test.rs @@ -46,7 +46,7 @@ fn main() -> anyhow::Result<()> { carrot.deposit(coins(10_000, "uosmo"), None)?; carrot.update_strategy(coins(10_000, "uosmo"), five_strategy())?; - carrot.withdraw(None)?; + carrot.withdraw(None, None)?; carrot.deposit(coins(10_000, "uosmo"), None)?; Ok(()) diff --git a/contracts/carrot-app/src/handlers/execute.rs b/contracts/carrot-app/src/handlers/execute.rs index 40026c9f..73aac8f9 100644 --- a/contracts/carrot-app/src/handlers/execute.rs +++ b/contracts/carrot-app/src/handlers/execute.rs @@ -1,22 +1,26 @@ -use super::internal::execute_internal_action; +use super::{internal::execute_internal_action, swap_helpers::MAX_SPREAD}; use crate::{ autocompound::AutocompoundState, check::Checkable, contract::{App, AppResult}, distribution::deposit::generate_deposit_strategy, error::AppError, + exchange_rate::query_exchange_rate, handlers::query::query_balance, helpers::assert_contract, msg::{AppExecuteMsg, ExecuteMsg}, state::{AUTOCOMPOUND_STATE, CONFIG, STRATEGY_CONFIG}, yield_sources::{AssetShare, Strategy, StrategyUnchecked}, }; -use abstract_app::abstract_sdk::features::AbstractResponse; +use abstract_app::traits::AbstractNameService; +use abstract_app::{abstract_sdk::features::AbstractResponse, objects::AssetEntry}; +use abstract_dex_adapter::DexInterface; use abstract_sdk::ExecutorMsg; use cosmwasm_std::{ to_json_binary, Coin, Coins, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Uint128, WasmMsg, }; +use cw_asset::Asset; pub fn execute_handler( deps: DepsMut, @@ -30,7 +34,9 @@ pub fn execute_handler( funds, yield_sources_params, } => deposit(deps, env, info, funds, yield_sources_params, app), - AppExecuteMsg::Withdraw { amount } => withdraw(deps, env, info, amount, app), + AppExecuteMsg::Withdraw { value, swap_to } => { + withdraw(deps, env, info, value, swap_to, app) + } AppExecuteMsg::Autocompound {} => autocompound(deps, env, info, app), AppExecuteMsg::UpdateStrategy { strategy, funds } => { update_strategy(deps, env, info, strategy, funds, app) @@ -81,12 +87,13 @@ fn withdraw( env: Env, info: MessageInfo, amount: Option, + swap_to: Option, app: App, ) -> AppResult { // Only the authorized addresses (admin ?) can withdraw app.admin.assert_admin(deps.as_ref(), &info.sender)?; - let msgs = _inner_withdraw(deps, &env, amount, &app)?; + let msgs = _inner_withdraw(deps, &env, amount, swap_to, &app)?; Ok(app.response("withdraw").add_messages(msgs)) } @@ -225,8 +232,9 @@ fn _inner_withdraw( deps: DepsMut, _env: &Env, value: Option, + swap_to: 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| { @@ -240,10 +248,40 @@ fn _inner_withdraw( .transpose()?; // We withdraw the necessary share from all registered investments - let withdraw_msgs = - STRATEGY_CONFIG - .load(deps.storage)? - .withdraw(deps.as_ref(), withdraw_share, app)?; + let mut withdraw_msgs: Vec = STRATEGY_CONFIG + .load(deps.storage)? + .withdraw(deps.as_ref(), withdraw_share, app)? + .into_iter() + .map(Into::into) + .collect(); + + if let Some(swap_to) = swap_to { + let withdraw_preview = STRATEGY_CONFIG.load(deps.storage)?.withdraw_preview( + deps.as_ref(), + withdraw_share, + app, + )?; + + // We swap all withdraw_preview coins to the swap asset + let config = CONFIG.load(deps.storage)?; + let ans = app.name_service(deps.as_ref()); + let dex = app.ans_dex(deps.as_ref(), config.dex); + withdraw_preview.into_iter().try_for_each(|fund| { + let asset = ans.query(&Asset::from(fund.clone()))?; + if asset.name != swap_to { + let swap_msg = dex.swap( + asset, + swap_to.clone(), + Some(MAX_SPREAD), + Some(query_exchange_rate(deps.as_ref(), fund.denom, app)?), + )?; + withdraw_msgs.push(swap_msg); + } + Ok::<_, AppError>(()) + })?; + } + + deps.api.debug(&format!("{:?}", withdraw_msgs)); - Ok(withdraw_msgs.into_iter().collect()) + Ok(withdraw_msgs) } diff --git a/contracts/carrot-app/src/handlers/swap_helpers.rs b/contracts/carrot-app/src/handlers/swap_helpers.rs index b18c75b3..270036e0 100644 --- a/contracts/carrot-app/src/handlers/swap_helpers.rs +++ b/contracts/carrot-app/src/handlers/swap_helpers.rs @@ -1,7 +1,7 @@ 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 MAX_SPREAD: Decimal = Decimal::percent(20); pub const DEFAULT_SLIPPAGE: Decimal = Decimal::permille(5); use crate::contract::{App, AppResult, OSMOSIS}; @@ -19,12 +19,7 @@ pub(crate) fn swap_msg( } 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, - )?; + let swap_msg = dex.swap(offer_asset, ask_asset, Some(MAX_SPREAD), None)?; Ok(Some(swap_msg)) } diff --git a/contracts/carrot-app/src/msg.rs b/contracts/carrot-app/src/msg.rs index 44944fc0..2f579078 100644 --- a/contracts/carrot-app/src/msg.rs +++ b/contracts/carrot-app/src/msg.rs @@ -1,3 +1,4 @@ +use abstract_app::objects::AssetEntry; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{wasm_execute, Coin, CosmosMsg, Decimal, Env, Uint128, Uint64}; use cw_asset::AssetBase; @@ -47,7 +48,10 @@ pub enum AppExecuteMsg { }, /// Partial withdraw of the funds available on the app /// If amount is omitted, withdraws everything that is on the app - Withdraw { amount: Option }, + Withdraw { + value: Option, + swap_to: Option, + }, /// Auto-compounds the pool rewards into the pool Autocompound {}, /// Rebalances all investments according to a new balance strategy diff --git a/contracts/carrot-app/tests/common.rs b/contracts/carrot-app/tests/common.rs index 503cc9dd..e13b2afb 100644 --- a/contracts/carrot-app/tests/common.rs +++ b/contracts/carrot-app/tests/common.rs @@ -53,8 +53,8 @@ pub fn deploy( gas_pool_id: u64, initial_deposit: Option>, ) -> anyhow::Result>> { - let asset0 = USDT.to_owned(); - let asset1 = USDC.to_owned(); + let asset0 = USDC.to_owned(); + let asset1 = USDT.to_owned(); // We register the pool inside the Abstract ANS let client = AbstractClient::builder(chain.clone()) .dex(DEX_NAME) @@ -89,7 +89,6 @@ 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 diff --git a/contracts/carrot-app/tests/deposit_withdraw.rs b/contracts/carrot-app/tests/deposit_withdraw.rs index fbb3ed44..b0f6be80 100644 --- a/contracts/carrot-app/tests/deposit_withdraw.rs +++ b/contracts/carrot-app/tests/deposit_withdraw.rs @@ -1,6 +1,7 @@ mod common; use crate::common::{setup_test_tube, USDC, USDT}; +use abstract_app::objects::AssetEntry; use abstract_client::Application; use carrot_app::{ msg::{AppExecuteMsgFns, AppQueryMsgFns, AssetsBalanceResponse}, @@ -12,7 +13,7 @@ use carrot_app::{ AppInterface, }; use common::{INITIAL_LOWER_TICK, INITIAL_UPPER_TICK}; -use cosmwasm_std::{coin, coins, Decimal, Uint128}; +use cosmwasm_std::{coin, coins, Coins, Decimal, Uint128}; use cw_orch::{anyhow, prelude::*}; fn query_balances( @@ -96,7 +97,7 @@ fn withdraw_position() -> anyhow::Result<()> { // 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(Some(half_of_liquidity))?; + carrot_app.withdraw(None, Some(half_of_liquidity))?; let balance_usdc_after_half_withdraw = chain .bank_querier() @@ -113,7 +114,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(None)?; + carrot_app.withdraw(None, None)?; let balance_usdc_after_full_withdraw = chain .bank_querier() .balance(chain.sender(), Some(USDT.to_owned()))? @@ -312,6 +313,46 @@ fn create_position_on_instantiation() -> anyhow::Result<()> { Ok(()) } +#[test] +fn withdraw_to() -> 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(); + + let balances_before = query_balances(&carrot_app)?; + chain.add_balance( + carrot_app.account().proxy()?.to_string(), + deposit_coins.clone(), + )?; + + // Do the deposit + carrot_app.deposit(deposit_coins.clone(), None)?; + + // Check almost everything landed and there is 2 types of coins + let balances_after = query_balances(&carrot_app)?; + assert!(balances_before < balances_after); + + let balances_before = chain.query_all_balances(carrot_app.account().proxy()?.as_str())?; + // Withdraw everything + carrot_app.withdraw(Some(AssetEntry::new(USDT)), None)?; + + // Check that the proxy only has one asset type back + let mut balances_after: Coins = chain + .query_all_balances(carrot_app.account().proxy()?.as_str())? + .try_into()?; + + for f in balances_before { + balances_after.sub(f)?; + } + // In balances before remains only the withdrawn funds + assert_eq!(balances_after.len(), 1); + + Ok(()) +} + // #[test] // fn withdraw_after_user_withdraw_liquidity_manually() -> anyhow::Result<()> { // let (_, carrot_app) = setup_test_tube(true)?;