clone_solana_rent/
lib.rs

1//! Configuration for network [rent].
2//!
3//! [rent]: https://docs.solanalabs.com/implemented-proposals/rent
4
5#![allow(clippy::arithmetic_side_effects)]
6#![no_std]
7#![cfg_attr(feature = "frozen-abi", feature(min_specialization))]
8#[cfg(feature = "frozen-abi")]
9extern crate std;
10
11#[cfg(feature = "sysvar")]
12pub mod sysvar;
13
14use clone_solana_sdk_macro::CloneZeroed;
15
16// inlined to avoid clone_solana_clock dep
17const DEFAULT_SLOTS_PER_EPOCH: u64 = 432_000;
18#[cfg(test)]
19static_assertions::const_assert_eq!(
20    DEFAULT_SLOTS_PER_EPOCH,
21    clone_solana_clock::DEFAULT_SLOTS_PER_EPOCH
22);
23
24/// Configuration of network rent.
25#[repr(C)]
26#[cfg_attr(
27    feature = "frozen-abi",
28    derive(clone_solana_frozen_abi_macro::AbiExample)
29)]
30#[cfg_attr(
31    feature = "serde",
32    derive(serde_derive::Deserialize, serde_derive::Serialize)
33)]
34#[derive(PartialEq, CloneZeroed, Debug)]
35pub struct Rent {
36    /// Rental rate in lamports/byte-year.
37    pub lamports_per_byte_year: u64,
38
39    /// Amount of time (in years) a balance must include rent for the account to
40    /// be rent exempt.
41    pub exemption_threshold: f64,
42
43    /// The percentage of collected rent that is burned.
44    ///
45    /// Valid values are in the range [0, 100]. The remaining percentage is
46    /// distributed to validators.
47    pub burn_percent: u8,
48}
49
50/// Default rental rate in lamports/byte-year.
51///
52/// This calculation is based on:
53/// - 10^9 lamports per SOL
54/// - $1 per SOL
55/// - $0.01 per megabyte day
56/// - $3.65 per megabyte year
57pub const DEFAULT_LAMPORTS_PER_BYTE_YEAR: u64 = 1_000_000_000 / 100 * 365 / (1024 * 1024);
58
59/// Default amount of time (in years) the balance has to include rent for the
60/// account to be rent exempt.
61pub const DEFAULT_EXEMPTION_THRESHOLD: f64 = 2.0;
62
63/// Default percentage of collected rent that is burned.
64///
65/// Valid values are in the range [0, 100]. The remaining percentage is
66/// distributed to validators.
67pub const DEFAULT_BURN_PERCENT: u8 = 50;
68
69/// Account storage overhead for calculation of base rent.
70///
71/// This is the number of bytes required to store an account with no data. It is
72/// added to an accounts data length when calculating [`Rent::minimum_balance`].
73pub const ACCOUNT_STORAGE_OVERHEAD: u64 = 128;
74
75impl Default for Rent {
76    fn default() -> Self {
77        Self {
78            lamports_per_byte_year: DEFAULT_LAMPORTS_PER_BYTE_YEAR,
79            exemption_threshold: DEFAULT_EXEMPTION_THRESHOLD,
80            burn_percent: DEFAULT_BURN_PERCENT,
81        }
82    }
83}
84
85impl Rent {
86    /// Calculate how much rent to burn from the collected rent.
87    ///
88    /// The first value returned is the amount burned. The second is the amount
89    /// to distribute to validators.
90    pub fn calculate_burn(&self, rent_collected: u64) -> (u64, u64) {
91        let burned_portion = (rent_collected * u64::from(self.burn_percent)) / 100;
92        (burned_portion, rent_collected - burned_portion)
93    }
94
95    /// Minimum balance due for rent-exemption of a given account data size.
96    pub fn minimum_balance(&self, data_len: usize) -> u64 {
97        let bytes = data_len as u64;
98        (((ACCOUNT_STORAGE_OVERHEAD + bytes) * self.lamports_per_byte_year) as f64
99            * self.exemption_threshold) as u64
100    }
101
102    /// Whether a given balance and data length would be exempt.
103    pub fn is_exempt(&self, balance: u64, data_len: usize) -> bool {
104        balance >= self.minimum_balance(data_len)
105    }
106
107    /// Rent due on account's data length with balance.
108    pub fn due(&self, balance: u64, data_len: usize, years_elapsed: f64) -> RentDue {
109        if self.is_exempt(balance, data_len) {
110            RentDue::Exempt
111        } else {
112            RentDue::Paying(self.due_amount(data_len, years_elapsed))
113        }
114    }
115
116    /// Rent due for account that is known to be not exempt.
117    pub fn due_amount(&self, data_len: usize, years_elapsed: f64) -> u64 {
118        let actual_data_len = data_len as u64 + ACCOUNT_STORAGE_OVERHEAD;
119        let lamports_per_year = self.lamports_per_byte_year * actual_data_len;
120        (lamports_per_year as f64 * years_elapsed) as u64
121    }
122
123    /// Creates a `Rent` that charges no lamports.
124    ///
125    /// This is used for testing.
126    pub fn free() -> Self {
127        Self {
128            lamports_per_byte_year: 0,
129            ..Rent::default()
130        }
131    }
132
133    /// Creates a `Rent` that is scaled based on the number of slots in an epoch.
134    ///
135    /// This is used for testing.
136    pub fn with_slots_per_epoch(slots_per_epoch: u64) -> Self {
137        let ratio = slots_per_epoch as f64 / DEFAULT_SLOTS_PER_EPOCH as f64;
138        let exemption_threshold = DEFAULT_EXEMPTION_THRESHOLD * ratio;
139        let lamports_per_byte_year = (DEFAULT_LAMPORTS_PER_BYTE_YEAR as f64 / ratio) as u64;
140        Self {
141            lamports_per_byte_year,
142            exemption_threshold,
143            ..Self::default()
144        }
145    }
146}
147
148/// The return value of [`Rent::due`].
149#[derive(Debug, Copy, Clone, Eq, PartialEq)]
150pub enum RentDue {
151    /// Used to indicate the account is rent exempt.
152    Exempt,
153    /// The account owes this much rent.
154    Paying(u64),
155}
156
157impl RentDue {
158    /// Return the lamports due for rent.
159    pub fn lamports(&self) -> u64 {
160        match self {
161            RentDue::Exempt => 0,
162            RentDue::Paying(x) => *x,
163        }
164    }
165
166    /// Return 'true' if rent exempt.
167    pub fn is_exempt(&self) -> bool {
168        match self {
169            RentDue::Exempt => true,
170            RentDue::Paying(_) => false,
171        }
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_due() {
181        let default_rent = Rent::default();
182
183        assert_eq!(
184            default_rent.due(0, 2, 1.2),
185            RentDue::Paying(
186                (((2 + ACCOUNT_STORAGE_OVERHEAD) * DEFAULT_LAMPORTS_PER_BYTE_YEAR) as f64 * 1.2)
187                    as u64
188            ),
189        );
190        assert_eq!(
191            default_rent.due(
192                (((2 + ACCOUNT_STORAGE_OVERHEAD) * DEFAULT_LAMPORTS_PER_BYTE_YEAR) as f64
193                    * DEFAULT_EXEMPTION_THRESHOLD) as u64,
194                2,
195                1.2
196            ),
197            RentDue::Exempt,
198        );
199
200        let custom_rent = Rent {
201            lamports_per_byte_year: 5,
202            exemption_threshold: 2.5,
203            ..Rent::default()
204        };
205
206        assert_eq!(
207            custom_rent.due(0, 2, 1.2),
208            RentDue::Paying(
209                (((2 + ACCOUNT_STORAGE_OVERHEAD) * custom_rent.lamports_per_byte_year) as f64 * 1.2)
210                    as u64,
211            )
212        );
213
214        assert_eq!(
215            custom_rent.due(
216                (((2 + ACCOUNT_STORAGE_OVERHEAD) * custom_rent.lamports_per_byte_year) as f64
217                    * custom_rent.exemption_threshold) as u64,
218                2,
219                1.2
220            ),
221            RentDue::Exempt
222        );
223    }
224
225    #[test]
226    fn test_rent_due_lamports() {
227        assert_eq!(RentDue::Exempt.lamports(), 0);
228
229        let amount = 123;
230        assert_eq!(RentDue::Paying(amount).lamports(), amount);
231    }
232
233    #[test]
234    fn test_rent_due_is_exempt() {
235        assert!(RentDue::Exempt.is_exempt());
236        assert!(!RentDue::Paying(0).is_exempt());
237    }
238
239    #[test]
240    fn test_clone() {
241        let rent = Rent {
242            lamports_per_byte_year: 1,
243            exemption_threshold: 2.2,
244            burn_percent: 3,
245        };
246        #[allow(clippy::clone_on_copy)]
247        let cloned_rent = rent.clone();
248        assert_eq!(cloned_rent, rent);
249    }
250}