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
8pub struct StoragePhaseContext<'a> {
10 pub adjust_msg_balance: bool,
13 pub received_message: Option<&'a mut ReceivedMessage>,
15}
16
17impl ExecutorState<'_> {
18 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 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 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 to_pay = to_pay.saturating_add(due_payment);
70 }
71
72 self.storage_stat.last_paid = if self.is_special {
74 0
75 } else {
76 self.params.block_unixtime
77 };
78
79 let storage_fees_collected;
81 let storage_fees_due;
82 let status_change;
83
84 if to_pay.is_zero() {
85 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 storage_fees_collected = to_pay;
92 storage_fees_due = None;
93 status_change = AccountStatusChange::Unchanged;
94
95 self.balance.tokens = new_balance;
97 self.storage_stat.due_payment = None;
99 } else {
100 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 status_change = match &self.state {
111 _ if self.is_special => AccountStatusChange::Unchanged,
113 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 AccountState::Uninit | AccountState::Frozen { .. } => {
124 AccountStatusChange::Unchanged
125 }
126 AccountState::Active { .. }
128 if fees_due.into_inner() > config.freeze_due_limit as u128 =>
129 {
130 AccountStatusChange::Frozen
131 }
132 AccountState::Active { .. } => AccountStatusChange::Unchanged,
134 };
135
136 if !self.is_special {
137 self.storage_stat.due_payment = storage_fees_due;
139 }
140 };
141
142 match status_change {
144 AccountStatusChange::Unchanged => {}
145 AccountStatusChange::Frozen => {
146 self.end_status = AccountStatus::Frozen;
148 }
149 AccountStatusChange::Deleted => {
150 self.end_status = AccountStatus::NotExists;
151 }
152 }
153
154 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 self.total_fees.try_add_assign(storage_fees_collected)?;
164
165 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(¶ms, &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 assert_eq!(state.end_status, prev_status);
237 assert_eq!(storage_phase.status_change, AccountStatusChange::Unchanged);
238 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 assert_eq!(
250 storage_phase.storage_fees_collected,
251 fee_for_storing(prev_storage_stat.used.clone(), 1000, &config)
252 );
253 assert!(storage_phase.storage_fees_due.is_none());
255 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(¶ms, &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 assert_eq!(state.end_status, prev_status);
297 assert_eq!(storage_phase.status_change, AccountStatusChange::Unchanged);
298 assert_eq!(state.balance.tokens, Tokens::ZERO);
300 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 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 assert_eq!(
317 storage_phase.storage_fees_due,
318 Some(prev_due + target_fee - prev_balance.tokens)
319 );
320 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(¶ms, &config, &STUB_ADDR, Tokens::ZERO);
342 state.state = AccountState::Active(Default::default());
343 state.end_status = AccountStatus::Active; 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 assert_eq!(state.end_status, AccountStatus::Frozen);
367 assert_eq!(storage_phase.status_change, AccountStatusChange::Frozen);
368 assert_eq!(state.balance.tokens, Tokens::ZERO);
370 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 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 assert_eq!(
387 storage_phase.storage_fees_due,
388 Some(prev_due + target_fee - prev_balance.tokens)
389 );
390 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 ¶ms,
417 &config,
418 &STUB_ADDR,
419 CurrencyCollection {
420 tokens: Tokens::MAX,
421 other: BTreeMap::from_iter([
422 (100u32, VarUint248::new(500)), ])
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 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}