Skip to content
This repository has been archived by the owner on Oct 16, 2023. It is now read-only.

Commit

Permalink
MP-3350. Slippage (#197)
Browse files Browse the repository at this point in the history
* Add max_slippage config.

* Use slippage for ProvideLiquidity and WithdrawLiquidity.

* Assert slippage in swap.

* Update scripts.

* Update error msg.
  • Loading branch information
piobab authored Sep 13, 2023
1 parent bb5481d commit 40cf48e
Show file tree
Hide file tree
Showing 38 changed files with 371 additions and 138 deletions.
3 changes: 2 additions & 1 deletion contracts/account-nft/tests/helpers/mock_env_builder.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::mem::take;

use anyhow::Result as AnyResult;
use cosmwasm_std::{Addr, Empty};
use cosmwasm_std::{Addr, Decimal, Empty};
use cw_multi_test::{BasicApp, Executor};
use mars_account_nft_types::msg::InstantiateMsg;
use mars_mock_credit_manager::msg::InstantiateMsg as CmMockInstantiateMsg;
Expand Down Expand Up @@ -147,6 +147,7 @@ impl MockEnvBuilder {
params: "n/a".to_string(),
account_nft: None,
max_unlocking_positions: Default::default(),
max_slippage: Decimal::percent(99),
swapper: "n/a".to_string(),
zapper: "n/a".to_string(),
health_contract: "n/a".to_string(),
Expand Down
16 changes: 8 additions & 8 deletions contracts/credit-manager/src/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,20 +242,20 @@ pub fn dispatch_actions(
Action::ProvideLiquidity {
coins_in,
lp_token_out,
minimum_receive,
slippage,
} => callbacks.push(CallbackMsg::ProvideLiquidity {
account_id: account_id.to_string(),
lp_token_out,
coins_in,
minimum_receive,
slippage,
}),
Action::WithdrawLiquidity {
lp_token,
minimum_receive,
slippage,
} => callbacks.push(CallbackMsg::WithdrawLiquidity {
account_id: account_id.to_string(),
lp_token,
minimum_receive,
slippage,
}),
Action::RefundAllCoinBalances {} => {
callbacks.push(CallbackMsg::RefundAllCoinBalances {
Expand Down Expand Up @@ -453,13 +453,13 @@ pub fn execute_callback(
account_id,
coins_in,
lp_token_out,
minimum_receive,
} => provide_liquidity(deps, env, &account_id, coins_in, &lp_token_out, minimum_receive),
slippage,
} => provide_liquidity(deps, env, &account_id, coins_in, &lp_token_out, slippage),
CallbackMsg::WithdrawLiquidity {
account_id,
lp_token,
minimum_receive,
} => withdraw_liquidity(deps, env, &account_id, &lp_token, minimum_receive),
slippage,
} => withdraw_liquidity(deps, env, &account_id, &lp_token, slippage),
CallbackMsg::RefundAllCoinBalances {
account_id,
} => refund_coin_balances(deps, env, &account_id),
Expand Down
13 changes: 10 additions & 3 deletions contracts/credit-manager/src/instantiate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ use cosmwasm_std::{DepsMut, Env};
use mars_owner::OwnerInit::SetInitialOwner;
use mars_rover::{error::ContractResult, msg::InstantiateMsg};

use crate::state::{
HEALTH_CONTRACT, INCENTIVES, MAX_UNLOCKING_POSITIONS, ORACLE, OWNER, PARAMS, RED_BANK, SWAPPER,
ZAPPER,
use crate::{
state::{
HEALTH_CONTRACT, INCENTIVES, MAX_SLIPPAGE, MAX_UNLOCKING_POSITIONS, ORACLE, OWNER, PARAMS,
RED_BANK, SWAPPER, ZAPPER,
},
utils::assert_max_slippage,
};

pub fn store_config(deps: DepsMut, env: Env, msg: &InstantiateMsg) -> ContractResult<()> {
Expand All @@ -21,6 +24,10 @@ pub fn store_config(deps: DepsMut, env: Env, msg: &InstantiateMsg) -> ContractRe
SWAPPER.save(deps.storage, &msg.swapper.check(deps.api)?)?;
ZAPPER.save(deps.storage, &msg.zapper.check(deps.api)?)?;
MAX_UNLOCKING_POSITIONS.save(deps.storage, &msg.max_unlocking_positions)?;

assert_max_slippage(msg.max_slippage)?;
MAX_SLIPPAGE.save(deps.storage, &msg.max_slippage)?;

HEALTH_CONTRACT.save(deps.storage, &msg.health_contract.check(deps.api)?)?;
PARAMS.save(deps.storage, &msg.params.check(deps.api)?)?;
INCENTIVES.save(deps.storage, &msg.incentives.check(deps.api, env.contract.address)?)?;
Expand Down
6 changes: 5 additions & 1 deletion contracts/credit-manager/src/migrations/v2_0_0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use mars_rover::{error::ContractResult, msg::migrate::V2Updates};

use crate::{
contract::{CONTRACT_NAME, CONTRACT_VERSION},
state::{HEALTH_CONTRACT, INCENTIVES, OWNER, PARAMS, SWAPPER},
state::{HEALTH_CONTRACT, INCENTIVES, MAX_SLIPPAGE, OWNER, PARAMS, SWAPPER},
utils::assert_max_slippage,
};

const FROM_VERSION: &str = "1.0.0";
Expand Down Expand Up @@ -44,6 +45,9 @@ pub fn migrate(deps: DepsMut, env: Env, updates: V2Updates) -> ContractResult<Re
INCENTIVES.save(deps.storage, &updates.incentives.check(deps.api, env.contract.address)?)?;
SWAPPER.save(deps.storage, &updates.swapper.check(deps.api)?)?;

assert_max_slippage(updates.max_slippage)?;
MAX_SLIPPAGE.save(deps.storage, &updates.max_slippage)?;

// Owner package updated, re-initializing
let old_owner_state = v1_state::OWNER.load(deps.storage)?;
let old_owner = v1_state::current_owner(old_owner_state);
Expand Down
5 changes: 3 additions & 2 deletions contracts/credit-manager/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ use mars_rover_health_types::AccountKind;
use crate::{
state::{
ACCOUNT_KINDS, ACCOUNT_NFT, COIN_BALANCES, DEBT_SHARES, HEALTH_CONTRACT, INCENTIVES,
MAX_UNLOCKING_POSITIONS, ORACLE, OWNER, PARAMS, RED_BANK, REWARDS_COLLECTOR, SWAPPER,
TOTAL_DEBT_SHARES, VAULT_POSITIONS, ZAPPER,
MAX_SLIPPAGE, MAX_UNLOCKING_POSITIONS, ORACLE, OWNER, PARAMS, RED_BANK, REWARDS_COLLECTOR,
SWAPPER, TOTAL_DEBT_SHARES, VAULT_POSITIONS, ZAPPER,
},
utils::debt_shares_to_amount,
vault::vault_utilization_in_deposit_cap_denom,
Expand Down Expand Up @@ -54,6 +54,7 @@ pub fn query_config(deps: Deps) -> ContractResult<ConfigResponse> {
oracle: ORACLE.load(deps.storage)?.address().into(),
params: PARAMS.load(deps.storage)?.address().into(),
max_unlocking_positions: MAX_UNLOCKING_POSITIONS.load(deps.storage)?,
max_slippage: MAX_SLIPPAGE.load(deps.storage)?,
swapper: SWAPPER.load(deps.storage)?.address().into(),
zapper: ZAPPER.load(deps.storage)?.address().into(),
health_contract: HEALTH_CONTRACT.load(deps.storage)?.address().into(),
Expand Down
3 changes: 2 additions & 1 deletion contracts/credit-manager/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use cosmwasm_std::{Addr, Uint128};
use cosmwasm_std::{Addr, Decimal, Uint128};
use cw_storage_plus::{Item, Map};
use mars_owner::Owner;
use mars_rover::{
Expand Down Expand Up @@ -27,6 +27,7 @@ pub const INCENTIVES: Item<Incentives> = Item::new("incentives");
pub const OWNER: Owner = Owner::new("owner");
pub const MAX_UNLOCKING_POSITIONS: Item<Uint128> = Item::new("max_unlocking_positions");
pub const REENTRANCY_GUARD: ReentrancyGuard = ReentrancyGuard::new("reentrancy_guard");
pub const MAX_SLIPPAGE: Item<Decimal> = Item::new("max_slippage");

// Positions
pub const ACCOUNT_KINDS: Map<&str, AccountKind> = Map::new("account_types"); // Map<AccountId, AccountKind>
Expand Down
6 changes: 5 additions & 1 deletion contracts/credit-manager/src/swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use mars_rover::{

use crate::{
state::{COIN_BALANCES, SWAPPER},
utils::{assert_coin_is_whitelisted, decrement_coin_balance, update_balance_msg},
utils::{
assert_coin_is_whitelisted, assert_slippage, decrement_coin_balance, update_balance_msg,
},
};

pub fn swap_exact_in(
Expand All @@ -17,6 +19,8 @@ pub fn swap_exact_in(
denom_out: &str,
slippage: Decimal,
) -> ContractResult<Response> {
assert_slippage(deps.storage, slippage)?;

assert_coin_is_whitelisted(&mut deps, denom_out)?;

let coin_in_to_trade = Coin {
Expand Down
12 changes: 10 additions & 2 deletions contracts/credit-manager/src/update_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ use mars_rover_health_types::AccountKind;
use crate::{
execute::create_credit_account,
state::{
ACCOUNT_NFT, HEALTH_CONTRACT, INCENTIVES, MAX_UNLOCKING_POSITIONS, ORACLE, OWNER, RED_BANK,
REWARDS_COLLECTOR, SWAPPER, ZAPPER,
ACCOUNT_NFT, HEALTH_CONTRACT, INCENTIVES, MAX_SLIPPAGE, MAX_UNLOCKING_POSITIONS, ORACLE,
OWNER, RED_BANK, REWARDS_COLLECTOR, SWAPPER, ZAPPER,
},
utils::assert_max_slippage,
};

pub fn update_config(
Expand Down Expand Up @@ -74,6 +75,13 @@ pub fn update_config(
.add_attribute("value", num.to_string());
}

if let Some(num) = updates.max_slippage {
assert_max_slippage(num)?;
MAX_SLIPPAGE.save(deps.storage, &num)?;
response =
response.add_attribute("key", "max_slippage").add_attribute("value", num.to_string());
}

if let Some(unchecked) = updates.health_contract {
HEALTH_CONTRACT.save(deps.storage, &unchecked.check(deps.api)?)?;
response = response
Expand Down
25 changes: 24 additions & 1 deletion contracts/credit-manager/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ use mars_rover::{
use mars_rover_health_types::AccountKind;

use crate::{
state::{ACCOUNT_KINDS, ACCOUNT_NFT, COIN_BALANCES, PARAMS, RED_BANK, TOTAL_DEBT_SHARES},
state::{
ACCOUNT_KINDS, ACCOUNT_NFT, COIN_BALANCES, MAX_SLIPPAGE, PARAMS, RED_BANK,
TOTAL_DEBT_SHARES,
},
update_coin_balances::query_balance,
};

Expand All @@ -31,6 +34,26 @@ pub fn assert_is_token_owner(deps: &DepsMut, user: &Addr, account_id: &str) -> C
Ok(())
}

pub fn assert_max_slippage(max_slippage: Decimal) -> ContractResult<()> {
if max_slippage.is_zero() || max_slippage >= Decimal::one() {
return Err(ContractError::InvalidConfig {
reason: "Max slippage must be greater than 0 and less than 1".to_string(),
});
}
Ok(())
}

pub fn assert_slippage(storage: &dyn Storage, slippage: Decimal) -> ContractResult<()> {
let max_slippage = MAX_SLIPPAGE.load(storage)?;
if slippage > max_slippage {
return Err(ContractError::SlippageExceeded {
slippage,
max_slippage,
});
}
Ok(())
}

pub fn query_nft_token_owner(deps: Deps, account_id: &str) -> ContractResult<String> {
let contract_addr = ACCOUNT_NFT.load(deps.storage)?;
let res: OwnerOfResponse = deps.querier.query_wasm_smart(
Expand Down
52 changes: 41 additions & 11 deletions contracts/credit-manager/src/zap.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use cosmwasm_std::{Coin, Deps, DepsMut, Env, Response, Uint128};
use cosmwasm_std::{
CheckedMultiplyFractionError, Coin, Decimal, Deps, DepsMut, Env, Response, Uint128,
};
use mars_rover::{
error::{ContractError, ContractResult},
msg::execute::{ActionAmount, ActionCoin, ChangeExpected},
Expand All @@ -8,8 +10,8 @@ use mars_rover::{
use crate::{
state::{COIN_BALANCES, ZAPPER},
utils::{
assert_coin_is_whitelisted, assert_coins_are_whitelisted, decrement_coin_balance,
update_balance_msg, update_balances_msgs,
assert_coin_is_whitelisted, assert_coins_are_whitelisted, assert_slippage,
decrement_coin_balance, update_balance_msg, update_balances_msgs,
},
};

Expand All @@ -19,8 +21,10 @@ pub fn provide_liquidity(
account_id: &str,
coins_in: Vec<ActionCoin>,
lp_token_out: &str,
minimum_receive: Uint128,
slippage: Decimal,
) -> ContractResult<Response> {
assert_slippage(deps.storage, slippage)?;

assert_coin_is_whitelisted(&mut deps, lp_token_out)?;
assert_coins_are_whitelisted(&mut deps, coins_in.to_denoms())?;

Expand All @@ -40,9 +44,21 @@ pub fn provide_liquidity(
updated_coins_in.push(updated_coin);
}

// After zap is complete, update account's LP token balance
let zapper = ZAPPER.load(deps.storage)?;
let zap_msg = zapper.provide_liquidity_msg(&updated_coins_in, lp_token_out, minimum_receive)?;

// Estimate how much LP token will be received from zapper with applied slippage
let estimated_min_receive =
zapper.estimate_provide_liquidity(&deps.querier, lp_token_out, &updated_coins_in)?;
let estimated_min_receive_slippage =
estimated_min_receive.checked_mul_floor(Decimal::one() - slippage)?;

let zap_msg = zapper.provide_liquidity_msg(
&updated_coins_in,
lp_token_out,
estimated_min_receive_slippage,
)?;

// After zap is complete, update account's LP token balance
let update_balance_msg = update_balance_msg(
&deps.querier,
&env.contract.address,
Expand All @@ -65,8 +81,10 @@ pub fn withdraw_liquidity(
env: Env,
account_id: &str,
lp_token_action: &ActionCoin,
minimum_receive: Vec<Coin>,
slippage: Decimal,
) -> ContractResult<Response> {
assert_slippage(deps.storage, slippage)?;

let lp_token = Coin {
denom: lp_token_action.denom.clone(),
amount: match lp_token_action.amount {
Expand All @@ -84,15 +102,27 @@ pub fn withdraw_liquidity(
let zapper = ZAPPER.load(deps.storage)?;
decrement_coin_balance(deps.storage, account_id, &lp_token)?;

let unzap_msg = zapper.withdraw_liquidity_msg(&lp_token, minimum_receive)?;
// Estimate how much coins will be received from zapper with applied slippage
let estimated_coins_out = zapper.estimate_withdraw_liquidity(&deps.querier, &lp_token)?;
let estimated_coins_out_slippage = estimated_coins_out
.iter()
.map(|c| {
let amount = c.amount.checked_mul_floor(Decimal::one() - slippage)?;
Ok(Coin {
denom: c.denom.clone(),
amount,
})
})
.collect::<Result<Vec<Coin>, CheckedMultiplyFractionError>>()?;

let unzap_msg = zapper.withdraw_liquidity_msg(&lp_token, estimated_coins_out_slippage)?;

// After unzap is complete, update account's coin balances
let coins_out = zapper.estimate_withdraw_liquidity(&deps.querier, &lp_token)?;
let update_balances_msgs = update_balances_msgs(
&deps.querier,
&env.contract.address,
account_id,
coins_out.to_denoms(),
estimated_coins_out.to_denoms(),
ChangeExpected::Increase,
)?;

Expand All @@ -102,7 +132,7 @@ pub fn withdraw_liquidity(
.add_attribute("action", "withdraw_liquidity")
.add_attribute("account_id", account_id)
.add_attribute("coin_in", lp_token.to_string())
.add_attribute("coins_out", coins_out.as_slice().to_string()))
.add_attribute("coins_out", estimated_coins_out.as_slice().to_string()))
}

pub fn estimate_provide_liquidity(
Expand Down
13 changes: 13 additions & 0 deletions contracts/credit-manager/tests/helpers/mock_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ pub struct MockEnvBuilder {
pub accounts_to_fund: Vec<AccountToFund>,
pub target_health_factor: Option<Decimal>,
pub max_unlocking_positions: Option<Uint128>,
pub max_slippage: Option<Decimal>,
pub health_contract: Option<HealthContract>,
pub evil_vault: Option<String>,
}
Expand All @@ -133,6 +134,7 @@ impl MockEnv {
accounts_to_fund: vec![],
target_health_factor: None,
max_unlocking_positions: None,
max_slippage: None,
health_contract: None,
evil_vault: None,
}
Expand Down Expand Up @@ -898,6 +900,7 @@ impl MockEnvBuilder {
let incentives = self.get_incentives();
let swapper = self.deploy_swapper().into();
let max_unlocking_positions = self.get_max_unlocking_positions();
let max_slippage = self.get_max_slippage();

let oracle = self.get_oracle().into();
let zapper = self.deploy_zapper(&oracle)?.into();
Expand All @@ -914,6 +917,7 @@ impl MockEnvBuilder {
red_bank,
oracle,
max_unlocking_positions,
max_slippage,
swapper,
zapper,
health_contract,
Expand Down Expand Up @@ -1289,6 +1293,10 @@ impl MockEnvBuilder {
self.max_unlocking_positions.unwrap_or_else(|| Uint128::new(100))
}

fn get_max_slippage(&self) -> Decimal {
self.max_slippage.unwrap_or_else(|| Decimal::percent(99))
}

//--------------------------------------------------------------------------------------------------
// Setter functions
//--------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -1363,6 +1371,11 @@ impl MockEnvBuilder {
self
}

pub fn max_slippage(&mut self, max: Decimal) -> &mut Self {
self.max_slippage = Some(max);
self
}

pub fn evil_vault(&mut self, credit_account: &str) -> &mut Self {
self.evil_vault = Some(credit_account.to_string());
self
Expand Down
Loading

0 comments on commit 40cf48e

Please sign in to comment.