@@ -2,8 +2,81 @@ use cosmwasm_std::{ensure, Decimal, Decimal256, SignedDecimal256, Uint128};
22
33use 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