1use anyhow::Result;
2use tycho_types::cell::{CellTreeStats, Lazy};
3use tycho_types::error::Error;
4use tycho_types::models::{
5 AccountState, AccountStatus, AccountStatusChange, ActionPhase, ChangeLibraryMode,
6 CurrencyCollection, ExecutedComputePhase, ExtraCurrencyCollection, LibRef, OutAction,
7 OwnedMessage, OwnedRelaxedMessage, RelaxedMsgInfo, ReserveCurrencyFlags, SendMsgFlags,
8 SimpleLib, StateInit, StorageUsedShort,
9};
10use tycho_types::num::{Tokens, VarUint56};
11use tycho_types::prelude::*;
12
13use crate::phase::receive::ReceivedMessage;
14use crate::util::{
15 ExtStorageStat, StateLimitsResult, StorageStatLimits, check_rewrite_dst_addr,
16 check_rewrite_src_addr, check_state_limits, check_state_limits_diff,
17};
18use crate::{ExecutorInspector, ExecutorState, PublicLibraryChange};
19
20pub struct ActionPhaseContext<'a, 'e> {
22 pub received_message: Option<&'a mut ReceivedMessage>,
24 pub original_balance: CurrencyCollection,
26 pub new_state: StateInit,
28 pub actions: Cell,
30 pub compute_phase: &'a ExecutedComputePhase,
32 pub inspector: Option<&'a mut ExecutorInspector<'e>>,
34}
35
36#[derive(Debug)]
38pub struct ActionPhaseFull {
39 pub action_phase: ActionPhase,
41 pub action_fine: Tokens,
43 pub state_exceeds_limits: bool,
45 pub bounce: bool,
47}
48
49impl ExecutorState<'_> {
50 pub fn action_phase(&mut self, mut ctx: ActionPhaseContext<'_, '_>) -> Result<ActionPhaseFull> {
51 const MAX_ACTIONS: u16 = 255;
52
53 let mut res = ActionPhaseFull {
54 action_phase: ActionPhase {
55 success: false,
56 valid: false,
57 no_funds: false,
58 status_change: AccountStatusChange::Unchanged,
59 total_fwd_fees: None,
60 total_action_fees: None,
61 result_code: -1,
62 result_arg: None,
63 total_actions: 0,
64 special_actions: 0,
65 skipped_actions: 0,
66 messages_created: 0,
67 action_list_hash: *ctx.actions.repr_hash(),
68 total_message_size: StorageUsedShort::ZERO,
69 },
70 action_fine: Tokens::ZERO,
71 state_exceeds_limits: false,
72 bounce: false,
73 };
74
75 let mut action_idx = 0u16;
77
78 let mut list = Vec::new();
79 let mut actions = ctx.actions.as_ref();
80 loop {
81 if actions.is_exotic() {
82 res.action_phase.result_code = ResultCode::ActionListInvalid as i32;
84 res.action_phase.result_arg = Some(action_idx as _);
85 res.action_phase.valid = false;
86 return Ok(res);
87 }
88
89 let mut cs = actions.as_slice_allow_exotic();
91 if cs.is_empty() {
92 break;
94 }
95
96 list.push(actions);
97
98 actions = match cs.load_reference() {
99 Ok(child) => child,
100 Err(_) => {
101 res.action_phase.result_code = ResultCode::ActionListInvalid as i32;
103 res.action_phase.result_arg = Some(action_idx as _);
104 res.action_phase.valid = false;
105 return Ok(res);
106 }
107 };
108
109 action_idx += 1;
110 if action_idx > MAX_ACTIONS {
111 res.action_phase.result_code = ResultCode::TooManyActions as i32;
113 res.action_phase.result_arg = Some(action_idx as _);
114 res.action_phase.valid = false;
115 return Ok(res);
116 }
117 }
118
119 res.action_phase.total_actions = action_idx;
120
121 let mut parsed_list = Vec::with_capacity(list.len());
123 for (action_idx, item) in list.into_iter().rev().enumerate() {
124 let mut cs = item.as_slice_allow_exotic();
125 cs.load_reference().ok(); let mut cs_parsed = cs;
129 if let Ok(item) = OutAction::load_from(&mut cs_parsed) {
130 if cs_parsed.is_empty() {
131 parsed_list.push(Some(item));
133 continue;
134 }
135 }
136
137 if cs.size_bits() >= 40 && cs.load_u32()? == OutAction::TAG_SEND_MSG {
139 let mode = SendMsgFlags::from_bits_retain(cs.load_u8()?);
140 if mode.contains(SendMsgFlags::IGNORE_ERROR) {
141 res.action_phase.skipped_actions += 1;
143 parsed_list.push(None);
144 continue;
145 } else if mode.contains(SendMsgFlags::BOUNCE_ON_ERROR) {
146 res.bounce = true;
149 }
150 }
151
152 res.action_phase.result_code = ResultCode::ActionInvalid as i32;
153 res.action_phase.result_arg = Some(action_idx as _);
154 res.action_phase.valid = false;
155 return Ok(res);
156 }
157
158 res.action_phase.valid = true;
160
161 let mut action_ctx = ActionContext {
163 need_bounce_on_fail: false,
164 strict_extra_currency: self.params.strict_extra_currency,
165 received_message: ctx.received_message,
166 original_balance: &ctx.original_balance,
167 remaining_balance: self.balance.clone(),
168 reserved_balance: CurrencyCollection::ZERO,
169 action_fine: &mut res.action_fine,
170 new_state: &mut ctx.new_state,
171 end_lt: self.end_lt,
172 out_msgs: Vec::new(),
173 delete_account: false,
174 public_libs_diff: ctx.inspector.is_some().then(Vec::new),
175 compute_phase: ctx.compute_phase,
176 action_phase: &mut res.action_phase,
177 };
178
179 for (action_idx, action) in parsed_list.into_iter().enumerate() {
180 let Some(action) = action else {
181 continue;
182 };
183
184 action_ctx.need_bounce_on_fail = false;
185 action_ctx.action_phase.result_code = -1;
186 action_ctx.action_phase.result_arg = Some(action_idx as _);
187
188 let action = match action {
189 OutAction::SendMsg { mode, out_msg } => {
190 let mut rewrite = None;
191 loop {
192 match self.do_send_message(mode, &out_msg, &mut action_ctx, rewrite) {
193 Ok(SendMsgResult::Sent) => break Ok(()),
194 Ok(SendMsgResult::Rewrite(r)) => rewrite = Some(r),
195 Err(e) => break Err(e),
196 }
197 }
198 }
199 OutAction::SetCode { new_code } => self.do_set_code(new_code, &mut action_ctx),
200 OutAction::ReserveCurrency { mode, value } => {
201 self.do_reserve_currency(mode, value, &mut action_ctx)
202 }
203 OutAction::ChangeLibrary { mode, lib } => {
204 self.do_change_library(mode, lib, &mut action_ctx)
205 }
206 };
207
208 if let Err(ActionFailed) = action {
209 let result_code = &mut action_ctx.action_phase.result_code;
210 if *result_code == -1 {
211 *result_code = ResultCode::ActionInvalid as i32;
212 }
213 if *result_code == ResultCode::NotEnoughBalance as i32
214 || *result_code == ResultCode::NotEnoughExtraBalance as i32
215 {
216 action_ctx.action_phase.no_funds = true;
217 }
218
219 action_ctx.apply_fine_on_error(
226 &mut self.balance,
227 &mut self.total_fees,
228 self.params.charge_action_fees_on_fail,
229 )?;
230
231 res.bounce |= action_ctx.need_bounce_on_fail;
233
234 return Ok(res);
236 }
237 }
238
239 if !self.is_special {
242 let limits = &self.config.size_limits;
243 let is_masterchain = self.address.is_masterchain();
244 let check = match &self.state {
245 AccountState::Active(current_state) => check_state_limits_diff(
246 current_state,
247 action_ctx.new_state,
248 limits,
249 is_masterchain,
250 &mut self.cached_storage_stat,
251 ),
252 AccountState::Uninit | AccountState::Frozen(_) => check_state_limits(
253 action_ctx.new_state.code.as_ref(),
254 action_ctx.new_state.data.as_ref(),
255 &action_ctx.new_state.libraries,
256 limits,
257 is_masterchain,
258 &mut self.cached_storage_stat,
259 ),
260 };
261
262 if matches!(check, StateLimitsResult::Exceeds) {
263 action_ctx.apply_fine_on_error(
265 &mut self.balance,
266 &mut self.total_fees,
267 self.params.charge_action_fees_on_fail,
268 )?;
269
270 res.bounce |= action_ctx.need_bounce_on_fail;
272 res.action_phase.result_code = ResultCode::StateOutOfLimits as i32;
273 res.state_exceeds_limits = true;
274 return Ok(res);
275 }
276
277 }
281
282 if !action_ctx.action_fine.is_zero() {
283 action_ctx
284 .action_phase
285 .total_action_fees
286 .get_or_insert_default()
287 .try_add_assign(*action_ctx.action_fine)?;
288 }
289
290 action_ctx
291 .remaining_balance
292 .try_add_assign(&action_ctx.reserved_balance)?;
293
294 action_ctx.action_phase.result_code = 0;
295 action_ctx.action_phase.result_arg = None;
296 action_ctx.action_phase.success = true;
297
298 if action_ctx.delete_account {
299 if self.params.strict_extra_currency {
300 debug_assert!(action_ctx.remaining_balance.tokens.is_zero());
302 } else {
303 debug_assert!(action_ctx.remaining_balance.is_zero());
305 }
306 action_ctx.action_phase.status_change = AccountStatusChange::Deleted;
307 self.end_status = if action_ctx.remaining_balance.is_zero() {
308 AccountStatus::NotExists
311 } else {
312 AccountStatus::Uninit
314 };
315 self.cached_storage_stat = None;
316 }
317
318 if let Some(fees) = action_ctx.action_phase.total_action_fees {
319 self.total_fees.try_add_assign(fees)?;
321 }
322 self.balance = action_ctx.remaining_balance;
323
324 if let Some(inspector) = ctx.inspector {
325 inspector.public_libs_diff = action_ctx.public_libs_diff.unwrap_or_default();
326 }
327
328 self.out_msgs = action_ctx.out_msgs;
329 self.end_lt = action_ctx.end_lt;
330 self.state = AccountState::Active(ctx.new_state);
331
332 Ok(res)
333 }
334
335 fn do_send_message(
337 &self,
338 mode: SendMsgFlags,
339 out_msg: &Lazy<OwnedRelaxedMessage>,
340 ctx: &mut ActionContext<'_>,
341 mut rewrite: Option<MessageRewrite>,
342 ) -> Result<SendMsgResult, ActionFailed> {
343 const MASK: u8 = SendMsgFlags::all().bits();
344 const INVALID_MASK: SendMsgFlags =
345 SendMsgFlags::ALL_BALANCE.union(SendMsgFlags::WITH_REMAINING_BALANCE);
346 const EXT_MSG_MASK: u8 = SendMsgFlags::PAY_FEE_SEPARATELY
347 .union(SendMsgFlags::IGNORE_ERROR)
348 .union(SendMsgFlags::BOUNCE_ON_ERROR)
349 .bits();
350 const DELETE_MASK: SendMsgFlags =
351 SendMsgFlags::ALL_BALANCE.union(SendMsgFlags::DELETE_IF_EMPTY);
352
353 if mode.contains(SendMsgFlags::BOUNCE_ON_ERROR) {
355 ctx.need_bounce_on_fail = true;
356 }
357
358 if mode.bits() & !MASK != 0 || mode.contains(INVALID_MASK) {
359 return Err(ActionFailed);
362 }
363
364 let skip_invalid = mode.contains(SendMsgFlags::IGNORE_ERROR);
366 let check_skip_invalid = |e: ResultCode, ctx: &mut ActionContext<'_>| {
367 if skip_invalid {
368 ctx.action_phase.skipped_actions += 1;
369 Ok(SendMsgResult::Sent)
370 } else {
371 ctx.action_phase.result_code = e as i32;
372 Err(ActionFailed)
373 }
374 };
375
376 if out_msg.is_exotic() {
378 return Err(ActionFailed);
379 }
380
381 let mut relaxed_info;
383 let mut state_init_cs;
384 let mut body_cs;
385
386 {
387 let mut cs = out_msg.as_slice_allow_exotic();
388
389 relaxed_info = RelaxedMsgInfo::load_from(&mut cs)?;
390 state_init_cs = load_state_init_as_slice(&mut cs)?;
391 body_cs = load_body_as_slice(&mut cs)?;
392
393 if !cs.is_empty() {
394 return Err(ActionFailed);
396 }
397 }
398
399 let rewritten_state_init_cb;
401 if let Some(MessageRewrite::StateInitToCell) = rewrite {
402 if state_init_cs.size_refs() >= 2 {
403 rewritten_state_init_cb = rewrite_state_init_to_cell(state_init_cs);
405 state_init_cs = rewritten_state_init_cb.as_full_slice();
406 } else {
407 rewrite = Some(MessageRewrite::BodyToCell);
409 }
410 }
411
412 let rewritten_body_cs;
413 if let Some(MessageRewrite::BodyToCell) = rewrite {
414 if body_cs.size_bits() > 1 && !body_cs.get_bit(0).unwrap() {
415 rewritten_body_cs = rewrite_body_to_cell(body_cs);
417 body_cs = rewritten_body_cs.as_full_slice();
418 }
419 }
420
421 let mut use_mc_prices = self.address.is_masterchain();
423 match &mut relaxed_info {
424 RelaxedMsgInfo::Int(info) => {
426 if !check_rewrite_src_addr(&self.address, &mut info.src) {
428 ctx.action_phase.result_code = ResultCode::InvalidSrcAddr as i32;
430 return Err(ActionFailed);
431 };
432
433 if !check_rewrite_dst_addr(&self.config.workchains, &mut info.dst) {
435 return check_skip_invalid(ResultCode::InvalidDstAddr, ctx);
436 }
437 use_mc_prices |= info.dst.is_masterchain();
438
439 if self.params.strict_extra_currency {
441 match normalize_extra_balance(
442 std::mem::take(&mut info.value.other),
443 MAX_MSG_EXTRA_CURRENCIES,
444 ) {
445 Ok(other) => info.value.other = other,
446 Err(BalanceExtraError::InvalidDict(_)) => {
447 return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
448 }
449 Err(BalanceExtraError::OutOfLimit) => {
450 return check_skip_invalid(ResultCode::TooManyExtraCurrencies, ctx);
451 }
452 }
453 }
454
455 info.ihr_fee = Tokens::ZERO;
457 info.fwd_fee = Tokens::ZERO;
458
459 info.created_at = self.params.block_unixtime;
461 info.created_lt = ctx.end_lt;
462
463 info.ihr_disabled = true;
465 info.bounced = false;
466 }
467 RelaxedMsgInfo::ExtOut(info) => {
469 if mode.bits() & !EXT_MSG_MASK != 0 {
470 return Err(ActionFailed);
472 }
473
474 if !check_rewrite_src_addr(&self.address, &mut info.src) {
476 ctx.action_phase.result_code = ResultCode::InvalidSrcAddr as i32;
477 return Err(ActionFailed);
478 }
479
480 info.created_at = self.params.block_unixtime;
482 info.created_lt = ctx.end_lt;
483 }
484 };
485
486 let prices = self.config.fwd_prices(use_mc_prices);
488 let mut max_cell_count = self.config.size_limits.max_msg_cells;
489 let fine_per_cell;
490 if self.is_special {
491 fine_per_cell = 0;
492 } else {
493 fine_per_cell = (prices.cell_price >> 16) / 4;
494
495 let mut funds = ctx.remaining_balance.tokens;
496 if let RelaxedMsgInfo::Int(info) = &relaxed_info {
497 if !mode.contains(SendMsgFlags::ALL_BALANCE)
498 && !mode.contains(SendMsgFlags::PAY_FEE_SEPARATELY)
499 {
500 let mut new_funds = info.value.tokens;
501
502 if mode.contains(SendMsgFlags::WITH_REMAINING_BALANCE)
503 && (|| {
504 let msg_balance_remaining = match &ctx.received_message {
505 Some(msg) => msg.balance_remaining.tokens,
506 None => Tokens::ZERO,
507 };
508 new_funds.try_add_assign(msg_balance_remaining)?;
509 new_funds.try_sub_assign(ctx.compute_phase.gas_fees)?;
510 new_funds.try_sub_assign(*ctx.action_fine)?;
511
512 Ok::<_, tycho_types::error::Error>(())
513 })()
514 .is_err()
515 {
516 return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
517 }
518
519 funds = std::cmp::min(funds, new_funds);
520 }
521 }
522
523 if funds < Tokens::new(max_cell_count as u128 * fine_per_cell as u128) {
524 debug_assert_ne!(fine_per_cell, 0);
525 max_cell_count = (funds.into_inner() / fine_per_cell as u128)
526 .try_into()
527 .unwrap_or(u32::MAX);
528 }
529 }
530
531 let collect_fine = |cells: u32, ctx: &mut ActionContext<'_>| {
532 let mut fine = Tokens::new(
533 fine_per_cell.saturating_mul(std::cmp::min(max_cell_count, cells) as u64) as _,
534 );
535 fine = std::cmp::min(fine, ctx.remaining_balance.tokens);
536 ctx.action_fine.try_add_assign(fine)?;
537 ctx.remaining_balance.try_sub_assign_tokens(fine)
538 };
539
540 let stats = 'stats: {
542 let mut stats = ExtStorageStat::with_limits(StorageStatLimits {
543 bit_count: self.config.size_limits.max_msg_bits,
544 cell_count: max_cell_count,
545 });
546
547 'valid: {
548 for cell in state_init_cs.references() {
549 if !stats.add_cell(cell) {
550 break 'valid;
551 }
552 }
553
554 for cell in body_cs.references() {
555 if !stats.add_cell(cell) {
556 break 'valid;
557 }
558 }
559
560 if !self.params.strict_extra_currency {
562 if let RelaxedMsgInfo::Int(int) = &relaxed_info {
563 if let Some(cell) = int.value.other.as_dict().root() {
564 if !stats.add_cell(cell.as_ref()) {
565 break 'valid;
566 }
567 }
568 }
569 }
570
571 break 'stats stats.stats();
572 }
573
574 collect_fine(stats.cells, ctx)?;
575 return check_skip_invalid(ResultCode::MessageOutOfLimits, ctx);
576 };
577
578 let check_skip_invalid = move |e: ResultCode, ctx: &mut ActionContext<'_>| {
580 collect_fine(stats.cell_count as _, ctx)?;
581 check_skip_invalid(e, ctx)
582 };
583
584 let fwd_fee = if self.is_special {
586 Tokens::ZERO
587 } else {
588 prices.compute_fwd_fee(stats)
589 };
590
591 let msg;
593 let fees_collected;
594 match &mut relaxed_info {
595 RelaxedMsgInfo::Int(info) => {
596 let value_to_pay = match ctx.rewrite_message_value(&mut info.value, mode, fwd_fee) {
598 Ok(total_value) => total_value,
599 Err(_) => return check_skip_invalid(ResultCode::NotEnoughBalance, ctx),
600 };
601
602 if ctx.remaining_balance.tokens < value_to_pay {
604 return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
605 }
606
607 if self.params.strict_extra_currency
609 && !check_extra_balance(&info.value.other, MAX_MSG_EXTRA_CURRENCIES)
610 {
611 return check_skip_invalid(ResultCode::TooManyExtraCurrencies, ctx);
612 }
613
614 let other = match ctx.remaining_balance.other.checked_sub(&info.value.other) {
616 Ok(other) => other,
617 Err(_) => return check_skip_invalid(ResultCode::NotEnoughExtraBalance, ctx),
618 };
619
620 fees_collected = prices.get_first_part(fwd_fee);
622 info.fwd_fee = fwd_fee - fees_collected;
623
624 msg = match build_message(&relaxed_info, &state_init_cs, &body_cs) {
626 Ok(msg) => msg,
627 Err(_) => match MessageRewrite::next(rewrite) {
628 Some(rewrite) => return Ok(SendMsgResult::Rewrite(rewrite)),
629 None => return check_skip_invalid(ResultCode::FailedToFitMessage, ctx),
630 },
631 };
632
633 if let Some(msg) = &mut ctx.received_message {
635 if mode.contains(SendMsgFlags::ALL_BALANCE)
636 || mode.contains(SendMsgFlags::WITH_REMAINING_BALANCE)
637 {
638 if self.params.strict_extra_currency {
639 msg.balance_remaining.tokens = Tokens::ZERO;
641 } else {
642 msg.balance_remaining = CurrencyCollection::ZERO;
644 }
645 }
646 }
647
648 ctx.remaining_balance.tokens -= value_to_pay;
650 ctx.remaining_balance.other = other;
651 }
652 RelaxedMsgInfo::ExtOut(_) => {
653 if ctx.remaining_balance.tokens < fwd_fee {
655 return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
656 }
657
658 msg = match build_message(&relaxed_info, &state_init_cs, &body_cs) {
660 Ok(msg) => msg,
661 Err(_) => match MessageRewrite::next(rewrite) {
662 Some(rewrite) => return Ok(SendMsgResult::Rewrite(rewrite)),
663 None => return check_skip_invalid(ResultCode::FailedToFitMessage, ctx),
664 },
665 };
666
667 ctx.remaining_balance.tokens -= fwd_fee;
669 fees_collected = fwd_fee;
670 }
671 }
672
673 update_total_msg_stat(
674 &mut ctx.action_phase.total_message_size,
675 stats,
676 msg.bit_len(),
677 );
678
679 ctx.action_phase.messages_created += 1;
680 ctx.end_lt += 1;
681
682 ctx.out_msgs.push(msg);
683
684 *ctx.action_phase.total_action_fees.get_or_insert_default() += fees_collected;
685 *ctx.action_phase.total_fwd_fees.get_or_insert_default() += fwd_fee;
686
687 if mode.contains(DELETE_MASK) {
688 ctx.delete_account = if self.params.strict_extra_currency {
689 debug_assert!(ctx.remaining_balance.tokens.is_zero());
691 ctx.reserved_balance.tokens.is_zero()
692 } else {
693 debug_assert!(ctx.remaining_balance.is_zero());
695 ctx.reserved_balance.is_zero()
696 };
697 }
698
699 Ok(SendMsgResult::Sent)
700 }
701
702 fn do_set_code(&self, new_code: Cell, ctx: &mut ActionContext<'_>) -> Result<(), ActionFailed> {
704 ctx.new_state.code = Some(new_code);
706 ctx.action_phase.special_actions += 1;
707
708 Ok(())
710 }
711
712 fn do_reserve_currency(
714 &self,
715 mode: ReserveCurrencyFlags,
716 mut reserve: CurrencyCollection,
717 ctx: &mut ActionContext<'_>,
718 ) -> Result<(), ActionFailed> {
719 const MASK: u8 = ReserveCurrencyFlags::all().bits();
720
721 if mode.contains(ReserveCurrencyFlags::BOUNCE_ON_ERROR) {
723 ctx.need_bounce_on_fail = true;
724 }
725
726 if mode.bits() & !MASK != 0 {
727 return Err(ActionFailed);
729 }
730
731 if self.params.strict_extra_currency && !reserve.other.is_empty() {
732 return Err(ActionFailed);
734 }
735
736 if mode.contains(ReserveCurrencyFlags::WITH_ORIGINAL_BALANCE) {
737 if mode.contains(ReserveCurrencyFlags::REVERSE) {
738 if self.params.strict_extra_currency {
739 reserve.tokens = ctx
740 .original_balance
741 .tokens
742 .checked_sub(reserve.tokens)
743 .ok_or(ActionFailed)?;
744 } else {
745 reserve = ctx.original_balance.checked_sub(&reserve)?;
746 }
747 } else if self.params.strict_extra_currency {
748 reserve.try_add_assign_tokens(ctx.original_balance.tokens)?;
749 } else {
750 reserve.try_add_assign(ctx.original_balance)?;
751 }
752 } else if mode.contains(ReserveCurrencyFlags::REVERSE) {
753 return Err(ActionFailed);
755 }
756
757 if mode.contains(ReserveCurrencyFlags::IGNORE_ERROR) {
758 reserve = reserve.checked_clamp(&ctx.remaining_balance)?;
760 }
761
762 let mut new_balance = CurrencyCollection {
764 tokens: match ctx.remaining_balance.tokens.checked_sub(reserve.tokens) {
765 Some(tokens) => tokens,
766 None => {
767 ctx.action_phase.result_code = ResultCode::NotEnoughBalance as i32;
768 return Err(ActionFailed);
769 }
770 },
771 other: match ctx.remaining_balance.other.checked_sub(&reserve.other) {
772 Ok(other) => other,
773 Err(_) => {
774 ctx.action_phase.result_code = ResultCode::NotEnoughExtraBalance as i32;
775 return Err(ActionFailed);
776 }
777 },
778 };
779
780 reserve.other.normalize()?;
782
783 if mode.contains(ReserveCurrencyFlags::ALL_BUT) {
785 if self.params.strict_extra_currency {
786 std::mem::swap(&mut new_balance.tokens, &mut reserve.tokens);
787 } else {
788 std::mem::swap(&mut new_balance, &mut reserve);
789 }
790 }
791
792 ctx.remaining_balance = new_balance;
794 ctx.reserved_balance.try_add_assign(&reserve)?;
795 ctx.action_phase.special_actions += 1;
796
797 Ok(())
799 }
800
801 fn do_change_library(
803 &self,
804 mode: ChangeLibraryMode,
805 mut lib: LibRef,
806 ctx: &mut ActionContext<'_>,
807 ) -> Result<(), ActionFailed> {
808 const INVALID_MODE: ChangeLibraryMode = ChangeLibraryMode::from_bits_retain(
810 ChangeLibraryMode::ADD_PRIVATE.bits() | ChangeLibraryMode::ADD_PUBLIC.bits(),
811 );
812
813 if mode.contains(ChangeLibraryMode::BOUNCE_ON_ERROR) {
815 ctx.need_bounce_on_fail = true;
816 }
817
818 if mode.contains(INVALID_MODE) {
819 return Err(ActionFailed);
820 }
821
822 let hash = match &lib {
823 LibRef::Cell(cell) => cell.repr_hash(),
824 LibRef::Hash(hash) => hash,
825 };
826
827 let is_masterchain = self.address.is_masterchain();
828
829 let add_public = mode.contains(ChangeLibraryMode::ADD_PUBLIC);
830 if add_public || mode.contains(ChangeLibraryMode::ADD_PRIVATE) {
831 let mut was_public = None;
833 if let Ok(Some(prev)) = ctx.new_state.libraries.get(hash) {
834 if prev.public == add_public {
835 ctx.action_phase.special_actions += 1;
837 return Ok(());
838 } else {
839 was_public = Some(prev.public);
841 lib = LibRef::Cell(prev.root);
842 }
843 }
844
845 let LibRef::Cell(root) = lib else {
846 ctx.action_phase.result_code = ResultCode::NoLibCode as i32;
847 return Err(ActionFailed);
848 };
849
850 let mut stats = ExtStorageStat::with_limits(StorageStatLimits {
851 bit_count: u32::MAX,
852 cell_count: self.config.size_limits.max_library_cells,
853 });
854 if !stats.add_cell(root.as_ref()) {
855 ctx.action_phase.result_code = ResultCode::LibOutOfLimits as i32;
856 return Err(ActionFailed);
857 }
858
859 match ctx.new_state.libraries.set(*root.repr_hash(), SimpleLib {
861 public: add_public,
862 root: root.clone(),
863 }) {
864 Ok(_) if is_masterchain => {
866 if let Some(diff) = &mut ctx.public_libs_diff {
868 match (was_public, add_public) {
869 (None, true) | (Some(false), true) => {
871 diff.push(PublicLibraryChange::Add(root))
872 }
873 (Some(true), false) => {
875 diff.push(PublicLibraryChange::Remove(*root.repr_hash()));
876 }
877 _ => {}
879 }
880 }
881 }
882 Ok(_) => {}
883 Err(_) => {
884 ctx.action_phase.result_code = ResultCode::InvalidLibrariesDict as i32;
885 return Err(ActionFailed);
886 }
887 }
888 } else {
889 match ctx.new_state.libraries.remove(hash) {
891 Ok(Some(lib)) if is_masterchain && lib.public => {
893 if let Some(diff) = &mut ctx.public_libs_diff {
894 diff.push(PublicLibraryChange::Remove(*hash));
895 }
896 }
897 Ok(_) => {}
898 Err(_) => {
899 ctx.action_phase.result_code = ResultCode::InvalidLibrariesDict as i32;
900 return Err(ActionFailed);
901 }
902 }
903 }
904
905 ctx.action_phase.special_actions += 1;
907
908 Ok(())
910 }
911}
912
913struct ActionContext<'a> {
914 need_bounce_on_fail: bool,
915 strict_extra_currency: bool,
916 received_message: Option<&'a mut ReceivedMessage>,
917 original_balance: &'a CurrencyCollection,
918 remaining_balance: CurrencyCollection,
919 reserved_balance: CurrencyCollection,
920 action_fine: &'a mut Tokens,
921 new_state: &'a mut StateInit,
922 end_lt: u64,
923 out_msgs: Vec<Lazy<OwnedMessage>>,
924 delete_account: bool,
925 public_libs_diff: Option<Vec<PublicLibraryChange>>,
926
927 compute_phase: &'a ExecutedComputePhase,
928 action_phase: &'a mut ActionPhase,
929}
930
931impl ActionContext<'_> {
932 fn apply_fine_on_error(
933 &mut self,
934 balance: &mut CurrencyCollection,
935 total_fees: &mut Tokens,
936 charge_action_fees: bool,
937 ) -> Result<(), Error> {
938 if charge_action_fees {
940 self.action_fine
941 .try_add_assign(self.action_phase.total_action_fees.unwrap_or_default())?;
942 }
943
944 self.action_phase.total_fwd_fees = None;
948
949 self.action_phase.total_action_fees = Some(*self.action_fine).filter(|t| !t.is_zero());
951
952 balance.tokens.try_sub_assign(*self.action_fine)?;
953 total_fees.try_add_assign(*self.action_fine)
954 }
955
956 fn rewrite_message_value(
957 &self,
958 value: &mut CurrencyCollection,
959 mut mode: SendMsgFlags,
960 fees_total: Tokens,
961 ) -> Result<Tokens, ActionFailed> {
962 if mode.contains(SendMsgFlags::ALL_BALANCE) {
964 if self.strict_extra_currency {
966 value.tokens = self.remaining_balance.tokens;
968 } else {
969 *value = self.remaining_balance.clone();
971 };
972 mode.remove(SendMsgFlags::PAY_FEE_SEPARATELY);
974 } else if mode.contains(SendMsgFlags::WITH_REMAINING_BALANCE) {
975 if let Some(msg) = &self.received_message {
976 if self.strict_extra_currency {
979 value.try_add_assign_tokens(msg.balance_remaining.tokens)?;
981 } else {
982 value.try_add_assign(&msg.balance_remaining)?;
984 }
985 }
986
987 if !mode.contains(SendMsgFlags::PAY_FEE_SEPARATELY) {
988 value.try_sub_assign_tokens(*self.action_fine)?;
990 value.try_sub_assign_tokens(self.compute_phase.gas_fees)?;
991 }
992 }
993
994 let mut total = value.tokens;
996 if mode.contains(SendMsgFlags::PAY_FEE_SEPARATELY) {
997 total.try_add_assign(fees_total)?;
998 } else {
999 value.tokens.try_sub_assign(fees_total)?;
1000 }
1001
1002 Ok(total)
1004 }
1005}
1006
1007struct ActionFailed;
1008
1009impl From<anyhow::Error> for ActionFailed {
1010 #[inline]
1011 fn from(_: anyhow::Error) -> Self {
1012 Self
1013 }
1014}
1015
1016impl From<Error> for ActionFailed {
1017 #[inline]
1018 fn from(_: Error) -> Self {
1019 Self
1020 }
1021}
1022
1023#[derive(Debug, Clone, Copy)]
1024enum SendMsgResult {
1025 Sent,
1026 Rewrite(MessageRewrite),
1027}
1028
1029#[derive(Debug, Clone, Copy)]
1030enum MessageRewrite {
1031 StateInitToCell,
1032 BodyToCell,
1033}
1034
1035impl MessageRewrite {
1036 pub fn next(rewrite: Option<Self>) -> Option<Self> {
1037 match rewrite {
1038 None => Some(Self::StateInitToCell),
1039 Some(Self::StateInitToCell) => Some(Self::BodyToCell),
1040 Some(Self::BodyToCell) => None,
1041 }
1042 }
1043}
1044
1045fn load_state_init_as_slice<'a>(cs: &mut CellSlice<'a>) -> Result<CellSlice<'a>, Error> {
1046 let mut res_cs = *cs;
1047
1048 if cs.load_bit()? {
1050 if cs.load_bit()? {
1051 let state_root = cs.load_reference()?;
1053 if state_root.is_exotic() {
1054 return Err(Error::InvalidData);
1056 }
1057
1058 let mut cs = state_root.as_slice_allow_exotic();
1060 StateInit::load_from(&mut cs)?;
1061
1062 if !cs.is_empty() {
1064 return Err(Error::CellOverflow);
1065 }
1066 } else {
1067 StateInit::load_from(cs)?;
1071 }
1072 }
1073
1074 res_cs.skip_last(cs.size_bits(), cs.size_refs())?;
1075 Ok(res_cs)
1076}
1077
1078fn load_body_as_slice<'a>(cs: &mut CellSlice<'a>) -> Result<CellSlice<'a>, Error> {
1079 let res_cs = *cs;
1080
1081 if cs.load_bit()? {
1082 cs.skip_first(0, 1)?;
1084 } else {
1085 cs.load_remaining();
1087 }
1088
1089 Ok(res_cs)
1090}
1091
1092fn rewrite_state_init_to_cell(mut cs: CellSlice<'_>) -> CellBuilder {
1093 let prefix = cs.load_small_uint(2).unwrap();
1095 debug_assert_eq!(prefix, 0b10);
1096
1097 let cell = CellBuilder::build_from(cs).unwrap();
1099
1100 let mut b = CellBuilder::new();
1102 b.store_small_uint(0b11, 2).unwrap();
1103 b.store_reference(cell).unwrap();
1104
1105 b
1107}
1108
1109fn rewrite_body_to_cell(mut cs: CellSlice<'_>) -> CellBuilder {
1110 let prefix = cs.load_bit().unwrap();
1112 debug_assert!(!prefix);
1113
1114 let cell = CellBuilder::build_from(cs).unwrap();
1116
1117 let mut b = CellBuilder::new();
1119 b.store_bit_one().unwrap();
1120 b.store_reference(cell).unwrap();
1121
1122 b
1124}
1125
1126fn build_message(
1127 info: &RelaxedMsgInfo,
1128 state_init_cs: &CellSlice<'_>,
1129 body_cs: &CellSlice<'_>,
1130) -> Result<Lazy<OwnedMessage>, Error> {
1131 CellBuilder::build_from((info, state_init_cs, body_cs)).map(|cell| {
1132 unsafe { Lazy::from_raw_unchecked(cell) }
1134 })
1135}
1136
1137fn update_total_msg_stat(
1138 total_message_size: &mut StorageUsedShort,
1139 stats: CellTreeStats,
1140 root_bits: u16,
1141) {
1142 let bits_diff = VarUint56::new(stats.bit_count.saturating_add(root_bits as _));
1143 let cells_diff = VarUint56::new(stats.cell_count.saturating_add(1));
1144
1145 total_message_size.bits = total_message_size.bits.saturating_add(bits_diff);
1146 total_message_size.cells = total_message_size.cells.saturating_add(cells_diff);
1147}
1148
1149fn check_extra_balance(value: &ExtraCurrencyCollection, limit: usize) -> bool {
1150 for (i, entry) in value.as_dict().iter().enumerate() {
1151 if i > limit || entry.is_err() {
1152 return false;
1153 }
1154 }
1155 true
1156}
1157
1158fn normalize_extra_balance(
1159 value: ExtraCurrencyCollection,
1160 limit: usize,
1161) -> Result<ExtraCurrencyCollection, BalanceExtraError> {
1162 let mut result = value.clone();
1163 for (i, entry) in value.as_dict().iter().enumerate() {
1164 if i > limit {
1165 return Err(BalanceExtraError::OutOfLimit);
1166 }
1167 let (currency_id, other) = entry.map_err(BalanceExtraError::InvalidDict)?;
1168 if other.is_zero() {
1169 result
1170 .as_dict_mut()
1171 .remove(currency_id)
1172 .map_err(BalanceExtraError::InvalidDict)?;
1173 }
1174 }
1175 Ok(result)
1176}
1177
1178enum BalanceExtraError {
1179 OutOfLimit,
1180 InvalidDict(#[allow(unused)] Error),
1181}
1182
1183#[repr(i32)]
1184#[derive(Debug, thiserror::Error)]
1185enum ResultCode {
1186 #[error("invalid action list")]
1187 ActionListInvalid = 32,
1188 #[error("too many actions")]
1189 TooManyActions = 33,
1190 #[error("invalid or unsupported action")]
1191 ActionInvalid = 34,
1192 #[error("invalid source address")]
1193 InvalidSrcAddr = 35,
1194 #[error("invalid destination address")]
1195 InvalidDstAddr = 36,
1196 #[error("not enough balance (base currency)")]
1197 NotEnoughBalance = 37,
1198 #[error("not enough balance (extra currency)")]
1199 NotEnoughExtraBalance = 38,
1200 #[error("failed to fit message into cell")]
1201 FailedToFitMessage = 39,
1202 #[error("message exceeds limits")]
1203 MessageOutOfLimits = 40,
1204 #[error("library code not found")]
1205 NoLibCode = 41,
1206 #[error("failed to change libraries dict")]
1207 InvalidLibrariesDict = 42,
1208 #[error("too many library cells")]
1209 LibOutOfLimits = 43,
1210 #[error("too many extra currencies")]
1211 TooManyExtraCurrencies = 44,
1212 #[error("state exceeds limits")]
1213 StateOutOfLimits = 50,
1214}
1215
1216const MAX_MSG_EXTRA_CURRENCIES: usize = 2;
1218
1219#[cfg(test)]
1220mod tests {
1221 use std::collections::BTreeMap;
1222
1223 use tycho_asm_macros::tvmasm;
1224 use tycho_types::merkle::MerkleProof;
1225 use tycho_types::models::{
1226 Anycast, IntAddr, MessageLayout, MsgInfo, RelaxedIntMsgInfo, RelaxedMessage, StdAddr,
1227 VarAddr,
1228 };
1229 use tycho_types::num::{Uint9, VarUint248};
1230
1231 use super::*;
1232 use crate::ExecutorParams;
1233 use crate::tests::{make_default_config, make_default_params};
1234
1235 const STUB_ADDR: StdAddr = StdAddr::new(0, HashBytes::ZERO);
1236 const OK_BALANCE: Tokens = Tokens::new(1_000_000_000);
1237 const OK_GAS: Tokens = Tokens::new(1_000_000);
1238
1239 fn stub_compute_phase(gas_fees: Tokens) -> ExecutedComputePhase {
1240 ExecutedComputePhase {
1241 success: true,
1242 msg_state_used: false,
1243 account_activated: false,
1244 gas_fees,
1245 gas_used: Default::default(),
1246 gas_limit: Default::default(),
1247 gas_credit: None,
1248 mode: 0,
1249 exit_code: 0,
1250 exit_arg: None,
1251 vm_steps: 0,
1252 vm_init_state_hash: Default::default(),
1253 vm_final_state_hash: Default::default(),
1254 }
1255 }
1256
1257 fn empty_action_phase() -> ActionPhase {
1258 ActionPhase {
1259 success: true,
1260 valid: true,
1261 no_funds: false,
1262 status_change: AccountStatusChange::Unchanged,
1263 total_fwd_fees: None,
1264 total_action_fees: None,
1265 result_code: 0,
1266 result_arg: None,
1267 total_actions: 0,
1268 special_actions: 0,
1269 skipped_actions: 0,
1270 messages_created: 0,
1271 action_list_hash: *Cell::empty_cell_ref().repr_hash(),
1272 total_message_size: Default::default(),
1273 }
1274 }
1275
1276 fn make_action_list<I: IntoIterator<Item: Store>>(actions: I) -> Cell {
1277 let mut root = Cell::default();
1278 for action in actions {
1279 root = CellBuilder::build_from((root, action)).unwrap();
1280 }
1281 root
1282 }
1283
1284 fn make_relaxed_message(
1285 info: impl Into<RelaxedMsgInfo>,
1286 init: Option<StateInit>,
1287 body: Option<CellBuilder>,
1288 ) -> Lazy<OwnedRelaxedMessage> {
1289 let body = match &body {
1290 None => Cell::empty_cell_ref().as_slice_allow_exotic(),
1291 Some(cell) => cell.as_full_slice(),
1292 };
1293 Lazy::new(&RelaxedMessage {
1294 info: info.into(),
1295 init,
1296 body,
1297 layout: None,
1298 })
1299 .unwrap()
1300 .cast_into()
1301 }
1302
1303 fn compute_full_stats(msg: &Lazy<OwnedMessage>, params: &ExecutorParams) -> StorageUsedShort {
1304 let msg = 'cell: {
1305 if params.strict_extra_currency {
1306 let mut parsed = msg.load().unwrap();
1307 if let MsgInfo::Int(int) = &mut parsed.info {
1308 if !int.value.other.is_empty() {
1309 int.value.other = ExtraCurrencyCollection::new();
1310 break 'cell CellBuilder::build_from(parsed).unwrap();
1311 }
1312 }
1313 }
1314 msg.inner().clone()
1315 };
1316
1317 let stats = {
1318 let mut stats = ExtStorageStat::with_limits(StorageStatLimits::UNLIMITED);
1319 assert!(stats.add_cell(msg.as_ref()));
1320 stats.stats()
1321 };
1322 StorageUsedShort {
1323 cells: VarUint56::new(stats.cell_count),
1324 bits: VarUint56::new(stats.bit_count),
1325 }
1326 }
1327
1328 fn original_balance(
1329 state: &ExecutorState<'_>,
1330 compute_phase: &ExecutedComputePhase,
1331 ) -> CurrencyCollection {
1332 state
1333 .balance
1334 .clone()
1335 .checked_add(&compute_phase.gas_fees.into())
1336 .unwrap()
1337 }
1338
1339 #[test]
1340 fn empty_actions() -> Result<()> {
1341 let params = make_default_params();
1342 let config = make_default_config();
1343 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
1344
1345 let compute_phase = stub_compute_phase(OK_GAS);
1346 let prev_total_fees = state.total_fees;
1347 let prev_balance = state.balance.clone();
1348 let prev_end_lt = state.end_lt;
1349
1350 let ActionPhaseFull {
1351 action_phase,
1352 action_fine,
1353 state_exceeds_limits,
1354 bounce,
1355 } = state.action_phase(ActionPhaseContext {
1356 received_message: None,
1357 original_balance: original_balance(&state, &compute_phase),
1358 new_state: StateInit::default(),
1359 actions: Cell::empty_cell(),
1360 compute_phase: &compute_phase,
1361 inspector: None,
1362 })?;
1363
1364 assert_eq!(action_phase, empty_action_phase());
1365 assert_eq!(action_fine, Tokens::ZERO);
1366 assert!(!state_exceeds_limits);
1367 assert!(!bounce);
1368 assert_eq!(state.total_fees, prev_total_fees);
1369 assert_eq!(state.balance, prev_balance);
1370 assert_eq!(state.end_lt, prev_end_lt);
1371 Ok(())
1372 }
1373
1374 #[test]
1375 fn too_many_actions() -> Result<()> {
1376 let params = make_default_params();
1377 let config = make_default_config();
1378 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
1379
1380 let compute_phase = stub_compute_phase(OK_GAS);
1381 let prev_total_fees = state.total_fees;
1382 let prev_balance = state.balance.clone();
1383 let prev_end_lt = state.end_lt;
1384
1385 let actions = make_action_list(
1386 std::iter::repeat_with(|| OutAction::SetCode {
1387 new_code: Cell::empty_cell(),
1388 })
1389 .take(300),
1390 );
1391
1392 let ActionPhaseFull {
1393 action_phase,
1394 action_fine,
1395 state_exceeds_limits,
1396 bounce,
1397 } = state.action_phase(ActionPhaseContext {
1398 received_message: None,
1399 original_balance: original_balance(&state, &compute_phase),
1400 new_state: StateInit::default(),
1401 actions: actions.clone(),
1402 compute_phase: &compute_phase,
1403 inspector: None,
1404 })?;
1405
1406 assert_eq!(action_phase, ActionPhase {
1407 success: false,
1408 valid: false,
1409 result_code: ResultCode::TooManyActions as i32,
1410 result_arg: Some(256),
1411 action_list_hash: *actions.repr_hash(),
1412 ..empty_action_phase()
1413 });
1414 assert_eq!(action_fine, Tokens::ZERO);
1415 assert!(!state_exceeds_limits);
1416 assert!(!bounce);
1417 assert_eq!(state.total_fees, prev_total_fees);
1418 assert_eq!(state.balance, prev_balance);
1419 assert_eq!(state.end_lt, prev_end_lt);
1420 Ok(())
1421 }
1422
1423 #[test]
1424 fn invalid_action_list() -> Result<()> {
1425 let params = make_default_params();
1426 let config = make_default_config();
1427 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
1428
1429 let compute_phase = stub_compute_phase(OK_GAS);
1430 let prev_total_fees = state.total_fees;
1431 let prev_balance = state.balance.clone();
1432 let prev_end_lt = state.end_lt;
1433
1434 let actions = CellBuilder::build_from((
1435 CellBuilder::build_from(MerkleProof::default())?,
1436 OutAction::SetCode {
1437 new_code: Cell::default(),
1438 },
1439 ))?;
1440
1441 let ActionPhaseFull {
1442 action_phase,
1443 action_fine,
1444 state_exceeds_limits,
1445 bounce,
1446 } = state.action_phase(ActionPhaseContext {
1447 received_message: None,
1448 original_balance: original_balance(&state, &compute_phase),
1449 new_state: StateInit::default(),
1450 actions: actions.clone(),
1451 compute_phase: &compute_phase,
1452 inspector: None,
1453 })?;
1454
1455 assert_eq!(action_phase, ActionPhase {
1456 success: false,
1457 valid: false,
1458 result_code: ResultCode::ActionListInvalid as i32,
1459 result_arg: Some(1),
1460 action_list_hash: *actions.repr_hash(),
1461 ..empty_action_phase()
1462 });
1463 assert_eq!(action_fine, Tokens::ZERO);
1464 assert!(!state_exceeds_limits);
1465 assert!(!bounce);
1466 assert_eq!(state.total_fees, prev_total_fees);
1467 assert_eq!(state.balance, prev_balance);
1468 assert_eq!(state.end_lt, prev_end_lt);
1469 Ok(())
1470 }
1471
1472 #[test]
1473 fn invalid_action() -> Result<()> {
1474 let params = make_default_params();
1475 let config = make_default_config();
1476 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
1477
1478 let compute_phase = stub_compute_phase(OK_GAS);
1479 let prev_total_fees = state.total_fees;
1480 let prev_balance = state.balance.clone();
1481 let prev_end_lt = state.end_lt;
1482
1483 let set_code_action = {
1484 let mut b = CellBuilder::new();
1485 OutAction::SetCode {
1486 new_code: Cell::empty_cell(),
1487 }
1488 .store_into(&mut b, Cell::empty_context())?;
1489 b
1490 };
1491 let invalid_action = {
1492 let mut b = CellBuilder::new();
1493 b.store_u32(0xdeafbeaf)?;
1494 b
1495 };
1496
1497 let actions = make_action_list([
1498 set_code_action.as_full_slice(),
1499 set_code_action.as_full_slice(),
1500 invalid_action.as_full_slice(),
1501 set_code_action.as_full_slice(),
1502 set_code_action.as_full_slice(),
1503 ]);
1504
1505 let ActionPhaseFull {
1506 action_phase,
1507 action_fine,
1508 state_exceeds_limits,
1509 bounce,
1510 } = state.action_phase(ActionPhaseContext {
1511 received_message: None,
1512 original_balance: original_balance(&state, &compute_phase),
1513 new_state: StateInit::default(),
1514 actions: actions.clone(),
1515 compute_phase: &compute_phase,
1516 inspector: None,
1517 })?;
1518
1519 assert_eq!(action_phase, ActionPhase {
1520 success: false,
1521 valid: false,
1522 result_code: ResultCode::ActionInvalid as i32,
1523 result_arg: Some(2),
1524 action_list_hash: *actions.repr_hash(),
1525 total_actions: 5,
1526 ..empty_action_phase()
1527 });
1528 assert_eq!(action_fine, Tokens::ZERO);
1529 assert!(!state_exceeds_limits);
1530 assert!(!bounce);
1531 assert_eq!(state.total_fees, prev_total_fees);
1532 assert_eq!(state.balance, prev_balance);
1533 assert_eq!(state.end_lt, prev_end_lt);
1534 Ok(())
1535 }
1536
1537 #[test]
1538 fn strict_reserve_extra_currency() -> Result<()> {
1539 let mut params = make_default_params();
1540 params.strict_extra_currency = true;
1541 let config = make_default_config();
1542 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
1543 let prev_balance = state.balance.clone();
1544
1545 let compute_phase = stub_compute_phase(OK_GAS);
1546 let prev_total_fees = state.total_fees;
1547 let prev_end_lt = state.end_lt;
1548
1549 let actions = make_action_list([OutAction::ReserveCurrency {
1550 mode: ReserveCurrencyFlags::empty(),
1551 value: CurrencyCollection {
1552 tokens: Tokens::ZERO,
1553 other: BTreeMap::from_iter([(123u32, VarUint248::new(10))]).try_into()?,
1554 },
1555 }]);
1556
1557 let ActionPhaseFull {
1558 action_phase,
1559 action_fine,
1560 state_exceeds_limits,
1561 bounce,
1562 } = state.action_phase(ActionPhaseContext {
1563 received_message: None,
1564 original_balance: original_balance(&state, &compute_phase),
1565 new_state: StateInit::default(),
1566 actions: actions.clone(),
1567 compute_phase: &compute_phase,
1568 inspector: None,
1569 })?;
1570
1571 assert_eq!(action_phase, ActionPhase {
1572 success: false,
1573 valid: true,
1574 result_code: ResultCode::ActionInvalid as i32,
1575 result_arg: Some(0),
1576 action_list_hash: *actions.repr_hash(),
1577 total_actions: 1,
1578 ..empty_action_phase()
1579 });
1580 assert_eq!(action_fine, Tokens::ZERO);
1581 assert!(!state_exceeds_limits);
1582 assert!(!bounce);
1583 assert_eq!(state.total_fees, prev_total_fees);
1584 assert_eq!(state.balance, prev_balance);
1585 assert_eq!(state.end_lt, prev_end_lt);
1586 Ok(())
1587 }
1588
1589 #[test]
1590 fn send_single_message() -> Result<()> {
1591 let params = make_default_params();
1592 let config = make_default_config();
1593 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
1594
1595 let compute_phase = stub_compute_phase(OK_GAS);
1596 let prev_total_fees = state.total_fees;
1597 let prev_balance = state.balance.clone();
1598 let prev_end_lt = state.end_lt;
1599
1600 let msg_value = Tokens::new(500_000_000);
1601
1602 let actions = make_action_list([OutAction::SendMsg {
1603 mode: SendMsgFlags::empty(),
1604 out_msg: make_relaxed_message(
1605 RelaxedIntMsgInfo {
1606 dst: STUB_ADDR.into(),
1607 value: msg_value.into(),
1608 ..Default::default()
1609 },
1610 None,
1611 None,
1612 ),
1613 }]);
1614
1615 let ActionPhaseFull {
1616 action_phase,
1617 action_fine,
1618 state_exceeds_limits,
1619 bounce,
1620 } = state.action_phase(ActionPhaseContext {
1621 received_message: None,
1622 original_balance: original_balance(&state, &compute_phase),
1623 new_state: StateInit::default(),
1624 actions: actions.clone(),
1625 compute_phase: &compute_phase,
1626 inspector: None,
1627 })?;
1628
1629 assert_eq!(action_fine, Tokens::ZERO);
1630 assert!(!state_exceeds_limits);
1631 assert!(!bounce);
1632
1633 assert_eq!(state.out_msgs.len(), 1);
1634 assert_eq!(state.end_lt, prev_end_lt + 1);
1635 let last_msg = state.out_msgs.last().unwrap();
1636
1637 let msg_info = {
1638 let msg = last_msg.load()?;
1639 assert!(msg.init.is_none());
1640 assert_eq!(msg.body, Default::default());
1641 match msg.info {
1642 MsgInfo::Int(info) => info,
1643 e => panic!("unexpected msg info {e:?}"),
1644 }
1645 };
1646 assert_eq!(msg_info.src, STUB_ADDR.into());
1647 assert_eq!(msg_info.dst, STUB_ADDR.into());
1648 assert!(msg_info.ihr_disabled);
1649 assert!(!msg_info.bounce);
1650 assert!(!msg_info.bounced);
1651 assert_eq!(msg_info.created_at, params.block_unixtime);
1652 assert_eq!(msg_info.created_lt, prev_end_lt);
1653
1654 let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
1655 let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
1656
1657 assert_eq!(msg_info.value, (msg_value - expected_fwd_fees).into());
1658 assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
1659 assert_eq!(msg_info.ihr_fee, Tokens::ZERO);
1660
1661 assert_eq!(action_phase, ActionPhase {
1662 total_fwd_fees: Some(expected_fwd_fees),
1663 total_action_fees: Some(expected_first_frac),
1664 total_actions: 1,
1665 messages_created: 1,
1666 action_list_hash: *actions.repr_hash(),
1667 total_message_size: compute_full_stats(last_msg, ¶ms),
1668 ..empty_action_phase()
1669 });
1670
1671 assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
1672 assert_eq!(state.balance.other, prev_balance.other);
1673 assert_eq!(state.balance.tokens, prev_balance.tokens - msg_value);
1674
1675 Ok(())
1676 }
1677
1678 #[test]
1679 fn send_all_balance() -> Result<()> {
1680 let params = make_default_params();
1681 let config = make_default_config();
1682 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
1683
1684 let compute_phase = stub_compute_phase(OK_GAS);
1685 let prev_total_fees = state.total_fees;
1686 let prev_balance = state.balance.clone();
1687 let prev_end_lt = state.end_lt;
1688
1689 let actions = make_action_list([OutAction::SendMsg {
1690 mode: SendMsgFlags::ALL_BALANCE,
1691 out_msg: make_relaxed_message(
1692 RelaxedIntMsgInfo {
1693 dst: STUB_ADDR.into(),
1694 value: CurrencyCollection::ZERO,
1695 ..Default::default()
1696 },
1697 None,
1698 None,
1699 ),
1700 }]);
1701
1702 let ActionPhaseFull {
1703 action_phase,
1704 action_fine,
1705 state_exceeds_limits,
1706 bounce,
1707 } = state.action_phase(ActionPhaseContext {
1708 received_message: None,
1709 original_balance: original_balance(&state, &compute_phase),
1710 new_state: StateInit::default(),
1711 actions: actions.clone(),
1712 compute_phase: &compute_phase,
1713 inspector: None,
1714 })?;
1715
1716 assert_eq!(action_fine, Tokens::ZERO);
1717 assert!(!state_exceeds_limits);
1718 assert!(!bounce);
1719
1720 assert_eq!(state.out_msgs.len(), 1);
1721 assert_eq!(state.end_lt, prev_end_lt + 1);
1722 let last_msg = state.out_msgs.last().unwrap();
1723
1724 let msg_info = {
1725 let msg = last_msg.load()?;
1726 assert!(msg.init.is_none());
1727 assert_eq!(msg.body, Default::default());
1728 match msg.info {
1729 MsgInfo::Int(info) => info,
1730 e => panic!("unexpected msg info {e:?}"),
1731 }
1732 };
1733 assert_eq!(msg_info.src, STUB_ADDR.into());
1734 assert_eq!(msg_info.dst, STUB_ADDR.into());
1735 assert!(msg_info.ihr_disabled);
1736 assert!(!msg_info.bounce);
1737 assert!(!msg_info.bounced);
1738 assert_eq!(msg_info.created_at, params.block_unixtime);
1739 assert_eq!(msg_info.created_lt, prev_end_lt);
1740
1741 let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
1742 let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
1743
1744 assert_eq!(
1745 msg_info.value,
1746 (prev_balance.tokens - expected_fwd_fees).into()
1747 );
1748 assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
1749 assert_eq!(msg_info.ihr_fee, Tokens::ZERO);
1750
1751 assert_eq!(action_phase, ActionPhase {
1752 total_fwd_fees: Some(expected_fwd_fees),
1753 total_action_fees: Some(expected_first_frac),
1754 total_actions: 1,
1755 messages_created: 1,
1756 action_list_hash: *actions.repr_hash(),
1757 total_message_size: compute_full_stats(last_msg, ¶ms),
1758 ..empty_action_phase()
1759 });
1760
1761 assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
1762 assert_eq!(state.balance, CurrencyCollection::ZERO);
1763
1764 Ok(())
1765 }
1766
1767 #[test]
1768 fn strict_send_all_balance() -> Result<()> {
1769 let mut params = make_default_params();
1770 params.strict_extra_currency = true;
1771 let config = make_default_config();
1772
1773 let mut state =
1774 ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, CurrencyCollection {
1775 tokens: OK_BALANCE,
1776 other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1777 });
1778
1779 let compute_phase = stub_compute_phase(OK_GAS);
1780 let prev_total_fees = state.total_fees;
1781 let prev_balance = state.balance.clone();
1782 let prev_end_lt = state.end_lt;
1783
1784 let sent_value = CurrencyCollection {
1785 tokens: Tokens::ZERO,
1786 other: BTreeMap::from_iter([(0u32, VarUint248::new(1))]).try_into()?,
1787 };
1788
1789 let actions = make_action_list([OutAction::SendMsg {
1790 mode: SendMsgFlags::ALL_BALANCE,
1791 out_msg: make_relaxed_message(
1792 RelaxedIntMsgInfo {
1793 dst: STUB_ADDR.into(),
1794 value: sent_value.clone(),
1795 ..Default::default()
1796 },
1797 None,
1798 None,
1799 ),
1800 }]);
1801
1802 let ActionPhaseFull {
1803 action_phase,
1804 action_fine,
1805 state_exceeds_limits,
1806 bounce,
1807 } = state.action_phase(ActionPhaseContext {
1808 received_message: None,
1809 original_balance: original_balance(&state, &compute_phase),
1810 new_state: StateInit::default(),
1811 actions: actions.clone(),
1812 compute_phase: &compute_phase,
1813 inspector: None,
1814 })?;
1815
1816 assert_eq!(action_fine, Tokens::ZERO);
1817 assert!(!state_exceeds_limits);
1818 assert!(!bounce);
1819
1820 assert_eq!(state.out_msgs.len(), 1);
1821 assert_eq!(state.end_lt, prev_end_lt + 1);
1822 let last_msg = state.out_msgs.last().unwrap();
1823
1824 let msg_info = {
1825 let msg = last_msg.load()?;
1826 assert!(msg.init.is_none());
1827 assert_eq!(msg.body, Default::default());
1828 match msg.info {
1829 MsgInfo::Int(info) => info,
1830 e => panic!("unexpected msg info {e:?}"),
1831 }
1832 };
1833 assert_eq!(msg_info.src, STUB_ADDR.into());
1834 assert_eq!(msg_info.dst, STUB_ADDR.into());
1835 assert!(msg_info.ihr_disabled);
1836 assert!(!msg_info.bounce);
1837 assert!(!msg_info.bounced);
1838 assert_eq!(msg_info.created_at, params.block_unixtime);
1839 assert_eq!(msg_info.created_lt, prev_end_lt);
1840
1841 let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
1842 let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
1843
1844 assert_eq!(msg_info.value, CurrencyCollection {
1845 tokens: prev_balance.tokens - expected_fwd_fees,
1846 other: sent_value.other.clone(),
1847 });
1848 assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
1849 assert_eq!(msg_info.ihr_fee, Tokens::ZERO);
1850
1851 assert_eq!(action_phase, ActionPhase {
1852 total_fwd_fees: Some(expected_fwd_fees),
1853 total_action_fees: Some(expected_first_frac),
1854 total_actions: 1,
1855 messages_created: 1,
1856 action_list_hash: *actions.repr_hash(),
1857 total_message_size: compute_full_stats(last_msg, ¶ms),
1858 ..empty_action_phase()
1859 });
1860
1861 assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
1862 assert_eq!(state.balance.tokens, Tokens::ZERO);
1863 assert_eq!(
1864 state.balance.other,
1865 prev_balance.other.checked_sub(&sent_value.other)?
1866 );
1867
1868 Ok(())
1869 }
1870
1871 #[test]
1872 fn strict_send_all_balance_destroy() -> Result<()> {
1873 struct TestCase {
1874 balance: CurrencyCollection,
1875 to_send: CurrencyCollection,
1876 expected_end_status: AccountStatus,
1877 }
1878
1879 let mut params = make_default_params();
1880 params.strict_extra_currency = true;
1881 let config = make_default_config();
1882
1883 for TestCase {
1884 balance,
1885 to_send,
1886 expected_end_status,
1887 } in [
1888 TestCase {
1890 balance: OK_BALANCE.into(),
1891 to_send: CurrencyCollection::ZERO,
1892 expected_end_status: AccountStatus::NotExists,
1893 },
1894 TestCase {
1896 balance: CurrencyCollection {
1897 tokens: OK_BALANCE,
1898 other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1899 },
1900 to_send: CurrencyCollection::ZERO,
1901 expected_end_status: AccountStatus::Uninit,
1902 },
1903 TestCase {
1905 balance: CurrencyCollection {
1906 tokens: OK_BALANCE,
1907 other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1908 },
1909 to_send: CurrencyCollection::ZERO,
1910 expected_end_status: AccountStatus::Uninit,
1911 },
1912 TestCase {
1914 balance: CurrencyCollection {
1915 tokens: OK_BALANCE,
1916 other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1917 },
1918 to_send: CurrencyCollection {
1919 tokens: OK_BALANCE,
1920 other: BTreeMap::from_iter([(0u32, VarUint248::new(1))]).try_into()?,
1921 },
1922 expected_end_status: AccountStatus::Uninit,
1923 },
1924 TestCase {
1926 balance: CurrencyCollection {
1927 tokens: OK_BALANCE,
1928 other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1929 },
1930 to_send: CurrencyCollection {
1931 tokens: OK_BALANCE,
1932 other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1933 },
1934 expected_end_status: AccountStatus::NotExists,
1935 },
1936 ] {
1937 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, balance);
1938
1939 let compute_phase = stub_compute_phase(OK_GAS);
1940 let prev_total_fees = state.total_fees;
1941 let prev_balance = state.balance.clone();
1942 let prev_end_lt = state.end_lt;
1943
1944 let actions = make_action_list([OutAction::SendMsg {
1945 mode: SendMsgFlags::ALL_BALANCE | SendMsgFlags::DELETE_IF_EMPTY,
1946 out_msg: make_relaxed_message(
1947 RelaxedIntMsgInfo {
1948 dst: STUB_ADDR.into(),
1949 value: to_send.clone(),
1950 ..Default::default()
1951 },
1952 None,
1953 None,
1954 ),
1955 }]);
1956
1957 let ActionPhaseFull {
1958 action_phase,
1959 action_fine,
1960 state_exceeds_limits,
1961 bounce,
1962 } = state.action_phase(ActionPhaseContext {
1963 received_message: None,
1964 original_balance: original_balance(&state, &compute_phase),
1965 new_state: StateInit::default(),
1966 actions: actions.clone(),
1967 compute_phase: &compute_phase,
1968 inspector: None,
1969 })?;
1970
1971 assert_eq!(action_fine, Tokens::ZERO);
1972 assert!(!state_exceeds_limits);
1973 assert!(!bounce);
1974
1975 assert_eq!(state.end_status, expected_end_status);
1976 assert_eq!(state.out_msgs.len(), 1);
1977 assert_eq!(state.end_lt, prev_end_lt + 1);
1978 let last_msg = state.out_msgs.last().unwrap();
1979
1980 let msg_info = {
1981 let msg = last_msg.load()?;
1982 assert!(msg.init.is_none());
1983 assert_eq!(msg.body, Default::default());
1984 match msg.info {
1985 MsgInfo::Int(info) => info,
1986 e => panic!("unexpected msg info {e:?}"),
1987 }
1988 };
1989 assert_eq!(msg_info.src, STUB_ADDR.into());
1990 assert_eq!(msg_info.dst, STUB_ADDR.into());
1991 assert!(msg_info.ihr_disabled);
1992 assert!(!msg_info.bounce);
1993 assert!(!msg_info.bounced);
1994 assert_eq!(msg_info.created_at, params.block_unixtime);
1995 assert_eq!(msg_info.created_lt, prev_end_lt);
1996
1997 let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
1998 let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
1999
2000 assert_eq!(msg_info.value, CurrencyCollection {
2001 tokens: prev_balance.tokens - expected_fwd_fees,
2002 other: to_send.other.clone(),
2003 });
2004 assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
2005 assert_eq!(msg_info.ihr_fee, Tokens::ZERO);
2006
2007 assert_eq!(action_phase, ActionPhase {
2008 total_fwd_fees: Some(expected_fwd_fees),
2009 total_action_fees: Some(expected_first_frac),
2010 total_actions: 1,
2011 messages_created: 1,
2012 action_list_hash: *actions.repr_hash(),
2013 total_message_size: compute_full_stats(last_msg, ¶ms),
2014 status_change: AccountStatusChange::Deleted,
2015 ..empty_action_phase()
2016 });
2017
2018 assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
2019 assert_eq!(state.balance.tokens, Tokens::ZERO);
2020 assert_eq!(
2021 state.balance.other,
2022 prev_balance.other.checked_sub(&to_send.other)?
2023 );
2024 }
2025
2026 Ok(())
2027 }
2028
2029 #[test]
2030 fn send_all_not_reserved() -> Result<()> {
2031 let params = make_default_params();
2032 let config = make_default_config();
2033 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
2034
2035 let compute_phase = stub_compute_phase(OK_GAS);
2036 let prev_total_fees = state.total_fees;
2037 let prev_end_lt = state.end_lt;
2038
2039 let expected_balance = CurrencyCollection::from(state.balance.tokens / 4);
2040 let actions = make_action_list([
2041 OutAction::ReserveCurrency {
2042 mode: ReserveCurrencyFlags::empty(),
2043 value: expected_balance.clone(),
2044 },
2045 OutAction::SendMsg {
2046 mode: SendMsgFlags::ALL_BALANCE,
2047 out_msg: make_relaxed_message(
2048 RelaxedIntMsgInfo {
2049 dst: STUB_ADDR.into(),
2050 value: CurrencyCollection::ZERO,
2051 ..Default::default()
2052 },
2053 None,
2054 None,
2055 ),
2056 },
2057 ]);
2058
2059 let ActionPhaseFull {
2060 action_phase,
2061 action_fine,
2062 state_exceeds_limits,
2063 bounce,
2064 } = state.action_phase(ActionPhaseContext {
2065 received_message: None,
2066 original_balance: original_balance(&state, &compute_phase),
2067 new_state: StateInit::default(),
2068 actions: actions.clone(),
2069 compute_phase: &compute_phase,
2070 inspector: None,
2071 })?;
2072
2073 assert_eq!(state.out_msgs.len(), 1);
2074 assert_eq!(state.end_lt, prev_end_lt + 1);
2075 let last_msg = state.out_msgs.last().unwrap();
2076
2077 let msg_info = {
2078 let msg = last_msg.load()?;
2079 assert!(msg.init.is_none());
2080 assert_eq!(msg.body, Default::default());
2081 match msg.info {
2082 MsgInfo::Int(info) => info,
2083 e => panic!("unexpected msg info {e:?}"),
2084 }
2085 };
2086 assert_eq!(msg_info.src, STUB_ADDR.into());
2087 assert_eq!(msg_info.dst, STUB_ADDR.into());
2088 assert!(msg_info.ihr_disabled);
2089 assert!(!msg_info.bounce);
2090 assert!(!msg_info.bounced);
2091 assert_eq!(msg_info.created_at, params.block_unixtime);
2092 assert_eq!(msg_info.created_lt, prev_end_lt);
2093
2094 let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
2095 let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
2096
2097 assert_eq!(
2098 msg_info.value,
2099 (OK_BALANCE * 3 / 4 - expected_fwd_fees).into()
2100 );
2101 assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
2102 assert_eq!(msg_info.ihr_fee, Tokens::ZERO);
2103
2104 assert_eq!(action_phase, ActionPhase {
2105 total_fwd_fees: Some(expected_fwd_fees),
2106 total_action_fees: Some(expected_first_frac),
2107 total_actions: 2,
2108 messages_created: 1,
2109 special_actions: 1,
2110 action_list_hash: *actions.repr_hash(),
2111 total_message_size: compute_full_stats(last_msg, ¶ms),
2112 ..empty_action_phase()
2113 });
2114 assert_eq!(action_fine, Tokens::ZERO);
2115 assert!(!state_exceeds_limits);
2116 assert!(!bounce);
2117
2118 assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
2119 assert_eq!(state.balance, expected_balance);
2120 Ok(())
2121 }
2122
2123 #[test]
2124 fn set_code() -> Result<()> {
2125 let params = make_default_params();
2126 let config = make_default_config();
2127
2128 let orig_data = CellBuilder::build_from(u32::MIN)?;
2129 let final_data = CellBuilder::build_from(u32::MAX)?;
2130
2131 let temp_code = Boc::decode(tvmasm!("NOP NOP"))?;
2132 let final_code = Boc::decode(tvmasm!("NOP"))?;
2133
2134 let mut state = ExecutorState::new_active(
2135 ¶ms,
2136 &config,
2137 &STUB_ADDR,
2138 OK_BALANCE,
2139 orig_data,
2140 tvmasm!("ACCEPT"),
2141 );
2142
2143 let compute_phase = stub_compute_phase(OK_GAS);
2144 let prev_total_fees = state.total_fees;
2145 let prev_balance = state.balance.clone();
2146
2147 let actions = make_action_list([
2148 OutAction::SetCode {
2149 new_code: temp_code,
2150 },
2151 OutAction::SetCode {
2152 new_code: final_code.clone(),
2153 },
2154 ]);
2155
2156 let AccountState::Active(mut new_state) = state.state.clone() else {
2157 panic!("unexpected account state");
2158 };
2159 new_state.data = Some(final_data.clone());
2160
2161 let ActionPhaseFull {
2162 action_phase,
2163 action_fine,
2164 state_exceeds_limits,
2165 bounce,
2166 } = state.action_phase(ActionPhaseContext {
2167 received_message: None,
2168 original_balance: original_balance(&state, &compute_phase),
2169 new_state,
2170 actions: actions.clone(),
2171 compute_phase: &compute_phase,
2172 inspector: None,
2173 })?;
2174
2175 assert_eq!(action_phase, ActionPhase {
2176 total_actions: 2,
2177 special_actions: 2,
2178 action_list_hash: *actions.repr_hash(),
2179 ..empty_action_phase()
2180 });
2181 assert_eq!(action_fine, Tokens::ZERO);
2182 assert!(!state_exceeds_limits);
2183 assert!(!bounce);
2184 assert_eq!(state.end_status, AccountStatus::Active);
2185 assert_eq!(
2186 state.state,
2187 AccountState::Active(StateInit {
2188 code: Some(final_code),
2189 data: Some(final_data),
2190 ..Default::default()
2191 })
2192 );
2193 assert_eq!(state.total_fees, prev_total_fees);
2194 assert_eq!(state.balance, prev_balance);
2195 Ok(())
2196 }
2197
2198 #[test]
2199 fn invalid_dst_addr() -> Result<()> {
2200 let params = make_default_params();
2201 let config = make_default_config();
2202
2203 let targets = [
2204 IntAddr::Std(StdAddr::new(123, HashBytes::ZERO)),
2206 IntAddr::Std({
2208 let mut addr = STUB_ADDR;
2209 let mut b = CellBuilder::new();
2210 b.store_u16(0xaabb)?;
2211 addr.anycast = Some(Box::new(Anycast::from_slice(&b.as_data_slice())?));
2212 addr
2213 }),
2214 IntAddr::Var(VarAddr {
2216 anycast: None,
2217 address_len: Uint9::new(80),
2218 workchain: 0,
2219 address: vec![0; 10],
2220 }),
2221 ];
2222
2223 for dst in targets {
2224 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
2225
2226 let compute_phase = stub_compute_phase(OK_GAS);
2227 let prev_total_fees = state.total_fees;
2228 let prev_balance = state.balance.clone();
2229 let prev_end_lt = state.end_lt;
2230
2231 let actions = make_action_list([OutAction::SendMsg {
2232 mode: SendMsgFlags::ALL_BALANCE,
2233 out_msg: make_relaxed_message(
2234 RelaxedIntMsgInfo {
2235 dst,
2236 ..Default::default()
2237 },
2238 None,
2239 None,
2240 ),
2241 }]);
2242
2243 let ActionPhaseFull {
2244 action_phase,
2245 action_fine,
2246 state_exceeds_limits,
2247 bounce,
2248 } = state.action_phase(ActionPhaseContext {
2249 received_message: None,
2250 original_balance: original_balance(&state, &compute_phase),
2251 new_state: StateInit::default(),
2252 actions: actions.clone(),
2253 compute_phase: &compute_phase,
2254 inspector: None,
2255 })?;
2256
2257 assert_eq!(action_phase, ActionPhase {
2258 success: false,
2259 total_actions: 1,
2260 messages_created: 0,
2261 result_code: ResultCode::InvalidDstAddr as _,
2262 result_arg: Some(0),
2263 action_list_hash: *actions.repr_hash(),
2264 ..empty_action_phase()
2265 });
2266 assert_eq!(action_fine, Tokens::ZERO);
2267 assert!(!state_exceeds_limits);
2268 assert!(!bounce);
2269
2270 assert!(state.out_msgs.is_empty());
2271 assert_eq!(state.end_lt, prev_end_lt);
2272
2273 assert_eq!(state.total_fees, prev_total_fees);
2274 assert_eq!(state.balance, prev_balance);
2275 }
2276 Ok(())
2277 }
2278
2279 #[test]
2280 fn cant_pay_fwd_fee() -> Result<()> {
2281 let params = make_default_params();
2282 let config = make_default_config();
2283 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, Tokens::new(50000));
2284
2285 let compute_phase = stub_compute_phase(OK_GAS);
2286 let prev_balance = state.balance.clone();
2287 let prev_total_fee = state.total_fees;
2288 let prev_end_lt = state.end_lt;
2289
2290 let actions = make_action_list([OutAction::SendMsg {
2291 mode: SendMsgFlags::PAY_FEE_SEPARATELY,
2292 out_msg: make_relaxed_message(
2293 RelaxedIntMsgInfo {
2294 value: CurrencyCollection::ZERO,
2295 dst: STUB_ADDR.into(),
2296 ..Default::default()
2297 },
2298 None,
2299 Some({
2300 let mut b = CellBuilder::new();
2301 b.store_reference(Cell::empty_cell())?;
2302 b.store_reference(CellBuilder::build_from(0xdeafbeafu32)?)?;
2303 b
2304 }),
2305 ),
2306 }]);
2307
2308 let ActionPhaseFull {
2309 action_phase,
2310 action_fine,
2311 state_exceeds_limits,
2312 bounce,
2313 } = state.action_phase(ActionPhaseContext {
2314 received_message: None,
2315 original_balance: original_balance(&state, &compute_phase),
2316 new_state: StateInit::default(),
2317 actions: actions.clone(),
2318 compute_phase: &compute_phase,
2319 inspector: None,
2320 })?;
2321
2322 assert_eq!(action_phase, ActionPhase {
2323 success: false,
2324 no_funds: true,
2325 result_code: ResultCode::NotEnoughBalance as _,
2326 result_arg: Some(0),
2327 total_actions: 1,
2328 total_action_fees: Some(prev_balance.tokens),
2329 action_list_hash: *actions.repr_hash(),
2330 ..empty_action_phase()
2331 });
2332 assert_eq!(action_fine, prev_balance.tokens);
2333 assert!(!state_exceeds_limits);
2334 assert!(!bounce);
2335
2336 assert_eq!(state.balance, CurrencyCollection::ZERO);
2337 assert_eq!(
2338 state.total_fees,
2339 prev_total_fee + action_phase.total_action_fees.unwrap_or_default()
2340 );
2341 assert!(state.out_msgs.is_empty());
2342 assert_eq!(state.end_lt, prev_end_lt);
2343 Ok(())
2344 }
2345
2346 #[test]
2347 fn rewrite_message() -> Result<()> {
2348 let params = make_default_params();
2349 let config = make_default_config();
2350 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
2351
2352 let compute_phase = stub_compute_phase(OK_GAS);
2353 let prev_balance = state.balance.clone();
2354 let prev_total_fee = state.total_fees;
2355 let prev_end_lt = state.end_lt;
2356
2357 let msg_body = {
2358 let mut b = CellBuilder::new();
2359 b.store_zeros(600)?;
2360 b.store_reference(Cell::empty_cell())?;
2361 b
2362 };
2363
2364 let actions = make_action_list([OutAction::SendMsg {
2365 mode: SendMsgFlags::PAY_FEE_SEPARATELY,
2366 out_msg: make_relaxed_message(
2367 RelaxedIntMsgInfo {
2368 value: CurrencyCollection::ZERO,
2369 dst: STUB_ADDR.into(),
2370 ..Default::default()
2371 },
2372 None,
2373 Some(msg_body.clone()),
2374 ),
2375 }]);
2376
2377 let ActionPhaseFull {
2378 action_phase,
2379 action_fine,
2380 state_exceeds_limits,
2381 bounce,
2382 } = state.action_phase(ActionPhaseContext {
2383 received_message: None,
2384 original_balance: original_balance(&state, &compute_phase),
2385 new_state: StateInit::default(),
2386 actions: actions.clone(),
2387 compute_phase: &compute_phase,
2388 inspector: None,
2389 })?;
2390
2391 assert_eq!(state.out_msgs.len(), 1);
2392 let last_msg = state.out_msgs.last().unwrap();
2393 let msg = last_msg.load()?;
2394 assert_eq!(
2395 msg.layout,
2396 Some(MessageLayout {
2397 init_to_cell: false,
2398 body_to_cell: true,
2399 })
2400 );
2401 assert_eq!(msg.body.1, msg_body.build()?);
2402
2403 let MsgInfo::Int(info) = msg.info else {
2404 panic!("expected an internal message");
2405 };
2406
2407 let expected_fwd_fees = config.fwd_prices.compute_fwd_fee(CellTreeStats {
2408 bit_count: 600,
2409 cell_count: 2,
2410 });
2411 let first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
2412
2413 assert_eq!(action_phase, ActionPhase {
2414 total_actions: 1,
2415 messages_created: 1,
2416 total_fwd_fees: Some(expected_fwd_fees),
2417 total_action_fees: Some(first_frac),
2418 action_list_hash: *actions.repr_hash(),
2419 total_message_size: compute_full_stats(last_msg, ¶ms),
2420 ..empty_action_phase()
2421 });
2422 assert_eq!(action_fine, Tokens::ZERO);
2423 assert!(!state_exceeds_limits);
2424 assert!(!bounce);
2425
2426 assert_eq!(state.end_lt, prev_end_lt + 1);
2427 assert_eq!(
2428 state.total_fees,
2429 prev_total_fee + action_phase.total_action_fees.unwrap_or_default()
2430 );
2431 assert_eq!(state.balance.other, prev_balance.other);
2432 assert_eq!(
2433 state.balance.tokens,
2434 prev_balance.tokens - info.value.tokens - expected_fwd_fees
2435 );
2436 Ok(())
2437 }
2438
2439 #[test]
2440 fn change_lib() -> Result<()> {
2441 struct TestCase {
2442 libraries: Dict<HashBytes, SimpleLib>,
2443 target: Dict<HashBytes, SimpleLib>,
2444 changes: Vec<(ChangeLibraryMode, LibRef)>,
2445 diff: Vec<PublicLibraryChange>,
2446 }
2447
2448 fn make_lib(id: u32) -> Cell {
2449 CellBuilder::build_from(id).unwrap()
2450 }
2451
2452 fn make_lib_hash(id: u32) -> HashBytes {
2453 *CellBuilder::build_from(id).unwrap().repr_hash()
2454 }
2455
2456 fn make_libs<I>(items: I) -> Dict<HashBytes, SimpleLib>
2457 where
2458 I: IntoIterator<Item = (u32, bool)>,
2459 {
2460 let mut items = items
2461 .into_iter()
2462 .map(|(id, public)| {
2463 let root = make_lib(id);
2464 (*root.repr_hash(), SimpleLib { root, public })
2465 })
2466 .collect::<Vec<_>>();
2467 items.sort_by(|(a, _), (b, _)| a.cmp(b));
2468 Dict::try_from_sorted_slice(&items).unwrap()
2469 }
2470
2471 let params = make_default_params();
2472 let config = make_default_config();
2473
2474 let test_cases = vec![
2475 TestCase {
2477 libraries: Dict::new(),
2478 target: Dict::new(),
2479 changes: Vec::new(),
2480 diff: Vec::new(),
2481 },
2482 TestCase {
2484 libraries: Dict::new(),
2485 target: make_libs([(123, false)]),
2486 changes: vec![(ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123)))],
2487 diff: Vec::new(),
2488 },
2489 TestCase {
2490 libraries: Dict::new(),
2491 target: make_libs([(123, false), (234, false)]),
2492 changes: vec![
2493 (ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123))),
2494 (ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(234))),
2495 ],
2496 diff: Vec::new(),
2497 },
2498 TestCase {
2499 libraries: make_libs([(123, false)]),
2500 target: make_libs([(123, false)]),
2501 changes: vec![(ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123)))],
2502 diff: Vec::new(),
2503 },
2504 TestCase {
2505 libraries: make_libs([(123, false)]),
2506 target: make_libs([(123, false)]),
2507 changes: vec![(
2508 ChangeLibraryMode::ADD_PRIVATE,
2509 LibRef::Hash(make_lib_hash(123)),
2510 )],
2511 diff: Vec::new(),
2512 },
2513 TestCase {
2515 libraries: Dict::new(),
2516 target: make_libs([(123, true)]),
2517 changes: vec![(ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123)))],
2518 diff: vec![PublicLibraryChange::Add(make_lib(123))],
2519 },
2520 TestCase {
2521 libraries: Dict::new(),
2522 target: make_libs([(123, true), (234, true)]),
2523 changes: vec![
2524 (ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123))),
2525 (ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(234))),
2526 ],
2527 diff: vec![
2528 PublicLibraryChange::Add(make_lib(123)),
2529 PublicLibraryChange::Add(make_lib(234)),
2530 ],
2531 },
2532 TestCase {
2533 libraries: make_libs([(123, true)]),
2534 target: make_libs([(123, true)]),
2535 changes: vec![(ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123)))],
2536 diff: Vec::new(),
2537 },
2538 TestCase {
2539 libraries: make_libs([(123, true)]),
2540 target: make_libs([(123, true)]),
2541 changes: vec![(
2542 ChangeLibraryMode::ADD_PUBLIC,
2543 LibRef::Hash(make_lib_hash(123)),
2544 )],
2545 diff: Vec::new(),
2546 },
2547 TestCase {
2549 libraries: Dict::new(),
2550 target: Dict::new(),
2551 changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Cell(make_lib(123)))],
2553 diff: Vec::new(),
2554 },
2555 TestCase {
2556 libraries: Dict::new(),
2557 target: Dict::new(),
2558 changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Hash(make_lib_hash(123)))],
2560 diff: Vec::new(),
2561 },
2562 TestCase {
2563 libraries: make_libs([(123, false)]),
2564 target: Dict::new(),
2565 changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Cell(make_lib(123)))],
2566 diff: Vec::new(),
2567 },
2568 TestCase {
2569 libraries: make_libs([(123, false)]),
2570 target: Dict::new(),
2571 changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Hash(make_lib_hash(123)))],
2572 diff: Vec::new(),
2573 },
2574 TestCase {
2575 libraries: make_libs([(123, true)]),
2576 target: Dict::new(),
2577 changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Cell(make_lib(123)))],
2578 diff: vec![PublicLibraryChange::Remove(make_lib_hash(123))],
2579 },
2580 TestCase {
2582 libraries: make_libs([(123, false)]),
2583 target: make_libs([(123, true)]),
2584 changes: vec![(ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123)))],
2585 diff: vec![PublicLibraryChange::Add(make_lib(123))],
2586 },
2587 TestCase {
2588 libraries: make_libs([(123, false)]),
2589 target: make_libs([(123, true)]),
2590 changes: vec![(
2591 ChangeLibraryMode::ADD_PUBLIC,
2592 LibRef::Hash(make_lib_hash(123)),
2593 )],
2594 diff: vec![PublicLibraryChange::Add(make_lib(123))],
2595 },
2596 TestCase {
2597 libraries: make_libs([(123, true)]),
2598 target: make_libs([(123, false)]),
2599 changes: vec![(ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123)))],
2600 diff: vec![PublicLibraryChange::Remove(make_lib_hash(123))],
2601 },
2602 TestCase {
2603 libraries: make_libs([(123, true)]),
2604 target: make_libs([(123, false)]),
2605 changes: vec![(
2606 ChangeLibraryMode::ADD_PRIVATE,
2607 LibRef::Hash(make_lib_hash(123)),
2608 )],
2609 diff: vec![PublicLibraryChange::Remove(make_lib_hash(123))],
2610 },
2611 ];
2612
2613 for TestCase {
2614 libraries,
2615 target,
2616 changes,
2617 diff,
2618 } in test_cases
2619 {
2620 let mut state = ExecutorState::new_active(
2621 ¶ms,
2622 &config,
2623 &StdAddr::new(-1, HashBytes::ZERO),
2624 OK_BALANCE,
2625 Cell::empty_cell(),
2626 tvmasm!("ACCEPT"),
2627 );
2628
2629 let target_state_init = match &mut state.state {
2630 AccountState::Active(state_init) => {
2631 state_init.libraries = libraries;
2632 StateInit {
2633 libraries: target,
2634 ..state_init.clone()
2635 }
2636 }
2637 AccountState::Uninit | AccountState::Frozen(..) => panic!("invalid initial state"),
2638 };
2639
2640 let compute_phase = stub_compute_phase(OK_GAS);
2641 let prev_total_fees = state.total_fees;
2642 let prev_balance = state.balance.clone();
2643
2644 let change_count = changes.len();
2645 let actions = make_action_list(
2646 changes
2647 .into_iter()
2648 .map(|(mode, lib)| OutAction::ChangeLibrary { mode, lib }),
2649 );
2650
2651 let mut inspector = ExecutorInspector::default();
2652
2653 let ActionPhaseFull {
2654 action_phase,
2655 action_fine,
2656 state_exceeds_limits,
2657 bounce,
2658 } = state.action_phase(ActionPhaseContext {
2659 received_message: None,
2660 original_balance: original_balance(&state, &compute_phase),
2661 new_state: match state.state.clone() {
2662 AccountState::Active(state_init) => state_init,
2663 AccountState::Uninit | AccountState::Frozen(..) => Default::default(),
2664 },
2665 actions: actions.clone(),
2666 compute_phase: &compute_phase,
2667 inspector: Some(&mut inspector),
2668 })?;
2669
2670 assert_eq!(action_phase, ActionPhase {
2671 total_actions: change_count as u16,
2672 special_actions: change_count as u16,
2673 action_list_hash: *actions.repr_hash(),
2674 ..empty_action_phase()
2675 });
2676 assert_eq!(action_fine, Tokens::ZERO);
2677 assert!(!state_exceeds_limits);
2678 assert!(!bounce);
2679 assert_eq!(state.end_status, AccountStatus::Active);
2680 assert_eq!(state.state, AccountState::Active(target_state_init));
2681 assert_eq!(state.total_fees, prev_total_fees);
2682 assert_eq!(state.balance, prev_balance);
2683 assert_eq!(inspector.public_libs_diff, diff);
2684 }
2685 Ok(())
2686 }
2687}