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::unwrap_used,
40    clippy::expect_used,
41    clippy::panic,
42    clippy::arithmetic_side_effects
43)]
44mod tests {
45    use super::*;
46    use pyra_types::Vault;
47
48    fn make_vault(
49        spend_limit_per_transaction: u64,
50        spend_limit_per_timeframe: u64,
51        remaining_spend_limit_per_timeframe: u64,
52        next_timeframe_reset_timestamp: u64,
53        timeframe_in_seconds: u64,
54    ) -> Vault {
55        Vault {
56            owner: vec![0; 32],
57            bump: 0,
58            spend_limit_per_transaction,
59            spend_limit_per_timeframe,
60            remaining_spend_limit_per_timeframe,
61            next_timeframe_reset_timestamp,
62            timeframe_in_seconds,
63        }
64    }
65
66    // --- usdc_base_units_to_cents ---
67
68    #[test]
69    fn cents_basic() {
70        assert_eq!(usdc_base_units_to_cents(1_000_000).unwrap(), 100); // 1 USDC = 100 cents
71    }
72
73    #[test]
74    fn cents_zero() {
75        assert_eq!(usdc_base_units_to_cents(0).unwrap(), 0);
76    }
77
78    #[test]
79    fn cents_sub_cent_truncates() {
80        assert_eq!(usdc_base_units_to_cents(9_999).unwrap(), 0); // < 1 cent
81        assert_eq!(usdc_base_units_to_cents(10_000).unwrap(), 1); // exactly 1 cent
82    }
83
84    // --- calculate_spend_limit_cents ---
85
86    const NOW: u64 = 1_700_000_000;
87
88    #[test]
89    fn spend_limit_basic() {
90        let vault = make_vault(
91            10_000_000, // 10 USDC per tx
92            50_000_000, // 50 USDC per timeframe
93            30_000_000, // 30 USDC remaining
94            NOW + 3600, // active timeframe
95            86_400,
96        );
97        let limit = calculate_spend_limit_cents(&vault, 10_000, NOW).unwrap();
98        assert_eq!(limit, 1000);
99    }
100
101    #[test]
102    fn spend_limit_expired_timeframe_uses_full() {
103        let vault = make_vault(
104            100_000_000, // 100 USDC per tx
105            50_000_000,  // 50 USDC per timeframe
106            10_000_000,  // 10 USDC remaining (ignored because expired)
107            NOW - 100,   // expired
108            86_400,
109        );
110        let limit = calculate_spend_limit_cents(&vault, 100_000, NOW).unwrap();
111        assert_eq!(limit, 5000);
112    }
113
114    #[test]
115    fn spend_limit_capped_by_max() {
116        let vault = make_vault(
117            1_000_000_000, // 1000 USDC per tx
118            1_000_000_000, // 1000 USDC per timeframe
119            1_000_000_000, // 1000 USDC remaining
120            NOW + 3600,
121            86_400,
122        );
123        let limit = calculate_spend_limit_cents(&vault, 500, NOW).unwrap();
124        assert_eq!(limit, 500);
125    }
126
127    #[test]
128    fn spend_limit_zero_timeframe() {
129        let vault = make_vault(10_000_000, 50_000_000, 30_000_000, NOW + 3600, 0);
130        let limit = calculate_spend_limit_cents(&vault, 10_000, NOW).unwrap();
131        assert_eq!(limit, 0);
132    }
133}
134
135#[cfg(test)]
136#[allow(
137    clippy::unwrap_used,
138    clippy::expect_used,
139    clippy::panic,
140    clippy::arithmetic_side_effects
141)]
142mod proptests {
143    use super::*;
144    use proptest::prelude::*;
145
146    proptest! {
147        #[test]
148        fn usdc_cents_never_exceeds_base_units(base_units in 0u64..=u64::MAX) {
149            let cents = usdc_base_units_to_cents(base_units).unwrap();
150            prop_assert!(cents <= base_units, "cents {} > base_units {}", cents, base_units);
151        }
152    }
153}