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