diff --git a/.gitignore b/.gitignore index 2a14b570..d2e22163 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ # generated by CI init-args.json + +# Scope-of-work +sow.txt diff --git a/README.md b/README.md index b8edd4c0..5580a760 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,12 @@ Templar Protocol is a chain-agnostic overcollateralized lending DeFi protocol. ./script/test.sh ``` +## Generate scope-of-work for auditing + +```bash +./script/sow.sh +``` + ## Links - [Website](https://templarfi.org/) diff --git a/common/src/chunked_append_only_list.rs b/common/src/collection/chunked_append_only_list.rs similarity index 76% rename from common/src/chunked_append_only_list.rs rename to common/src/collection/chunked_append_only_list.rs index 37275c01..50f3e459 100644 --- a/common/src/chunked_append_only_list.rs +++ b/common/src/collection/chunked_append_only_list.rs @@ -1,6 +1,9 @@ use borsh::{BorshDeserialize, BorshSerialize}; use near_sdk::{env, near, store::Vector, BorshStorageKey, IntoStorageKey}; +#[cfg(test)] +mod tests; + #[derive(Debug, Clone, Copy, BorshSerialize, BorshStorageKey, PartialEq, Eq, PartialOrd, Ord)] enum StorageKey { Inner, @@ -156,65 +159,3 @@ impl DoubleEndedIte Some(value) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn basic() { - let mut list = ChunkedAppendOnlyList::<_, 47>::new(b"l"); - assert_eq!(list.len(), 0); - assert!(list.is_empty()); - - for i in 0..10_000usize { - list.push(i); - assert_eq!(list.len() as usize, i + 1); - assert!(!list.is_empty()); - } - - let mut count = 0; - for (i, v) in list.iter().enumerate() { - assert_eq!(i, *v); - count += 1; - } - - assert_eq!(count, 10_000); - } - - #[test] - fn replace_last() { - let mut list = ChunkedAppendOnlyList::<_, 47>::new(b"l"); - for i in 0..10_000u32 { - list.push(i); - list.replace_last(i * 2); - assert_eq!(list.len(), i + 1); - assert!(!list.is_empty()); - } - - for i in 0..10_000u32 { - let x = list.get(i).unwrap(); - assert_eq!(*x, i * 2); - } - - assert_eq!(list.len(), 10_000); - } - - #[test] - fn next_back() { - let mut list = ChunkedAppendOnlyList::<_, 47>::new(b"l"); - for i in 0..10_000u32 { - list.push(i); - } - - let mut it = list.iter(); - - let mut i = 10_000; - while let Some(x) = it.next_back() { - i -= 1; - assert_eq!(*x, i); - } - - assert_eq!(i, 0); - } -} diff --git a/common/src/collection/chunked_append_only_list/tests.rs b/common/src/collection/chunked_append_only_list/tests.rs new file mode 100644 index 00000000..e6ade89f --- /dev/null +++ b/common/src/collection/chunked_append_only_list/tests.rs @@ -0,0 +1,58 @@ +use super::*; + +#[test] +fn basic() { + let mut list = ChunkedAppendOnlyList::<_, 47>::new(b"l"); + assert_eq!(list.len(), 0); + assert!(list.is_empty()); + + for i in 0..10_000usize { + list.push(i); + assert_eq!(list.len() as usize, i + 1); + assert!(!list.is_empty()); + } + + let mut count = 0; + for (i, v) in list.iter().enumerate() { + assert_eq!(i, *v); + count += 1; + } + + assert_eq!(count, 10_000); +} + +#[test] +fn replace_last() { + let mut list = ChunkedAppendOnlyList::<_, 47>::new(b"l"); + for i in 0..10_000u32 { + list.push(i); + list.replace_last(i * 2); + assert_eq!(list.len(), i + 1); + assert!(!list.is_empty()); + } + + for i in 0..10_000u32 { + let x = list.get(i).unwrap(); + assert_eq!(*x, i * 2); + } + + assert_eq!(list.len(), 10_000); +} + +#[test] +fn next_back() { + let mut list = ChunkedAppendOnlyList::<_, 47>::new(b"l"); + for i in 0..10_000u32 { + list.push(i); + } + + let mut it = list.iter(); + + let mut i = 10_000; + while let Some(x) = it.next_back() { + i -= 1; + assert_eq!(*x, i); + } + + assert_eq!(i, 0); +} diff --git a/common/src/collection/mod.rs b/common/src/collection/mod.rs new file mode 100644 index 00000000..53263538 --- /dev/null +++ b/common/src/collection/mod.rs @@ -0,0 +1,2 @@ +pub mod chunked_append_only_list; +pub mod withdrawal_queue; diff --git a/common/src/withdrawal_queue.rs b/common/src/collection/withdrawal_queue.rs similarity index 83% rename from common/src/withdrawal_queue.rs rename to common/src/collection/withdrawal_queue.rs index 9430425f..1dbe7df9 100644 --- a/common/src/withdrawal_queue.rs +++ b/common/src/collection/withdrawal_queue.rs @@ -4,6 +4,9 @@ use near_sdk::{collections::LookupMap, env, near, AccountId, BorshStorageKey, In use crate::{asset::BorrowAssetAmount, asset_op}; +#[cfg(test)] +mod tests; + #[derive(Debug)] #[near(serializers = [borsh])] pub struct QueueNode { @@ -355,61 +358,3 @@ pub mod error { Empty(#[from] EmptyError), } } - -#[cfg(test)] -mod tests { - use near_sdk::AccountId; - - use super::WithdrawalQueue; - - #[test] - fn withdrawal_remove() { - let mut wq = WithdrawalQueue::new(b"w"); - - let alice: AccountId = "alice".parse().unwrap(); - let bob: AccountId = "bob".parse().unwrap(); - let charlie: AccountId = "charlie".parse().unwrap(); - - wq.insert_or_update(&alice, 1.into()); - wq.insert_or_update(&bob, 2.into()); - wq.insert_or_update(&charlie, 3.into()); - assert_eq!(wq.len(), 3); - assert_eq!(wq.remove(&bob), Some(2.into())); - assert_eq!(wq.len(), 2); - assert_eq!(wq.remove(&charlie), Some(3.into())); - assert_eq!(wq.len(), 1); - assert_eq!(wq.remove(&alice), Some(1.into())); - assert_eq!(wq.len(), 0); - } - - #[test] - fn withdrawal_queueing() { - let mut wq = WithdrawalQueue::new(b"w"); - - let alice: AccountId = "alice".parse().unwrap(); - let bob: AccountId = "bob".parse().unwrap(); - let charlie: AccountId = "charlie".parse().unwrap(); - - assert_eq!(wq.len(), 0); - assert_eq!(wq.peek(), None); - wq.insert_or_update(&alice, 1.into()); - assert_eq!(wq.len(), 1); - assert_eq!(wq.peek(), Some((alice.clone(), 1.into()))); - wq.insert_or_update(&alice, 99.into()); - assert_eq!(wq.len(), 1); - assert_eq!(wq.peek(), Some((alice.clone(), 99.into()))); - wq.insert_or_update(&bob, 123.into()); - assert_eq!(wq.len(), 2); - wq.try_lock().unwrap(); - assert_eq!(wq.try_pop(), Some((alice.clone(), 99.into()))); - assert_eq!(wq.len(), 1); - wq.insert_or_update(&charlie, 42.into()); - assert_eq!(wq.len(), 2); - wq.try_lock().unwrap(); - assert_eq!(wq.try_pop(), Some((bob.clone(), 123.into()))); - assert_eq!(wq.len(), 1); - wq.try_lock().unwrap(); - assert_eq!(wq.try_pop(), Some((charlie.clone(), 42.into()))); - assert_eq!(wq.len(), 0); - } -} diff --git a/common/src/collection/withdrawal_queue/tests.rs b/common/src/collection/withdrawal_queue/tests.rs new file mode 100644 index 00000000..40d450a0 --- /dev/null +++ b/common/src/collection/withdrawal_queue/tests.rs @@ -0,0 +1,54 @@ +use near_sdk::AccountId; + +use super::WithdrawalQueue; + +#[test] +fn withdrawal_remove() { + let mut wq = WithdrawalQueue::new(b"w"); + + let alice: AccountId = "alice".parse().unwrap(); + let bob: AccountId = "bob".parse().unwrap(); + let charlie: AccountId = "charlie".parse().unwrap(); + + wq.insert_or_update(&alice, 1.into()); + wq.insert_or_update(&bob, 2.into()); + wq.insert_or_update(&charlie, 3.into()); + assert_eq!(wq.len(), 3); + assert_eq!(wq.remove(&bob), Some(2.into())); + assert_eq!(wq.len(), 2); + assert_eq!(wq.remove(&charlie), Some(3.into())); + assert_eq!(wq.len(), 1); + assert_eq!(wq.remove(&alice), Some(1.into())); + assert_eq!(wq.len(), 0); +} + +#[test] +fn withdrawal_queueing() { + let mut wq = WithdrawalQueue::new(b"w"); + + let alice: AccountId = "alice".parse().unwrap(); + let bob: AccountId = "bob".parse().unwrap(); + let charlie: AccountId = "charlie".parse().unwrap(); + + assert_eq!(wq.len(), 0); + assert_eq!(wq.peek(), None); + wq.insert_or_update(&alice, 1.into()); + assert_eq!(wq.len(), 1); + assert_eq!(wq.peek(), Some((alice.clone(), 1.into()))); + wq.insert_or_update(&alice, 99.into()); + assert_eq!(wq.len(), 1); + assert_eq!(wq.peek(), Some((alice.clone(), 99.into()))); + wq.insert_or_update(&bob, 123.into()); + assert_eq!(wq.len(), 2); + wq.try_lock().unwrap(); + assert_eq!(wq.try_pop(), Some((alice.clone(), 99.into()))); + assert_eq!(wq.len(), 1); + wq.insert_or_update(&charlie, 42.into()); + assert_eq!(wq.len(), 2); + wq.try_lock().unwrap(); + assert_eq!(wq.try_pop(), Some((bob.clone(), 123.into()))); + assert_eq!(wq.len(), 1); + wq.try_lock().unwrap(); + assert_eq!(wq.try_pop(), Some((charlie.clone(), 42.into()))); + assert_eq!(wq.len(), 0); +} diff --git a/common/src/accumulator.rs b/common/src/data/accumulator.rs similarity index 72% rename from common/src/accumulator.rs rename to common/src/data/accumulator.rs index 1068b5c9..0cac7440 100644 --- a/common/src/accumulator.rs +++ b/common/src/data/accumulator.rs @@ -2,6 +2,9 @@ use near_sdk::{json_types::U128, near, require}; use crate::asset::{AssetClass, FungibleAssetAmount}; +#[cfg(test)] +mod tests; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[near(serializers = [borsh, json])] pub struct Accumulator { @@ -108,54 +111,3 @@ impl AccumulationRecord { self.amount } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn amortization() { - let mut a = Accumulator::::new(1); - - a.accumulate(AccumulationRecord { - amount: 100.into(), - fraction_as_u128_dividend: 0, - next_snapshot_index: 2, - }); - - assert_eq!(a.get_total(), 100.into()); - - a.amortize(25.into()); - - assert_eq!(a.get_total(), 125.into()); - - a.accumulate(AccumulationRecord { - amount: 100.into(), - fraction_as_u128_dividend: 0, - next_snapshot_index: 3, - }); - - assert_eq!(a.get_total(), 200.into()); - } - - #[test] - fn fraction() { - let mut a = Accumulator::::new(1); - - a.accumulate(AccumulationRecord { - amount: 100.into(), - fraction_as_u128_dividend: 1 << 127, - next_snapshot_index: 2, - }); - - assert_eq!(a.get_total(), 100.into()); - - a.accumulate(AccumulationRecord { - amount: 100.into(), - fraction_as_u128_dividend: 1 << 127, - next_snapshot_index: 3, - }); - - assert_eq!(a.get_total(), 201.into()); - } -} diff --git a/common/src/data/accumulator/tests.rs b/common/src/data/accumulator/tests.rs new file mode 100644 index 00000000..22706a72 --- /dev/null +++ b/common/src/data/accumulator/tests.rs @@ -0,0 +1,47 @@ +use super::*; + +#[test] +fn amortization() { + let mut a = Accumulator::::new(1); + + a.accumulate(AccumulationRecord { + amount: 100.into(), + fraction_as_u128_dividend: 0, + next_snapshot_index: 2, + }); + + assert_eq!(a.get_total(), 100.into()); + + a.amortize(25.into()); + + assert_eq!(a.get_total(), 125.into()); + + a.accumulate(AccumulationRecord { + amount: 100.into(), + fraction_as_u128_dividend: 0, + next_snapshot_index: 3, + }); + + assert_eq!(a.get_total(), 200.into()); +} + +#[test] +fn fraction() { + let mut a = Accumulator::::new(1); + + a.accumulate(AccumulationRecord { + amount: 100.into(), + fraction_as_u128_dividend: 1 << 127, + next_snapshot_index: 2, + }); + + assert_eq!(a.get_total(), 100.into()); + + a.accumulate(AccumulationRecord { + amount: 100.into(), + fraction_as_u128_dividend: 1 << 127, + next_snapshot_index: 3, + }); + + assert_eq!(a.get_total(), 201.into()); +} diff --git a/common/src/asset.rs b/common/src/data/asset.rs similarity index 92% rename from common/src/asset.rs rename to common/src/data/asset.rs index bc9814c8..a9d55ae0 100644 --- a/common/src/asset.rs +++ b/common/src/data/asset.rs @@ -11,6 +11,9 @@ use near_sdk::{ use crate::number::Decimal; +#[cfg(test)] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[near(serializers = [json, borsh])] pub struct FungibleAsset { @@ -313,38 +316,3 @@ impl std::fmt::Display for FungibleAssetAmount { pub type BorrowAssetAmount = FungibleAssetAmount; pub type CollateralAssetAmount = FungibleAssetAmount; - -#[cfg(test)] -mod tests { - use super::*; - use near_sdk::serde_json; - - #[test] - fn serialization() { - let amount = BorrowAssetAmount::new(100); - let serialized = serde_json::to_string(&amount).unwrap(); - assert_eq!(serialized, "\"100\""); - let deserialized: BorrowAssetAmount = serde_json::from_str(&serialized).unwrap(); - assert_eq!(deserialized, amount); - } - - #[test] - #[should_panic = "a + u128::MAX overflow"] - fn asset_op_macro_overflow() { - let mut a = BorrowAssetAmount::new(100); - - asset_op! { - a += u128::MAX; - }; - } - - #[test] - #[should_panic = "a - 101u128 underflow"] - fn asset_op_macro_underflow() { - let mut a = BorrowAssetAmount::new(100); - - asset_op! { - a -= 101u128; - }; - } -} diff --git a/common/src/data/asset/tests.rs b/common/src/data/asset/tests.rs new file mode 100644 index 00000000..e6c813e2 --- /dev/null +++ b/common/src/data/asset/tests.rs @@ -0,0 +1,33 @@ +use crate::asset_op; +use near_sdk::serde_json; + +use super::*; + +#[test] +fn serialization() { + let amount = BorrowAssetAmount::new(100); + let serialized = serde_json::to_string(&amount).unwrap(); + assert_eq!(serialized, "\"100\""); + let deserialized: BorrowAssetAmount = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, amount); +} + +#[test] +#[should_panic = "a + u128::MAX overflow"] +fn asset_op_macro_overflow() { + let mut a = BorrowAssetAmount::new(100); + + asset_op! { + a += u128::MAX; + }; +} + +#[test] +#[should_panic = "a - 101u128 underflow"] +fn asset_op_macro_underflow() { + let mut a = BorrowAssetAmount::new(100); + + asset_op! { + a -= 101u128; + }; +} diff --git a/common/src/fee.rs b/common/src/data/fee.rs similarity index 100% rename from common/src/fee.rs rename to common/src/data/fee.rs diff --git a/common/src/interest_rate_strategy.rs b/common/src/data/interest_rate_strategy.rs similarity index 84% rename from common/src/interest_rate_strategy.rs rename to common/src/data/interest_rate_strategy.rs index 6baf10cf..6f8eb8c8 100644 --- a/common/src/interest_rate_strategy.rs +++ b/common/src/data/interest_rate_strategy.rs @@ -4,6 +4,9 @@ use near_sdk::{near, require}; use crate::number::Decimal; +#[cfg(test)] +mod tests; + pub trait UsageCurve { fn at(&self, usage_ratio: Decimal) -> Decimal; } @@ -241,35 +244,3 @@ impl From for Exponential2Params { value.params } } - -#[cfg(test)] -mod tests { - use std::ops::Div; - - use crate::dec; - - use super::*; - - #[test] - fn piecewise() { - let s = Piecewise::new(Decimal::ZERO, dec!("0.9"), dec!("0.035"), dec!("0.6")).unwrap(); - - assert!(s.at(Decimal::ZERO).near_equal(Decimal::ZERO)); - assert!(s.at(dec!("0.1")).near_equal(dec!("0.0035"))); - assert!(s.at(dec!("0.5")).near_equal(dec!("0.0175"))); - assert!(s.at(dec!("0.6")).near_equal(dec!("0.021"))); - assert!(s.at(dec!("0.9")).near_equal(dec!("0.0315"))); - assert!(s.at(dec!("0.95")).near_equal(dec!("0.0615"))); - assert!(s.at(Decimal::ONE).near_equal(dec!("0.0915"))); - } - - #[test] - fn exponential2() { - let s = Exponential2::new(dec!("0.005"), dec!("0.08"), dec!("6")).unwrap(); - assert!(s.at(Decimal::ZERO).near_equal(dec!("0.005"))); - assert!(s.at(dec!("0.25")).near_equal(dec!( - "0.00717669895803117868762306839097547161564207589375463826946828509045412494" - ))); - assert!(s.at(Decimal::ONE_HALF).near_equal(Decimal::ONE.div(75u32))); - } -} diff --git a/common/src/data/interest_rate_strategy/tests.rs b/common/src/data/interest_rate_strategy/tests.rs new file mode 100644 index 00000000..c4f19a71 --- /dev/null +++ b/common/src/data/interest_rate_strategy/tests.rs @@ -0,0 +1,28 @@ +use std::ops::Div; + +use crate::dec; + +use super::*; + +#[test] +fn piecewise() { + let s = Piecewise::new(Decimal::ZERO, dec!("0.9"), dec!("0.035"), dec!("0.6")).unwrap(); + + assert!(s.at(Decimal::ZERO).near_equal(Decimal::ZERO)); + assert!(s.at(dec!("0.1")).near_equal(dec!("0.0035"))); + assert!(s.at(dec!("0.5")).near_equal(dec!("0.0175"))); + assert!(s.at(dec!("0.6")).near_equal(dec!("0.021"))); + assert!(s.at(dec!("0.9")).near_equal(dec!("0.0315"))); + assert!(s.at(dec!("0.95")).near_equal(dec!("0.0615"))); + assert!(s.at(Decimal::ONE).near_equal(dec!("0.0915"))); +} + +#[test] +fn exponential2() { + let s = Exponential2::new(dec!("0.005"), dec!("0.08"), dec!("6")).unwrap(); + assert!(s.at(Decimal::ZERO).near_equal(dec!("0.005"))); + assert!(s.at(dec!("0.25")).near_equal(dec!( + "0.00717669895803117868762306839097547161564207589375463826946828509045412494" + ))); + assert!(s.at(Decimal::ONE_HALF).near_equal(Decimal::ONE.div(75u32))); +} diff --git a/common/src/data/mod.rs b/common/src/data/mod.rs new file mode 100644 index 00000000..b0af3188 --- /dev/null +++ b/common/src/data/mod.rs @@ -0,0 +1,9 @@ +pub mod accumulator; +pub mod asset; +pub mod fee; +pub mod interest_rate_strategy; +pub mod number; +pub mod price; +pub mod snapshot; +pub mod static_yield; +pub mod time_chunk; diff --git a/common/src/number.rs b/common/src/data/number.rs similarity index 64% rename from common/src/number.rs rename to common/src/data/number.rs index 1f194647..e2f717cc 100644 --- a/common/src/number.rs +++ b/common/src/data/number.rs @@ -11,6 +11,9 @@ use near_sdk::{ use primitive_types::U512; use schemars::JsonSchema; +#[cfg(test)] +mod tests; + pub const FRACTIONAL_BITS: usize = 128; /// `floor(FRACTIONAL_BITS / log2(10))` pub const FRACTIONAL_DECIMAL_DIGITS: usize = 38; @@ -622,329 +625,3 @@ impl_int!(u32); impl_int!(u64); impl_int!(u128); impl_int!(::primitive_types::U256); - -#[cfg(test)] -mod tests { - use near_sdk::serde_json; - use primitive_types::U256; - use rand::Rng; - use rstest::rstest; - - use super::*; - - // These functions are intentionally implemented using mathematical - // operations instead of bitwise operations, so as to test the - // correctness of the mathematical operators. - - fn with_upper_u128(n: u128) -> Decimal { - let mut d = Decimal::from(n); - d *= Decimal::from(u128::pow(2, 64)); - d *= Decimal::from(u128::pow(2, 64)); - d - } - - fn get_upper_u128(mut d: Decimal) -> u128 { - d /= Decimal::from(u128::pow(2, 64)); - d /= Decimal::from(u128::pow(2, 64)); - d.to_u128_floor().unwrap() - } - - #[rstest] - #[case(0, 0)] - #[case(0, 1)] - #[case(1, 0)] - #[case(1, 1)] - #[case(2_934_570_000_008_u128, 9_595_959_283_u128)] - #[case(u128::MAX, 0)] - #[case(0, u128::MAX)] - #[test] - fn addition(#[case] a: u128, #[case] b: u128) { - assert_eq!(Decimal::from(a) + Decimal::from(b), a + b); - assert_eq!( - get_upper_u128(with_upper_u128(a) + with_upper_u128(b)), - a + b, - ); - } - - #[rstest] - #[case(0, 0)] - #[case(1, 0)] - #[case(1, 1)] - #[case(2_934_570_000_008_u128, 9_595_959_283_u128)] - #[case(u128::MAX, 0)] - #[case(u128::MAX, 1)] - #[case(u128::MAX, u128::MAX / 2)] - #[case(u128::MAX, u128::MAX)] - #[test] - fn subtraction(#[case] a: u128, #[case] b: u128) { - assert_eq!(Decimal::from(a) - Decimal::from(b), a - b); - assert_eq!( - get_upper_u128(with_upper_u128(a) - with_upper_u128(b)), - a - b, - ); - } - - #[rstest] - #[case(0, 0)] - #[case(0, 1)] - #[case(1, 0)] - #[case(1, 1)] - #[case(2, 2)] - #[case(u128::MAX, 0)] - #[case(u128::MAX, 1)] - #[case(0, u128::MAX)] - #[case(1, u128::MAX)] - #[test] - fn multiplication(#[case] a: u128, #[case] b: u128) { - assert_eq!(Decimal::from(a) * Decimal::from(b), a * b); - assert_eq!(get_upper_u128(with_upper_u128(a) * b), a * b); - assert_eq!(get_upper_u128(a * with_upper_u128(b)), a * b); - } - - #[rstest] - #[case(0, 1)] - #[case(1, 1)] - #[case(1, 2)] - #[case(u128::MAX, u128::MAX)] - #[case(u128::MAX, 1)] - #[case(0, u128::MAX)] - #[case(1, u128::MAX)] - #[case(1, 10)] - #[case(3, 10_000)] - #[test] - fn division(#[case] a: u128, #[case] b: u128) { - #[allow(clippy::cast_precision_loss)] - let quotient = a as f64 / b as f64; - let abs_difference_lte = |d: Decimal, f: f64| (d.to_f64_lossy() - f).abs() <= 1e-200; - assert!(abs_difference_lte( - Decimal::from(a) / Decimal::from(b), - quotient, - )); - assert!(abs_difference_lte( - with_upper_u128(a) / with_upper_u128(b), - quotient, - )); - } - - #[rstest] - #[case(12, 2)] - #[case(2, 32)] - #[case(1, 0)] - #[case(0, 0)] - #[case(0, 1)] - #[case(1, 1)] - #[test] - fn power(#[case] x: u128, #[case] n: u32) { - #[allow(clippy::cast_possible_wrap)] - let n_i32 = n as i32; - assert_eq!(Decimal::from(x).pow(n_i32), Decimal::from(x.pow(n))); - } - - #[test] - #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - fn pow10_valid_range() { - assert_eq!( - Decimal::ONE.mul_pow10(-(FRACTIONAL_DECIMAL_DIGITS as i32) - 1), - None, - ); - for i in -(FRACTIONAL_DECIMAL_DIGITS as i32)..=(WHOLE_DECIMAL_DIGITS as i32) { - eprintln!("10^{i} = {:?}", Decimal::ONE.mul_pow10(i).unwrap()); - } - assert_eq!( - Decimal::ONE.mul_pow10((WHOLE_DECIMAL_DIGITS as i32) + 1), - None, - ); - } - - #[rstest] - #[case(0, 0)] - #[case(0, 1)] - #[case(0, -1)] - #[case(1, 0)] - #[case(1, 1)] - #[case(1, -1)] - #[case(1, i32::try_from(WHOLE_DECIMAL_DIGITS).unwrap())] - #[case(1, i32::try_from(FRACTIONAL_DECIMAL_DIGITS).unwrap())] - #[case(1, -i32::try_from(FRACTIONAL_DECIMAL_DIGITS).unwrap())] - #[case(12, 20)] - #[case(12, 0)] - #[case(12, -20)] - #[case(u128::MAX, 0)] - #[case(u128::MAX, -20)] - #[test] - fn mul_pow10(#[case] x: u128, #[case] n: i32) { - #[allow(clippy::cast_sign_loss)] - if n >= 0 { - assert_eq!( - Decimal::from(x).mul_pow10(n).unwrap(), - Decimal::from(x) * Decimal::from(10u32).pow(n), - ); - } else { - assert!(Decimal::from(x) - .mul_pow10(n) - .unwrap() - .near_equal(Decimal::from(x) / U256::exp10(-n as usize))); - } - } - - #[test] - fn constants_are_accurate() { - assert_eq!(Decimal::ZERO.to_u128_floor().unwrap(), 0); - assert!((Decimal::ONE_HALF.to_f64_lossy() - 0.5_f64).abs() < 1e-200); - assert_eq!(Decimal::ONE.to_u128_floor().unwrap(), 1); - assert_eq!(Decimal::TWO.to_u128_floor().unwrap(), 2); - } - - #[rstest] - #[case(Decimal::ONE, 0)] - #[case(Decimal::ONE_HALF, 1u128 << 127)] - #[test] - fn get_fractional_dividend(#[case] value: Decimal, #[case] expected: u128) { - assert_eq!(value.fractional_part_as_u128_dividend(), expected); - } - - #[rstest] - #[case(Decimal::ONE)] - #[case(Decimal::TWO)] - #[case(Decimal::ZERO)] - #[case(Decimal::ONE_HALF)] - #[case(Decimal::from(u128::MAX))] - #[case(Decimal::from(u64::MAX) / Decimal::from(u128::MAX))] - #[test] - fn serialization(#[case] value: Decimal) { - let serialized = serde_json::to_string(&value).unwrap(); - let deserialized: Decimal = serde_json::from_str(&serialized).unwrap(); - - assert!(value.near_equal(deserialized)); - } - - #[test] - fn from_self_string_serialization_precision() { - const ITERATIONS: usize = 1_024; - const TRANSFORMATIONS: usize = 16; - - let mut rng = rand::thread_rng(); - - let mut max_error = U512::zero(); - let mut error_distribution = [0u32; 16]; - let mut value_with_max_error = Decimal::ZERO; - - #[allow(clippy::cast_possible_truncation)] - for _ in 0..ITERATIONS { - let actual = Decimal { - repr: U512(rng.gen()), - }; - - let mut s = actual.to_fixed(FRACTIONAL_DECIMAL_DIGITS); - for _ in 0..(TRANSFORMATIONS - 1) { - s = Decimal::from_str(&s) - .unwrap() - .to_fixed(FRACTIONAL_DECIMAL_DIGITS); - } - let parsed = Decimal::from_str(&s).unwrap(); - - let e = actual.abs_diff(parsed).repr; - - if e > max_error { - max_error = e; - value_with_max_error = actual; - } - - error_distribution[e.0[0] as usize] += 1; - } - - println!("Error distribution:"); - for (i, x) in error_distribution.iter().enumerate() { - println!("\t{i}: {x:b}"); - } - println!("Max error: {:?}", max_error.0); - - assert!( - max_error <= Decimal::REPR_EPSILON, - "Stringification error of repr {:?} is repr {:?}", - value_with_max_error.repr.0, - max_error.0, - ); - } - - #[test] - #[allow(clippy::cast_precision_loss)] - fn from_f64_string_serialization_precision() { - const ITERATIONS: usize = 10_000; - let mut rng = rand::thread_rng(); - let epsilon = Decimal { - repr: Decimal::REPR_EPSILON, - } - .to_f64_lossy(); - - let t = |f: f64| { - let actual = f.abs(); - let string = actual.to_string(); - let parsed = Decimal::from_str(&string).unwrap(); - - let e = (parsed.to_f64_lossy() - actual).abs(); - - assert!(e <= epsilon, "Stringification error of f64 {actual} is {e}"); - }; - - for _ in 0..ITERATIONS { - t(rng.gen::() * rng.gen::() as f64); - } - } - - #[test] - fn round_up_repr() { - let cases = [ - Decimal { - #[rustfmt::skip] - repr: U512([ 0x0966_4E4C_9169_501F, 0xB226_2812_5CF2_3CD0, 1, 0, 0, 0, 0, 0 ]), - }, - Decimal { - repr: U512([u64::MAX, u64::MAX, 1, 0, 0, 0, 0, 0]), - // 1.99999999999999999999999999999999999999706126412294428123007815865694438580... - }, - Decimal { - repr: U512([u64::MAX - 1, u64::MAX, 1, 0, 0, 0, 0, 0]), - }, - Decimal { repr: U512::MAX }, - Decimal { - repr: U512::MAX.saturating_sub(U512::one()), - }, - Decimal { repr: U512::zero() }, - ]; - - for case in cases { - let p: Decimal = case.to_fixed(FRACTIONAL_DECIMAL_DIGITS).parse().unwrap(); - - eprintln!("{:x?}", case.repr.0); - eprintln!("{:x?}", p.repr.0); - eprintln!("|{p:?} - {case:?}| = {:?}", p.abs_diff(case).as_repr()); - - assert!(p.near_equal(case)); - } - } - - #[test] - fn round_up_str() { - // Cases that are (generally) not evenly representable in binary fraction. - let cases = [ - "1", - "0", - "1.6958947224456518", - "2.79", - "0.6", - "10.6", - "0.01", - "0.599999999999999999999999999999999999", - ]; - for case in cases { - println!("Testing {case}..."); - let n = Decimal::from_str(case).unwrap(); - let s = n.to_fixed(FRACTIONAL_DECIMAL_DIGITS); - let parsed = Decimal::from_str(&s).unwrap(); - assert_eq!(n, parsed); - println!("{n:?}"); - println!(); - } - } -} diff --git a/common/src/data/number/tests.rs b/common/src/data/number/tests.rs new file mode 100644 index 00000000..80cb684d --- /dev/null +++ b/common/src/data/number/tests.rs @@ -0,0 +1,322 @@ +use near_sdk::serde_json; +use primitive_types::U256; +use rand::Rng; +use rstest::rstest; + +use super::*; + +// These functions are intentionally implemented using mathematical +// operations instead of bitwise operations, so as to test the +// correctness of the mathematical operators. + +fn with_upper_u128(n: u128) -> Decimal { + let mut d = Decimal::from(n); + d *= Decimal::from(u128::pow(2, 64)); + d *= Decimal::from(u128::pow(2, 64)); + d +} + +fn get_upper_u128(mut d: Decimal) -> u128 { + d /= Decimal::from(u128::pow(2, 64)); + d /= Decimal::from(u128::pow(2, 64)); + d.to_u128_floor().unwrap() +} + +#[rstest] +#[case(0, 0)] +#[case(0, 1)] +#[case(1, 0)] +#[case(1, 1)] +#[case(2_934_570_000_008_u128, 9_595_959_283_u128)] +#[case(u128::MAX, 0)] +#[case(0, u128::MAX)] +#[test] +fn addition(#[case] a: u128, #[case] b: u128) { + assert_eq!(Decimal::from(a) + Decimal::from(b), a + b); + assert_eq!( + get_upper_u128(with_upper_u128(a) + with_upper_u128(b)), + a + b, + ); +} + +#[rstest] +#[case(0, 0)] +#[case(1, 0)] +#[case(1, 1)] +#[case(2_934_570_000_008_u128, 9_595_959_283_u128)] +#[case(u128::MAX, 0)] +#[case(u128::MAX, 1)] +#[case(u128::MAX, u128::MAX / 2)] +#[case(u128::MAX, u128::MAX)] +#[test] +fn subtraction(#[case] a: u128, #[case] b: u128) { + assert_eq!(Decimal::from(a) - Decimal::from(b), a - b); + assert_eq!( + get_upper_u128(with_upper_u128(a) - with_upper_u128(b)), + a - b, + ); +} + +#[rstest] +#[case(0, 0)] +#[case(0, 1)] +#[case(1, 0)] +#[case(1, 1)] +#[case(2, 2)] +#[case(u128::MAX, 0)] +#[case(u128::MAX, 1)] +#[case(0, u128::MAX)] +#[case(1, u128::MAX)] +#[test] +fn multiplication(#[case] a: u128, #[case] b: u128) { + assert_eq!(Decimal::from(a) * Decimal::from(b), a * b); + assert_eq!(get_upper_u128(with_upper_u128(a) * b), a * b); + assert_eq!(get_upper_u128(a * with_upper_u128(b)), a * b); +} + +#[rstest] +#[case(0, 1)] +#[case(1, 1)] +#[case(1, 2)] +#[case(u128::MAX, u128::MAX)] +#[case(u128::MAX, 1)] +#[case(0, u128::MAX)] +#[case(1, u128::MAX)] +#[case(1, 10)] +#[case(3, 10_000)] +#[test] +fn division(#[case] a: u128, #[case] b: u128) { + #[allow(clippy::cast_precision_loss)] + let quotient = a as f64 / b as f64; + let abs_difference_lte = |d: Decimal, f: f64| (d.to_f64_lossy() - f).abs() <= 1e-200; + assert!(abs_difference_lte( + Decimal::from(a) / Decimal::from(b), + quotient, + )); + assert!(abs_difference_lte( + with_upper_u128(a) / with_upper_u128(b), + quotient, + )); +} + +#[rstest] +#[case(12, 2)] +#[case(2, 32)] +#[case(1, 0)] +#[case(0, 0)] +#[case(0, 1)] +#[case(1, 1)] +#[test] +fn power(#[case] x: u128, #[case] n: u32) { + #[allow(clippy::cast_possible_wrap)] + let n_i32 = n as i32; + assert_eq!(Decimal::from(x).pow(n_i32), Decimal::from(x.pow(n))); +} + +#[test] +#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] +fn pow10_valid_range() { + assert_eq!( + Decimal::ONE.mul_pow10(-(FRACTIONAL_DECIMAL_DIGITS as i32) - 1), + None, + ); + for i in -(FRACTIONAL_DECIMAL_DIGITS as i32)..=(WHOLE_DECIMAL_DIGITS as i32) { + eprintln!("10^{i} = {:?}", Decimal::ONE.mul_pow10(i).unwrap()); + } + assert_eq!( + Decimal::ONE.mul_pow10((WHOLE_DECIMAL_DIGITS as i32) + 1), + None, + ); +} + +#[rstest] +#[case(0, 0)] +#[case(0, 1)] +#[case(0, -1)] +#[case(1, 0)] +#[case(1, 1)] +#[case(1, -1)] +#[case(1, i32::try_from(WHOLE_DECIMAL_DIGITS).unwrap())] +#[case(1, i32::try_from(FRACTIONAL_DECIMAL_DIGITS).unwrap())] +#[case(1, -i32::try_from(FRACTIONAL_DECIMAL_DIGITS).unwrap())] +#[case(12, 20)] +#[case(12, 0)] +#[case(12, -20)] +#[case(u128::MAX, 0)] +#[case(u128::MAX, -20)] +#[test] +fn mul_pow10(#[case] x: u128, #[case] n: i32) { + #[allow(clippy::cast_sign_loss)] + if n >= 0 { + assert_eq!( + Decimal::from(x).mul_pow10(n).unwrap(), + Decimal::from(x) * Decimal::from(10u32).pow(n), + ); + } else { + assert!(Decimal::from(x) + .mul_pow10(n) + .unwrap() + .near_equal(Decimal::from(x) / U256::exp10(-n as usize))); + } +} + +#[test] +fn constants_are_accurate() { + assert_eq!(Decimal::ZERO.to_u128_floor().unwrap(), 0); + assert!((Decimal::ONE_HALF.to_f64_lossy() - 0.5_f64).abs() < 1e-200); + assert_eq!(Decimal::ONE.to_u128_floor().unwrap(), 1); + assert_eq!(Decimal::TWO.to_u128_floor().unwrap(), 2); +} + +#[rstest] +#[case(Decimal::ONE, 0)] +#[case(Decimal::ONE_HALF, 1u128 << 127)] +#[test] +fn get_fractional_dividend(#[case] value: Decimal, #[case] expected: u128) { + assert_eq!(value.fractional_part_as_u128_dividend(), expected); +} + +#[rstest] +#[case(Decimal::ONE)] +#[case(Decimal::TWO)] +#[case(Decimal::ZERO)] +#[case(Decimal::ONE_HALF)] +#[case(Decimal::from(u128::MAX))] +#[case(Decimal::from(u64::MAX) / Decimal::from(u128::MAX))] +#[test] +fn serialization(#[case] value: Decimal) { + let serialized = serde_json::to_string(&value).unwrap(); + let deserialized: Decimal = serde_json::from_str(&serialized).unwrap(); + + assert!(value.near_equal(deserialized)); +} + +#[test] +fn from_self_string_serialization_precision() { + const ITERATIONS: usize = 1_024; + const TRANSFORMATIONS: usize = 16; + + let mut rng = rand::thread_rng(); + + let mut max_error = U512::zero(); + let mut error_distribution = [0u32; 16]; + let mut value_with_max_error = Decimal::ZERO; + + #[allow(clippy::cast_possible_truncation)] + for _ in 0..ITERATIONS { + let actual = Decimal { + repr: U512(rng.gen()), + }; + + let mut s = actual.to_fixed(FRACTIONAL_DECIMAL_DIGITS); + for _ in 0..(TRANSFORMATIONS - 1) { + s = Decimal::from_str(&s) + .unwrap() + .to_fixed(FRACTIONAL_DECIMAL_DIGITS); + } + let parsed = Decimal::from_str(&s).unwrap(); + + let e = actual.abs_diff(parsed).repr; + + if e > max_error { + max_error = e; + value_with_max_error = actual; + } + + error_distribution[e.0[0] as usize] += 1; + } + + println!("Error distribution:"); + for (i, x) in error_distribution.iter().enumerate() { + println!("\t{i}: {x:b}"); + } + println!("Max error: {:?}", max_error.0); + + assert!( + max_error <= Decimal::REPR_EPSILON, + "Stringification error of repr {:?} is repr {:?}", + value_with_max_error.repr.0, + max_error.0, + ); +} + +#[test] +#[allow(clippy::cast_precision_loss)] +fn from_f64_string_serialization_precision() { + const ITERATIONS: usize = 10_000; + let mut rng = rand::thread_rng(); + let epsilon = Decimal { + repr: Decimal::REPR_EPSILON, + } + .to_f64_lossy(); + + let t = |f: f64| { + let actual = f.abs(); + let string = actual.to_string(); + let parsed = Decimal::from_str(&string).unwrap(); + + let e = (parsed.to_f64_lossy() - actual).abs(); + + assert!(e <= epsilon, "Stringification error of f64 {actual} is {e}"); + }; + + for _ in 0..ITERATIONS { + t(rng.gen::() * rng.gen::() as f64); + } +} + +#[test] +fn round_up_repr() { + let cases = [ + Decimal { + #[rustfmt::skip] + repr: U512([ 0x0966_4E4C_9169_501F, 0xB226_2812_5CF2_3CD0, 1, 0, 0, 0, 0, 0 ]), + }, + Decimal { + repr: U512([u64::MAX, u64::MAX, 1, 0, 0, 0, 0, 0]), + // 1.99999999999999999999999999999999999999706126412294428123007815865694438580... + }, + Decimal { + repr: U512([u64::MAX - 1, u64::MAX, 1, 0, 0, 0, 0, 0]), + }, + Decimal { repr: U512::MAX }, + Decimal { + repr: U512::MAX.saturating_sub(U512::one()), + }, + Decimal { repr: U512::zero() }, + ]; + + for case in cases { + let p: Decimal = case.to_fixed(FRACTIONAL_DECIMAL_DIGITS).parse().unwrap(); + + eprintln!("{:x?}", case.repr.0); + eprintln!("{:x?}", p.repr.0); + eprintln!("|{p:?} - {case:?}| = {:?}", p.abs_diff(case).as_repr()); + + assert!(p.near_equal(case)); + } +} + +#[test] +fn round_up_str() { + // Cases that are (generally) not evenly representable in binary fraction. + let cases = [ + "1", + "0", + "1.6958947224456518", + "2.79", + "0.6", + "10.6", + "0.01", + "0.599999999999999999999999999999999999", + ]; + for case in cases { + println!("Testing {case}..."); + let n = Decimal::from_str(case).unwrap(); + let s = n.to_fixed(FRACTIONAL_DECIMAL_DIGITS); + let parsed = Decimal::from_str(&s).unwrap(); + assert_eq!(n, parsed); + println!("{n:?}"); + println!(); + } +} diff --git a/common/src/price.rs b/common/src/data/price.rs similarity index 61% rename from common/src/price.rs rename to common/src/data/price.rs index fa769bbd..2e922749 100644 --- a/common/src/price.rs +++ b/common/src/data/price.rs @@ -153,109 +153,4 @@ impl Valuation { } #[cfg(test)] -mod tests { - use rstest::rstest; - - use crate::dec; - - use super::*; - - #[test] - fn valuation_eq() { - let o = Valuation::optimistic( - 1000u128.into(), - &Price:: { - _asset: PhantomData, - price: 250, - confidence: 12, - exponent: -5, - }, - ); - - assert_eq!(o.coefficient, U256::from(1000 * (250 + 12))); - assert_eq!(o.exponent, -5); - - let p = Valuation::pessimistic( - 1000u128.into(), - &Price:: { - _asset: PhantomData, - price: 250, - confidence: 12, - exponent: -5, - }, - ); - - assert_eq!(p.coefficient, U256::from(1000 * (250 - 12))); - assert_eq!(p.exponent, -5); - } - - #[test] - fn valuation_ratio_equal() { - let first = Valuation::optimistic( - 600u128.into(), - &Price:: { - _asset: PhantomData, - price: 100, - confidence: 0, - exponent: 4, - }, - ); - let second = Valuation::pessimistic( - 60u128.into(), - &Price:: { - _asset: PhantomData, - price: 1000, - confidence: 0, - exponent: 4, - }, - ); - - assert_eq!(first.ratio(second).unwrap(), Decimal::ONE); - } - - #[rstest] - #[case(8, 1, 8, 0, dec!("1"))] - #[case(1, 25, 1, -2, dec!("4"))] - #[case(0, 1, 1, 0, dec!("0"))] - #[case(800, 2, 4, 2, dec!("1"))] - #[case(u128::MAX, 1, 1, i32::MIN, Decimal::MAX)] - #[case(1, 1, 1, i32::MAX, Decimal::MIN)] - // The following case returns a power of 2. Whereas the *correct* answer is - // 1e+115, the approximation 2^382 is about 9.85e+114. Keep in mind Decimal - // only supports a total of 115 whole decimal digits. - #[case(u128::MAX, u128::MAX, 1, -115, Decimal::pow2_int(382).unwrap())] - #[case(1, 1, 1, 39, Decimal::ZERO)] - #[test] - fn valuation_ratios( - #[case] value: u128, - #[case] divisor_value: u128, - #[case] divisor_price: u128, - #[case] divisor_exponent: i32, - #[case] expected_result: impl Into, - ) { - let dividend = Valuation::optimistic( - value.into(), - &Price:: { - _asset: PhantomData, - price: 1, - confidence: 0, - exponent: 0, - }, - ); - - let divisor = Valuation::optimistic( - divisor_value.into(), - &Price:: { - _asset: PhantomData, - price: divisor_price, - confidence: 0, - exponent: divisor_exponent, - }, - ); - - println!("{dividend:?}"); - println!("{divisor:?}"); - - assert_eq!(dividend.ratio(divisor).unwrap(), expected_result.into()); - } -} +mod tests; diff --git a/common/src/data/price/tests.rs b/common/src/data/price/tests.rs new file mode 100644 index 00000000..6fb9e006 --- /dev/null +++ b/common/src/data/price/tests.rs @@ -0,0 +1,104 @@ +use rstest::rstest; + +use crate::dec; + +use super::*; + +#[test] +fn valuation_eq() { + let o = Valuation::optimistic( + 1000u128.into(), + &Price:: { + _asset: PhantomData, + price: 250, + confidence: 12, + exponent: -5, + }, + ); + + assert_eq!(o.coefficient, U256::from(1000 * (250 + 12))); + assert_eq!(o.exponent, -5); + + let p = Valuation::pessimistic( + 1000u128.into(), + &Price:: { + _asset: PhantomData, + price: 250, + confidence: 12, + exponent: -5, + }, + ); + + assert_eq!(p.coefficient, U256::from(1000 * (250 - 12))); + assert_eq!(p.exponent, -5); +} + +#[test] +fn valuation_ratio_equal() { + let first = Valuation::optimistic( + 600u128.into(), + &Price:: { + _asset: PhantomData, + price: 100, + confidence: 0, + exponent: 4, + }, + ); + let second = Valuation::pessimistic( + 60u128.into(), + &Price:: { + _asset: PhantomData, + price: 1000, + confidence: 0, + exponent: 4, + }, + ); + + assert_eq!(first.ratio(second).unwrap(), Decimal::ONE); +} + +#[rstest] +#[case(8, 1, 8, 0, dec!("1"))] +#[case(1, 25, 1, -2, dec!("4"))] +#[case(0, 1, 1, 0, dec!("0"))] +#[case(800, 2, 4, 2, dec!("1"))] +#[case(u128::MAX, 1, 1, i32::MIN, Decimal::MAX)] +#[case(1, 1, 1, i32::MAX, Decimal::MIN)] +// The following case returns a power of 2. Whereas the *correct* answer is +// 1e+115, the approximation 2^382 is about 9.85e+114. Keep in mind Decimal +// only supports a total of 115 whole decimal digits. +#[case(u128::MAX, u128::MAX, 1, -115, Decimal::pow2_int(382).unwrap())] +#[case(1, 1, 1, 39, Decimal::ZERO)] +#[test] +fn valuation_ratios( + #[case] value: u128, + #[case] divisor_value: u128, + #[case] divisor_price: u128, + #[case] divisor_exponent: i32, + #[case] expected_result: impl Into, +) { + let dividend = Valuation::optimistic( + value.into(), + &Price:: { + _asset: PhantomData, + price: 1, + confidence: 0, + exponent: 0, + }, + ); + + let divisor = Valuation::optimistic( + divisor_value.into(), + &Price:: { + _asset: PhantomData, + price: divisor_price, + confidence: 0, + exponent: divisor_exponent, + }, + ); + + println!("{dividend:?}"); + println!("{divisor:?}"); + + assert_eq!(dividend.ratio(divisor).unwrap(), expected_result.into()); +} diff --git a/common/src/snapshot.rs b/common/src/data/snapshot.rs similarity index 100% rename from common/src/snapshot.rs rename to common/src/data/snapshot.rs diff --git a/common/src/static_yield.rs b/common/src/data/static_yield.rs similarity index 100% rename from common/src/static_yield.rs rename to common/src/data/static_yield.rs diff --git a/common/src/time_chunk.rs b/common/src/data/time_chunk.rs similarity index 100% rename from common/src/time_chunk.rs rename to common/src/data/time_chunk.rs diff --git a/common/src/lib.rs b/common/src/lib.rs index ca531b4b..2a7c1243 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,19 +1,11 @@ -pub mod accumulator; -pub mod asset; -pub mod borrow; -pub mod chunked_append_only_list; -pub mod event; -pub mod fee; -pub mod interest_rate_strategy; +mod collection; +pub use collection::*; +mod data; +pub use data::*; pub mod market; -pub mod number; pub mod oracle; -pub mod price; -pub mod snapshot; -pub mod static_yield; -pub mod supply; -pub mod time_chunk; -pub mod withdrawal_queue; +mod position; +pub use position::*; pub static MS_IN_A_YEAR: std::sync::LazyLock = std::sync::LazyLock::new(|| number::Decimal::from(31_556_952_000_u128)); // 1000 * 60 * 60 * 24 * 365.2425 diff --git a/common/src/market/configuration.rs b/common/src/market/configuration.rs index 6adce631..e33b22ab 100644 --- a/common/src/market/configuration.rs +++ b/common/src/market/configuration.rs @@ -17,6 +17,9 @@ use crate::{ use super::{PriceOracleConfiguration, YieldWeights}; +#[cfg(test)] +mod tests; + /// Reject >10,000,000% APY interest rates as misconfigurations. /// This also guarantees a reasonable upper-limit to interest rates to help avoid overflows. pub const APY_LIMIT: u128 = 100_000; @@ -312,96 +315,3 @@ fn satisfies_minimum_collateral_ratio( .ratio(borrow_valuation) .is_some_and(|ratio| ratio >= mcr) } - -#[cfg(test)] -mod tests { - use near_sdk::{ - json_types::U128, - serde_json::{self, json}, - }; - use rstest::rstest; - - use crate::{borrow::InterestAccumulationProof, dec, oracle::pyth}; - - use super::*; - - #[test] - fn test_satisfies_minimum_collateral_ratio() { - let mut b = BorrowPosition::new(0); - b.increase_collateral_asset_deposit(121u128.into()); - b.increase_borrow_asset_principal(InterestAccumulationProof::test(), 100u128.into(), 0); - assert!(satisfies_minimum_collateral_ratio( - dec!("1.2"), - &b, - &PricePair::new( - &pyth::Price { - price: near_sdk::json_types::I64(10000), - conf: U64(1), - expo: -4, - publish_time: 0, - }, - 18, - &pyth::Price { - price: near_sdk::json_types::I64(10000), - conf: U64(1), - expo: -4, - publish_time: 0, - }, - 18, - ) - .unwrap() - )); - } - - #[rstest] - #[case(1, 0)] - #[case(0, 0)] - #[case(u128::MAX, 0)] - #[case(u128::MAX, u128::MAX - 1)] - #[case(500, 10)] - #[should_panic = "Invalid range specified"] - fn invalid_amount_range(#[case] min: u128, #[case] max: u128) { - ValidAmountRange::::try_from((min, Some(max))).unwrap(); - } - - #[rstest] - #[case(1, 0)] - #[case(0, 0)] - #[case(u128::MAX, 0)] - #[case(u128::MAX, u128::MAX - 1)] - #[case(500, 10)] - #[should_panic = "Invalid range specified"] - fn invalid_amount_range_json(#[case] min: u128, #[case] max: u128) { - serde_json::from_value::>(json!({ - "minimum": U128(min), - "maximum": U128(max), - })) - .unwrap(); - } - - #[rstest] - #[case(1, 1)] - #[case(0, u128::MAX)] - #[case(1, u128::MAX)] - #[case(u128::MAX, u128::MAX)] - #[case(u128::MAX - 1, u128::MAX)] - #[case(10, 500)] - fn valid_amount_range(#[case] min: u128, #[case] max: u128) { - ValidAmountRange::::try_from((min, Some(max))).unwrap(); - } - - #[rstest] - #[case(1, 1)] - #[case(0, u128::MAX)] - #[case(1, u128::MAX)] - #[case(u128::MAX, u128::MAX)] - #[case(u128::MAX - 1, u128::MAX)] - #[case(10, 500)] - fn valid_amount_range_json(#[case] min: u128, #[case] max: u128) { - serde_json::from_value::>(json!({ - "minimum": U128(min), - "maximum": U128(max), - })) - .unwrap(); - } -} diff --git a/common/src/market/configuration/tests.rs b/common/src/market/configuration/tests.rs new file mode 100644 index 00000000..5a608a1f --- /dev/null +++ b/common/src/market/configuration/tests.rs @@ -0,0 +1,89 @@ +use near_sdk::{ + json_types::U128, + serde_json::{self, json}, +}; +use rstest::rstest; + +use crate::{borrow::InterestAccumulationProof, dec, oracle::pyth}; + +use super::*; + +#[test] +fn test_satisfies_minimum_collateral_ratio() { + let mut b = BorrowPosition::new(0); + b.increase_collateral_asset_deposit(121u128.into()); + b.increase_borrow_asset_principal(InterestAccumulationProof::test(), 100u128.into(), 0); + assert!(satisfies_minimum_collateral_ratio( + dec!("1.2"), + &b, + &PricePair::new( + &pyth::Price { + price: near_sdk::json_types::I64(10000), + conf: U64(1), + expo: -4, + publish_time: 0, + }, + 18, + &pyth::Price { + price: near_sdk::json_types::I64(10000), + conf: U64(1), + expo: -4, + publish_time: 0, + }, + 18, + ) + .unwrap() + )); +} + +#[rstest] +#[case(1, 0)] +#[case(0, 0)] +#[case(u128::MAX, 0)] +#[case(u128::MAX, u128::MAX - 1)] +#[case(500, 10)] +#[should_panic = "Invalid range specified"] +fn invalid_amount_range(#[case] min: u128, #[case] max: u128) { + ValidAmountRange::::try_from((min, Some(max))).unwrap(); +} + +#[rstest] +#[case(1, 0)] +#[case(0, 0)] +#[case(u128::MAX, 0)] +#[case(u128::MAX, u128::MAX - 1)] +#[case(500, 10)] +#[should_panic = "Invalid range specified"] +fn invalid_amount_range_json(#[case] min: u128, #[case] max: u128) { + serde_json::from_value::>(json!({ + "minimum": U128(min), + "maximum": U128(max), + })) + .unwrap(); +} + +#[rstest] +#[case(1, 1)] +#[case(0, u128::MAX)] +#[case(1, u128::MAX)] +#[case(u128::MAX, u128::MAX)] +#[case(u128::MAX - 1, u128::MAX)] +#[case(10, 500)] +fn valid_amount_range(#[case] min: u128, #[case] max: u128) { + ValidAmountRange::::try_from((min, Some(max))).unwrap(); +} + +#[rstest] +#[case(1, 1)] +#[case(0, u128::MAX)] +#[case(1, u128::MAX)] +#[case(u128::MAX, u128::MAX)] +#[case(u128::MAX - 1, u128::MAX)] +#[case(10, 500)] +fn valid_amount_range_json(#[case] min: u128, #[case] max: u128) { + serde_json::from_value::>(json!({ + "minimum": U128(min), + "maximum": U128(max), + })) + .unwrap(); +} diff --git a/common/src/event.rs b/common/src/market/event.rs similarity index 100% rename from common/src/event.rs rename to common/src/market/event.rs diff --git a/common/src/market/impl.rs b/common/src/market/impl.rs index b2134740..f71f8fc6 100644 --- a/common/src/market/impl.rs +++ b/common/src/market/impl.rs @@ -10,8 +10,7 @@ use crate::{ asset_op, borrow::{BorrowPosition, BorrowPositionGuard, BorrowPositionRef}, chunked_append_only_list::ChunkedAppendOnlyList, - event::MarketEvent, - market::{MarketConfiguration, WithdrawalResolution}, + market::{event::MarketEvent, MarketConfiguration, WithdrawalResolution}, number::Decimal, snapshot::Snapshot, static_yield::StaticYieldRecord, diff --git a/common/src/market/mod.rs b/common/src/market/mod.rs index f76bd0cd..8396d393 100644 --- a/common/src/market/mod.rs +++ b/common/src/market/mod.rs @@ -6,6 +6,8 @@ use near_sdk::{env, near, AccountId}; use crate::{asset::BorrowAssetAmount, number::Decimal}; mod configuration; pub use configuration::{MarketConfiguration, APY_LIMIT}; +mod event; +pub use event::*; mod external; pub use external::*; mod r#impl; diff --git a/common/src/oracle/price_transformer.rs b/common/src/oracle/price_transformer.rs index 9a87914b..7768a522 100644 --- a/common/src/oracle/price_transformer.rs +++ b/common/src/oracle/price_transformer.rs @@ -7,6 +7,9 @@ use crate::number::Decimal; use super::pyth::{self, PriceIdentifier}; +#[cfg(test)] +mod tests; + #[derive(Clone, Debug, PartialEq, Eq)] #[near(serializers = [json, borsh])] pub enum Action { @@ -93,35 +96,3 @@ impl PriceTransformer { } } } - -#[cfg(test)] -mod tests { - use crate::dec; - - use super::*; - - #[test] - fn price_transformation() { - let transformation = Action::NormalizeNativeLstPrice { decimals: 24 }; - let price_before = pyth::Price { - price: 1234.into(), - conf: 4.into(), - expo: 5, - publish_time: 0.into(), - }; - - let price_after = transformation - .apply(price_before, dec!("1.2").mul_pow10(24).unwrap()) - .unwrap(); - - assert_eq!( - price_after, - pyth::Price { - price: 1480.into(), - conf: 5.into(), - expo: 5, - publish_time: 0.into(), - }, - ); - } -} diff --git a/common/src/oracle/price_transformer/tests.rs b/common/src/oracle/price_transformer/tests.rs new file mode 100644 index 00000000..a23a327f --- /dev/null +++ b/common/src/oracle/price_transformer/tests.rs @@ -0,0 +1,28 @@ +use crate::dec; + +use super::*; + +#[test] +fn price_transformation() { + let transformation = Action::NormalizeNativeLstPrice { decimals: 24 }; + let price_before = pyth::Price { + price: 1234.into(), + conf: 4.into(), + expo: 5, + publish_time: 0.into(), + }; + + let price_after = transformation + .apply(price_before, dec!("1.2").mul_pow10(24).unwrap()) + .unwrap(); + + assert_eq!( + price_after, + pyth::Price { + price: 1480.into(), + conf: 5.into(), + expo: 5, + publish_time: 0.into(), + }, + ); +} diff --git a/common/src/borrow.rs b/common/src/position/borrow.rs similarity index 98% rename from common/src/borrow.rs rename to common/src/position/borrow.rs index 6af4eed8..5edabf0d 100644 --- a/common/src/borrow.rs +++ b/common/src/position/borrow.rs @@ -3,13 +3,14 @@ use std::ops::{Deref, DerefMut}; use near_sdk::{env, json_types::U64, near, AccountId}; use crate::{ - accumulator::{AccumulationRecord, Accumulator}, - asset::{BorrowAsset, BorrowAssetAmount, CollateralAssetAmount}, asset_op, - event::MarketEvent, - market::Market, + data::{ + accumulator::{AccumulationRecord, Accumulator}, + asset::{BorrowAsset, BorrowAssetAmount, CollateralAssetAmount}, + price::PricePair, + }, + market::{Market, MarketEvent}, number::Decimal, - price::PricePair, MS_IN_A_YEAR, }; diff --git a/common/src/position/mod.rs b/common/src/position/mod.rs new file mode 100644 index 00000000..0136a8c5 --- /dev/null +++ b/common/src/position/mod.rs @@ -0,0 +1,2 @@ +pub mod borrow; +pub mod supply; diff --git a/common/src/supply.rs b/common/src/position/supply.rs similarity index 99% rename from common/src/supply.rs rename to common/src/position/supply.rs index 380d2307..77692299 100644 --- a/common/src/supply.rs +++ b/common/src/position/supply.rs @@ -6,8 +6,7 @@ use crate::{ accumulator::{AccumulationRecord, Accumulator}, asset::{BorrowAsset, BorrowAssetAmount, FungibleAssetAmount}, asset_op, - event::MarketEvent, - market::{Market, WithdrawalResolution}, + market::{Market, MarketEvent, WithdrawalResolution}, number::Decimal, }; diff --git a/script/sow.sh b/script/sow.sh new file mode 100755 index 00000000..14266740 --- /dev/null +++ b/script/sow.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Requires https://github.com/AlDanial/cloc + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && cd .. && pwd)" +REL_DIR=$(realpath -s --relative-to="${PWD}" "$ROOT_DIR") + +cloc \ + $REL_DIR/common \ + $REL_DIR/contract \ + $REL_DIR/contract/lst-oracle \ + $REL_DIR/contract/market \ + $REL_DIR/contract/registry \ + --exclude-dir=tests \ + --not-match-f=tests.rs \ + --counted=sow.txt