diff --git a/smart-contracts/osmosis/contracts/cl-vault/src/contract.rs b/smart-contracts/osmosis/contracts/cl-vault/src/contract.rs index 682a78ce2..6ff9e1b6f 100644 --- a/smart-contracts/osmosis/contracts/cl-vault/src/contract.rs +++ b/smart-contracts/osmosis/contracts/cl-vault/src/contract.rs @@ -28,8 +28,8 @@ use crate::vault::{ handle_merge_withdraw_position_reply, }, range::{ - execute_update_range, handle_initial_create_position_reply, - handle_iteration_create_position_reply, handle_swap_reply, handle_withdraw_position_reply, + execute_update_range, handle_create_position, handle_swap_reply, + handle_withdraw_position_reply, }, swap::execute_swap_non_vault_funds, withdraw::{execute_withdraw, handle_withdraw_user_reply}, @@ -200,12 +200,7 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result handle_collect_incentives_reply(deps, env, msg.result), Replies::CollectSpreadRewards => handle_collect_spread_rewards_reply(deps, env, msg.result), Replies::WithdrawPosition => handle_withdraw_position_reply(deps, env), - Replies::RangeInitialCreatePosition => { - handle_initial_create_position_reply(deps, env, msg.result) - } - Replies::RangeIterationCreatePosition => { - handle_iteration_create_position_reply(deps, env, msg.result) - } + Replies::CreatePosition => handle_create_position(deps, env, msg.result), Replies::Swap => handle_swap_reply(deps, env), Replies::Merge => handle_merge_reply(deps, env, msg.result), Replies::CreateDenom => handle_create_denom_reply(deps, msg.result), diff --git a/smart-contracts/osmosis/contracts/cl-vault/src/reply.rs b/smart-contracts/osmosis/contracts/cl-vault/src/reply.rs index bbddb1e06..5ad961bbc 100644 --- a/smart-contracts/osmosis/contracts/cl-vault/src/reply.rs +++ b/smart-contracts/osmosis/contracts/cl-vault/src/reply.rs @@ -12,10 +12,7 @@ pub enum Replies { // withdraw position WithdrawPosition, - // create position in the modify range inital step - RangeInitialCreatePosition, - // create position in the modify range iteration step - RangeIterationCreatePosition, + CreatePosition, // swap Swap, /// Merge positions, used to merge positions diff --git a/smart-contracts/osmosis/contracts/cl-vault/src/vault/concentrated_liquidity.rs b/smart-contracts/osmosis/contracts/cl-vault/src/vault/concentrated_liquidity.rs index a0bcea70a..ee998f6f8 100644 --- a/smart-contracts/osmosis/contracts/cl-vault/src/vault/concentrated_liquidity.rs +++ b/smart-contracts/osmosis/contracts/cl-vault/src/vault/concentrated_liquidity.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Coin, Decimal256, DepsMut, Env, QuerierWrapper, Storage, Uint128}; +use cosmwasm_std::{Coin, Decimal256, DepsMut, Env, QuerierWrapper, Storage, Uint128, Uint256}; use osmosis_std::types::osmosis::concentratedliquidity::v1beta1::{ ConcentratedliquidityQuerier, FullPositionBreakdown, MsgCreatePosition, MsgWithdrawPosition, Pool, @@ -108,6 +108,65 @@ pub fn _may_get_position( } } +// see https://uniswap.org/whitepaper-v3.pdf for below formulas (eq 6.29 & 6.30) +pub fn get_liquidity_for_base_token( + amount: Uint256, + sqrt_p: Decimal256, + sqrt_pl: Decimal256, + sqrt_pu: Decimal256, +) -> Result { + debug_assert!( + sqrt_p < sqrt_pu, + "can't compute liquidity if sqrt_p >= sqrt_pu" + ); + if sqrt_p >= sqrt_pu { + return Ok(Uint256::MAX); + } + let sqrt_p = std::cmp::max(sqrt_p, sqrt_pl); + let delta_p = sqrt_pu - sqrt_p; + Ok(amount.checked_mul_floor(sqrt_pu.checked_mul(sqrt_p)?.checked_div(delta_p)?)?) +} + +pub fn get_liquidity_for_quote_token( + amount: Uint256, + sqrt_p: Decimal256, + sqrt_pl: Decimal256, + sqrt_pu: Decimal256, +) -> Result { + debug_assert!( + sqrt_p > sqrt_pl, + "can't compute liquidity if sqrt_p <= sqrt_pl" + ); + if sqrt_p <= sqrt_pl { + return Ok(Uint256::MAX); + } + let sqrt_p = std::cmp::min(sqrt_p, sqrt_pu); + let delta_p = sqrt_p - sqrt_pl; + Ok(amount.checked_div_floor(delta_p)?) +} + +pub fn get_amount_from_liquidity_for_base_token( + liq: Uint256, + sqrt_p: Decimal256, + sqrt_pl: Decimal256, + sqrt_pu: Decimal256, +) -> Result { + let sqrt_p = std::cmp::max(sqrt_p, sqrt_pl); + let delta_p = sqrt_pu.checked_sub(sqrt_p).unwrap_or_default(); + Ok(liq.checked_mul_floor(delta_p.checked_div(sqrt_pu.checked_mul(sqrt_p)?)?)?) +} + +pub fn get_amount_from_liquidity_for_quote_token( + liq: Uint256, + sqrt_p: Decimal256, + sqrt_pl: Decimal256, + sqrt_pu: Decimal256, +) -> Result { + let sqrt_p = std::cmp::min(sqrt_p, sqrt_pu); + let delta_p = sqrt_p.checked_sub(sqrt_pl).unwrap_or_default(); + Ok(liq.checked_mul_floor(delta_p)?) +} + #[cfg(test)] mod tests { use crate::{ @@ -115,7 +174,7 @@ mod tests { test_helpers::QuasarQuerier, }; use cosmwasm_std::{ - coin, + assert_approx_eq, coin, testing::{mock_dependencies, mock_env}, Coin, Uint128, }; @@ -225,4 +284,174 @@ mod tests { } ); } + + #[test] + fn test_get_amount_from_liquidity_for_base_token_if_price_above_range() { + let liq = Uint256::from(50u64); + let sqrt_pl = Decimal256::percent(50); + let sqrt_pu = Decimal256::percent(100); + let sqrt_p = Decimal256::percent(200); + let amount = + get_amount_from_liquidity_for_base_token(liq, sqrt_p, sqrt_pl, sqrt_pu).unwrap(); + let expected_amount = Uint256::zero(); + assert_eq!(amount, expected_amount); + } + + #[test] + fn test_get_amount_from_liquidity_for_base_token_if_price_in_range() { + let liq = Uint256::from(50u64); + let sqrt_pl = Decimal256::percent(25); + let sqrt_pu = Decimal256::percent(100); + let sqrt_p = Decimal256::percent(50); + let amount = + get_amount_from_liquidity_for_base_token(liq, sqrt_p, sqrt_pl, sqrt_pu).unwrap(); + let expected_amount = liq; + assert_eq!(amount, expected_amount); + } + + #[test] + fn test_get_amount_from_liquidity_for_base_token_if_price_below_range() { + let liq = Uint256::from(50u64); + let sqrt_pl = Decimal256::percent(50); + let sqrt_pu = Decimal256::percent(100); + let sqrt_p = Decimal256::percent(25); + let amount = + get_amount_from_liquidity_for_base_token(liq, sqrt_p, sqrt_pl, sqrt_pu).unwrap(); + let expected_amount = liq; + assert_eq!(amount, expected_amount); + } + + #[test] + fn test_get_amount_from_liquidity_for_quote_token_if_price_below_range() { + let liq = Uint256::from(50u64); + let sqrt_pl = Decimal256::percent(50); + let sqrt_pu = Decimal256::percent(100); + let sqrt_p = Decimal256::percent(25); + let amount = + get_amount_from_liquidity_for_quote_token(liq, sqrt_p, sqrt_pl, sqrt_pu).unwrap(); + let expected_amount = Uint256::zero(); + assert_eq!(amount, expected_amount); + } + + #[test] + fn test_get_amount_from_liquidity_for_quote_token_if_price_in_range() { + let liq = Uint256::from(50u64); + let sqrt_pl = Decimal256::percent(25); + let sqrt_pu = Decimal256::percent(100); + let sqrt_p = Decimal256::percent(75); + let amount = + get_amount_from_liquidity_for_quote_token(liq, sqrt_p, sqrt_pl, sqrt_pu).unwrap(); + let expected_amount = Uint256::from(25u64); + assert_eq!(amount, expected_amount); + } + + #[test] + fn test_get_amount_from_liquidity_for_quote_token_if_price_above_range() { + let liq = Uint256::from(50u64); + let sqrt_pl = Decimal256::percent(50); + let sqrt_pu = Decimal256::percent(100); + let sqrt_p = Decimal256::percent(200); + let amount = + get_amount_from_liquidity_for_quote_token(liq, sqrt_p, sqrt_pl, sqrt_pu).unwrap(); + let expected_amount = Uint256::from(25u64); + assert_eq!(amount, expected_amount); + } + + #[test] + fn test_get_liquidity_from_amount_for_base_token_if_price_in_range() { + let amount = Uint256::from(150u64); + let sqrt_pl = Decimal256::percent(10); + let sqrt_pu = Decimal256::percent(100); + let sqrt_p = Decimal256::percent(25); + let liq = get_liquidity_for_base_token(amount, sqrt_p, sqrt_pl, sqrt_pu).unwrap(); + let expected_liq = Uint256::from(49u64); + assert_eq!(liq, expected_liq); + } + + #[test] + fn test_get_liquidity_from_amount_for_base_token_if_price_below_range() { + let amount = Uint256::from(150u64); + let sqrt_pl = Decimal256::percent(25); + let sqrt_pu = Decimal256::percent(100); + let sqrt_p = Decimal256::percent(10); + let liq = get_liquidity_for_base_token(amount, sqrt_p, sqrt_pl, sqrt_pu).unwrap(); + let expected_liq = Uint256::from(49u64); + assert_eq!(liq, expected_liq); + } + + #[test] + fn test_get_liquidity_from_amount_for_quote_token_if_price_in_range() { + let amount = Uint256::from(150u64); + let sqrt_pl = Decimal256::percent(10); + let sqrt_pu = Decimal256::percent(100); + let sqrt_p = Decimal256::percent(60); + let liq = get_liquidity_for_quote_token(amount, sqrt_p, sqrt_pl, sqrt_pu).unwrap(); + let expected_liq = Uint256::from(300u64); + assert_eq!(liq, expected_liq); + } + + #[test] + fn test_get_liquidity_from_amount_for_quote_token_if_price_above_range() { + let amount = Uint256::from(150u64); + let sqrt_pl = Decimal256::percent(10); + let sqrt_pu = Decimal256::percent(60); + let sqrt_p = Decimal256::percent(100); + let liq = get_liquidity_for_quote_token(amount, sqrt_p, sqrt_pl, sqrt_pu).unwrap(); + let expected_liq = Uint256::from(300u64); + assert_eq!(liq, expected_liq); + } + + // tests + + #[test] + fn test_get_liquidity_for_base() { + let amount = Uint256::from(1_000_000u64); + let sqrt_pl = Decimal256::percent(65); + let sqrt_pu = Decimal256::percent(130); + let sqrt_p = Decimal256::one(); + let liq = get_liquidity_for_base_token(amount, sqrt_p, sqrt_pl, sqrt_pu).unwrap(); + let expected_liq = Uint256::from(4_333_333u64); + assert_eq!(liq, expected_liq); + + let final_amount: Uint128 = + get_amount_from_liquidity_for_base_token(liq, sqrt_p, sqrt_pl, sqrt_pu) + .unwrap() + .try_into() + .unwrap(); + assert_approx_eq!(final_amount, amount.try_into().unwrap(), "0.000001"); + + let used_liquidity = Uint256::from(2_857_142u64); + let residual_liquidity = Uint256::from(4_333_333u64) - used_liquidity; + let residual_amount: Uint128 = + get_amount_from_liquidity_for_base_token(residual_liquidity, sqrt_p, sqrt_pl, sqrt_pu) + .unwrap() + .try_into() + .unwrap(); + let expected_residual_amount = Uint128::from(340659u64); + assert_eq!(residual_amount, expected_residual_amount); + + let used_amount = + get_amount_from_liquidity_for_base_token(used_liquidity, sqrt_p, sqrt_pl, sqrt_pu) + .unwrap(); + let residual_amount: Uint128 = (amount - used_amount).try_into().unwrap(); + let expected_residual_amount = Uint128::from(340660u64); + assert_eq!(residual_amount, expected_residual_amount); + } + #[test] + fn test_get_liquidity_for_quote() { + let amount = Uint256::from(1_000_000u64); + let sqrt_pl = Decimal256::percent(65); + let sqrt_pu = Decimal256::percent(130); + let sqrt_p = Decimal256::one(); + let liq = get_liquidity_for_quote_token(amount, sqrt_p, sqrt_pl, sqrt_pu).unwrap(); + let expected_liq = Uint256::from(2_857_142u64); + assert_eq!(liq, expected_liq); + + let final_amount: Uint128 = + get_amount_from_liquidity_for_quote_token(liq, sqrt_p, sqrt_pl, sqrt_pu) + .unwrap() + .try_into() + .unwrap(); + assert_approx_eq!(final_amount, amount.try_into().unwrap(), "0.000001"); + } } diff --git a/smart-contracts/osmosis/contracts/cl-vault/src/vault/range.rs b/smart-contracts/osmosis/contracts/cl-vault/src/vault/range.rs index 843df79b6..da224d04b 100644 --- a/smart-contracts/osmosis/contracts/cl-vault/src/vault/range.rs +++ b/smart-contracts/osmosis/contracts/cl-vault/src/vault/range.rs @@ -4,22 +4,25 @@ use crate::{ get_single_sided_deposit_0_to_1_swap_amount, get_single_sided_deposit_1_to_0_swap_amount, get_twap_price, get_unused_pair_balances, }, - math::tick::price_to_tick, - msg::{ExecuteMsg, MergePositionMsg}, + math::tick::{price_to_tick, tick_to_price}, reply::Replies, state::{ ModifyRangeState, Position, SwapDepositMergeState, DEX_ROUTER, MODIFY_RANGE_STATE, POOL_CONFIG, POSITION, SWAP_DEPOSIT_MERGE_STATE, }, vault::{ - concentrated_liquidity::{create_position, get_cl_pool_info, get_position}, + concentrated_liquidity::{ + create_position, get_amount_from_liquidity_for_base_token, + get_amount_from_liquidity_for_quote_token, get_cl_pool_info, + get_liquidity_for_base_token, get_liquidity_for_quote_token, get_position, + }, swap::{estimate_swap_min_out_amount, swap_msg}, }, ContractError, }; use cosmwasm_std::{ - attr, coin, to_json_binary, CheckedMultiplyFractionError, Coin, Decimal, Decimal256, DepsMut, - Env, Fraction, MessageInfo, Response, SubMsg, SubMsgResult, Uint128, + coin, Decimal, Decimal256, DepsMut, Env, Fraction, MessageInfo, Response, StdResult, SubMsg, + SubMsgResult, Uint128, Uint256, }; use osmosis_std::types::osmosis::{ concentratedliquidity::v1beta1::{MsgCreatePositionResponse, MsgWithdrawPosition}, @@ -72,16 +75,12 @@ pub fn execute_update_range( }), )?; - // Load the current Position to set new join_time and claim_after, leaving current position_id unchanged. - let position_state = POSITION.load(deps.storage)?; - POSITION.save( - deps.storage, - &Position { - position_id: position_state.position_id, - join_time: env.block.time.seconds(), - claim_after, - }, - )?; + POSITION.update(deps.storage, |position| -> StdResult { + let mut position = position; + position.join_time = env.block.time.seconds(); + position.claim_after = claim_after; + Ok(position) + })?; Ok(Response::default() .add_submessage(SubMsg::reply_on_success( @@ -94,7 +93,24 @@ pub fn execute_update_range( .add_attribute("liquidity_amount", position.liquidity)) } -// do create new position +fn requires_swap( + sqrt_p: Decimal256, + sqrt_pl: Decimal256, + sqrt_pu: Decimal256, + base_amount: Uint128, + quote_amount: Uint128, + base_liquidity: Uint256, + quote_liquidity: Uint256, +) -> bool { + if sqrt_p >= sqrt_pu { + return !base_amount.is_zero(); + } + if sqrt_p <= sqrt_pl { + return !quote_amount.is_zero(); + } + base_liquidity != quote_liquidity +} + pub fn handle_withdraw_position_reply(deps: DepsMut, env: Env) -> Result { let modify_range_state = MODIFY_RANGE_STATE.load(deps.storage)?.unwrap(); let pool_config = POOL_CONFIG.load(deps.storage)?; @@ -102,11 +118,88 @@ pub fn handle_withdraw_position_reply(deps: DepsMut, env: Env) -> Result modify_range_state.lower_tick) - { + let sqrt_pu = tick_to_price(modify_range_state.upper_tick)?.sqrt(); + let sqrt_pl = tick_to_price(modify_range_state.lower_tick)?.sqrt(); + let sqrt_p = tick_to_price(pool_details.current_tick)?.sqrt(); + let base_coin = unused_pair_balances[0].clone(); + let quote_coin = unused_pair_balances[1].clone(); + let base_liquidity = + get_liquidity_for_base_token(base_coin.amount.into(), sqrt_p, sqrt_pl, sqrt_pu)?; + let quote_liquidity = + get_liquidity_for_quote_token(quote_coin.amount.into(), sqrt_p, sqrt_pl, sqrt_pu)?; + + let response = Response::new() + .add_attribute("method", "reply") + .add_attribute("action", "handle_withdraw_position") + .add_attribute("lower_tick", modify_range_state.lower_tick.to_string()) + .add_attribute("upper_tick", modify_range_state.upper_tick.to_string()) + .add_attribute("token0", format!("{}", base_coin)) + .add_attribute("token1", format!("{}", quote_coin)); + if requires_swap( + sqrt_p, + sqrt_pl, + sqrt_pu, + base_coin.amount, + quote_coin.amount, + base_liquidity, + quote_liquidity, + ) { + let twap_price = get_twap_price( + deps.storage, + &deps.querier, + &env, + modify_range_state.twap_window_seconds, + )?; + let (token_in, out_denom, price) = if sqrt_p <= sqrt_pl { + ( + quote_coin, + base_coin.denom, + twap_price.inv().expect("Invalid price"), + ) + } else if sqrt_p >= sqrt_pu { + (base_coin, quote_coin.denom, twap_price) + } else if base_liquidity > quote_liquidity { + let used_base_amount: Uint128 = get_amount_from_liquidity_for_base_token( + quote_liquidity, + sqrt_p, + sqrt_pl, + sqrt_pu, + )? + .try_into()?; + let residual_amount = base_coin.amount.checked_sub(used_base_amount)?; + let swap_amount = get_single_sided_deposit_0_to_1_swap_amount( + residual_amount, + modify_range_state.lower_tick, + pool_details.current_tick, + modify_range_state.upper_tick, + )?; + ( + coin(swap_amount.into(), base_coin.denom), + quote_coin.denom, + twap_price, + ) + } else { + let used_quote_amount = get_amount_from_liquidity_for_quote_token( + base_liquidity, + sqrt_p, + sqrt_pl, + sqrt_pu, + )? + .try_into()?; + let residual_amount = quote_coin.amount.checked_sub(used_quote_amount)?; + let swap_amount = get_single_sided_deposit_1_to_0_swap_amount( + residual_amount, + modify_range_state.lower_tick, + pool_details.current_tick, + modify_range_state.upper_tick, + )?; + ( + coin(swap_amount.into(), quote_coin.denom), + base_coin.denom, + twap_price.inv().expect("Invalid price"), + ) + }; + SWAP_DEPOSIT_MERGE_STATE.save( deps.storage, &SwapDepositMergeState { @@ -116,18 +209,24 @@ pub fn handle_withdraw_position_reply(deps: DepsMut, env: Env) -> Result Result Result { - let create_position_message: MsgCreatePositionResponse = data.try_into()?; - let modify_range_state = MODIFY_RANGE_STATE.load(deps.storage)?.unwrap(); - let pool_config = POOL_CONFIG.load(deps.storage)?; - - // taking from response message is important because they may differ from the ones in our request - let target_lower_tick = create_position_message.lower_tick; - let target_upper_tick = create_position_message.upper_tick; - - let unused_pair_balances = get_unused_pair_balances(&deps, &env, &pool_config)?; - - if unused_pair_balances - .iter() - .all(|coin: &Coin| coin.amount.is_zero()) - { - let position = POSITION.load(deps.storage)?; - - // if we have not tokens to swap, that means all tokens we correctly used in the create position - // this means we can save the position id of the first create_position - POSITION.save( - deps.storage, - &Position { - position_id: create_position_message.position_id, - join_time: position.join_time, - claim_after: position.claim_after, - }, - )?; - - return Ok(Response::new() - .add_attribute("method", "reply") - .add_attribute("action", "handle_initial_create_position_reply") - .add_attribute( - "new_position", - create_position_message.position_id.to_string(), - )); + Ok(response.add_submessage(SubMsg::reply_on_success( + create_position_msg, + Replies::CreatePosition.into(), + ))) } - - SWAP_DEPOSIT_MERGE_STATE.save( - deps.storage, - &SwapDepositMergeState { - target_lower_tick, - target_upper_tick, - target_range_position_ids: vec![create_position_message.position_id], - }, - )?; - - do_swap_deposit_merge( - deps, - env, - target_lower_tick, - target_upper_tick, - &unused_pair_balances, - modify_range_state.ratio_of_swappable_funds_to_use, - modify_range_state.twap_window_seconds, - ) } -/// this function assumes that we are swapping and depositing into a valid range -/// -/// It also calculates the exact amount we should be swapping based on current balances and the new range -#[allow(clippy::too_many_arguments)] -fn do_swap_deposit_merge( - deps: DepsMut, - env: Env, - target_lower_tick: i64, - target_upper_tick: i64, - tokens_provided: &[Coin], - ratio_of_swappable_funds_to_use: Decimal, - twap_window_seconds: u64, -) -> Result { - let swap_tokens: Result, _> = tokens_provided - .iter() - .map(|c| -> Result { - Ok(coin( - c.amount - .checked_mul_floor(ratio_of_swappable_funds_to_use)? - .into(), - c.denom.clone(), - )) - }) - .collect(); - let swap_tokens = swap_tokens?; - - let pool_config = POOL_CONFIG.load(deps.storage)?; - let pool_details = get_cl_pool_info(&deps.querier, pool_config.pool_id)?; - - let mrs = MODIFY_RANGE_STATE.load(deps.storage)?.unwrap(); - let twap_price = get_twap_price(deps.storage, &deps.querier, &env, twap_window_seconds)?; - //TODO: further optimizations can be made by increasing the swap amount by half of our expected slippage, - // to reduce the total number of non-deposited tokens that we will then need to refund - let (token_in, out_denom, price) = if !swap_tokens[0].amount.is_zero() { - ( - // range is above current tick - if pool_details.current_tick > target_upper_tick { - swap_tokens[0].clone() - } else { - coin( - get_single_sided_deposit_0_to_1_swap_amount( - swap_tokens[0].amount, - target_lower_tick, - pool_details.current_tick, - target_upper_tick, - )? - .into(), - swap_tokens[0].denom.clone(), - ) - }, - swap_tokens[1].denom.clone(), - twap_price, - ) - } else { - ( - // current tick is above range - if pool_details.current_tick < target_lower_tick { - swap_tokens[1].clone() - } else { - coin( - get_single_sided_deposit_1_to_0_swap_amount( - swap_tokens[1].amount, - target_lower_tick, - pool_details.current_tick, - target_upper_tick, - )? - .into(), - swap_tokens[1].denom.clone(), - ) - }, - swap_tokens[0].denom.clone(), - twap_price.inv().expect("Invalid price"), - ) - }; - - let token_out_min_amount = - estimate_swap_min_out_amount(token_in.amount, price, mrs.max_slippage)?; - - let dex_router = DEX_ROUTER.may_load(deps.storage)?; - let swap_msg = swap_msg( - env.contract.address, - pool_config.pool_id, - token_in.clone(), - coin(token_out_min_amount.into(), out_denom.clone()), - mrs.forced_swap_route, - dex_router, - )?; - - Ok(Response::new() - .add_attribute("method", "reply") - .add_attribute("action", "do_swap_deposit_merge") - .add_submessage(SubMsg::reply_on_success(swap_msg, Replies::Swap.into())) - .add_attributes(vec![ - attr("token_in", format!("{}", token_in)), - attr("token_out_min", token_out_min_amount.to_string()), - ])) -} - -// do deposit pub fn handle_swap_reply(deps: DepsMut, env: Env) -> Result { let swap_deposit_merge_state = SWAP_DEPOSIT_MERGE_STATE.load(deps.storage)?; - + SWAP_DEPOSIT_MERGE_STATE.remove(deps.storage); let pool_config = POOL_CONFIG.load(deps.storage)?; let unused_pair_balances = get_unused_pair_balances(&deps, &env, &pool_config)?; @@ -332,7 +263,7 @@ pub fn handle_swap_reply(deps: DepsMut, env: Env) -> Result Result Result { let create_position_message: MsgCreatePositionResponse = data.try_into()?; - let mut swap_deposit_merge_state = SWAP_DEPOSIT_MERGE_STATE.load(deps.storage)?; - - // add the position id to the ones we need to merge - swap_deposit_merge_state - .target_range_position_ids - .push(create_position_message.position_id); - - // call merge - let merge_msg = - ExecuteMsg::VaultExtension(crate::msg::ExtensionExecuteMsg::Merge(MergePositionMsg { - position_ids: swap_deposit_merge_state.target_range_position_ids.clone(), - })); - - let merge_submsg = SubMsg::reply_on_success( - cosmwasm_std::WasmMsg::Execute { - contract_addr: env.contract.address.to_string(), - msg: to_json_binary(&merge_msg)?, - funds: vec![], - }, - Replies::Merge.into(), - ); - // clear state to allow for new liquidity movement operations - SWAP_DEPOSIT_MERGE_STATE.remove(deps.storage); + POSITION.update(deps.storage, |position| -> StdResult { + let mut position = position; + position.position_id = create_position_message.position_id; + position.join_time = env.block.time.seconds(); + Ok(position) + })?; - Ok(Response::new() - .add_submessage(merge_submsg) - .add_attribute("method", "reply") - .add_attribute("action", "handle_iteration_create_position") - .add_attribute( - "position_ids", - format!("{:?}", swap_deposit_merge_state.target_range_position_ids), - )) + Ok(Response::default()) } #[cfg(test)] @@ -397,7 +303,7 @@ mod tests { use cosmwasm_std::{ coin, testing::{mock_dependencies, mock_env, mock_info, MOCK_CONTRACT_ADDR}, - Decimal, + Decimal, Decimal256, Uint128, Uint256, }; use crate::{ @@ -405,6 +311,7 @@ mod tests { math::tick::build_tick_exp_cache, state::{MODIFY_RANGE_STATE, RANGE_ADMIN}, test_helpers::{mock_deps_with_querier, mock_deps_with_querier_with_balance}, + vault::range::requires_swap, }; #[test] @@ -492,4 +399,132 @@ mod tests { "11234token1" ); // 10000 withdrawn + 1234 local balance } + + #[test] + fn test_when_price_is_below_range_and_quote_amount_is_zero_then_no_swap_is_required() { + let sqrt_p = Decimal256::one(); + let sqrt_pl = Decimal256::from_str("2.0").unwrap(); + let sqrt_pu = Decimal256::from_str("3.0").unwrap(); + let base_amount = Uint128::one(); + let quote_amount = Uint128::zero(); + let base_liquidity = Uint256::one(); + let quote_liquidity = Uint256::one(); + + assert!(!requires_swap( + sqrt_p, + sqrt_pl, + sqrt_pu, + base_amount, + quote_amount, + base_liquidity, + quote_liquidity + )); + } + + #[test] + fn test_when_price_is_below_range_and_quote_amount_is_not_zero_then_swap_is_required() { + let sqrt_p = Decimal256::one(); + let sqrt_pl = Decimal256::from_str("2.0").unwrap(); + let sqrt_pu = Decimal256::from_str("3.0").unwrap(); + let base_amount = Uint128::one(); + let quote_amount = Uint128::one(); + let base_liquidity = Uint256::one(); + let quote_liquidity = Uint256::one(); + + assert!(requires_swap( + sqrt_p, + sqrt_pl, + sqrt_pu, + base_amount, + quote_amount, + base_liquidity, + quote_liquidity + )); + } + + #[test] + fn test_when_price_is_above_range_and_base_amount_is_zero_then_no_swap_is_required() { + let sqrt_p = Decimal256::from_str("4.0").unwrap(); + let sqrt_pl = Decimal256::from_str("2.0").unwrap(); + let sqrt_pu = Decimal256::from_str("3.0").unwrap(); + let base_amount = Uint128::zero(); + let quote_amount = Uint128::one(); + let base_liquidity = Uint256::one(); + let quote_liquidity = Uint256::one(); + + assert!(!requires_swap( + sqrt_p, + sqrt_pl, + sqrt_pu, + base_amount, + quote_amount, + base_liquidity, + quote_liquidity + )); + } + + #[test] + fn test_when_price_is_above_range_and_base_amount_is_not_zero_then_swap_is_required() { + let sqrt_p = Decimal256::from_str("4.0").unwrap(); + let sqrt_pl = Decimal256::from_str("2.0").unwrap(); + let sqrt_pu = Decimal256::from_str("3.0").unwrap(); + let base_amount = Uint128::one(); + let quote_amount = Uint128::one(); + let base_liquidity = Uint256::one(); + let quote_liquidity = Uint256::one(); + + assert!(requires_swap( + sqrt_p, + sqrt_pl, + sqrt_pu, + base_amount, + quote_amount, + base_liquidity, + quote_liquidity + )); + } + + #[test] + fn test_when_price_is_in_range_and_base_liquidity_differs_from_quote_liquidity_then_swap_is_required( + ) { + let sqrt_p = Decimal256::from_str("2.5").unwrap(); + let sqrt_pl = Decimal256::from_str("2.0").unwrap(); + let sqrt_pu = Decimal256::from_str("3.0").unwrap(); + let base_amount = Uint128::one(); + let quote_amount = Uint128::one(); + let base_liquidity = Uint256::one(); + let quote_liquidity = Uint256::from(2u32); + + assert!(requires_swap( + sqrt_p, + sqrt_pl, + sqrt_pu, + base_amount, + quote_amount, + base_liquidity, + quote_liquidity + )); + } + + #[test] + fn test_when_price_is_in_range_and_base_liquidity_equals_quote_liquidity_then_no_swap_is_required( + ) { + let sqrt_p = Decimal256::from_str("2.5").unwrap(); + let sqrt_pl = Decimal256::from_str("2.0").unwrap(); + let sqrt_pu = Decimal256::from_str("3.0").unwrap(); + let base_amount = Uint128::one(); + let quote_amount = Uint128::one(); + let base_liquidity = Uint256::one(); + let quote_liquidity = Uint256::one(); + + assert!(!requires_swap( + sqrt_p, + sqrt_pl, + sqrt_pu, + base_amount, + quote_amount, + base_liquidity, + quote_liquidity + )); + } } diff --git a/smart-contracts/osmosis/contracts/cl-vault/tests/test-tube/range.rs b/smart-contracts/osmosis/contracts/cl-vault/tests/test-tube/range.rs index 113bb82b6..bafdcfb53 100644 --- a/smart-contracts/osmosis/contracts/cl-vault/tests/test-tube/range.rs +++ b/smart-contracts/osmosis/contracts/cl-vault/tests/test-tube/range.rs @@ -51,7 +51,7 @@ fn move_range_works() { &ExecuteMsg::VaultExtension(ExtensionExecuteMsg::ModifyRange(ModifyRangeMsg { lower_price: Decimal::from_str("0.65").unwrap(), upper_price: Decimal::from_str("1.3").unwrap(), - max_slippage: Decimal::bps(MAX_SLIPPAGE_HIGH), + max_slippage: Decimal::percent(89), ratio_of_swappable_funds_to_use: Decimal::one(), twap_window_seconds: 45, forced_swap_route: None, @@ -74,7 +74,7 @@ fn move_range_works() { ); assert_eq!( event.attributes[pos + DO_SWAP_DEPOSIT_MIN_OUT_OFFSET].value, - "201280" + "199044" ); } @@ -85,11 +85,11 @@ fn move_range_works() { if let Some(pos) = pos { assert_eq!( event.attributes[pos + SWAP_SUCCESS_BASE_BALANCE_OFFSET].value, - "141894uatom" + "776354uatom" ); assert_eq!( event.attributes[pos + SWAP_SUCCESS_QUOTE_BALANCE_OFFSET].value, - "201280ubtc" + "1201279ubtc" ); } } @@ -104,7 +104,7 @@ fn move_range_works() { .unwrap(); assert_eq!(response.position_ids.len(), 1); let position_id = response.position_ids[0]; - assert_eq!(position_id, 5u64); + assert_eq!(position_id, 3u64); let cl = ConcentratedLiquidity::new(&app); let pos = cl @@ -114,8 +114,8 @@ fn move_range_works() { .unwrap(); let pos_base: Coin = pos.asset0.unwrap().try_into().unwrap(); let pos_quote: Coin = pos.asset1.unwrap().try_into().unwrap(); - assert_eq!(pos_base, coin(762163u128, DENOM_BASE)); - assert_eq!(pos_quote, coin(1201277u128, DENOM_QUOTE)); + assert_eq!(pos_base, coin(762164u128, DENOM_BASE)); + assert_eq!(pos_quote, coin(1201279u128, DENOM_QUOTE)); } #[test] @@ -180,7 +180,7 @@ fn move_range_cw_dex_works() { if let Some(pos) = pos { assert_eq!( event.attributes[pos + SWAP_SUCCESS_BASE_BALANCE_OFFSET].value, - "899999uatom" + "1899998uatom" ); assert_eq!( event.attributes[pos + SWAP_SUCCESS_QUOTE_BALANCE_OFFSET].value, @@ -199,7 +199,7 @@ fn move_range_cw_dex_works() { .unwrap(); assert_eq!(response.position_ids.len(), 1); let position_id = response.position_ids[0]; - assert_eq!(position_id, 5u64); + assert_eq!(position_id, 3u64); let cl = ConcentratedLiquidity::new(&app); let pos = cl @@ -209,7 +209,7 @@ fn move_range_cw_dex_works() { .unwrap(); let pos_base: Coin = pos.asset0.unwrap().try_into().unwrap(); let pos_quote: Coin = pos.asset1.unwrap().try_into().unwrap(); - assert_eq!(pos_base, coin(1899996u128, DENOM_BASE)); + assert_eq!(pos_base, coin(1899998u128, DENOM_BASE)); assert_eq!(pos_quote, coin(0u128, DENOM_QUOTE)); } @@ -283,7 +283,7 @@ fn move_range_cw_dex_works_forced_swap_route() { if let Some(pos) = pos { assert_eq!( event.attributes[pos + SWAP_SUCCESS_BASE_BALANCE_OFFSET].value, - "899999uatom" + "1899998uatom" ); assert_eq!( event.attributes[pos + SWAP_SUCCESS_QUOTE_BALANCE_OFFSET].value, @@ -301,7 +301,7 @@ fn move_range_cw_dex_works_forced_swap_route() { .unwrap(); assert_eq!(response.position_ids.len(), 1); let position_id = response.position_ids[0]; - assert_eq!(position_id, 5u64); + assert_eq!(position_id, 3u64); let cl = ConcentratedLiquidity::new(&app); let pos = cl @@ -311,7 +311,7 @@ fn move_range_cw_dex_works_forced_swap_route() { .unwrap(); let pos_base: Coin = pos.asset0.unwrap().try_into().unwrap(); let pos_quote: Coin = pos.asset1.unwrap().try_into().unwrap(); - assert_eq!(pos_base, coin(1899996u128, DENOM_BASE)); + assert_eq!(pos_base, coin(1899998u128, DENOM_BASE)); assert_eq!(pos_quote, coin(0u128, DENOM_QUOTE)); } @@ -361,7 +361,7 @@ fn move_range_single_side_works() { if let Some(pos) = pos { assert_eq!( event.attributes[pos + SWAP_SUCCESS_BASE_BALANCE_OFFSET].value, - "899999uatom" + "1899998uatom" ); assert_eq!( event.attributes[pos + SWAP_SUCCESS_QUOTE_BALANCE_OFFSET].value, @@ -395,11 +395,11 @@ fn move_range_single_side_works() { if let Some(pos) = pos { assert_eq!( event.attributes[pos + DO_SWAP_DEPOSIT_TOKEN_IN_OFFSET].value, - "1899995uatom" + "1899997uatom" ); assert_eq!( event.attributes[pos + DO_SWAP_DEPOSIT_MIN_OUT_OFFSET].value, - "1709995" + "1709997" ); } @@ -414,7 +414,7 @@ fn move_range_single_side_works() { ); assert_eq!( event.attributes[pos + SWAP_SUCCESS_QUOTE_BALANCE_OFFSET].value, - "1709995ubtc" + "1709997ubtc" ); } } @@ -429,7 +429,7 @@ fn move_range_single_side_works() { .unwrap(); assert_eq!(response.position_ids.len(), 1); let position_id = response.position_ids[0]; - assert_eq!(position_id, 7u64); + assert_eq!(position_id, 4u64); let cl = ConcentratedLiquidity::new(&app); let pos = cl @@ -440,7 +440,7 @@ fn move_range_single_side_works() { let pos_base: Coin = pos.asset0.unwrap().try_into().unwrap(); let pos_quote: Coin = pos.asset1.unwrap().try_into().unwrap(); assert_eq!(pos_base, coin(0u128, DENOM_BASE)); - assert_eq!(pos_quote, coin(1709994u128, DENOM_QUOTE)); + assert_eq!(pos_quote, coin(1709997u128, DENOM_QUOTE)); } /*