1use anyhow::Result;
2use tycho_types::models::{AccountState, AccountStatus, AccountStatusChange, StoragePhase};
3use tycho_types::num::Tokens;
4
5use crate::phase::receive::ReceivedMessage;
6use crate::{ExecutorInspector, ExecutorState, PublicLibraryChange};
7
8pub struct StoragePhaseContext<'a, 'e> {
10 pub adjust_msg_balance: bool,
13 pub received_message: Option<&'a mut ReceivedMessage>,
15 pub inspector: Option<&'a mut ExecutorInspector<'e>>,
17}
18
19impl ExecutorState<'_> {
20 pub fn storage_phase(&mut self, ctx: StoragePhaseContext<'_, '_>) -> Result<StoragePhase> {
41 anyhow::ensure!(
42 self.params.block_unixtime >= self.storage_stat.last_paid,
43 "current unixtime is less than the account last_paid",
44 );
45
46 let is_masterchain = self.address.is_masterchain();
47 let config = self.config.gas_prices(is_masterchain);
48
49 if self.is_suspended_by_marks {
50 return Ok(StoragePhase {
54 storage_fees_collected: Tokens::ZERO,
55 storage_fees_due: self.storage_stat.due_payment,
56 status_change: AccountStatusChange::Unchanged,
57 });
58 }
59
60 let mut to_pay = self.config.compute_storage_fees(
62 &self.storage_stat,
63 self.params.block_unixtime,
64 self.is_special,
65 is_masterchain,
66 );
67 if let Some(due_payment) = self.storage_stat.due_payment {
68 to_pay = to_pay.saturating_add(due_payment);
72 }
73
74 self.storage_stat.last_paid = if self.is_special {
76 0
77 } else {
78 self.params.block_unixtime
79 };
80
81 let storage_fees_collected;
83 let storage_fees_due;
84 let status_change;
85
86 if to_pay.is_zero() {
87 storage_fees_collected = Tokens::ZERO;
89 storage_fees_due = None;
90 status_change = AccountStatusChange::Unchanged;
91 } else if let Some(new_balance) = self.balance.tokens.checked_sub(to_pay) {
92 storage_fees_collected = to_pay;
94 storage_fees_due = None;
95 status_change = AccountStatusChange::Unchanged;
96
97 self.balance.tokens = new_balance;
99 self.storage_stat.due_payment = None;
101 } else {
102 let fees_due = to_pay - self.balance.tokens;
105
106 storage_fees_collected = std::mem::take(&mut self.balance.tokens);
107 storage_fees_due = Some(fees_due).filter(|t| !t.is_zero());
108
109 debug_assert!(self.balance.tokens.is_zero());
110
111 status_change = match &self.state {
113 _ if self.is_special => AccountStatusChange::Unchanged,
115 AccountState::Uninit | AccountState::Frozen { .. }
117 if (matches!(&self.state, AccountState::Uninit)
118 || !self.params.disable_delete_frozen_accounts)
119 && fees_due.into_inner() > config.delete_due_limit as u128
120 && self.balance.other.is_empty() =>
121 {
122 AccountStatusChange::Deleted
123 }
124 AccountState::Uninit | AccountState::Frozen { .. } => {
126 AccountStatusChange::Unchanged
127 }
128 AccountState::Active { .. }
130 if fees_due.into_inner() > config.freeze_due_limit as u128 =>
131 {
132 AccountStatusChange::Frozen
133 }
134 AccountState::Active { .. } => AccountStatusChange::Unchanged,
136 };
137
138 if !self.is_special {
139 self.storage_stat.due_payment = storage_fees_due;
141 }
142 };
143
144 match status_change {
146 AccountStatusChange::Unchanged => {}
147 AccountStatusChange::Frozen => {
148 self.end_status = AccountStatus::Frozen;
150 }
151 AccountStatusChange::Deleted => {
152 self.end_status = AccountStatus::NotExists;
153 }
154 }
155
156 if is_masterchain
158 && status_change != AccountStatusChange::Unchanged
159 && let AccountState::Active(state) = &self.state
160 && let Some(inspector) = ctx.inspector
161 {
162 for entry in state.libraries.values() {
163 let lib = entry?;
164 if lib.public {
165 inspector
166 .public_libs_diff
167 .insert(*lib.root.repr_hash(), PublicLibraryChange::Remove);
168 }
169 }
170 }
171
172 if ctx.adjust_msg_balance
174 && let Some(msg) = ctx.received_message
175 && msg.balance_remaining.tokens > self.balance.tokens
176 {
177 msg.balance_remaining.tokens = self.balance.tokens;
178 }
179
180 self.total_fees.try_add_assign(storage_fees_collected)?;
182
183 Ok(StoragePhase {
185 storage_fees_collected,
186 storage_fees_due,
187 status_change,
188 })
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use std::collections::BTreeMap;
195
196 use tycho_asm_macros::tvmasm;
197 use tycho_types::cell::{CellBuilder, HashBytes};
198 use tycho_types::dict::Dict;
199 use tycho_types::models::{
200 AuthorityMarksConfig, CurrencyCollection, StdAddr, StorageInfo, StorageUsed,
201 };
202 use tycho_types::num::{VarUint56, VarUint248};
203
204 use super::*;
205 use crate::tests::{make_custom_config, make_default_config, make_default_params};
206 use crate::util::shift_ceil_price;
207 use crate::{ExecutorParams, ParsedConfig};
208
209 const STUB_ADDR: StdAddr = StdAddr::new(0, HashBytes::ZERO);
210
211 fn fee_for_storing(used: StorageUsed, delta: u32, config: &ParsedConfig) -> Tokens {
212 let prices = config.storage_prices.last().unwrap();
213 let fee = (prices.bit_price_ps as u128)
214 .saturating_mul(used.bits.into_inner() as u128)
215 .saturating_add(
216 (prices.cell_price_ps as u128).saturating_mul(used.cells.into_inner() as u128),
217 )
218 .saturating_mul(delta as _);
219 Tokens::new(shift_ceil_price(fee))
220 }
221
222 #[test]
223 fn account_has_enough_balance() {
224 let mut params = make_default_params();
225 let config = make_default_config();
226
227 params.block_unixtime = 2000;
228
229 let mut state =
230 ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, Tokens::new(1_000_000_000));
231 state.storage_stat = StorageInfo {
232 used: StorageUsed {
233 bits: VarUint56::new(1000),
234 cells: VarUint56::new(10),
235 },
236 storage_extra: Default::default(),
237 last_paid: 1000,
238 due_payment: None,
239 };
240
241 let prev_storage_stat = state.storage_stat.clone();
242 let prev_balance = state.balance.clone();
243 let prev_status = state.end_status;
244 let prev_total_fees = state.total_fees;
245
246 let storage_phase = state
247 .storage_phase(StoragePhaseContext {
248 adjust_msg_balance: false,
249 received_message: None,
250 inspector: None,
251 })
252 .unwrap();
253
254 assert_eq!(state.end_status, prev_status);
256 assert_eq!(storage_phase.status_change, AccountStatusChange::Unchanged);
257 assert_eq!(state.balance.other, prev_balance.other);
259 assert_eq!(
260 state.balance.tokens,
261 prev_balance.tokens - storage_phase.storage_fees_collected
262 );
263 assert_eq!(
264 state.total_fees,
265 prev_total_fees + storage_phase.storage_fees_collected
266 );
267 assert_eq!(
269 storage_phase.storage_fees_collected,
270 fee_for_storing(prev_storage_stat.used.clone(), 1000, &config)
271 );
272 assert!(storage_phase.storage_fees_due.is_none());
274 assert_eq!(state.storage_stat.used, prev_storage_stat.used);
276 assert_eq!(state.storage_stat.last_paid, params.block_unixtime);
277 assert!(state.storage_stat.due_payment.is_none());
278 }
279
280 #[test]
281 fn account_does_not_have_enough_balance() {
282 let mut params = make_default_params();
283 let config = make_default_config();
284
285 let mut balance = CurrencyCollection::from(Tokens::new(1));
286 let mut storage_stat = StorageInfo {
287 used: StorageUsed {
288 bits: VarUint56::new(1000),
289 cells: VarUint56::new(10),
290 },
291 storage_extra: Default::default(),
292 last_paid: 1000,
293 due_payment: None,
294 };
295
296 for time in [2000, 3000, 4000] {
297 params.block_unixtime = time;
298
299 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, balance);
300 state.storage_stat = storage_stat.clone();
301
302 let prev_storage_stat = state.storage_stat.clone();
303 let prev_balance = state.balance.clone();
304 let prev_status = state.end_status;
305 let prev_total_fees = state.total_fees;
306
307 let storage_phase = state
308 .storage_phase(StoragePhaseContext {
309 adjust_msg_balance: false,
310 received_message: None,
311 inspector: None,
312 })
313 .unwrap();
314
315 assert_eq!(state.end_status, prev_status);
317 assert_eq!(storage_phase.status_change, AccountStatusChange::Unchanged);
318 assert_eq!(state.balance.tokens, Tokens::ZERO);
320 assert_eq!(state.balance.other, prev_balance.other);
322 assert_eq!(
323 state.balance.tokens,
324 prev_balance.tokens - storage_phase.storage_fees_collected
325 );
326 assert_eq!(
327 state.total_fees,
328 prev_total_fees + storage_phase.storage_fees_collected
329 );
330 let delta = params.block_unixtime - prev_storage_stat.last_paid;
332 let prev_due = prev_storage_stat.due_payment.unwrap_or_default();
333 let target_fee = fee_for_storing(prev_storage_stat.used.clone(), delta, &config);
334 assert_eq!(storage_phase.storage_fees_collected, prev_balance.tokens);
335 assert_eq!(
337 storage_phase.storage_fees_due,
338 Some(prev_due + target_fee - prev_balance.tokens)
339 );
340 assert_eq!(state.storage_stat.used, prev_storage_stat.used);
342 assert_eq!(state.storage_stat.last_paid, params.block_unixtime);
343 assert_eq!(
344 state.storage_stat.due_payment,
345 Some(prev_due + target_fee - prev_balance.tokens)
346 );
347
348 balance = state.balance;
349 storage_stat = state.storage_stat;
350 println!("storage_stat: {storage_stat:#?}");
351 }
352 }
353
354 #[test]
355 fn account_freezes_with_storage_due() {
356 let mut params = make_default_params();
357 let config = make_default_config();
358
359 params.block_unixtime = 2000;
360
361 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, Tokens::ZERO);
362 state.state = AccountState::Active(Default::default());
363 state.end_status = AccountStatus::Active; state.storage_stat = StorageInfo {
365 used: StorageUsed {
366 bits: VarUint56::new(1000),
367 cells: VarUint56::new(10),
368 },
369 storage_extra: Default::default(),
370 last_paid: 1000,
371 due_payment: Some(Tokens::new(config.gas_prices.freeze_due_limit as u128 - 50)),
372 };
373
374 let prev_storage_stat = state.storage_stat.clone();
375 let prev_balance = state.balance.clone();
376 let prev_total_fees = state.total_fees;
377
378 let storage_phase = state
379 .storage_phase(StoragePhaseContext {
380 adjust_msg_balance: false,
381 received_message: None,
382 inspector: None,
383 })
384 .unwrap();
385
386 assert_eq!(state.end_status, AccountStatus::Frozen);
388 assert_eq!(storage_phase.status_change, AccountStatusChange::Frozen);
389 assert_eq!(state.balance.tokens, Tokens::ZERO);
391 assert_eq!(state.balance.other, prev_balance.other);
393 assert_eq!(
394 state.balance.tokens,
395 prev_balance.tokens - storage_phase.storage_fees_collected
396 );
397 assert_eq!(
398 state.total_fees,
399 prev_total_fees + storage_phase.storage_fees_collected
400 );
401 let delta = params.block_unixtime - prev_storage_stat.last_paid;
403 let prev_due = prev_storage_stat.due_payment.unwrap_or_default();
404 let target_fee = fee_for_storing(prev_storage_stat.used.clone(), delta, &config);
405 assert_eq!(storage_phase.storage_fees_collected, prev_balance.tokens);
406 assert_eq!(
408 storage_phase.storage_fees_due,
409 Some(prev_due + target_fee - prev_balance.tokens)
410 );
411 assert_eq!(state.storage_stat.used, prev_storage_stat.used);
413 assert_eq!(state.storage_stat.last_paid, params.block_unixtime);
414 assert_eq!(
415 state.storage_stat.due_payment,
416 Some(prev_due + target_fee - prev_balance.tokens)
417 );
418 }
419
420 #[test]
421 fn suspended_account_storage_phase_skipped() -> anyhow::Result<()> {
422 let params = ExecutorParams {
423 authority_marks_enabled: true,
424 ..make_default_params()
425 };
426
427 let config = make_custom_config(|config| {
428 config.set_authority_marks_config(&AuthorityMarksConfig {
429 authority_addresses: Dict::new(),
430 black_mark_id: 100,
431 white_mark_id: 101,
432 })?;
433 Ok(())
434 });
435
436 let mut state = ExecutorState::new_active(
437 ¶ms,
438 &config,
439 &STUB_ADDR,
440 CurrencyCollection {
441 tokens: Tokens::MAX,
442 other: BTreeMap::from_iter([
443 (100u32, VarUint248::new(500)), ])
445 .try_into()?,
446 },
447 CellBuilder::build_from(u32::MIN)?,
448 tvmasm!("ACCEPT"),
449 );
450 state.storage_stat = StorageInfo {
451 used: StorageUsed {
452 bits: VarUint56::new(1000),
453 cells: VarUint56::new(10),
454 },
455 storage_extra: Default::default(),
456 last_paid: 1000,
457 due_payment: None,
458 };
459
460 let prev_storage_stat = state.storage_stat.clone();
461 let prev_balance = state.balance.clone();
462 let prev_status = state.end_status;
463 let prev_total_fees = state.total_fees;
464 let prev_due = state.storage_stat.due_payment;
465
466 let storage_phase = state.storage_phase(StoragePhaseContext {
467 adjust_msg_balance: false,
468 received_message: None,
469 inspector: None,
470 })?;
471
472 assert_eq!(prev_storage_stat, state.storage_stat);
474 assert_eq!(prev_balance, state.balance);
475 assert_eq!(prev_status, state.end_status);
476 assert_eq!(prev_total_fees, state.total_fees);
477
478 assert_eq!(storage_phase.storage_fees_collected, Tokens::ZERO);
479 assert_eq!(storage_phase.storage_fees_due, prev_due);
480 assert_eq!(storage_phase.status_change, AccountStatusChange::Unchanged);
481
482 Ok(())
483 }
484}