Skip to content

Commit d5fe090

Browse files
committed
test calculate_impact_factor
1 parent 3d69b05 commit d5fe090

File tree

2 files changed

+217
-47
lines changed

2 files changed

+217
-47
lines changed

packages/transmuter_math/proptest-regressions/rebalancing_incentive.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ cc 05667d509ee73bc4dc7059dabfd936ca1b83e0858079419ef4ec85d3134e545d # shrinks to
1010
cc b254ff7ece5fb165f9e4d53746fc7222427cff1d17afbcd15cb8e3b3a7282cfc # shrinks to normalized_balance = 0, ideal_balance_lower_bound = 1, ideal_balance_upper_bound = 0, upper_limit = 0
1111
cc 6bbf8d95569b53ec005ed88b282ebcb88f2a327770cf267800b744274aeaf323 # shrinks to normalized_balance = 340640889074348996, ideal_balance_lower_bound = 340640889074348997, ideal_balance_upper_bound = 340640889074348998, upper_limit = 340640889074348998
1212
cc 4c6ad67ab30e522b8de8314e2219263f2e0dff53070517988a192b64099320ee # shrinks to normalized_balance = 866170838754561023, ideal_balance_lower_bound = 0, ideal_balance_range = 0
13+
cc efe12946674c15ab9d9f273f5dba5da2b33de803bd00a3c5397935644bca7b99 # shrinks to prev_balance = 115660145667945757, update_balance = 481327951497769476, ideal_lower = 115660145667945758, ideal_upper = 115660145667945758, upper_limit = 481327951497769476

packages/transmuter_math/src/rebalancing_incentive.rs

Lines changed: 216 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,81 @@ use cosmwasm_std::{ensure, Decimal, Decimal256, SignedDecimal256, Uint128};
22

33
use crate::TransmuterMathError;
44

5+
#[derive(Debug, PartialEq, Eq)]
6+
pub enum ImpactFactor {
7+
Incentive(Decimal256),
8+
Fee(Decimal256),
9+
None,
10+
}
511

12+
/// combine all the impact factor components
13+
///
14+
/// $$
15+
/// f = \frac{\Vert\vec{\gamma}\Vert}{\sqrt{n}}
16+
/// $$
17+
///
18+
/// That gives a normalized magnitude of the vector of $n$ dimension into $[0,1]$.
19+
/// The reason why it needs to include all dimensions is because the case that swapping with alloyed asset, which will effect overall composition rather than just 2 assets.
20+
pub fn calculate_impact_factor(
21+
impact_factor_param_groups: &[ImpactFactorParamGroup],
22+
) -> Result<ImpactFactor, TransmuterMathError> {
23+
if impact_factor_param_groups.is_empty() {
24+
return Ok(ImpactFactor::None);
25+
}
26+
27+
let mut cumulative_impact_factor_sqaure = Decimal256::zero();
28+
let mut impact_factor_component_sum = SignedDecimal256::zero();
629

30+
// accumulated impact_factor_component_square rounded to smallest possible Decimal256
31+
// when impact_factor_component is smaller then 10^-9 to prevent fee exploitation
32+
let mut lost_rounded_impact_factor_component_square_sum = Decimal256::zero();
33+
34+
let n = Decimal256::from_atomics(impact_factor_param_groups.len() as u64, 0)?;
35+
36+
for impact_factor_params in impact_factor_param_groups {
37+
// optimiztion: if there is no change in balance, the result will be 0 anyway, accumulating 0 has no effect
38+
if impact_factor_params.has_no_change_in_balance() {
39+
continue;
40+
}
41+
42+
let impact_factor_component = impact_factor_params.calculate_impact_factor_component()?;
43+
44+
// when impact_factor_component<= 10^-9, the result after squaring will be 0, then
45+
// - if total is counted as incentive, there will be no incentive and it's fine
46+
// since it's neglectible and will not overincentivize and drain incentive pool
47+
// - if total is counted as fee, it could be exploited by
48+
// making swap with small impact_factor_component over and over again to avoid being counted as fee
49+
let impact_factor_component_dec = impact_factor_component.abs_diff(SignedDecimal256::zero());
50+
if impact_factor_component_dec <= Decimal256::raw(1_000_000_000u128) {
51+
lost_rounded_impact_factor_component_square_sum = lost_rounded_impact_factor_component_square_sum.checked_add(Decimal256::raw(1u128))?;
52+
}
53+
54+
let impact_factor_component_square = impact_factor_component_dec.checked_pow(2)?;
55+
56+
impact_factor_component_sum =
57+
impact_factor_component_sum.checked_add(impact_factor_component)?;
58+
cumulative_impact_factor_sqaure =
59+
cumulative_impact_factor_sqaure.checked_add(impact_factor_component_square)?;
60+
}
61+
62+
if impact_factor_component_sum.is_zero() {
63+
Ok(ImpactFactor::None)
64+
} else if impact_factor_component_sum.is_negative() {
65+
Ok(ImpactFactor::Incentive(
66+
cumulative_impact_factor_sqaure
67+
.checked_div(n)?
68+
.sqrt()
69+
))
70+
} else {
71+
// add back lost impact_factor_component_square_sum before normalizing
72+
Ok(ImpactFactor::Fee(
73+
cumulative_impact_factor_sqaure
74+
.checked_add(lost_rounded_impact_factor_component_square_sum)?
75+
.checked_div(n)?
76+
.sqrt()
77+
))
78+
}
79+
}
780

881
/// Calculating impact factor component
982
///
@@ -159,53 +232,6 @@ impl ImpactFactorParamGroup {
159232
}
160233
}
161234

162-
pub enum PayoffType {
163-
Incentive,
164-
Fee,
165-
}
166-
167-
/// combine all the impact factor components
168-
///
169-
/// $$
170-
/// f = \frac{\Vert\vec{\gamma}\Vert}{\sqrt{n}}
171-
/// $$
172-
///
173-
/// That gives a normalized magnitude of the vector of $n$ dimension into $[0,1]$.
174-
/// The reason why it needs to include all dimensions is because the case that swapping with alloyed asset, which will effect overall composition rather than just 2 assets.
175-
pub fn calculate_impact_factor(
176-
impact_factor_param_groups: &[ImpactFactorParamGroup],
177-
) -> Result<(PayoffType, Decimal256), TransmuterMathError> {
178-
let mut cumulative_impact_factor_sqaure = Decimal256::zero();
179-
let mut impact_factor_component_sum = SignedDecimal256::zero();
180-
181-
let n = Decimal256::from_atomics(impact_factor_param_groups.len() as u64, 0)?;
182-
183-
for impact_factor_params in impact_factor_param_groups {
184-
// optimiztion: if there is no change in balance, the result will be 0 anyway, accumulating 0 has no effect
185-
if impact_factor_params.has_no_change_in_balance() {
186-
continue;
187-
}
188-
189-
let impact_factor_component = impact_factor_params.calculate_impact_factor_component()?;
190-
let impact_factor_component_square =
191-
Decimal256::try_from(impact_factor_component.checked_pow(2)?)?;
192-
193-
impact_factor_component_sum =
194-
impact_factor_component_sum.checked_add(impact_factor_component)?;
195-
cumulative_impact_factor_sqaure =
196-
cumulative_impact_factor_sqaure.checked_add(impact_factor_component_square)?;
197-
}
198-
199-
let payoff_type = if impact_factor_component_sum.is_negative() {
200-
PayoffType::Incentive
201-
} else {
202-
PayoffType::Fee
203-
};
204-
205-
let impact_factor = cumulative_impact_factor_sqaure.checked_div(n)?.sqrt();
206-
207-
Ok((payoff_type, impact_factor))
208-
}
209235

210236
/// Calculate the rebalancing fee
211237
///
@@ -799,4 +825,147 @@ mod tests {
799825
let result = group.calculate_impact_factor_component();
800826
assert_eq!(result, expected);
801827
}
828+
829+
#[rstest]
830+
#[case::empty_input(Vec::new(), Ok(ImpactFactor::None))]
831+
#[case::all_no_change(
832+
vec![
833+
ImpactFactorParamGroup {
834+
prev_normalized_balance: Decimal::percent(50),
835+
update_normalized_balance: Decimal::percent(50),
836+
ideal_balance_lower_bound: Decimal::percent(40),
837+
ideal_balance_upper_bound: Decimal::percent(60),
838+
upper_limit: Decimal::percent(100),
839+
},
840+
ImpactFactorParamGroup {
841+
prev_normalized_balance: Decimal::percent(70),
842+
update_normalized_balance: Decimal::percent(70),
843+
ideal_balance_lower_bound: Decimal::percent(40),
844+
ideal_balance_upper_bound: Decimal::percent(60),
845+
upper_limit: Decimal::percent(100),
846+
}
847+
],
848+
Ok(ImpactFactor::None)
849+
)]
850+
#[case::all_positive_resulted_in_fee(
851+
vec![
852+
ImpactFactorParamGroup {
853+
prev_normalized_balance: Decimal::percent(70),
854+
update_normalized_balance: Decimal::percent(80),
855+
ideal_balance_lower_bound: Decimal::percent(40),
856+
ideal_balance_upper_bound: Decimal::percent(60),
857+
upper_limit: Decimal::percent(100),
858+
},
859+
ImpactFactorParamGroup {
860+
prev_normalized_balance: Decimal::percent(65),
861+
update_normalized_balance: Decimal::percent(75),
862+
ideal_balance_lower_bound: Decimal::percent(40),
863+
ideal_balance_upper_bound: Decimal::percent(60),
864+
upper_limit: Decimal::percent(100),
865+
},
866+
],
867+
Ok(ImpactFactor::Fee(Decimal256::from_str("0.159344359799774525").unwrap()))
868+
)]
869+
#[case::all_negative_resulted_in_incentive(
870+
vec![
871+
ImpactFactorParamGroup {
872+
prev_normalized_balance: Decimal::percent(70),
873+
update_normalized_balance: Decimal::percent(60),
874+
ideal_balance_lower_bound: Decimal::percent(40),
875+
ideal_balance_upper_bound: Decimal::percent(60),
876+
upper_limit: Decimal::percent(100),
877+
},
878+
ImpactFactorParamGroup {
879+
prev_normalized_balance: Decimal::percent(35),
880+
update_normalized_balance: Decimal::percent(45),
881+
ideal_balance_lower_bound: Decimal::percent(40),
882+
ideal_balance_upper_bound: Decimal::percent(60),
883+
upper_limit: Decimal::percent(100),
884+
},
885+
],
886+
Ok(ImpactFactor::Incentive(Decimal256::from_str("0.045554311678478909").unwrap())))
887+
]
888+
#[case::mixed_positive_and_negative_resulted_in_fee(
889+
vec![
890+
ImpactFactorParamGroup {
891+
prev_normalized_balance: Decimal::percent(70),
892+
update_normalized_balance: Decimal::percent(80),
893+
ideal_balance_lower_bound: Decimal::percent(40),
894+
ideal_balance_upper_bound: Decimal::percent(60),
895+
upper_limit: Decimal::percent(100),
896+
},
897+
ImpactFactorParamGroup {
898+
prev_normalized_balance: Decimal::percent(35),
899+
update_normalized_balance: Decimal::percent(45),
900+
ideal_balance_lower_bound: Decimal::percent(40),
901+
ideal_balance_upper_bound: Decimal::percent(60),
902+
upper_limit: Decimal::percent(100),
903+
},
904+
],
905+
Ok(ImpactFactor::Fee(Decimal256::from_str("0.133042080983800009").unwrap()))
906+
)]
907+
#[case::mixed_positive_and_negative_resulted_in_incentive(
908+
vec![
909+
ImpactFactorParamGroup {
910+
prev_normalized_balance: Decimal::percent(70),
911+
update_normalized_balance: Decimal::percent(60),
912+
ideal_balance_lower_bound: Decimal::percent(40),
913+
ideal_balance_upper_bound: Decimal::percent(60),
914+
upper_limit: Decimal::percent(100),
915+
},
916+
ImpactFactorParamGroup {
917+
prev_normalized_balance: Decimal::percent(35),
918+
update_normalized_balance: Decimal::percent(30),
919+
ideal_balance_lower_bound: Decimal::percent(40),
920+
ideal_balance_upper_bound: Decimal::percent(60),
921+
upper_limit: Decimal::percent(100),
922+
},
923+
],
924+
Ok(ImpactFactor::Incentive(Decimal256::from_str("0.055242717280199025").unwrap()))
925+
)]
926+
#[case::loss_rounding_fee(
927+
vec![
928+
ImpactFactorParamGroup {
929+
prev_normalized_balance: Decimal::percent(60),
930+
update_normalized_balance: Decimal::from_atomics(600_000_000_000_000_001u128, 18).unwrap(),
931+
ideal_balance_lower_bound: Decimal::percent(40),
932+
ideal_balance_upper_bound: Decimal::percent(60),
933+
upper_limit: Decimal::percent(100),
934+
},
935+
ImpactFactorParamGroup {
936+
prev_normalized_balance: Decimal::percent(60),
937+
update_normalized_balance: Decimal::from_atomics(600_000_000_000_000_001u128, 18).unwrap(),
938+
ideal_balance_lower_bound: Decimal::percent(40),
939+
ideal_balance_upper_bound: Decimal::percent(60),
940+
upper_limit: Decimal::percent(100),
941+
},
942+
],
943+
Ok(ImpactFactor::Fee(Decimal256::from_str("0.000000001").unwrap()))
944+
)]
945+
#[case::no_loss_rounding_incentive(
946+
vec![
947+
ImpactFactorParamGroup {
948+
prev_normalized_balance: Decimal::from_atomics(600_000_000_000_000_001u128, 18).unwrap(),
949+
update_normalized_balance: Decimal::percent(60),
950+
ideal_balance_lower_bound: Decimal::percent(40),
951+
ideal_balance_upper_bound: Decimal::percent(60),
952+
upper_limit: Decimal::percent(100),
953+
},
954+
ImpactFactorParamGroup {
955+
prev_normalized_balance: Decimal::from_atomics(600_000_000_000_000_001u128, 18).unwrap(),
956+
update_normalized_balance: Decimal::percent(60),
957+
ideal_balance_lower_bound: Decimal::percent(40),
958+
ideal_balance_upper_bound: Decimal::percent(60),
959+
upper_limit: Decimal::percent(100),
960+
},
961+
],
962+
Ok(ImpactFactor::None)
963+
)]
964+
fn test_calculate_impact_factor(
965+
#[case] input_param_groups: Vec<ImpactFactorParamGroup>,
966+
#[case] expected: Result<ImpactFactor, TransmuterMathError>,
967+
) {
968+
let result = calculate_impact_factor(&input_param_groups);
969+
assert_eq!(result, expected);
970+
}
802971
}

0 commit comments

Comments
 (0)