Skip to main content

pyra_margin/
common.rs

1use std::cmp;
2
3use crate::error::{MathError, MathResult};
4use crate::spend_limits::get_remaining_timeframe_limit;
5
6/// USDC has 6 decimals: 1 USDC = 1_000_000 base units = 100 cents.
7/// So 1 cent = 10_000 base units.
8pub const USDC_BASE_UNITS_PER_CENT: u64 = 10_000;
9
10/// Convert USDC base units to cents.
11///
12/// 1 USDC = 1_000_000 base units = 100 cents, so divide by 10_000.
13pub fn usdc_base_units_to_cents(base_units: u64) -> MathResult<u64> {
14    base_units
15        .checked_div(USDC_BASE_UNITS_PER_CENT)
16        .ok_or(MathError::Overflow)
17}
18
19/// Calculate spend limit in cents from vault state.
20///
21/// Returns `min(transaction_limit, timeframe_remaining, max_cap)` in cents.
22///
23/// `now_unix` is the current Unix timestamp in seconds (e.g. `Utc::now().timestamp() as u64`).
24pub fn calculate_spend_limit_cents(
25    vault: &pyra_types::Vault,
26    max_transaction_limit_cents: u64,
27    now_unix: u64,
28) -> MathResult<u64> {
29    let timeframe_base_units = get_remaining_timeframe_limit(vault, now_unix);
30    let transaction_limit_cents = usdc_base_units_to_cents(vault.spend_limit_per_transaction)?;
31    let timeframe_remaining_cents = usdc_base_units_to_cents(timeframe_base_units)?;
32
33    let spend_limit_no_cap = cmp::min(transaction_limit_cents, timeframe_remaining_cents);
34    Ok(cmp::min(spend_limit_no_cap, max_transaction_limit_cents))
35}
36
37#[cfg(test)]
38#[allow(
39    clippy::allow_attributes,
40    clippy::allow_attributes_without_reason,
41    clippy::unwrap_used,
42    clippy::expect_used,
43    clippy::panic,
44    clippy::arithmetic_side_effects,
45    reason = "test code"
46)]
47mod tests {
48    use super::*;
49    use pyra_types::Vault;
50
51    fn make_vault(
52        spend_limit_per_transaction: u64,
53        spend_limit_per_timeframe: u64,
54        remaining_spend_limit_per_timeframe: u64,
55        next_timeframe_reset_timestamp: u64,
56        timeframe_in_seconds: u64,
57    ) -> Vault {
58        Vault {
59            owner: vec![0; 32],
60            bump: 0,
61            spend_limit_per_transaction,
62            spend_limit_per_timeframe,
63            remaining_spend_limit_per_timeframe,
64            next_timeframe_reset_timestamp,
65            timeframe_in_seconds,
66        }
67    }
68
69    // --- usdc_base_units_to_cents ---
70
71    #[test]
72    fn cents_basic() {
73        assert_eq!(usdc_base_units_to_cents(1_000_000).unwrap(), 100); // 1 USDC = 100 cents
74    }
75
76    #[test]
77    fn cents_zero() {
78        assert_eq!(usdc_base_units_to_cents(0).unwrap(), 0);
79    }
80
81    #[test]
82    fn cents_sub_cent_truncates() {
83        assert_eq!(usdc_base_units_to_cents(9_999).unwrap(), 0); // < 1 cent
84        assert_eq!(usdc_base_units_to_cents(10_000).unwrap(), 1); // exactly 1 cent
85    }
86
87    // --- calculate_spend_limit_cents ---
88
89    const NOW: u64 = 1_700_000_000;
90
91    #[test]
92    fn spend_limit_basic() {
93        let vault = make_vault(
94            10_000_000, // 10 USDC per tx
95            50_000_000, // 50 USDC per timeframe
96            30_000_000, // 30 USDC remaining
97            NOW + 3600, // active timeframe
98            86_400,
99        );
100        let limit = calculate_spend_limit_cents(&vault, 10_000, NOW).unwrap();
101        assert_eq!(limit, 1000);
102    }
103
104    #[test]
105    fn spend_limit_expired_timeframe_uses_full() {
106        let vault = make_vault(
107            100_000_000, // 100 USDC per tx
108            50_000_000,  // 50 USDC per timeframe
109            10_000_000,  // 10 USDC remaining (ignored because expired)
110            NOW - 100,   // expired
111            86_400,
112        );
113        let limit = calculate_spend_limit_cents(&vault, 100_000, NOW).unwrap();
114        assert_eq!(limit, 5000);
115    }
116
117    #[test]
118    fn spend_limit_capped_by_max() {
119        let vault = make_vault(
120            1_000_000_000, // 1000 USDC per tx
121            1_000_000_000, // 1000 USDC per timeframe
122            1_000_000_000, // 1000 USDC remaining
123            NOW + 3600,
124            86_400,
125        );
126        let limit = calculate_spend_limit_cents(&vault, 500, NOW).unwrap();
127        assert_eq!(limit, 500);
128    }
129
130    #[test]
131    fn spend_limit_zero_timeframe() {
132        let vault = make_vault(10_000_000, 50_000_000, 30_000_000, NOW + 3600, 0);
133        let limit = calculate_spend_limit_cents(&vault, 10_000, NOW).unwrap();
134        assert_eq!(limit, 0);
135    }
136}
137
138#[cfg(test)]
139#[allow(
140    clippy::allow_attributes,
141    clippy::allow_attributes_without_reason,
142    clippy::unwrap_used,
143    clippy::expect_used,
144    clippy::panic,
145    clippy::arithmetic_side_effects,
146    reason = "test code"
147)]
148mod proptests {
149    use super::*;
150    use proptest::prelude::*;
151
152    proptest! {
153        #[test]
154        fn usdc_cents_never_exceeds_base_units(base_units in 0u64..=u64::MAX) {
155            let cents = usdc_base_units_to_cents(base_units).unwrap();
156            prop_assert!(cents <= base_units, "cents {} > base_units {}", cents, base_units);
157        }
158    }
159}