Skip to main content

tycho_executor/phase/
storage.rs

1use anyhow::Result;
2use tycho_types::models::{AccountState, AccountStatus, AccountStatusChange, StoragePhase};
3use tycho_types::num::Tokens;
4
5use crate::ExecutorState;
6use crate::phase::receive::ReceivedMessage;
7
8/// Storage phase input context.
9pub struct StoragePhaseContext<'a> {
10    /// Whether to adjust remaining message balance
11    /// if it becomes greater than the account balance.
12    pub adjust_msg_balance: bool,
13    /// Received message (external or internal).
14    pub received_message: Option<&'a mut ReceivedMessage>,
15}
16
17impl ExecutorState<'_> {
18    /// Storage phase of ordinary or ticktock transactions.
19    ///
20    /// - Precedes the credit phase when [`bounce_enabled`],
21    ///   otherwise must be called after it;
22    /// - Necessary for all types of messages or even without them;
23    /// - Computes storage fee and due payment;
24    /// - Tries to charge the account balance for the storage fees;
25    /// - Freezes or deletes the account if its balance is not enough
26    ///   (doesn't change the state itself, but rather tells other
27    ///   phases to do so).
28    ///
29    /// Returns an executed [`StoragePhase`].
30    ///
31    /// Fails if called in an older context than account's [`last_paid`].
32    /// Can also fail on [`total_fees`] overflow, but this should
33    /// not happen on networks with valid value flow.
34    ///
35    /// [`bounce_enabled`]: ReceivedMessage::bounce_enabled
36    /// [`last_paid`]: tycho_types::models::StorageInfo::last_paid
37    /// [`total_fees`]: Self::total_fees
38    pub fn storage_phase(&mut self, ctx: StoragePhaseContext<'_>) -> Result<StoragePhase> {
39        anyhow::ensure!(
40            self.params.block_unixtime >= self.storage_stat.last_paid,
41            "current unixtime is less than the account last_paid",
42        );
43
44        let is_masterchain = self.address.is_masterchain();
45        let config = self.config.gas_prices(is_masterchain);
46
47        if self.is_suspended_by_marks {
48            // Skip storage phase for accounts suspended by marks.
49            // Authority has frozen the exact amount of tokens so this value
50            // cannot be changed.
51            return Ok(StoragePhase {
52                storage_fees_collected: Tokens::ZERO,
53                storage_fees_due: self.storage_stat.due_payment,
54                status_change: AccountStatusChange::Unchanged,
55            });
56        }
57
58        // Compute how much this account must pay for storing its state up until now.
59        let mut to_pay = self.config.compute_storage_fees(
60            &self.storage_stat,
61            self.params.block_unixtime,
62            self.is_special,
63            is_masterchain,
64        );
65        if let Some(due_payment) = self.storage_stat.due_payment {
66            // NOTE: We are using saturating math here to reduce strange
67            // invariants. If account must pay more than `Tokens::MAX`,
68            // it will certanly be frozen in almost any real scenario.
69            to_pay = to_pay.saturating_add(due_payment);
70        }
71
72        // Update `last_paid` (only for ordinary accounts).
73        self.storage_stat.last_paid = if self.is_special {
74            0
75        } else {
76            self.params.block_unixtime
77        };
78
79        // Start filling the storage phase.
80        let storage_fees_collected;
81        let storage_fees_due;
82        let status_change;
83
84        if to_pay.is_zero() {
85            // No fees at all.
86            storage_fees_collected = Tokens::ZERO;
87            storage_fees_due = None;
88            status_change = AccountStatusChange::Unchanged;
89        } else if let Some(new_balance) = self.balance.tokens.checked_sub(to_pay) {
90            // Account balance is enough to pay storage fees.
91            storage_fees_collected = to_pay;
92            storage_fees_due = None;
93            status_change = AccountStatusChange::Unchanged;
94
95            // Update account balance.
96            self.balance.tokens = new_balance;
97            // Reset `due_payment` if there was any.
98            self.storage_stat.due_payment = None;
99        } else {
100            // Account balance is not enough to pay storage fees,
101            // so we use all of its balance and try to freeze the account.
102            let fees_due = to_pay - self.balance.tokens;
103
104            storage_fees_collected = std::mem::take(&mut self.balance.tokens);
105            storage_fees_due = Some(fees_due).filter(|t| !t.is_zero());
106
107            debug_assert!(self.balance.tokens.is_zero());
108
109            // NOTE: Keep all cases explicit here without "default" branch.
110            status_change = match &self.state {
111                // Do nothing for special accounts.
112                _ if self.is_special => AccountStatusChange::Unchanged,
113                // Try to delete account.
114                AccountState::Uninit | AccountState::Frozen { .. }
115                    if (matches!(&self.state, AccountState::Uninit)
116                        || !self.params.disable_delete_frozen_accounts)
117                        && fees_due.into_inner() > config.delete_due_limit as u128
118                        && !self.balance.other.is_empty() =>
119                {
120                    AccountStatusChange::Deleted
121                }
122                // Do nothing if not deleting.
123                AccountState::Uninit | AccountState::Frozen { .. } => {
124                    AccountStatusChange::Unchanged
125                }
126                // Freeze account with big enough due.
127                AccountState::Active { .. }
128                    if fees_due.into_inner() > config.freeze_due_limit as u128 =>
129                {
130                    AccountStatusChange::Frozen
131                }
132                // Do nothing if `fees_due` is not big enough.
133                AccountState::Active { .. } => AccountStatusChange::Unchanged,
134            };
135
136            if !self.is_special {
137                // Update account's due payment.
138                self.storage_stat.due_payment = storage_fees_due;
139            }
140        };
141
142        // Apply status change.
143        match status_change {
144            AccountStatusChange::Unchanged => {}
145            AccountStatusChange::Frozen => {
146                // NOTE: We are not changing the account state yet, just updating status.
147                self.end_status = AccountStatus::Frozen;
148            }
149            AccountStatusChange::Deleted => {
150                self.end_status = AccountStatus::NotExists;
151            }
152        }
153
154        // Adjust message value.
155        if ctx.adjust_msg_balance
156            && let Some(msg) = ctx.received_message
157            && msg.balance_remaining.tokens > self.balance.tokens
158        {
159            msg.balance_remaining.tokens = self.balance.tokens;
160        }
161
162        // Add fees.
163        self.total_fees.try_add_assign(storage_fees_collected)?;
164
165        // Done
166        Ok(StoragePhase {
167            storage_fees_collected,
168            storage_fees_due,
169            status_change,
170        })
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use std::collections::BTreeMap;
177
178    use tycho_asm_macros::tvmasm;
179    use tycho_types::cell::{CellBuilder, HashBytes};
180    use tycho_types::dict::Dict;
181    use tycho_types::models::{
182        AuthorityMarksConfig, CurrencyCollection, StdAddr, StorageInfo, StorageUsed,
183    };
184    use tycho_types::num::{VarUint56, VarUint248};
185
186    use super::*;
187    use crate::tests::{make_custom_config, make_default_config, make_default_params};
188    use crate::util::shift_ceil_price;
189    use crate::{ExecutorParams, ParsedConfig};
190
191    const STUB_ADDR: StdAddr = StdAddr::new(0, HashBytes::ZERO);
192
193    fn fee_for_storing(used: StorageUsed, delta: u32, config: &ParsedConfig) -> Tokens {
194        let prices = config.storage_prices.last().unwrap();
195        let fee = (prices.bit_price_ps as u128)
196            .saturating_mul(used.bits.into_inner() as u128)
197            .saturating_add(
198                (prices.cell_price_ps as u128).saturating_mul(used.cells.into_inner() as u128),
199            )
200            .saturating_mul(delta as _);
201        Tokens::new(shift_ceil_price(fee))
202    }
203
204    #[test]
205    fn account_has_enough_balance() {
206        let mut params = make_default_params();
207        let config = make_default_config();
208
209        params.block_unixtime = 2000;
210
211        let mut state =
212            ExecutorState::new_uninit(&params, &config, &STUB_ADDR, Tokens::new(1_000_000_000));
213        state.storage_stat = StorageInfo {
214            used: StorageUsed {
215                bits: VarUint56::new(1000),
216                cells: VarUint56::new(10),
217            },
218            storage_extra: Default::default(),
219            last_paid: 1000,
220            due_payment: None,
221        };
222
223        let prev_storage_stat = state.storage_stat.clone();
224        let prev_balance = state.balance.clone();
225        let prev_status = state.end_status;
226        let prev_total_fees = state.total_fees;
227
228        let storage_phase = state
229            .storage_phase(StoragePhaseContext {
230                adjust_msg_balance: false,
231                received_message: None,
232            })
233            .unwrap();
234
235        // Account status must not change.
236        assert_eq!(state.end_status, prev_status);
237        assert_eq!(storage_phase.status_change, AccountStatusChange::Unchanged);
238        // No extra fees must be taken.
239        assert_eq!(state.balance.other, prev_balance.other);
240        assert_eq!(
241            state.balance.tokens,
242            prev_balance.tokens - storage_phase.storage_fees_collected
243        );
244        assert_eq!(
245            state.total_fees,
246            prev_total_fees + storage_phase.storage_fees_collected
247        );
248        // Expect account to pay for storing 1000 bits and 10 cells for 1000 seconds.
249        assert_eq!(
250            storage_phase.storage_fees_collected,
251            fee_for_storing(prev_storage_stat.used.clone(), 1000, &config)
252        );
253        // All fees must be paid.
254        assert!(storage_phase.storage_fees_due.is_none());
255        // Storage stat must be updated.
256        assert_eq!(state.storage_stat.used, prev_storage_stat.used);
257        assert_eq!(state.storage_stat.last_paid, params.block_unixtime);
258        assert!(state.storage_stat.due_payment.is_none());
259    }
260
261    #[test]
262    fn account_does_not_have_enough_balance() {
263        let mut params = make_default_params();
264        let config = make_default_config();
265
266        let mut balance = CurrencyCollection::from(Tokens::new(1));
267        let mut storage_stat = StorageInfo {
268            used: StorageUsed {
269                bits: VarUint56::new(1000),
270                cells: VarUint56::new(10),
271            },
272            storage_extra: Default::default(),
273            last_paid: 1000,
274            due_payment: None,
275        };
276
277        for time in [2000, 3000, 4000] {
278            params.block_unixtime = time;
279
280            let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, balance);
281            state.storage_stat = storage_stat.clone();
282
283            let prev_storage_stat = state.storage_stat.clone();
284            let prev_balance = state.balance.clone();
285            let prev_status = state.end_status;
286            let prev_total_fees = state.total_fees;
287
288            let storage_phase = state
289                .storage_phase(StoragePhaseContext {
290                    adjust_msg_balance: false,
291                    received_message: None,
292                })
293                .unwrap();
294
295            // Account status must not change.
296            assert_eq!(state.end_status, prev_status);
297            assert_eq!(storage_phase.status_change, AccountStatusChange::Unchanged);
298            // Account balance in tokens must be empty.
299            assert_eq!(state.balance.tokens, Tokens::ZERO);
300            // No extra fees must be taken.
301            assert_eq!(state.balance.other, prev_balance.other);
302            assert_eq!(
303                state.balance.tokens,
304                prev_balance.tokens - storage_phase.storage_fees_collected
305            );
306            assert_eq!(
307                state.total_fees,
308                prev_total_fees + storage_phase.storage_fees_collected
309            );
310            // Expect account to pay for storing 1000 bits and 10 cells for 1000 seconds.
311            let delta = params.block_unixtime - prev_storage_stat.last_paid;
312            let prev_due = prev_storage_stat.due_payment.unwrap_or_default();
313            let target_fee = fee_for_storing(prev_storage_stat.used.clone(), delta, &config);
314            assert_eq!(storage_phase.storage_fees_collected, prev_balance.tokens);
315            // All fees must be paid.
316            assert_eq!(
317                storage_phase.storage_fees_due,
318                Some(prev_due + target_fee - prev_balance.tokens)
319            );
320            // Storage stat must be updated.
321            assert_eq!(state.storage_stat.used, prev_storage_stat.used);
322            assert_eq!(state.storage_stat.last_paid, params.block_unixtime);
323            assert_eq!(
324                state.storage_stat.due_payment,
325                Some(prev_due + target_fee - prev_balance.tokens)
326            );
327
328            balance = state.balance;
329            storage_stat = state.storage_stat;
330            println!("storage_stat: {storage_stat:#?}");
331        }
332    }
333
334    #[test]
335    fn account_freezes_with_storage_due() {
336        let mut params = make_default_params();
337        let config = make_default_config();
338
339        params.block_unixtime = 2000;
340
341        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, Tokens::ZERO);
342        state.state = AccountState::Active(Default::default());
343        state.end_status = AccountStatus::Active; // Only active accounts can be frozen.
344        state.storage_stat = StorageInfo {
345            used: StorageUsed {
346                bits: VarUint56::new(1000),
347                cells: VarUint56::new(10),
348            },
349            storage_extra: Default::default(),
350            last_paid: 1000,
351            due_payment: Some(Tokens::new(config.gas_prices.freeze_due_limit as u128 - 50)),
352        };
353
354        let prev_storage_stat = state.storage_stat.clone();
355        let prev_balance = state.balance.clone();
356        let prev_total_fees = state.total_fees;
357
358        let storage_phase = state
359            .storage_phase(StoragePhaseContext {
360                adjust_msg_balance: false,
361                received_message: None,
362            })
363            .unwrap();
364
365        // Account status must not change.
366        assert_eq!(state.end_status, AccountStatus::Frozen);
367        assert_eq!(storage_phase.status_change, AccountStatusChange::Frozen);
368        // Account balance in tokens must be empty.
369        assert_eq!(state.balance.tokens, Tokens::ZERO);
370        // No extra fees must be taken.
371        assert_eq!(state.balance.other, prev_balance.other);
372        assert_eq!(
373            state.balance.tokens,
374            prev_balance.tokens - storage_phase.storage_fees_collected
375        );
376        assert_eq!(
377            state.total_fees,
378            prev_total_fees + storage_phase.storage_fees_collected
379        );
380        // Expect account to pay for storing 1000 bits and 10 cells for 1000 seconds.
381        let delta = params.block_unixtime - prev_storage_stat.last_paid;
382        let prev_due = prev_storage_stat.due_payment.unwrap_or_default();
383        let target_fee = fee_for_storing(prev_storage_stat.used.clone(), delta, &config);
384        assert_eq!(storage_phase.storage_fees_collected, prev_balance.tokens);
385        // All fees must be paid.
386        assert_eq!(
387            storage_phase.storage_fees_due,
388            Some(prev_due + target_fee - prev_balance.tokens)
389        );
390        // Storage stat must be updated.
391        assert_eq!(state.storage_stat.used, prev_storage_stat.used);
392        assert_eq!(state.storage_stat.last_paid, params.block_unixtime);
393        assert_eq!(
394            state.storage_stat.due_payment,
395            Some(prev_due + target_fee - prev_balance.tokens)
396        );
397    }
398
399    #[test]
400    fn suspended_account_storage_phase_skipped() -> anyhow::Result<()> {
401        let params = ExecutorParams {
402            authority_marks_enabled: true,
403            ..make_default_params()
404        };
405
406        let config = make_custom_config(|config| {
407            config.set_authority_marks_config(&AuthorityMarksConfig {
408                authority_addresses: Dict::new(),
409                black_mark_id: 100,
410                white_mark_id: 101,
411            })?;
412            Ok(())
413        });
414
415        let mut state = ExecutorState::new_active(
416            &params,
417            &config,
418            &STUB_ADDR,
419            CurrencyCollection {
420                tokens: Tokens::MAX,
421                other: BTreeMap::from_iter([
422                    (100u32, VarUint248::new(500)), // black marks
423                ])
424                .try_into()?,
425            },
426            CellBuilder::build_from(u32::MIN)?,
427            tvmasm!("ACCEPT"),
428        );
429        state.storage_stat = StorageInfo {
430            used: StorageUsed {
431                bits: VarUint56::new(1000),
432                cells: VarUint56::new(10),
433            },
434            storage_extra: Default::default(),
435            last_paid: 1000,
436            due_payment: None,
437        };
438
439        let prev_storage_stat = state.storage_stat.clone();
440        let prev_balance = state.balance.clone();
441        let prev_status = state.end_status;
442        let prev_total_fees = state.total_fees;
443        let prev_due = state.storage_stat.due_payment;
444
445        let storage_phase = state.storage_phase(StoragePhaseContext {
446            adjust_msg_balance: false,
447            received_message: None,
448        })?;
449
450        // everything should not change
451        assert_eq!(prev_storage_stat, state.storage_stat);
452        assert_eq!(prev_balance, state.balance);
453        assert_eq!(prev_status, state.end_status);
454        assert_eq!(prev_total_fees, state.total_fees);
455
456        assert_eq!(storage_phase.storage_fees_collected, Tokens::ZERO);
457        assert_eq!(storage_phase.storage_fees_due, prev_due);
458        assert_eq!(storage_phase.status_change, AccountStatusChange::Unchanged);
459
460        Ok(())
461    }
462}