diff --git a/Cargo.lock b/Cargo.lock index 8edb66be..46a4485f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2509,6 +2509,16 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "price" +version = "0.1.0" +dependencies = [ + "pinocchio", + "static_assertions", + "strum", + "strum_macros", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" diff --git a/Cargo.toml b/Cargo.toml index 0f2a4f4d..d0056d87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "instruction-macros/crates/test-fixtures", "interface", "grpc-stream", + "price", "program", "transaction-parser", ] diff --git a/instruction-macros/crates/instruction-macros-impl/src/render/try_from_tag_macro.rs b/instruction-macros/crates/instruction-macros-impl/src/render/try_from_tag_macro.rs index 04338232..ec125601 100644 --- a/instruction-macros/crates/instruction-macros-impl/src/render/try_from_tag_macro.rs +++ b/instruction-macros/crates/instruction-macros-impl/src/render/try_from_tag_macro.rs @@ -59,7 +59,7 @@ use crate::parse::{ /// // Use it to implement `TryFrom`: /// impl TryFrom for MyInstruction { /// type Error = ProgramError; -/// +/// /// #[inline(always)] /// fn try_from(tag: u8) -> Result { /// MyInstruction_try_from_tag!(tag, ProgramError::InvalidInstructionData) diff --git a/instruction-macros/crates/test-fixtures/src/client.rs b/instruction-macros/crates/test-fixtures/src/client.rs index fc5f459a..058f389d 100644 --- a/instruction-macros/crates/test-fixtures/src/client.rs +++ b/instruction-macros/crates/test-fixtures/src/client.rs @@ -19,7 +19,7 @@ mod test { #[account(9, name = "quote_token_program", desc = "The quote mint's token program.")] #[args(sector_index_hint: u32, "A hint indicating which sector the user's seat resides in.")] CloseSeat, - + #[account(0, signer, name = "user", desc = "The user depositing or registering their seat.")] #[account(1, writable, name = "market_account", desc = "The market account PDA.")] #[account(2, writable, name = "user_ata", desc = "The user's associated token account.")] @@ -29,10 +29,10 @@ mod test { #[args(amount: u64, "The amount to deposit.")] #[args(sector_index_hint: u32, "A hint indicating which sector the user's seat resides in (pass `NIL` when registering a new seat).")] Deposit, - + #[account(0, signer, name = "event_authority", desc = "Flush events.")] FlushEvents, - + Batch, } } diff --git a/interface/src/events/mod.rs b/interface/src/events/mod.rs index acd28c17..c33a9352 100644 --- a/interface/src/events/mod.rs +++ b/interface/src/events/mod.rs @@ -39,7 +39,7 @@ pub enum DropsetEventTag { #[args(seat_sector_index: u32, "The user's (possibly newly registered) market seat sector index.")] DepositEvent, #[args(amount: u64, "The amount withdrawn.")] - #[args(is_base: bool, "Which token, i.e., `true` => base token, `false` => quote token.")] + #[args(is_base: bool, "Which token, i.e., `true` => base token, `false` => quote token.")] WithdrawEvent, #[args(market: [u8; 32], "The newly registered market.")] RegisterMarketEvent, diff --git a/price/Cargo.toml b/price/Cargo.toml new file mode 100644 index 00000000..7742a432 --- /dev/null +++ b/price/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "price" +version.workspace = true +edition.workspace = true + +[dependencies] +static_assertions.workspace = true +pinocchio.workspace = true + +[dev-dependencies] +strum.workspace = true +strum_macros.workspace = true + +[lints] +workspace = true diff --git a/price/src/decoded_price.rs b/price/src/decoded_price.rs new file mode 100644 index 00000000..b6dc7c32 --- /dev/null +++ b/price/src/decoded_price.rs @@ -0,0 +1,79 @@ +use crate::{ + EncodedPrice, + OrderInfoError, + ValidatedPriceMantissa, + BIAS, + ENCODED_PRICE_INFINITY, + ENCODED_PRICE_ZERO, + PRICE_MANTISSA_BITS, + PRICE_MANTISSA_MASK, +}; + +/// An enum representing a decoded `EncodedPrice`. +#[derive(Clone)] +#[cfg_attr(test, derive(Debug))] +pub enum DecodedPrice { + Zero, + Infinity, + ExponentAndMantissa { + price_exponent_biased: u8, + price_mantissa: ValidatedPriceMantissa, + }, +} + +impl DecodedPrice { + /// Return the optional tuple of exponent and mantissa from a decoded price. + /// If the decoded price is not a [`DecodedPrice::ExponentAndMantissa`], this returns `None`. + pub fn as_exponent_and_mantissa(&self) -> Option<(&u8, &ValidatedPriceMantissa)> { + if let DecodedPrice::ExponentAndMantissa { + price_exponent_biased, + price_mantissa, + } = self + { + Some((price_exponent_biased, price_mantissa)) + } else { + None + } + } +} + +impl TryFrom for DecodedPrice { + type Error = OrderInfoError; + + fn try_from(encoded: EncodedPrice) -> Result { + let res = match encoded.get() { + ENCODED_PRICE_ZERO => Self::Zero, + ENCODED_PRICE_INFINITY => Self::Infinity, + value => { + let price_exponent_biased = (value >> PRICE_MANTISSA_BITS) as u8; + let validated_mantissa = value & PRICE_MANTISSA_MASK; + + Self::ExponentAndMantissa { + price_exponent_biased, + price_mantissa: ValidatedPriceMantissa::try_from(validated_mantissa)?, + } + } + }; + + Ok(res) + } +} + +impl TryFrom for f64 { + type Error = OrderInfoError; + + fn try_from(decoded: DecodedPrice) -> Result { + match decoded { + DecodedPrice::Zero => Ok(0f64), + DecodedPrice::Infinity => Err(OrderInfoError::InfinityIsNotAFloat), + DecodedPrice::ExponentAndMantissa { + price_exponent_biased, + price_mantissa, + } => { + let res = (price_mantissa.get() as f64) + * 10f64.powi(price_exponent_biased as i32 - BIAS as i32); + Ok(res) + } + } + } +} diff --git a/price/src/encoded_price.rs b/price/src/encoded_price.rs new file mode 100644 index 00000000..81abb3df --- /dev/null +++ b/price/src/encoded_price.rs @@ -0,0 +1,102 @@ +use crate::{ + ValidatedPriceMantissa, + PRICE_MANTISSA_BITS, +}; + +/// The encoded price as a u32. +/// +/// If `N` = the number of exponent bits and `M` = the number of price mantissa bits, the u32 bit +/// layout is: +/// +/// ```text +/// N M +/// |-------------------|-------------------| +/// [ exponent_bits ] | [ mantissa_bits ] +/// |---------------------------------------| +/// 32 +/// ``` +#[repr(transparent)] +#[derive(Copy, Clone, Debug)] +pub struct EncodedPrice(u32); + +pub const ENCODED_PRICE_INFINITY: u32 = u32::MAX; +pub const ENCODED_PRICE_ZERO: u32 = 0; + +impl EncodedPrice { + /// Creates a new [`EncodedPrice`] from a biased price exponent and a validated price mantissa. + #[inline(always)] + pub fn new(price_exponent_biased: u8, price_mantissa: ValidatedPriceMantissa) -> Self { + // The biased price exponent doesn't need to be checked because a leftwards bitshift will + // always discard irrelevant bits. + let exponent_bits = (price_exponent_biased as u32) << PRICE_MANTISSA_BITS; + + // No need to mask the price mantissa since it has already been range checked/validated. + // Thus it's guaranteed it will only occupy the lower M bits where M = PRICE_MANTISSA_BITS. + Self(exponent_bits | price_mantissa.get()) + } + + /// Returns the inner encoded price as a u32. + #[inline(always)] + pub fn get(&self) -> u32 { + self.0 + } + + /// The encoded price representation of a market buy/taker order with no constraints on the + /// maximum filled ask price. + #[inline(always)] + pub const fn infinity() -> Self { + Self(ENCODED_PRICE_INFINITY) + } + + #[inline(always)] + pub fn is_infinity(&self) -> bool { + self.0 == ENCODED_PRICE_INFINITY + } + + /// The encoded price representation of a market sell/taker order with no constraints on the + /// minimum filled bid price. + #[inline(always)] + pub const fn zero() -> Self { + Self(ENCODED_PRICE_ZERO) + } + + #[inline(always)] + pub fn is_zero(&self) -> bool { + self.0 == ENCODED_PRICE_ZERO + } +} + +#[cfg(test)] +mod tests { + use crate::{ + to_biased_exponent, + EncodedPrice, + ValidatedPriceMantissa, + BIAS, + PRICE_MANTISSA_BITS, + PRICE_MANTISSA_MASK, + }; + + #[test] + fn encoded_price_mantissa_bits() { + let exponent = 0b0_1111; + let price_mantissa = 0b000_1111_0000_1111_0000_1111_0000; + let encoded_price = EncodedPrice::new( + to_biased_exponent!(exponent), + ValidatedPriceMantissa::try_from(price_mantissa).unwrap(), + ); + assert_eq!( + encoded_price.0 >> PRICE_MANTISSA_BITS, + (exponent + BIAS) as u32 + ); + assert_eq!(encoded_price.0 & PRICE_MANTISSA_MASK, price_mantissa); + } + + #[test] + fn test_infinity() { + assert_eq!(EncodedPrice::infinity().0, u32::MAX); + assert_eq!(EncodedPrice::zero().0, 0); + assert!(EncodedPrice::infinity().is_infinity()); + assert!(EncodedPrice::zero().is_zero()); + } +} diff --git a/price/src/error.rs b/price/src/error.rs new file mode 100644 index 00000000..d4a7e03a --- /dev/null +++ b/price/src/error.rs @@ -0,0 +1,10 @@ +#[repr(u8)] +#[derive(Debug)] +#[cfg_attr(test, derive(strum_macros::Display))] +pub enum OrderInfoError { + ExponentUnderflow, + ArithmeticOverflow, + InvalidPriceMantissa, + InvalidBiasedExponent, + InfinityIsNotAFloat, +} diff --git a/price/src/lib.rs b/price/src/lib.rs new file mode 100644 index 00000000..ce09537b --- /dev/null +++ b/price/src/lib.rs @@ -0,0 +1,337 @@ +mod decoded_price; +mod encoded_price; +mod error; +mod macros; +mod validated_mantissa; + +pub use decoded_price::*; +pub use encoded_price::*; +pub use error::*; +pub use validated_mantissa::*; + +pub const MANTISSA_DIGITS_LOWER_BOUND: u32 = 10_000_000; +pub const MANTISSA_DIGITS_UPPER_BOUND: u32 = 99_999_999; + +const U32_BITS: u8 = 32; +const PRICE_MANTISSA_BITS: u8 = 27; + +/// The number of exponent bits is simply the remaining bits in a u32 after storing the price +/// mantissa bits. +#[allow(dead_code)] +const EXPONENT_BITS: u8 = U32_BITS - PRICE_MANTISSA_BITS; + +/// The max biased exponent. This also determines the range of valid exponents. +/// I.e., 0 <= biased_exponent <= [`MAX_BIASED_EXPONENT`]. +#[allow(dead_code)] +const MAX_BIASED_EXPONENT: u8 = (1 << (EXPONENT_BITS)) - 1; + +/// [`BIAS`] is the number that satisfies: `BIAS + SMALLEST_POSSIBLE_EXPONENT == 0`. +/// It facilitates the expression of negative exponents with only unsigned integers. +/// +/// The exponent range is 32 values from -16 <= n <= 15 and the smallest possible exponent +/// is -16, so the BIAS must be 16. +/// +/// See [`pow10_u64`] for more information on the reasoning behind the exponent range. +pub const BIAS: u8 = 16; + +/// The minimum unbiased exponent value. +#[cfg(test)] +const UNBIASED_MIN: i16 = 0 - BIAS as i16; +/// The maximum unbiased exponent value. +#[cfg(test)] +const UNBIASED_MAX: i16 = MAX_BIASED_EXPONENT as i16 - BIAS as i16; + +// Ensure that adding the bias to the max biased exponent never overflows. +static_assertions::const_assert!((MAX_BIASED_EXPONENT as u16) + (BIAS as u16) <= (u8::MAX as u16)); + +/// The bitmask for the price mantissa calculated from the number of bits it uses. +pub const PRICE_MANTISSA_MASK: u32 = u32::MAX >> ((U32_BITS - PRICE_MANTISSA_BITS) as usize); + +#[cfg(debug_assertions)] +mod debug_assertions { + use static_assertions::*; + + use super::*; + + // The max price mantissa representable with `PRICE_MANTISSA_BITS` should exceed the upper bound + // used to ensure a fixed number of digits for the price mantissa. + const_assert!(MANTISSA_DIGITS_UPPER_BOUND < PRICE_MANTISSA_MASK); + + /// The bitmask for the price exponent calculated from the number of bits in the price mantissa. + #[allow(dead_code)] + pub const PRICE_EXPONENT_MASK: u32 = u32::MAX << (PRICE_MANTISSA_BITS as usize); + + // XOR'ing the price exponent and mantissa bit masks should result in a u32 with all 1 bits, + // aka u32::MAX. + const_assert_eq!(PRICE_EXPONENT_MASK ^ PRICE_MANTISSA_MASK, u32::MAX); +} + +/// The fixed struct layout for information about a `dropset` order. +/// +/// This struct is a C-style struct to facilitate a predictable, fixed layout for on-chain function +/// calls related to `dropset` orders. +#[repr(C)] +#[cfg_attr(test, derive(Debug))] +pub struct OrderInfo { + /// The encoded price, containing an exponent and price mantissa. + /// See [`EncodedPrice`] for more details. + pub encoded_price: EncodedPrice, + /// The indivisible units (aka atoms) of base token. + pub base_atoms: u64, + /// The indivisible units (aka atoms) of quote token. + pub quote_atoms: u64, +} + +/// # Safety note: +/// +/// In the rebiased exponent calculation, there is an unchecked add that is actually safe. It's +/// safe because the prior function body ensures the quote exponent is <= MAX_BIASED_EXPONENT, and +/// const assertions ensure that `MAX_BIASED_EXPONENT + BIAS` is always <= `u8::MAX`. +/// +/// [`tests::ensure_invalid_quote_exponent_fails_early`] ensures that the function fails early if +/// the quote exponent isn't <= MAX_BIASED_EXPONENT prior to the unchecked add. +pub fn to_order_info( + price_mantissa: u32, + base_scalar: u64, + base_exponent_biased: u8, + quote_exponent_biased: u8, +) -> Result { + let validated_mantissa = ValidatedPriceMantissa::try_from(price_mantissa)?; + + let base_atoms = pow10_u64!(base_scalar, base_exponent_biased)?; + + let price_mantissa_times_base_scalar = checked_mul!( + validated_mantissa.get() as u64, + base_scalar, + OrderInfoError::ArithmeticOverflow + )?; + + let quote_atoms = pow10_u64!(price_mantissa_times_base_scalar, quote_exponent_biased)?; + + // Ultimately, the price mantissa is multiplied by: + // 10 ^ (quote_exponent_biased - base_exponent_biased) + // aka 10 ^ (q - b) + // which means q - b may be negative and must be re-biased. + // + // Exponent underflow only occurs here if: + // `quote_exponent_biased + BIAS < base_exponent_biased`. + let price_exponent_rebiased = checked_sub!( + // Safety: See the function documentation. + unsafe { quote_exponent_biased.unchecked_add(BIAS) }, + base_exponent_biased, + OrderInfoError::ExponentUnderflow + )?; + + Ok(OrderInfo { + encoded_price: EncodedPrice::new(price_exponent_rebiased, validated_mantissa), + base_atoms, + quote_atoms, + }) +} + +#[cfg(test)] +mod tests { + use std::ops::Mul; + + use static_assertions::*; + + use super::*; + + #[test] + fn happy_path_simple_price() { + let base_biased_exponent = to_biased_exponent!(0); + let quote_biased_exponent = to_biased_exponent!(-4); + let order = to_order_info(12_340_000, 1, base_biased_exponent, quote_biased_exponent) + .expect("Should calculate price"); + assert_eq!(order.base_atoms, 1); + assert_eq!(order.quote_atoms, 1234); + + let decoded_price: f64 = DecodedPrice::try_from(order.encoded_price) + .expect("Should decode") + .try_into() + .expect("Should be a valid f64"); + assert_eq!(decoded_price, "1234".parse().unwrap()); + } + + #[test] + fn price_with_max_sig_digits() { + let order = to_order_info(12345678, 1, to_biased_exponent!(0), to_biased_exponent!(0)) + .expect("Should calculate price"); + assert_eq!(order.base_atoms, 1); + assert_eq!(order.quote_atoms, 12345678); + + let decoded_price: f64 = DecodedPrice::try_from(order.encoded_price) + .expect("Should decode") + .try_into() + .expect("Should be a valid f64"); + assert_eq!(decoded_price, "12345678".parse().unwrap()); + } + + #[test] + fn decimal_price() { + let mantissa = 12345678; + let order = to_order_info(mantissa, 1, to_biased_exponent!(8), to_biased_exponent!(0)) + .expect("Should calculate price"); + assert_eq!(order.quote_atoms, 12345678); + assert_eq!(order.base_atoms, 100000000); + + let decoded_price = DecodedPrice::try_from(order.encoded_price).expect("Should decode"); + + let (decoded_exponent, decoded_mantissa) = decoded_price + .as_exponent_and_mantissa() + .expect("Should be exponent + mantissa"); + let decoded_f64: f64 = decoded_price + .clone() + .try_into() + .expect("Should be a valid f64"); + assert_eq!(decoded_mantissa.get(), mantissa); + assert_eq!(decoded_f64, "0.12345678".parse().unwrap()); + assert_eq!( + (decoded_mantissa.get() as f64).mul(10f64.powi(*decoded_exponent as i32 - BIAS as i32)), + decoded_f64 + ); + } + + #[test] + fn bias_ranges() { + const_assert_eq!(16, BIAS); + + let val_156_e_neg_16: u64 = 1_560_000_000_000_000_000; + let calculated = val_156_e_neg_16 / 10u64.pow(BIAS as u32); + let expected = 156; + assert_eq!( + pow10_u64!(val_156_e_neg_16, 0).expect("0 is a valid biased exponent"), + calculated, + ); + assert_eq!(calculated, expected); + + let val: u64 = 156; + let max_exponent = MAX_BIASED_EXPONENT as u32; + let calculated = val + * 10u64 + .checked_pow(max_exponent - BIAS as u32) + .expect("Shouldn't overflow"); + let expected: u64 = 156_000_000_000_000_000; + assert_eq!( + pow10_u64!(val, max_exponent).expect("Exponent should be valid"), + calculated + ); + assert_eq!(calculated, expected); + } + + #[test] + fn ensure_price_mantissa_times_base_scalar_arithmetic_overflow() { + const PRICE_MANTISSA: u32 = 10_000_000; + + assert!(to_order_info( + PRICE_MANTISSA, + u64::MAX / PRICE_MANTISSA as u64, + to_biased_exponent!(0), + to_biased_exponent!(0), + ) + .is_ok()); + + assert!(matches!( + to_order_info( + PRICE_MANTISSA + 1, + u64::MAX / PRICE_MANTISSA as u64, + to_biased_exponent!(0), + to_biased_exponent!(0) + ), + Err(OrderInfoError::ArithmeticOverflow) + )); + } + + #[test] + fn ensure_exponent_underflow() { + let price_mantissa = 10_000_000; + let base_scalar = 1; + + assert!(to_order_info(price_mantissa, base_scalar, BIAS, 0).is_ok()); + + assert!(matches!( + to_order_info(price_mantissa, base_scalar, BIAS + 1, 0), + Err(OrderInfoError::ExponentUnderflow) + )); + } + + #[test] + pub(crate) fn ensure_invalid_quote_exponent_fails_early() { + let e_base = to_biased_exponent!(0); + let e_quote = MAX_BIASED_EXPONENT + 1; + + // Ensure the base exponent is valid so that it can't be the trigger for the error. + let _one_to_the_base_exponent = pow10_u64!(1u64, e_base).unwrap(); + + let all_good = to_order_info(10_000_000, 1, e_base, e_base); + let arithmetic_overflow = to_order_info(10_000_000, 1, e_base, e_quote - 1); + let invalid_biased_exponent = to_order_info(10_000_000, 1, e_base, e_quote); + + assert!(all_good.is_ok()); + #[rustfmt::skip] + assert!(matches!(arithmetic_overflow, Err(OrderInfoError::ArithmeticOverflow))); + #[rustfmt::skip] + assert!(matches!(invalid_biased_exponent, Err(OrderInfoError::InvalidBiasedExponent))); + } + + #[test] + fn max_and_max_plus_one_base() { + let e_base = MAX_BIASED_EXPONENT; + let e_quote = to_biased_exponent!(0); + + // Ensure the quote exponent is valid so that it can't be the trigger for the error. + let _one_to_the_quote_exponent = pow10_u64!(1u64, e_quote).unwrap(); + + let all_good = to_order_info(10_000_000, 1, e_base, e_quote); + let invalid_quote_exponent = to_order_info(10_000_000, 1, e_base + 1, e_quote); + + assert!(all_good.is_ok()); + assert!(matches!( + invalid_quote_exponent, + Err(OrderInfoError::InvalidBiasedExponent) + )); + } + + #[test] + fn quote_atoms_overflow() { + let mantissa: u32 = 10_000_000; + let base_scalar: u64 = 1; + + let quote_exponent = 12; + assert!((mantissa as u64).checked_mul(base_scalar).is_some()); + + // No overflow with quote exponent using core rust operations. + assert!((mantissa as u64) + .checked_mul(base_scalar) + .unwrap() + .checked_mul(10u64.checked_pow(quote_exponent as u32).unwrap()) + .is_some()); + + // Overflow with quote exponent + 1 using core rust operations. + assert!((mantissa as u64) + .checked_mul(base_scalar) + .unwrap() + .checked_mul(10u64.checked_pow((quote_exponent + 1) as u32).unwrap()) + .is_none()); + + // No overflow with quote exponent in `to_order_info`. + assert!(to_order_info( + mantissa, + base_scalar, + to_biased_exponent!(0), + to_biased_exponent!(quote_exponent) + ) + .is_ok()); + + // Overflow with quote exponent + 1 in `to_order_info`. + assert!(matches!( + to_order_info( + mantissa, + base_scalar, + to_biased_exponent!(0), + to_biased_exponent!(quote_exponent + 1) + ), + Err(OrderInfoError::ArithmeticOverflow) + )); + } +} diff --git a/price/src/macros.rs b/price/src/macros.rs new file mode 100644 index 00000000..5503b191 --- /dev/null +++ b/price/src/macros.rs @@ -0,0 +1,232 @@ +use static_assertions::const_assert_eq; + +// Static assertions for macro invariants. +static_assertions::const_assert_eq!(crate::BIAS - 16, 0); +static_assertions::const_assert_eq!(crate::MAX_BIASED_EXPONENT, 31); + +/// Documentation for [`pow10_u64`] relies on [`crate::BIAS`] == 16. If that changes, +/// [`crate::BIAS`] and the [`pow10_u64`] documentation needs to be updated. +const _: () = { + const_assert_eq!(crate::BIAS, 16); +}; + +/// Performs base-10 exponentiation on a value using a biased exponent. +/// +/// This facilitates representing negative exponent values with unsigned integers by ensuring the +/// biased exponent is never negative. The unbiased exponent is therefore the real exponent value. +/// +/// # Parameters +/// - `$value`: The `u64` to be scaled by a power of 10. +/// - `$biased_exponent`: A biased exponent in the range `0..=31`. +/// +/// # Biased Exponent Concept +/// The actual (aka unbiased) exponent is: +/// +/// `exponent = $biased_exponent - price::BIAS` +/// +/// With the current `BIAS = 16`, this means: +/// - `0` → exponent `-16` (division by 10^16) +/// - `16` → exponent `0` (multiplication by 1 aka 10^0) +/// - `31` → exponent `+15` (multiplication by 10^15) +/// +/// The code output from the macro will error on an invalid biased exponent or arithmetic overflow. +/// +/// # Reasoning behind exponent range +/// +/// The decision to use a larger negative range instead of a larger positive range is because +/// a larger negative range results in the price mantissa * exponent product forming in a tighter +/// range around `1`. +/// +/// For example, with `[-2, 1] vs [-1, 2]`: +/// +/// ```markdown +/// # With [-2, 1] as the smallest/largest exponents +/// | | Smallest exponent | Largest exponent | +/// | -------------------- | ------------------- | ------------------- | +/// | Smallest mantissa | 1.00 * 10^-2 = 0.01 | 1.00 * 10^1 = 10 | +/// | Largest mantissa | 9.99 * 10^-2 = ~0.1 | 9.99 * 10^1 = ~100 | +/// | -------------------- | ------------------- | ------------------- | +/// ``` +/// +/// Both the smallest and largest products (0.01 and 100) are 2 orders +/// of magnitude below/above `1`. +/// +/// ```markdown +/// # With [-1, 2] as the smallest/largest exponents +/// | | Smallest exponent | Largest exponent | +/// | -------------------- | ------------------ | -------------------- | +/// | Smallest mantissa | 1.00 * 10^-1 = 0.1 | 1.00 * 10^2 = 100 | +/// | Largest mantissa | 9.99 * 10^-1 = ~1 | 9.99 * 10^2 = ~1000 | +/// | -------------------- | ------------------ | -------------------- | +/// ``` +/// +/// The lower product (0.1) is 1 order of magnitude below 1 and the higher +/// product (1000) is 3 orders of magnitude above 1. +/// +/// The first option is preferable because it offers a more dynamic, +/// symmetrical range in terms of orders of magnitude below/above 1. +/// +/// Therefore, [-16, 15] is used as the exponent range instead of [-15, 16]. +/// +#[macro_export] +#[rustfmt::skip] +macro_rules! pow10_u64 { + ($value:expr, $biased_exponent:expr) => {{ + match $biased_exponent { + /* BIAS - 16 */ 0 => Ok($value / 10000000000000000u64), + /* BIAS - 15 */ 1 => Ok($value / 1000000000000000), + /* BIAS - 14 */ 2 => Ok($value / 100000000000000), + /* BIAS - 13 */ 3 => Ok($value / 10000000000000), + /* BIAS - 12 */ 4 => Ok($value / 1000000000000), + /* BIAS - 11 */ 5 => Ok($value / 100000000000), + /* BIAS - 10 */ 6 => Ok($value / 10000000000), + /* BIAS - 9 */ 7 => Ok($value / 1000000000), + /* BIAS - 8 */ 8 => Ok($value / 100000000), + /* BIAS - 7 */ 9 => Ok($value / 10000000), + /* BIAS - 6 */ 10 => Ok($value / 1000000), + /* BIAS - 5 */ 11 => Ok($value / 100000), + /* BIAS - 4 */ 12 => Ok($value / 10000), + /* BIAS - 3 */ 13 => Ok($value / 1000), + /* BIAS - 2 */ 14 => Ok($value / 100), + /* BIAS - 1 */ 15 => Ok($value / 10), + /* BIAS - 0 */ 16 => Ok($value), + /* BIAS + 1 */ 17 => checked_mul!($value, 10, OrderInfoError::ArithmeticOverflow), + /* BIAS + 2 */ 18 => checked_mul!($value, 100, OrderInfoError::ArithmeticOverflow), + /* BIAS + 3 */ 19 => checked_mul!($value, 1000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 4 */ 20 => checked_mul!($value, 10000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 5 */ 21 => checked_mul!($value, 100000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 6 */ 22 => checked_mul!($value, 1000000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 7 */ 23 => checked_mul!($value, 10000000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 8 */ 24 => checked_mul!($value, 100000000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 9 */ 25 => checked_mul!($value, 1000000000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 10 */ 26 => checked_mul!($value, 10000000000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 11 */ 27 => checked_mul!($value, 100000000000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 12 */ 28 => checked_mul!($value, 1000000000000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 13 */ 29 => checked_mul!($value, 10000000000000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 14 */ 30 => checked_mul!($value, 100000000000000, OrderInfoError::ArithmeticOverflow), + /* BIAS + 15 */ 31 => checked_mul!($value, 1000000000000000, OrderInfoError::ArithmeticOverflow), + _ => Err(OrderInfoError::InvalidBiasedExponent), + } + }}; +} + +/// A checked subtraction with a custom error return value and the error path marked as cold. +/// +/// *NOTE: This is only intended for usage with **unsigned** integer types.* +/// +/// # Example +/// ```rust +/// enum MyError { BadSub } +/// +/// let res: Result = price::checked_sub!(5, 4, MyError::BadSub); +/// assert!(matches!(res, Ok(1))); +/// +/// let res: Result = price::checked_sub!(5, 6, MyError::BadSub); +/// assert!(matches!(res, Err(MyError::BadSub))); +/// ``` +#[macro_export] +macro_rules! checked_sub { + ($lhs:expr, $rhs:expr, $err:expr $(,)?) => {{ + let lhs = $lhs; + let rhs = $rhs; + if lhs >= rhs { + Ok(lhs - rhs) + } else { + ::pinocchio::hint::cold_path(); + Err($err) + } + }}; +} + +/// A checked multiplication with a custom error return value and the error path marked as cold. +/// +/// *NOTE: This is only intended for usage with **unsigned** integer types.* +/// +/// # Example +/// ```rust +/// enum MyError { BadMul } +/// +/// let res: Result = price::checked_mul!(255u8, 1, MyError::BadMul); +/// assert!(matches!(res, Ok(255))); +/// +/// let res: Result = price::checked_mul!(255u8, 2, MyError::BadMul); +/// assert!(matches!(res, Err(MyError::BadMul))); +/// ``` +#[macro_export] +macro_rules! checked_mul { + ($lhs:expr, $rhs:expr, $err:expr $(,)?) => {{ + match $lhs.checked_mul($rhs) { + Some(val) => Ok(val), + None => { + ::pinocchio::hint::cold_path(); + Err($err) + } + } + }}; +} + +/// Test utility macro for converting unbiased exponents to biased exponents. +#[cfg(test)] +#[macro_export] +macro_rules! to_biased_exponent { + ($unbiased_exponent:expr) => {{ + let unbiased_signed = $unbiased_exponent as i16; + match unbiased_signed { + $crate::UNBIASED_MIN..=$crate::UNBIASED_MAX => { + (unbiased_signed + $crate::BIAS as i16) as u8 + } + _ => panic!("Invalid unbiased exponent."), + } + }}; +} + +#[cfg(test)] +mod tests { + use crate::{ + OrderInfoError, + BIAS, + MAX_BIASED_EXPONENT, + UNBIASED_MAX, + UNBIASED_MIN, + }; + + #[test] + fn check_max_biased_exponent() { + // The max biased exponent should be valid. + assert_eq!( + pow10_u64!(2u64, MAX_BIASED_EXPONENT).unwrap(), + 2 * 10u64 + .checked_pow(MAX_BIASED_EXPONENT as u32 - BIAS as u32) + .unwrap() + ); + // One past the max biased exponent should result in an error. + assert!(matches!( + pow10_u64!(2u64, MAX_BIASED_EXPONENT + 1), + Err(OrderInfoError::InvalidBiasedExponent) + )); + } + + #[test] + fn unbiased_exponent_happy_paths() { + let expected_min = (UNBIASED_MIN + BIAS as i16) as u8; + assert_eq!(to_biased_exponent!(UNBIASED_MIN), expected_min); + + let expected_mid = BIAS; + assert_eq!(to_biased_exponent!(0), expected_mid); + + let expected_max = (UNBIASED_MAX + BIAS as i16) as u8; + assert_eq!(to_biased_exponent!(UNBIASED_MAX), expected_max); + } + + #[test] + #[should_panic(expected = "Invalid unbiased exponent.")] + fn below_minimum_unbiased() { + to_biased_exponent!(UNBIASED_MIN - 1); + } + + #[test] + #[should_panic(expected = "Invalid unbiased exponent.")] + fn above_maximum_unbiased() { + to_biased_exponent!(UNBIASED_MAX + 1); + } +} diff --git a/price/src/validated_mantissa.rs b/price/src/validated_mantissa.rs new file mode 100644 index 00000000..8f6f04fa --- /dev/null +++ b/price/src/validated_mantissa.rs @@ -0,0 +1,64 @@ +use pinocchio::hint; + +use crate::{ + OrderInfoError, + MANTISSA_DIGITS_LOWER_BOUND, + MANTISSA_DIGITS_UPPER_BOUND, +}; + +#[derive(Clone, Copy)] +#[cfg_attr(test, derive(Debug))] +pub struct ValidatedPriceMantissa(u32); + +impl TryFrom for ValidatedPriceMantissa { + type Error = OrderInfoError; + + #[inline(always)] + fn try_from(price_mantissa: u32) -> Result { + if (MANTISSA_DIGITS_LOWER_BOUND..=MANTISSA_DIGITS_UPPER_BOUND).contains(&price_mantissa) { + Ok(Self(price_mantissa)) + } else { + hint::cold_path(); + Err(OrderInfoError::InvalidPriceMantissa) + } + } +} + +impl ValidatedPriceMantissa { + /// Returns the validated price mantissa as a u32. + #[inline(always)] + pub fn get(&self) -> u32 { + self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn valid_mantissas() { + for mantissa in [ + MANTISSA_DIGITS_LOWER_BOUND, + MANTISSA_DIGITS_LOWER_BOUND + 1, + MANTISSA_DIGITS_UPPER_BOUND, + MANTISSA_DIGITS_UPPER_BOUND - 1, + ] { + let validated_mantissa = ValidatedPriceMantissa::try_from(mantissa); + assert!(validated_mantissa.is_ok()); + assert_eq!(validated_mantissa.unwrap().0, mantissa); + } + } + + #[test] + fn invalid_mantissas() { + assert!(matches!( + ValidatedPriceMantissa::try_from(MANTISSA_DIGITS_LOWER_BOUND - 1), + Err(OrderInfoError::InvalidPriceMantissa) + )); + assert!(matches!( + ValidatedPriceMantissa::try_from(MANTISSA_DIGITS_UPPER_BOUND + 1), + Err(OrderInfoError::InvalidPriceMantissa) + )); + } +}