From d5144f618dd75b65b568252d426d848f079f19ab Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Wed, 18 Jan 2023 10:54:25 -0500 Subject: [PATCH 01/19] add error messages for LiquidityOracleError --- pyth-sdk/src/error.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 pyth-sdk/src/error.rs diff --git a/pyth-sdk/src/error.rs b/pyth-sdk/src/error.rs new file mode 100644 index 0000000..0634d9d --- /dev/null +++ b/pyth-sdk/src/error.rs @@ -0,0 +1,12 @@ +use solana_program::program_error::ProgramError; +use thiserror::Error; + +#[derive(Error, Debug, Copy, Clone)] +pub enum LiquidityOracleError { + #[error("deposits exceeds max depositable")] + ExceedsMaxDeposits, + #[error("initial discount rate should not be greater than final discount rate")] + InitialDiscountExceedsFinalDiscount, + #[error("final discount rate should not be greater than the discount precision")] + FinalDiscountExceedsPrecision, +} \ No newline at end of file From aed6f3ac7ed71e76852b6dae9460310943d7cc0f Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Wed, 18 Jan 2023 10:54:50 -0500 Subject: [PATCH 02/19] add logic and init test for getting collateral valuation price --- pyth-sdk/src/price.rs | 108 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/pyth-sdk/src/price.rs b/pyth-sdk/src/price.rs index d0232c0..9c1e996 100644 --- a/pyth-sdk/src/price.rs +++ b/pyth-sdk/src/price.rs @@ -8,6 +8,7 @@ use schemars::JsonSchema; use crate::{ utils, UnixTimestamp, + error::LiquidityOracleError, }; // Constants for working with pyth's number representation @@ -89,6 +90,60 @@ impl Price { self.div(quote)?.scale_to_exponent(result_expo) } + /// Get the valuation of a collateral position according to: + /// 1. the net amount deposited (across the protocol) + /// 2. the max amount depositable (across the protocol) + /// 3. the initial and final valuation discount rates + /// + /// We use a linear interpolation between the the initial and final discount rates, + /// scaled by the proportion of max depositable amount that has been deposited. + /// This essentially assumes a linear liquidity cumulative density function, + /// which has been shown to be a reasonable assumption for many crypto tokens in literature. + pub fn get_collateral_valuation_price(&self, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) -> Price { + if max_deposits < deposits { + return err!(LiquidityOracleError::ExceedsMaxDeposits); + } + + if discount_initial > discount_final { + return err!(LiquidityOracleError::InitialDiscountExceedsFinalDiscount); + } + + if discount_final > discount_precision { + return err!(LiquidityOracleError::FinalDiscountExceedsPrecision); + } + + // get initial expo; use later to convert conf to the right expo + let expo_init = self.expo; + + let left = self.cmul(deposits, 0)?.cmul(discount_precision-discount_final, 0)?; + let right = self.cmul(max_deposits-deposits, 0)?.cmul(discount_precision-discount_initial, 0)?; + + let numer = left.add(&right)?; + let denom = max_deposits * discount_precision; + + // TODO: divide price by a constant (denom) + // can denote denom as a Price with tiny confidence, + // perform the div, and throw away the resulting confidence, + // since all we care about from the div is price and expo + let denom_as_price = Price { + price: denom, + conf: 1, + expo: 0, + publish_time: self.publish_time, + }; + let price_discounted = numer.div(denom_as_price)?; + + // we want to scale the original confidence to the new expo + let conf_scaled = self.scale_confidence_to_exponent(price_discounted.expo); + + Price { + price: price_discounted.price, + conf: conf_scaled, + expo: price_discounted.expo, + publish_time: self.publish_time, + } + } + /// Get the price of a basket of currencies. /// /// Each entry in `amounts` is of the form `(price, qty, qty_expo)`, and the result is the sum @@ -334,6 +389,38 @@ impl Price { } } + /// Scale confidence so that its exponent is `target_expo`. + /// + /// Logic of this function similar to that of scale_to_exponent; + /// only difference is that this is scaling the confidence alone, + /// and it returns a u64 option. + /// + /// Useful in the case of get_collateral_valuation_price function, + /// since separate explicit confidence scaling required there. + pub fn scale_confidence_to_exponent(&self, target_expo: i32) -> Option { + let mut delta = target_expo.checked_sub(self.expo)?; + if delta >= 0 { + let mut c = self.conf; + // 2nd term is a short-circuit to bound op consumption + while delta > 0 && (c != 0) { + c = c.checked_div(10)?; + delta = delta.checked_sub(1)?; + } + + Some(c) + } else { + let mut c = self.conf; + + // c == None will short-circuit to bound op consumption + while delta < 0 { + c = c.checked_mul(10)?; + delta = delta.checked_add(1)?; + } + + Some(c) + } + } + /// Helper function to convert signed integers to unsigned and a sign bit, which simplifies /// some of the computations above. fn to_unsigned(x: i64) -> (u64, i64) { @@ -897,4 +984,25 @@ mod test { assert_eq!(p1.mul(&p2).unwrap().publish_time, 100); assert_eq!(p2.mul(&p1).unwrap().publish_time, 100); } + + #[test] + fn test_get_collateral_valuation_price() { + fn succeeds(price1: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64, expected: Price) { + assert_eq!(price1.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision).unwrap(), expected); + } + + fn fails(price1: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) { + assert_eq!(price1.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision), None); + } + + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + 0, + 100, + 0, + 5, + 100, + pc(100 * (PD_SCALE), 2 * PD_SCALE, 0) + ); + } } From e7ab5d857a930253ccf404db22347cf1e5b07e08 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Wed, 18 Jan 2023 17:41:33 -0500 Subject: [PATCH 03/19] added tests, corrections to collateral valuation --- pyth-sdk/Cargo.toml | 1 + pyth-sdk/src/error.rs | 5 +- pyth-sdk/src/lib.rs | 1 + pyth-sdk/src/price.rs | 201 ++++++++++++++++++++++++++++++++++++------ 4 files changed, 180 insertions(+), 28 deletions(-) diff --git a/pyth-sdk/Cargo.toml b/pyth-sdk/Cargo.toml index 79b161c..e1e24cf 100644 --- a/pyth-sdk/Cargo.toml +++ b/pyth-sdk/Cargo.toml @@ -19,6 +19,7 @@ borsh = "0.9" borsh-derive = "0.9.0" serde = { version = "1.0.136", features = ["derive"] } schemars = "0.8.8" +thiserror = "1.0.24" [dev-dependencies] serde_json = "1.0.79" diff --git a/pyth-sdk/src/error.rs b/pyth-sdk/src/error.rs index 0634d9d..0d01ab6 100644 --- a/pyth-sdk/src/error.rs +++ b/pyth-sdk/src/error.rs @@ -1,7 +1,6 @@ -use solana_program::program_error::ProgramError; use thiserror::Error; -#[derive(Error, Debug, Copy, Clone)] +#[derive(Error, Debug, Copy, Clone, PartialEq)] pub enum LiquidityOracleError { #[error("deposits exceeds max depositable")] ExceedsMaxDeposits, @@ -9,4 +8,6 @@ pub enum LiquidityOracleError { InitialDiscountExceedsFinalDiscount, #[error("final discount rate should not be greater than the discount precision")] FinalDiscountExceedsPrecision, + #[error("None encountered")] + NoneEncountered } \ No newline at end of file diff --git a/pyth-sdk/src/lib.rs b/pyth-sdk/src/lib.rs index c22d29f..5d2944f 100644 --- a/pyth-sdk/src/lib.rs +++ b/pyth-sdk/src/lib.rs @@ -8,6 +8,7 @@ use schemars::JsonSchema; use std::fmt; pub mod utils; +pub mod error; mod price; pub use price::Price; diff --git a/pyth-sdk/src/price.rs b/pyth-sdk/src/price.rs index 9c1e996..0643c32 100644 --- a/pyth-sdk/src/price.rs +++ b/pyth-sdk/src/price.rs @@ -99,26 +99,58 @@ impl Price { /// scaled by the proportion of max depositable amount that has been deposited. /// This essentially assumes a linear liquidity cumulative density function, /// which has been shown to be a reasonable assumption for many crypto tokens in literature. - pub fn get_collateral_valuation_price(&self, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) -> Price { + /// + /// Args + /// deposits: u64, quantity of token deposited in the protocol + /// max_deposits: u64, max quantity of token that can be deposited in the protocol + /// discount_initial: u64, initial discount rate at 0 deposits (units given by discount_precision) + /// discount_final: u64, final discount rate at max_deposits deposits (units given by discount_precision) + /// discount_precision: u64, the precision used for discounts + /// + /// Logic + /// collateral_valuation_price = (deposits * price * (discount_precision-discount_final) + (max_deposits - deposits) * price * (discount_precision - discount_initial)) / (max_deposits * discount_precision) + pub fn get_collateral_valuation_price(&self, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) -> Result { if max_deposits < deposits { - return err!(LiquidityOracleError::ExceedsMaxDeposits); + return Err(LiquidityOracleError::ExceedsMaxDeposits.into()); } if discount_initial > discount_final { - return err!(LiquidityOracleError::InitialDiscountExceedsFinalDiscount); + return Err(LiquidityOracleError::InitialDiscountExceedsFinalDiscount.into()); } if discount_final > discount_precision { - return err!(LiquidityOracleError::FinalDiscountExceedsPrecision); + return Err(LiquidityOracleError::FinalDiscountExceedsPrecision.into()); } - - // get initial expo; use later to convert conf to the right expo - let expo_init = self.expo; - let left = self.cmul(deposits, 0)?.cmul(discount_precision-discount_final, 0)?; - let right = self.cmul(max_deposits-deposits, 0)?.cmul(discount_precision-discount_initial, 0)?; + let remaining_depositable = (max_deposits-deposits) as i64; + let diff_discount_precision_initial = (discount_precision-discount_initial) as i64; + let diff_discount_precision_final = (discount_precision-discount_final) as i64; + + let mut left = self.cmul(deposits as i64, 0). + ok_or(LiquidityOracleError::NoneEncountered)?. + cmul(diff_discount_precision_final, 0). + ok_or(LiquidityOracleError::NoneEncountered)? + ; + let mut right = self.cmul(remaining_depositable, 0). + ok_or(LiquidityOracleError::NoneEncountered)?. + cmul(diff_discount_precision_initial, 0). + ok_or(LiquidityOracleError::NoneEncountered)? + ; + // scale left and right to match expo + if left.expo > right.expo { + left = left. + scale_to_exponent(right.expo). + ok_or(LiquidityOracleError::NoneEncountered)? + ; + } + else if left.expo < right.expo { + right = right. + scale_to_exponent(left.expo). + ok_or(LiquidityOracleError::NoneEncountered)? + ; + } - let numer = left.add(&right)?; + let numer = left.add(&right).ok_or(LiquidityOracleError::NoneEncountered)?; let denom = max_deposits * discount_precision; // TODO: divide price by a constant (denom) @@ -126,22 +158,24 @@ impl Price { // perform the div, and throw away the resulting confidence, // since all we care about from the div is price and expo let denom_as_price = Price { - price: denom, + price: denom as i64, conf: 1, expo: 0, publish_time: self.publish_time, }; - let price_discounted = numer.div(denom_as_price)?; + let price_discounted = numer.div(&denom_as_price).ok_or(LiquidityOracleError::NoneEncountered)?; // we want to scale the original confidence to the new expo let conf_scaled = self.scale_confidence_to_exponent(price_discounted.expo); - Price { - price: price_discounted.price, - conf: conf_scaled, - expo: price_discounted.expo, - publish_time: self.publish_time, - } + Ok( + Price { + price: price_discounted.price, + conf: conf_scaled.ok_or(LiquidityOracleError::NoneEncountered)?, + expo: price_discounted.expo, + publish_time: self.publish_time, + } + ) } /// Get the price of a basket of currencies. @@ -437,12 +471,12 @@ impl Price { #[cfg(test)] mod test { - use crate::price::{ + use crate::{price::{ Price, MAX_PD_V_U64, PD_EXPO, PD_SCALE, - }; + }, error::LiquidityOracleError}; const MAX_PD_V_I64: i64 = MAX_PD_V_U64 as i64; const MIN_PD_V_I64: i64 = -MAX_PD_V_I64; @@ -987,22 +1021,137 @@ mod test { #[test] fn test_get_collateral_valuation_price() { - fn succeeds(price1: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64, expected: Price) { - assert_eq!(price1.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision).unwrap(), expected); + fn succeeds(price: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64, mut expected: Price) { + let mut price_collat = price.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision).unwrap(); + print!("Output: price is {}, conf is {}, expo is {}, ts is {}", price_collat.price, price_collat.conf, price_collat.expo, price_collat.publish_time); + print!("Exepcted: price is {}, conf is {}, expo is {}, ts is {}", expected.price, expected.conf, expected.expo, expected.publish_time); + + // scale price_collat and expected to match in expo + if price_collat.expo > expected.expo { + price_collat = price_collat.scale_to_exponent(expected.expo).unwrap(); + } + else if price_collat.expo < expected.expo { + expected = expected.scale_to_exponent(price_collat.expo).unwrap(); + } + + assert_eq!(price_collat, expected); } - fn fails(price1: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) { - assert_eq!(price1.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision), None); + fn fails_exceeds_max_deposits(price: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) { + let result = price.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision); + assert_eq!(result.unwrap_err(), LiquidityOracleError::ExceedsMaxDeposits); } + fn fails_initial_discount_exceeds_final_discount(price: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) { + let result = price.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision); + assert_eq!(result.unwrap_err(), LiquidityOracleError::InitialDiscountExceedsFinalDiscount); + } + + fn fails_final_discount_exceeds_precision(price: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) { + let result = price.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision); + assert_eq!(result.unwrap_err(), LiquidityOracleError::FinalDiscountExceedsPrecision); + } + + fn fails_none_encountered(price: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) { + let result = price.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision); + assert_eq!(result.unwrap_err(), LiquidityOracleError::NoneEncountered); + } + + // 0 deposits + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + 0, + 100, + 0, + 10, + 100, + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0) + ); + + // half deposits + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + 50, + 100, + 0, + 10, + 100, + pc(95 * (PD_SCALE as i64), 2 * PD_SCALE, 0) + ); + + // full deposits + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + 100, + 100, + 0, + 10, + 100, + pc(90 * (PD_SCALE as i64), 2 * PD_SCALE, 0) + ); + + // 0 deposits, staggered initial discount succeeds( pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), 0, 100, + 2, + 10, + 100, + pc(98 * (PD_SCALE as i64), 2 * PD_SCALE, 0) + ); + + // half deposits, staggered initial discount + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + 50, + 100, + 2, + 10, + 100, + pc(94 * (PD_SCALE as i64), 2 * PD_SCALE, 0) + ); + + // full deposits, staggered initial discount + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + 100, + 100, + 2, + 10, + 100, + pc(90 * (PD_SCALE as i64), 2 * PD_SCALE, 0) + ); + + // fails bc over max deposit limit + fails_exceeds_max_deposits( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + 101, + 100, 0, - 5, + 10, + 100 + ); + + // fails bc initial discount exceeds final discount + fails_initial_discount_exceeds_final_discount( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + 50, + 100, + 11, + 10, + 100 + ); + + // fails bc final discount exceeds precision + fails_final_discount_exceeds_precision( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + 50, + 100, + 0, + 101, 100, - pc(100 * (PD_SCALE), 2 * PD_SCALE, 0) ); + } } From 017cad198d9d79ac5b5279775b12d24d05ac5af0 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Fri, 20 Jan 2023 17:49:30 -0500 Subject: [PATCH 04/19] made changes to accomodate precision issues, handled overflow bugs --- pyth-sdk/src/price.rs | 104 ++++++++++++++++++++++++++++++------------ 1 file changed, 74 insertions(+), 30 deletions(-) diff --git a/pyth-sdk/src/price.rs b/pyth-sdk/src/price.rs index 0643c32..5a6effe 100644 --- a/pyth-sdk/src/price.rs +++ b/pyth-sdk/src/price.rs @@ -108,7 +108,7 @@ impl Price { /// discount_precision: u64, the precision used for discounts /// /// Logic - /// collateral_valuation_price = (deposits * price * (discount_precision-discount_final) + (max_deposits - deposits) * price * (discount_precision - discount_initial)) / (max_deposits * discount_precision) + /// collateral_valuation_price = (deposits / max_deposits) * ((discount_precision-discount_final) / discount_precision) * price + ((max_deposits - deposits) / max_deposits) * ((discount_precision - discount_initial) / discount_precision) * price pub fn get_collateral_valuation_price(&self, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) -> Result { if max_deposits < deposits { return Err(LiquidityOracleError::ExceedsMaxDeposits.into()); @@ -126,44 +126,90 @@ impl Price { let diff_discount_precision_initial = (discount_precision-discount_initial) as i64; let diff_discount_precision_final = (discount_precision-discount_final) as i64; - let mut left = self.cmul(deposits as i64, 0). + // get fractions for deposits + let deposits_as_price = Price { + price: deposits as i64, + conf: 0, + expo: 0, + publish_time: 0, + }; + let remaining_depositable_as_price = Price { + price: remaining_depositable, + conf: 0, + expo: 0, + publish_time: 0, + }; + let max_deposits_as_price = Price { + price: max_deposits as i64, + conf: 1, + expo: 0, + publish_time: 0, + }; + + let deposits_percentage = deposits_as_price.div(&max_deposits_as_price).ok_or(LiquidityOracleError::NoneEncountered)?; + let remaining_depositable_percentage = remaining_depositable_as_price.div(&max_deposits_as_price).ok_or(LiquidityOracleError::NoneEncountered)?; + + // get fractions for discount + let diff_discount_precision_initial_as_price = Price { + price: diff_discount_precision_initial, + conf: 0, + expo: 0, + publish_time: 0, + }; + let diff_discount_precision_final_as_price = Price { + price: diff_discount_precision_final, + conf: 0, + expo: 0, + publish_time: 0, + }; + let discount_precision_as_price = Price { + price: discount_precision as i64, + conf: 1, + expo: 0, + publish_time: 0, + }; + + let initial_percentage = diff_discount_precision_initial_as_price.div(&discount_precision_as_price).ok_or(LiquidityOracleError::NoneEncountered)?; + let final_percentage = diff_discount_precision_final_as_price.div(&discount_precision_as_price).ok_or(LiquidityOracleError::NoneEncountered)?; + + + // compute left and right terms of the sum + let mut left = self.mul(&deposits_percentage). ok_or(LiquidityOracleError::NoneEncountered)?. - cmul(diff_discount_precision_final, 0). + mul(&final_percentage). ok_or(LiquidityOracleError::NoneEncountered)? ; - let mut right = self.cmul(remaining_depositable, 0). + let mut right = self.mul(&remaining_depositable_percentage). ok_or(LiquidityOracleError::NoneEncountered)?. - cmul(diff_discount_precision_initial, 0). + mul(&initial_percentage). ok_or(LiquidityOracleError::NoneEncountered)? ; - // scale left and right to match expo + + // scale left and right to match expo; need to ensure no overflow so have a match for NoneEncountered error if left.expo > right.expo { - left = left. - scale_to_exponent(right.expo). - ok_or(LiquidityOracleError::NoneEncountered)? - ; + // prefer right scaled up to left if no overflow, to prevent any precision loss + let right_scaled = right.scale_to_exponent(left.expo); + let left_scaled = left.scale_to_exponent(right.expo); + + match right_scaled { + Some(_x) => right = right_scaled.ok_or(LiquidityOracleError::NoneEncountered)?, + + None => left = left_scaled.ok_or(LiquidityOracleError::NoneEncountered)?, + } } else if left.expo < right.expo { - right = right. - scale_to_exponent(left.expo). - ok_or(LiquidityOracleError::NoneEncountered)? - ; - } + // prefer left scaled up to right if no overflow, to prevent any precision loss + let left_scaled = left.scale_to_exponent(right.expo); + let right_scaled = right.scale_to_exponent(left.expo); - let numer = left.add(&right).ok_or(LiquidityOracleError::NoneEncountered)?; - let denom = max_deposits * discount_precision; + match left_scaled { + Some(_x) => left = left_scaled.ok_or(LiquidityOracleError::NoneEncountered)?, - // TODO: divide price by a constant (denom) - // can denote denom as a Price with tiny confidence, - // perform the div, and throw away the resulting confidence, - // since all we care about from the div is price and expo - let denom_as_price = Price { - price: denom as i64, - conf: 1, - expo: 0, - publish_time: self.publish_time, - }; - let price_discounted = numer.div(&denom_as_price).ok_or(LiquidityOracleError::NoneEncountered)?; + None => right = right_scaled.ok_or(LiquidityOracleError::NoneEncountered)?, + } + } + + let price_discounted = left.add(&right).ok_or(LiquidityOracleError::NoneEncountered)?; // we want to scale the original confidence to the new expo let conf_scaled = self.scale_confidence_to_exponent(price_discounted.expo); @@ -1023,8 +1069,6 @@ mod test { fn test_get_collateral_valuation_price() { fn succeeds(price: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64, mut expected: Price) { let mut price_collat = price.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision).unwrap(); - print!("Output: price is {}, conf is {}, expo is {}, ts is {}", price_collat.price, price_collat.conf, price_collat.expo, price_collat.publish_time); - print!("Exepcted: price is {}, conf is {}, expo is {}, ts is {}", expected.price, expected.conf, expected.expo, expected.publish_time); // scale price_collat and expected to match in expo if price_collat.expo > expected.expo { From 03bb5a239a43ddf3ce8c0b31731eefcd737f0662 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Mon, 23 Jan 2023 10:58:35 -0500 Subject: [PATCH 05/19] reformulated test cases + added some bounds description --- pyth-sdk/src/price.rs | 101 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 87 insertions(+), 14 deletions(-) diff --git a/pyth-sdk/src/price.rs b/pyth-sdk/src/price.rs index 5a6effe..a2d330b 100644 --- a/pyth-sdk/src/price.rs +++ b/pyth-sdk/src/price.rs @@ -108,7 +108,20 @@ impl Price { /// discount_precision: u64, the precision used for discounts /// /// Logic - /// collateral_valuation_price = (deposits / max_deposits) * ((discount_precision-discount_final) / discount_precision) * price + ((max_deposits - deposits) / max_deposits) * ((discount_precision - discount_initial) / discount_precision) * price + /// A = deposits / max_deposits + /// B = (discount_precision-discount_final) / discount_precision + /// C = (max_deposits - deposits) / max_deposits + /// D = (discount_precision - discount_initial) / discount_precision + /// collateral_valuation_price = [(A * B) + (C * D)] * price + /// + /// Bounds due to precision loss + /// A, B, C, D each has precision up to PD_SCALE + /// x = 1/PD_SCALE + /// Err(A*B) <= (1+x)^2 - 1 (in fractional terms) + /// Err(C*D) <= (1+x)^2 - 1 + /// Err(A*B + C*D) <= 2*(1+x)^2 - 2 + /// Err((A*B + C*D) * price) <= 2*(1+x)^2 - 2 = 2x^2 + 2x ~= 2/PD_SCALE + /// Thus, we expect the computed collateral valuation price to be no more than 2/PD_SCALE off of the mathematically true value pub fn get_collateral_valuation_price(&self, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) -> Result { if max_deposits < deposits { return Err(LiquidityOracleError::ExceedsMaxDeposits.into()); @@ -172,18 +185,9 @@ impl Price { let initial_percentage = diff_discount_precision_initial_as_price.div(&discount_precision_as_price).ok_or(LiquidityOracleError::NoneEncountered)?; let final_percentage = diff_discount_precision_final_as_price.div(&discount_precision_as_price).ok_or(LiquidityOracleError::NoneEncountered)?; - - // compute left and right terms of the sum - let mut left = self.mul(&deposits_percentage). - ok_or(LiquidityOracleError::NoneEncountered)?. - mul(&final_percentage). - ok_or(LiquidityOracleError::NoneEncountered)? - ; - let mut right = self.mul(&remaining_depositable_percentage). - ok_or(LiquidityOracleError::NoneEncountered)?. - mul(&initial_percentage). - ok_or(LiquidityOracleError::NoneEncountered)? - ; + // // compute left and right terms of the sum + let mut left = deposits_percentage.mul(&final_percentage).ok_or(LiquidityOracleError::NoneEncountered)?; + let mut right = remaining_depositable_percentage.mul(&initial_percentage).ok_or(LiquidityOracleError::NoneEncountered)?; // scale left and right to match expo; need to ensure no overflow so have a match for NoneEncountered error if left.expo > right.expo { @@ -209,7 +213,11 @@ impl Price { } } - let price_discounted = left.add(&right).ok_or(LiquidityOracleError::NoneEncountered)?; + // get product term + let mult_discounted = left.add(&right).ok_or(LiquidityOracleError::NoneEncountered)?; + + // get price discounted + let price_discounted = self.mul(&mult_discounted).ok_or(LiquidityOracleError::NoneEncountered)?; // we want to scale the original confidence to the new expo let conf_scaled = self.scale_confidence_to_exponent(price_discounted.expo); @@ -1167,6 +1175,71 @@ mod test { pc(90 * (PD_SCALE as i64), 2 * PD_SCALE, 0) ); + // test precision limits + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + 1, + 1_000_000_000_000_000_000, + 0, + 10, + 100, + pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, 0), + ); + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + 100_000_000, + 1_000_000_000_000_000_000, + 0, + 10, + 100, + pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, 0), + ); + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + 1_000_000_000, + 1_000_000_000_000_000_000, + 0, + 10, + 100, + pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, 0), + ); + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + 10_000_000_000, + 1_000_000_000_000_000_000, + 0, + 10, + 100, + pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, 0), + ); + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + 100_000_000_000, + 1_000_000_000_000_000_000, + 0, + 10, + 100, + pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, 0), + ); + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + 200_000_000_000, + 1_000_000_000_000_000_000, + 0, + 10, + 100, + pc(100 * (PD_SCALE as i64)-2000, 2 * PD_SCALE, 0), + ); + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + 1_000_000_000_000, + 1_000_000_000_000_000_000, + 0, + 10, + 100, + pc(100 * (PD_SCALE as i64)-10000, 2 * PD_SCALE, 0), + ); + // fails bc over max deposit limit fails_exceeds_max_deposits( pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), From 6e8227ef797bc43028b2635c7997b17537d91c9e Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Mon, 23 Jan 2023 11:33:42 -0500 Subject: [PATCH 06/19] better way to convert from u64 to i64 --- pyth-sdk/src/error.rs | 4 +++- pyth-sdk/src/price.rs | 12 ++++++------ pyth-sdk/src/utils.rs | 8 ++++++++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/pyth-sdk/src/error.rs b/pyth-sdk/src/error.rs index 0d01ab6..d814193 100644 --- a/pyth-sdk/src/error.rs +++ b/pyth-sdk/src/error.rs @@ -9,5 +9,7 @@ pub enum LiquidityOracleError { #[error("final discount rate should not be greater than the discount precision")] FinalDiscountExceedsPrecision, #[error("None encountered")] - NoneEncountered + NoneEncountered, + #[error("i64 try from error")] + I64ConversionError, } \ No newline at end of file diff --git a/pyth-sdk/src/price.rs b/pyth-sdk/src/price.rs index a2d330b..42838e4 100644 --- a/pyth-sdk/src/price.rs +++ b/pyth-sdk/src/price.rs @@ -135,13 +135,13 @@ impl Price { return Err(LiquidityOracleError::FinalDiscountExceedsPrecision.into()); } - let remaining_depositable = (max_deposits-deposits) as i64; - let diff_discount_precision_initial = (discount_precision-discount_initial) as i64; - let diff_discount_precision_final = (discount_precision-discount_final) as i64; + let remaining_depositable = utils::u64_to_i64(max_deposits-deposits)?; + let diff_discount_precision_initial = utils::u64_to_i64(discount_precision-discount_initial)?; + let diff_discount_precision_final = utils::u64_to_i64(discount_precision-discount_final)?; // get fractions for deposits let deposits_as_price = Price { - price: deposits as i64, + price: utils::u64_to_i64(deposits)?, conf: 0, expo: 0, publish_time: 0, @@ -153,7 +153,7 @@ impl Price { publish_time: 0, }; let max_deposits_as_price = Price { - price: max_deposits as i64, + price: utils::u64_to_i64(max_deposits)?, conf: 1, expo: 0, publish_time: 0, @@ -176,7 +176,7 @@ impl Price { publish_time: 0, }; let discount_precision_as_price = Price { - price: discount_precision as i64, + price: utils::u64_to_i64(discount_precision)?, conf: 1, expo: 0, publish_time: 0, diff --git a/pyth-sdk/src/utils.rs b/pyth-sdk/src/utils.rs index b83c69c..550b664 100644 --- a/pyth-sdk/src/utils.rs +++ b/pyth-sdk/src/utils.rs @@ -1,3 +1,7 @@ +use std::convert::TryFrom; + +use crate::error::LiquidityOracleError; + /// This module helps serde to serialize deserialize some fields as String /// /// The reason this is added is that `#[serde(with = "String")]` does not work @@ -30,3 +34,7 @@ pub mod as_string { .map_err(|_| D::Error::custom("Input is not valid")) } } + +pub fn u64_to_i64(value: u64) -> Result { + i64::try_from(value).map_err(|_| LiquidityOracleError::I64ConversionError) +} From 44c55b04cc3896492d353598685ca2d0bf17b7b7 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Tue, 24 Jan 2023 13:41:47 -0500 Subject: [PATCH 07/19] rewrite funcs in line with bound considerations --- pyth-sdk/src/error.rs | 10 +- pyth-sdk/src/price.rs | 523 ++++++++++++++++++++++++++++++------------ pyth-sdk/src/utils.rs | 6 +- 3 files changed, 390 insertions(+), 149 deletions(-) diff --git a/pyth-sdk/src/error.rs b/pyth-sdk/src/error.rs index d814193..f4feaf2 100644 --- a/pyth-sdk/src/error.rs +++ b/pyth-sdk/src/error.rs @@ -1,13 +1,15 @@ use thiserror::Error; #[derive(Error, Debug, Copy, Clone, PartialEq)] -pub enum LiquidityOracleError { - #[error("deposits exceeds max depositable")] - ExceedsMaxDeposits, - #[error("initial discount rate should not be greater than final discount rate")] +pub enum OracleError { + #[error("initial endpoint should not be greater than or equal to final endpoint")] + InitialEndpointExceedsFinalEndpoint, + #[error("initial discount should not exceed final discount, for collateral valuation")] InitialDiscountExceedsFinalDiscount, #[error("final discount rate should not be greater than the discount precision")] FinalDiscountExceedsPrecision, + #[error("initial premium should not exceed final premium, for borrow valuation")] + InitialPremiumExceedsFinalPremium, #[error("None encountered")] NoneEncountered, #[error("i64 try from error")] diff --git a/pyth-sdk/src/price.rs b/pyth-sdk/src/price.rs index 42838e4..8223853 100644 --- a/pyth-sdk/src/price.rs +++ b/pyth-sdk/src/price.rs @@ -8,7 +8,7 @@ use schemars::JsonSchema; use crate::{ utils, UnixTimestamp, - error::LiquidityOracleError, + error::OracleError, }; // Constants for working with pyth's number representation @@ -91,145 +91,283 @@ impl Price { } /// Get the valuation of a collateral position according to: - /// 1. the net amount deposited (across the protocol) - /// 2. the max amount depositable (across the protocol) - /// 3. the initial and final valuation discount rates + /// 1. the net amount currently deposited (across the protocol) + /// 2. the deposits endpoint for the affine combination (across the protocol) + /// 3. the initial (at 0 deposits) and final (at the deposits endpoint) valuation discount rates /// /// We use a linear interpolation between the the initial and final discount rates, - /// scaled by the proportion of max depositable amount that has been deposited. + /// scaled by the proportion of the deposits endpoint that has been deposited. /// This essentially assumes a linear liquidity cumulative density function, /// which has been shown to be a reasonable assumption for many crypto tokens in literature. + /// If the assumptions of the liquidity curve hold true, we are obtaining a lower bound for the net price + /// at which one can sell the quantity of token specified by deposits in the open markets. + /// We value collateral according to the total deposits in the protocol due to the present + /// intractability of assessing collateral at risk by price range. /// /// Args /// deposits: u64, quantity of token deposited in the protocol - /// max_deposits: u64, max quantity of token that can be deposited in the protocol + /// deposits_endpoint: u64, deposits right endpoint for the affine combination /// discount_initial: u64, initial discount rate at 0 deposits (units given by discount_precision) - /// discount_final: u64, final discount rate at max_deposits deposits (units given by discount_precision) + /// discount_final: u64, final discount rate at deposits_endpoint deposits (units given by discount_precision) /// discount_precision: u64, the precision used for discounts /// - /// Logic - /// A = deposits / max_deposits - /// B = (discount_precision-discount_final) / discount_precision - /// C = (max_deposits - deposits) / max_deposits - /// D = (discount_precision - discount_initial) / discount_precision - /// collateral_valuation_price = [(A * B) + (C * D)] * price + /// affine_combination yields us error <= 2/PD_SCALE for discount_interpolated + /// We then multiply this with the price to yield price_discounted before scaling this back to the original expo + /// Output of affine_combination has expo >= -18, price (self) has arbitrary expo + /// Scaling this back to the original expo then has error bounded by the expo (10^expo). + /// This is because reverting a potentially finer expo to a coarser grid has the potential to be off by + /// the order of the atomic unit of the coarser grid. + /// This scaling error combines with the previous error additively: Err <= 2/PD_SCALE + 10^expo /// - /// Bounds due to precision loss - /// A, B, C, D each has precision up to PD_SCALE - /// x = 1/PD_SCALE - /// Err(A*B) <= (1+x)^2 - 1 (in fractional terms) - /// Err(C*D) <= (1+x)^2 - 1 - /// Err(A*B + C*D) <= 2*(1+x)^2 - 2 - /// Err((A*B + C*D) * price) <= 2*(1+x)^2 - 2 = 2x^2 + 2x ~= 2/PD_SCALE - /// Thus, we expect the computed collateral valuation price to be no more than 2/PD_SCALE off of the mathematically true value - pub fn get_collateral_valuation_price(&self, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) -> Result { - if max_deposits < deposits { - return Err(LiquidityOracleError::ExceedsMaxDeposits.into()); - } - + /// The practical error is based on the original expo: + /// if it is big, then the 10^expo loss dominates; + /// otherwise, the 2/PD_SCALE error dominates. + /// + /// Thus, we expect the computed collateral valuation price to be no more than 2/PD_SCALE + 10^expo off of the mathematically true value + /// For this reason, we encourage using this function with prices that have high expos, to minimize the potential error. + pub fn get_collateral_valuation_price(&self, deposits: u64, deposits_endpoint: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) -> Result { if discount_initial > discount_final { - return Err(LiquidityOracleError::InitialDiscountExceedsFinalDiscount.into()); + return Err(OracleError::InitialDiscountExceedsFinalDiscount.into()); } if discount_final > discount_precision { - return Err(LiquidityOracleError::FinalDiscountExceedsPrecision.into()); + return Err(OracleError::FinalDiscountExceedsPrecision.into()); } - let remaining_depositable = utils::u64_to_i64(max_deposits-deposits)?; let diff_discount_precision_initial = utils::u64_to_i64(discount_precision-discount_initial)?; let diff_discount_precision_final = utils::u64_to_i64(discount_precision-discount_final)?; - // get fractions for deposits - let deposits_as_price = Price { - price: utils::u64_to_i64(deposits)?, + // get fractions for discount + let diff_discount_precision_initial_as_price = Price { + price: diff_discount_precision_initial, conf: 0, expo: 0, publish_time: 0, }; - let remaining_depositable_as_price = Price { - price: remaining_depositable, + let diff_discount_precision_final_as_price = Price { + price: diff_discount_precision_final, conf: 0, expo: 0, publish_time: 0, }; - let max_deposits_as_price = Price { - price: utils::u64_to_i64(max_deposits)?, + let discount_precision_as_price = Price { + price: utils::u64_to_i64(discount_precision)?, conf: 1, expo: 0, publish_time: 0, }; - let deposits_percentage = deposits_as_price.div(&max_deposits_as_price).ok_or(LiquidityOracleError::NoneEncountered)?; - let remaining_depositable_percentage = remaining_depositable_as_price.div(&max_deposits_as_price).ok_or(LiquidityOracleError::NoneEncountered)?; + let initial_percentage = diff_discount_precision_initial_as_price.div(&discount_precision_as_price).ok_or(OracleError::NoneEncountered)?; + let final_percentage = diff_discount_precision_final_as_price.div(&discount_precision_as_price).ok_or(OracleError::NoneEncountered)?; + + // get the interpolated discount as a price + let discount_interpolated = Price::affine_combination( + 0, + initial_percentage, + utils::u64_to_i64(deposits_endpoint)?, + final_percentage, + utils::u64_to_i64(deposits)? + )?; + + let conf_orig = self.conf; + let expo_orig = self.expo; + + // get price discounted, convert back to the original exponents we received the price in + let price_discounted = self. + mul(&discount_interpolated). + ok_or(OracleError::NoneEncountered)?. + scale_to_exponent(expo_orig). + ok_or(OracleError::NoneEncountered)? + ; + Ok( + Price { + price: price_discounted.price, + conf: conf_orig, + expo: price_discounted.expo, + publish_time: self.publish_time, + } + ) + } + + /// Get the valuation of a borrow position according to: + /// 1. the net amount currently borrowed (across the protocol) + /// 2. the borrowed endpoint for the affine combination (across the protocol) + /// 3. the initial (at 0 borrows) and final (at the borrow endpoint) valuation premiums + /// + /// We use a linear interpolation between the the initial and final premiums, + /// scaled by the proportion of the borrows endpoint that has been borrowed out. + /// This essentially assumes a linear liquidity cumulative density function, + /// which has been shown to be a reasonable assumption for many crypto tokens in literature. + /// If the assumptions of the liquidity curve hold true, we are obtaining an upper bound for the net price + /// at which one can buy the quantity of token specified by borrows in the open markets. + /// We value the borrows according to the total borrows out of the protocol due to the present + /// intractability of assessing collateral at risk and repayment likelihood by price range. + /// + /// Args + /// borrows: u64, quantity of token borrowed from the protocol + /// borrows_endpoint: u64, borrows right endpoint for the affine combination + /// premium_initial: u64, initial premium at 0 borrows (units given by premium_precision) + /// premium_final: u64, final premium at borrows_endpoint borrows (units given by premium_precision) + /// premium_precision: u64, the precision used for premium + /// + /// affine_combination yields us error <= 2/PD_SCALE for premium_interpolated + /// We then multiply this with the price to yield price_premium before scaling this back to the original expo + /// Output of affine_combination has expo >= -18, price (self) has arbitrary expo + /// Scaling this back to the original expo then has error bounded by the expo (10^expo). + /// This is because reverting a potentially finer expo to a coarser grid has the potential to be off by + /// the order of the atomic unit of the coarser grid. + /// This scaling error combines with the previous error additively: Err <= 2/PD_SCALE + 10^expo + /// + /// The practical error is based on the original expo: + /// if it is big, then the 10^expo loss dominates; + /// otherwise, the 2/PD_SCALE error dominates. + /// + /// Thus, we expect the computed borrow valuation price to be no more than 2/PD_SCALE + 10^expo off of the mathematically true value + /// For this reason, we encourage using this function with prices that have high expos, to minimize the potential error. + pub fn get_borrow_valuation_price(&self, borrows: u64, borrows_endpoint: u64, premium_initial: u64, premium_final: u64, premium_precision: u64) -> Result { + if premium_initial > premium_final { + return Err(OracleError::InitialPremiumExceedsFinalPremium.into()); + } + + let premium_factor_initial = utils::u64_to_i64(premium_precision+premium_initial)?; + let premium_factor_final = utils::u64_to_i64(premium_precision+premium_final)?; + // get fractions for discount - let diff_discount_precision_initial_as_price = Price { - price: diff_discount_precision_initial, + let premium_factor_initial_as_price = Price { + price: premium_factor_initial, conf: 0, expo: 0, publish_time: 0, }; - let diff_discount_precision_final_as_price = Price { - price: diff_discount_precision_final, + let premium_factor_final_as_price = Price { + price: premium_factor_final, conf: 0, expo: 0, publish_time: 0, }; - let discount_precision_as_price = Price { - price: utils::u64_to_i64(discount_precision)?, + let premium_precision_as_price = Price { + price: utils::u64_to_i64(premium_precision)?, conf: 1, expo: 0, publish_time: 0, }; - let initial_percentage = diff_discount_precision_initial_as_price.div(&discount_precision_as_price).ok_or(LiquidityOracleError::NoneEncountered)?; - let final_percentage = diff_discount_precision_final_as_price.div(&discount_precision_as_price).ok_or(LiquidityOracleError::NoneEncountered)?; - - // // compute left and right terms of the sum - let mut left = deposits_percentage.mul(&final_percentage).ok_or(LiquidityOracleError::NoneEncountered)?; - let mut right = remaining_depositable_percentage.mul(&initial_percentage).ok_or(LiquidityOracleError::NoneEncountered)?; - - // scale left and right to match expo; need to ensure no overflow so have a match for NoneEncountered error - if left.expo > right.expo { - // prefer right scaled up to left if no overflow, to prevent any precision loss - let right_scaled = right.scale_to_exponent(left.expo); - let left_scaled = left.scale_to_exponent(right.expo); - - match right_scaled { - Some(_x) => right = right_scaled.ok_or(LiquidityOracleError::NoneEncountered)?, - - None => left = left_scaled.ok_or(LiquidityOracleError::NoneEncountered)?, + let initial_percentage = premium_factor_initial_as_price.div(&premium_precision_as_price).ok_or(OracleError::NoneEncountered)?; + let final_percentage = premium_factor_final_as_price.div(&premium_precision_as_price).ok_or(OracleError::NoneEncountered)?; + + // get the interpolated discount as a price + let premium_interpolated = Price::affine_combination( + 0, + initial_percentage, + utils::u64_to_i64(borrows_endpoint)?, + final_percentage, + utils::u64_to_i64(borrows)? + )?; + + let conf_orig = self.conf; + let expo_orig = self.expo; + + // get price premium, convert back to the original exponents we received the price in + let price_premium = self. + mul(&premium_interpolated). + ok_or(OracleError::NoneEncountered)?. + scale_to_exponent(expo_orig). + ok_or(OracleError::NoneEncountered)? + ; + + Ok( + Price { + price: price_premium.price, + conf: conf_orig, + expo: price_premium.expo, + publish_time: self.publish_time, } - } - else if left.expo < right.expo { - // prefer left scaled up to right if no overflow, to prevent any precision loss - let left_scaled = left.scale_to_exponent(right.expo); - let right_scaled = right.scale_to_exponent(left.expo); - - match left_scaled { - Some(_x) => left = left_scaled.ok_or(LiquidityOracleError::NoneEncountered)?, + ) + } - None => right = right_scaled.ok_or(LiquidityOracleError::NoneEncountered)?, - } + /// Performs an affine combination after setting everything to expo -9 + /// Takes in 2 points and a 3rd "query" x coordinate, to compute the value at + /// Effectively draws a line between the 2 points and then proceeds to + /// interpolate/exterpolate to find the value at the query coordinate according to that line + /// + /// Args + /// x1: i64, the x coordinate of the first point + /// y1: Price, the y coordinate of the first point, represented as a Price struct + /// x2: i64, the x coordinate of the second point, must be greater than x1 + /// y2: Price, the y coordinate of the second point, represented as a Price struct + /// x_query: the query x coordinate, at which we wish to impute a y value + /// + /// Logic + /// imputed y value = y2 * ((x3-x1)/(x2-x1)) + y1 * ((x2-x3)/(x2-x1)) + /// 1. compute A = x3-x1 + /// 2. compute B = x2-x3 + /// 3. compute C = x2-x1 + /// 4. compute D = A/C + /// 5. compute E = B/C + /// 6. compute F = y2 * D + /// 7. compute G = y1 * E + /// 8. compute H = F + G + /// + /// Bounds due to precision loss + /// x = 1/PD_SCALE + /// division incurs max loss of x + /// Err(D), Err(E) is relatively negligible--by scaling to expo -9 we are imposing + /// a grid composed of 1 billion units between x1 and x2 endpoints. Moreover, D, E <= 1. + /// Thus, max loss here: Err(D), Err(E) <= x + /// Err(y1), Err(y2) often <= x + /// Err(F), Err(G) <= (1+x)^2 - 1 (in fractional terms) + /// Err(H) <= 2*(1+x)^2 - 2 ~= 2x = 2/PD_SCALE + pub fn affine_combination(x1: i64, y1: Price, x2: i64, y2: Price, x_query: i64) -> Result { + if x2 <= x1 { + return Err(OracleError::InitialEndpointExceedsFinalEndpoint); } + + // get the deltas for the x coordinates + let delta_q1 = x_query - x1; + let delta_2q = x2 - x_query; + let delta_21 = x2 - x1; + + // convert deltas to Prices + let delta_q1_as_price = Price { + price: delta_q1, + conf: 0, + expo: 0, + publish_time: 0, + }; + let delta_2q_as_price = Price { + price: delta_2q, + conf: 0, + expo: 0, + publish_time: 0, + }; + let delta_21_as_price = Price { + price: delta_21, + conf: 0, + expo: 0, + publish_time: 0, + }; - // get product term - let mult_discounted = left.add(&right).ok_or(LiquidityOracleError::NoneEncountered)?; + // get the relevant fractions of the deltas + let mut frac_q1 = delta_q1_as_price.div(&delta_21_as_price).ok_or(OracleError::NoneEncountered)?; + let mut frac_2q = delta_2q_as_price.div(&delta_21_as_price).ok_or(OracleError::NoneEncountered)?; - // get price discounted - let price_discounted = self.mul(&mult_discounted).ok_or(LiquidityOracleError::NoneEncountered)?; + // scale all the prices to expo -9 for the mul and addition + frac_q1 = frac_q1.scale_to_exponent(-9).ok_or(OracleError::NoneEncountered)?; + frac_2q = frac_2q.scale_to_exponent(-9).ok_or(OracleError::NoneEncountered)?; - // we want to scale the original confidence to the new expo - let conf_scaled = self.scale_confidence_to_exponent(price_discounted.expo); + let y1_scaled = y1.scale_to_exponent(-9).ok_or(OracleError::NoneEncountered)?; + let y2_scaled = y2.scale_to_exponent(-9).ok_or(OracleError::NoneEncountered)?; + + // calculate products for left and right + let mut left = y2_scaled.mul(&frac_q1).ok_or(OracleError::NoneEncountered)?; + let mut right = y1_scaled.mul(&frac_2q).ok_or(OracleError::NoneEncountered)?; - Ok( - Price { - price: price_discounted.price, - conf: conf_scaled.ok_or(LiquidityOracleError::NoneEncountered)?, - expo: price_discounted.expo, - publish_time: self.publish_time, - } - ) + // standardize to expo -18 for addition + left = left.scale_to_exponent(-18).ok_or(OracleError::NoneEncountered)?; + right = right.scale_to_exponent(-18).ok_or(OracleError::NoneEncountered)?; + + Ok(left.add(&right).ok_or(OracleError::NoneEncountered)?) } /// Get the price of a basket of currencies. @@ -530,7 +668,7 @@ mod test { MAX_PD_V_U64, PD_EXPO, PD_SCALE, - }, error::LiquidityOracleError}; + }, error::OracleError}; const MAX_PD_V_I64: i64 = MAX_PD_V_U64 as i64; const MIN_PD_V_I64: i64 = -MAX_PD_V_I64; @@ -1075,8 +1213,8 @@ mod test { #[test] fn test_get_collateral_valuation_price() { - fn succeeds(price: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64, mut expected: Price) { - let mut price_collat = price.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision).unwrap(); + fn succeeds(price: Price, deposits: u64, deposits_endpoint: u64, discount_initial: u64, discount_final: u64, discount_precision: u64, mut expected: Price) { + let mut price_collat = price.get_collateral_valuation_price(deposits, deposits_endpoint, discount_initial, discount_final, discount_precision).unwrap(); // scale price_collat and expected to match in expo if price_collat.expo > expected.expo { @@ -1089,170 +1227,161 @@ mod test { assert_eq!(price_collat, expected); } - fn fails_exceeds_max_deposits(price: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) { - let result = price.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision); - assert_eq!(result.unwrap_err(), LiquidityOracleError::ExceedsMaxDeposits); - } - - fn fails_initial_discount_exceeds_final_discount(price: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) { - let result = price.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision); - assert_eq!(result.unwrap_err(), LiquidityOracleError::InitialDiscountExceedsFinalDiscount); - } - - fn fails_final_discount_exceeds_precision(price: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) { - let result = price.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision); - assert_eq!(result.unwrap_err(), LiquidityOracleError::FinalDiscountExceedsPrecision); + fn fails_initial_discount_exceeds_final_discount(price: Price, deposits: u64, deposits_endpoint: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) { + let result = price.get_collateral_valuation_price(deposits, deposits_endpoint, discount_initial, discount_final, discount_precision); + assert_eq!(result.unwrap_err(), OracleError::InitialDiscountExceedsFinalDiscount); } - fn fails_none_encountered(price: Price, deposits: u64, max_deposits: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) { - let result = price.get_collateral_valuation_price(deposits, max_deposits, discount_initial, discount_final, discount_precision); - assert_eq!(result.unwrap_err(), LiquidityOracleError::NoneEncountered); + fn fails_final_discount_exceeds_precision(price: Price, deposits: u64, deposits_endpoint: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) { + let result = price.get_collateral_valuation_price(deposits, deposits_endpoint, discount_initial, discount_final, discount_precision); + assert_eq!(result.unwrap_err(), OracleError::FinalDiscountExceedsPrecision); } // 0 deposits succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 0, 100, 0, 10, 100, - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0) + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9) ); // half deposits succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 50, 100, 0, 10, 100, - pc(95 * (PD_SCALE as i64), 2 * PD_SCALE, 0) + pc(95 * (PD_SCALE as i64), 2 * PD_SCALE, -9) ); // full deposits succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 100, + 100, + 0, + 10, 100, + pc(90 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // beyond final endpoint deposits + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 150, 100, 0, 10, 100, - pc(90 * (PD_SCALE as i64), 2 * PD_SCALE, 0) + pc(85 * (PD_SCALE as i64), 2 * PD_SCALE, -9) ); // 0 deposits, staggered initial discount succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 0, 100, 2, 10, 100, - pc(98 * (PD_SCALE as i64), 2 * PD_SCALE, 0) + pc(98 * (PD_SCALE as i64), 2 * PD_SCALE, -9) ); // half deposits, staggered initial discount succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 50, 100, 2, 10, 100, - pc(94 * (PD_SCALE as i64), 2 * PD_SCALE, 0) + pc(94 * (PD_SCALE as i64), 2 * PD_SCALE, -9) ); // full deposits, staggered initial discount succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 100, 100, 2, 10, 100, - pc(90 * (PD_SCALE as i64), 2 * PD_SCALE, 0) + pc(90 * (PD_SCALE as i64), 2 * PD_SCALE, -9) ); // test precision limits succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 1, 1_000_000_000_000_000_000, 0, 10, 100, - pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, -9), ); succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 100_000_000, 1_000_000_000_000_000_000, 0, 10, 100, - pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, -9), ); succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 1_000_000_000, 1_000_000_000_000_000_000, 0, 10, 100, - pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, -9), ); succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 10_000_000_000, 1_000_000_000_000_000_000, 0, 10, 100, - pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, -9), ); succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 100_000_000_000, 1_000_000_000_000_000_000, 0, 10, 100, - pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, -9), ); succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 200_000_000_000, 1_000_000_000_000_000_000, 0, 10, 100, - pc(100 * (PD_SCALE as i64)-2000, 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64)-2000, 2 * PD_SCALE, -9), ); succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 1_000_000_000_000, 1_000_000_000_000_000_000, 0, 10, 100, - pc(100 * (PD_SCALE as i64)-10000, 2 * PD_SCALE, 0), - ); - - // fails bc over max deposit limit - fails_exceeds_max_deposits( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), - 101, - 100, - 0, - 10, - 100 + pc(100 * (PD_SCALE as i64)-10000, 2 * PD_SCALE, -9), ); // fails bc initial discount exceeds final discount fails_initial_discount_exceeds_final_discount( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 50, 100, 11, @@ -1262,7 +1391,7 @@ mod test { // fails bc final discount exceeds precision fails_final_discount_exceeds_precision( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, 0), + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 50, 100, 0, @@ -1271,4 +1400,114 @@ mod test { ); } + + #[test] + fn test_get_borrow_valuation_price() { + fn succeeds(price: Price, borrows: u64, borrows_endpoint: u64, premium_initial: u64, premium_final: u64, premium_precision: u64, mut expected: Price) { + let mut price_borrow = price.get_borrow_valuation_price(borrows, borrows_endpoint, premium_initial, premium_final, premium_precision).unwrap(); + + // scale price_collat and expected to match in expo + if price_borrow.expo > expected.expo { + price_borrow = price_borrow.scale_to_exponent(expected.expo).unwrap(); + } + else if price_borrow.expo < expected.expo { + expected = expected.scale_to_exponent(price_borrow.expo).unwrap(); + } + + assert_eq!(price_borrow, expected); + } + + fn fails_initial_discount_exceeds_final_discount(price: Price, borrows: u64, borrows_endpoint: u64, premium_initial: u64, premium_final: u64, premium_precision: u64) { + let result = price.get_borrow_valuation_price(borrows, borrows_endpoint, premium_initial, premium_final, premium_precision); + assert_eq!(result.unwrap_err(), OracleError::InitialPremiumExceedsFinalPremium); + } + + // 0 borrows + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 0, + 100, + 0, + 10, + 100, + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // half borrows + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 50, + 100, + 0, + 10, + 100, + pc(105 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // full borrows + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 100, + 100, + 0, + 10, + 100, + pc(110 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // beyond final endpoint borrows + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 150, + 100, + 0, + 10, + 100, + pc(115 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // 0 borrows, staggered initial premium + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 0, + 100, + 2, + 10, + 100, + pc(102 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // half borrows, staggered initial premium + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 50, + 100, + 2, + 10, + 100, + pc(106 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // full borrows, staggered initial premium + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 100, + 100, + 2, + 10, + 100, + pc(110 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // fails bc initial premium exceeds final premium + fails_initial_discount_exceeds_final_discount( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 50, + 100, + 11, + 10, + 100 + ); + + } } diff --git a/pyth-sdk/src/utils.rs b/pyth-sdk/src/utils.rs index 550b664..dbc27d0 100644 --- a/pyth-sdk/src/utils.rs +++ b/pyth-sdk/src/utils.rs @@ -1,6 +1,6 @@ use std::convert::TryFrom; -use crate::error::LiquidityOracleError; +use crate::error::OracleError; /// This module helps serde to serialize deserialize some fields as String /// @@ -35,6 +35,6 @@ pub mod as_string { } } -pub fn u64_to_i64(value: u64) -> Result { - i64::try_from(value).map_err(|_| LiquidityOracleError::I64ConversionError) +pub fn u64_to_i64(value: u64) -> Result { + i64::try_from(value).map_err(|_| OracleError::I64ConversionError) } From b4f23ca7f6baa08e54fa2625f7316b194d63dafd Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Tue, 24 Jan 2023 13:50:47 -0500 Subject: [PATCH 08/19] correct 1 conf typo --- pyth-sdk/src/price.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyth-sdk/src/price.rs b/pyth-sdk/src/price.rs index 8223853..038b34b 100644 --- a/pyth-sdk/src/price.rs +++ b/pyth-sdk/src/price.rs @@ -152,7 +152,7 @@ impl Price { }; let discount_precision_as_price = Price { price: utils::u64_to_i64(discount_precision)?, - conf: 1, + conf: 0, expo: 0, publish_time: 0, }; @@ -248,7 +248,7 @@ impl Price { }; let premium_precision_as_price = Price { price: utils::u64_to_i64(premium_precision)?, - conf: 1, + conf: 0, expo: 0, publish_time: 0, }; From ee86d387b567c765d9242e931711d1afc60c7555 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Mon, 30 Jan 2023 23:14:27 -0500 Subject: [PATCH 09/19] updates to testing --- pyth-sdk/src/error.rs | 17 - pyth-sdk/src/lib.rs | 1 - pyth-sdk/src/price.rs | 978 +++++++++++++++++++++++++++++++----------- pyth-sdk/src/utils.rs | 20 +- 4 files changed, 751 insertions(+), 265 deletions(-) delete mode 100644 pyth-sdk/src/error.rs diff --git a/pyth-sdk/src/error.rs b/pyth-sdk/src/error.rs deleted file mode 100644 index f4feaf2..0000000 --- a/pyth-sdk/src/error.rs +++ /dev/null @@ -1,17 +0,0 @@ -use thiserror::Error; - -#[derive(Error, Debug, Copy, Clone, PartialEq)] -pub enum OracleError { - #[error("initial endpoint should not be greater than or equal to final endpoint")] - InitialEndpointExceedsFinalEndpoint, - #[error("initial discount should not exceed final discount, for collateral valuation")] - InitialDiscountExceedsFinalDiscount, - #[error("final discount rate should not be greater than the discount precision")] - FinalDiscountExceedsPrecision, - #[error("initial premium should not exceed final premium, for borrow valuation")] - InitialPremiumExceedsFinalPremium, - #[error("None encountered")] - NoneEncountered, - #[error("i64 try from error")] - I64ConversionError, -} \ No newline at end of file diff --git a/pyth-sdk/src/lib.rs b/pyth-sdk/src/lib.rs index 5d2944f..c22d29f 100644 --- a/pyth-sdk/src/lib.rs +++ b/pyth-sdk/src/lib.rs @@ -8,7 +8,6 @@ use schemars::JsonSchema; use std::fmt; pub mod utils; -pub mod error; mod price; pub use price::Price; diff --git a/pyth-sdk/src/price.rs b/pyth-sdk/src/price.rs index 038b34b..02841f6 100644 --- a/pyth-sdk/src/price.rs +++ b/pyth-sdk/src/price.rs @@ -8,7 +8,6 @@ use schemars::JsonSchema; use crate::{ utils, UnixTimestamp, - error::OracleError, }; // Constants for working with pyth's number representation @@ -99,6 +98,8 @@ impl Price { /// scaled by the proportion of the deposits endpoint that has been deposited. /// This essentially assumes a linear liquidity cumulative density function, /// which has been shown to be a reasonable assumption for many crypto tokens in literature. + /// For more detail on this: https://pythnetwork.medium.com/improving-lending-protocols-with-liquidity-oracles-fd1ea4f96f37 + /// /// If the assumptions of the liquidity curve hold true, we are obtaining a lower bound for the net price /// at which one can sell the quantity of token specified by deposits in the open markets. /// We value collateral according to the total deposits in the protocol due to the present @@ -107,9 +108,9 @@ impl Price { /// Args /// deposits: u64, quantity of token deposited in the protocol /// deposits_endpoint: u64, deposits right endpoint for the affine combination - /// discount_initial: u64, initial discount rate at 0 deposits (units given by discount_precision) - /// discount_final: u64, final discount rate at deposits_endpoint deposits (units given by discount_precision) - /// discount_precision: u64, the precision used for discounts + /// discount_initial: u64, initial discount rate at 0 deposits (units given by discount_exponent) + /// discount_final: u64, final discount rate at deposits_endpoint deposits (units given by discount_exponent) + /// discount_exponent: u64, the exponent to apply to the discounts above (e.g. if discount_final is 10 but meant to express 0.1/10%, exponent would be -2) /// /// affine_combination yields us error <= 2/PD_SCALE for discount_interpolated /// We then multiply this with the price to yield price_discounted before scaling this back to the original expo @@ -125,48 +126,37 @@ impl Price { /// /// Thus, we expect the computed collateral valuation price to be no more than 2/PD_SCALE + 10^expo off of the mathematically true value /// For this reason, we encourage using this function with prices that have high expos, to minimize the potential error. - pub fn get_collateral_valuation_price(&self, deposits: u64, deposits_endpoint: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) -> Result { - if discount_initial > discount_final { - return Err(OracleError::InitialDiscountExceedsFinalDiscount.into()); + pub fn get_collateral_valuation_price(&self, deposits: u64, deposits_endpoint: u64, discount_initial: u64, discount_final: u64, discount_exponent: i32) -> Option { + if discount_initial < discount_final { + return None; } - if discount_final > discount_precision { - return Err(OracleError::FinalDiscountExceedsPrecision.into()); + if !Price::check_precision_geq(discount_initial, discount_exponent)? { + return None; } - let diff_discount_precision_initial = utils::u64_to_i64(discount_precision-discount_initial)?; - let diff_discount_precision_final = utils::u64_to_i64(discount_precision-discount_final)?; - - // get fractions for discount - let diff_discount_precision_initial_as_price = Price { - price: diff_discount_precision_initial, + // get price versions of discounts + let initial_percentage = Price { + price: utils::u64_to_i64(discount_initial)?, conf: 0, - expo: 0, + expo: discount_exponent, publish_time: 0, }; - let diff_discount_precision_final_as_price = Price { - price: diff_discount_precision_final, + let final_percentage = Price { + price: utils::u64_to_i64(discount_final)?, conf: 0, - expo: 0, - publish_time: 0, - }; - let discount_precision_as_price = Price { - price: utils::u64_to_i64(discount_precision)?, - conf: 0, - expo: 0, + expo: discount_exponent, publish_time: 0, }; - let initial_percentage = diff_discount_precision_initial_as_price.div(&discount_precision_as_price).ok_or(OracleError::NoneEncountered)?; - let final_percentage = diff_discount_precision_final_as_price.div(&discount_precision_as_price).ok_or(OracleError::NoneEncountered)?; - // get the interpolated discount as a price let discount_interpolated = Price::affine_combination( 0, initial_percentage, utils::u64_to_i64(deposits_endpoint)?, final_percentage, - utils::u64_to_i64(deposits)? + utils::u64_to_i64(deposits)?, + Some(-9) )?; let conf_orig = self.conf; @@ -174,20 +164,18 @@ impl Price { // get price discounted, convert back to the original exponents we received the price in let price_discounted = self. - mul(&discount_interpolated). - ok_or(OracleError::NoneEncountered)?. - scale_to_exponent(expo_orig). - ok_or(OracleError::NoneEncountered)? + mul(&discount_interpolated)?. + scale_to_exponent(expo_orig)? ; - Ok( + return Some( Price { price: price_discounted.price, conf: conf_orig, expo: price_discounted.expo, publish_time: self.publish_time, } - ) + ); } /// Get the valuation of a borrow position according to: @@ -199,6 +187,8 @@ impl Price { /// scaled by the proportion of the borrows endpoint that has been borrowed out. /// This essentially assumes a linear liquidity cumulative density function, /// which has been shown to be a reasonable assumption for many crypto tokens in literature. + /// For more detail on this: https://pythnetwork.medium.com/improving-lending-protocols-with-liquidity-oracles-fd1ea4f96f37 + /// /// If the assumptions of the liquidity curve hold true, we are obtaining an upper bound for the net price /// at which one can buy the quantity of token specified by borrows in the open markets. /// We value the borrows according to the total borrows out of the protocol due to the present @@ -209,7 +199,7 @@ impl Price { /// borrows_endpoint: u64, borrows right endpoint for the affine combination /// premium_initial: u64, initial premium at 0 borrows (units given by premium_precision) /// premium_final: u64, final premium at borrows_endpoint borrows (units given by premium_precision) - /// premium_precision: u64, the precision used for premium + /// premium_exponent: u64, the exponent of the precision (10^{premium_exponent}) used for premiums /// /// affine_combination yields us error <= 2/PD_SCALE for premium_interpolated /// We then multiply this with the price to yield price_premium before scaling this back to the original expo @@ -225,13 +215,13 @@ impl Price { /// /// Thus, we expect the computed borrow valuation price to be no more than 2/PD_SCALE + 10^expo off of the mathematically true value /// For this reason, we encourage using this function with prices that have high expos, to minimize the potential error. - pub fn get_borrow_valuation_price(&self, borrows: u64, borrows_endpoint: u64, premium_initial: u64, premium_final: u64, premium_precision: u64) -> Result { + pub fn get_borrow_valuation_price(&self, borrows: u64, borrows_endpoint: u64, premium_initial: u64, premium_final: u64, premium_precision: u64) -> Option { if premium_initial > premium_final { - return Err(OracleError::InitialPremiumExceedsFinalPremium.into()); + return None; } - let premium_factor_initial = utils::u64_to_i64(premium_precision+premium_initial)?; - let premium_factor_final = utils::u64_to_i64(premium_precision+premium_final)?; + let premium_factor_initial = utils::u64_to_i64(premium_precision.checked_add(premium_initial)?)?; + let premium_factor_final = utils::u64_to_i64(premium_precision.checked_add(premium_final)?)?; // get fractions for discount let premium_factor_initial_as_price = Price { @@ -253,8 +243,8 @@ impl Price { publish_time: 0, }; - let initial_percentage = premium_factor_initial_as_price.div(&premium_precision_as_price).ok_or(OracleError::NoneEncountered)?; - let final_percentage = premium_factor_final_as_price.div(&premium_precision_as_price).ok_or(OracleError::NoneEncountered)?; + let initial_percentage = premium_factor_initial_as_price.div(&premium_precision_as_price)?; + let final_percentage = premium_factor_final_as_price.div(&premium_precision_as_price)?; // get the interpolated discount as a price let premium_interpolated = Price::affine_combination( @@ -262,7 +252,8 @@ impl Price { initial_percentage, utils::u64_to_i64(borrows_endpoint)?, final_percentage, - utils::u64_to_i64(borrows)? + utils::u64_to_i64(borrows)?, + Some(-9) )?; let conf_orig = self.conf; @@ -270,20 +261,18 @@ impl Price { // get price premium, convert back to the original exponents we received the price in let price_premium = self. - mul(&premium_interpolated). - ok_or(OracleError::NoneEncountered)?. - scale_to_exponent(expo_orig). - ok_or(OracleError::NoneEncountered)? + mul(&premium_interpolated)?. + scale_to_exponent(expo_orig)? ; - Ok( + return Some( Price { price: price_premium.price, conf: conf_orig, expo: price_premium.expo, publish_time: self.publish_time, } - ) + ); } /// Performs an affine combination after setting everything to expo -9 @@ -296,7 +285,12 @@ impl Price { /// y1: Price, the y coordinate of the first point, represented as a Price struct /// x2: i64, the x coordinate of the second point, must be greater than x1 /// y2: Price, the y coordinate of the second point, represented as a Price struct - /// x_query: the query x coordinate, at which we wish to impute a y value + /// x_query: i64, the query x coordinate, at which we wish to impute a y value + /// pre_add_expo: Option, the exponent to scale to, before final addition; essentially the final precision you want + /// + /// We want for y1 and y2 to be in the same exponent post normalization. + /// The function will return None otherwise. The easiest way to pass this check + /// is to simply pass in a y1 and a y2 that are already normalized and have the same exponent. /// /// Logic /// imputed y value = y2 * ((x3-x1)/(x2-x1)) + y1 * ((x2-x3)/(x2-x1)) @@ -318,56 +312,44 @@ impl Price { /// Err(y1), Err(y2) often <= x /// Err(F), Err(G) <= (1+x)^2 - 1 (in fractional terms) /// Err(H) <= 2*(1+x)^2 - 2 ~= 2x = 2/PD_SCALE - pub fn affine_combination(x1: i64, y1: Price, x2: i64, y2: Price, x_query: i64) -> Result { + pub fn affine_combination(x1: i64, y1: Price, x2: i64, y2: Price, x_query: i64, pre_add_expo: Option) -> Option { if x2 <= x1 { - return Err(OracleError::InitialEndpointExceedsFinalEndpoint); + return None; } - - // get the deltas for the x coordinates - let delta_q1 = x_query - x1; - let delta_2q = x2 - x_query; - let delta_21 = x2 - x1; - - // convert deltas to Prices - let delta_q1_as_price = Price { - price: delta_q1, - conf: 0, - expo: 0, - publish_time: 0, - }; - let delta_2q_as_price = Price { - price: delta_2q, - conf: 0, - expo: 0, - publish_time: 0, - }; - let delta_21_as_price = Price { - price: delta_21, - conf: 0, - expo: 0, - publish_time: 0, - }; - // get the relevant fractions of the deltas - let mut frac_q1 = delta_q1_as_price.div(&delta_21_as_price).ok_or(OracleError::NoneEncountered)?; - let mut frac_2q = delta_2q_as_price.div(&delta_21_as_price).ok_or(OracleError::NoneEncountered)?; + let y1_norm = y1.normalize()?; + let y2_norm = y2.normalize()?; - // scale all the prices to expo -9 for the mul and addition - frac_q1 = frac_q1.scale_to_exponent(-9).ok_or(OracleError::NoneEncountered)?; - frac_2q = frac_2q.scale_to_exponent(-9).ok_or(OracleError::NoneEncountered)?; + // require that normalized versions of y1 and y2 have the same expo + if y1_norm.expo != y2_norm.expo { + return None; + } + + // get the deltas for the x coordinates + let delta_q1 = x_query.checked_sub(x1)?; + let delta_2q = x2.checked_sub(x_query)?; + let delta_21 = x2.checked_sub(x1)?; - let y1_scaled = y1.scale_to_exponent(-9).ok_or(OracleError::NoneEncountered)?; - let y2_scaled = y2.scale_to_exponent(-9).ok_or(OracleError::NoneEncountered)?; + // get the relevant fractions of the deltas, with scaling + let frac_q1 = Price::fraction(delta_q1, delta_21, None)?; + let frac_2q = Price::fraction(delta_2q, delta_21, None)?; // calculate products for left and right - let mut left = y2_scaled.mul(&frac_q1).ok_or(OracleError::NoneEncountered)?; - let mut right = y1_scaled.mul(&frac_2q).ok_or(OracleError::NoneEncountered)?; + let mut left = y2.mul(&frac_q1)?; + let mut right = y1.mul(&frac_2q)?; + + if pre_add_expo.is_some() { + left = left.scale_to_exponent(pre_add_expo?)?; + right = right.scale_to_exponent(pre_add_expo?)?; + } - // standardize to expo -18 for addition - left = left.scale_to_exponent(-18).ok_or(OracleError::NoneEncountered)?; - right = right.scale_to_exponent(-18).ok_or(OracleError::NoneEncountered)?; + if left.expo != right.expo { + return None; + } + + let total = left.add(&right); - Ok(left.add(&right).ok_or(OracleError::NoneEncountered)?) + return left.add(&right); } /// Get the price of a basket of currencies. @@ -615,38 +597,6 @@ impl Price { } } - /// Scale confidence so that its exponent is `target_expo`. - /// - /// Logic of this function similar to that of scale_to_exponent; - /// only difference is that this is scaling the confidence alone, - /// and it returns a u64 option. - /// - /// Useful in the case of get_collateral_valuation_price function, - /// since separate explicit confidence scaling required there. - pub fn scale_confidence_to_exponent(&self, target_expo: i32) -> Option { - let mut delta = target_expo.checked_sub(self.expo)?; - if delta >= 0 { - let mut c = self.conf; - // 2nd term is a short-circuit to bound op consumption - while delta > 0 && (c != 0) { - c = c.checked_div(10)?; - delta = delta.checked_sub(1)?; - } - - Some(c) - } else { - let mut c = self.conf; - - // c == None will short-circuit to bound op consumption - while delta < 0 { - c = c.checked_mul(10)?; - delta = delta.checked_add(1)?; - } - - Some(c) - } - } - /// Helper function to convert signed integers to unsigned and a sign bit, which simplifies /// some of the computations above. fn to_unsigned(x: i64) -> (u64, i64) { @@ -659,16 +609,61 @@ impl Price { (x as u64, 1) } } + + /// Helper function to check if precision implied by exponent >= value, exponent <= 0 + fn check_precision_geq(value: u64, exponent: i32) -> Option { + if exponent > 0 { + return None; + } + + let mut precision: u64 = 1; + let mut delta = -exponent; + + while delta > 0 && (value != 0) { + precision = precision.checked_mul(10)?; + delta = delta.checked_sub(1)?; + } + + return Some(value <= precision); + } + + /// Helper function to create fraction and possibly scale to expo -9 + fn fraction(x: i64, y: i64, scale_expo: Option) -> Option { + // convert x and y to Prices + let x_as_price = Price { + price: x, + conf: 0, + expo: 0, + publish_time: 0, + }; + let y_as_price = Price { + price: y, + conf: 0, + expo: 0, + publish_time: 0, + }; + + // get the relevant fraction + let mut frac = x_as_price.div(&y_as_price)?; + + if scale_expo.is_some() { + frac = frac.scale_to_exponent(scale_expo?)?; + } + + return Some(frac); + } } #[cfg(test)] mod test { - use crate::{price::{ + use std::char::MAX; + + use crate::price::{ Price, MAX_PD_V_U64, PD_EXPO, PD_SCALE, - }, error::OracleError}; + }; const MAX_PD_V_I64: i64 = MAX_PD_V_U64 as i64; const MIN_PD_V_I64: i64 = -MAX_PD_V_I64; @@ -1212,29 +1207,43 @@ mod test { } #[test] - fn test_get_collateral_valuation_price() { - fn succeeds(price: Price, deposits: u64, deposits_endpoint: u64, discount_initial: u64, discount_final: u64, discount_precision: u64, mut expected: Price) { - let mut price_collat = price.get_collateral_valuation_price(deposits, deposits_endpoint, discount_initial, discount_final, discount_precision).unwrap(); - - // scale price_collat and expected to match in expo - if price_collat.expo > expected.expo { - price_collat = price_collat.scale_to_exponent(expected.expo).unwrap(); - } - else if price_collat.expo < expected.expo { - expected = expected.scale_to_exponent(price_collat.expo).unwrap(); - } + fn test_check_precision_geq() { + fn succeeds(value: u64, exponent: i32, expected: bool) { + let precision_geq = Price::check_precision_geq(value, exponent).unwrap(); - assert_eq!(price_collat, expected); + assert_eq!(precision_geq, expected); } - fn fails_initial_discount_exceeds_final_discount(price: Price, deposits: u64, deposits_endpoint: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) { - let result = price.get_collateral_valuation_price(deposits, deposits_endpoint, discount_initial, discount_final, discount_precision); - assert_eq!(result.unwrap_err(), OracleError::InitialDiscountExceedsFinalDiscount); + fn fails(value: u64, exponent: i32) { + let result = Price::check_precision_geq(value, exponent); + assert_eq!(result, None); + } + + succeeds(1, 0, true); + succeeds(1, -1, true); + + succeeds(100, -1, false); + succeeds(100, -2, true); + succeeds(100, -3, true); + + succeeds(101, -2, false); + succeeds(101, -3, true); + + // fails bc exponent > 0 + fails(10, 1); + } + + #[test] + fn test_get_collateral_valuation_price() { + fn succeeds(price: Price, deposits: u64, deposits_endpoint: u64, discount_initial: u64, discount_final: u64, discount_exponent: i32, expected: Price) { + let price_collat = price.get_collateral_valuation_price(deposits, deposits_endpoint, discount_initial, discount_final, discount_exponent).unwrap(); + + assert_eq!(price_collat, expected); } - fn fails_final_discount_exceeds_precision(price: Price, deposits: u64, deposits_endpoint: u64, discount_initial: u64, discount_final: u64, discount_precision: u64) { - let result = price.get_collateral_valuation_price(deposits, deposits_endpoint, discount_initial, discount_final, discount_precision); - assert_eq!(result.unwrap_err(), OracleError::FinalDiscountExceedsPrecision); + fn fails(price: Price, deposits: u64, deposits_endpoint: u64, discount_initial: u64, discount_final: u64, discount_exponent: i32) { + let result = price.get_collateral_valuation_price(deposits, deposits_endpoint, discount_initial, discount_final, discount_exponent); + assert_eq!(result, None); } // 0 deposits @@ -1242,9 +1251,9 @@ mod test { pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 0, 100, - 0, - 10, 100, + 90, + -2, pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9) ); @@ -1253,9 +1262,9 @@ mod test { pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 50, 100, - 0, - 10, 100, + 90, + -2, pc(95 * (PD_SCALE as i64), 2 * PD_SCALE, -9) ); @@ -1264,9 +1273,42 @@ mod test { pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 100, 100, + 100, + 90, + -2, + pc(90 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // 0 deposits, diff precision + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 0, - 10, 100, + 1000, + 900, + -3, + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // half deposits, diff precision + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 50, + 100, + 1000, + 900, + -3, + pc(95 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // full deposits, diff precision + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 100, + 100, + 1000, + 900, + -3, pc(90 * (PD_SCALE as i64), 2 * PD_SCALE, -9) ); @@ -1275,9 +1317,9 @@ mod test { pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 150, 100, - 0, - 10, 100, + 90, + -2, pc(85 * (PD_SCALE as i64), 2 * PD_SCALE, -9) ); @@ -1286,9 +1328,9 @@ mod test { pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 0, 100, - 2, - 10, - 100, + 98, + 90, + -2, pc(98 * (PD_SCALE as i64), 2 * PD_SCALE, -9) ); @@ -1297,9 +1339,9 @@ mod test { pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 50, 100, - 2, - 10, - 100, + 98, + 90, + -2, pc(94 * (PD_SCALE as i64), 2 * PD_SCALE, -9) ); @@ -1308,9 +1350,9 @@ mod test { pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 100, 100, - 2, - 10, - 100, + 98, + 90, + -2, pc(90 * (PD_SCALE as i64), 2 * PD_SCALE, -9) ); @@ -1319,195 +1361,645 @@ mod test { pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 1, 1_000_000_000_000_000_000, - 0, - 10, 100, + 90, + -2, pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, -9), ); succeeds( pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 100_000_000, 1_000_000_000_000_000_000, - 0, - 10, 100, + 90, + -2, pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, -9), ); succeeds( pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 1_000_000_000, 1_000_000_000_000_000_000, - 0, - 10, 100, + 90, + -2, pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, -9), ); succeeds( pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 10_000_000_000, 1_000_000_000_000_000_000, - 0, - 10, 100, + 90, + -2, pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, -9), ); succeeds( pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 100_000_000_000, 1_000_000_000_000_000_000, - 0, - 10, 100, + 90, + -2, pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, -9), ); succeeds( pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 200_000_000_000, 1_000_000_000_000_000_000, - 0, - 10, 100, + 90, + -2, pc(100 * (PD_SCALE as i64)-2000, 2 * PD_SCALE, -9), ); succeeds( pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 1_000_000_000_000, 1_000_000_000_000_000_000, - 0, - 10, 100, + 90, + -2, pc(100 * (PD_SCALE as i64)-10000, 2 * PD_SCALE, -9), ); - // fails bc initial discount exceeds final discount - fails_initial_discount_exceeds_final_discount( + // fails bc initial discount lower than final discount + fails( pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 50, 100, - 11, - 10, - 100 + 89, + 90, + -2 ); - // fails bc final discount exceeds precision - fails_final_discount_exceeds_precision( + // fails bc initial discount exceeds precision + fails( pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 50, 100, - 0, 101, - 100, + 90, + -2, ); } #[test] fn test_get_borrow_valuation_price() { - fn succeeds(price: Price, borrows: u64, borrows_endpoint: u64, premium_initial: u64, premium_final: u64, premium_precision: u64, mut expected: Price) { - let mut price_borrow = price.get_borrow_valuation_price(borrows, borrows_endpoint, premium_initial, premium_final, premium_precision).unwrap(); + fn succeeds(price: Price, borrows: u64, borrows_endpoint: u64, premium_initial: u64, premium_final: u64, premium_precision: u64, expected: Price) { + let price_borrow = price.get_borrow_valuation_price(borrows, borrows_endpoint, premium_initial, premium_final, premium_precision).unwrap(); - // scale price_collat and expected to match in expo - if price_borrow.expo > expected.expo { - price_borrow = price_borrow.scale_to_exponent(expected.expo).unwrap(); - } - else if price_borrow.expo < expected.expo { - expected = expected.scale_to_exponent(price_borrow.expo).unwrap(); - } - assert_eq!(price_borrow, expected); } - fn fails_initial_discount_exceeds_final_discount(price: Price, borrows: u64, borrows_endpoint: u64, premium_initial: u64, premium_final: u64, premium_precision: u64) { + fn fails(price: Price, borrows: u64, borrows_endpoint: u64, premium_initial: u64, premium_final: u64, premium_precision: u64) { let result = price.get_borrow_valuation_price(borrows, borrows_endpoint, premium_initial, premium_final, premium_precision); - assert_eq!(result.unwrap_err(), OracleError::InitialPremiumExceedsFinalPremium); + assert_eq!(result, None); } - // 0 borrows + // // 0 borrows + // succeeds( + // pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + // 0, + // 100, + // 0, + // 10, + // 100, + // pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + // ); + + // // half borrows + // succeeds( + // pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + // 50, + // 100, + // 0, + // 10, + // 100, + // pc(105 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + // ); + + // // full borrows + // succeeds( + // pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + // 100, + // 100, + // 0, + // 10, + // 100, + // pc(110 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + // ); + + // // beyond final endpoint borrows + // succeeds( + // pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + // 150, + // 100, + // 0, + // 10, + // 100, + // pc(115 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + // ); + + // // 0 borrows, staggered initial premium + // succeeds( + // pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + // 0, + // 100, + // 2, + // 10, + // 100, + // pc(102 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + // ); + + // // half borrows, staggered initial premium + // succeeds( + // pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + // 50, + // 100, + // 2, + // 10, + // 100, + // pc(106 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + // ); + + // // full borrows, staggered initial premium + // succeeds( + // pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + // 100, + // 100, + // 2, + // 10, + // 100, + // pc(110 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + // ); + + // // fails bc initial premium exceeds final premium + // fails( + // pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + // 50, + // 100, + // 11, + // 10, + // 100 + // ); + + } + + #[test] + fn test_affine_combination() { + fn succeeds(x1: i64, y1: Price, x2: i64, y2: Price, x_query: i64, pre_add_expo: Option, expected: Price) { + let y_query = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo).unwrap(); + + assert_eq!(y_query, expected); + } + + fn fails(x1: i64, y1: Price, x2: i64, y2: Price, x_query: i64, pre_add_expo: Option) { + let result = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo); + assert_eq!(result, None); + } + + // constant, inbounds succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 0, - 100, + pc(100, 0, -4), + 10, + pc(100, 0, -4), + 5, + Some(-9), + pc(10_000_000, 0, -9) + ); + + // constant, outside the bounds + succeeds( 0, + pc(100, 0, -4), 10, - 100, - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(100, 0, -4), + 15, + Some(-9), + pc(10_000_000, 0, -9), ); - // half borrows + // increasing, inbounds succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), - 50, - 100, 0, + pc(90, 0, -4), 10, - 100, - pc(105 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(100, 0, -4), + 5, + Some(-9), + pc(9_500_000, 0, -9) ); - // full borrows + // increasing, out of bounds succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), - 100, - 100, 0, + pc(90, 0, -4), 10, - 100, - pc(110 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(100, 0, -4), + 15, + Some(-9), + pc(10_500_000, 0, -9) ); - // beyond final endpoint borrows + // decreasing, inbounds succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), - 150, - 100, 0, + pc(100, 0, -4), 10, - 100, - pc(115 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(80, 0, -4), + 5, + Some(-9), + pc(9_000_000, 0, -9) ); - // 0 borrows, staggered initial premium + // decreasing, out of bounds succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 0, - 100, - 2, + pc(100, 0, -4), 10, - 100, - pc(102 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(80, 0, -4), + 15, + Some(-9), + pc(7_000_000, 0, -9) ); - // half borrows, staggered initial premium + // test loss due to scaling succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), - 50, - 100, - 2, + 0, + pc(0, 0, -2), + 13, + pc(10, 0, -2), + 1, + Some(-8), + pc(769230, 0, -8) + ); + succeeds( + 0, + pc(0, 0, -2), + 13, + pc(10, 0, -2), + 1, + Some(-9), + pc(7692307, 0, -9) + ); + succeeds( + 0, + pc(0, 0, -3), + 13, + pc(100, 0, -3), + 1, + Some(-9), + pc(7692307, 0, -9) + ); + succeeds( + 0, + pc(0, 0, -2), + 13, + pc(100, 0, -2), + 1, + Some(-9), + pc(76923076, 0, -9) + ); + + // Test with end range of possible inputs on endpoint xs (no precision loss) + succeeds( + 0, + pc(100, 0, -9), + i64::MAX, + pc(0, 0, -9), + i64::MAX/10, + Some(-9), + pc(90, 0, -9) + ); + succeeds( + i64::MIN, + pc(100, 0, -9), + i64::MIN/2, + pc(0, 0, -9), + (i64::MIN/4)*3, + Some(-9), + pc(50, 0, -9) + ); + succeeds( + i64::MIN+1, + pc(100, 0, -9), + 0, + pc(0, 0, -9), + i64::MIN/4, + Some(-9), + pc(25, 0, -9) + ); + succeeds( + i64::MIN+1, + pc(100, 0, -9), + 0, + pc(0, 0, -9), + 0, + Some(-9), + pc(0, 0, -9) + ); + + // Test with end range of possible inputs in prices to identify precision inaccuracy + succeeds( + 0, + pc(MAX_PD_V_I64-10, 0, -4), 10, - 100, - pc(106 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(MAX_PD_V_I64, 0, -4), + 5, + Some(-4), + pc(MAX_PD_V_I64-6, 0, -4) + ); + succeeds( + 0, + pc(MAX_PD_V_I64-1, 0, -4), + 10, + pc(MAX_PD_V_I64, 0, -4), + 9, + Some(-4), + pc(MAX_PD_V_I64-1, 0, -4) ); - // full borrows, staggered initial premium + // test w confidence (same at both endpoints) succeeds( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), - 100, - 100, - 2, + 0, + pc(90, 10, -4), + 10, + pc(100, 10, -4), + 5, + Some(-9), + pc(9_500_000, 1_000_000, -9) + ); + + // test w confidence (different at the endpoints) + succeeds( + 0, + pc(90, 10, -4), 10, + pc(100, 15, -4), + 5, + Some(-9), + pc(9_500_000, 1_250_000, -9) + ); + succeeds( + 0, + pc(90, 10, -4), + 10, + pc(100, 15, -4), + 8, + Some(-9), + pc(9_800_000, 1_400_000, -9) + ); + succeeds( + 0, + pc(90, 10, -4), + 10, + pc(100, 15, -4), + 15, + Some(-9), + pc(10_500_000, 2_750_000, -9) + ); + + // fails bc x1 > x2 + fails( + 20, + pc(100, 0, -4), + 10, + pc(100, 0, -4), + 15, + Some(-9) + ); + // fails bc x1 is MIN, x2-x1 --> overflow in delta + fails( + i64::MIN, + pc(100, 0, -5), + 10, + pc(1000, 0, -5), + 5, + Some(-9) + ); + // fails bc x2 is MAX, x1 is negative --> overflow in delta + fails( + -5, + pc(100, 0, -4), + i64::MAX, + pc(1000, 0, -4), + 5, + Some(-9) + ); + // fails bc of overflow in the checked_sub + fails( + i64::MIN/2, + pc(100, 0, -4), + i64::MAX/2+1, + pc(100, 0, -4), + 5, + Some(-9) + ); + // fails bc expos don't match for y1 and y2 + fails( + 0, + pc(100, 0, -4), + 10, + pc(100, 0, -5), + 5, + Some(-9) + ); + // fails bc price too small to be realized + fails( + 0, + pc(100, 0, -4), + 10, + pc(5, 0, -4), + i64::MAX-100, + Some(-9) + ); + // fails bc no pre_add_expo --> no shared precision pre add + fails( + 0, + pc(100, 0, -4), + i64::MAX, + pc(0, 0, -4), + 25, + None + ); + // fails bc 0-i64::MIN > i64::MAX, so overflow + fails( + i64::MIN, + pc(100, 0, -9), + 0, + pc(0, 0, -9), + 0, + Some(-9) + ); + } + + #[test] + fn test_fraction() { + fn succeeds(x: i64, y: i64, scale_expo: Option, expected: Price) { + let frac = Price::fraction(x, y, scale_expo).unwrap(); + + assert_eq!(frac, expected); + } + + fn fails(x: i64, y: i64, scale_expo: Option) { + let result = Price::fraction(x, y, scale_expo); + + assert_eq!(result, None); + } + + succeeds( + 100, + 1000, + Some(-9), + pc(100_000_000, 0, -9) + ); + succeeds( + 1, + 1_000_000_000, + Some(-9), + pc(1, 0, -9) + ); + + // test loss due to big numer + succeeds( + 1_000_000_000_123, + 1, + None, + pc(100_000_000_000_000_000, 0, -5) + ); + + // test loss due to big denom + succeeds( + 1, + 10_000_000_011, + None, + pc(10, 0, -11) + ); + + // test loss due to scaling + succeeds( + 1, + 1, + None, + pc(1000000000, 0, -9) + ); + succeeds( + 1, + 1, + Some(0), + pc(1, 0, 0) + ); + succeeds( + 1, + 1, + Some(1), + pc(0, 0, 1) + ); + succeeds( 100, - pc(110 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + 1, + Some(i32::MAX-9), + pc(0, 0, i32::MAX-9) ); - // fails bc initial premium exceeds final premium - fails_initial_discount_exceeds_final_discount( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), - 50, + // Test with big inputs where the output will lose precision. + succeeds( + i64::MAX, 100, - 11, - 10, - 100 + None, + pc(922337200000000, 0, 2) + ); + succeeds( + i64::MAX, + 1, + None, + pc(92233720000000000, 0, 2) + ); + succeeds( + i64::MAX-10, + i64::MAX-5, + None, + pc(1000000000, 0, -9) + ); + succeeds( + 1_000_000_004, + 1_000_000_008, + None, + pc(1000000000, 0, -9) + ); + // dividing both of the terms in the above check by 4, but now precision cutoff doesn't kick in in normalize + succeeds( + 250_000_001, + 250_000_002, + None, + pc(999999996, 0, -9) ); + // Test with end range of possible inputs where the output should not lose precision. + succeeds( + MAX_PD_V_I64, + MAX_PD_V_I64, + None, + pc(1_000_000_000, 0, -9) + ); + succeeds( + MAX_PD_V_I64, + 1, + None, + pc(MAX_PD_V_I64*1_000_000_000, 0, -9) + ); + succeeds( + MAX_PD_V_I64, + MIN_PD_V_I64, + None, + pc(-1_000_000_000, 0, -9) + ); + succeeds( + MIN_PD_V_I64, + 1, + None, + pc(MIN_PD_V_I64*1_000_000_000, 0, -9) + ); + + // // Test with end range of possible inputs to identify precision inaccuracy + succeeds( + 1, + MAX_PD_V_I64, + None, + pc(3, 0, -9) + ); + succeeds( + 1, + MIN_PD_V_I64, + None, + pc(-3, 0, -9) + ); + + + // fails due to div by 0 + fails( + 100, + 0, + None + ); + + succeeds( + 1_000_000_000_000_000, + 1, + None, + pc(100_000_000_000_000_000, 0, -2) + ); + // fails due to overflow when scaling + fails( + 1_000_000_000_000_000, + 1, + Some(-18) + ); + fails( + 100, + 1, + Some(i32::MIN) + ); } } diff --git a/pyth-sdk/src/utils.rs b/pyth-sdk/src/utils.rs index dbc27d0..77c4b8b 100644 --- a/pyth-sdk/src/utils.rs +++ b/pyth-sdk/src/utils.rs @@ -1,7 +1,5 @@ use std::convert::TryFrom; -use crate::error::OracleError; - /// This module helps serde to serialize deserialize some fields as String /// /// The reason this is added is that `#[serde(with = "String")]` does not work @@ -35,6 +33,20 @@ pub mod as_string { } } -pub fn u64_to_i64(value: u64) -> Result { - i64::try_from(value).map_err(|_| OracleError::I64ConversionError) +pub fn u64_to_i64(value: u64) -> Option { + let value_signed = i64::try_from(value); + + match value_signed { + Ok(x) => return Some(x), + Err(_) => return None, + } +} + +pub fn i32_to_u32(value: i32) -> Option { + let value_unsigned = u32::try_from(value); + + match value_unsigned { + Ok(x) => return Some(x), + Err(_) => return None, + } } From 458ab9367af7cbf36461df58e8b38f55587eb127 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Tue, 31 Jan 2023 22:02:03 -0500 Subject: [PATCH 10/19] fraction manual tests --- pyth-sdk/Cargo.toml | 2 + pyth-sdk/src/lib.rs | 6 + pyth-sdk/src/price.rs | 705 ++++++++++++++++++++++++------------------ pyth-sdk/src/utils.rs | 22 +- 4 files changed, 409 insertions(+), 326 deletions(-) diff --git a/pyth-sdk/Cargo.toml b/pyth-sdk/Cargo.toml index e1e24cf..36e8c0b 100644 --- a/pyth-sdk/Cargo.toml +++ b/pyth-sdk/Cargo.toml @@ -23,3 +23,5 @@ thiserror = "1.0.24" [dev-dependencies] serde_json = "1.0.79" +quickcheck = "1" +quickcheck_macros = "1" \ No newline at end of file diff --git a/pyth-sdk/src/lib.rs b/pyth-sdk/src/lib.rs index c22d29f..d2aef87 100644 --- a/pyth-sdk/src/lib.rs +++ b/pyth-sdk/src/lib.rs @@ -1,3 +1,9 @@ +#[cfg(test)] +extern crate quickcheck; +#[cfg(test)] +#[macro_use(quickcheck)] +extern crate quickcheck_macros; + use borsh::{ BorshDeserialize, BorshSerialize, diff --git a/pyth-sdk/src/price.rs b/pyth-sdk/src/price.rs index 02841f6..c3dce91 100644 --- a/pyth-sdk/src/price.rs +++ b/pyth-sdk/src/price.rs @@ -3,6 +3,8 @@ use borsh::{ BorshSerialize, }; +use std::convert::TryFrom; + use schemars::JsonSchema; use crate::{ @@ -111,6 +113,7 @@ impl Price { /// discount_initial: u64, initial discount rate at 0 deposits (units given by discount_exponent) /// discount_final: u64, final discount rate at deposits_endpoint deposits (units given by discount_exponent) /// discount_exponent: u64, the exponent to apply to the discounts above (e.g. if discount_final is 10 but meant to express 0.1/10%, exponent would be -2) + /// note that if discount_initial is bigger than 100% per the discount exponent scale, then the initial valuation of the collateral will be higher than the oracle price /// /// affine_combination yields us error <= 2/PD_SCALE for discount_interpolated /// We then multiply this with the price to yield price_discounted before scaling this back to the original expo @@ -131,19 +134,15 @@ impl Price { return None; } - if !Price::check_precision_geq(discount_initial, discount_exponent)? { - return None; - } - // get price versions of discounts let initial_percentage = Price { - price: utils::u64_to_i64(discount_initial)?, + price: i64::try_from(discount_initial).ok()?, conf: 0, expo: discount_exponent, publish_time: 0, }; let final_percentage = Price { - price: utils::u64_to_i64(discount_final)?, + price: i64::try_from(discount_final).ok()?, conf: 0, expo: discount_exponent, publish_time: 0, @@ -153,10 +152,10 @@ impl Price { let discount_interpolated = Price::affine_combination( 0, initial_percentage, - utils::u64_to_i64(deposits_endpoint)?, + i64::try_from(deposits_endpoint).ok()?, final_percentage, - utils::u64_to_i64(deposits)?, - Some(-9) + i64::try_from(deposits).ok()?, + -9 )?; let conf_orig = self.conf; @@ -197,9 +196,10 @@ impl Price { /// Args /// borrows: u64, quantity of token borrowed from the protocol /// borrows_endpoint: u64, borrows right endpoint for the affine combination - /// premium_initial: u64, initial premium at 0 borrows (units given by premium_precision) - /// premium_final: u64, final premium at borrows_endpoint borrows (units given by premium_precision) - /// premium_exponent: u64, the exponent of the precision (10^{premium_exponent}) used for premiums + /// premium_initial: u64, initial premium at 0 borrows (units given by premium_exponent) + /// premium_final: u64, final premium at borrows_endpoint borrows (units given by premium_exponent) + /// premium_exponent: u64, the exponent to apply to the premiums above (e.g. if premium_final is 50 but meant to express 0.05/5%, exponent would be -3) + /// note that if premium_initial is less than 100% per the premium exponent scale, then the initial valuation of the borrow will be lower than the oracle price /// /// affine_combination yields us error <= 2/PD_SCALE for premium_interpolated /// We then multiply this with the price to yield price_premium before scaling this back to the original expo @@ -215,45 +215,33 @@ impl Price { /// /// Thus, we expect the computed borrow valuation price to be no more than 2/PD_SCALE + 10^expo off of the mathematically true value /// For this reason, we encourage using this function with prices that have high expos, to minimize the potential error. - pub fn get_borrow_valuation_price(&self, borrows: u64, borrows_endpoint: u64, premium_initial: u64, premium_final: u64, premium_precision: u64) -> Option { + pub fn get_borrow_valuation_price(&self, borrows: u64, borrows_endpoint: u64, premium_initial: u64, premium_final: u64, premium_exponent: i32) -> Option { if premium_initial > premium_final { return None; } - let premium_factor_initial = utils::u64_to_i64(premium_precision.checked_add(premium_initial)?)?; - let premium_factor_final = utils::u64_to_i64(premium_precision.checked_add(premium_final)?)?; - - // get fractions for discount - let premium_factor_initial_as_price = Price { - price: premium_factor_initial, - conf: 0, - expo: 0, - publish_time: 0, - }; - let premium_factor_final_as_price = Price { - price: premium_factor_final, + // get price versions of premiums + let initial_percentage = Price { + price: i64::try_from(premium_initial).ok()?, conf: 0, - expo: 0, + expo: premium_exponent, publish_time: 0, }; - let premium_precision_as_price = Price { - price: utils::u64_to_i64(premium_precision)?, + let final_percentage = Price { + price: i64::try_from(premium_final).ok()?, conf: 0, - expo: 0, + expo: premium_exponent, publish_time: 0, }; - let initial_percentage = premium_factor_initial_as_price.div(&premium_precision_as_price)?; - let final_percentage = premium_factor_final_as_price.div(&premium_precision_as_price)?; - - // get the interpolated discount as a price + // get the interpolated premium as a price let premium_interpolated = Price::affine_combination( - 0, + 0, initial_percentage, - utils::u64_to_i64(borrows_endpoint)?, + i64::try_from(borrows_endpoint).ok()?, final_percentage, - utils::u64_to_i64(borrows)?, - Some(-9) + i64::try_from(borrows).ok()?, + -9 )?; let conf_orig = self.conf; @@ -264,6 +252,13 @@ impl Price { mul(&premium_interpolated)?. scale_to_exponent(expo_orig)? ; + + println!("init perc: {}, {}", initial_percentage.price, initial_percentage.expo); + println!("final perc: {}, {}", final_percentage.price, final_percentage.expo); + println!("interpolated premium: {}, {}", premium_interpolated.price, premium_interpolated.expo); + println!("in btwn: {}, {}", self.mul(&premium_interpolated)?.price, self.mul(&premium_interpolated)?.expo); + println!("price (adj for premium): {}, {}", price_premium.price, price_premium.expo); + println!("======="); return Some( Price { @@ -286,16 +281,12 @@ impl Price { /// x2: i64, the x coordinate of the second point, must be greater than x1 /// y2: Price, the y coordinate of the second point, represented as a Price struct /// x_query: i64, the query x coordinate, at which we wish to impute a y value - /// pre_add_expo: Option, the exponent to scale to, before final addition; essentially the final precision you want - /// - /// We want for y1 and y2 to be in the same exponent post normalization. - /// The function will return None otherwise. The easiest way to pass this check - /// is to simply pass in a y1 and a y2 that are already normalized and have the same exponent. + /// pre_add_expo: i32, the exponent to scale to, before final addition; essentially the final precision you want /// /// Logic - /// imputed y value = y2 * ((x3-x1)/(x2-x1)) + y1 * ((x2-x3)/(x2-x1)) - /// 1. compute A = x3-x1 - /// 2. compute B = x2-x3 + /// imputed y value = y2 * ((xq-x1)/(x2-x1)) + y1 * ((x2-x3)/(x2-x1)) + /// 1. compute A = xq-x1 + /// 2. compute B = x2-xq /// 3. compute C = x2-x1 /// 4. compute D = A/C /// 5. compute E = B/C @@ -309,46 +300,58 @@ impl Price { /// Err(D), Err(E) is relatively negligible--by scaling to expo -9 we are imposing /// a grid composed of 1 billion units between x1 and x2 endpoints. Moreover, D, E <= 1. /// Thus, max loss here: Err(D), Err(E) <= x - /// Err(y1), Err(y2) often <= x + /// Err(y1), Err(y2) with normalization <= x /// Err(F), Err(G) <= (1+x)^2 - 1 (in fractional terms) /// Err(H) <= 2*(1+x)^2 - 2 ~= 2x = 2/PD_SCALE - pub fn affine_combination(x1: i64, y1: Price, x2: i64, y2: Price, x_query: i64, pre_add_expo: Option) -> Option { + /// + /// Scaling this back has error bounded by the expo (10^pre_add_expo). + /// This is because reverting a potentially finer expo to a coarser grid has the potential to be off by + /// the order of the atomic unit of the coarser grid. + /// This scaling error combines with the previous error additively: Err <= 2/PD_SCALE + 2*10^pre_add_expo + /// But if pre_add_expo is reasonably small (<= -9), then other term will dominate + pub fn affine_combination(x1: i64, y1: Price, x2: i64, y2: Price, x_query: i64, pre_add_expo: i32) -> Option { if x2 <= x1 { return None; } - - let y1_norm = y1.normalize()?; - let y2_norm = y2.normalize()?; - - // require that normalized versions of y1 and y2 have the same expo - if y1_norm.expo != y2_norm.expo { - return None; - } // get the deltas for the x coordinates + // 1. compute A = xq-x1 let delta_q1 = x_query.checked_sub(x1)?; + // 2. compute B = x2-xq let delta_2q = x2.checked_sub(x_query)?; + // 3. compute C = x2-x1 let delta_21 = x2.checked_sub(x1)?; // get the relevant fractions of the deltas, with scaling - let frac_q1 = Price::fraction(delta_q1, delta_21, None)?; - let frac_2q = Price::fraction(delta_2q, delta_21, None)?; + // 4. compute D = A/C, Err(D) <= x = 1/PD_SCALE + let frac_q1 = Price::fraction(delta_q1, delta_21)?; + // 5. compute E = B/C, Err(E) <= x + let frac_2q = Price::fraction(delta_2q, delta_21)?; // calculate products for left and right + // 6. compute F = y2 * D, Err(F) <= (1+x)^2 - 1 let mut left = y2.mul(&frac_q1)?; + // 7. compute G = y1 * E, Err(G) <= (1+x)^2 - 1 let mut right = y1.mul(&frac_2q)?; - if pre_add_expo.is_some() { - left = left.scale_to_exponent(pre_add_expo?)?; - right = right.scale_to_exponent(pre_add_expo?)?; - } + println!("frac_q1: {}, {}", frac_q1.price, frac_q1.expo); + println!("frac_2q: {}, {}", frac_2q.price, frac_2q.expo); + + println!("left before scaling: {}, {}", left.price, left.expo); + println!("right before scaling: {}, {}", right.price, right.expo); + + // Err(scaling) += 2*10^pre_add_expo + left = left.scale_to_exponent(pre_add_expo)?; + right = right.scale_to_exponent(pre_add_expo)?; if left.expo != right.expo { return None; } - let total = left.add(&right); + println!("left after scaling: {}, {}", left.price, left.expo); + println!("right after scaling: {}, {}", right.price, right.expo); + // 8. compute H = F + G, Err(H) ~= 2x + 2*10^pre_add_expo return left.add(&right); } @@ -408,9 +411,15 @@ impl Price { // Price is not guaranteed to store its price/confidence in normalized form. // Normalize them here to bound the range of price/conf, which is required to perform // arithmetic operations. + println!("numerator orig: {}, {}", self.price, self.expo); + println!("orig price other: {}, {}", other.price, other.expo); + let base = self.normalize()?; let other = other.normalize()?; + println!("numerator post: {}, {}", base.price, base.expo); + println!("orig price post: {}, {}", other.price, other.expo); + if other.price == 0 { return None; } @@ -627,8 +636,16 @@ impl Price { return Some(value <= precision); } - /// Helper function to create fraction and possibly scale to expo -9 - fn fraction(x: i64, y: i64, scale_expo: Option) -> Option { + /// Helper function to create fraction + /// + /// fraction(x, y) gives you the normalized Price closest to x/y. + /// If you cannot represent x/y exactly within 8 digits of precision, it may zero out the remainder. + /// In particular, if x and/or y cannot be represented within 8 digits of precision, potential for precision error. + /// If x and y can both be represented within 8 digits of precision AND x/y can be represented within 8 digits, no precision loss. + /// + /// Error of normalizing x, y <= 10^(PD_EXPO+2) = 10^-7 + /// Inherits any bounded errors from normalization and div + fn fraction(x: i64, y: i64) -> Option { // convert x and y to Prices let x_as_price = Price { price: x, @@ -646,17 +663,13 @@ impl Price { // get the relevant fraction let mut frac = x_as_price.div(&y_as_price)?; - if scale_expo.is_some() { - frac = frac.scale_to_exponent(scale_expo?)?; - } - return Some(frac); } } #[cfg(test)] mod test { - use std::char::MAX; + use quickcheck::{Arbitrary, Gen, TestResult}; use crate::price::{ Price, @@ -688,6 +701,18 @@ mod test { .unwrap() } + // arbitrary trait for quickcheck + impl Arbitrary for Price { + fn arbitrary(g: &mut Gen) -> Price { + Price { + price: i64::arbitrary(g), + conf: u64::arbitrary(g), + expo: i32::arbitrary(g), + publish_time: 0 + } + } + } + #[test] fn test_normalize() { fn succeeds(price1: Price, expected: Price) { @@ -1357,6 +1382,15 @@ mod test { ); // test precision limits + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 0, + 1_000_000_000_000_000_000, + 100, + 90, + -2, + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + ); succeeds( pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), 1, @@ -1430,130 +1464,235 @@ mod test { 90, -2 ); - - // fails bc initial discount exceeds precision - fails( - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), - 50, - 100, - 101, - 90, - -2, - ); - } #[test] fn test_get_borrow_valuation_price() { - fn succeeds(price: Price, borrows: u64, borrows_endpoint: u64, premium_initial: u64, premium_final: u64, premium_precision: u64, expected: Price) { - let price_borrow = price.get_borrow_valuation_price(borrows, borrows_endpoint, premium_initial, premium_final, premium_precision).unwrap(); + fn succeeds(price: Price, borrows: u64, borrows_endpoint: u64, premium_initial: u64, premium_final: u64, premium_exponent: i32, expected: Price) { + let price_borrow = price.get_borrow_valuation_price(borrows, borrows_endpoint, premium_initial, premium_final, premium_exponent).unwrap(); assert_eq!(price_borrow, expected); } - fn fails(price: Price, borrows: u64, borrows_endpoint: u64, premium_initial: u64, premium_final: u64, premium_precision: u64) { - let result = price.get_borrow_valuation_price(borrows, borrows_endpoint, premium_initial, premium_final, premium_precision); + fn fails(price: Price, borrows: u64, borrows_endpoint: u64, premium_initial: u64, premium_final: u64, premium_exponent: i32) { + let result = price.get_borrow_valuation_price(borrows, borrows_endpoint, premium_initial, premium_final, premium_exponent); assert_eq!(result, None); } - // // 0 borrows - // succeeds( - // pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), - // 0, - // 100, - // 0, - // 10, - // 100, - // pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9) - // ); - - // // half borrows - // succeeds( - // pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), - // 50, - // 100, - // 0, - // 10, - // 100, - // pc(105 * (PD_SCALE as i64), 2 * PD_SCALE, -9) - // ); - - // // full borrows - // succeeds( - // pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), - // 100, - // 100, - // 0, - // 10, - // 100, - // pc(110 * (PD_SCALE as i64), 2 * PD_SCALE, -9) - // ); - - // // beyond final endpoint borrows - // succeeds( - // pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), - // 150, - // 100, - // 0, - // 10, - // 100, - // pc(115 * (PD_SCALE as i64), 2 * PD_SCALE, -9) - // ); - - // // 0 borrows, staggered initial premium - // succeeds( - // pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), - // 0, - // 100, - // 2, - // 10, - // 100, - // pc(102 * (PD_SCALE as i64), 2 * PD_SCALE, -9) - // ); - - // // half borrows, staggered initial premium - // succeeds( - // pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), - // 50, - // 100, - // 2, - // 10, - // 100, - // pc(106 * (PD_SCALE as i64), 2 * PD_SCALE, -9) - // ); - - // // full borrows, staggered initial premium - // succeeds( - // pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), - // 100, - // 100, - // 2, - // 10, - // 100, - // pc(110 * (PD_SCALE as i64), 2 * PD_SCALE, -9) - // ); - - // // fails bc initial premium exceeds final premium - // fails( - // pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), - // 50, - // 100, - // 11, - // 10, - // 100 - // ); + // 0 borrows + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 0, + 100, + 100, + 110, + -2, + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // half borrows + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 50, + 100, + 100, + 110, + -2, + pc(105 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // full borrows + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 100, + 100, + 100, + 110, + -2, + pc(110 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // 0 borrows, diff precision + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 0, + 100, + 1000, + 1100, + -3, + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // half borrows, diff precision + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 50, + 100, + 1000, + 1100, + -3, + pc(105 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // full borrows, diff precision + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 100, + 100, + 1000, + 1100, + -3, + pc(110 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // beyond final endpoint borrows + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 150, + 100, + 100, + 110, + -2, + pc(115 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // 0 borrows, staggered initial premium + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 0, + 100, + 102, + 110, + -2, + pc(102 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // half borrows, staggered initial premium + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 50, + 100, + 102, + 110, + -2, + pc(106 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + // full borrows, staggered initial premium + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 100, + 100, + 102, + 110, + -2, + pc(110 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + ); + + + + + + + + + // test precision limits + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 1, + 1_000_000_000_000_000_000, + 100, + 110, + -2, + pc(100 * (PD_SCALE as i64 - 10), 2 * PD_SCALE, -9), + ); + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 100_000_000, + 1_000_000_000_000_000_000, + 100, + 110, + -2, + pc(100 * (PD_SCALE as i64 - 10), 2 * PD_SCALE, -9), + ); + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 1_000_000_000, + 1_000_000_000_000_000_000, + 100, + 110, + -2, + pc(100 * (PD_SCALE as i64 - 10), 2 * PD_SCALE, -9), + ); + // interpolation now doesn't lose precision, but normalize in final multiply loses precision + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 10_000_000_000, + 1_000_000_000_000_000_000, + 100, + 110, + -2, + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + ); + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 20_000_000_000, + 1_000_000_000_000_000_000, + 100, + 110, + -2, + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + ); + // precision no longer lost + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 100_000_000_000, + 1_000_000_000_000_000_000, + 100, + 110, + -2, + pc(100 * (PD_SCALE as i64 + 10), 2 * PD_SCALE, -9), + ); + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 200_000_000_000, + 1_000_000_000_000_000_000, + 100, + 110, + -2, + pc(100 * (PD_SCALE as i64 + 20), 2 * PD_SCALE, -9), + ); + succeeds( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 1_000_000_000_000, + 1_000_000_000_000_000_000, + 100, + 110, + -2, + pc(100 * (PD_SCALE as i64+100), 2 * PD_SCALE, -9), + ); + + // fails bc initial premium exceeds final premium + fails( + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), + 50, + 100, + 111, + 110, + -2 + ); } #[test] fn test_affine_combination() { - fn succeeds(x1: i64, y1: Price, x2: i64, y2: Price, x_query: i64, pre_add_expo: Option, expected: Price) { + fn succeeds(x1: i64, y1: Price, x2: i64, y2: Price, x_query: i64, pre_add_expo: i32, expected: Price) { let y_query = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo).unwrap(); assert_eq!(y_query, expected); } - fn fails(x1: i64, y1: Price, x2: i64, y2: Price, x_query: i64, pre_add_expo: Option) { + fn fails(x1: i64, y1: Price, x2: i64, y2: Price, x_query: i64, pre_add_expo: i32) { let result = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo); assert_eq!(result, None); } @@ -1565,7 +1704,7 @@ mod test { 10, pc(100, 0, -4), 5, - Some(-9), + -9, pc(10_000_000, 0, -9) ); @@ -1576,7 +1715,7 @@ mod test { 10, pc(100, 0, -4), 15, - Some(-9), + -9, pc(10_000_000, 0, -9), ); @@ -1587,7 +1726,7 @@ mod test { 10, pc(100, 0, -4), 5, - Some(-9), + -9, pc(9_500_000, 0, -9) ); @@ -1598,7 +1737,7 @@ mod test { 10, pc(100, 0, -4), 15, - Some(-9), + -9, pc(10_500_000, 0, -9) ); @@ -1609,7 +1748,7 @@ mod test { 10, pc(80, 0, -4), 5, - Some(-9), + -9, pc(9_000_000, 0, -9) ); @@ -1620,7 +1759,7 @@ mod test { 10, pc(80, 0, -4), 15, - Some(-9), + -9, pc(7_000_000, 0, -9) ); @@ -1631,7 +1770,7 @@ mod test { 13, pc(10, 0, -2), 1, - Some(-8), + -8, pc(769230, 0, -8) ); succeeds( @@ -1640,7 +1779,7 @@ mod test { 13, pc(10, 0, -2), 1, - Some(-9), + -9, pc(7692307, 0, -9) ); succeeds( @@ -1649,7 +1788,7 @@ mod test { 13, pc(100, 0, -3), 1, - Some(-9), + -9, pc(7692307, 0, -9) ); succeeds( @@ -1658,7 +1797,7 @@ mod test { 13, pc(100, 0, -2), 1, - Some(-9), + -9, pc(76923076, 0, -9) ); @@ -1669,7 +1808,7 @@ mod test { i64::MAX, pc(0, 0, -9), i64::MAX/10, - Some(-9), + -9, pc(90, 0, -9) ); succeeds( @@ -1678,7 +1817,7 @@ mod test { i64::MIN/2, pc(0, 0, -9), (i64::MIN/4)*3, - Some(-9), + -9, pc(50, 0, -9) ); succeeds( @@ -1687,7 +1826,7 @@ mod test { 0, pc(0, 0, -9), i64::MIN/4, - Some(-9), + -9, pc(25, 0, -9) ); succeeds( @@ -1696,7 +1835,7 @@ mod test { 0, pc(0, 0, -9), 0, - Some(-9), + -9, pc(0, 0, -9) ); @@ -1707,7 +1846,7 @@ mod test { 10, pc(MAX_PD_V_I64, 0, -4), 5, - Some(-4), + -4, pc(MAX_PD_V_I64-6, 0, -4) ); succeeds( @@ -1716,7 +1855,7 @@ mod test { 10, pc(MAX_PD_V_I64, 0, -4), 9, - Some(-4), + -4, pc(MAX_PD_V_I64-1, 0, -4) ); @@ -1727,7 +1866,7 @@ mod test { 10, pc(100, 10, -4), 5, - Some(-9), + -9, pc(9_500_000, 1_000_000, -9) ); @@ -1738,7 +1877,7 @@ mod test { 10, pc(100, 15, -4), 5, - Some(-9), + -9, pc(9_500_000, 1_250_000, -9) ); succeeds( @@ -1747,7 +1886,7 @@ mod test { 10, pc(100, 15, -4), 8, - Some(-9), + -9, pc(9_800_000, 1_400_000, -9) ); succeeds( @@ -1756,7 +1895,7 @@ mod test { 10, pc(100, 15, -4), 15, - Some(-9), + -9, pc(10_500_000, 2_750_000, -9) ); @@ -1767,7 +1906,7 @@ mod test { 10, pc(100, 0, -4), 15, - Some(-9) + -9 ); // fails bc x1 is MIN, x2-x1 --> overflow in delta fails( @@ -1776,7 +1915,7 @@ mod test { 10, pc(1000, 0, -5), 5, - Some(-9) + -9 ); // fails bc x2 is MAX, x1 is negative --> overflow in delta fails( @@ -1785,7 +1924,7 @@ mod test { i64::MAX, pc(1000, 0, -4), 5, - Some(-9) + -9 ); // fails bc of overflow in the checked_sub fails( @@ -1794,16 +1933,7 @@ mod test { i64::MAX/2+1, pc(100, 0, -4), 5, - Some(-9) - ); - // fails bc expos don't match for y1 and y2 - fails( - 0, - pc(100, 0, -4), - 10, - pc(100, 0, -5), - 5, - Some(-9) + -9 ); // fails bc price too small to be realized fails( @@ -1812,16 +1942,7 @@ mod test { 10, pc(5, 0, -4), i64::MAX-100, - Some(-9) - ); - // fails bc no pre_add_expo --> no shared precision pre add - fails( - 0, - pc(100, 0, -4), - i64::MAX, - pc(0, 0, -4), - 25, - None + -9 ); // fails bc 0-i64::MIN > i64::MAX, so overflow fails( @@ -1830,176 +1951,150 @@ mod test { 0, pc(0, 0, -9), 0, - Some(-9) + -9 ); } + // quickcheck to confirm affine_combination introduces no error if normalization done explicitly on prices + #[quickcheck] + fn quickcheck_affine(x1: i64, y1: Price, x2: i64, y2: Price, x_query: i64, pre_add_expo: i32) -> TestResult { + // require x2 > x1 + if x1 >= x2 { + return TestResult::discard() + } + + // require low pre_add_expo + if pre_add_expo >= 2 { + return TestResult::discard() + } + + // require reasonable price/conf range + if (y1.price > 2*MAX_PD_V_I64) || (y1.price < 2*MIN_PD_V_I64) || (y1.conf > 2*MAX_PD_V_U64) { + return TestResult::discard() + } + if (y2.price > 2*MAX_PD_V_I64) || (y2.price < 2*MIN_PD_V_I64) || (y2.conf > 2*MAX_PD_V_U64) { + return TestResult::discard() + } + + let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo).unwrap(); + + let y1_norm = y1.normalize().unwrap(); + let y2_norm = y2.normalize().unwrap(); + + let result_norm = Price::affine_combination(x1, y1_norm, x2, y2_norm, x_query, pre_add_expo).unwrap(); + + TestResult::from_bool(result_norm == result_orig) + } + #[test] fn test_fraction() { - fn succeeds(x: i64, y: i64, scale_expo: Option, expected: Price) { - let frac = Price::fraction(x, y, scale_expo).unwrap(); + fn succeeds(x: i64, y: i64, expected: Price) { + let frac = Price::fraction(x, y).unwrap(); assert_eq!(frac, expected); } - fn fails(x: i64, y: i64, scale_expo: Option) { - let result = Price::fraction(x, y, scale_expo); + fn fails(x: i64, y: i64) { + let result = Price::fraction(x, y); assert_eq!(result, None); } + // check basic tests of fraction division succeeds( 100, - 1000, - Some(-9), + 1000, pc(100_000_000, 0, -9) ); succeeds( 1, 1_000_000_000, - Some(-9), - pc(1, 0, -9) + pc(10, 0, -10) ); - - // test loss due to big numer + // when x and y and x/y can be represented in 8 digits, no loss succeeds( - 1_000_000_000_123, - 1, - None, - pc(100_000_000_000_000_000, 0, -5) + 10_000_001, + 20_000_002, + pc(500_000_000, 0, -9) ); - - // test loss due to big denom succeeds( - 1, - 10_000_000_011, - None, - pc(10, 0, -11) + 102, + 3, + pc(34_000_000_000, 0, -9) ); - // test loss due to scaling + // test loss due to big numer (x cannot be represented in 8 digits)--only preserves 8 digits of precision succeeds( + 3_000_000_021_123, 1, - 1, - None, - pc(1000000000, 0, -9) + pc(30_000_000_000_000_000, 0, -4) ); + + // test loss due to big denom (y cannot be represented in 8 digits) succeeds( 1, - 1, - Some(0), - pc(1, 0, 0) + 10_000_000_011, + pc(10, 0, -11) ); + + // x and y representable within 8 digits, but x/y is not succeeds( 1, - 1, - Some(1), - pc(0, 0, 1) - ); - succeeds( - 100, - 1, - Some(i32::MAX-9), - pc(0, 0, i32::MAX-9) + 7, + pc(142_857_142, 0, -9) ); // Test with big inputs where the output will lose precision. + // x not representable within 8 digits succeeds( i64::MAX, 100, - None, pc(922337200000000, 0, 2) ); succeeds( i64::MAX, 1, - None, pc(92233720000000000, 0, 2) ); + // Neither x nor y representable within 8 digits succeeds( i64::MAX-10, - i64::MAX-5, - None, - pc(1000000000, 0, -9) - ); - succeeds( - 1_000_000_004, - 1_000_000_008, - None, + i64::MAX-10_000_000_000, pc(1000000000, 0, -9) ); - // dividing both of the terms in the above check by 4, but now precision cutoff doesn't kick in in normalize + // Neither x nor y representable within 8 digits, but this subtraction actually influences relevant digit for precision succeeds( - 250_000_001, - 250_000_002, - None, - pc(999999996, 0, -9) + i64::MAX-10, + i64::MAX-100_000_000_000, + pc(1_000_000_010, 0, -9) ); // Test with end range of possible inputs where the output should not lose precision. succeeds( MAX_PD_V_I64, MAX_PD_V_I64, - None, pc(1_000_000_000, 0, -9) ); succeeds( MAX_PD_V_I64, 1, - None, pc(MAX_PD_V_I64*1_000_000_000, 0, -9) ); succeeds( MAX_PD_V_I64, MIN_PD_V_I64, - None, pc(-1_000_000_000, 0, -9) ); succeeds( MIN_PD_V_I64, 1, - None, pc(MIN_PD_V_I64*1_000_000_000, 0, -9) ); - // // Test with end range of possible inputs to identify precision inaccuracy - succeeds( - 1, - MAX_PD_V_I64, - None, - pc(3, 0, -9) - ); - succeeds( - 1, - MIN_PD_V_I64, - None, - pc(-3, 0, -9) - ); - - // fails due to div by 0 fails( 100, 0, - None - ); - - succeeds( - 1_000_000_000_000_000, - 1, - None, - pc(100_000_000_000_000_000, 0, -2) - ); - // fails due to overflow when scaling - fails( - 1_000_000_000_000_000, - 1, - Some(-18) - ); - fails( - 100, - 1, - Some(i32::MIN) ); } } diff --git a/pyth-sdk/src/utils.rs b/pyth-sdk/src/utils.rs index 77c4b8b..030ad06 100644 --- a/pyth-sdk/src/utils.rs +++ b/pyth-sdk/src/utils.rs @@ -1,5 +1,3 @@ -use std::convert::TryFrom; - /// This module helps serde to serialize deserialize some fields as String /// /// The reason this is added is that `#[serde(with = "String")]` does not work @@ -31,22 +29,4 @@ pub mod as_string { .parse() .map_err(|_| D::Error::custom("Input is not valid")) } -} - -pub fn u64_to_i64(value: u64) -> Option { - let value_signed = i64::try_from(value); - - match value_signed { - Ok(x) => return Some(x), - Err(_) => return None, - } -} - -pub fn i32_to_u32(value: i32) -> Option { - let value_unsigned = u32::try_from(value); - - match value_unsigned { - Ok(x) => return Some(x), - Err(_) => return None, - } -} +} \ No newline at end of file From ac2f5779d968c5c0ee7d053bcf6fa127e72ab6ff Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Wed, 1 Feb 2023 14:49:00 -0500 Subject: [PATCH 11/19] updates to quickcheck and manual tests for affine_combination --- pyth-sdk/src/price.rs | 342 +++++++++++++++++++++++++++--------------- 1 file changed, 221 insertions(+), 121 deletions(-) diff --git a/pyth-sdk/src/price.rs b/pyth-sdk/src/price.rs index c3dce91..d645576 100644 --- a/pyth-sdk/src/price.rs +++ b/pyth-sdk/src/price.rs @@ -253,12 +253,12 @@ impl Price { scale_to_exponent(expo_orig)? ; - println!("init perc: {}, {}", initial_percentage.price, initial_percentage.expo); - println!("final perc: {}, {}", final_percentage.price, final_percentage.expo); - println!("interpolated premium: {}, {}", premium_interpolated.price, premium_interpolated.expo); - println!("in btwn: {}, {}", self.mul(&premium_interpolated)?.price, self.mul(&premium_interpolated)?.expo); - println!("price (adj for premium): {}, {}", price_premium.price, price_premium.expo); - println!("======="); + // println!("init perc: {}, {}", initial_percentage.price, initial_percentage.expo); + // println!("final perc: {}, {}", final_percentage.price, final_percentage.expo); + // println!("interpolated premium: {}, {}", premium_interpolated.price, premium_interpolated.expo); + // println!("in btwn: {}, {}", self.mul(&premium_interpolated)?.price, self.mul(&premium_interpolated)?.expo); + // println!("price (adj for premium): {}, {}", price_premium.price, price_premium.expo); + // println!("======="); return Some( Price { @@ -270,10 +270,20 @@ impl Price { ); } - /// Performs an affine combination after setting everything to expo -9 - /// Takes in 2 points and a 3rd "query" x coordinate, to compute the value at + /// affine_combination performs an affine combination of two prices located at x coordinates x1 and x2, for query x coordinate x_query + /// Takes in 2 points and a 3rd "query" x coordinate, to compute the value at x_query /// Effectively draws a line between the 2 points and then proceeds to - /// interpolate/exterpolate to find the value at the query coordinate according to that line + /// interpolate/extrapolate to find the value at the query coordinate according to that line + /// + /// affine_combination gives you the Price, scaled to a specified exponent, closest to y2 * ((xq-x1)/(x2-x1)) + y1 * ((x2-x3)/(x2-x1)) + /// If the numerators and denominators of the fractions there are both representable within 8 digits of precision + /// and the fraction itself is also representable within 8 digits of precision, there is no loss due to taking the fractions. + /// If the prices are normalized, then there is no loss in taking the products via mul. + /// Otherwise, the prices will be converted to a form representable within 8 digits of precision. + /// The scaling to the specified expo pre_add_expo introduces a max error of 2*10^pre_add_expo. + /// If pre_add_expo is small enough relative to the products, then there is no loss due to scaling. + /// If the fractions are expressable within 8 digits of precision, the ys are normalized, and the exponent is sufficiently small, + /// then you get an exact result. Otherwise, your error is bounded as given below. /// /// Args /// x1: i64, the x coordinate of the first point @@ -295,14 +305,12 @@ impl Price { /// 8. compute H = F + G /// /// Bounds due to precision loss - /// x = 1/PD_SCALE - /// division incurs max loss of x - /// Err(D), Err(E) is relatively negligible--by scaling to expo -9 we are imposing - /// a grid composed of 1 billion units between x1 and x2 endpoints. Moreover, D, E <= 1. + /// x = 10^(PD_EXPO+2) + /// fraction (due to normalization & division) incurs max loss of x /// Thus, max loss here: Err(D), Err(E) <= x - /// Err(y1), Err(y2) with normalization <= x - /// Err(F), Err(G) <= (1+x)^2 - 1 (in fractional terms) - /// Err(H) <= 2*(1+x)^2 - 2 ~= 2x = 2/PD_SCALE + /// If y1, y2 already normalized, no additional error. O/w, Err(y1), Err(y2) with normalization <= x + /// Err(F), Err(G) <= (1+x)^2 - 1 (in fractional terms) ~= 2x + /// Err(H) <= 2*2x = 4x, when PD_EXPO = -9 ==> Err(H) <= 4*10^-7 /// /// Scaling this back has error bounded by the expo (10^pre_add_expo). /// This is because reverting a potentially finer expo to a coarser grid has the potential to be off by @@ -313,6 +321,8 @@ impl Price { if x2 <= x1 { return None; } + + println!("{}, {}, {}, {}, {}", x1, x2, y1.price, y2.price, x_query); // get the deltas for the x coordinates // 1. compute A = xq-x1 @@ -348,8 +358,8 @@ impl Price { return None; } - println!("left after scaling: {}, {}", left.price, left.expo); - println!("right after scaling: {}, {}", right.price, right.expo); + // println!("left after scaling: {}, {}", left.price, left.expo); + // println!("right after scaling: {}, {}", right.price, right.expo); // 8. compute H = F + G, Err(H) ~= 2x + 2*10^pre_add_expo return left.add(&right); @@ -411,6 +421,7 @@ impl Price { // Price is not guaranteed to store its price/confidence in normalized form. // Normalize them here to bound the range of price/conf, which is required to perform // arithmetic operations. + println!("numerator orig: {}, {}", self.price, self.expo); println!("orig price other: {}, {}", other.price, other.expo); @@ -619,23 +630,6 @@ impl Price { } } - /// Helper function to check if precision implied by exponent >= value, exponent <= 0 - fn check_precision_geq(value: u64, exponent: i32) -> Option { - if exponent > 0 { - return None; - } - - let mut precision: u64 = 1; - let mut delta = -exponent; - - while delta > 0 && (value != 0) { - precision = precision.checked_mul(10)?; - delta = delta.checked_sub(1)?; - } - - return Some(value <= precision); - } - /// Helper function to create fraction /// /// fraction(x, y) gives you the normalized Price closest to x/y. @@ -661,7 +655,7 @@ impl Price { }; // get the relevant fraction - let mut frac = x_as_price.div(&y_as_price)?; + let frac = x_as_price.div(&y_as_price)?; return Some(frac); } @@ -670,6 +664,7 @@ impl Price { #[cfg(test)] mod test { use quickcheck::{Arbitrary, Gen, TestResult}; + use std::convert::TryFrom; use crate::price::{ Price, @@ -705,9 +700,9 @@ mod test { impl Arbitrary for Price { fn arbitrary(g: &mut Gen) -> Price { Price { - price: i64::arbitrary(g), - conf: u64::arbitrary(g), - expo: i32::arbitrary(g), + price: i64::try_from(i32::arbitrary(g)).ok().unwrap(), + conf: 0, + expo: -9,//i32::arbitrary(g), publish_time: 0 } } @@ -1231,33 +1226,6 @@ mod test { assert_eq!(p2.mul(&p1).unwrap().publish_time, 100); } - #[test] - fn test_check_precision_geq() { - fn succeeds(value: u64, exponent: i32, expected: bool) { - let precision_geq = Price::check_precision_geq(value, exponent).unwrap(); - - assert_eq!(precision_geq, expected); - } - - fn fails(value: u64, exponent: i32) { - let result = Price::check_precision_geq(value, exponent); - assert_eq!(result, None); - } - - succeeds(1, 0, true); - succeeds(1, -1, true); - - succeeds(100, -1, false); - succeeds(100, -2, true); - succeeds(100, -3, true); - - succeeds(101, -2, false); - succeeds(101, -3, true); - - // fails bc exponent > 0 - fails(10, 1); - } - #[test] fn test_get_collateral_valuation_price() { fn succeeds(price: Price, deposits: u64, deposits_endpoint: u64, discount_initial: u64, discount_final: u64, discount_exponent: i32, expected: Price) { @@ -1589,13 +1557,6 @@ mod test { pc(110 * (PD_SCALE as i64), 2 * PD_SCALE, -9) ); - - - - - - - // test precision limits succeeds( pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), @@ -1764,6 +1725,7 @@ mod test { ); // test loss due to scaling + // lose more bc scale to higher expo succeeds( 0, pc(0, 0, -2), @@ -1773,6 +1735,7 @@ mod test { -8, pc(769230, 0, -8) ); + // lose less bc scale to lower expo succeeds( 0, pc(0, 0, -2), @@ -1782,6 +1745,7 @@ mod test { -9, pc(7692307, 0, -9) ); + // lose more bc need to increment expo more in scaling succeeds( 0, pc(0, 0, -3), @@ -1791,6 +1755,7 @@ mod test { -9, pc(7692307, 0, -9) ); + // lose less bc need to increment expo less in scaling succeeds( 0, pc(0, 0, -2), @@ -1801,7 +1766,7 @@ mod test { pc(76923076, 0, -9) ); - // Test with end range of possible inputs on endpoint xs (no precision loss) + // Test with end range of possible inputs on endpoint xs succeeds( 0, pc(100, 0, -9), @@ -1820,45 +1785,88 @@ mod test { -9, pc(50, 0, -9) ); + + // Test with end range of possible inputs in prices to identify precision inaccuracy + // precision inaccuracy due to loss in scaling succeeds( - i64::MIN+1, - pc(100, 0, -9), 0, - pc(0, 0, -9), - i64::MIN/4, + pc(MAX_PD_V_I64-10, 0, -9), + 10, + pc(MAX_PD_V_I64, 0, -9), + 5, -9, - pc(25, 0, -9) + pc(MAX_PD_V_I64-6, 0, -9) ); + // precision inaccruacy due to loss in scaling succeeds( - i64::MIN+1, - pc(100, 0, -9), - 0, - pc(0, 0, -9), 0, + pc(MAX_PD_V_I64-1, 0, -9), + 10, + pc(MAX_PD_V_I64, 0, -9), + 9, -9, - pc(0, 0, -9) + pc(MAX_PD_V_I64-1, 0, -9) ); - // Test with end range of possible inputs in prices to identify precision inaccuracy + // Test with combinations of (in)exact fractions + (un)normalized ys; making pre_add_expo very small to abstract away scaling error + // exact fraction, normalized ys --> exact result succeeds( 0, - pc(MAX_PD_V_I64-10, 0, -4), - 10, - pc(MAX_PD_V_I64, 0, -4), - 5, - -4, - pc(MAX_PD_V_I64-6, 0, -4) + pc(0, 0, -9), + 512, + pc(MAX_PD_V_I64-511, 0, -9), + 1, + -18, + pc(524_287_000_000_000, 0, -18) ); + // exact fraction, unnormalized ys, should be 524_289_000_000_000 exactly, but due to normalization lose <= 2*10^(PD_EXPO+2) + // we see the actual result is off by < 16_000_000, which corresponds to loss of ~= 1.6*10^-8 < 2*10^-7 succeeds( 0, - pc(MAX_PD_V_I64-1, 0, -4), - 10, - pc(MAX_PD_V_I64, 0, -4), - 9, - -4, - pc(MAX_PD_V_I64-1, 0, -4) + pc(0, 0, -9), + 512, + pc(MAX_PD_V_I64+513, 0, -9), + 1, + -18, + pc(524_288_984_375_000, 0, -18) + ); + // inexact fraciton, normalized ys, should be 262_143_000_000_000 exactly, but due to fraction imprecision lose <= 2*10^(PD_EXPO+2) + // 1/1024 = 0.0009765625, but due to imprecision --> 0.00976562; similar for 1023/1024 + // we see the actual result is off by < 140_000_000, which corresponds to loss of 1.4*10^-7 < 2*10^-7 + succeeds( + 0, + pc(0, 0, -9), + 1024, + pc(MAX_PD_V_I64-1023, 0, -9), + 1, + -18, + pc(262_142_865_782_784, 0, -18) + ); + // inexact fraction, unnormalized ys, should be 262_145_000_000_000 exactly, but due to normalization and fraction imprecision lose <= 4*10^(PD_EXPO+2) + // 1/1024 and 1023/1024 precision losses described above + normalization of y2 + // actual result off by < 140_000_000, which corresponds to loss of 1.4*10^-7 < 2*10^-7 + succeeds( + 0, + pc(0, 0, -9), + 1024, + pc(MAX_PD_V_I64+1025, 0, -9), + 1, + -18, + pc(262_144_865_781_760, 0, -18) + ); + // should be -267_912_190_000_000_000 exactly, but due to normalization and fraction imprecision lose <= 4^10^(PD_EXPO+2) + // actual result off by < 2_000_000_000, which corresponds to loss of 2*10^-7 < 4*10^-7 (counting figures from the start of the number) + succeeds( + 0, + pc(MIN_PD_V_I64-1025, 0, -9), + 1024, + pc(MAX_PD_V_I64+1025, 0, -9), + 1, + -18, + pc(-267_912_188_120_944_640, 0, -18) ); + // test w confidence (same at both endpoints) succeeds( 0, @@ -1926,7 +1934,7 @@ mod test { 5, -9 ); - // fails bc of overflow in the checked_sub + // fails bc of overflow in the checked_sub for x2-x1 fails( i64::MIN/2, pc(100, 0, -4), @@ -1935,7 +1943,7 @@ mod test { 5, -9 ); - // fails bc price too small to be realized + // fails bc output price too small to be realized, cannot be scaled to fit with specified pre_add_expo fails( 0, pc(100, 0, -4), @@ -1955,36 +1963,112 @@ mod test { ); } - // quickcheck to confirm affine_combination introduces no error if normalization done explicitly on prices + // quickcheck to confirm affine_combination introduces no error if normalization done explicitly on prices first #[quickcheck] - fn quickcheck_affine(x1: i64, y1: Price, x2: i64, y2: Price, x_query: i64, pre_add_expo: i32) -> TestResult { - // require x2 > x1 - if x1 >= x2 { - return TestResult::discard() - } + fn quickcheck_affine_combination_normalize_prices(x1_inp: i32, y1: Price, x2_inp: i32, y2: Price, x_query_inp: i32) -> TestResult { + let x1 = i64::try_from(x1_inp).ok().unwrap(); + let x2 = i64::try_from(x2_inp).ok().unwrap(); + let x_query = i64::try_from(x_query_inp).ok().unwrap(); - // require low pre_add_expo - if pre_add_expo >= 2 { - return TestResult::discard() - } + let pre_add_expo = -9; - // require reasonable price/conf range - if (y1.price > 2*MAX_PD_V_I64) || (y1.price < 2*MIN_PD_V_I64) || (y1.conf > 2*MAX_PD_V_U64) { - return TestResult::discard() - } - if (y2.price > 2*MAX_PD_V_I64) || (y2.price < 2*MIN_PD_V_I64) || (y2.conf > 2*MAX_PD_V_U64) { - return TestResult::discard() - } + // require x2 > x1 + if x1 >= x2 { + return TestResult::discard() + } + + // require low pre_add_expo + if pre_add_expo >= 2 { + return TestResult::discard() + } + + // require reasonable price/conf range + if (y1.price > 2*MAX_PD_V_I64) || (y1.price < 2*MIN_PD_V_I64) { + return TestResult::discard() + } + if (y2.price > 2*MAX_PD_V_I64) || (y2.price < 2*MIN_PD_V_I64) { + return TestResult::discard() + } + + println!("QUICKCHECKED {}, {}, {}, {}", y1.price, y1.conf, y2.price, y2.conf); + + let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo).unwrap(); + + let y1_norm = y1.normalize().unwrap(); + let y2_norm = y2.normalize().unwrap(); + + let result_norm = Price::affine_combination(x1, y1_norm, x2, y2_norm, x_query, pre_add_expo).unwrap(); + + TestResult::from_bool(result_norm == result_orig) + } - let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo).unwrap(); + // quickcheck to confirm affine_combination introduces bounded error if close fraction x/y passed in first + #[quickcheck] + fn quickcheck_affine_combination_normalize_fractions(x1_inp: i32, y1: Price, x2_inp: i32, y2: Price, x_query_inp: i32) -> TestResult { + println!("Arguments (BEGIN): {}, {}, {}, {}, {}", x1_inp, y1.price, x2_inp, y2.price, x_query_inp); + let x1 = i64::try_from(x1_inp).ok().unwrap(); + let x2 = i64::try_from(x2_inp).ok().unwrap(); + let x_query = i64::try_from(x_query_inp).ok().unwrap(); + + let pre_add_expo = -9; + + // require x2 > x1 + if x1 >= x2 { + return TestResult::discard() + } + + if (x_query > 5*x2) || (x_query < 2*x1 - x2) { + return TestResult::discard() + } + + // require low pre_add_expo + if pre_add_expo >= 2 { + return TestResult::discard() + } - let y1_norm = y1.normalize().unwrap(); - let y2_norm = y2.normalize().unwrap(); + // require reasonable price/conf range + if (y1.price > 2*MAX_PD_V_I64) || (y1.price < 2*MIN_PD_V_I64) { + return TestResult::discard() + } + if (y2.price > 2*MAX_PD_V_I64) || (y2.price < 2*MIN_PD_V_I64) { + return TestResult::discard() + } - let result_norm = Price::affine_combination(x1, y1_norm, x2, y2_norm, x_query, pre_add_expo).unwrap(); + let x1_new: i64; + let xq_new: i64; + let x2_new: i64; - TestResult::from_bool(result_norm == result_orig) + if x2 == 0 { + x1_new = x1; + xq_new = x_query; + x2_new = x2; } + else { + let mut frac_q2 = Price::fraction(x_query-x1, x2-x1).unwrap(); + frac_q2 = frac_q2.scale_to_exponent(-8).unwrap(); + + x1_new = 0; + xq_new = frac_q2.price; + x2_new = 100_000_000 as i64; + } + + let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo).unwrap(). + scale_to_exponent(-7).unwrap(); + // println!("GOT HERE 6, {}, {}, {}", x1_new, x2_new, xq_new); + + let result_norm = Price::affine_combination(x1_new, y1, x2_new, y2, xq_new, pre_add_expo).unwrap(). + scale_to_exponent(-7).unwrap(); + // println!("GOT HERE 7"); + + println!("Arguments (END): {}, {}, {}, {}, {}", x1, y1.price, x2, y2.price, x_query); + println!("result orig: {}, {}, {}, {}", result_orig.price, result_orig.expo, result_orig.conf, result_orig.publish_time); + println!("result norm: {}, {}, {}, {}", result_norm.price, result_norm.expo, result_norm.conf, result_norm.publish_time); + + + let price_diff = result_norm.add(&result_orig.cmul(-1, 0).unwrap()).unwrap(); + + TestResult::from_bool((price_diff.price < 4) && (price_diff.price > -4)) + } #[test] fn test_fraction() { @@ -2022,6 +2106,11 @@ mod test { 3, pc(34_000_000_000, 0, -9) ); + succeeds( + 11_111_111, + 10_000_000, + pc(1_111_111_100, 0, -9) + ); // test loss due to big numer (x cannot be represented in 8 digits)--only preserves 8 digits of precision succeeds( @@ -2090,6 +2179,17 @@ mod test { 1, pc(MIN_PD_V_I64*1_000_000_000, 0, -9) ); + // test cases near the boundary where output should lose precision + succeeds( + MAX_PD_V_I64+1, + 1, + pc(MAX_PD_V_I64/10 * 1_000_000_000, 0, -8) + ); + succeeds( + MAX_PD_V_I64+10, + 1, + pc((MAX_PD_V_I64/10 + 1) * 1_000_000_000, 0, -8) + ); // fails due to div by 0 fails( From aada8abfa2d1c35402f6d24401a6706187cb59ae Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Wed, 1 Feb 2023 14:50:35 -0500 Subject: [PATCH 12/19] get rid of printlns --- pyth-sdk/src/price.rs | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/pyth-sdk/src/price.rs b/pyth-sdk/src/price.rs index d645576..6a31409 100644 --- a/pyth-sdk/src/price.rs +++ b/pyth-sdk/src/price.rs @@ -252,13 +252,6 @@ impl Price { mul(&premium_interpolated)?. scale_to_exponent(expo_orig)? ; - - // println!("init perc: {}, {}", initial_percentage.price, initial_percentage.expo); - // println!("final perc: {}, {}", final_percentage.price, final_percentage.expo); - // println!("interpolated premium: {}, {}", premium_interpolated.price, premium_interpolated.expo); - // println!("in btwn: {}, {}", self.mul(&premium_interpolated)?.price, self.mul(&premium_interpolated)?.expo); - // println!("price (adj for premium): {}, {}", price_premium.price, price_premium.expo); - // println!("======="); return Some( Price { @@ -321,8 +314,6 @@ impl Price { if x2 <= x1 { return None; } - - println!("{}, {}, {}, {}, {}", x1, x2, y1.price, y2.price, x_query); // get the deltas for the x coordinates // 1. compute A = xq-x1 @@ -344,12 +335,6 @@ impl Price { // 7. compute G = y1 * E, Err(G) <= (1+x)^2 - 1 let mut right = y1.mul(&frac_2q)?; - println!("frac_q1: {}, {}", frac_q1.price, frac_q1.expo); - println!("frac_2q: {}, {}", frac_2q.price, frac_2q.expo); - - println!("left before scaling: {}, {}", left.price, left.expo); - println!("right before scaling: {}, {}", right.price, right.expo); - // Err(scaling) += 2*10^pre_add_expo left = left.scale_to_exponent(pre_add_expo)?; right = right.scale_to_exponent(pre_add_expo)?; @@ -358,9 +343,6 @@ impl Price { return None; } - // println!("left after scaling: {}, {}", left.price, left.expo); - // println!("right after scaling: {}, {}", right.price, right.expo); - // 8. compute H = F + G, Err(H) ~= 2x + 2*10^pre_add_expo return left.add(&right); } @@ -422,15 +404,9 @@ impl Price { // Normalize them here to bound the range of price/conf, which is required to perform // arithmetic operations. - println!("numerator orig: {}, {}", self.price, self.expo); - println!("orig price other: {}, {}", other.price, other.expo); - let base = self.normalize()?; let other = other.normalize()?; - println!("numerator post: {}, {}", base.price, base.expo); - println!("orig price post: {}, {}", other.price, other.expo); - if other.price == 0 { return None; } @@ -1990,8 +1966,6 @@ mod test { return TestResult::discard() } - println!("QUICKCHECKED {}, {}, {}, {}", y1.price, y1.conf, y2.price, y2.conf); - let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo).unwrap(); let y1_norm = y1.normalize().unwrap(); @@ -2005,7 +1979,6 @@ mod test { // quickcheck to confirm affine_combination introduces bounded error if close fraction x/y passed in first #[quickcheck] fn quickcheck_affine_combination_normalize_fractions(x1_inp: i32, y1: Price, x2_inp: i32, y2: Price, x_query_inp: i32) -> TestResult { - println!("Arguments (BEGIN): {}, {}, {}, {}, {}", x1_inp, y1.price, x2_inp, y2.price, x_query_inp); let x1 = i64::try_from(x1_inp).ok().unwrap(); let x2 = i64::try_from(x2_inp).ok().unwrap(); let x_query = i64::try_from(x_query_inp).ok().unwrap(); @@ -2054,16 +2027,9 @@ mod test { let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo).unwrap(). scale_to_exponent(-7).unwrap(); - // println!("GOT HERE 6, {}, {}, {}", x1_new, x2_new, xq_new); let result_norm = Price::affine_combination(x1_new, y1, x2_new, y2, xq_new, pre_add_expo).unwrap(). scale_to_exponent(-7).unwrap(); - // println!("GOT HERE 7"); - - println!("Arguments (END): {}, {}, {}, {}, {}", x1, y1.price, x2, y2.price, x_query); - println!("result orig: {}, {}, {}, {}", result_orig.price, result_orig.expo, result_orig.conf, result_orig.publish_time); - println!("result norm: {}, {}, {}, {}", result_norm.price, result_norm.expo, result_norm.conf, result_norm.publish_time); - let price_diff = result_norm.add(&result_orig.cmul(-1, 0).unwrap()).unwrap(); From b9828dd11b88f551460f4e867617510f4e18060f Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Wed, 1 Feb 2023 17:44:30 -0500 Subject: [PATCH 13/19] add tests for different pre_add_expos and different expo fractions --- pyth-sdk/src/price.rs | 115 +++++++++++++++++++++++++++++------------- 1 file changed, 81 insertions(+), 34 deletions(-) diff --git a/pyth-sdk/src/price.rs b/pyth-sdk/src/price.rs index 6a31409..1786ecd 100644 --- a/pyth-sdk/src/price.rs +++ b/pyth-sdk/src/price.rs @@ -639,7 +639,7 @@ impl Price { #[cfg(test)] mod test { - use quickcheck::{Arbitrary, Gen, TestResult}; + use quickcheck::TestResult; use std::convert::TryFrom; use crate::price::{ @@ -672,17 +672,6 @@ mod test { .unwrap() } - // arbitrary trait for quickcheck - impl Arbitrary for Price { - fn arbitrary(g: &mut Gen) -> Price { - Price { - price: i64::try_from(i32::arbitrary(g)).ok().unwrap(), - conf: 0, - expo: -9,//i32::arbitrary(g), - publish_time: 0 - } - } - } #[test] fn test_normalize() { @@ -1700,6 +1689,53 @@ mod test { pc(7_000_000, 0, -9) ); + // test with different pre_add_expos than -9 + succeeds( + 0, + pc(100, 0, -2), + 100, + pc(8000, 0, -4), + 50, + -1, + pc(9, 0, -1) + ); + succeeds( + 100_000, + pc(200_000, 0, -6), + 200_000, + pc(-20_000_000_000, 0, -11), + 175_000, + -4, + pc(-1_000, 0, -4) + ); + succeeds( + 2000, + pc(75, 0, 3), + 10000, + pc(675_000_000, 0, -3), + 6000, + -2, + pc(37_500_000, 0, -2) + ); + succeeds( + 0, + pc(100, 0, 2), + 100, + pc(0, 0, -12), + 200, + -12, + pc(-10_000_000_000_000_000, 0, -12) + ); + succeeds( + 0, + pc(10, 0, 9), + 1000, + pc(2, 0, 10), + 6000, + 6, + pc(70_000, 0, 6) + ); + // test loss due to scaling // lose more bc scale to higher expo succeeds( @@ -1761,6 +1797,16 @@ mod test { -9, pc(50, 0, -9) ); + // test with xs that yield fractions with significantly different expos + succeeds( + 0, + pc(100_000_000, 0, -9), + 1_000_000_000_000_000, + pc(0, 0, -9), + 10_000_000, + -9, + pc(99_999_999, 0, -9) + ); // Test with end range of possible inputs in prices to identify precision inaccuracy // precision inaccuracy due to loss in scaling @@ -1939,33 +1985,34 @@ mod test { ); } + pub fn construct_quickcheck_affine_combination_price(price: i64) -> Price { + return Price { + price: price, + conf: 0, + expo: -9, + publish_time: 0 + } + } + // quickcheck to confirm affine_combination introduces no error if normalization done explicitly on prices first #[quickcheck] - fn quickcheck_affine_combination_normalize_prices(x1_inp: i32, y1: Price, x2_inp: i32, y2: Price, x_query_inp: i32) -> TestResult { + fn quickcheck_affine_combination_normalize_prices(x1_inp: i32, p1: i32, x2_inp: i32, p2: i32, x_query_inp: i32) -> TestResult { + // generating xs and prices from i32 to limit the range to reasonable values and guard against overflow/bespoke constraint setting for quickcheck + let y1 = construct_quickcheck_affine_combination_price(i64::try_from(p1).ok().unwrap()); + let y2 = construct_quickcheck_affine_combination_price(i64::try_from(p2).ok().unwrap()); + let x1 = i64::try_from(x1_inp).ok().unwrap(); let x2 = i64::try_from(x2_inp).ok().unwrap(); let x_query = i64::try_from(x_query_inp).ok().unwrap(); + // stick with single expo for ease of testing and generation let pre_add_expo = -9; - // require x2 > x1 + // require x2 > x1, as needed for affine_combination if x1 >= x2 { return TestResult::discard() } - // require low pre_add_expo - if pre_add_expo >= 2 { - return TestResult::discard() - } - - // require reasonable price/conf range - if (y1.price > 2*MAX_PD_V_I64) || (y1.price < 2*MIN_PD_V_I64) { - return TestResult::discard() - } - if (y2.price > 2*MAX_PD_V_I64) || (y2.price < 2*MIN_PD_V_I64) { - return TestResult::discard() - } - let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo).unwrap(); let y1_norm = y1.normalize().unwrap(); @@ -1978,14 +2025,19 @@ mod test { // quickcheck to confirm affine_combination introduces bounded error if close fraction x/y passed in first #[quickcheck] - fn quickcheck_affine_combination_normalize_fractions(x1_inp: i32, y1: Price, x2_inp: i32, y2: Price, x_query_inp: i32) -> TestResult { + fn quickcheck_affine_combination_normalize_fractions(x1_inp: i32, p1: i32, x2_inp: i32, p2: i32, x_query_inp: i32) -> TestResult { + // generating xs and prices from i32 to limit the range to reasonable values and guard against overflow/bespoke constraint setting for quickcheck + let y1 = construct_quickcheck_affine_combination_price(i64::try_from(p1).ok().unwrap()); + let y2 = construct_quickcheck_affine_combination_price(i64::try_from(p2).ok().unwrap()); + let x1 = i64::try_from(x1_inp).ok().unwrap(); let x2 = i64::try_from(x2_inp).ok().unwrap(); let x_query = i64::try_from(x_query_inp).ok().unwrap(); + // stick with single expo for ease of testing and generation let pre_add_expo = -9; - // require x2 > x1 + // require x2 > x1, as needed for affine_combination if x1 >= x2 { return TestResult::discard() } @@ -1994,11 +2046,6 @@ mod test { return TestResult::discard() } - // require low pre_add_expo - if pre_add_expo >= 2 { - return TestResult::discard() - } - // require reasonable price/conf range if (y1.price > 2*MAX_PD_V_I64) || (y1.price < 2*MIN_PD_V_I64) { return TestResult::discard() From 452f0804c65ca4f5d5cdcb015869d592d7473314 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Wed, 1 Feb 2023 18:35:57 -0500 Subject: [PATCH 14/19] add comments and explanations --- pyth-sdk/src/price.rs | 108 +++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 64 deletions(-) diff --git a/pyth-sdk/src/price.rs b/pyth-sdk/src/price.rs index 1786ecd..fc8380b 100644 --- a/pyth-sdk/src/price.rs +++ b/pyth-sdk/src/price.rs @@ -110,39 +110,25 @@ impl Price { /// Args /// deposits: u64, quantity of token deposited in the protocol /// deposits_endpoint: u64, deposits right endpoint for the affine combination - /// discount_initial: u64, initial discount rate at 0 deposits (units given by discount_exponent) - /// discount_final: u64, final discount rate at deposits_endpoint deposits (units given by discount_exponent) + /// rate_discount_initial: u64, initial discounted rate at 0 deposits (units given by discount_exponent) + /// rate_discount_final: u64, final discounted rate at deposits_endpoint deposits (units given by discount_exponent) /// discount_exponent: u64, the exponent to apply to the discounts above (e.g. if discount_final is 10 but meant to express 0.1/10%, exponent would be -2) /// note that if discount_initial is bigger than 100% per the discount exponent scale, then the initial valuation of the collateral will be higher than the oracle price - /// - /// affine_combination yields us error <= 2/PD_SCALE for discount_interpolated - /// We then multiply this with the price to yield price_discounted before scaling this back to the original expo - /// Output of affine_combination has expo >= -18, price (self) has arbitrary expo - /// Scaling this back to the original expo then has error bounded by the expo (10^expo). - /// This is because reverting a potentially finer expo to a coarser grid has the potential to be off by - /// the order of the atomic unit of the coarser grid. - /// This scaling error combines with the previous error additively: Err <= 2/PD_SCALE + 10^expo - /// - /// The practical error is based on the original expo: - /// if it is big, then the 10^expo loss dominates; - /// otherwise, the 2/PD_SCALE error dominates. - /// - /// Thus, we expect the computed collateral valuation price to be no more than 2/PD_SCALE + 10^expo off of the mathematically true value - /// For this reason, we encourage using this function with prices that have high expos, to minimize the potential error. - pub fn get_collateral_valuation_price(&self, deposits: u64, deposits_endpoint: u64, discount_initial: u64, discount_final: u64, discount_exponent: i32) -> Option { - if discount_initial < discount_final { + pub fn get_collateral_valuation_price(&self, deposits: u64, deposits_endpoint: u64, rate_discount_initial: u64, rate_discount_final: u64, discount_exponent: i32) -> Option { + // valuation price should not increase as amount of collateral grows, so rate_discount_initial should >= rate_discount_final + if rate_discount_initial < rate_discount_final { return None; } // get price versions of discounts let initial_percentage = Price { - price: i64::try_from(discount_initial).ok()?, + price: i64::try_from(rate_discount_initial).ok()?, conf: 0, expo: discount_exponent, publish_time: 0, }; let final_percentage = Price { - price: i64::try_from(discount_final).ok()?, + price: i64::try_from(rate_discount_final).ok()?, conf: 0, expo: discount_exponent, publish_time: 0, @@ -196,39 +182,25 @@ impl Price { /// Args /// borrows: u64, quantity of token borrowed from the protocol /// borrows_endpoint: u64, borrows right endpoint for the affine combination - /// premium_initial: u64, initial premium at 0 borrows (units given by premium_exponent) - /// premium_final: u64, final premium at borrows_endpoint borrows (units given by premium_exponent) + /// rate_premium_initial: u64, initial premium at 0 borrows (units given by premium_exponent) + /// rate_premium_final: u64, final premium at borrows_endpoint borrows (units given by premium_exponent) /// premium_exponent: u64, the exponent to apply to the premiums above (e.g. if premium_final is 50 but meant to express 0.05/5%, exponent would be -3) /// note that if premium_initial is less than 100% per the premium exponent scale, then the initial valuation of the borrow will be lower than the oracle price - /// - /// affine_combination yields us error <= 2/PD_SCALE for premium_interpolated - /// We then multiply this with the price to yield price_premium before scaling this back to the original expo - /// Output of affine_combination has expo >= -18, price (self) has arbitrary expo - /// Scaling this back to the original expo then has error bounded by the expo (10^expo). - /// This is because reverting a potentially finer expo to a coarser grid has the potential to be off by - /// the order of the atomic unit of the coarser grid. - /// This scaling error combines with the previous error additively: Err <= 2/PD_SCALE + 10^expo - /// - /// The practical error is based on the original expo: - /// if it is big, then the 10^expo loss dominates; - /// otherwise, the 2/PD_SCALE error dominates. - /// - /// Thus, we expect the computed borrow valuation price to be no more than 2/PD_SCALE + 10^expo off of the mathematically true value - /// For this reason, we encourage using this function with prices that have high expos, to minimize the potential error. - pub fn get_borrow_valuation_price(&self, borrows: u64, borrows_endpoint: u64, premium_initial: u64, premium_final: u64, premium_exponent: i32) -> Option { - if premium_initial > premium_final { + pub fn get_borrow_valuation_price(&self, borrows: u64, borrows_endpoint: u64, rate_premium_initial: u64, rate_premium_final: u64, premium_exponent: i32) -> Option { + // valuation price should not decrease as amount of borrow grows, so rate_premium_initial should <= rate_premium_final + if rate_premium_initial > rate_premium_final { return None; } // get price versions of premiums let initial_percentage = Price { - price: i64::try_from(premium_initial).ok()?, + price: i64::try_from(rate_premium_initial).ok()?, conf: 0, expo: premium_exponent, publish_time: 0, }; let final_percentage = Price { - price: i64::try_from(premium_final).ok()?, + price: i64::try_from(rate_premium_final).ok()?, conf: 0, expo: premium_exponent, publish_time: 0, @@ -308,7 +280,7 @@ impl Price { /// Scaling this back has error bounded by the expo (10^pre_add_expo). /// This is because reverting a potentially finer expo to a coarser grid has the potential to be off by /// the order of the atomic unit of the coarser grid. - /// This scaling error combines with the previous error additively: Err <= 2/PD_SCALE + 2*10^pre_add_expo + /// This scaling error combines with the previous error additively: Err <= 4x + 2*10^pre_add_expo /// But if pre_add_expo is reasonably small (<= -9), then other term will dominate pub fn affine_combination(x1: i64, y1: Price, x2: i64, y2: Price, x_query: i64, pre_add_expo: i32) -> Option { if x2 <= x1 { @@ -324,15 +296,15 @@ impl Price { let delta_21 = x2.checked_sub(x1)?; // get the relevant fractions of the deltas, with scaling - // 4. compute D = A/C, Err(D) <= x = 1/PD_SCALE + // 4. compute D = A/C, Err(D) <= x let frac_q1 = Price::fraction(delta_q1, delta_21)?; // 5. compute E = B/C, Err(E) <= x let frac_2q = Price::fraction(delta_2q, delta_21)?; // calculate products for left and right - // 6. compute F = y2 * D, Err(F) <= (1+x)^2 - 1 + // 6. compute F = y2 * D, Err(F) <= (1+x)^2 - 1 ~= 2x let mut left = y2.mul(&frac_q1)?; - // 7. compute G = y1 * E, Err(G) <= (1+x)^2 - 1 + // 7. compute G = y1 * E, Err(G) <= (1+x)^2 - 1 ~= 2x let mut right = y1.mul(&frac_2q)?; // Err(scaling) += 2*10^pre_add_expo @@ -343,7 +315,7 @@ impl Price { return None; } - // 8. compute H = F + G, Err(H) ~= 2x + 2*10^pre_add_expo + // 8. compute H = F + G, Err(H) ~= 4x + 2*10^pre_add_expo return left.add(&right); } @@ -1623,7 +1595,7 @@ mod test { assert_eq!(result, None); } - // constant, inbounds + // constant, in the bounds [x1, x2] succeeds( 0, pc(100, 0, -4), @@ -1645,7 +1617,7 @@ mod test { pc(10_000_000, 0, -9), ); - // increasing, inbounds + // increasing, in the bounds succeeds( 0, pc(90, 0, -4), @@ -1667,7 +1639,7 @@ mod test { pc(10_500_000, 0, -9) ); - // decreasing, inbounds + // decreasing, in the bounds succeeds( 0, pc(100, 0, -4), @@ -1737,7 +1709,7 @@ mod test { ); // test loss due to scaling - // lose more bc scale to higher expo + // lose more bc scaling to higher expo succeeds( 0, pc(0, 0, -2), @@ -1747,7 +1719,7 @@ mod test { -8, pc(769230, 0, -8) ); - // lose less bc scale to lower expo + // lose less bc scaling to lower expo succeeds( 0, pc(0, 0, -2), @@ -1757,7 +1729,7 @@ mod test { -9, pc(7692307, 0, -9) ); - // lose more bc need to increment expo more in scaling + // lose more bc need to increment expo more in scaling from original inputs succeeds( 0, pc(0, 0, -3), @@ -1767,7 +1739,7 @@ mod test { -9, pc(7692307, 0, -9) ); - // lose less bc need to increment expo less in scaling + // lose less bc need to increment expo less in scaling from original inputs succeeds( 0, pc(0, 0, -2), @@ -1889,7 +1861,7 @@ mod test { ); - // test w confidence (same at both endpoints) + // test w confidence (same at both endpoints)--expect linear change btwn x1 and x2 and growth in conf as distance from interval [x1, x2] increases succeeds( 0, pc(90, 10, -4), @@ -1995,6 +1967,10 @@ mod test { } // quickcheck to confirm affine_combination introduces no error if normalization done explicitly on prices first + // this quickcheck calls affine_combination with two sets of almost identical inputs: + // the first set has potentially unnormalized prices, the second set simply has the normalized versions of those prices + // this set of checks should pass because normalization is automatically performed on the prices before they are multiplied + // this set of checks passing indicates that it doesn't matter whether the prices passed in are normalized #[quickcheck] fn quickcheck_affine_combination_normalize_prices(x1_inp: i32, p1: i32, x2_inp: i32, p2: i32, x_query_inp: i32) -> TestResult { // generating xs and prices from i32 to limit the range to reasonable values and guard against overflow/bespoke constraint setting for quickcheck @@ -2013,17 +1989,24 @@ mod test { return TestResult::discard() } + // original result let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo).unwrap(); let y1_norm = y1.normalize().unwrap(); let y2_norm = y2.normalize().unwrap(); + // result with normalized price inputs let result_norm = Price::affine_combination(x1, y1_norm, x2, y2_norm, x_query, pre_add_expo).unwrap(); + // results should match exactly TestResult::from_bool(result_norm == result_orig) } // quickcheck to confirm affine_combination introduces bounded error if close fraction x/y passed in first + // this quickcheck calls affine_combination with two sets of similar inputs: + // the first set has xs generated by the quickcheck generation process, leading to potentially inexact fractions that don't fit within 8 digits of precision + // the second set "normalizes" down to 8 digits of precision by setting x1 to 0, x2 to 100_000_000, and xquery proportionally + // based on the bounds described in the docstring of affine_combination, we expect error due to this to be leq 4*10^-7 #[quickcheck] fn quickcheck_affine_combination_normalize_fractions(x1_inp: i32, p1: i32, x2_inp: i32, p2: i32, x_query_inp: i32) -> TestResult { // generating xs and prices from i32 to limit the range to reasonable values and guard against overflow/bespoke constraint setting for quickcheck @@ -2042,15 +2025,8 @@ mod test { return TestResult::discard() } - if (x_query > 5*x2) || (x_query < 2*x1 - x2) { - return TestResult::discard() - } - - // require reasonable price/conf range - if (y1.price > 2*MAX_PD_V_I64) || (y1.price < 2*MIN_PD_V_I64) { - return TestResult::discard() - } - if (y2.price > 2*MAX_PD_V_I64) || (y2.price < 2*MIN_PD_V_I64) { + // constrain x_query to be within 5 interval lengths of x1 or x2 + if (x_query > x2 + 5*(x2-x1)) || (x_query < x1 - 5*(x2-x1)) { return TestResult::discard() } @@ -2072,14 +2048,18 @@ mod test { x2_new = 100_000_000 as i64; } + // original result let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo).unwrap(). scale_to_exponent(-7).unwrap(); + // xs "normalized" result let result_norm = Price::affine_combination(x1_new, y1, x2_new, y2, xq_new, pre_add_expo).unwrap(). scale_to_exponent(-7).unwrap(); + // compute difference in prices let price_diff = result_norm.add(&result_orig.cmul(-1, 0).unwrap()).unwrap(); + // results should differ by less than 4*10^-7 TestResult::from_bool((price_diff.price < 4) && (price_diff.price > -4)) } From a286fad24deeaa657fb8ad0da47990d5c8bc7280 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Thu, 2 Feb 2023 11:11:34 -0500 Subject: [PATCH 15/19] add comment on quickcheck_affine_combination_normalize_fractions --- pyth-sdk/src/price.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/pyth-sdk/src/price.rs b/pyth-sdk/src/price.rs index fc8380b..f6dd889 100644 --- a/pyth-sdk/src/price.rs +++ b/pyth-sdk/src/price.rs @@ -2030,6 +2030,7 @@ mod test { return TestResult::discard() } + // generate new xs based on scaling x_1 --> 0, x_2 --> 10^8 let x1_new: i64; let xq_new: i64; let x2_new: i64; From 0d3ff1864e8941016691826605dcd7b44fcff9d3 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Thu, 2 Feb 2023 15:03:15 -0500 Subject: [PATCH 16/19] nightly fixes --- pyth-sdk/Cargo.toml | 3 +- pyth-sdk/src/price.rs | 875 ++++++++++++++++++++++-------------------- pyth-sdk/src/utils.rs | 2 +- 3 files changed, 469 insertions(+), 411 deletions(-) diff --git a/pyth-sdk/Cargo.toml b/pyth-sdk/Cargo.toml index 36e8c0b..6e781a9 100644 --- a/pyth-sdk/Cargo.toml +++ b/pyth-sdk/Cargo.toml @@ -19,9 +19,8 @@ borsh = "0.9" borsh-derive = "0.9.0" serde = { version = "1.0.136", features = ["derive"] } schemars = "0.8.8" -thiserror = "1.0.24" [dev-dependencies] serde_json = "1.0.79" quickcheck = "1" -quickcheck_macros = "1" \ No newline at end of file +quickcheck_macros = "1" diff --git a/pyth-sdk/src/price.rs b/pyth-sdk/src/price.rs index f6dd889..d0a3034 100644 --- a/pyth-sdk/src/price.rs +++ b/pyth-sdk/src/price.rs @@ -95,169 +95,189 @@ impl Price { /// 1. the net amount currently deposited (across the protocol) /// 2. the deposits endpoint for the affine combination (across the protocol) /// 3. the initial (at 0 deposits) and final (at the deposits endpoint) valuation discount rates - /// + /// /// We use a linear interpolation between the the initial and final discount rates, /// scaled by the proportion of the deposits endpoint that has been deposited. /// This essentially assumes a linear liquidity cumulative density function, /// which has been shown to be a reasonable assumption for many crypto tokens in literature. /// For more detail on this: https://pythnetwork.medium.com/improving-lending-protocols-with-liquidity-oracles-fd1ea4f96f37 - /// - /// If the assumptions of the liquidity curve hold true, we are obtaining a lower bound for the net price - /// at which one can sell the quantity of token specified by deposits in the open markets. - /// We value collateral according to the total deposits in the protocol due to the present - /// intractability of assessing collateral at risk by price range. - /// + /// + /// If the assumptions of the liquidity curve hold true, we are obtaining a lower bound for the + /// net price at which one can sell the quantity of token specified by deposits in the open + /// markets. We value collateral according to the total deposits in the protocol due to the + /// present intractability of assessing collateral at risk by price range. + /// /// Args /// deposits: u64, quantity of token deposited in the protocol /// deposits_endpoint: u64, deposits right endpoint for the affine combination - /// rate_discount_initial: u64, initial discounted rate at 0 deposits (units given by discount_exponent) - /// rate_discount_final: u64, final discounted rate at deposits_endpoint deposits (units given by discount_exponent) - /// discount_exponent: u64, the exponent to apply to the discounts above (e.g. if discount_final is 10 but meant to express 0.1/10%, exponent would be -2) - /// note that if discount_initial is bigger than 100% per the discount exponent scale, then the initial valuation of the collateral will be higher than the oracle price - pub fn get_collateral_valuation_price(&self, deposits: u64, deposits_endpoint: u64, rate_discount_initial: u64, rate_discount_final: u64, discount_exponent: i32) -> Option { - // valuation price should not increase as amount of collateral grows, so rate_discount_initial should >= rate_discount_final + /// rate_discount_initial: u64, initial discounted rate at 0 deposits (units given by + /// discount_exponent) rate_discount_final: u64, final discounted rate at deposits_endpoint + /// deposits (units given by discount_exponent) discount_exponent: u64, the exponent to + /// apply to the discounts above (e.g. if discount_final is 10 but meant to express 0.1/10%, + /// exponent would be -2) note that if discount_initial is bigger than 100% per the discount + /// exponent scale, then the initial valuation of the collateral will be higher than the oracle + /// price + pub fn get_collateral_valuation_price( + &self, + deposits: u64, + deposits_endpoint: u64, + rate_discount_initial: u64, + rate_discount_final: u64, + discount_exponent: i32, + ) -> Option { + // valuation price should not increase as amount of collateral grows, so + // rate_discount_initial should >= rate_discount_final if rate_discount_initial < rate_discount_final { return None; } // get price versions of discounts let initial_percentage = Price { - price: i64::try_from(rate_discount_initial).ok()?, - conf: 0, - expo: discount_exponent, + price: i64::try_from(rate_discount_initial).ok()?, + conf: 0, + expo: discount_exponent, publish_time: 0, }; let final_percentage = Price { - price: i64::try_from(rate_discount_final).ok()?, - conf: 0, - expo: discount_exponent, + price: i64::try_from(rate_discount_final).ok()?, + conf: 0, + expo: discount_exponent, publish_time: 0, }; // get the interpolated discount as a price let discount_interpolated = Price::affine_combination( - 0, - initial_percentage, - i64::try_from(deposits_endpoint).ok()?, - final_percentage, + 0, + initial_percentage, + i64::try_from(deposits_endpoint).ok()?, + final_percentage, i64::try_from(deposits).ok()?, - -9 + -9, )?; let conf_orig = self.conf; let expo_orig = self.expo; // get price discounted, convert back to the original exponents we received the price in - let price_discounted = self. - mul(&discount_interpolated)?. - scale_to_exponent(expo_orig)? - ; - - return Some( - Price { - price: price_discounted.price, - conf: conf_orig, - expo: price_discounted.expo, - publish_time: self.publish_time, - } - ); + let price_discounted = self + .mul(&discount_interpolated)? + .scale_to_exponent(expo_orig)?; + + return Some(Price { + price: price_discounted.price, + conf: conf_orig, + expo: price_discounted.expo, + publish_time: self.publish_time, + }); } /// Get the valuation of a borrow position according to: /// 1. the net amount currently borrowed (across the protocol) /// 2. the borrowed endpoint for the affine combination (across the protocol) /// 3. the initial (at 0 borrows) and final (at the borrow endpoint) valuation premiums - /// + /// /// We use a linear interpolation between the the initial and final premiums, /// scaled by the proportion of the borrows endpoint that has been borrowed out. /// This essentially assumes a linear liquidity cumulative density function, /// which has been shown to be a reasonable assumption for many crypto tokens in literature. /// For more detail on this: https://pythnetwork.medium.com/improving-lending-protocols-with-liquidity-oracles-fd1ea4f96f37 - /// - /// If the assumptions of the liquidity curve hold true, we are obtaining an upper bound for the net price - /// at which one can buy the quantity of token specified by borrows in the open markets. - /// We value the borrows according to the total borrows out of the protocol due to the present - /// intractability of assessing collateral at risk and repayment likelihood by price range. - /// + /// + /// If the assumptions of the liquidity curve hold true, we are obtaining an upper bound for the + /// net price at which one can buy the quantity of token specified by borrows in the open + /// markets. We value the borrows according to the total borrows out of the protocol due to + /// the present intractability of assessing collateral at risk and repayment likelihood by + /// price range. + /// /// Args /// borrows: u64, quantity of token borrowed from the protocol /// borrows_endpoint: u64, borrows right endpoint for the affine combination /// rate_premium_initial: u64, initial premium at 0 borrows (units given by premium_exponent) - /// rate_premium_final: u64, final premium at borrows_endpoint borrows (units given by premium_exponent) - /// premium_exponent: u64, the exponent to apply to the premiums above (e.g. if premium_final is 50 but meant to express 0.05/5%, exponent would be -3) - /// note that if premium_initial is less than 100% per the premium exponent scale, then the initial valuation of the borrow will be lower than the oracle price - pub fn get_borrow_valuation_price(&self, borrows: u64, borrows_endpoint: u64, rate_premium_initial: u64, rate_premium_final: u64, premium_exponent: i32) -> Option { - // valuation price should not decrease as amount of borrow grows, so rate_premium_initial should <= rate_premium_final + /// rate_premium_final: u64, final premium at borrows_endpoint borrows (units given by + /// premium_exponent) premium_exponent: u64, the exponent to apply to the premiums above + /// (e.g. if premium_final is 50 but meant to express 0.05/5%, exponent would be -3) + /// note that if premium_initial is less than 100% per the premium exponent scale, then the + /// initial valuation of the borrow will be lower than the oracle price + pub fn get_borrow_valuation_price( + &self, + borrows: u64, + borrows_endpoint: u64, + rate_premium_initial: u64, + rate_premium_final: u64, + premium_exponent: i32, + ) -> Option { + // valuation price should not decrease as amount of borrow grows, so rate_premium_initial + // should <= rate_premium_final if rate_premium_initial > rate_premium_final { return None; } // get price versions of premiums let initial_percentage = Price { - price: i64::try_from(rate_premium_initial).ok()?, - conf: 0, - expo: premium_exponent, + price: i64::try_from(rate_premium_initial).ok()?, + conf: 0, + expo: premium_exponent, publish_time: 0, }; let final_percentage = Price { - price: i64::try_from(rate_premium_final).ok()?, - conf: 0, - expo: premium_exponent, + price: i64::try_from(rate_premium_final).ok()?, + conf: 0, + expo: premium_exponent, publish_time: 0, }; // get the interpolated premium as a price let premium_interpolated = Price::affine_combination( 0, - initial_percentage, - i64::try_from(borrows_endpoint).ok()?, - final_percentage, + initial_percentage, + i64::try_from(borrows_endpoint).ok()?, + final_percentage, i64::try_from(borrows).ok()?, - -9 + -9, )?; let conf_orig = self.conf; let expo_orig = self.expo; // get price premium, convert back to the original exponents we received the price in - let price_premium = self. - mul(&premium_interpolated)?. - scale_to_exponent(expo_orig)? - ; - - return Some( - Price { - price: price_premium.price, - conf: conf_orig, - expo: price_premium.expo, - publish_time: self.publish_time, - } - ); + let price_premium = self + .mul(&premium_interpolated)? + .scale_to_exponent(expo_orig)?; + + return Some(Price { + price: price_premium.price, + conf: conf_orig, + expo: price_premium.expo, + publish_time: self.publish_time, + }); } - /// affine_combination performs an affine combination of two prices located at x coordinates x1 and x2, for query x coordinate x_query - /// Takes in 2 points and a 3rd "query" x coordinate, to compute the value at x_query - /// Effectively draws a line between the 2 points and then proceeds to - /// interpolate/extrapolate to find the value at the query coordinate according to that line - /// - /// affine_combination gives you the Price, scaled to a specified exponent, closest to y2 * ((xq-x1)/(x2-x1)) + y1 * ((x2-x3)/(x2-x1)) - /// If the numerators and denominators of the fractions there are both representable within 8 digits of precision - /// and the fraction itself is also representable within 8 digits of precision, there is no loss due to taking the fractions. - /// If the prices are normalized, then there is no loss in taking the products via mul. - /// Otherwise, the prices will be converted to a form representable within 8 digits of precision. - /// The scaling to the specified expo pre_add_expo introduces a max error of 2*10^pre_add_expo. - /// If pre_add_expo is small enough relative to the products, then there is no loss due to scaling. - /// If the fractions are expressable within 8 digits of precision, the ys are normalized, and the exponent is sufficiently small, + /// affine_combination performs an affine combination of two prices located at x coordinates x1 + /// and x2, for query x coordinate x_query Takes in 2 points and a 3rd "query" x coordinate, + /// to compute the value at x_query Effectively draws a line between the 2 points and then + /// proceeds to interpolate/extrapolate to find the value at the query coordinate according + /// to that line + /// + /// affine_combination gives you the Price, scaled to a specified exponent, closest to y2 * + /// ((xq-x1)/(x2-x1)) + y1 * ((x2-x3)/(x2-x1)) If the numerators and denominators of the + /// fractions there are both representable within 8 digits of precision and the fraction + /// itself is also representable within 8 digits of precision, there is no loss due to taking + /// the fractions. If the prices are normalized, then there is no loss in taking the + /// products via mul. Otherwise, the prices will be converted to a form representable within + /// 8 digits of precision. The scaling to the specified expo pre_add_expo introduces a max + /// error of 2*10^pre_add_expo. If pre_add_expo is small enough relative to the products, + /// then there is no loss due to scaling. If the fractions are expressable within 8 digits + /// of precision, the ys are normalized, and the exponent is sufficiently small, /// then you get an exact result. Otherwise, your error is bounded as given below. - /// + /// /// Args /// x1: i64, the x coordinate of the first point /// y1: Price, the y coordinate of the first point, represented as a Price struct /// x2: i64, the x coordinate of the second point, must be greater than x1 /// y2: Price, the y coordinate of the second point, represented as a Price struct /// x_query: i64, the query x coordinate, at which we wish to impute a y value - /// pre_add_expo: i32, the exponent to scale to, before final addition; essentially the final precision you want - /// + /// pre_add_expo: i32, the exponent to scale to, before final addition; essentially the final + /// precision you want + /// /// Logic /// imputed y value = y2 * ((xq-x1)/(x2-x1)) + y1 * ((x2-x3)/(x2-x1)) /// 1. compute A = xq-x1 @@ -268,25 +288,38 @@ impl Price { /// 6. compute F = y2 * D /// 7. compute G = y1 * E /// 8. compute H = F + G - /// + /// /// Bounds due to precision loss /// x = 10^(PD_EXPO+2) /// fraction (due to normalization & division) incurs max loss of x /// Thus, max loss here: Err(D), Err(E) <= x - /// If y1, y2 already normalized, no additional error. O/w, Err(y1), Err(y2) with normalization <= x - /// Err(F), Err(G) <= (1+x)^2 - 1 (in fractional terms) ~= 2x + /// If y1, y2 already normalized, no additional error. O/w, Err(y1), Err(y2) with normalization + /// <= x Err(F), Err(G) <= (1+x)^2 - 1 (in fractional terms) ~= 2x /// Err(H) <= 2*2x = 4x, when PD_EXPO = -9 ==> Err(H) <= 4*10^-7 - /// + /// /// Scaling this back has error bounded by the expo (10^pre_add_expo). - /// This is because reverting a potentially finer expo to a coarser grid has the potential to be off by - /// the order of the atomic unit of the coarser grid. - /// This scaling error combines with the previous error additively: Err <= 4x + 2*10^pre_add_expo - /// But if pre_add_expo is reasonably small (<= -9), then other term will dominate - pub fn affine_combination(x1: i64, y1: Price, x2: i64, y2: Price, x_query: i64, pre_add_expo: i32) -> Option { + /// This is because reverting a potentially finer expo to a coarser grid has the potential to be + /// off by the order of the atomic unit of the coarser grid. + /// This scaling error combines with the previous error additively: Err <= 4x + + /// 2*10^pre_add_expo But if pre_add_expo is reasonably small (<= -9), then other term will + /// dominate + /// + /// Note that if the ys are unnormalized due to the confidence but not the price, the + /// normalization could zero out the price fields. Based on this, it is recommended that + /// input prices are normalized, or at least do not contain huge discrepancies between price and + /// confidence. + pub fn affine_combination( + x1: i64, + y1: Price, + x2: i64, + y2: Price, + x_query: i64, + pre_add_expo: i32, + ) -> Option { if x2 <= x1 { return None; } - + // get the deltas for the x coordinates // 1. compute A = xq-x1 let delta_q1 = x_query.checked_sub(x1)?; @@ -305,15 +338,11 @@ impl Price { // 6. compute F = y2 * D, Err(F) <= (1+x)^2 - 1 ~= 2x let mut left = y2.mul(&frac_q1)?; // 7. compute G = y1 * E, Err(G) <= (1+x)^2 - 1 ~= 2x - let mut right = y1.mul(&frac_2q)?; + let mut right = y1.mul(&frac_2q)?; // Err(scaling) += 2*10^pre_add_expo left = left.scale_to_exponent(pre_add_expo)?; right = right.scale_to_exponent(pre_add_expo)?; - - if left.expo != right.expo { - return None; - } // 8. compute H = F + G, Err(H) ~= 4x + 2*10^pre_add_expo return left.add(&right); @@ -579,26 +608,29 @@ impl Price { } /// Helper function to create fraction - /// - /// fraction(x, y) gives you the normalized Price closest to x/y. - /// If you cannot represent x/y exactly within 8 digits of precision, it may zero out the remainder. - /// In particular, if x and/or y cannot be represented within 8 digits of precision, potential for precision error. - /// If x and y can both be represented within 8 digits of precision AND x/y can be represented within 8 digits, no precision loss. - /// + /// + /// fraction(x, y) gives you the unnormalized Price closest to x/y. + /// This output could have arbitrary exponent due to the div, so you may need to call + /// scale_to_exponent to scale to your desired expo. If you cannot represent x/y exactly + /// within 8 digits of precision, it may zero out the remainder. In particular, if x and/or + /// y cannot be represented within 8 digits of precision, potential for precision error. + /// If x and y can both be represented within 8 digits of precision AND x/y can be represented + /// within 8 digits, no precision loss. + /// /// Error of normalizing x, y <= 10^(PD_EXPO+2) = 10^-7 /// Inherits any bounded errors from normalization and div fn fraction(x: i64, y: i64) -> Option { // convert x and y to Prices let x_as_price = Price { - price: x, - conf: 0, - expo: 0, + price: x, + conf: 0, + expo: 0, publish_time: 0, }; let y_as_price = Price { - price: y, - conf: 0, - expo: 0, + price: y, + conf: 0, + expo: 0, publish_time: 0, }; @@ -1165,14 +1197,43 @@ mod test { #[test] fn test_get_collateral_valuation_price() { - fn succeeds(price: Price, deposits: u64, deposits_endpoint: u64, discount_initial: u64, discount_final: u64, discount_exponent: i32, expected: Price) { - let price_collat = price.get_collateral_valuation_price(deposits, deposits_endpoint, discount_initial, discount_final, discount_exponent).unwrap(); + fn succeeds( + price: Price, + deposits: u64, + deposits_endpoint: u64, + discount_initial: u64, + discount_final: u64, + discount_exponent: i32, + expected: Price, + ) { + let price_collat = price + .get_collateral_valuation_price( + deposits, + deposits_endpoint, + discount_initial, + discount_final, + discount_exponent, + ) + .unwrap(); assert_eq!(price_collat, expected); } - fn fails(price: Price, deposits: u64, deposits_endpoint: u64, discount_initial: u64, discount_final: u64, discount_exponent: i32) { - let result = price.get_collateral_valuation_price(deposits, deposits_endpoint, discount_initial, discount_final, discount_exponent); + fn fails( + price: Price, + deposits: u64, + deposits_endpoint: u64, + discount_initial: u64, + discount_final: u64, + discount_exponent: i32, + ) { + let result = price.get_collateral_valuation_price( + deposits, + deposits_endpoint, + discount_initial, + discount_final, + discount_exponent, + ); assert_eq!(result, None); } @@ -1184,7 +1245,7 @@ mod test { 100, 90, -2, - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // half deposits @@ -1195,7 +1256,7 @@ mod test { 100, 90, -2, - pc(95 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(95 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // full deposits @@ -1206,7 +1267,7 @@ mod test { 100, 90, -2, - pc(90 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(90 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // 0 deposits, diff precision @@ -1217,7 +1278,7 @@ mod test { 1000, 900, -3, - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // half deposits, diff precision @@ -1228,7 +1289,7 @@ mod test { 1000, 900, -3, - pc(95 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(95 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // full deposits, diff precision @@ -1239,7 +1300,7 @@ mod test { 1000, 900, -3, - pc(90 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(90 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // beyond final endpoint deposits @@ -1250,7 +1311,7 @@ mod test { 100, 90, -2, - pc(85 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(85 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // 0 deposits, staggered initial discount @@ -1261,7 +1322,7 @@ mod test { 98, 90, -2, - pc(98 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(98 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // half deposits, staggered initial discount @@ -1272,7 +1333,7 @@ mod test { 98, 90, -2, - pc(94 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(94 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // full deposits, staggered initial discount @@ -1283,7 +1344,7 @@ mod test { 98, 90, -2, - pc(90 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(90 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // test precision limits @@ -1303,7 +1364,7 @@ mod test { 100, 90, -2, - pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, -9), + pc(100 * (PD_SCALE as i64) - 1000, 2 * PD_SCALE, -9), ); succeeds( pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), @@ -1312,7 +1373,7 @@ mod test { 100, 90, -2, - pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, -9), + pc(100 * (PD_SCALE as i64) - 1000, 2 * PD_SCALE, -9), ); succeeds( pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), @@ -1321,7 +1382,7 @@ mod test { 100, 90, -2, - pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, -9), + pc(100 * (PD_SCALE as i64) - 1000, 2 * PD_SCALE, -9), ); succeeds( pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), @@ -1330,7 +1391,7 @@ mod test { 100, 90, -2, - pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, -9), + pc(100 * (PD_SCALE as i64) - 1000, 2 * PD_SCALE, -9), ); succeeds( pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), @@ -1339,7 +1400,7 @@ mod test { 100, 90, -2, - pc(100 * (PD_SCALE as i64)-1000, 2 * PD_SCALE, -9), + pc(100 * (PD_SCALE as i64) - 1000, 2 * PD_SCALE, -9), ); succeeds( pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), @@ -1348,7 +1409,7 @@ mod test { 100, 90, -2, - pc(100 * (PD_SCALE as i64)-2000, 2 * PD_SCALE, -9), + pc(100 * (PD_SCALE as i64) - 2000, 2 * PD_SCALE, -9), ); succeeds( pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), @@ -1357,7 +1418,7 @@ mod test { 100, 90, -2, - pc(100 * (PD_SCALE as i64)-10000, 2 * PD_SCALE, -9), + pc(100 * (PD_SCALE as i64) - 10000, 2 * PD_SCALE, -9), ); // fails bc initial discount lower than final discount @@ -1367,20 +1428,49 @@ mod test { 100, 89, 90, - -2 + -2, ); } #[test] fn test_get_borrow_valuation_price() { - fn succeeds(price: Price, borrows: u64, borrows_endpoint: u64, premium_initial: u64, premium_final: u64, premium_exponent: i32, expected: Price) { - let price_borrow = price.get_borrow_valuation_price(borrows, borrows_endpoint, premium_initial, premium_final, premium_exponent).unwrap(); - + fn succeeds( + price: Price, + borrows: u64, + borrows_endpoint: u64, + premium_initial: u64, + premium_final: u64, + premium_exponent: i32, + expected: Price, + ) { + let price_borrow = price + .get_borrow_valuation_price( + borrows, + borrows_endpoint, + premium_initial, + premium_final, + premium_exponent, + ) + .unwrap(); + assert_eq!(price_borrow, expected); } - fn fails(price: Price, borrows: u64, borrows_endpoint: u64, premium_initial: u64, premium_final: u64, premium_exponent: i32) { - let result = price.get_borrow_valuation_price(borrows, borrows_endpoint, premium_initial, premium_final, premium_exponent); + fn fails( + price: Price, + borrows: u64, + borrows_endpoint: u64, + premium_initial: u64, + premium_final: u64, + premium_exponent: i32, + ) { + let result = price.get_borrow_valuation_price( + borrows, + borrows_endpoint, + premium_initial, + premium_final, + premium_exponent, + ); assert_eq!(result, None); } @@ -1392,7 +1482,7 @@ mod test { 100, 110, -2, - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // half borrows @@ -1403,7 +1493,7 @@ mod test { 100, 110, -2, - pc(105 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(105 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // full borrows @@ -1414,7 +1504,7 @@ mod test { 100, 110, -2, - pc(110 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(110 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // 0 borrows, diff precision @@ -1425,7 +1515,7 @@ mod test { 1000, 1100, -3, - pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(100 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // half borrows, diff precision @@ -1436,7 +1526,7 @@ mod test { 1000, 1100, -3, - pc(105 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(105 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // full borrows, diff precision @@ -1447,7 +1537,7 @@ mod test { 1000, 1100, -3, - pc(110 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(110 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // beyond final endpoint borrows @@ -1458,7 +1548,7 @@ mod test { 100, 110, -2, - pc(115 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(115 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // 0 borrows, staggered initial premium @@ -1469,7 +1559,7 @@ mod test { 102, 110, -2, - pc(102 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(102 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // half borrows, staggered initial premium @@ -1480,7 +1570,7 @@ mod test { 102, 110, -2, - pc(106 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(106 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // full borrows, staggered initial premium @@ -1491,7 +1581,7 @@ mod test { 102, 110, -2, - pc(110 * (PD_SCALE as i64), 2 * PD_SCALE, -9) + pc(110 * (PD_SCALE as i64), 2 * PD_SCALE, -9), ); // test precision limits @@ -1567,7 +1657,7 @@ mod test { 100, 110, -2, - pc(100 * (PD_SCALE as i64+100), 2 * PD_SCALE, -9), + pc(100 * (PD_SCALE as i64 + 100), 2 * PD_SCALE, -9), ); // fails bc initial premium exceeds final premium @@ -1577,16 +1667,23 @@ mod test { 100, 111, 110, - -2 + -2, ); - } #[test] fn test_affine_combination() { - fn succeeds(x1: i64, y1: Price, x2: i64, y2: Price, x_query: i64, pre_add_expo: i32, expected: Price) { + fn succeeds( + x1: i64, + y1: Price, + x2: i64, + y2: Price, + x_query: i64, + pre_add_expo: i32, + expected: Price, + ) { let y_query = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo).unwrap(); - + assert_eq!(y_query, expected); } @@ -1598,114 +1695,114 @@ mod test { // constant, in the bounds [x1, x2] succeeds( 0, - pc(100, 0, -4), + pc(100, 10, -4), 10, - pc(100, 0, -4), + pc(100, 10, -4), 5, -9, - pc(10_000_000, 0, -9) + pc(10_000_000, 1_000_000, -9), ); // constant, outside the bounds succeeds( 0, - pc(100, 0, -4), + pc(100, 10, -4), 10, - pc(100, 0, -4), + pc(100, 10, -4), 15, -9, - pc(10_000_000, 0, -9), + pc(10_000_000, 2_000_000, -9), ); // increasing, in the bounds succeeds( 0, - pc(90, 0, -4), + pc(90, 9, -4), 10, - pc(100, 0, -4), + pc(100, 10, -4), 5, -9, - pc(9_500_000, 0, -9) + pc(9_500_000, 950_000, -9), ); // increasing, out of bounds succeeds( 0, - pc(90, 0, -4), + pc(90, 9, -4), 10, - pc(100, 0, -4), + pc(100, 10, -4), 15, -9, - pc(10_500_000, 0, -9) + pc(10_500_000, 1_950_000, -9), ); // decreasing, in the bounds succeeds( 0, - pc(100, 0, -4), + pc(100, 10, -4), 10, - pc(80, 0, -4), + pc(80, 8, -4), 5, -9, - pc(9_000_000, 0, -9) + pc(9_000_000, 900_000, -9), ); // decreasing, out of bounds succeeds( 0, - pc(100, 0, -4), + pc(100, 10, -4), 10, - pc(80, 0, -4), + pc(80, 8, -4), 15, -9, - pc(7_000_000, 0, -9) + pc(7_000_000, 1_700_000, -9), ); // test with different pre_add_expos than -9 succeeds( 0, - pc(100, 0, -2), + pc(100, 10, -2), 100, - pc(8000, 0, -4), + pc(8000, 800, -4), 50, - -1, - pc(9, 0, -1) + -3, + pc(900, 90, -3), ); succeeds( 100_000, - pc(200_000, 0, -6), + pc(200_000, 20_000, -6), 200_000, - pc(-20_000_000_000, 0, -11), + pc(-20_000_000_000, 2_000_000_000, -11), 175_000, -4, - pc(-1_000, 0, -4) + pc(-1_000, 200, -4), ); succeeds( 2000, - pc(75, 0, 3), + pc(75, 7, 3), 10000, - pc(675_000_000, 0, -3), + pc(675_000_000, 67_500_000, -3), 6000, -2, - pc(37_500_000, 0, -2) + pc(37_500_000, 3_725_000, -2), ); succeeds( 0, - pc(100, 0, 2), + pc(100, 10, 2), 100, pc(0, 0, -12), 200, -12, - pc(-10_000_000_000_000_000, 0, -12) + pc(-10_000_000_000_000_000, 1_000_000_000_000_000, -12), ); succeeds( 0, - pc(10, 0, 9), + pc(10, 1, 9), 1000, pc(2, 0, 10), 6000, 6, - pc(70_000, 0, 6) + pc(70_000, 5_000, 6), ); // test loss due to scaling @@ -1714,154 +1811,185 @@ mod test { 0, pc(0, 0, -2), 13, - pc(10, 0, -2), + pc(10, 1, -2), 1, -8, - pc(769230, 0, -8) + pc(769230, 76923, -8), ); // lose less bc scaling to lower expo succeeds( 0, pc(0, 0, -2), 13, - pc(10, 0, -2), + pc(10, 1, -2), 1, -9, - pc(7692307, 0, -9) + pc(7692307, 769230, -9), ); // lose more bc need to increment expo more in scaling from original inputs succeeds( 0, pc(0, 0, -3), 13, - pc(100, 0, -3), + pc(100, 10, -3), 1, -9, - pc(7692307, 0, -9) + pc(7692307, 769230, -9), ); // lose less bc need to increment expo less in scaling from original inputs succeeds( 0, pc(0, 0, -2), 13, - pc(100, 0, -2), + pc(100, 10, -2), 1, -9, - pc(76923076, 0, -9) + pc(76923076, 7692307, -9), ); // Test with end range of possible inputs on endpoint xs succeeds( 0, - pc(100, 0, -9), + pc(100, 10, -9), i64::MAX, pc(0, 0, -9), - i64::MAX/10, + i64::MAX / 10, -9, - pc(90, 0, -9) + pc(90, 9, -9), ); succeeds( i64::MIN, - pc(100, 0, -9), - i64::MIN/2, + pc(100, 10, -9), + i64::MIN / 2, pc(0, 0, -9), - (i64::MIN/4)*3, + (i64::MIN / 4) * 3, -9, - pc(50, 0, -9) + pc(50, 5, -9), ); // test with xs that yield fractions with significantly different expos succeeds( 0, - pc(100_000_000, 0, -9), + pc(100_000_000, 10_000_000, -9), 1_000_000_000_000_000, pc(0, 0, -9), 10_000_000, -9, - pc(99_999_999, 0, -9) + pc(99_999_999, 9_999_999, -9), ); // Test with end range of possible inputs in prices to identify precision inaccuracy // precision inaccuracy due to loss in scaling succeeds( 0, - pc(MAX_PD_V_I64-10, 0, -9), + pc(MAX_PD_V_I64 - 10, 1000, -9), 10, - pc(MAX_PD_V_I64, 0, -9), + pc(MAX_PD_V_I64, 997, -9), 5, -9, - pc(MAX_PD_V_I64-6, 0, -9) + pc(MAX_PD_V_I64 - 6, 998, -9), ); // precision inaccruacy due to loss in scaling succeeds( 0, - pc(MAX_PD_V_I64-1, 0, -9), + pc(MAX_PD_V_I64 - 1, 200, -9), 10, - pc(MAX_PD_V_I64, 0, -9), + pc(MAX_PD_V_I64, 191, -9), 9, -9, - pc(MAX_PD_V_I64-1, 0, -9) + pc(MAX_PD_V_I64 - 1, 191, -9), + ); + // // test with max u64 in conf + // // normalization to first price causes loss of price; loss in conf precision, only + // preserve 8 digits of precision + succeeds( + 0, + pc(1000, u64::MAX, -9), + 1000, + pc(-1000, 0, -9), + 500, + -9, + pc(-500, 92_23_372_000_000_000_000, -9), ); + // test with MAX_PD_V_U64 in conf--no loss in precision unlike above + succeeds( + 0, + pc(1000, MAX_PD_V_U64, -9), + 1000, + pc(-1000, 0, -9), + 500, + -9, + pc(0, MAX_PD_V_U64 / 2, -9), + ); + - // Test with combinations of (in)exact fractions + (un)normalized ys; making pre_add_expo very small to abstract away scaling error - // exact fraction, normalized ys --> exact result + // Test with combinations of (in)exact fractions + (un)normalized ys; making pre_add_expo + // very small to abstract away scaling error exact fraction, normalized ys --> exact + // result succeeds( 0, pc(0, 0, -9), 512, - pc(MAX_PD_V_I64-511, 0, -9), + pc(MAX_PD_V_I64 - 511, 512, -9), 1, -18, - pc(524_287_000_000_000, 0, -18) + pc(524_287_000_000_000, 1_000_000_000, -18), ); - // exact fraction, unnormalized ys, should be 524_289_000_000_000 exactly, but due to normalization lose <= 2*10^(PD_EXPO+2) - // we see the actual result is off by < 16_000_000, which corresponds to loss of ~= 1.6*10^-8 < 2*10^-7 + // exact fraction, unnormalized ys, should be 524_289_000_000_000 exactly, but due to + // normalization lose <= 2*10^(PD_EXPO+2) we see the actual result is off by < + // 16_000_000, which corresponds to loss of ~= 1.6*10^-8 < 2*10^-7 as can be seen, + // the normalization also messes with the final confidence precision succeeds( 0, pc(0, 0, -9), 512, - pc(MAX_PD_V_I64+513, 0, -9), + pc(MAX_PD_V_I64 + 513, 512, -9), 1, -18, - pc(524_288_984_375_000, 0, -18) + pc(524_288_984_375_000, 996_093_750, -18), ); - // inexact fraciton, normalized ys, should be 262_143_000_000_000 exactly, but due to fraction imprecision lose <= 2*10^(PD_EXPO+2) - // 1/1024 = 0.0009765625, but due to imprecision --> 0.00976562; similar for 1023/1024 - // we see the actual result is off by < 140_000_000, which corresponds to loss of 1.4*10^-7 < 2*10^-7 + // inexact fraciton, normalized ys, should be 262_143_000_000_000 exactly, but due to + // fraction imprecision lose <= 2*10^(PD_EXPO+2) 1/1024 = 0.0009765625, but due to + // imprecision --> 0.00976562; similar for 1023/1024 we see the actual result is off + // by < 140_000_000, which corresponds to loss of 1.4*10^-7 < 2*10^-7 + // inexact fraction also messes with the final confidence precision succeeds( 0, pc(0, 0, -9), 1024, - pc(MAX_PD_V_I64-1023, 0, -9), + pc(MAX_PD_V_I64 - 1023, 1024, -9), 1, -18, - pc(262_142_865_782_784, 0, -18) + pc(262_142_865_782_784, 999_999_488, -18), ); - // inexact fraction, unnormalized ys, should be 262_145_000_000_000 exactly, but due to normalization and fraction imprecision lose <= 4*10^(PD_EXPO+2) - // 1/1024 and 1023/1024 precision losses described above + normalization of y2 - // actual result off by < 140_000_000, which corresponds to loss of 1.4*10^-7 < 2*10^-7 + // inexact fraction, unnormalized ys, should be 262_145_000_000_000 exactly, but due to + // normalization and fraction imprecision lose <= 4*10^(PD_EXPO+2) 1/1024 and + // 1023/1024 precision losses described above + normalization of y2 actual result + // off by < 140_000_000, which corresponds to loss of 1.4*10^-7 < 2*10^-7 succeeds( 0, pc(0, 0, -9), 1024, - pc(MAX_PD_V_I64+1025, 0, -9), + pc(MAX_PD_V_I64 + 1025, 1024, -9), 1, -18, - pc(262_144_865_781_760, 0, -18) + pc(262_144_865_781_760, 996_093_240, -18), ); - // should be -267_912_190_000_000_000 exactly, but due to normalization and fraction imprecision lose <= 4^10^(PD_EXPO+2) - // actual result off by < 2_000_000_000, which corresponds to loss of 2*10^-7 < 4*10^-7 (counting figures from the start of the number) + // should be -267_912_190_000_000_000 exactly, but due to normalization and fraction + // imprecision lose <= 4^10^(PD_EXPO+2) actual result off by < 2_000_000_000, which + // corresponds to loss of 2*10^-7 < 4*10^-7 (counting figures from the start of the number) succeeds( 0, - pc(MIN_PD_V_I64-1025, 0, -9), + pc(MIN_PD_V_I64 - 1025, 0, -9), 1024, - pc(MAX_PD_V_I64+1025, 0, -9), + pc(MAX_PD_V_I64 + 1025, 0, -9), 1, -18, - pc(-267_912_188_120_944_640, 0, -18) + pc(-267_912_188_120_944_640, 0, -18), ); - // test w confidence (same at both endpoints)--expect linear change btwn x1 and x2 and growth in conf as distance from interval [x1, x2] increases + // test w confidence (same at both endpoints)--expect linear change btwn x1 and x2 and + // growth in conf as distance from interval [x1, x2] increases succeeds( 0, pc(90, 10, -4), @@ -1869,7 +1997,7 @@ mod test { pc(100, 10, -4), 5, -9, - pc(9_500_000, 1_000_000, -9) + pc(9_500_000, 1_000_000, -9), ); // test w confidence (different at the endpoints) @@ -1880,7 +2008,7 @@ mod test { pc(100, 15, -4), 5, -9, - pc(9_500_000, 1_250_000, -9) + pc(9_500_000, 1_250_000, -9), ); succeeds( 0, @@ -1889,7 +2017,7 @@ mod test { pc(100, 15, -4), 8, -9, - pc(9_800_000, 1_400_000, -9) + pc(9_800_000, 1_400_000, -9), ); succeeds( 0, @@ -1898,85 +2026,60 @@ mod test { pc(100, 15, -4), 15, -9, - pc(10_500_000, 2_750_000, -9) + pc(10_500_000, 2_750_000, -9), ); // fails bc x1 > x2 - fails( - 20, - pc(100, 0, -4), - 10, - pc(100, 0, -4), - 15, - -9 - ); + fails(20, pc(100, 10, -4), 10, pc(100, 20, -4), 15, -9); // fails bc x1 is MIN, x2-x1 --> overflow in delta - fails( - i64::MIN, - pc(100, 0, -5), - 10, - pc(1000, 0, -5), - 5, - -9 - ); + fails(i64::MIN, pc(100, 20, -5), 10, pc(1000, 40, -5), 5, -9); // fails bc x2 is MAX, x1 is negative --> overflow in delta - fails( - -5, - pc(100, 0, -4), - i64::MAX, - pc(1000, 0, -4), - 5, - -9 - ); + fails(-5, pc(100, 40, -4), i64::MAX, pc(1000, 10, -4), 5, -9); // fails bc of overflow in the checked_sub for x2-x1 fails( - i64::MIN/2, - pc(100, 0, -4), - i64::MAX/2+1, - pc(100, 0, -4), + i64::MIN / 2, + pc(100, 20, -4), + i64::MAX / 2 + 1, + pc(100, 30, -4), 5, - -9 - ); - // fails bc output price too small to be realized, cannot be scaled to fit with specified pre_add_expo - fails( - 0, - pc(100, 0, -4), - 10, - pc(5, 0, -4), - i64::MAX-100, - -9 + -9, ); + // fails bc output price too small to be realized, cannot be scaled to fit with specified + // pre_add_expo + fails(0, pc(100, 0, -4), 10, pc(5, 50, -4), i64::MAX - 100, -9); // fails bc 0-i64::MIN > i64::MAX, so overflow - fails( - i64::MIN, - pc(100, 0, -9), - 0, - pc(0, 0, -9), - 0, - -9 - ); + fails(i64::MIN, pc(100, 10, -9), 0, pc(0, 12, -9), 0, -9); } pub fn construct_quickcheck_affine_combination_price(price: i64) -> Price { return Price { - price: price, - conf: 0, - expo: -9, - publish_time: 0 - } + price: price, + conf: 0, + expo: -9, + publish_time: 0, + }; } - // quickcheck to confirm affine_combination introduces no error if normalization done explicitly on prices first - // this quickcheck calls affine_combination with two sets of almost identical inputs: - // the first set has potentially unnormalized prices, the second set simply has the normalized versions of those prices - // this set of checks should pass because normalization is automatically performed on the prices before they are multiplied - // this set of checks passing indicates that it doesn't matter whether the prices passed in are normalized + // quickcheck to confirm affine_combination introduces no error if normalization done explicitly + // on prices first this quickcheck calls affine_combination with two sets of almost + // identical inputs: the first set has potentially unnormalized prices, the second set + // simply has the normalized versions of those prices this set of checks should pass because + // normalization is automatically performed on the prices before they are multiplied + // this set of checks passing indicates that it doesn't matter whether the prices passed in are + // normalized #[quickcheck] - fn quickcheck_affine_combination_normalize_prices(x1_inp: i32, p1: i32, x2_inp: i32, p2: i32, x_query_inp: i32) -> TestResult { - // generating xs and prices from i32 to limit the range to reasonable values and guard against overflow/bespoke constraint setting for quickcheck + fn quickcheck_affine_combination_normalize_prices( + x1_inp: i32, + p1: i32, + x2_inp: i32, + p2: i32, + x_query_inp: i32, + ) -> TestResult { + // generating xs and prices from i32 to limit the range to reasonable values and guard + // against overflow/bespoke constraint setting for quickcheck let y1 = construct_quickcheck_affine_combination_price(i64::try_from(p1).ok().unwrap()); let y2 = construct_quickcheck_affine_combination_price(i64::try_from(p2).ok().unwrap()); - + let x1 = i64::try_from(x1_inp).ok().unwrap(); let x2 = i64::try_from(x2_inp).ok().unwrap(); let x_query = i64::try_from(x_query_inp).ok().unwrap(); @@ -1986,7 +2089,7 @@ mod test { // require x2 > x1, as needed for affine_combination if x1 >= x2 { - return TestResult::discard() + return TestResult::discard(); } // original result @@ -1996,23 +2099,33 @@ mod test { let y2_norm = y2.normalize().unwrap(); // result with normalized price inputs - let result_norm = Price::affine_combination(x1, y1_norm, x2, y2_norm, x_query, pre_add_expo).unwrap(); + let result_norm = + Price::affine_combination(x1, y1_norm, x2, y2_norm, x_query, pre_add_expo).unwrap(); // results should match exactly TestResult::from_bool(result_norm == result_orig) } - // quickcheck to confirm affine_combination introduces bounded error if close fraction x/y passed in first - // this quickcheck calls affine_combination with two sets of similar inputs: - // the first set has xs generated by the quickcheck generation process, leading to potentially inexact fractions that don't fit within 8 digits of precision - // the second set "normalizes" down to 8 digits of precision by setting x1 to 0, x2 to 100_000_000, and xquery proportionally - // based on the bounds described in the docstring of affine_combination, we expect error due to this to be leq 4*10^-7 + // quickcheck to confirm affine_combination introduces bounded error if close fraction x/y + // passed in first this quickcheck calls affine_combination with two sets of similar inputs: + // the first set has xs generated by the quickcheck generation process, leading to potentially + // inexact fractions that don't fit within 8 digits of precision the second set "normalizes" + // down to 8 digits of precision by setting x1 to 0, x2 to 100_000_000, and xquery + // proportionally based on the bounds described in the docstring of affine_combination, we + // expect error due to this to be leq 4*10^-7 #[quickcheck] - fn quickcheck_affine_combination_normalize_fractions(x1_inp: i32, p1: i32, x2_inp: i32, p2: i32, x_query_inp: i32) -> TestResult { - // generating xs and prices from i32 to limit the range to reasonable values and guard against overflow/bespoke constraint setting for quickcheck + fn quickcheck_affine_combination_normalize_fractions( + x1_inp: i32, + p1: i32, + x2_inp: i32, + p2: i32, + x_query_inp: i32, + ) -> TestResult { + // generating xs and prices from i32 to limit the range to reasonable values and guard + // against overflow/bespoke constraint setting for quickcheck let y1 = construct_quickcheck_affine_combination_price(i64::try_from(p1).ok().unwrap()); let y2 = construct_quickcheck_affine_combination_price(i64::try_from(p2).ok().unwrap()); - + let x1 = i64::try_from(x1_inp).ok().unwrap(); let x2 = i64::try_from(x2_inp).ok().unwrap(); let x_query = i64::try_from(x_query_inp).ok().unwrap(); @@ -2022,12 +2135,12 @@ mod test { // require x2 > x1, as needed for affine_combination if x1 >= x2 { - return TestResult::discard() + return TestResult::discard(); } // constrain x_query to be within 5 interval lengths of x1 or x2 - if (x_query > x2 + 5*(x2-x1)) || (x_query < x1 - 5*(x2-x1)) { - return TestResult::discard() + if (x_query > x2 + 5 * (x2 - x1)) || (x_query < x1 - 5 * (x2 - x1)) { + return TestResult::discard(); } // generate new xs based on scaling x_1 --> 0, x_2 --> 10^8 @@ -2039,9 +2152,8 @@ mod test { x1_new = x1; xq_new = x_query; x2_new = x2; - } - else { - let mut frac_q2 = Price::fraction(x_query-x1, x2-x1).unwrap(); + } else { + let mut frac_q2 = Price::fraction(x_query - x1, x2 - x1).unwrap(); frac_q2 = frac_q2.scale_to_exponent(-8).unwrap(); x1_new = 0; @@ -2050,16 +2162,20 @@ mod test { } // original result - let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo).unwrap(). - scale_to_exponent(-7).unwrap(); + let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo) + .unwrap() + .scale_to_exponent(-7) + .unwrap(); // xs "normalized" result - let result_norm = Price::affine_combination(x1_new, y1, x2_new, y2, xq_new, pre_add_expo).unwrap(). - scale_to_exponent(-7).unwrap(); + let result_norm = Price::affine_combination(x1_new, y1, x2_new, y2, xq_new, pre_add_expo) + .unwrap() + .scale_to_exponent(-7) + .unwrap(); // compute difference in prices let price_diff = result_norm.add(&result_orig.cmul(-1, 0).unwrap()).unwrap(); - + // results should differ by less than 4*10^-7 TestResult::from_bool((price_diff.price < 4) && (price_diff.price > -4)) } @@ -2068,7 +2184,7 @@ mod test { fn test_fraction() { fn succeeds(x: i64, y: i64, expected: Price) { let frac = Price::fraction(x, y).unwrap(); - + assert_eq!(frac, expected); } @@ -2077,118 +2193,61 @@ mod test { assert_eq!(result, None); } - + // check basic tests of fraction division - succeeds( - 100, - 1000, - pc(100_000_000, 0, -9) - ); - succeeds( - 1, - 1_000_000_000, - pc(10, 0, -10) - ); + succeeds(100, 1000, pc(100_000_000, 0, -9)); + succeeds(1, 1_000_000_000, pc(10, 0, -10)); // when x and y and x/y can be represented in 8 digits, no loss - succeeds( - 10_000_001, - 20_000_002, - pc(500_000_000, 0, -9) - ); - succeeds( - 102, - 3, - pc(34_000_000_000, 0, -9) - ); - succeeds( - 11_111_111, - 10_000_000, - pc(1_111_111_100, 0, -9) - ); + succeeds(10_000_001, 20_000_002, pc(500_000_000, 0, -9)); + succeeds(102, 3, pc(34_000_000_000, 0, -9)); + succeeds(11_111_111, 10_000_000, pc(1_111_111_100, 0, -9)); - // test loss due to big numer (x cannot be represented in 8 digits)--only preserves 8 digits of precision - succeeds( - 3_000_000_021_123, - 1, - pc(30_000_000_000_000_000, 0, -4) - ); + // test loss due to big numer (x cannot be represented in 8 digits)--only preserves 8 digits + // of precision + succeeds(3_000_000_021_123, 1, pc(30_000_000_000_000_000, 0, -4)); // test loss due to big denom (y cannot be represented in 8 digits) - succeeds( - 1, - 10_000_000_011, - pc(10, 0, -11) - ); + succeeds(1, 10_000_000_011, pc(10, 0, -11)); // x and y representable within 8 digits, but x/y is not - succeeds( - 1, - 7, - pc(142_857_142, 0, -9) - ); + succeeds(1, 7, pc(142_857_142, 0, -9)); // Test with big inputs where the output will lose precision. // x not representable within 8 digits - succeeds( - i64::MAX, - 100, - pc(922337200000000, 0, 2) - ); - succeeds( - i64::MAX, - 1, - pc(92233720000000000, 0, 2) - ); + succeeds(i64::MAX, 100, pc(922337200000000, 0, 2)); + succeeds(i64::MAX, 1, pc(92233720000000000, 0, 2)); // Neither x nor y representable within 8 digits succeeds( - i64::MAX-10, - i64::MAX-10_000_000_000, - pc(1000000000, 0, -9) + i64::MAX - 10, + i64::MAX - 10_000_000_000, + pc(1000000000, 0, -9), ); - // Neither x nor y representable within 8 digits, but this subtraction actually influences relevant digit for precision + // Neither x nor y representable within 8 digits, but this subtraction actually influences + // relevant digit for precision succeeds( - i64::MAX-10, - i64::MAX-100_000_000_000, - pc(1_000_000_010, 0, -9) + i64::MAX - 10, + i64::MAX - 100_000_000_000, + pc(1_000_000_010, 0, -9), ); // Test with end range of possible inputs where the output should not lose precision. - succeeds( - MAX_PD_V_I64, - MAX_PD_V_I64, - pc(1_000_000_000, 0, -9) - ); - succeeds( - MAX_PD_V_I64, - 1, - pc(MAX_PD_V_I64*1_000_000_000, 0, -9) - ); - succeeds( - MAX_PD_V_I64, - MIN_PD_V_I64, - pc(-1_000_000_000, 0, -9) - ); - succeeds( - MIN_PD_V_I64, - 1, - pc(MIN_PD_V_I64*1_000_000_000, 0, -9) - ); + succeeds(MAX_PD_V_I64, MAX_PD_V_I64, pc(1_000_000_000, 0, -9)); + succeeds(MAX_PD_V_I64, 1, pc(MAX_PD_V_I64 * 1_000_000_000, 0, -9)); + succeeds(MAX_PD_V_I64, MIN_PD_V_I64, pc(-1_000_000_000, 0, -9)); + succeeds(MIN_PD_V_I64, 1, pc(MIN_PD_V_I64 * 1_000_000_000, 0, -9)); // test cases near the boundary where output should lose precision succeeds( - MAX_PD_V_I64+1, + MAX_PD_V_I64 + 1, 1, - pc(MAX_PD_V_I64/10 * 1_000_000_000, 0, -8) + pc(MAX_PD_V_I64 / 10 * 1_000_000_000, 0, -8), ); succeeds( - MAX_PD_V_I64+10, + MAX_PD_V_I64 + 10, 1, - pc((MAX_PD_V_I64/10 + 1) * 1_000_000_000, 0, -8) + pc((MAX_PD_V_I64 / 10 + 1) * 1_000_000_000, 0, -8), ); // fails due to div by 0 - fails( - 100, - 0, - ); + fails(100, 0); } } diff --git a/pyth-sdk/src/utils.rs b/pyth-sdk/src/utils.rs index 030ad06..b83c69c 100644 --- a/pyth-sdk/src/utils.rs +++ b/pyth-sdk/src/utils.rs @@ -29,4 +29,4 @@ pub mod as_string { .parse() .map_err(|_| D::Error::custom("Input is not valid")) } -} \ No newline at end of file +} From 2ff14432c9b0c5206b3ff3c2f7a61c7c13b3aef4 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Thu, 2 Feb 2023 16:06:04 -0500 Subject: [PATCH 17/19] experiment removing quickcheck --- pyth-sdk/Cargo.toml | 2 - pyth-sdk/src/lib.rs | 10 +- pyth-sdk/src/price.rs | 259 +++++++++++++++++++++--------------------- 3 files changed, 135 insertions(+), 136 deletions(-) diff --git a/pyth-sdk/Cargo.toml b/pyth-sdk/Cargo.toml index 6e781a9..79b161c 100644 --- a/pyth-sdk/Cargo.toml +++ b/pyth-sdk/Cargo.toml @@ -22,5 +22,3 @@ schemars = "0.8.8" [dev-dependencies] serde_json = "1.0.79" -quickcheck = "1" -quickcheck_macros = "1" diff --git a/pyth-sdk/src/lib.rs b/pyth-sdk/src/lib.rs index d2aef87..f7066f3 100644 --- a/pyth-sdk/src/lib.rs +++ b/pyth-sdk/src/lib.rs @@ -1,8 +1,8 @@ -#[cfg(test)] -extern crate quickcheck; -#[cfg(test)] -#[macro_use(quickcheck)] -extern crate quickcheck_macros; +// #[cfg(test)] +// extern crate quickcheck; +// #[cfg(test)] +// #[macro_use(quickcheck)] +// extern crate quickcheck_macros; use borsh::{ BorshDeserialize, diff --git a/pyth-sdk/src/price.rs b/pyth-sdk/src/price.rs index d0a3034..37909d0 100644 --- a/pyth-sdk/src/price.rs +++ b/pyth-sdk/src/price.rs @@ -643,7 +643,7 @@ impl Price { #[cfg(test)] mod test { - use quickcheck::TestResult; + // use quickcheck::TestResult; use std::convert::TryFrom; use crate::price::{ @@ -2051,134 +2051,135 @@ mod test { fails(i64::MIN, pc(100, 10, -9), 0, pc(0, 12, -9), 0, -9); } - pub fn construct_quickcheck_affine_combination_price(price: i64) -> Price { - return Price { - price: price, - conf: 0, - expo: -9, - publish_time: 0, - }; - } - - // quickcheck to confirm affine_combination introduces no error if normalization done explicitly - // on prices first this quickcheck calls affine_combination with two sets of almost - // identical inputs: the first set has potentially unnormalized prices, the second set - // simply has the normalized versions of those prices this set of checks should pass because - // normalization is automatically performed on the prices before they are multiplied - // this set of checks passing indicates that it doesn't matter whether the prices passed in are - // normalized - #[quickcheck] - fn quickcheck_affine_combination_normalize_prices( - x1_inp: i32, - p1: i32, - x2_inp: i32, - p2: i32, - x_query_inp: i32, - ) -> TestResult { - // generating xs and prices from i32 to limit the range to reasonable values and guard - // against overflow/bespoke constraint setting for quickcheck - let y1 = construct_quickcheck_affine_combination_price(i64::try_from(p1).ok().unwrap()); - let y2 = construct_quickcheck_affine_combination_price(i64::try_from(p2).ok().unwrap()); - - let x1 = i64::try_from(x1_inp).ok().unwrap(); - let x2 = i64::try_from(x2_inp).ok().unwrap(); - let x_query = i64::try_from(x_query_inp).ok().unwrap(); - - // stick with single expo for ease of testing and generation - let pre_add_expo = -9; - - // require x2 > x1, as needed for affine_combination - if x1 >= x2 { - return TestResult::discard(); - } - - // original result - let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo).unwrap(); - - let y1_norm = y1.normalize().unwrap(); - let y2_norm = y2.normalize().unwrap(); - - // result with normalized price inputs - let result_norm = - Price::affine_combination(x1, y1_norm, x2, y2_norm, x_query, pre_add_expo).unwrap(); - - // results should match exactly - TestResult::from_bool(result_norm == result_orig) - } - - // quickcheck to confirm affine_combination introduces bounded error if close fraction x/y - // passed in first this quickcheck calls affine_combination with two sets of similar inputs: - // the first set has xs generated by the quickcheck generation process, leading to potentially - // inexact fractions that don't fit within 8 digits of precision the second set "normalizes" - // down to 8 digits of precision by setting x1 to 0, x2 to 100_000_000, and xquery - // proportionally based on the bounds described in the docstring of affine_combination, we - // expect error due to this to be leq 4*10^-7 - #[quickcheck] - fn quickcheck_affine_combination_normalize_fractions( - x1_inp: i32, - p1: i32, - x2_inp: i32, - p2: i32, - x_query_inp: i32, - ) -> TestResult { - // generating xs and prices from i32 to limit the range to reasonable values and guard - // against overflow/bespoke constraint setting for quickcheck - let y1 = construct_quickcheck_affine_combination_price(i64::try_from(p1).ok().unwrap()); - let y2 = construct_quickcheck_affine_combination_price(i64::try_from(p2).ok().unwrap()); - - let x1 = i64::try_from(x1_inp).ok().unwrap(); - let x2 = i64::try_from(x2_inp).ok().unwrap(); - let x_query = i64::try_from(x_query_inp).ok().unwrap(); - - // stick with single expo for ease of testing and generation - let pre_add_expo = -9; - - // require x2 > x1, as needed for affine_combination - if x1 >= x2 { - return TestResult::discard(); - } - - // constrain x_query to be within 5 interval lengths of x1 or x2 - if (x_query > x2 + 5 * (x2 - x1)) || (x_query < x1 - 5 * (x2 - x1)) { - return TestResult::discard(); - } - - // generate new xs based on scaling x_1 --> 0, x_2 --> 10^8 - let x1_new: i64; - let xq_new: i64; - let x2_new: i64; - - if x2 == 0 { - x1_new = x1; - xq_new = x_query; - x2_new = x2; - } else { - let mut frac_q2 = Price::fraction(x_query - x1, x2 - x1).unwrap(); - frac_q2 = frac_q2.scale_to_exponent(-8).unwrap(); - - x1_new = 0; - xq_new = frac_q2.price; - x2_new = 100_000_000 as i64; - } - - // original result - let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo) - .unwrap() - .scale_to_exponent(-7) - .unwrap(); - - // xs "normalized" result - let result_norm = Price::affine_combination(x1_new, y1, x2_new, y2, xq_new, pre_add_expo) - .unwrap() - .scale_to_exponent(-7) - .unwrap(); - - // compute difference in prices - let price_diff = result_norm.add(&result_orig.cmul(-1, 0).unwrap()).unwrap(); - - // results should differ by less than 4*10^-7 - TestResult::from_bool((price_diff.price < 4) && (price_diff.price > -4)) - } + // pub fn construct_quickcheck_affine_combination_price(price: i64) -> Price { + // return Price { + // price: price, + // conf: 0, + // expo: -9, + // publish_time: 0, + // }; + // } + + // // quickcheck to confirm affine_combination introduces no error if normalization done + // explicitly // on prices first this quickcheck calls affine_combination with two sets of + // almost // identical inputs: the first set has potentially unnormalized prices, the second + // set // simply has the normalized versions of those prices this set of checks should pass + // because // normalization is automatically performed on the prices before they are + // multiplied // this set of checks passing indicates that it doesn't matter whether the + // prices passed in are // normalized + // #[quickcheck] + // fn quickcheck_affine_combination_normalize_prices( + // x1_inp: i32, + // p1: i32, + // x2_inp: i32, + // p2: i32, + // x_query_inp: i32, + // ) -> TestResult { + // // generating xs and prices from i32 to limit the range to reasonable values and guard + // // against overflow/bespoke constraint setting for quickcheck + // let y1 = construct_quickcheck_affine_combination_price(i64::try_from(p1).ok().unwrap()); + // let y2 = construct_quickcheck_affine_combination_price(i64::try_from(p2).ok().unwrap()); + + // let x1 = i64::try_from(x1_inp).ok().unwrap(); + // let x2 = i64::try_from(x2_inp).ok().unwrap(); + // let x_query = i64::try_from(x_query_inp).ok().unwrap(); + + // // stick with single expo for ease of testing and generation + // let pre_add_expo = -9; + + // // require x2 > x1, as needed for affine_combination + // if x1 >= x2 { + // return TestResult::discard(); + // } + + // // original result + // let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, + // pre_add_expo).unwrap(); + + // let y1_norm = y1.normalize().unwrap(); + // let y2_norm = y2.normalize().unwrap(); + + // // result with normalized price inputs + // let result_norm = + // Price::affine_combination(x1, y1_norm, x2, y2_norm, x_query, pre_add_expo).unwrap(); + + // // results should match exactly + // TestResult::from_bool(result_norm == result_orig) + // } + + // // quickcheck to confirm affine_combination introduces bounded error if close fraction x/y + // // passed in first this quickcheck calls affine_combination with two sets of similar inputs: + // // the first set has xs generated by the quickcheck generation process, leading to + // potentially // inexact fractions that don't fit within 8 digits of precision the second + // set "normalizes" // down to 8 digits of precision by setting x1 to 0, x2 to 100_000_000, + // and xquery // proportionally based on the bounds described in the docstring of + // affine_combination, we // expect error due to this to be leq 4*10^-7 + // #[quickcheck] + // fn quickcheck_affine_combination_normalize_fractions( + // x1_inp: i32, + // p1: i32, + // x2_inp: i32, + // p2: i32, + // x_query_inp: i32, + // ) -> TestResult { + // // generating xs and prices from i32 to limit the range to reasonable values and guard + // // against overflow/bespoke constraint setting for quickcheck + // let y1 = construct_quickcheck_affine_combination_price(i64::try_from(p1).ok().unwrap()); + // let y2 = construct_quickcheck_affine_combination_price(i64::try_from(p2).ok().unwrap()); + + // let x1 = i64::try_from(x1_inp).ok().unwrap(); + // let x2 = i64::try_from(x2_inp).ok().unwrap(); + // let x_query = i64::try_from(x_query_inp).ok().unwrap(); + + // // stick with single expo for ease of testing and generation + // let pre_add_expo = -9; + + // // require x2 > x1, as needed for affine_combination + // if x1 >= x2 { + // return TestResult::discard(); + // } + + // // constrain x_query to be within 5 interval lengths of x1 or x2 + // if (x_query > x2 + 5 * (x2 - x1)) || (x_query < x1 - 5 * (x2 - x1)) { + // return TestResult::discard(); + // } + + // // generate new xs based on scaling x_1 --> 0, x_2 --> 10^8 + // let x1_new: i64; + // let xq_new: i64; + // let x2_new: i64; + + // if x2 == 0 { + // x1_new = x1; + // xq_new = x_query; + // x2_new = x2; + // } else { + // let mut frac_q2 = Price::fraction(x_query - x1, x2 - x1).unwrap(); + // frac_q2 = frac_q2.scale_to_exponent(-8).unwrap(); + + // x1_new = 0; + // xq_new = frac_q2.price; + // x2_new = 100_000_000 as i64; + // } + + // // original result + // let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo) + // .unwrap() + // .scale_to_exponent(-7) + // .unwrap(); + + // // xs "normalized" result + // let result_norm = Price::affine_combination(x1_new, y1, x2_new, y2, xq_new, pre_add_expo) + // .unwrap() + // .scale_to_exponent(-7) + // .unwrap(); + + // // compute difference in prices + // let price_diff = result_norm.add(&result_orig.cmul(-1, 0).unwrap()).unwrap(); + + // // results should differ by less than 4*10^-7 + // TestResult::from_bool((price_diff.price < 4) && (price_diff.price > -4)) + // } #[test] fn test_fraction() { From 2fbeb17fb1cbce5c71eb784d7e3915260ac52b13 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Thu, 2 Feb 2023 16:59:26 -0500 Subject: [PATCH 18/19] attempt to fix build issues --- Cargo.toml | 2 - pyth-sdk/Cargo.toml | 3 + pyth-sdk/src/lib.rs | 10 +- pyth-sdk/src/price.rs | 259 +++++++++++++++++++++--------------------- 4 files changed, 137 insertions(+), 137 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aa2dca3..fda81a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,5 @@ members = [ "pyth-sdk", "pyth-sdk-solana", "pyth-sdk-solana/test-contract", - "pyth-sdk-cw", - "examples/cw-contract", "examples/sol-contract" ] diff --git a/pyth-sdk/Cargo.toml b/pyth-sdk/Cargo.toml index 79b161c..580ca5c 100644 --- a/pyth-sdk/Cargo.toml +++ b/pyth-sdk/Cargo.toml @@ -19,6 +19,9 @@ borsh = "0.9" borsh-derive = "0.9.0" serde = { version = "1.0.136", features = ["derive"] } schemars = "0.8.8" +getrandom = { version = "0.2.2", features = ["custom"] } [dev-dependencies] serde_json = "1.0.79" +quickcheck = "1" +quickcheck_macros = "1" diff --git a/pyth-sdk/src/lib.rs b/pyth-sdk/src/lib.rs index f7066f3..d2aef87 100644 --- a/pyth-sdk/src/lib.rs +++ b/pyth-sdk/src/lib.rs @@ -1,8 +1,8 @@ -// #[cfg(test)] -// extern crate quickcheck; -// #[cfg(test)] -// #[macro_use(quickcheck)] -// extern crate quickcheck_macros; +#[cfg(test)] +extern crate quickcheck; +#[cfg(test)] +#[macro_use(quickcheck)] +extern crate quickcheck_macros; use borsh::{ BorshDeserialize, diff --git a/pyth-sdk/src/price.rs b/pyth-sdk/src/price.rs index 37909d0..26798fc 100644 --- a/pyth-sdk/src/price.rs +++ b/pyth-sdk/src/price.rs @@ -643,7 +643,7 @@ impl Price { #[cfg(test)] mod test { - // use quickcheck::TestResult; + use quickcheck::TestResult; use std::convert::TryFrom; use crate::price::{ @@ -2051,135 +2051,134 @@ mod test { fails(i64::MIN, pc(100, 10, -9), 0, pc(0, 12, -9), 0, -9); } - // pub fn construct_quickcheck_affine_combination_price(price: i64) -> Price { - // return Price { - // price: price, - // conf: 0, - // expo: -9, - // publish_time: 0, - // }; - // } - - // // quickcheck to confirm affine_combination introduces no error if normalization done - // explicitly // on prices first this quickcheck calls affine_combination with two sets of - // almost // identical inputs: the first set has potentially unnormalized prices, the second - // set // simply has the normalized versions of those prices this set of checks should pass - // because // normalization is automatically performed on the prices before they are - // multiplied // this set of checks passing indicates that it doesn't matter whether the - // prices passed in are // normalized - // #[quickcheck] - // fn quickcheck_affine_combination_normalize_prices( - // x1_inp: i32, - // p1: i32, - // x2_inp: i32, - // p2: i32, - // x_query_inp: i32, - // ) -> TestResult { - // // generating xs and prices from i32 to limit the range to reasonable values and guard - // // against overflow/bespoke constraint setting for quickcheck - // let y1 = construct_quickcheck_affine_combination_price(i64::try_from(p1).ok().unwrap()); - // let y2 = construct_quickcheck_affine_combination_price(i64::try_from(p2).ok().unwrap()); - - // let x1 = i64::try_from(x1_inp).ok().unwrap(); - // let x2 = i64::try_from(x2_inp).ok().unwrap(); - // let x_query = i64::try_from(x_query_inp).ok().unwrap(); - - // // stick with single expo for ease of testing and generation - // let pre_add_expo = -9; - - // // require x2 > x1, as needed for affine_combination - // if x1 >= x2 { - // return TestResult::discard(); - // } - - // // original result - // let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, - // pre_add_expo).unwrap(); - - // let y1_norm = y1.normalize().unwrap(); - // let y2_norm = y2.normalize().unwrap(); - - // // result with normalized price inputs - // let result_norm = - // Price::affine_combination(x1, y1_norm, x2, y2_norm, x_query, pre_add_expo).unwrap(); - - // // results should match exactly - // TestResult::from_bool(result_norm == result_orig) - // } - - // // quickcheck to confirm affine_combination introduces bounded error if close fraction x/y - // // passed in first this quickcheck calls affine_combination with two sets of similar inputs: - // // the first set has xs generated by the quickcheck generation process, leading to - // potentially // inexact fractions that don't fit within 8 digits of precision the second - // set "normalizes" // down to 8 digits of precision by setting x1 to 0, x2 to 100_000_000, - // and xquery // proportionally based on the bounds described in the docstring of - // affine_combination, we // expect error due to this to be leq 4*10^-7 - // #[quickcheck] - // fn quickcheck_affine_combination_normalize_fractions( - // x1_inp: i32, - // p1: i32, - // x2_inp: i32, - // p2: i32, - // x_query_inp: i32, - // ) -> TestResult { - // // generating xs and prices from i32 to limit the range to reasonable values and guard - // // against overflow/bespoke constraint setting for quickcheck - // let y1 = construct_quickcheck_affine_combination_price(i64::try_from(p1).ok().unwrap()); - // let y2 = construct_quickcheck_affine_combination_price(i64::try_from(p2).ok().unwrap()); - - // let x1 = i64::try_from(x1_inp).ok().unwrap(); - // let x2 = i64::try_from(x2_inp).ok().unwrap(); - // let x_query = i64::try_from(x_query_inp).ok().unwrap(); - - // // stick with single expo for ease of testing and generation - // let pre_add_expo = -9; - - // // require x2 > x1, as needed for affine_combination - // if x1 >= x2 { - // return TestResult::discard(); - // } - - // // constrain x_query to be within 5 interval lengths of x1 or x2 - // if (x_query > x2 + 5 * (x2 - x1)) || (x_query < x1 - 5 * (x2 - x1)) { - // return TestResult::discard(); - // } - - // // generate new xs based on scaling x_1 --> 0, x_2 --> 10^8 - // let x1_new: i64; - // let xq_new: i64; - // let x2_new: i64; - - // if x2 == 0 { - // x1_new = x1; - // xq_new = x_query; - // x2_new = x2; - // } else { - // let mut frac_q2 = Price::fraction(x_query - x1, x2 - x1).unwrap(); - // frac_q2 = frac_q2.scale_to_exponent(-8).unwrap(); - - // x1_new = 0; - // xq_new = frac_q2.price; - // x2_new = 100_000_000 as i64; - // } - - // // original result - // let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo) - // .unwrap() - // .scale_to_exponent(-7) - // .unwrap(); - - // // xs "normalized" result - // let result_norm = Price::affine_combination(x1_new, y1, x2_new, y2, xq_new, pre_add_expo) - // .unwrap() - // .scale_to_exponent(-7) - // .unwrap(); - - // // compute difference in prices - // let price_diff = result_norm.add(&result_orig.cmul(-1, 0).unwrap()).unwrap(); - - // // results should differ by less than 4*10^-7 - // TestResult::from_bool((price_diff.price < 4) && (price_diff.price > -4)) - // } + pub fn construct_quickcheck_affine_combination_price(price: i64) -> Price { + return Price { + price: price, + conf: 0, + expo: -9, + publish_time: 0, + }; + } + + // quickcheck to confirm affine_combination introduces no error if normalization done + // explicitly on prices first this quickcheck calls affine_combination with two sets of + // almost identical inputs: the first set has potentially unnormalized prices, the second + // set simply has the normalized versions of those prices this set of checks should pass + // because normalization is automatically performed on the prices before they are + // multiplied this set of checks passing indicates that it doesn't matter whether the + // prices passed in are normalized + #[quickcheck] + fn quickcheck_affine_combination_normalize_prices( + x1_inp: i32, + p1: i32, + x2_inp: i32, + p2: i32, + x_query_inp: i32, + ) -> TestResult { + // generating xs and prices from i32 to limit the range to reasonable values and guard + // against overflow/bespoke constraint setting for quickcheck + let y1 = construct_quickcheck_affine_combination_price(i64::try_from(p1).ok().unwrap()); + let y2 = construct_quickcheck_affine_combination_price(i64::try_from(p2).ok().unwrap()); + + let x1 = i64::try_from(x1_inp).ok().unwrap(); + let x2 = i64::try_from(x2_inp).ok().unwrap(); + let x_query = i64::try_from(x_query_inp).ok().unwrap(); + + // stick with single expo for ease of testing and generation + let pre_add_expo = -9; + + // require x2 > x1, as needed for affine_combination + if x1 >= x2 { + return TestResult::discard(); + } + + // original result + let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo).unwrap(); + + let y1_norm = y1.normalize().unwrap(); + let y2_norm = y2.normalize().unwrap(); + + // result with normalized price inputs + let result_norm = + Price::affine_combination(x1, y1_norm, x2, y2_norm, x_query, pre_add_expo).unwrap(); + + // results should match exactly + TestResult::from_bool(result_norm == result_orig) + } + + // quickcheck to confirm affine_combination introduces bounded error if close fraction x/y + // passed in first this quickcheck calls affine_combination with two sets of similar inputs: + // the first set has xs generated by the quickcheck generation process, leading to + // potentially inexact fractions that don't fit within 8 digits of precision the second + // set "normalizes" down to 8 digits of precision by setting x1 to 0, x2 to 100_000_000, + // and xquery proportionally based on the bounds described in the docstring of + // affine_combination, we expect error due to this to be leq 4*10^-7 + #[quickcheck] + fn quickcheck_affine_combination_normalize_fractions( + x1_inp: i32, + p1: i32, + x2_inp: i32, + p2: i32, + x_query_inp: i32, + ) -> TestResult { + // generating xs and prices from i32 to limit the range to reasonable values and guard + // against overflow/bespoke constraint setting for quickcheck + let y1 = construct_quickcheck_affine_combination_price(i64::try_from(p1).ok().unwrap()); + let y2 = construct_quickcheck_affine_combination_price(i64::try_from(p2).ok().unwrap()); + + let x1 = i64::try_from(x1_inp).ok().unwrap(); + let x2 = i64::try_from(x2_inp).ok().unwrap(); + let x_query = i64::try_from(x_query_inp).ok().unwrap(); + + // stick with single expo for ease of testing and generation + let pre_add_expo = -9; + + // require x2 > x1, as needed for affine_combination + if x1 >= x2 { + return TestResult::discard(); + } + + // constrain x_query to be within 5 interval lengths of x1 or x2 + if (x_query > x2 + 5 * (x2 - x1)) || (x_query < x1 - 5 * (x2 - x1)) { + return TestResult::discard(); + } + + // generate new xs based on scaling x_1 --> 0, x_2 --> 10^8 + let x1_new: i64; + let xq_new: i64; + let x2_new: i64; + + if x2 == 0 { + x1_new = x1; + xq_new = x_query; + x2_new = x2; + } else { + let mut frac_q2 = Price::fraction(x_query - x1, x2 - x1).unwrap(); + frac_q2 = frac_q2.scale_to_exponent(-8).unwrap(); + + x1_new = 0; + xq_new = frac_q2.price; + x2_new = 100_000_000 as i64; + } + + // original result + let result_orig = Price::affine_combination(x1, y1, x2, y2, x_query, pre_add_expo) + .unwrap() + .scale_to_exponent(-7) + .unwrap(); + + // xs "normalized" result + let result_norm = Price::affine_combination(x1_new, y1, x2_new, y2, xq_new, pre_add_expo) + .unwrap() + .scale_to_exponent(-7) + .unwrap(); + + // compute difference in prices + let price_diff = result_norm.add(&result_orig.cmul(-1, 0).unwrap()).unwrap(); + + // results should differ by less than 4*10^-7 + TestResult::from_bool((price_diff.price < 4) && (price_diff.price > -4)) + } #[test] fn test_fraction() { From 1a17c6618ed5548c61ca67576cbda6ae4fb3ec58 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Thu, 2 Feb 2023 17:27:36 -0500 Subject: [PATCH 19/19] try g's rust toolchain fix to CI rust issues --- pyth-sdk-solana/test-contract/rust-toolchain | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 pyth-sdk-solana/test-contract/rust-toolchain diff --git a/pyth-sdk-solana/test-contract/rust-toolchain b/pyth-sdk-solana/test-contract/rust-toolchain new file mode 100644 index 0000000..ffa7dbf --- /dev/null +++ b/pyth-sdk-solana/test-contract/rust-toolchain @@ -0,0 +1,5 @@ + +# This is only used for tests +[toolchain] +channel = "1.66.1" +profile = "minimal"