soroban_env_host_zephyr/
fees.rs

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