Skip to content

Commit 6f43f25

Browse files
bobthebuidlrpiobab
andauthored
Add slippage, add max borrow for swap target (#349)
* Add slippage, add max borrow for swap target * Fix formulas, comments. * Fix tests. * Add prop test case for Borrow and Swap. --------- Co-authored-by: piobab <[email protected]>
1 parent f99f6c0 commit 6f43f25

19 files changed

+214
-50
lines changed

packages/health-computer/src/health_computer.rs

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pub struct HealthComputer {
3131
}
3232

3333
impl HealthComputer {
34-
pub fn compute_health(&self) -> HealthResult<Health> {
34+
pub fn compute_health(&self) -> mars_types::health::HealthResult<Health> {
3535
let CollateralValue {
3636
total_collateral_value,
3737
max_ltv_adjusted_collateral,
@@ -138,6 +138,7 @@ impl HealthComputer {
138138
from_denom: &str,
139139
to_denom: &str,
140140
kind: &SwapKind,
141+
slippage: Decimal,
141142
) -> HealthResult<Uint128> {
142143
// Both deposits and lends should be considered, as the funds can automatically be un-lent and
143144
// and also used to swap.
@@ -172,22 +173,29 @@ impl HealthComputer {
172173
// Swapping that asset for an asset with the same price, but 0.8 max ltv results in a collateral_value of 0.8.
173174
// Therefore, when the asset that is swapped to has a higher or equal max ltv than the asset swapped from,
174175
// the collateral value will increase and we can allow the full balance to be swapped.
175-
let swappable_amount = if to_ltv >= from_ltv {
176+
// The ltv_out is adjusted for slippage, as the swap_out_value can drop by the slippage.
177+
let to_ltv_slippage_corrected = to_ltv.checked_mul(Decimal::one() - slippage)?;
178+
let swappable_amount = if to_ltv_slippage_corrected >= from_ltv {
176179
from_coin.amount
177180
} else {
178181
// In order to calculate the output of the swap, the formula looks like this:
179182
// 1 = (collateral_value + to_amount * to_price * to_ltv - from_amount * from_price * from_ltv) / debt_value
180183
// The unknown variables here are to_amount and from_amount. In order to only have 1 unknown variable, from_amount,
181184
// to_amount can be replaced by:
182-
// to_amount = from_amount * from_price / to_price
185+
// to_amount = slippage * from_amount * from_price / to_price
183186
// This results in the following formula:
184-
// 1 = (collateral_value + from_amount * from_price / to_price * to_price * to_ltv - from_amount * from_price * from_ltv) / debt_value
187+
// 1 = (collateral_value + slippage * from_amount * from_price / to_price * to_price * to_ltv - from_amount * from_price * from_ltv) / debt_value
188+
// debt_value = collateral_value + slippage * from_amount * from_price * to_ltv - from_amount * from_price * from_ltv
189+
// slippage * from_amount * from_price * to_ltv - from_amount * from_price * from_ltv = debt_value - collateral_value
190+
// from_amount * (slippage * from_price * to_ltv - from_price * from_ltv) = debt_value - collateral_value
185191
// Rearranging this formula to isolate from_amount results in the following formula:
186-
// from_amount = (collateral_value - debt_value) / (from_price * ( from_ltv - to_ltv))
192+
// from_amount = (debt_value - collateral_value) / (from_price * (slippage * to_ltv - from_ltv))
193+
// Rearranging to avoid negative numbers for the denominator (to_ltv_slippage_corrected < from_ltv):
194+
// from_amount = (collateral_value - debt_value) / (from_price * (from_ltv - slippage * to_ltv)
187195
let amount = total_max_ltv_adjusted_value
188196
.checked_sub(debt_value)?
189197
.checked_sub(Uint128::one())?
190-
.checked_div_floor(from_price.checked_mul(from_ltv - to_ltv)?)?;
198+
.checked_div_floor(from_price.checked_mul(from_ltv - to_ltv_slippage_corrected)?)?;
191199

192200
// Cap the swappable amount at the current balance of the coin
193201
min(amount, from_coin.amount)
@@ -219,14 +227,19 @@ impl HealthComputer {
219227
// The total swappable amount for margin is represented by the available coin balance + the
220228
// the maximum amount that can be borrowed (and then swapped).
221229
// This is represented by the formula:
222-
// 1 = (collateral_after_swap + borrow_amount * borrow_price * to_ltv) / (debt + borrow_amount * borrow_price)
230+
// 1 = (collateral_after_swap + slippage * borrow_amount * borrow_price * to_ltv) / (debt + borrow_amount * borrow_price)
231+
// debt + borrow_amount * borrow_price = collateral_after_swap + slippage * borrow_amount * borrow_price * to_ltv
232+
// borrow_amount * borrow_price - slippage * borrow_amount * borrow_price * to_ltv = collateral_after_swap - debt
233+
// borrow_amount * borrow_price * (1 - slippage * to_ltv) = collateral_after_swap - debt
223234
// Rearranging this results in:
224-
// borrow_amount = (collateral_after_swap - debt) / ((1 - to_ltv) * borrow_price)
235+
// borrow_amount = (collateral_after_swap - debt) / (borrow_price * (1 - slippage * to_ltv))
225236
let borrow_amount = total_max_ltv_adjust_value_after_swap
226237
.checked_sub(debt_value)?
227238
.checked_sub(Uint128::one())?
228239
.checked_div_floor(
229-
Decimal::one().checked_sub(to_ltv)?.checked_mul(*from_price)?,
240+
Decimal::one()
241+
.checked_sub(to_ltv_slippage_corrected)?
242+
.checked_mul(*from_price)?,
230243
)?;
231244

232245
// The total amount that can be swapped is then the balance of the coin + the additional amount
@@ -359,6 +372,32 @@ impl HealthComputer {
359372
.checked_mul(Decimal::one().checked_sub(checked_vault_max_ltv)?)?,
360373
)?
361374
}
375+
376+
BorrowTarget::Swap {
377+
slippage,
378+
denom_out,
379+
} => {
380+
let denom_out_ltv = self.get_coin_max_ltv(denom_out).unwrap();
381+
382+
// The max borrow for swap can be calculated as:
383+
// 1 = (total_max_ltv_adjusted_value + (denom_amount_out * denom_price_out * denom_out_ltv)) / (debt_value + (max_borrow_denom_amount * borrow_denom_price))
384+
// denom_amount_out can be replaced by:
385+
// denom_amount_out = slippage * max_borrow_denom_amount * borrow_denom_price / denom_price_out
386+
// This results in the following formula:
387+
// 1 = (total_max_ltv_adjusted_value + (slippage * max_borrow_denom_amount * borrow_denom_price * denom_out_ltv)) / (debt_value + (max_borrow_denom_amount * borrow_denom_price))
388+
// Re-arranging this to isolate borrow denom amount renders:
389+
// max_borrow_denom_amount = (total_max_ltv_adjusted_value - debt_value) / (borrow_denom_price * (1 - slippage * denom_out_ltv))
390+
let out_ltv_slippage_corrected =
391+
denom_out_ltv.checked_mul(Decimal::one() - slippage)?;
392+
total_max_ltv_adjusted_value
393+
.checked_sub(debt_value)?
394+
.checked_sub(Uint128::one())?
395+
.checked_div_floor(
396+
Decimal::one()
397+
.checked_sub(out_ltv_slippage_corrected)?
398+
.checked_mul(borrow_denom_price)?,
399+
)?
400+
}
362401
};
363402

364403
Ok(max_borrow_amount)

packages/health-computer/src/javascript.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use mars_types::health::{BorrowTarget, HealthValuesResponse, SwapKind};
1+
use mars_types::health::{BorrowTarget, HealthValuesResponse, Slippage, SwapKind};
22
use wasm_bindgen::prelude::*;
33

44
use crate::HealthComputer;
@@ -33,6 +33,9 @@ pub fn max_swap_estimate_js(
3333
from_denom: String,
3434
to_denom: String,
3535
kind: SwapKind,
36+
slippage: Slippage,
3637
) -> String {
37-
c.max_swap_amount_estimate(&from_denom, &to_denom, &kind).unwrap().to_string()
38+
c.max_swap_amount_estimate(&from_denom, &to_denom, &kind, slippage.as_decimal())
39+
.unwrap()
40+
.to_string()
3841
}

packages/health-computer/tests/tests/helpers/prop_test_runner_borrow.rs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use cosmwasm_std::{Coin, StdResult, Uint128};
1+
use cosmwasm_std::{Coin, Decimal, StdResult, Uint128};
22
use mars_rover_health_computer::HealthComputer;
33
use mars_types::{
44
adapters::vault::{CoinValue, VaultPositionValue},
@@ -27,6 +27,10 @@ pub fn max_borrow_prop_test_runner(cases: u32, target: &BorrowTarget) {
2727
}
2828
}),
2929
|h| {
30+
let mut keys = h.denoms_data.params.keys();
31+
let denom_to_borrow = keys.next().unwrap();
32+
let denom_to_swap_to = keys.next().unwrap();
33+
3034
let updated_target = match target {
3135
BorrowTarget::Deposit => BorrowTarget::Deposit,
3236
BorrowTarget::Wallet => BorrowTarget::Wallet,
@@ -38,9 +42,15 @@ pub fn max_borrow_prop_test_runner(cases: u32, target: &BorrowTarget) {
3842
address: vault_position.vault.address.clone(),
3943
}
4044
}
45+
BorrowTarget::Swap {
46+
denom_out: _,
47+
slippage,
48+
} => BorrowTarget::Swap {
49+
denom_out: denom_to_swap_to.clone(),
50+
slippage: *slippage,
51+
},
4152
};
4253

43-
let denom_to_borrow = h.denoms_data.params.keys().next().unwrap();
4454
let max_borrow =
4555
h.max_borrow_amount_estimate(denom_to_borrow, &updated_target).unwrap();
4656

@@ -107,6 +117,26 @@ fn add_borrow(
107117
});
108118
}
109119
}
120+
BorrowTarget::Swap {
121+
denom_out,
122+
slippage,
123+
} => {
124+
let price_in = new_h.denoms_data.prices.get(denom).unwrap();
125+
let price_out = new_h.denoms_data.prices.get(denom_out).unwrap();
126+
127+
// denom_amount_out = (1 - slippage) * max_borrow_denom_amount * borrow_denom_price / denom_price_out
128+
// Use ceil math to avoid rounding errors in the test otheriwse we might end up with a health that is
129+
// slightly above max_ltv.
130+
let slippage = Decimal::one() - slippage;
131+
let amount = amount.mul_ceil(slippage);
132+
let value_in = amount.mul_ceil(*price_in);
133+
let amount_out = value_in.div_ceil(*price_out);
134+
135+
new_h.positions.deposits.push(Coin {
136+
denom: denom_out.to_string(),
137+
amount: amount_out,
138+
});
139+
}
110140
}
111141
Ok(new_h)
112142
}

packages/health-computer/tests/tests/helpers/prop_test_runner_swap.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use cosmwasm_std::{Coin, StdResult, Uint128};
1+
use cosmwasm_std::{Coin, Decimal, StdResult, Uint128};
22
use mars_rover_health_computer::HealthComputer;
33
use mars_types::{credit_manager::DebtAmount, health::SwapKind};
44
use proptest::{
@@ -34,7 +34,9 @@ pub fn max_swap_prop_test_runner(cases: u32, kind: &SwapKind) {
3434
let from_denom = h.denoms_data.params.keys().next().unwrap();
3535
let to_denom = h.denoms_data.params.keys().nth(1).unwrap();
3636

37-
let max_swap = h.max_swap_amount_estimate(from_denom, to_denom, kind).unwrap();
37+
let max_swap = h
38+
.max_swap_amount_estimate(from_denom, to_denom, kind, Decimal::zero())
39+
.unwrap();
3840

3941
let health_before = h.compute_health().unwrap();
4042
if health_before.is_above_max_ltv() {

packages/health-computer/tests/tests/helpers/prop_test_strategies.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ fn random_coin_info() -> impl Strategy<Value = AssetParams> {
7575
}
7676

7777
fn random_denoms_data() -> impl Strategy<Value = DenomsData> {
78-
vec((random_coin_info(), random_price()), 1..=5).prop_map(|info| {
78+
vec((random_coin_info(), random_price()), 2..=5).prop_map(|info| {
7979
let mut prices = HashMap::new();
8080
let mut params = HashMap::new();
8181

packages/health-computer/tests/tests/test_max_borrow_prop.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use cosmwasm_std::Addr;
1+
use cosmwasm_std::{Addr, Decimal};
22
use mars_types::health::BorrowTarget;
33

44
use super::helpers::max_borrow_prop_test_runner;
@@ -22,3 +22,25 @@ fn max_borrow_amount_vault_renders_healthy_max_ltv() {
2222
},
2323
);
2424
}
25+
26+
#[test]
27+
fn max_borrow_amount_swap_no_slippage_renders_healthy_max_ltv() {
28+
max_borrow_prop_test_runner(
29+
2000,
30+
&BorrowTarget::Swap {
31+
denom_out: "abc".to_string(),
32+
slippage: Decimal::zero(),
33+
},
34+
);
35+
}
36+
37+
#[test]
38+
fn max_borrow_amount_swap_renders_healthy_max_ltv() {
39+
max_borrow_prop_test_runner(
40+
2000,
41+
&BorrowTarget::Swap {
42+
denom_out: "abc".to_string(),
43+
slippage: Decimal::percent(1),
44+
},
45+
);
46+
}

packages/health-computer/tests/tests/test_max_swap.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::collections::HashMap;
22

3-
use cosmwasm_std::{coin, Uint128};
3+
use cosmwasm_std::{coin, Decimal, Uint128};
44
use mars_rover_health_computer::{DenomsData, HealthComputer, VaultsData};
55
use mars_types::{
66
credit_manager::Positions,
@@ -43,8 +43,9 @@ fn max_swap_default() {
4343
vaults_data,
4444
};
4545

46-
let max_borrow_amount =
47-
h.max_swap_amount_estimate(&udai.denom, &umars.denom, &SwapKind::Default).unwrap();
46+
let max_borrow_amount = h
47+
.max_swap_amount_estimate(&udai.denom, &umars.denom, &SwapKind::Default, Decimal::zero())
48+
.unwrap();
4849
assert_eq!(Uint128::new(1200), max_borrow_amount);
4950
}
5051

@@ -82,7 +83,8 @@ fn max_swap_margin() {
8283
vaults_data,
8384
};
8485

85-
let max_borrow_amount =
86-
h.max_swap_amount_estimate(&udai.denom, &umars.denom, &SwapKind::Margin).unwrap();
86+
let max_borrow_amount = h
87+
.max_swap_amount_estimate(&udai.denom, &umars.denom, &SwapKind::Margin, Decimal::zero())
88+
.unwrap();
8789
assert_eq!(Uint128::new(31351), max_borrow_amount);
8890
}

packages/health-computer/tests/tests/test_max_swap_validation.rs

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ fn missing_price_data() {
5555
vaults_data,
5656
};
5757

58-
let err: HealthError =
59-
h.max_swap_amount_estimate(&udai.denom, &umars.denom, &SwapKind::Default).unwrap_err();
58+
let err: HealthError = h
59+
.max_swap_amount_estimate(&udai.denom, &umars.denom, &SwapKind::Default, Decimal::zero())
60+
.unwrap_err();
6061
assert_eq!(err, HealthError::MissingPrice(udai.denom));
6162
}
6263

@@ -102,8 +103,9 @@ fn missing_params() {
102103
vaults_data,
103104
};
104105

105-
let err: HealthError =
106-
h.max_swap_amount_estimate(&umars.denom, &udai.denom, &SwapKind::Default).unwrap_err();
106+
let err: HealthError = h
107+
.max_swap_amount_estimate(&umars.denom, &udai.denom, &SwapKind::Default, Decimal::zero())
108+
.unwrap_err();
107109
assert_eq!(err, HealthError::MissingParams(umars.denom));
108110
}
109111

@@ -134,8 +136,9 @@ fn deposit_not_present() {
134136
vaults_data,
135137
};
136138

137-
let max_withdraw_amount =
138-
h.max_swap_amount_estimate("xyz", &udai.denom, &SwapKind::Default).unwrap();
139+
let max_withdraw_amount = h
140+
.max_swap_amount_estimate("xyz", &udai.denom, &SwapKind::Default, Decimal::zero())
141+
.unwrap();
139142
assert_eq!(max_withdraw_amount, Uint128::zero());
140143
}
141144

@@ -186,8 +189,9 @@ fn zero_when_unhealthy() {
186189

187190
let health = h.compute_health().unwrap();
188191
assert!(health.max_ltv_health_factor < Some(Decimal::one()));
189-
let max_swap_amount =
190-
h.max_swap_amount_estimate(&udai.denom, &umars.denom, &SwapKind::Default).unwrap();
192+
let max_swap_amount = h
193+
.max_swap_amount_estimate(&udai.denom, &umars.denom, &SwapKind::Default, Decimal::zero())
194+
.unwrap();
191195
assert_eq!(Uint128::zero(), max_swap_amount);
192196
}
193197

@@ -226,8 +230,9 @@ fn no_debts() {
226230
vaults_data,
227231
};
228232

229-
let max_swap_amount =
230-
h.max_swap_amount_estimate(&ustars.denom, &umars.denom, &SwapKind::Default).unwrap();
233+
let max_swap_amount = h
234+
.max_swap_amount_estimate(&ustars.denom, &umars.denom, &SwapKind::Default, Decimal::zero())
235+
.unwrap();
231236
assert_eq!(deposit_amount, max_swap_amount);
232237
}
233238

@@ -271,8 +276,9 @@ fn should_allow_max_swap() {
271276
};
272277

273278
// Max when debt value is smaller than collateral value - withdraw denom value
274-
let max_swap_amount =
275-
h.max_swap_amount_estimate(&udai.denom, &umars.denom, &SwapKind::Default).unwrap();
279+
let max_swap_amount = h
280+
.max_swap_amount_estimate(&udai.denom, &umars.denom, &SwapKind::Default, Decimal::zero())
281+
.unwrap();
276282
assert_eq!(deposit_amount, max_swap_amount);
277283
}
278284

@@ -354,10 +360,12 @@ fn hls_with_max_withdraw() {
354360
vaults_data,
355361
};
356362

357-
let max_before =
358-
h.max_swap_amount_estimate(&ustars.denom, &uatom.denom, &SwapKind::Default).unwrap();
363+
let max_before = h
364+
.max_swap_amount_estimate(&ustars.denom, &uatom.denom, &SwapKind::Default, Decimal::zero())
365+
.unwrap();
359366
h.kind = AccountKind::HighLeveredStrategy;
360-
let max_after =
361-
h.max_swap_amount_estimate(&ustars.denom, &uatom.denom, &SwapKind::Default).unwrap();
367+
let max_after = h
368+
.max_swap_amount_estimate(&ustars.denom, &uatom.denom, &SwapKind::Default, Decimal::zero())
369+
.unwrap();
362370
assert!(max_after > max_before)
363371
}

0 commit comments

Comments
 (0)