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