use anyhow::Result;
use tycho_types::models::{AccountState, AccountStatus, AccountStatusChange, StoragePhase};
use tycho_types::num::Tokens;
use crate::ExecutorState;
use crate::phase::receive::ReceivedMessage;
pub struct StoragePhaseContext<'a> {
pub adjust_msg_balance: bool,
pub received_message: Option<&'a mut ReceivedMessage>,
}
impl ExecutorState<'_> {
pub fn storage_phase(&mut self, ctx: StoragePhaseContext<'_>) -> Result<StoragePhase> {
anyhow::ensure!(
self.params.block_unixtime >= self.storage_stat.last_paid,
"current unixtime is less than the account last_paid",
);
let is_masterchain = self.address.is_masterchain();
let config = self.config.gas_prices(is_masterchain);
if self.is_suspended_by_marks {
return Ok(StoragePhase {
storage_fees_collected: Tokens::ZERO,
storage_fees_due: self.storage_stat.due_payment,
status_change: AccountStatusChange::Unchanged,
});
}
let mut to_pay = self.config.compute_storage_fees(
&self.storage_stat,
self.params.block_unixtime,
self.is_special,
is_masterchain,
);
if let Some(due_payment) = self.storage_stat.due_payment {
to_pay = to_pay.saturating_add(due_payment);
}
self.storage_stat.last_paid = if self.is_special {
0
} else {
self.params.block_unixtime
};
let storage_fees_collected;
let storage_fees_due;
let status_change;
if to_pay.is_zero() {
storage_fees_collected = Tokens::ZERO;
storage_fees_due = None;
status_change = AccountStatusChange::Unchanged;
} else if let Some(new_balance) = self.balance.tokens.checked_sub(to_pay) {
storage_fees_collected = to_pay;
storage_fees_due = None;
status_change = AccountStatusChange::Unchanged;
self.balance.tokens = new_balance;
self.storage_stat.due_payment = None;
} else {
let fees_due = to_pay - self.balance.tokens;
storage_fees_collected = std::mem::take(&mut self.balance.tokens);
storage_fees_due = Some(fees_due).filter(|t| !t.is_zero());
debug_assert!(self.balance.tokens.is_zero());
status_change = match &self.state {
_ if self.is_special => AccountStatusChange::Unchanged,
AccountState::Uninit | AccountState::Frozen { .. }
if (matches!(&self.state, AccountState::Uninit)
|| !self.params.disable_delete_frozen_accounts)
&& fees_due.into_inner() > config.delete_due_limit as u128
&& !self.balance.other.is_empty() =>
{
AccountStatusChange::Deleted
}
AccountState::Uninit | AccountState::Frozen { .. } => {
AccountStatusChange::Unchanged
}
AccountState::Active { .. }
if fees_due.into_inner() > config.freeze_due_limit as u128 =>
{
AccountStatusChange::Frozen
}
AccountState::Active { .. } => AccountStatusChange::Unchanged,
};
if !self.is_special {
self.storage_stat.due_payment = storage_fees_due;
}
};
match status_change {
AccountStatusChange::Unchanged => {}
AccountStatusChange::Frozen => {
self.end_status = AccountStatus::Frozen;
}
AccountStatusChange::Deleted => {
self.end_status = AccountStatus::NotExists;
}
}
if ctx.adjust_msg_balance
&& let Some(msg) = ctx.received_message
&& msg.balance_remaining.tokens > self.balance.tokens
{
msg.balance_remaining.tokens = self.balance.tokens;
}
self.total_fees.try_add_assign(storage_fees_collected)?;
Ok(StoragePhase {
storage_fees_collected,
storage_fees_due,
status_change,
})
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use tycho_asm_macros::tvmasm;
use tycho_types::cell::{CellBuilder, HashBytes};
use tycho_types::dict::Dict;
use tycho_types::models::{
AuthorityMarksConfig, CurrencyCollection, StdAddr, StorageInfo, StorageUsed,
};
use tycho_types::num::{VarUint56, VarUint248};
use super::*;
use crate::tests::{make_custom_config, make_default_config, make_default_params};
use crate::util::shift_ceil_price;
use crate::{ExecutorParams, ParsedConfig};
const STUB_ADDR: StdAddr = StdAddr::new(0, HashBytes::ZERO);
fn fee_for_storing(used: StorageUsed, delta: u32, config: &ParsedConfig) -> Tokens {
let prices = config.storage_prices.last().unwrap();
let fee = (prices.bit_price_ps as u128)
.saturating_mul(used.bits.into_inner() as u128)
.saturating_add(
(prices.cell_price_ps as u128).saturating_mul(used.cells.into_inner() as u128),
)
.saturating_mul(delta as _);
Tokens::new(shift_ceil_price(fee))
}
#[test]
fn account_has_enough_balance() {
let mut params = make_default_params();
let config = make_default_config();
params.block_unixtime = 2000;
let mut state =
ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, Tokens::new(1_000_000_000));
state.storage_stat = StorageInfo {
used: StorageUsed {
bits: VarUint56::new(1000),
cells: VarUint56::new(10),
},
storage_extra: Default::default(),
last_paid: 1000,
due_payment: None,
};
let prev_storage_stat = state.storage_stat.clone();
let prev_balance = state.balance.clone();
let prev_status = state.end_status;
let prev_total_fees = state.total_fees;
let storage_phase = state
.storage_phase(StoragePhaseContext {
adjust_msg_balance: false,
received_message: None,
})
.unwrap();
assert_eq!(state.end_status, prev_status);
assert_eq!(storage_phase.status_change, AccountStatusChange::Unchanged);
assert_eq!(state.balance.other, prev_balance.other);
assert_eq!(
state.balance.tokens,
prev_balance.tokens - storage_phase.storage_fees_collected
);
assert_eq!(
state.total_fees,
prev_total_fees + storage_phase.storage_fees_collected
);
assert_eq!(
storage_phase.storage_fees_collected,
fee_for_storing(prev_storage_stat.used.clone(), 1000, &config)
);
assert!(storage_phase.storage_fees_due.is_none());
assert_eq!(state.storage_stat.used, prev_storage_stat.used);
assert_eq!(state.storage_stat.last_paid, params.block_unixtime);
assert!(state.storage_stat.due_payment.is_none());
}
#[test]
fn account_does_not_have_enough_balance() {
let mut params = make_default_params();
let config = make_default_config();
let mut balance = CurrencyCollection::from(Tokens::new(1));
let mut storage_stat = StorageInfo {
used: StorageUsed {
bits: VarUint56::new(1000),
cells: VarUint56::new(10),
},
storage_extra: Default::default(),
last_paid: 1000,
due_payment: None,
};
for time in [2000, 3000, 4000] {
params.block_unixtime = time;
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, balance);
state.storage_stat = storage_stat.clone();
let prev_storage_stat = state.storage_stat.clone();
let prev_balance = state.balance.clone();
let prev_status = state.end_status;
let prev_total_fees = state.total_fees;
let storage_phase = state
.storage_phase(StoragePhaseContext {
adjust_msg_balance: false,
received_message: None,
})
.unwrap();
assert_eq!(state.end_status, prev_status);
assert_eq!(storage_phase.status_change, AccountStatusChange::Unchanged);
assert_eq!(state.balance.tokens, Tokens::ZERO);
assert_eq!(state.balance.other, prev_balance.other);
assert_eq!(
state.balance.tokens,
prev_balance.tokens - storage_phase.storage_fees_collected
);
assert_eq!(
state.total_fees,
prev_total_fees + storage_phase.storage_fees_collected
);
let delta = params.block_unixtime - prev_storage_stat.last_paid;
let prev_due = prev_storage_stat.due_payment.unwrap_or_default();
let target_fee = fee_for_storing(prev_storage_stat.used.clone(), delta, &config);
assert_eq!(storage_phase.storage_fees_collected, prev_balance.tokens);
assert_eq!(
storage_phase.storage_fees_due,
Some(prev_due + target_fee - prev_balance.tokens)
);
assert_eq!(state.storage_stat.used, prev_storage_stat.used);
assert_eq!(state.storage_stat.last_paid, params.block_unixtime);
assert_eq!(
state.storage_stat.due_payment,
Some(prev_due + target_fee - prev_balance.tokens)
);
balance = state.balance;
storage_stat = state.storage_stat;
println!("storage_stat: {storage_stat:#?}");
}
}
#[test]
fn account_freezes_with_storage_due() {
let mut params = make_default_params();
let config = make_default_config();
params.block_unixtime = 2000;
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, Tokens::ZERO);
state.state = AccountState::Active(Default::default());
state.end_status = AccountStatus::Active; state.storage_stat = StorageInfo {
used: StorageUsed {
bits: VarUint56::new(1000),
cells: VarUint56::new(10),
},
storage_extra: Default::default(),
last_paid: 1000,
due_payment: Some(Tokens::new(config.gas_prices.freeze_due_limit as u128 - 50)),
};
let prev_storage_stat = state.storage_stat.clone();
let prev_balance = state.balance.clone();
let prev_total_fees = state.total_fees;
let storage_phase = state
.storage_phase(StoragePhaseContext {
adjust_msg_balance: false,
received_message: None,
})
.unwrap();
assert_eq!(state.end_status, AccountStatus::Frozen);
assert_eq!(storage_phase.status_change, AccountStatusChange::Frozen);
assert_eq!(state.balance.tokens, Tokens::ZERO);
assert_eq!(state.balance.other, prev_balance.other);
assert_eq!(
state.balance.tokens,
prev_balance.tokens - storage_phase.storage_fees_collected
);
assert_eq!(
state.total_fees,
prev_total_fees + storage_phase.storage_fees_collected
);
let delta = params.block_unixtime - prev_storage_stat.last_paid;
let prev_due = prev_storage_stat.due_payment.unwrap_or_default();
let target_fee = fee_for_storing(prev_storage_stat.used.clone(), delta, &config);
assert_eq!(storage_phase.storage_fees_collected, prev_balance.tokens);
assert_eq!(
storage_phase.storage_fees_due,
Some(prev_due + target_fee - prev_balance.tokens)
);
assert_eq!(state.storage_stat.used, prev_storage_stat.used);
assert_eq!(state.storage_stat.last_paid, params.block_unixtime);
assert_eq!(
state.storage_stat.due_payment,
Some(prev_due + target_fee - prev_balance.tokens)
);
}
#[test]
fn suspended_account_storage_phase_skipped() -> anyhow::Result<()> {
let params = ExecutorParams {
authority_marks_enabled: true,
..make_default_params()
};
let config = make_custom_config(|config| {
config.set_authority_marks_config(&AuthorityMarksConfig {
authority_addresses: Dict::new(),
black_mark_id: 100,
white_mark_id: 101,
})?;
Ok(())
});
let mut state = ExecutorState::new_active(
¶ms,
&config,
&STUB_ADDR,
CurrencyCollection {
tokens: Tokens::MAX,
other: BTreeMap::from_iter([
(100u32, VarUint248::new(500)), ])
.try_into()?,
},
CellBuilder::build_from(u32::MIN)?,
tvmasm!("ACCEPT"),
);
state.storage_stat = StorageInfo {
used: StorageUsed {
bits: VarUint56::new(1000),
cells: VarUint56::new(10),
},
storage_extra: Default::default(),
last_paid: 1000,
due_payment: None,
};
let prev_storage_stat = state.storage_stat.clone();
let prev_balance = state.balance.clone();
let prev_status = state.end_status;
let prev_total_fees = state.total_fees;
let prev_due = state.storage_stat.due_payment;
let storage_phase = state.storage_phase(StoragePhaseContext {
adjust_msg_balance: false,
received_message: None,
})?;
assert_eq!(prev_storage_stat, state.storage_stat);
assert_eq!(prev_balance, state.balance);
assert_eq!(prev_status, state.end_status);
assert_eq!(prev_total_fees, state.total_fees);
assert_eq!(storage_phase.storage_fees_collected, Tokens::ZERO);
assert_eq!(storage_phase.storage_fees_due, prev_due);
assert_eq!(storage_phase.status_change, AccountStatusChange::Unchanged);
Ok(())
}
}