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        // Compute how much this account must pay for storing its state up until now.
48        let mut to_pay = self.config.compute_storage_fees(
49            &self.storage_stat,
50            self.params.block_unixtime,
51            self.is_special,
52            is_masterchain,
53        );
54        if let Some(due_payment) = self.storage_stat.due_payment {
55            // NOTE: We are using saturating math here to reduce strange
56            // invariants. If account must pay more than `Tokens::MAX`,
57            // it will certanly be frozen in almost any real scenario.
58            to_pay = to_pay.saturating_add(due_payment);
59        }
60
61        // Update `last_paid` (only for ordinary accounts).
62        self.storage_stat.last_paid = if self.is_special {
63            0
64        } else {
65            self.params.block_unixtime
66        };
67
68        // Start filling the storage phase.
69        let storage_fees_collected;
70        let storage_fees_due;
71        let status_change;
72
73        if to_pay.is_zero() {
74            // No fees at all.
75            storage_fees_collected = Tokens::ZERO;
76            storage_fees_due = None;
77            status_change = AccountStatusChange::Unchanged;
78        } else if let Some(new_balance) = self.balance.tokens.checked_sub(to_pay) {
79            // Account balance is enough to pay storage fees.
80            storage_fees_collected = to_pay;
81            storage_fees_due = None;
82            status_change = AccountStatusChange::Unchanged;
83
84            // Update account balance.
85            self.balance.tokens = new_balance;
86            // Reset `due_payment` if there was any.
87            self.storage_stat.due_payment = None;
88        } else {
89            // Account balance is not enough to pay storage fees,
90            // so we use all of its balance and try to freeze the account.
91            let fees_due = to_pay - self.balance.tokens;
92
93            storage_fees_collected = std::mem::take(&mut self.balance.tokens);
94            storage_fees_due = Some(fees_due).filter(|t| !t.is_zero());
95
96            debug_assert!(self.balance.tokens.is_zero());
97
98            // NOTE: Keep all cases explicit here without "default" branch.
99            status_change = match &self.state {
100                // Do nothing for special accounts.
101                _ if self.is_special => AccountStatusChange::Unchanged,
102                // Try to delete account.
103                AccountState::Uninit | AccountState::Frozen { .. }
104                    if (matches!(&self.state, AccountState::Uninit)
105                        || !self.params.disable_delete_frozen_accounts)
106                        && fees_due.into_inner() > config.delete_due_limit as u128
107                        && !self.balance.other.is_empty() =>
108                {
109                    AccountStatusChange::Deleted
110                }
111                // Do nothing if not deleting.
112                AccountState::Uninit | AccountState::Frozen { .. } => {
113                    AccountStatusChange::Unchanged
114                }
115                // Freeze account with big enough due.
116                AccountState::Active { .. }
117                    if fees_due.into_inner() > config.freeze_due_limit as u128 =>
118                {
119                    AccountStatusChange::Frozen
120                }
121                // Do nothing if `fees_due` is not big enough.
122                AccountState::Active { .. } => AccountStatusChange::Unchanged,
123            };
124
125            if !self.is_special {
126                // Update account's due payment.
127                self.storage_stat.due_payment = storage_fees_due;
128            }
129        };
130
131        // Apply status change.
132        match status_change {
133            AccountStatusChange::Unchanged => {}
134            AccountStatusChange::Frozen => {
135                // NOTE: We are not changing the account state yet, just updating status.
136                self.end_status = AccountStatus::Frozen;
137            }
138            AccountStatusChange::Deleted => {
139                self.end_status = AccountStatus::NotExists;
140            }
141        }
142
143        // Adjust message value.
144        if let Some(msg) = ctx.received_message {
145            if ctx.adjust_msg_balance && msg.balance_remaining.tokens > self.balance.tokens {
146                msg.balance_remaining.tokens = self.balance.tokens;
147            }
148        }
149
150        // Add fees.
151        self.total_fees.try_add_assign(storage_fees_collected)?;
152
153        // Done
154        Ok(StoragePhase {
155            storage_fees_collected,
156            storage_fees_due,
157            status_change,
158        })
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use tycho_types::cell::HashBytes;
165    use tycho_types::models::{CurrencyCollection, StdAddr, StorageInfo, StorageUsed};
166    use tycho_types::num::VarUint56;
167
168    use super::*;
169    use crate::ParsedConfig;
170    use crate::tests::{make_default_config, make_default_params};
171    use crate::util::shift_ceil_price;
172
173    const STUB_ADDR: StdAddr = StdAddr::new(0, HashBytes::ZERO);
174
175    fn fee_for_storing(used: StorageUsed, delta: u32, config: &ParsedConfig) -> Tokens {
176        let prices = config.storage_prices.last().unwrap();
177        let fee = (prices.bit_price_ps as u128)
178            .saturating_mul(used.bits.into_inner() as u128)
179            .saturating_add(
180                (prices.cell_price_ps as u128).saturating_mul(used.cells.into_inner() as u128),
181            )
182            .saturating_mul(delta as _);
183        Tokens::new(shift_ceil_price(fee))
184    }
185
186    #[test]
187    fn account_has_enough_balance() {
188        let mut params = make_default_params();
189        let config = make_default_config();
190
191        params.block_unixtime = 2000;
192
193        let mut state =
194            ExecutorState::new_uninit(&params, &config, &STUB_ADDR, Tokens::new(1_000_000_000));
195        state.storage_stat = StorageInfo {
196            used: StorageUsed {
197                bits: VarUint56::new(1000),
198                cells: VarUint56::new(10),
199            },
200            storage_extra: Default::default(),
201            last_paid: 1000,
202            due_payment: None,
203        };
204
205        let prev_storage_stat = state.storage_stat.clone();
206        let prev_balance = state.balance.clone();
207        let prev_status = state.end_status;
208        let prev_total_fees = state.total_fees;
209
210        let storage_phase = state
211            .storage_phase(StoragePhaseContext {
212                adjust_msg_balance: false,
213                received_message: None,
214            })
215            .unwrap();
216
217        // Account status must not change.
218        assert_eq!(state.end_status, prev_status);
219        assert_eq!(storage_phase.status_change, AccountStatusChange::Unchanged);
220        // No extra fees must be taken.
221        assert_eq!(state.balance.other, prev_balance.other);
222        assert_eq!(
223            state.balance.tokens,
224            prev_balance.tokens - storage_phase.storage_fees_collected
225        );
226        assert_eq!(
227            state.total_fees,
228            prev_total_fees + storage_phase.storage_fees_collected
229        );
230        // Expect account to pay for storing 1000 bits and 10 cells for 1000 seconds.
231        assert_eq!(
232            storage_phase.storage_fees_collected,
233            fee_for_storing(prev_storage_stat.used.clone(), 1000, &config)
234        );
235        // All fees must be paid.
236        assert!(storage_phase.storage_fees_due.is_none());
237        // Storage stat must be updated.
238        assert_eq!(state.storage_stat.used, prev_storage_stat.used);
239        assert_eq!(state.storage_stat.last_paid, params.block_unixtime);
240        assert!(state.storage_stat.due_payment.is_none());
241    }
242
243    #[test]
244    fn account_does_not_have_enough_balance() {
245        let mut params = make_default_params();
246        let config = make_default_config();
247
248        let mut balance = CurrencyCollection::from(Tokens::new(1));
249        let mut storage_stat = StorageInfo {
250            used: StorageUsed {
251                bits: VarUint56::new(1000),
252                cells: VarUint56::new(10),
253            },
254            storage_extra: Default::default(),
255            last_paid: 1000,
256            due_payment: None,
257        };
258
259        for time in [2000, 3000, 4000] {
260            params.block_unixtime = time;
261
262            let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, balance);
263            state.storage_stat = storage_stat.clone();
264
265            let prev_storage_stat = state.storage_stat.clone();
266            let prev_balance = state.balance.clone();
267            let prev_status = state.end_status;
268            let prev_total_fees = state.total_fees;
269
270            let storage_phase = state
271                .storage_phase(StoragePhaseContext {
272                    adjust_msg_balance: false,
273                    received_message: None,
274                })
275                .unwrap();
276
277            // Account status must not change.
278            assert_eq!(state.end_status, prev_status);
279            assert_eq!(storage_phase.status_change, AccountStatusChange::Unchanged);
280            // Account balance in tokens must be empty.
281            assert_eq!(state.balance.tokens, Tokens::ZERO);
282            // No extra fees must be taken.
283            assert_eq!(state.balance.other, prev_balance.other);
284            assert_eq!(
285                state.balance.tokens,
286                prev_balance.tokens - storage_phase.storage_fees_collected
287            );
288            assert_eq!(
289                state.total_fees,
290                prev_total_fees + storage_phase.storage_fees_collected
291            );
292            // Expect account to pay for storing 1000 bits and 10 cells for 1000 seconds.
293            let delta = params.block_unixtime - prev_storage_stat.last_paid;
294            let prev_due = prev_storage_stat.due_payment.unwrap_or_default();
295            let target_fee = fee_for_storing(prev_storage_stat.used.clone(), delta, &config);
296            assert_eq!(storage_phase.storage_fees_collected, prev_balance.tokens);
297            // All fees must be paid.
298            assert_eq!(
299                storage_phase.storage_fees_due,
300                Some(prev_due + target_fee - prev_balance.tokens)
301            );
302            // Storage stat must be updated.
303            assert_eq!(state.storage_stat.used, prev_storage_stat.used);
304            assert_eq!(state.storage_stat.last_paid, params.block_unixtime);
305            assert_eq!(
306                state.storage_stat.due_payment,
307                Some(prev_due + target_fee - prev_balance.tokens)
308            );
309
310            balance = state.balance;
311            storage_stat = state.storage_stat;
312            println!("storage_stat: {storage_stat:#?}");
313        }
314    }
315
316    #[test]
317    fn account_freezes_with_storage_due() {
318        let mut params = make_default_params();
319        let config = make_default_config();
320
321        params.block_unixtime = 2000;
322
323        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, Tokens::ZERO);
324        state.state = AccountState::Active(Default::default());
325        state.end_status = AccountStatus::Active; // Only active accounts can be frozen.
326        state.storage_stat = StorageInfo {
327            used: StorageUsed {
328                bits: VarUint56::new(1000),
329                cells: VarUint56::new(10),
330            },
331            storage_extra: Default::default(),
332            last_paid: 1000,
333            due_payment: Some(Tokens::new(config.gas_prices.freeze_due_limit as u128 - 50)),
334        };
335
336        let prev_storage_stat = state.storage_stat.clone();
337        let prev_balance = state.balance.clone();
338        let prev_total_fees = state.total_fees;
339
340        let storage_phase = state
341            .storage_phase(StoragePhaseContext {
342                adjust_msg_balance: false,
343                received_message: None,
344            })
345            .unwrap();
346
347        // Account status must not change.
348        assert_eq!(state.end_status, AccountStatus::Frozen);
349        assert_eq!(storage_phase.status_change, AccountStatusChange::Frozen);
350        // Account balance in tokens must be empty.
351        assert_eq!(state.balance.tokens, Tokens::ZERO);
352        // No extra fees must be taken.
353        assert_eq!(state.balance.other, prev_balance.other);
354        assert_eq!(
355            state.balance.tokens,
356            prev_balance.tokens - storage_phase.storage_fees_collected
357        );
358        assert_eq!(
359            state.total_fees,
360            prev_total_fees + storage_phase.storage_fees_collected
361        );
362        // Expect account to pay for storing 1000 bits and 10 cells for 1000 seconds.
363        let delta = params.block_unixtime - prev_storage_stat.last_paid;
364        let prev_due = prev_storage_stat.due_payment.unwrap_or_default();
365        let target_fee = fee_for_storing(prev_storage_stat.used.clone(), delta, &config);
366        assert_eq!(storage_phase.storage_fees_collected, prev_balance.tokens);
367        // All fees must be paid.
368        assert_eq!(
369            storage_phase.storage_fees_due,
370            Some(prev_due + target_fee - prev_balance.tokens)
371        );
372        // Storage stat must be updated.
373        assert_eq!(state.storage_stat.used, prev_storage_stat.used);
374        assert_eq!(state.storage_stat.last_paid, params.block_unixtime);
375        assert_eq!(
376            state.storage_stat.due_payment,
377            Some(prev_due + target_fee - prev_balance.tokens)
378        );
379    }
380}