mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-06-28 11:10:08 +00:00
parent
bd8064a587
commit
3eab96a217
@ -6,6 +6,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3", features = ["host"] }
|
||||
borsh = { version = "1.5", features = ["derive"] }
|
||||
primitive-types = { version = "0.13", default-features = false }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
twap_oracle_core = { path = "../../twap_oracle/core" }
|
||||
risc0-zkvm = { version = "=3.0.5", default-features = false }
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
//! Core data structures and utilities for the Stablecoin Program.
|
||||
|
||||
pub mod math;
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use nssa_core::{
|
||||
account::{AccountId, AccountWithMetadata, Data},
|
||||
|
||||
214
programs/stablecoin/core/src/math.rs
Normal file
214
programs/stablecoin/core/src/math.rs
Normal file
@ -0,0 +1,214 @@
|
||||
//! Fixed-point arithmetic primitives for the Stablecoin program.
|
||||
//!
|
||||
//! All rate, ratio, and price-multiplier values in the protocol are stored as
|
||||
//! `u128` integers scaled by [`FIXED_POINT_ONE`], so the integer `1.0` is
|
||||
//! `10^27`. Multiplications use `U256` intermediates to avoid overflow.
|
||||
|
||||
use primitive_types::U256;
|
||||
|
||||
/// The value `1.0` in our 27-decimal fixed-point representation.
|
||||
///
|
||||
/// Rate fields store `actual_value * FIXED_POINT_ONE`.
|
||||
pub const FIXED_POINT_ONE: u128 = 10u128.pow(27);
|
||||
|
||||
/// `(a * b) / c` computed via `U256` intermediates and rounded toward zero.
|
||||
///
|
||||
/// # Panics
|
||||
/// - `c == 0` (division by zero).
|
||||
/// - The result exceeds `u128::MAX`.
|
||||
#[must_use]
|
||||
pub fn mul_div(a: u128, b: u128, c: u128) -> u128 {
|
||||
assert!(c != 0, "mul_div: division by zero");
|
||||
let product = U256::from(a)
|
||||
.checked_mul(U256::from(b))
|
||||
.expect("mul_div: intermediate product overflows U256");
|
||||
let quotient = product
|
||||
.checked_div(U256::from(c))
|
||||
.expect("mul_div: division by zero");
|
||||
quotient.try_into().expect("mul_div: result exceeds u128")
|
||||
}
|
||||
|
||||
/// `ceil((a * b) / c)` via `U256` intermediates.
|
||||
///
|
||||
/// # Panics
|
||||
/// - `c == 0`.
|
||||
/// - Result exceeds `u128::MAX`.
|
||||
#[must_use]
|
||||
pub fn mul_div_ceil(a: u128, b: u128, c: u128) -> u128 {
|
||||
assert!(c != 0, "mul_div_ceil: division by zero");
|
||||
let product = U256::from(a)
|
||||
.checked_mul(U256::from(b))
|
||||
.expect("mul_div_ceil: intermediate product overflows U256");
|
||||
let divisor = U256::from(c);
|
||||
let quotient = product
|
||||
.checked_div(divisor)
|
||||
.expect("mul_div_ceil: division by zero");
|
||||
let remainder = product
|
||||
.checked_rem(divisor)
|
||||
.expect("mul_div_ceil: division by zero");
|
||||
let ceiled = if remainder.is_zero() {
|
||||
quotient
|
||||
} else {
|
||||
quotient
|
||||
.checked_add(U256::one())
|
||||
.expect("mul_div_ceil: ceil increment overflows U256")
|
||||
};
|
||||
ceiled
|
||||
.try_into()
|
||||
.expect("mul_div_ceil: result exceeds u128")
|
||||
}
|
||||
|
||||
/// Compute `per_millisecond_rate^milliseconds_elapsed` in fixed-point semantics, where
|
||||
/// `per_millisecond_rate == FIXED_POINT_ONE` represents `1.0`.
|
||||
///
|
||||
/// Algorithm: exponentiation by squaring. `O(log milliseconds_elapsed)`.
|
||||
///
|
||||
/// # Edge cases
|
||||
/// - `milliseconds_elapsed == 0` returns `FIXED_POINT_ONE` (identity).
|
||||
/// - `per_millisecond_rate == FIXED_POINT_ONE` returns `FIXED_POINT_ONE` regardless of
|
||||
/// `milliseconds_elapsed`.
|
||||
///
|
||||
/// # Overflow
|
||||
/// NOT self-bounding. For any `per_millisecond_rate > FIXED_POINT_ONE` this
|
||||
/// eventually overflows `u128` as `milliseconds_elapsed` grows — the §8 rate
|
||||
/// bound alone does not prevent it. Callers MUST clamp the elapsed window to
|
||||
/// `MAXIMUM_COMPOUNDING_WINDOW_MILLISECONDS` (spec §5.3) before calling.
|
||||
#[must_use]
|
||||
pub fn compound_rate(per_millisecond_rate: u128, milliseconds_elapsed: u64) -> u128 {
|
||||
if milliseconds_elapsed == 0 {
|
||||
return FIXED_POINT_ONE;
|
||||
}
|
||||
if per_millisecond_rate == FIXED_POINT_ONE {
|
||||
return FIXED_POINT_ONE;
|
||||
}
|
||||
let mut result = FIXED_POINT_ONE;
|
||||
let mut base = per_millisecond_rate;
|
||||
let mut exponent = milliseconds_elapsed;
|
||||
while exponent > 0 {
|
||||
if exponent & 1 == 1 {
|
||||
result = mul_div(result, base, FIXED_POINT_ONE);
|
||||
}
|
||||
exponent >>= 1;
|
||||
if exponent > 0 {
|
||||
base = mul_div(base, base, FIXED_POINT_ONE);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn fixed_point_one_is_ten_to_the_twenty_seventh() {
|
||||
assert_eq!(FIXED_POINT_ONE, 1_000_000_000_000_000_000_000_000_000_u128);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mul_div_zero_inputs() {
|
||||
assert_eq!(mul_div(0, 5, 3), 0);
|
||||
assert_eq!(mul_div(5, 0, 3), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mul_div_exact() {
|
||||
assert_eq!(mul_div(100, FIXED_POINT_ONE, FIXED_POINT_ONE), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mul_div_floors_remainder() {
|
||||
// 10 * 3 / 4 = 7.5 -> 7
|
||||
assert_eq!(mul_div(10, 3, 4), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mul_div_handles_full_u128() {
|
||||
// a * b would overflow u128 if not promoted to U256.
|
||||
assert_eq!(mul_div(u128::MAX, 2, 4), u128::MAX / 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "mul_div: division by zero")]
|
||||
fn mul_div_panics_on_zero_divisor() {
|
||||
let _ = mul_div(1, 1, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mul_div_ceil_exact_equals_floor() {
|
||||
assert_eq!(mul_div_ceil(100, FIXED_POINT_ONE, FIXED_POINT_ONE), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mul_div_ceil_rounds_up_when_remainder_non_zero() {
|
||||
// 10 * 3 / 4 = 7.5 -> 8
|
||||
assert_eq!(mul_div_ceil(10, 3, 4), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mul_div_ceil_never_less_than_floor() {
|
||||
for &(a, b, c) in &[(7_u128, 3, 4), (101, 99, 100), (FIXED_POINT_ONE, 3, 7)] {
|
||||
assert!(mul_div_ceil(a, b, c) >= mul_div(a, b, c));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "mul_div_ceil: division by zero")]
|
||||
fn mul_div_ceil_panics_on_zero_divisor() {
|
||||
let _ = mul_div_ceil(1, 1, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compound_rate_identity_on_zero_milliseconds() {
|
||||
assert_eq!(compound_rate(FIXED_POINT_ONE * 2, 0), FIXED_POINT_ONE);
|
||||
assert_eq!(compound_rate(123, 0), FIXED_POINT_ONE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compound_rate_one_is_one() {
|
||||
for &milliseconds in &[0u64, 1, 1_000, 86_400_000, 31_536_000_000] {
|
||||
assert_eq!(
|
||||
compound_rate(FIXED_POINT_ONE, milliseconds),
|
||||
FIXED_POINT_ONE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compound_rate_one_millisecond_equals_rate() {
|
||||
let rate = FIXED_POINT_ONE + 1_000_000_000_000_000_000; // 1.001 in fixed-point
|
||||
assert_eq!(compound_rate(rate, 1), rate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compound_rate_two_milliseconds_squares_rate() {
|
||||
// Pick a rate where rate * rate / FIXED_POINT_ONE is easy to verify.
|
||||
let rate = FIXED_POINT_ONE + 10u128.pow(25); // 1.01 in fixed-point
|
||||
let expected = mul_div(rate, rate, FIXED_POINT_ONE);
|
||||
assert_eq!(compound_rate(rate, 2), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compound_rate_growth_is_monotonic_above_one() {
|
||||
let rate = FIXED_POINT_ONE + 10u128.pow(25); // 1.01 in fixed-point
|
||||
let mut prev = FIXED_POINT_ONE;
|
||||
for milliseconds in 0..20u64 {
|
||||
let now = compound_rate(rate, milliseconds);
|
||||
assert!(
|
||||
now >= prev,
|
||||
"compound_rate not monotonic: {milliseconds}ms -> {now} < {prev}"
|
||||
);
|
||||
prev = now;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compound_rate_decay_below_one() {
|
||||
// 0.99 in fixed-point (i.e. rate < 1).
|
||||
let rate = FIXED_POINT_ONE - 10u128.pow(25);
|
||||
// After many milliseconds, should be strictly less than FIXED_POINT_ONE.
|
||||
let result = compound_rate(rate, 100);
|
||||
assert!(result < FIXED_POINT_ONE);
|
||||
assert!(result > 0);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user