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 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 to_pay = to_pay.saturating_add(due_payment);
59 }
60
61 self.storage_stat.last_paid = if self.is_special {
63 0
64 } else {
65 self.params.block_unixtime
66 };
67
68 let storage_fees_collected;
70 let storage_fees_due;
71 let status_change;
72
73 if to_pay.is_zero() {
74 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 storage_fees_collected = to_pay;
81 storage_fees_due = None;
82 status_change = AccountStatusChange::Unchanged;
83
84 self.balance.tokens = new_balance;
86 self.storage_stat.due_payment = None;
88 } else {
89 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 status_change = match &self.state {
100 _ if self.is_special => AccountStatusChange::Unchanged,
102 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 AccountState::Uninit | AccountState::Frozen { .. } => {
113 AccountStatusChange::Unchanged
114 }
115 AccountState::Active { .. }
117 if fees_due.into_inner() > config.freeze_due_limit as u128 =>
118 {
119 AccountStatusChange::Frozen
120 }
121 AccountState::Active { .. } => AccountStatusChange::Unchanged,
123 };
124
125 if !self.is_special {
126 self.storage_stat.due_payment = storage_fees_due;
128 }
129 };
130
131 match status_change {
133 AccountStatusChange::Unchanged => {}
134 AccountStatusChange::Frozen => {
135 self.end_status = AccountStatus::Frozen;
137 }
138 AccountStatusChange::Deleted => {
139 self.end_status = AccountStatus::NotExists;
140 }
141 }
142
143 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 self.total_fees.try_add_assign(storage_fees_collected)?;
152
153 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(¶ms, &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 assert_eq!(state.end_status, prev_status);
219 assert_eq!(storage_phase.status_change, AccountStatusChange::Unchanged);
220 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 assert_eq!(
232 storage_phase.storage_fees_collected,
233 fee_for_storing(prev_storage_stat.used.clone(), 1000, &config)
234 );
235 assert!(storage_phase.storage_fees_due.is_none());
237 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(¶ms, &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 assert_eq!(state.end_status, prev_status);
279 assert_eq!(storage_phase.status_change, AccountStatusChange::Unchanged);
280 assert_eq!(state.balance.tokens, Tokens::ZERO);
282 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 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 assert_eq!(
299 storage_phase.storage_fees_due,
300 Some(prev_due + target_fee - prev_balance.tokens)
301 );
302 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(¶ms, &config, &STUB_ADDR, Tokens::ZERO);
324 state.state = AccountState::Active(Default::default());
325 state.end_status = AccountStatus::Active; 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 assert_eq!(state.end_status, AccountStatus::Frozen);
349 assert_eq!(storage_phase.status_change, AccountStatusChange::Frozen);
350 assert_eq!(state.balance.tokens, Tokens::ZERO);
352 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 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 assert_eq!(
369 storage_phase.storage_fees_due,
370 Some(prev_due + target_fee - prev_balance.tokens)
371 );
372 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}