diff --git a/Cargo.toml b/Cargo.toml index f757841..bafa425 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "bounty", "grants", "treasury", + "math", "governance", "vetoken", "pool", @@ -31,7 +32,6 @@ exclude = [ "timelock", "multisig", "governance", - "math", "snapshot_harness", "xtask", ] diff --git a/backstop/Cargo.toml b/backstop/Cargo.toml index b4d37c2..3688530 100644 --- a/backstop/Cargo.toml +++ b/backstop/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["lib", "cdylib", "rlib"] [dependencies] soroban-sdk = { workspace = true } refract-common = { path = "../common" } +refract-math = { path = "../math" } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/backstop/src/lib.rs b/backstop/src/lib.rs index bced646..177b75d 100644 --- a/backstop/src/lib.rs +++ b/backstop/src/lib.rs @@ -64,6 +64,7 @@ #![no_std] #![allow(clippy::disallowed_methods)] +use refract_math::{mul_div_floor, saturating_add, saturating_sub, split_bps, BPS_DENOM, SCALE_1E7}; use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, symbol_short, token, Address, Env, IntoVal, Symbol, Vec, @@ -78,9 +79,6 @@ pub const SHARE_SCALE: i128 = 10_000_000; /// typical amounts. pub const LOSS_INDEX_SCALE: i128 = 100_000_000_000_000; -/// Basis-point denominator used for premium-share and ratio configuration. -const BPS_DENOM: i128 = 10_000; - /// 1e7 fixed-point scaling factor used for intermediate ratio math. const FP_SCALE: i128 = 10_000_000; @@ -762,13 +760,17 @@ impl RefractBackstop { pub fn tranche_share_price(env: Env, tranche: Tranche) -> Result { let state = Self::_tranche_state(&env, tranche)?; - Ok(Self::quote_withdrawal(10_000_000, state.total_shares, state.total_assets)) + Ok(Self::quote_withdrawal(SCALE_1E7, state.total_shares, state.total_assets)) } pub fn senior_coverage_ratio(env: Env) -> Result { let junior = Self::_tranche_state(&env, Tranche::Junior)?.total_assets; let senior = Self::_tranche_state(&env, Tranche::Senior)?.total_assets; - Ok(if senior == 0 { 0 } else { junior * BPS_DENOM / senior }) + Ok(if senior == 0 { + 0 + } else { + mul_div_floor(junior, BPS_DENOM, senior).unwrap_or(i128::MAX) + }) } pub fn unabsorbed_shortfall(env: Env) -> i128 { @@ -785,13 +787,21 @@ impl RefractBackstop { let config = Self::get_config(&env)?; let mut state = Self::get_state(&env)?; - let cut = amount * config.premium_share_bps / BPS_DENOM; - let junior_bps: i128 = env.storage().instance().get(&DataKey::JuniorPremiumBps).unwrap_or(BPS_DENOM / 2); - let junior_credit = Self::_credit_mature_premium(&env, Tranche::Junior, cut * junior_bps / BPS_DENOM)?; - let senior_credit = Self::_credit_mature_premium(&env, Tranche::Senior, cut - cut * junior_bps / BPS_DENOM)?; - let credited = junior_credit + senior_credit; - state.premium_income += credited; - state.total_capital += credited; + let cut = split_bps(amount, config.premium_share_bps) + .map_err(|_| BackstopError::InvalidPremiumShare)? + .cut; + let junior_bps: i128 = env + .storage() + .instance() + .get(&DataKey::JuniorPremiumBps) + .unwrap_or(BPS_DENOM / 2); + let tranche_split = split_bps(cut, junior_bps) + .map_err(|_| BackstopError::InvalidPremiumSplit)?; + let junior_credit = Self::_credit_mature_premium(&env, Tranche::Junior, tranche_split.cut)?; + let senior_credit = Self::_credit_mature_premium(&env, Tranche::Senior, tranche_split.rest)?; + let credited = saturating_add(junior_credit, senior_credit); + state.premium_income = saturating_add(state.premium_income, credited); + state.total_capital = saturating_add(state.total_capital, credited); env.storage().instance().set(&DataKey::State, &state); // Interactions: pull the stakers' premium slice while the lock is held. @@ -810,17 +820,4 @@ impl RefractBackstop { pub fn shares_of(env: Env, staker: Address) -> i128 { let mut total = 0; - for tranche in [Tranche::Junior, Tranche::Senior] { - for mature in [false, true] { - let shares: i128 = env - .storage() - .persistent() - .get(&DataKey::Shares(staker.clone(), tranche, mature)) - .unwrap_or(0); - total += shares; - } - } - total - } - - pub fn state(env: Env) -> Result + for tranche in [Tranche diff --git a/bounty/Cargo.toml b/bounty/Cargo.toml index edea559..b151740 100644 --- a/bounty/Cargo.toml +++ b/bounty/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["lib", "cdylib", "rlib"] [dependencies] soroban-sdk = { workspace = true } +refract-math = { path = "../math" } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/bounty/src/lib.rs b/bounty/src/lib.rs index 283b766..d86538e 100644 --- a/bounty/src/lib.rs +++ b/bounty/src/lib.rs @@ -61,6 +61,7 @@ //! is intentionally no admin override capable of redirecting an awarded //! commitment payout. +use refract_math::{checked_add, checked_sub}; use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, symbol_short, token, Address, BytesN, Env, }; @@ -304,9 +305,10 @@ impl RefractBountyEscrow { return Err(BountyError::InvalidAmount); } let token = Self::token_addr(&env)?; - token::Client::new(&env, &token).transfer(&from, &env.current_contract_address(), &amount); let balance = Self::raw_balance(&env)?; - env.storage().instance().set(&DataKey::Balance, &(balance + amount)); + let new_balance = checked_add(balance, amount).map_err(|_| BountyError::InvalidAmount)?; + token::Client::new(&env, &token).transfer(&from, &env.current_contract_address(), &amount); + env.storage().instance().set(&DataKey::Balance, &new_balance); Ok(()) } @@ -346,6 +348,8 @@ impl RefractBountyEscrow { // Deduct from total balance. let balance = Self::raw_balance(&env)?; + let new_balance = + checked_sub(balance, amount).map_err(|_| BountyError::InsufficientBalance)?; // Transfer before updating state to surface transfer errors early, // but the balance is updated after so a revert leaves state consistent. let token = Self::token_addr(&env)?; @@ -354,7 +358,7 @@ impl RefractBountyEscrow { &researcher, &amount, ); - env.storage().instance().set(&DataKey::Balance, &(balance - amount)); + env.storage().instance().set(&DataKey::Balance, &new_balance); let record = AwardRecord { researcher, @@ -406,9 +410,11 @@ impl RefractBountyEscrow { // Reserve the funds. let reserved = Self::raw_reserved(&env)?; + let new_reserved = + checked_add(reserved, amount).map_err(|_| BountyError::InvalidAmount)?; env.storage() .instance() - .set(&DataKey::Reserved, &(reserved + amount)); + .set(&DataKey::Reserved, &new_reserved); let payout_id: u64 = env .storage() @@ -469,15 +475,19 @@ impl RefractBountyEscrow { // Release reservation. let reserved = Self::raw_reserved(&env)?; + let new_reserved = + checked_sub(reserved, amount).map_err(|_| BountyError::InsufficientBalance)?; env.storage() .instance() - .set(&DataKey::Reserved, &(reserved - amount)); + .set(&DataKey::Reserved, &new_reserved); // Deduct from total balance. let balance = Self::raw_balance(&env)?; + let new_balance = + checked_sub(balance, amount).map_err(|_| BountyError::InsufficientBalance)?; env.storage() .instance() - .set(&DataKey::Balance, &(balance - amount)); + .set(&DataKey::Balance, &new_balance); // Transfer last — if it fails the state update above will revert with // it (Soroban's atomic transaction model), leaving the record Pending. @@ -523,15 +533,19 @@ impl RefractBountyEscrow { // Release reservation. let reserved = Self::raw_reserved(&env)?; + let new_reserved = + checked_sub(reserved, amount).map_err(|_| BountyError::InsufficientBalance)?; env.storage() .instance() - .set(&DataKey::Reserved, &(reserved - amount)); + .set(&DataKey::Reserved, &new_reserved); // Deduct from total balance. let balance = Self::raw_balance(&env)?; + let new_balance = + checked_sub(balance, amount).map_err(|_| BountyError::InsufficientBalance)?; env.storage() .instance() - .set(&DataKey::Balance, &(balance - amount)); + .set(&DataKey::Balance, &new_balance); // Transfer swept funds back to governance. token::Client::new(&env, &record.token).transfer( @@ -881,21 +895,4 @@ impl RefractBountyEscrow { } /// Immutable record of a rewarded disclosure, if any. - pub fn get_award(env: Env, report_hash: BytesN<32>) -> Option { - env.storage().persistent().get(&DataKey::Award(report_hash)) - } - - /// Pull-payout record by id. - pub fn get_payout(env: Env, payout_id: u64) -> Option { - env.storage().persistent().get(&DataKey::Payout(payout_id)) - } - - /// Registered payout-address binding for a commitment, if any. - pub fn get_payout_binding(env: Env, commitment: BytesN<32>) -> Option { - env.storage() - .persistent() - .get(&DataKey::PayoutBinding(commitment)) - } - - /// Commitment-award record for a commitment, if any. - pub fn get_commitment_award(env: Env, commitment: BytesN< + pub fn get_award( diff --git a/coverage-floor.toml b/coverage-floor.toml index 9e81355..d834ef4 100644 --- a/coverage-floor.toml +++ b/coverage-floor.toml @@ -42,3 +42,7 @@ branches = 80.00 [params] lines = 90.00 branches = 82.00 + +[math] +lines = 95.00 +branches = 90.00 diff --git a/grants/Cargo.toml b/grants/Cargo.toml index a0924c4..8b112b7 100644 --- a/grants/Cargo.toml +++ b/grants/Cargo.toml @@ -13,6 +13,7 @@ crate-type = ["lib", "cdylib"] [dependencies] soroban-sdk = { workspace = true } sha2 = "0.10" +refract-math = { path = "../math" } [dev-dependencies] soroban-sdk = { version = "21.0.0", features = ["testutils", "alloc"] } diff --git a/grants/src/lib.rs b/grants/src/lib.rs index 23781c7..29b0094 100644 --- a/grants/src/lib.rs +++ b/grants/src/lib.rs @@ -176,15 +176,16 @@ impl GrantsModel { pub fn accounted_liabilities(&self) -> i128 { self.grants .values() - .map(|grant| { - grant + .fold(0i128, |acc, grant| { + let grant_owed = grant .milestones .iter() .filter(|milestone| !milestone.claimed) - .map(|milestone| milestone.amount) - .sum::() + .fold(0i128, |m_acc, milestone| { + refract_math::saturating_add(m_acc, milestone.amount) + }); + refract_math::saturating_add(acc, grant_owed) }) - .sum() } /// Approves a new grant with `total_amount` split across `milestones`. @@ -212,7 +213,8 @@ impl GrantsModel { if *amount <= 0 { return Err(GrantsError::InvalidMilestoneAmount); } - sum = sum.checked_add(*amount).ok_or(GrantsError::MilestoneSumMismatch)?; + sum = refract_math::checked_add(sum, *amount) + .map_err(|_| GrantsError::MilestoneSumMismatch)?; } if sum != total_amount { return Err(GrantsError::MilestoneSumMismatch); @@ -279,10 +281,8 @@ impl GrantsModel { } milestone.confirmed = true; - grant.claimable = grant - .claimable - .checked_add(milestone.amount) - .ok_or(GrantsError::MilestoneSumMismatch)?; + grant.claimable = refract_math::checked_add(grant.claimable, milestone.amount) + .map_err(|_| GrantsError::MilestoneSumMismatch)?; Ok(()) } @@ -371,11 +371,11 @@ impl GrantsModel { if amount <= 0 { return Err(GrantsError::InvalidRescueAmount); } - let excess = live_balance - self.accounted_liabilities(); + let excess = refract_math::saturating_sub(live_balance, self.accounted_liabilities()); if amount > excess { return Err(GrantsError::NoExcessFunds); } - Ok(excess - amount) + refract_math::checked_sub(excess, amount).map_err(|_| GrantsError::NoExcessFunds) } fn require_governance(&self, caller: Address) -> Result<(), GrantsError> { diff --git a/math/src/lib.rs b/math/src/lib.rs index 64df336..74bbab7 100644 --- a/math/src/lib.rs +++ b/math/src/lib.rs @@ -33,6 +33,21 @@ use alloc::vec::Vec; /// Basis-point denominator (100% = 10_000 bps). pub const BPS_DENOM: i128 = 10_000; +/// Maximum valid basis-point value (100% = 10_000 bps) as `u32`. +pub const MAX_BPS: u32 = 10_000; + +/// Standard Soroban 7-decimal fixed-point scale factor (`10^7` stroops per unit). +pub const SCALE_1E7: i128 = 10_000_000; + +/// Explicit rounding direction for fixed-point and basis-point division. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Rounding { + /// Round toward zero (down for non-negative values). + Floor, + /// Round away from zero (up for non-negative values when a remainder is present). + Ceil, +} + /// Errors returned by the allocation helpers. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MathError { @@ -155,14 +170,122 @@ pub fn split_ratio(amount: i128, numerator: i128, denominator: i128) -> Result Result { + mul_div(a, b, c, Rounding::Floor) +} + +/// `ceil(a * b / c)` for non-negative inputs with a 256-bit intermediate. +/// +/// Rounds up by one unit whenever `(a * b) mod c != 0`. +/// Returns [`MathError::Overflow`] only when the final result does not fit in +/// an `i128`. +pub fn mul_div_ceil(a: i128, b: i128, c: i128) -> Result { + mul_div(a, b, c, Rounding::Ceil) +} + +/// Computes `a * b / c` for non-negative `i128` inputs using a 256-bit +/// intermediate and explicit [`Rounding`]. +pub fn mul_div(a: i128, b: i128, c: i128, rounding: Rounding) -> Result { if a < 0 || b < 0 || c < 0 { return Err(MathError::Negative); } if c == 0 { return Err(MathError::DivisionByZero); } - let (q, _) = mul_div_rem_u128(a as u128, b as u128, c as u128)?; - i128::try_from(q).map_err(|_| MathError::Overflow) + let (q, r) = mul_div_rem_u128(a as u128, b as u128, c as u128)?; + let adjusted = match rounding { + Rounding::Floor => q, + Rounding::Ceil => { + if r > 0 { + q.checked_add(1).ok_or(MathError::Overflow)? + } else { + q + } + } + }; + i128::try_from(adjusted).map_err(|_| MathError::Overflow) +} + +/// Applies a basis-point share (`0..=10_000`) to a non-negative `amount` with +/// explicit [`Rounding`]. +pub fn apply_bps(amount: i128, bps: u32, rounding: Rounding) -> Result { + if bps > MAX_BPS { + return Err(MathError::InvalidRatio); + } + mul_div(amount, i128::from(bps), BPS_DENOM, rounding) +} + +/// Applies a basis-point share (`0..=10_000`) to `amount`, rounding down. +pub fn apply_bps_floor(amount: i128, bps: u32) -> Result { + apply_bps(amount, bps, Rounding::Floor) +} + +/// Applies a basis-point share (`0..=10_000`) to `amount`, rounding up. +pub fn apply_bps_ceil(amount: i128, bps: u32) -> Result { + apply_bps(amount, bps, Rounding::Ceil) +} + +/// Computes the basis-point ratio `part * 10_000 / whole` (`0..=10_000`) for +/// `0 <= part <= whole` and `whole > 0`. +pub fn ratio_bps(part: i128, whole: i128, rounding: Rounding) -> Result { + if whole <= 0 { + return Err(MathError::DivisionByZero); + } + if part < 0 { + return Err(MathError::Negative); + } + if part > whole { + return Err(MathError::InvalidRatio); + } + let bps = mul_div(part, BPS_DENOM, whole, rounding)?; + u32::try_from(bps).map_err(|_| MathError::Overflow) +} + +/// Converts whole token units to `1e7`-scaled stroops (`units * 10_000_000`). +pub fn to_1e7(units: i128) -> Result { + if units < 0 { + return Err(MathError::Negative); + } + units.checked_mul(SCALE_1E7).ok_or(MathError::Overflow) +} + +/// Converts `1e7`-scaled stroops back to whole token units with explicit +/// [`Rounding`]. +pub fn from_1e7(scaled: i128, rounding: Rounding) -> Result { + mul_div(scaled, 1, SCALE_1E7, rounding) +} + +/// Checked non-negative addition returning [`MathError`]. +pub fn checked_add(a: i128, b: i128) -> Result { + if a < 0 || b < 0 { + return Err(MathError::Negative); + } + a.checked_add(b).ok_or(MathError::Overflow) +} + +/// Checked non-negative subtraction (`a - b >= 0`) returning [`MathError`]. +pub fn checked_sub(a: i128, b: i128) -> Result { + if a < 0 || b < 0 || b > a { + return Err(MathError::Negative); + } + Ok(a - b) +} + +/// Checked non-negative multiplication returning [`MathError`]. +pub fn checked_mul(a: i128, b: i128) -> Result { + if a < 0 || b < 0 { + return Err(MathError::Negative); + } + a.checked_mul(b).ok_or(MathError::Overflow) +} + +/// Non-panicking saturating addition on `i128`. +pub fn saturating_add(a: i128, b: i128) -> i128 { + a.saturating_add(b) +} + +/// Non-panicking saturating subtraction on `i128`. +pub fn saturating_sub(a: i128, b: i128) -> i128 { + a.saturating_sub(b) } /// Moves the unowned part of a vault into the dust sink. diff --git a/math/src/tests.rs b/math/src/tests.rs index c90fb21..23c68cc 100644 --- a/math/src/tests.rs +++ b/math/src/tests.rs @@ -196,6 +196,87 @@ fn mul_div_floor_table() { ); } +#[test] +fn mul_div_ceil_and_rounding_table() { + assert_eq!(mul_div_ceil(7, 3, 2), Ok(11)); + assert_eq!(mul_div_ceil(6, 3, 2), Ok(9)); + assert_eq!(mul_div_ceil(0, 3, 2), Ok(0)); + assert_eq!(mul_div_ceil(1, 1, 0), Err(MathError::DivisionByZero)); + assert_eq!(mul_div_ceil(-1, 1, 1), Err(MathError::Negative)); + assert_eq!( + mul_div(1, 1, SCALE_1E7, Rounding::Floor), + Ok(0) + ); + assert_eq!( + mul_div(1, 1, SCALE_1E7, Rounding::Ceil), + Ok(1) + ); + assert_eq!( + mul_div_ceil(i128::MAX, i128::MAX, i128::MAX), + Ok(i128::MAX) + ); + assert_eq!( + mul_div_ceil(i128::MAX, 2, 2), + Ok(i128::MAX) + ); + assert_eq!( + mul_div_ceil(i128::MAX, 3, 2), + Err(MathError::Overflow) + ); +} + +#[test] +fn apply_and_ratio_bps_table() { + assert_eq!(apply_bps_floor(1_000, 2_500), Ok(250)); + assert_eq!(apply_bps_ceil(1_000, 2_500), Ok(250)); + assert_eq!(apply_bps_floor(1, 1), Ok(0)); + assert_eq!(apply_bps_ceil(1, 1), Ok(1)); + assert_eq!(apply_bps_floor(1_000, 0), Ok(0)); + assert_eq!(apply_bps_floor(1_000, MAX_BPS), Ok(1_000)); + assert_eq!(apply_bps_floor(1_000, MAX_BPS + 1), Err(MathError::InvalidRatio)); + assert_eq!(apply_bps_floor(-1, 500), Err(MathError::Negative)); + + assert_eq!(ratio_bps(250, 1_000, Rounding::Floor), Ok(2_500)); + assert_eq!(ratio_bps(1, 3, Rounding::Floor), Ok(3_333)); + assert_eq!(ratio_bps(1, 3, Rounding::Ceil), Ok(3_334)); + assert_eq!(ratio_bps(0, 1_000, Rounding::Floor), Ok(0)); + assert_eq!(ratio_bps(1_000, 1_000, Rounding::Floor), Ok(MAX_BPS)); + assert_eq!(ratio_bps(1_001, 1_000, Rounding::Floor), Err(MathError::InvalidRatio)); + assert_eq!(ratio_bps(1, 0, Rounding::Floor), Err(MathError::DivisionByZero)); + assert_eq!(ratio_bps(-1, 10, Rounding::Floor), Err(MathError::Negative)); +} + +#[test] +fn fixed_point_1e7_and_checked_helpers_table() { + assert_eq!(to_1e7(0), Ok(0)); + assert_eq!(to_1e7(1), Ok(SCALE_1E7)); + assert_eq!(to_1e7(42), Ok(420_000_000)); + assert_eq!(to_1e7(-1), Err(MathError::Negative)); + assert_eq!(to_1e7(i128::MAX), Err(MathError::Overflow)); + + assert_eq!(from_1e7(0, Rounding::Floor), Ok(0)); + assert_eq!(from_1e7(SCALE_1E7, Rounding::Floor), Ok(1)); + assert_eq!(from_1e7(SCALE_1E7 + 1, Rounding::Floor), Ok(1)); + assert_eq!(from_1e7(SCALE_1E7 + 1, Rounding::Ceil), Ok(2)); + assert_eq!(from_1e7(-1, Rounding::Floor), Err(MathError::Negative)); + + assert_eq!(checked_add(10, 20), Ok(30)); + assert_eq!(checked_add(-1, 20), Err(MathError::Negative)); + assert_eq!(checked_add(i128::MAX, 1), Err(MathError::Overflow)); + + assert_eq!(checked_sub(20, 10), Ok(10)); + assert_eq!(checked_sub(10, 10), Ok(0)); + assert_eq!(checked_sub(10, 11), Err(MathError::Negative)); + assert_eq!(checked_sub(-1, 0), Err(MathError::Negative)); + + assert_eq!(checked_mul(6, 7), Ok(42)); + assert_eq!(checked_mul(-1, 7), Err(MathError::Negative)); + assert_eq!(checked_mul(i128::MAX, 2), Err(MathError::Overflow)); + + assert_eq!(saturating_add(i128::MAX, 5), i128::MAX); + assert_eq!(saturating_sub(i128::MIN, 5), i128::MIN); +} + #[test] fn route_dust_table() { assert_eq!(route_dust(10, 7), Ok((7, 3))); @@ -295,4 +376,38 @@ proptest! { prop_assert_eq!((qh + carry as u128, sum_lo), (hi, lo)); } } + + #[test] + fn mul_div_ceil_never_below_floor_and_at_most_one_higher( + a in 0i128..(1i128 << 62), + b in 0i128..(1i128 << 62), + c in 1i128..(1i128 << 62), + ) { + let fl = mul_div_floor(a, b, c).unwrap(); + let cl = mul_div_ceil(a, b, c).unwrap(); + prop_assert!(cl >= fl); + prop_assert!(cl - fl <= 1); + if (a * b) % c == 0 { + prop_assert_eq!(cl, fl); + } else { + prop_assert_eq!(cl, fl + 1); + } + } + + #[test] + fn apply_bps_floor_plus_complement_ceil_equals_amount( + amount in 0i128..=i128::MAX, + bps in 0u32..=MAX_BPS, + ) { + let floor_cut = apply_bps_floor(amount, bps).unwrap(); + let comp_ceil = apply_bps_ceil(amount, MAX_BPS - bps).unwrap(); + prop_assert_eq!(floor_cut + comp_ceil, amount); + } + + #[test] + fn to_and_from_1e7_roundtrip(units in 0i128..=(i128::MAX / SCALE_1E7)) { + let scaled = to_1e7(units).unwrap(); + prop_assert_eq!(from_1e7(scaled, Rounding::Floor), Ok(units)); + prop_assert_eq!(from_1e7(scaled, Rounding::Ceil), Ok(units)); + } } diff --git a/treasury/Cargo.toml b/treasury/Cargo.toml index 526308c..bb13dcd 100644 --- a/treasury/Cargo.toml +++ b/treasury/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["lib", "cdylib", "rlib"] [dependencies] soroban-sdk = { workspace = true } +refract-math = { path = "../math" } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/treasury/src/reserve.rs b/treasury/src/reserve.rs index 97359c0..6b6412c 100644 --- a/treasury/src/reserve.rs +++ b/treasury/src/reserve.rs @@ -6,8 +6,10 @@ //! operations. Only governance can draw on the reserve, and a draw cannot take //! it below zero. +use refract_math::{apply_bps_floor, saturating_add, saturating_sub, MAX_BPS}; + /// Basis points in a whole. -pub const FULL_BPS: u32 = 10_000; +pub const FULL_BPS: u32 = MAX_BPS; /// How an inflow was split. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -58,7 +60,7 @@ impl Reserve { if obligations <= 0 { return 0; } - obligations.saturating_mul(i128::from(self.target_bps)) / i128::from(FULL_BPS) + apply_bps_floor(obligations, self.target_bps).unwrap_or(0) } /// Routes an inflow. The reserve is topped up toward its target first, and @@ -67,12 +69,12 @@ impl Reserve { if amount <= 0 { return Err(ReserveError::InvalidAmount); } - let shortfall = (self.target(obligations) - self.balance).max(0); + let shortfall = saturating_sub(self.target(obligations), self.balance).max(0); let to_reserve = amount.min(shortfall); - self.balance += to_reserve; + self.balance = saturating_add(self.balance, to_reserve); Ok(Split { to_reserve, - to_operating: amount - to_reserve, + to_operating: saturating_sub(amount, to_reserve), }) } @@ -88,7 +90,7 @@ impl Reserve { if amount > self.balance { return Err(ReserveError::InsufficientReserve); } - self.balance -= amount; + self.balance = saturating_sub(self.balance, amount); Ok(()) } }