soroban_env_host/
fees.rs

1/// This module defines the fee computation protocol for Soroban.
2///
3/// This is technically not part of the Soroban host and is provided here for
4/// the sake of sharing between the systems that run Soroban host (such as
5/// Stellar core or Soroban RPC service).
6///
7/// Rough estimate of the base size of any transaction result in the archives
8/// (independent of the transaction envelope size).
9pub const TX_BASE_RESULT_SIZE: u32 = 300;
10/// Estimate for any `TtlEntry` ledger entry
11pub const TTL_ENTRY_SIZE: u32 = 48;
12
13pub const INSTRUCTIONS_INCREMENT: i64 = 10000;
14pub const DATA_SIZE_1KB_INCREMENT: i64 = 1024;
15
16// Minimum effective rent write fee per 1KB.
17pub const MINIMUM_RENT_WRITE_FEE_PER_1KB: i64 = 1000;
18
19// The effective rent cost for the contract code entries will be divided by
20// this value.
21// Note, this is a constant for now, but can become a network setting in the
22// future protocols.
23pub const CODE_ENTRY_RENT_DISCOUNT_FACTOR: i64 = 3;
24
25/// These are the resource upper bounds specified by the Soroban transaction.
26pub struct TransactionResources {
27    /// Number of CPU instructions.
28    pub instructions: u32,
29    /// Number of ledger entries the transaction reads.
30    pub disk_read_entries: u32,
31    /// Number of ledger entries the transaction writes (these are also counted
32    /// as entries that are being read for the sake of the respective fees).
33    pub write_entries: u32,
34    /// Number of bytes read from ledger.
35    pub disk_read_bytes: u32,
36    /// Number of bytes written to ledger.
37    pub write_bytes: u32,
38    /// Size of the contract events XDR.
39    pub contract_events_size_bytes: u32,
40    /// Size of the transaction XDR.
41    pub transaction_size_bytes: u32,
42}
43
44/// Fee-related network configuration.
45///
46/// This should be normally loaded from the ledger, with exception of the
47/// `fee_per_write_1kb`, that has to be computed via `compute_write_fee_per_1kb`
48/// function.
49
50#[derive(Debug, Default, PartialEq, Eq)]
51pub struct FeeConfiguration {
52    /// Fee per `INSTRUCTIONS_INCREMENT=10000` instructions.
53    pub fee_per_instruction_increment: i64,
54    /// Fee per 1 entry read from ledger.
55    pub fee_per_disk_read_entry: i64,
56    /// Fee per 1 entry written to ledger.
57    pub fee_per_write_entry: i64,
58    /// Fee per 1KB read from ledger.
59    pub fee_per_disk_read_1kb: i64,
60    /// Fee per 1KB of state written to the ledger.
61    pub fee_per_write_1kb: i64,
62    /// Fee per 1KB written to history (the history write size is based on
63    /// transaction size and `TX_BASE_RESULT_SIZE`).
64    pub fee_per_historical_1kb: i64,
65    /// Fee per 1KB of contract events written.
66    pub fee_per_contract_event_1kb: i64,
67    /// Fee per 1KB of transaction size.
68    pub fee_per_transaction_size_1kb: i64,
69}
70
71/// Network configuration used to determine the ledger write fee used in rent
72/// computations for the Soroban state.
73///
74/// This should be normally loaded from the ledger.
75#[derive(Debug, Default, PartialEq, Eq)]
76pub struct RentWriteFeeConfiguration {
77    // Write fee grows linearly until the Soroban state reaches this size.
78    pub state_target_size_bytes: i64,
79    // Fee per 1KB write when the state size is 0.
80    pub rent_fee_1kb_state_size_low: i64,
81    // Fee per 1KB write when the Soroban state has reached
82    // `state_target_size_bytes`.
83    pub rent_fee_1kb_state_size_high: i64,
84    // Write fee multiplier for any additional data past the first
85    // `state_target_size_bytes`.
86    pub state_size_rent_fee_growth_factor: u32,
87}
88
89/// Change in a single ledger entry with parameters relevant for rent fee
90/// computations.
91///
92/// This represents the entry state before and after transaction has been
93/// applied.
94pub struct LedgerEntryRentChange {
95    /// Whether this is persistent or temporary entry.
96    pub is_persistent: bool,
97    /// Whether this is a contract code entry.
98    pub is_code_entry: bool,
99    /// In-memory size of the entry in bytes before it has been modified.
100    /// Should be `0` for newly-created entires.
101    pub old_size_bytes: u32,
102    /// In-memory size of the entry in bytes after it has been modified.
103    /// Should be `0` for removed entries.
104    pub new_size_bytes: u32,
105    /// Live until ledger of the entry before it has been modified.
106    /// Should be '0' for newly-created entires.
107    pub old_live_until_ledger: u32,
108    /// Live until ledger of the entry after it has been modified.
109    /// Should be `0` for removed entries.
110    pub new_live_until_ledger: u32,
111}
112
113/// Rent fee-related network configuration.
114///
115/// This should be normally loaded from the ledger, with exception of the
116/// `fee_per_rent_1kb`, that has to be computed via
117/// `compute_rent_write_fee_per_1kb` function.
118
119#[derive(Debug, Default, PartialEq, Eq)]
120pub struct RentFeeConfiguration {
121    // Fee per 1KB written to the ledger.
122    // This is the same value as `fee_per_write_1kb` in `FeeConfiguration`.
123    pub fee_per_write_1kb: i64,
124    /// Fee per 1KB of rented ledger space.
125    /// This has to be computed via `compute_rent_write_fee_per_1kb`.
126    pub fee_per_rent_1kb: i64,
127    /// Fee per 1 entry written to ledger.
128    /// This is the same field as in `FeeConfiguration`.
129    pub fee_per_write_entry: i64,
130    /// Denominator for the total rent fee for persistent storage.
131    ///
132    /// 1 KB of ledger space gets charged `fee_per_write_1kb` every
133    /// `persistent_rent_rate_denominator` ledgers.
134    pub persistent_rent_rate_denominator: i64,
135    /// Denominator for the total rent fee for temporary storage.
136    ///
137    /// This has the same semantics as `persistent_rent_rate_denominator`.
138    pub temporary_rent_rate_denominator: i64,
139}
140
141/// Computes the resource fee for a transaction based on the resource
142/// consumption and the fee-related network configuration.
143///
144/// This can handle unsantized user inputs for `tx_resources`, but expects
145/// sane configuration.
146///
147/// Returns a pair of `(non_refundable_fee, refundable_fee)` that represent
148/// non-refundable and refundable resource fee components respectively.
149pub fn compute_transaction_resource_fee(
150    tx_resources: &TransactionResources,
151    fee_config: &FeeConfiguration,
152) -> (i64, i64) {
153    let compute_fee = compute_fee_per_increment(
154        tx_resources.instructions,
155        fee_config.fee_per_instruction_increment,
156        INSTRUCTIONS_INCREMENT,
157    );
158    let ledger_read_entry_fee: i64 = fee_config
159        .fee_per_disk_read_entry
160        .saturating_mul(tx_resources.disk_read_entries.into());
161    let ledger_write_entry_fee = fee_config
162        .fee_per_write_entry
163        .saturating_mul(tx_resources.write_entries.into());
164    let ledger_read_bytes_fee = compute_fee_per_increment(
165        tx_resources.disk_read_bytes,
166        fee_config.fee_per_disk_read_1kb,
167        DATA_SIZE_1KB_INCREMENT,
168    );
169    let ledger_write_bytes_fee = compute_fee_per_increment(
170        tx_resources.write_bytes,
171        fee_config.fee_per_write_1kb,
172        DATA_SIZE_1KB_INCREMENT,
173    );
174
175    let historical_fee = compute_fee_per_increment(
176        tx_resources
177            .transaction_size_bytes
178            .saturating_add(TX_BASE_RESULT_SIZE),
179        fee_config.fee_per_historical_1kb,
180        DATA_SIZE_1KB_INCREMENT,
181    );
182
183    let events_fee = compute_fee_per_increment(
184        tx_resources.contract_events_size_bytes,
185        fee_config.fee_per_contract_event_1kb,
186        DATA_SIZE_1KB_INCREMENT,
187    );
188
189    let bandwidth_fee = compute_fee_per_increment(
190        tx_resources.transaction_size_bytes,
191        fee_config.fee_per_transaction_size_1kb,
192        DATA_SIZE_1KB_INCREMENT,
193    );
194
195    let refundable_fee = events_fee;
196    let non_refundable_fee = compute_fee
197        .saturating_add(ledger_read_entry_fee)
198        .saturating_add(ledger_write_entry_fee)
199        .saturating_add(ledger_read_bytes_fee)
200        .saturating_add(ledger_write_bytes_fee)
201        .saturating_add(historical_fee)
202        .saturating_add(bandwidth_fee);
203
204    (non_refundable_fee, refundable_fee)
205}
206
207// Helper for clamping values to the range of positive i64, with
208// invalid cases mapped to i64::MAX.
209trait ClampFee {
210    fn clamp_fee(self) -> i64;
211}
212
213impl ClampFee for i64 {
214    fn clamp_fee(self) -> i64 {
215        if self < 0 {
216            // Negatives shouldn't be possible -- they're banned in the logic
217            // that sets most of the configs, and we're only using i64 for XDR
218            // sake, ultimately I think compatibility with java which only has
219            // signed types -- anyway we're assuming i64::MAX is more likely the
220            // safest in-band default-value for erroneous cses, since it's more
221            // likely to fail a tx, than to open a "0 cost tx" DoS vector.
222            i64::MAX
223        } else {
224            self
225        }
226    }
227}
228
229impl ClampFee for i128 {
230    fn clamp_fee(self) -> i64 {
231        if self < 0 {
232            i64::MAX
233        } else {
234            i64::try_from(self).unwrap_or(i64::MAX)
235        }
236    }
237}
238
239/// Computes the effective rent fee per 1 KB of ledger space.
240///
241/// The computed fee should be used in rent fee configuration for
242/// `compute_rent_fee` function.
243///
244/// This depends only on the current Soroban in-memory state size.
245pub fn compute_rent_write_fee_per_1kb(
246    soroban_state_size_bytes: i64,
247    fee_config: &RentWriteFeeConfiguration,
248) -> i64 {
249    let fee_rate_multiplier = fee_config
250        .rent_fee_1kb_state_size_high
251        .saturating_sub(fee_config.rent_fee_1kb_state_size_low)
252        .clamp_fee();
253    let mut rent_write_fee_per_1kb: i64;
254    if soroban_state_size_bytes < fee_config.state_target_size_bytes {
255        // Convert multipliers to i128 to make sure we can handle large bucket list
256        // sizes.
257        rent_write_fee_per_1kb = num_integer::div_ceil(
258            (fee_rate_multiplier as i128).saturating_mul(soroban_state_size_bytes as i128),
259            (fee_config.state_target_size_bytes as i128).max(1),
260        )
261        .clamp_fee();
262        // no clamp_fee here
263        rent_write_fee_per_1kb =
264            rent_write_fee_per_1kb.saturating_add(fee_config.rent_fee_1kb_state_size_low);
265    } else {
266        rent_write_fee_per_1kb = fee_config.rent_fee_1kb_state_size_high;
267        let bucket_list_size_after_reaching_target =
268            soroban_state_size_bytes.saturating_sub(fee_config.state_target_size_bytes);
269        let post_target_fee = num_integer::div_ceil(
270            (fee_rate_multiplier as i128)
271                .saturating_mul(bucket_list_size_after_reaching_target as i128)
272                .saturating_mul(fee_config.state_size_rent_fee_growth_factor as i128),
273            (fee_config.state_target_size_bytes as i128).max(1),
274        )
275        .clamp_fee();
276        rent_write_fee_per_1kb = rent_write_fee_per_1kb.saturating_add(post_target_fee);
277    }
278
279    rent_write_fee_per_1kb.max(MINIMUM_RENT_WRITE_FEE_PER_1KB)
280}
281
282/// Computes the total rent-related fee for the provided ledger entry changes.
283///
284/// The rent-related fees consist of the fees for TTL extensions and fees for
285/// increasing the entry size (with or without TTL extensions).
286///
287/// This cannot handle unsanitized inputs and relies on sane configuration and
288/// ledger changes. This is due to the fact that rent is managed automatically
289/// without user-provided inputs.
290pub fn compute_rent_fee(
291    changed_entries: &[LedgerEntryRentChange],
292    fee_config: &RentFeeConfiguration,
293    current_ledger_seq: u32,
294) -> i64 {
295    let mut fee: i64 = 0;
296    let mut extended_entries: i64 = 0;
297    let mut extended_entry_key_size_bytes: u32 = 0;
298    for e in changed_entries {
299        fee = fee.saturating_add(rent_fee_per_entry_change(e, fee_config, current_ledger_seq));
300        if e.old_live_until_ledger < e.new_live_until_ledger {
301            extended_entries = extended_entries.saturating_add(1);
302            extended_entry_key_size_bytes =
303                extended_entry_key_size_bytes.saturating_add(TTL_ENTRY_SIZE);
304        }
305    }
306    // The TTL extensions need to be written to the ledger. As they have
307    // constant size, we can charge for writing them independently of the actual
308    // entry size.
309    fee = fee.saturating_add(
310        fee_config
311            .fee_per_write_entry
312            .saturating_mul(extended_entries),
313    );
314    fee = fee.saturating_add(compute_fee_per_increment(
315        extended_entry_key_size_bytes,
316        fee_config.fee_per_write_1kb,
317        DATA_SIZE_1KB_INCREMENT,
318    ));
319
320    fee
321}
322
323// Size of half-open range (lo, hi], or None if lo>hi
324fn exclusive_ledger_diff(lo: u32, hi: u32) -> Option<u32> {
325    hi.checked_sub(lo)
326}
327
328// Size of closed range [lo, hi] or None if lo>hi
329fn inclusive_ledger_diff(lo: u32, hi: u32) -> Option<u32> {
330    exclusive_ledger_diff(lo, hi).map(|diff| diff.saturating_add(1))
331}
332
333impl LedgerEntryRentChange {
334    fn entry_is_new(&self) -> bool {
335        self.old_size_bytes == 0 && self.old_live_until_ledger == 0
336    }
337
338    fn extension_ledgers(&self, current_ledger: u32) -> Option<u32> {
339        let ledger_before_extension = if self.entry_is_new() {
340            current_ledger.saturating_sub(1)
341        } else {
342            self.old_live_until_ledger
343        };
344        exclusive_ledger_diff(ledger_before_extension, self.new_live_until_ledger)
345    }
346
347    fn prepaid_ledgers(&self, current_ledger: u32) -> Option<u32> {
348        if self.entry_is_new() {
349            None
350        } else {
351            inclusive_ledger_diff(current_ledger, self.old_live_until_ledger)
352        }
353    }
354
355    fn size_increase(&self) -> Option<u32> {
356        self.new_size_bytes.checked_sub(self.old_size_bytes)
357    }
358}
359
360fn rent_fee_per_entry_change(
361    entry_change: &LedgerEntryRentChange,
362    fee_config: &RentFeeConfiguration,
363    current_ledger: u32,
364) -> i64 {
365    let mut fee: i64 = 0;
366    // If there was a difference-in-expiration, pay for the new ledger range
367    // at the new size.
368    if let Some(rent_ledgers) = entry_change.extension_ledgers(current_ledger) {
369        fee = fee.saturating_add(rent_fee_for_size_and_ledgers(
370            entry_change.is_persistent,
371            entry_change.new_size_bytes,
372            rent_ledgers,
373            fee_config,
374        ));
375    }
376
377    // If there were some ledgers already paid for at an old size, and the size
378    // of the entry increased, those pre-paid ledgers need to pay top-up fees to
379    // account for the change in size.
380    if let (Some(rent_ledgers), Some(entry_size)) = (
381        entry_change.prepaid_ledgers(current_ledger),
382        entry_change.size_increase(),
383    ) {
384        fee = fee.saturating_add(rent_fee_for_size_and_ledgers(
385            entry_change.is_persistent,
386            entry_size,
387            rent_ledgers,
388            fee_config,
389        ));
390    }
391    if entry_change.is_code_entry {
392        fee /= CODE_ENTRY_RENT_DISCOUNT_FACTOR;
393    }
394    fee
395}
396
397fn rent_fee_for_size_and_ledgers(
398    is_persistent: bool,
399    entry_size: u32,
400    rent_ledgers: u32,
401    fee_config: &RentFeeConfiguration,
402) -> i64 {
403    // Multiplication can overflow here - unlike fee computation this can rely
404    // on sane input parameters as rent fee computation does not depend on any
405    // user inputs.
406    let num = (entry_size as i64)
407        .saturating_mul(fee_config.fee_per_rent_1kb)
408        .saturating_mul(rent_ledgers as i64);
409    let storage_coef = if is_persistent {
410        fee_config.persistent_rent_rate_denominator
411    } else {
412        fee_config.temporary_rent_rate_denominator
413    };
414    let denom = DATA_SIZE_1KB_INCREMENT.saturating_mul(storage_coef);
415    num_integer::div_ceil(num, denom.max(1))
416}
417
418fn compute_fee_per_increment(resource_value: u32, fee_rate: i64, increment: i64) -> i64 {
419    let resource_val: i64 = resource_value.into();
420    num_integer::div_ceil(resource_val.saturating_mul(fee_rate), increment.max(1))
421}