Skip to content

Commit

Permalink
Fix/cl-vault account for unused funds (#501)
Browse files Browse the repository at this point in the history
## 1. Overview

Changing the CL Vault contract to also use any funds that aren't being
saved as rewards

## 2. Implementation details

Added a function that gets the usable balance of the contract

## 3. How to test/use

<!-- How can people test/use this? -->

## 4. Checklist

<!-- Checklist for PR author(s). -->

- [ ] Does the Readme need to be updated?

## 5. Limitations (optional)

<!-- Describe any limitation of the capabilities listed in the Overview
section. -->

## 6. Future Work (optional)

<!-- Describe follow up work, if any. -->

---------

Co-authored-by: LaurensKubat <[email protected]>
Co-authored-by: magiodev.eth <[email protected]>
  • Loading branch information
3 people authored Sep 18, 2023
1 parent 3866390 commit 3c643c8
Show file tree
Hide file tree
Showing 21 changed files with 991 additions and 286 deletions.
6 changes: 3 additions & 3 deletions smart-contracts/contracts/cl-vault/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ pub fn execute(
execute_exact_deposit(deps, env, info, recipient)
}
cw_vault_multi_standard::VaultStandardExecuteMsg::Redeem { recipient, amount } => {
execute_withdraw(deps, env, info, recipient, amount)
execute_withdraw(deps, env, info, recipient, amount.into())
}
cw_vault_multi_standard::VaultStandardExecuteMsg::VaultExtension(vault_msg) => {
match vault_msg {
Expand All @@ -86,7 +86,7 @@ pub fn execute(
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult<Binary> {
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult<Binary> {
match msg {
cw_vault_multi_standard::VaultStandardQueryMsg::VaultStandardInfo {} => todo!(),
cw_vault_multi_standard::VaultStandardQueryMsg::Info {} => {
Expand All @@ -96,7 +96,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult<Binary> {
cw_vault_multi_standard::VaultStandardQueryMsg::DepositRatio => todo!(),
cw_vault_multi_standard::VaultStandardQueryMsg::PreviewRedeem { amount: _ } => todo!(),
cw_vault_multi_standard::VaultStandardQueryMsg::TotalAssets {} => {
Ok(to_binary(&query_total_assets(deps, env)?)?)
Ok(to_binary(&query_total_assets(deps)?)?)
}
cw_vault_multi_standard::VaultStandardQueryMsg::TotalVaultTokenSupply {} => {
Ok(to_binary(&query_total_vault_token_supply(deps)?)?)
Expand Down
187 changes: 184 additions & 3 deletions smart-contracts/contracts/cl-vault/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use std::str::FromStr;

use crate::math::tick::tick_to_price;
use crate::state::ADMIN_ADDRESS;
use crate::rewards::CoinList;
use crate::state::{ADMIN_ADDRESS, STRATEGIST_REWARDS, USER_REWARDS};
use crate::vault::concentrated_liquidity::{get_cl_pool_info, get_position};
use crate::{error::ContractResult, state::POOL_CONFIG, ContractError};
use cosmwasm_std::{
coin, Addr, Coin, Decimal, Decimal256, Deps, Fraction, MessageInfo, QuerierWrapper, Storage,
Uint128, Uint256,
coin, Addr, Coin, Decimal, Decimal256, Deps, DepsMut, Env, Fraction, MessageInfo,
QuerierWrapper, Storage, Uint128, Uint256,
};

use osmosis_std::types::osmosis::poolmanager::v1beta1::PoolmanagerQuerier;

/// returns the Coin of the needed denoms in the order given in denoms
Expand Down Expand Up @@ -244,6 +247,184 @@ pub fn sort_tokens(tokens: Vec<Coin>) -> Vec<Coin> {
sorted_tokens
}

/// this function subtracts out anything from the raw contract balance that isn't dedicated towards user or strategist rewards.
/// this function is expensive.
pub fn get_unused_balances(
storage: &dyn Storage,
querier: &QuerierWrapper,
env: &Env,
) -> Result<CoinList, ContractError> {
let mut balances =
CoinList::from_coins(querier.query_all_balances(env.contract.address.to_string())?);

// subtract out strategist rewards and all user rewards
let strategist_rewards = STRATEGIST_REWARDS.load(storage)?;

balances.sub(&strategist_rewards)?;

for user_reward in USER_REWARDS.range(storage, None, None, cosmwasm_std::Order::Ascending) {
balances.sub(&user_reward?.1)?;
}

Ok(balances)
}

pub fn get_max_utilization_for_ratio(
token0: Uint256,
token1: Uint256,
ratio: Decimal256,
) -> Result<(Uint256, Uint256), ContractError> {
// maxdep1 = T0 / R
let max_deposit1_from_0 =
token0.checked_multiply_ratio(ratio.denominator(), ratio.numerator())?;
// maxdep0 = T1 * R
let max_deposit0_from_1 =
token1.checked_multiply_ratio(ratio.numerator(), ratio.denominator())?;

if max_deposit0_from_1 > token0 {
Ok((token0, max_deposit1_from_0))
} else if max_deposit1_from_0 > token1 {
Ok((max_deposit0_from_1, token1))
} else {
Ok((token0, token1))
}
}

pub fn get_liquidity_amount_for_unused_funds(
deps: DepsMut,
env: &Env,
additional_excluded_funds: (Uint128, Uint128),
) -> Result<Decimal256, ContractError> {
// first get the ratio of token0:token1 in the position.
let p = get_position(deps.storage, &deps.querier)?;
// if there is no position, then we can assume that there are 0 unused funds
if p.position.is_none() {
return Ok(Decimal256::zero());
}
let position_unwrapped = p.position.unwrap();
let token0: Coin = p.asset0.unwrap().try_into()?;
let token1: Coin = p.asset1.unwrap().try_into()?;
// if any of the values are 0, we fill 1
let ratio = if token0.amount.is_zero() {
Decimal256::from_ratio(1_u128, token1.amount)
} else if token1.amount.is_zero() {
Decimal256::from_ratio(token0.amount, 1_u128)
} else {
Decimal256::from_ratio(token0.amount, token1.amount)
};
let pool_config = POOL_CONFIG.load(deps.storage)?;
let pool_details = get_cl_pool_info(&deps.querier, pool_config.pool_id)?;

// then figure out based on current unused balance, what the max initial deposit could be
// (with the ratio, what is the max tokens we can deposit)
let tokens = get_unused_balances(deps.storage, &deps.querier, env)?;
let unused_t0: Uint256 = tokens
.find_coin(token0.denom)
.amount
.checked_sub(additional_excluded_funds.0)?
.into();
let unused_t1: Uint256 = tokens
.find_coin(token1.denom)
.amount
.checked_sub(additional_excluded_funds.1)?
.into();

let max_initial_deposit = get_max_utilization_for_ratio(unused_t0, unused_t1, ratio)?;

// then figure out how much liquidity this would give us.
// Formula: current_position_liquidity * token0_initial_deposit_amount / token0_in_current_position
// EDGE CASE: what if it's a one-sided position with only token1?
// SOLUTION: take whichever token is greater than the other to plug into the formula 1 line above
let position_liquidity = Decimal256::from_str(&position_unwrapped.liquidity)?;
let max_initial_deposit_liquidity = if token0.amount > token1.amount {
position_liquidity
.checked_mul(Decimal256::new(max_initial_deposit.0))?
.checked_div(Decimal256::new(token0.amount.into()))?
} else {
position_liquidity
.checked_mul(Decimal256::new(max_initial_deposit.1))?
.checked_div(Decimal256::new(token1.amount.into()))?
};

// subtract out the max deposit from both tokens, which will leave us with only one token, lets call this leftover_balance0 or 1
let leftover_balance0 = unused_t0.checked_sub(max_initial_deposit.0)?;
let leftover_balance1 = unused_t1.checked_sub(max_initial_deposit.1)?;

// call get_single_sided_deposit_0_to_1_swap_amount or get_single_sided_deposit_1_to_0_swap_amount to see how much we would swap to enter with the rest of our funds
let post_swap_liquidity = if leftover_balance0 > leftover_balance1 {
let swap_amount = if pool_details.current_tick > position_unwrapped.upper_tick {
leftover_balance0.try_into().unwrap()
} else {
get_single_sided_deposit_0_to_1_swap_amount(
leftover_balance0.try_into().unwrap(),
position_unwrapped.lower_tick,
pool_details.current_tick,
position_unwrapped.upper_tick,
)?
};
// let swap_amount = get_single_sided_deposit_0_to_1_swap_amount(
// leftover_balance0.try_into().unwrap(),
// position_unwrapped.lower_tick,
// pool_details.current_tick,
// position_unwrapped.upper_tick,
// )?;

// subtract the resulting swap_amount from leftover_balance0 or 1, we can then use the same formula as above to get the correct liquidity amount.
// we are also mindful of the same edge case
let leftover_balance0 = leftover_balance0.checked_sub(swap_amount.into())?;

if leftover_balance0.is_zero() {
// in this case we need to get the expected token1 from doing a full swap, meaning we need to multiply by the spot price
let token1_from_swap_amount = Decimal256::new(swap_amount.into())
.checked_mul(tick_to_price(pool_details.current_tick)?)?;
position_liquidity
.checked_mul(token1_from_swap_amount)?
.checked_div(Decimal256::new(token1.amount.into()))?
} else {
position_liquidity
.checked_mul(Decimal256::new(leftover_balance0))?
.checked_div(Decimal256::new(token0.amount.into()))?
}
} else {
let swap_amount = if pool_details.current_tick < position_unwrapped.lower_tick {
leftover_balance1.try_into().unwrap()
} else {
get_single_sided_deposit_1_to_0_swap_amount(
leftover_balance1.try_into().unwrap(),
position_unwrapped.lower_tick,
pool_details.current_tick,
position_unwrapped.upper_tick,
)?
};
// let swap_amount = get_single_sided_deposit_1_to_0_swap_amount(
// leftover_balance1.try_into().unwrap(),
// position_unwrapped.lower_tick,
// pool_details.current_tick,
// position_unwrapped.upper_tick,
// )?;

// subtract the resulting swap_amount from leftover_balance0 or 1, we can then use the same formula as above to get the correct liquidity amount.
// we are also mindful of the same edge case
let leftover_balance1 = leftover_balance1.checked_sub(swap_amount.into())?;

if leftover_balance1.is_zero() {
// in this case we need to get the expected token0 from doing a full swap, meaning we need to multiply by the spot price
let token0_from_swap_amount = Decimal256::new(swap_amount.into())
.checked_div(tick_to_price(pool_details.current_tick)?)?;
position_liquidity
.checked_mul(token0_from_swap_amount)?
.checked_div(Decimal256::new(token0.amount.into()))?
} else {
position_liquidity
.checked_mul(Decimal256::new(leftover_balance1))?
.checked_div(Decimal256::new(token1.amount.into()))?
}
};

// add together the liquidity from the initial deposit and the swap deposit and return that
Ok(max_initial_deposit_liquidity.checked_add(post_swap_liquidity)?)
}

#[cfg(test)]
mod tests {

Expand Down
4 changes: 2 additions & 2 deletions smart-contracts/contracts/cl-vault/src/instantiate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use osmosis_std::types::osmosis::tokenfactory::v1beta1::{
use crate::helpers::must_pay_one_or_two;
use crate::msg::InstantiateMsg;
use crate::reply::Replies;
use crate::rewards::Rewards;
use crate::rewards::CoinList;
use crate::state::{
Metadata, PoolConfig, Position, ADMIN_ADDRESS, METADATA, POOL_CONFIG, POSITION, RANGE_ADMIN,
STRATEGIST_REWARDS, VAULT_CONFIG, VAULT_DENOM,
Expand Down Expand Up @@ -54,7 +54,7 @@ pub fn handle_instantiate(
},
)?;

STRATEGIST_REWARDS.save(deps.storage, &Rewards::new())?;
STRATEGIST_REWARDS.save(deps.storage, &CoinList::new())?;

METADATA.save(
deps.storage,
Expand Down
1 change: 0 additions & 1 deletion smart-contracts/contracts/cl-vault/src/math/tick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,6 @@ mod tests {
let tick_index = 27445000_i128;
let _expected_price = Decimal256::from_str("30352").unwrap();
let price = tick_to_price(tick_index.try_into().unwrap()).unwrap();
println!("{:?}", price.to_string());
// assert_eq!(price, expected_price);
let tick = price_to_tick(deps.as_mut().storage, price).unwrap();
assert_eq!(tick_index, tick)
Expand Down
6 changes: 3 additions & 3 deletions smart-contracts/contracts/cl-vault/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
},
};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{coin, Coin, Deps, Env, Uint128};
use cosmwasm_std::{coin, Coin, Deps, Uint128};
use cw_vault_multi_standard::VaultInfoResponse;
use osmosis_std::types::cosmos::bank::v1beta1::BankQuerier;

Expand Down Expand Up @@ -119,8 +119,8 @@ pub fn query_user_rewards(deps: Deps, user: String) -> ContractResult<UserReward
Ok(UserRewardsResponse { rewards })
}

pub fn query_total_assets(deps: Deps, env: Env) -> ContractResult<TotalAssetsResponse> {
let position = get_position(deps.storage, &deps.querier, &env)?;
pub fn query_total_assets(deps: Deps) -> ContractResult<TotalAssetsResponse> {
let position = get_position(deps.storage, &deps.querier)?;
let pool = POOL_CONFIG.load(deps.storage)?;
Ok(TotalAssetsResponse {
token0: position
Expand Down
14 changes: 7 additions & 7 deletions smart-contracts/contracts/cl-vault/src/rewards/distribution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ use osmosis_std::types::{
},
};

use super::helpers::Rewards;
use super::helpers::CoinList;

/// claim_rewards claims rewards from Osmosis and update the rewards map to reflect each users rewards
pub fn execute_distribute_rewards(deps: DepsMut, env: Env) -> Result<Response, ContractError> {
CURRENT_REWARDS.save(deps.storage, &Rewards::new())?;
CURRENT_REWARDS.save(deps.storage, &CoinList::new())?;
let msg = collect_incentives(deps.as_ref(), env)?;

Ok(Response::new().add_submessage(SubMsg::reply_on_success(
Expand Down Expand Up @@ -53,7 +53,7 @@ pub fn handle_collect_incentives_reply(
let response: MsgCollectIncentivesResponse = data?;
CURRENT_REWARDS.update(
deps.storage,
|mut rewards| -> Result<Rewards, ContractError> {
|mut rewards| -> Result<CoinList, ContractError> {
rewards.update_rewards(response.collected_incentives)?;
Ok(rewards)
},
Expand Down Expand Up @@ -96,7 +96,7 @@ pub fn handle_collect_spread_rewards_reply(

fn distribute_rewards(
mut deps: DepsMut,
mut rewards: Rewards,
mut rewards: CoinList,
) -> Result<Vec<Attribute>, ContractError> {
if rewards.is_empty() {
return Ok(vec![Attribute::new("total_rewards_amount", "0")]);
Expand All @@ -121,9 +121,9 @@ fn distribute_rewards(

// for each user with locked tokens, we distribute some part of the rewards to them
// get all users and their current pre-distribution rewards
let user_rewards: Result<Vec<(Addr, Rewards)>, ContractError> = SHARES
let user_rewards: Result<Vec<(Addr, CoinList)>, ContractError> = SHARES
.range(deps.branch().storage, None, None, Order::Ascending)
.map(|v| -> Result<(Addr, Rewards), ContractError> {
.map(|v| -> Result<(Addr, CoinList), ContractError> {
let (address, user_shares) = v?;
// calculate the amount of each asset the user should get in rewards
// we need to always round down here, so we never expect more rewards than we have
Expand All @@ -136,7 +136,7 @@ fn distribute_rewards(
user_rewards?
.into_iter()
.try_for_each(|(addr, reward)| -> ContractResult<()> {
USER_REWARDS.update(deps.storage, addr, |old| -> ContractResult<Rewards> {
USER_REWARDS.update(deps.storage, addr, |old| -> ContractResult<CoinList> {
if let Some(old_user_rewards) = old {
Ok(reward.add(old_user_rewards)?)
} else {
Expand Down
Loading

0 comments on commit 3c643c8

Please sign in to comment.