Skip to content

Commit e7ab5d8

Browse files
committed
added tests, corrections to collateral valuation
1 parent aed6f3a commit e7ab5d8

File tree

4 files changed

+180
-28
lines changed

4 files changed

+180
-28
lines changed

pyth-sdk/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ borsh = "0.9"
1919
borsh-derive = "0.9.0"
2020
serde = { version = "1.0.136", features = ["derive"] }
2121
schemars = "0.8.8"
22+
thiserror = "1.0.24"
2223

2324
[dev-dependencies]
2425
serde_json = "1.0.79"

pyth-sdk/src/error.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
use solana_program::program_error::ProgramError;
21
use thiserror::Error;
32

4-
#[derive(Error, Debug, Copy, Clone)]
3+
#[derive(Error, Debug, Copy, Clone, PartialEq)]
54
pub enum LiquidityOracleError {
65
#[error("deposits exceeds max depositable")]
76
ExceedsMaxDeposits,
87
#[error("initial discount rate should not be greater than final discount rate")]
98
InitialDiscountExceedsFinalDiscount,
109
#[error("final discount rate should not be greater than the discount precision")]
1110
FinalDiscountExceedsPrecision,
11+
#[error("None encountered")]
12+
NoneEncountered
1213
}

pyth-sdk/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use schemars::JsonSchema;
88
use std::fmt;
99

1010
pub mod utils;
11+
pub mod error;
1112

1213
mod price;
1314
pub use price::Price;

pyth-sdk/src/price.rs

Lines changed: 175 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -99,49 +99,83 @@ impl Price {
9999
/// scaled by the proportion of max depositable amount that has been deposited.
100100
/// This essentially assumes a linear liquidity cumulative density function,
101101
/// which has been shown to be a reasonable assumption for many crypto tokens in literature.
102-
pub fn get_collateral_valuation_price(&self, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) -> Price {
102+
///
103+
/// Args
104+
/// deposits: u64, quantity of token deposited in the protocol
105+
/// max_deposits: u64, max quantity of token that can be deposited in the protocol
106+
/// discount_initial: u64, initial discount rate at 0 deposits (units given by discount_precision)
107+
/// discount_final: u64, final discount rate at max_deposits deposits (units given by discount_precision)
108+
/// discount_precision: u64, the precision used for discounts
109+
///
110+
/// Logic
111+
/// collateral_valuation_price = (deposits * price * (discount_precision-discount_final) + (max_deposits - deposits) * price * (discount_precision - discount_initial)) / (max_deposits * discount_precision)
112+
pub fn get_collateral_valuation_price(&self, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) -> Result<Price, LiquidityOracleError> {
103113
if max_deposits < deposits {
104-
return err!(LiquidityOracleError::ExceedsMaxDeposits);
114+
return Err(LiquidityOracleError::ExceedsMaxDeposits.into());
105115
}
106116

107117
if discount_initial > discount_final {
108-
return err!(LiquidityOracleError::InitialDiscountExceedsFinalDiscount);
118+
return Err(LiquidityOracleError::InitialDiscountExceedsFinalDiscount.into());
109119
}
110120

111121
if discount_final > discount_precision {
112-
return err!(LiquidityOracleError::FinalDiscountExceedsPrecision);
122+
return Err(LiquidityOracleError::FinalDiscountExceedsPrecision.into());
113123
}
114-
115-
// get initial expo; use later to convert conf to the right expo
116-
let expo_init = self.expo;
117124

118-
let left = self.cmul(deposits, 0)?.cmul(discount_precision-discount_final, 0)?;
119-
let right = self.cmul(max_deposits-deposits, 0)?.cmul(discount_precision-discount_initial, 0)?;
125+
let remaining_depositable = (max_deposits-deposits) as i64;
126+
let diff_discount_precision_initial = (discount_precision-discount_initial) as i64;
127+
let diff_discount_precision_final = (discount_precision-discount_final) as i64;
128+
129+
let mut left = self.cmul(deposits as i64, 0).
130+
ok_or(LiquidityOracleError::NoneEncountered)?.
131+
cmul(diff_discount_precision_final, 0).
132+
ok_or(LiquidityOracleError::NoneEncountered)?
133+
;
134+
let mut right = self.cmul(remaining_depositable, 0).
135+
ok_or(LiquidityOracleError::NoneEncountered)?.
136+
cmul(diff_discount_precision_initial, 0).
137+
ok_or(LiquidityOracleError::NoneEncountered)?
138+
;
139+
// scale left and right to match expo
140+
if left.expo > right.expo {
141+
left = left.
142+
scale_to_exponent(right.expo).
143+
ok_or(LiquidityOracleError::NoneEncountered)?
144+
;
145+
}
146+
else if left.expo < right.expo {
147+
right = right.
148+
scale_to_exponent(left.expo).
149+
ok_or(LiquidityOracleError::NoneEncountered)?
150+
;
151+
}
120152

121-
let numer = left.add(&right)?;
153+
let numer = left.add(&right).ok_or(LiquidityOracleError::NoneEncountered)?;
122154
let denom = max_deposits * discount_precision;
123155

124156
// TODO: divide price by a constant (denom)
125157
// can denote denom as a Price with tiny confidence,
126158
// perform the div, and throw away the resulting confidence,
127159
// since all we care about from the div is price and expo
128160
let denom_as_price = Price {
129-
price: denom,
161+
price: denom as i64,
130162
conf: 1,
131163
expo: 0,
132164
publish_time: self.publish_time,
133165
};
134-
let price_discounted = numer.div(denom_as_price)?;
166+
let price_discounted = numer.div(&denom_as_price).ok_or(LiquidityOracleError::NoneEncountered)?;
135167

136168
// we want to scale the original confidence to the new expo
137169
let conf_scaled = self.scale_confidence_to_exponent(price_discounted.expo);
138170

139-
Price {
140-
price: price_discounted.price,
141-
conf: conf_scaled,
142-
expo: price_discounted.expo,
143-
publish_time: self.publish_time,
144-
}
171+
Ok(
172+
Price {
173+
price: price_discounted.price,
174+
conf: conf_scaled.ok_or(LiquidityOracleError::NoneEncountered)?,
175+
expo: price_discounted.expo,
176+
publish_time: self.publish_time,
177+
}
178+
)
145179
}
146180

147181
/// Get the price of a basket of currencies.
@@ -437,12 +471,12 @@ impl Price {
437471

438472
#[cfg(test)]
439473
mod test {
440-
use crate::price::{
474+
use crate::{price::{
441475
Price,
442476
MAX_PD_V_U64,
443477
PD_EXPO,
444478
PD_SCALE,
445-
};
479+
}, error::LiquidityOracleError};
446480

447481
const MAX_PD_V_I64: i64 = MAX_PD_V_U64 as i64;
448482
const MIN_PD_V_I64: i64 = -MAX_PD_V_I64;
@@ -987,22 +1021,137 @@ mod test {
9871021

9881022
#[test]
9891023
fn test_get_collateral_valuation_price() {
990-
fn succeeds(price1: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64, expected: Price) {
991-
assert_eq!(price1.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision).unwrap(), expected);
1024+
fn succeeds(price: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64, mut expected: Price) {
1025+
let mut price_collat = price.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision).unwrap();
1026+
print!("Output: price is {}, conf is {}, expo is {}, ts is {}", price_collat.price, price_collat.conf, price_collat.expo, price_collat.publish_time);
1027+
print!("Exepcted: price is {}, conf is {}, expo is {}, ts is {}", expected.price, expected.conf, expected.expo, expected.publish_time);
1028+
1029+
// scale price_collat and expected to match in expo
1030+
if price_collat.expo > expected.expo {
1031+
price_collat = price_collat.scale_to_exponent(expected.expo).unwrap();
1032+
}
1033+
else if price_collat.expo < expected.expo {
1034+
expected = expected.scale_to_exponent(price_collat.expo).unwrap();
1035+
}
1036+
1037+
assert_eq!(price_collat, expected);
9921038
}
9931039

994-
fn fails(price1: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) {
995-
assert_eq!(price1.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision), None);
1040+
fn fails_exceeds_max_deposits(price: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) {
1041+
let result = price.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision);
1042+
assert_eq!(result.unwrap_err(), LiquidityOracleError::ExceedsMaxDeposits);
9961043
}
9971044

1045+
fn fails_initial_discount_exceeds_final_discount(price: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) {
1046+
let result = price.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision);
1047+
assert_eq!(result.unwrap_err(), LiquidityOracleError::InitialDiscountExceedsFinalDiscount);
1048+
}
1049+
1050+
fn fails_final_discount_exceeds_precision(price: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) {
1051+
let result = price.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision);
1052+
assert_eq!(result.unwrap_err(), LiquidityOracleError::FinalDiscountExceedsPrecision);
1053+
}
1054+
1055+
fn fails_none_encountered(price: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) {
1056+
let result = price.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision);
1057+
assert_eq!(result.unwrap_err(), LiquidityOracleError::NoneEncountered);
1058+
}
1059+
1060+
// 0 deposits
1061+
succeeds(
1062+
pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0),
1063+
0,
1064+
100,
1065+
0,
1066+
10,
1067+
100,
1068+
pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0)
1069+
);
1070+
1071+
// half deposits
1072+
succeeds(
1073+
pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0),
1074+
50,
1075+
100,
1076+
0,
1077+
10,
1078+
100,
1079+
pc(95 * (PD_SCALE as i64), 2 * PD_SCALE, 0)
1080+
);
1081+
1082+
// full deposits
1083+
succeeds(
1084+
pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0),
1085+
100,
1086+
100,
1087+
0,
1088+
10,
1089+
100,
1090+
pc(90 * (PD_SCALE as i64), 2 * PD_SCALE, 0)
1091+
);
1092+
1093+
// 0 deposits, staggered initial discount
9981094
succeeds(
9991095
pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0),
10001096
0,
10011097
100,
1098+
2,
1099+
10,
1100+
100,
1101+
pc(98 * (PD_SCALE as i64), 2 * PD_SCALE, 0)
1102+
);
1103+
1104+
// half deposits, staggered initial discount
1105+
succeeds(
1106+
pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0),
1107+
50,
1108+
100,
1109+
2,
1110+
10,
1111+
100,
1112+
pc(94 * (PD_SCALE as i64), 2 * PD_SCALE, 0)
1113+
);
1114+
1115+
// full deposits, staggered initial discount
1116+
succeeds(
1117+
pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0),
1118+
100,
1119+
100,
1120+
2,
1121+
10,
1122+
100,
1123+
pc(90 * (PD_SCALE as i64), 2 * PD_SCALE, 0)
1124+
);
1125+
1126+
// fails bc over max deposit limit
1127+
fails_exceeds_max_deposits(
1128+
pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0),
1129+
101,
1130+
100,
10021131
0,
1003-
5,
1132+
10,
1133+
100
1134+
);
1135+
1136+
// fails bc initial discount exceeds final discount
1137+
fails_initial_discount_exceeds_final_discount(
1138+
pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0),
1139+
50,
1140+
100,
1141+
11,
1142+
10,
1143+
100
1144+
);
1145+
1146+
// fails bc final discount exceeds precision
1147+
fails_final_discount_exceeds_precision(
1148+
pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0),
1149+
50,
1150+
100,
1151+
0,
1152+
101,
10041153
100,
1005-
pc(100 * (PD_SCALE), 2 * PD_SCALE, 0)
10061154
);
1155+
10071156
}
10081157
}

0 commit comments

Comments
 (0)