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 && cs_parsed.is_empty()
131 {
132 parsed_list.push(Some(item));
134 continue;
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 && body_cs.size_bits() > 1
415 && !body_cs.get_bit(0).unwrap()
416 {
417 rewritten_body_cs = rewrite_body_to_cell(body_cs);
419 body_cs = rewritten_body_cs.as_full_slice();
420 }
421
422 let mut use_mc_prices = self.address.is_masterchain();
424 match &mut relaxed_info {
425 RelaxedMsgInfo::Int(info) => {
427 if !check_rewrite_src_addr(&self.address, &mut info.src) {
429 ctx.action_phase.result_code = ResultCode::InvalidSrcAddr as i32;
431 return Err(ActionFailed);
432 };
433
434 if !check_rewrite_dst_addr(&self.config.workchains, &mut info.dst) {
436 return check_skip_invalid(ResultCode::InvalidDstAddr, ctx);
437 }
438 use_mc_prices |= info.dst.is_masterchain();
439
440 if self.params.strict_extra_currency {
442 match normalize_extra_balance(
443 std::mem::take(&mut info.value.other),
444 MAX_MSG_EXTRA_CURRENCIES,
445 ) {
446 Ok(other) => info.value.other = other,
447 Err(BalanceExtraError::InvalidDict(_)) => {
448 return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
449 }
450 Err(BalanceExtraError::OutOfLimit) => {
451 return check_skip_invalid(ResultCode::TooManyExtraCurrencies, ctx);
452 }
453 }
454 }
455
456 info.ihr_fee = Tokens::ZERO;
458 info.fwd_fee = Tokens::ZERO;
459
460 info.created_at = self.params.block_unixtime;
462 info.created_lt = ctx.end_lt;
463
464 info.ihr_disabled = true;
466 info.bounced = false;
467 }
468 RelaxedMsgInfo::ExtOut(info) => {
470 if mode.bits() & !EXT_MSG_MASK != 0 {
471 return Err(ActionFailed);
473 }
474
475 if !check_rewrite_src_addr(&self.address, &mut info.src) {
477 ctx.action_phase.result_code = ResultCode::InvalidSrcAddr as i32;
478 return Err(ActionFailed);
479 }
480
481 info.created_at = self.params.block_unixtime;
483 info.created_lt = ctx.end_lt;
484 }
485 };
486
487 let prices = self.config.fwd_prices(use_mc_prices);
489 let mut max_cell_count = self.config.size_limits.max_msg_cells;
490 let fine_per_cell;
491 if self.is_special {
492 fine_per_cell = 0;
493 } else {
494 fine_per_cell = (prices.cell_price >> 16) / 4;
495
496 let mut funds = ctx.remaining_balance.tokens;
497 if let RelaxedMsgInfo::Int(info) = &relaxed_info
498 && !mode.contains(SendMsgFlags::ALL_BALANCE)
499 && !mode.contains(SendMsgFlags::PAY_FEE_SEPARATELY)
500 {
501 let mut new_funds = info.value.tokens;
502
503 if mode.contains(SendMsgFlags::WITH_REMAINING_BALANCE)
504 && (|| {
505 let msg_balance_remaining = match &ctx.received_message {
506 Some(msg) => msg.balance_remaining.tokens,
507 None => Tokens::ZERO,
508 };
509 new_funds.try_add_assign(msg_balance_remaining)?;
510 new_funds.try_sub_assign(ctx.compute_phase.gas_fees)?;
511 new_funds.try_sub_assign(*ctx.action_fine)?;
512
513 Ok::<_, tycho_types::error::Error>(())
514 })()
515 .is_err()
516 {
517 return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
518 }
519
520 funds = std::cmp::min(funds, new_funds);
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 && let RelaxedMsgInfo::Int(int) = &relaxed_info
563 && let Some(cell) = int.value.other.as_dict().root()
564 && !stats.add_cell(cell.as_ref())
565 {
566 break 'valid;
567 }
568
569 break 'stats stats.stats();
570 }
571
572 collect_fine(stats.cells, ctx)?;
573 return check_skip_invalid(ResultCode::MessageOutOfLimits, ctx);
574 };
575
576 let check_skip_invalid = move |e: ResultCode, ctx: &mut ActionContext<'_>| {
578 collect_fine(stats.cell_count as _, ctx)?;
579 check_skip_invalid(e, ctx)
580 };
581
582 let fwd_fee = if self.is_special {
584 Tokens::ZERO
585 } else {
586 prices.compute_fwd_fee(stats)
587 };
588
589 let msg;
591 let fees_collected;
592 match &mut relaxed_info {
593 RelaxedMsgInfo::Int(info) => {
594 let value_to_pay = match ctx.rewrite_message_value(&mut info.value, mode, fwd_fee) {
596 Ok(total_value) => total_value,
597 Err(_) => return check_skip_invalid(ResultCode::NotEnoughBalance, ctx),
598 };
599
600 if ctx.remaining_balance.tokens < value_to_pay {
602 return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
603 }
604
605 if self.params.strict_extra_currency
607 && !check_extra_balance(&info.value.other, MAX_MSG_EXTRA_CURRENCIES)
608 {
609 return check_skip_invalid(ResultCode::TooManyExtraCurrencies, ctx);
610 }
611
612 if self.params.authority_marks_enabled
614 && !self.is_marks_authority
615 && let Some(marks) = &self.config.authority_marks
616 && marks.has_authority_marks_in(&info.value)?
617 {
618 return check_skip_invalid(ResultCode::NotEnoughExtraBalance, ctx);
619 }
620
621 let other = match ctx.remaining_balance.other.checked_sub(&info.value.other) {
623 Ok(other) => other,
624 Err(_) => return check_skip_invalid(ResultCode::NotEnoughExtraBalance, ctx),
625 };
626
627 fees_collected = prices.get_first_part(fwd_fee);
629 info.fwd_fee = fwd_fee - fees_collected;
630
631 msg = match build_message(&relaxed_info, &state_init_cs, &body_cs) {
633 Ok(msg) => msg,
634 Err(_) => match MessageRewrite::next(rewrite) {
635 Some(rewrite) => return Ok(SendMsgResult::Rewrite(rewrite)),
636 None => return check_skip_invalid(ResultCode::FailedToFitMessage, ctx),
637 },
638 };
639
640 if let Some(msg) = &mut ctx.received_message
642 && (mode.contains(SendMsgFlags::ALL_BALANCE)
643 || mode.contains(SendMsgFlags::WITH_REMAINING_BALANCE))
644 {
645 if self.params.strict_extra_currency {
646 msg.balance_remaining.tokens = Tokens::ZERO;
648 } else {
649 msg.balance_remaining = CurrencyCollection::ZERO;
651 }
652 }
653
654 ctx.remaining_balance.tokens -= value_to_pay;
656 ctx.remaining_balance.other = other;
657 }
658 RelaxedMsgInfo::ExtOut(_) => {
659 if ctx.remaining_balance.tokens < fwd_fee {
661 return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
662 }
663
664 msg = match build_message(&relaxed_info, &state_init_cs, &body_cs) {
666 Ok(msg) => msg,
667 Err(_) => match MessageRewrite::next(rewrite) {
668 Some(rewrite) => return Ok(SendMsgResult::Rewrite(rewrite)),
669 None => return check_skip_invalid(ResultCode::FailedToFitMessage, ctx),
670 },
671 };
672
673 ctx.remaining_balance.tokens -= fwd_fee;
675 fees_collected = fwd_fee;
676 }
677 }
678
679 update_total_msg_stat(
680 &mut ctx.action_phase.total_message_size,
681 stats,
682 msg.bit_len(),
683 );
684
685 ctx.action_phase.messages_created += 1;
686 ctx.end_lt += 1;
687
688 ctx.out_msgs.push(msg);
689
690 *ctx.action_phase.total_action_fees.get_or_insert_default() += fees_collected;
691 *ctx.action_phase.total_fwd_fees.get_or_insert_default() += fwd_fee;
692
693 if mode.contains(DELETE_MASK) {
694 ctx.delete_account = if self.params.strict_extra_currency {
695 debug_assert!(ctx.remaining_balance.tokens.is_zero());
697 ctx.reserved_balance.tokens.is_zero()
698 } else {
699 debug_assert!(ctx.remaining_balance.is_zero());
701 ctx.reserved_balance.is_zero()
702 };
703 }
704
705 Ok(SendMsgResult::Sent)
706 }
707
708 fn do_set_code(&self, new_code: Cell, ctx: &mut ActionContext<'_>) -> Result<(), ActionFailed> {
710 ctx.new_state.code = Some(new_code);
712 ctx.action_phase.special_actions += 1;
713
714 Ok(())
716 }
717
718 fn do_reserve_currency(
720 &self,
721 mode: ReserveCurrencyFlags,
722 mut reserve: CurrencyCollection,
723 ctx: &mut ActionContext<'_>,
724 ) -> Result<(), ActionFailed> {
725 const MASK: u8 = ReserveCurrencyFlags::all().bits();
726
727 if mode.contains(ReserveCurrencyFlags::BOUNCE_ON_ERROR) {
729 ctx.need_bounce_on_fail = true;
730 }
731
732 if mode.bits() & !MASK != 0 {
733 return Err(ActionFailed);
735 }
736
737 if self.params.strict_extra_currency && !reserve.other.is_empty() {
738 return Err(ActionFailed);
740 }
741
742 if mode.contains(ReserveCurrencyFlags::WITH_ORIGINAL_BALANCE) {
743 if mode.contains(ReserveCurrencyFlags::REVERSE) {
744 if self.params.strict_extra_currency {
745 reserve.tokens = ctx
746 .original_balance
747 .tokens
748 .checked_sub(reserve.tokens)
749 .ok_or(ActionFailed)?;
750 } else {
751 reserve = ctx.original_balance.checked_sub(&reserve)?;
752 }
753 } else if self.params.strict_extra_currency {
754 reserve.try_add_assign_tokens(ctx.original_balance.tokens)?;
755 } else {
756 reserve.try_add_assign(ctx.original_balance)?;
757 }
758 } else if mode.contains(ReserveCurrencyFlags::REVERSE) {
759 return Err(ActionFailed);
761 }
762
763 if mode.contains(ReserveCurrencyFlags::IGNORE_ERROR) {
764 reserve = reserve.checked_clamp(&ctx.remaining_balance)?;
766 }
767
768 let mut new_balance = CurrencyCollection {
770 tokens: match ctx.remaining_balance.tokens.checked_sub(reserve.tokens) {
771 Some(tokens) => tokens,
772 None => {
773 ctx.action_phase.result_code = ResultCode::NotEnoughBalance as i32;
774 return Err(ActionFailed);
775 }
776 },
777 other: match ctx.remaining_balance.other.checked_sub(&reserve.other) {
778 Ok(other) => other,
779 Err(_) => {
780 ctx.action_phase.result_code = ResultCode::NotEnoughExtraBalance as i32;
781 return Err(ActionFailed);
782 }
783 },
784 };
785
786 reserve.other.normalize()?;
788
789 if mode.contains(ReserveCurrencyFlags::ALL_BUT) {
791 if self.params.strict_extra_currency {
792 std::mem::swap(&mut new_balance.tokens, &mut reserve.tokens);
793 } else {
794 std::mem::swap(&mut new_balance, &mut reserve);
795 }
796 }
797
798 ctx.remaining_balance = new_balance;
800 ctx.reserved_balance.try_add_assign(&reserve)?;
801 ctx.action_phase.special_actions += 1;
802
803 Ok(())
805 }
806
807 fn do_change_library(
809 &self,
810 mode: ChangeLibraryMode,
811 mut lib: LibRef,
812 ctx: &mut ActionContext<'_>,
813 ) -> Result<(), ActionFailed> {
814 const INVALID_MODE: ChangeLibraryMode = ChangeLibraryMode::from_bits_retain(
816 ChangeLibraryMode::ADD_PRIVATE.bits() | ChangeLibraryMode::ADD_PUBLIC.bits(),
817 );
818
819 if mode.contains(ChangeLibraryMode::BOUNCE_ON_ERROR) {
821 ctx.need_bounce_on_fail = true;
822 }
823
824 if mode.contains(INVALID_MODE) {
825 return Err(ActionFailed);
826 }
827
828 let hash = match &lib {
829 LibRef::Cell(cell) => cell.repr_hash(),
830 LibRef::Hash(hash) => hash,
831 };
832
833 let is_masterchain = self.address.is_masterchain();
834
835 let add_public = mode.contains(ChangeLibraryMode::ADD_PUBLIC);
836 if add_public || mode.contains(ChangeLibraryMode::ADD_PRIVATE) {
837 let mut was_public = None;
839 if let Ok(Some(prev)) = ctx.new_state.libraries.get(hash) {
840 if prev.public == add_public {
841 ctx.action_phase.special_actions += 1;
843 return Ok(());
844 } else {
845 was_public = Some(prev.public);
847 lib = LibRef::Cell(prev.root);
848 }
849 }
850
851 let LibRef::Cell(root) = lib else {
852 ctx.action_phase.result_code = ResultCode::NoLibCode as i32;
853 return Err(ActionFailed);
854 };
855
856 let mut stats = ExtStorageStat::with_limits(StorageStatLimits {
857 bit_count: u32::MAX,
858 cell_count: self.config.size_limits.max_library_cells,
859 });
860 if !stats.add_cell(root.as_ref()) {
861 ctx.action_phase.result_code = ResultCode::LibOutOfLimits as i32;
862 return Err(ActionFailed);
863 }
864
865 match ctx.new_state.libraries.set(*root.repr_hash(), SimpleLib {
867 public: add_public,
868 root: root.clone(),
869 }) {
870 Ok(_) if is_masterchain => {
872 if let Some(diff) = &mut ctx.public_libs_diff {
874 match (was_public, add_public) {
875 (None, true) | (Some(false), true) => {
877 diff.push(PublicLibraryChange::Add(root))
878 }
879 (Some(true), false) => {
881 diff.push(PublicLibraryChange::Remove(*root.repr_hash()));
882 }
883 _ => {}
885 }
886 }
887 }
888 Ok(_) => {}
889 Err(_) => {
890 ctx.action_phase.result_code = ResultCode::InvalidLibrariesDict as i32;
891 return Err(ActionFailed);
892 }
893 }
894 } else {
895 match ctx.new_state.libraries.remove(hash) {
897 Ok(Some(lib)) if is_masterchain && lib.public => {
899 if let Some(diff) = &mut ctx.public_libs_diff {
900 diff.push(PublicLibraryChange::Remove(*hash));
901 }
902 }
903 Ok(_) => {}
904 Err(_) => {
905 ctx.action_phase.result_code = ResultCode::InvalidLibrariesDict as i32;
906 return Err(ActionFailed);
907 }
908 }
909 }
910
911 ctx.action_phase.special_actions += 1;
913
914 Ok(())
916 }
917}
918
919struct ActionContext<'a> {
920 need_bounce_on_fail: bool,
921 strict_extra_currency: bool,
922 received_message: Option<&'a mut ReceivedMessage>,
923 original_balance: &'a CurrencyCollection,
924 remaining_balance: CurrencyCollection,
925 reserved_balance: CurrencyCollection,
926 action_fine: &'a mut Tokens,
927 new_state: &'a mut StateInit,
928 end_lt: u64,
929 out_msgs: Vec<Lazy<OwnedMessage>>,
930 delete_account: bool,
931 public_libs_diff: Option<Vec<PublicLibraryChange>>,
932
933 compute_phase: &'a ExecutedComputePhase,
934 action_phase: &'a mut ActionPhase,
935}
936
937impl ActionContext<'_> {
938 fn apply_fine_on_error(
939 &mut self,
940 balance: &mut CurrencyCollection,
941 total_fees: &mut Tokens,
942 charge_action_fees: bool,
943 ) -> Result<(), Error> {
944 if charge_action_fees {
946 self.action_fine
947 .try_add_assign(self.action_phase.total_action_fees.unwrap_or_default())?;
948 }
949
950 self.action_phase.total_fwd_fees = None;
954
955 self.action_phase.total_action_fees = Some(*self.action_fine).filter(|t| !t.is_zero());
957
958 balance.tokens.try_sub_assign(*self.action_fine)?;
959 total_fees.try_add_assign(*self.action_fine)
960 }
961
962 fn rewrite_message_value(
963 &self,
964 value: &mut CurrencyCollection,
965 mut mode: SendMsgFlags,
966 fees_total: Tokens,
967 ) -> Result<Tokens, ActionFailed> {
968 if mode.contains(SendMsgFlags::ALL_BALANCE) {
970 if self.strict_extra_currency {
972 value.tokens = self.remaining_balance.tokens;
974 } else {
975 *value = self.remaining_balance.clone();
977 };
978 mode.remove(SendMsgFlags::PAY_FEE_SEPARATELY);
980 } else if mode.contains(SendMsgFlags::WITH_REMAINING_BALANCE) {
981 if let Some(msg) = &self.received_message {
982 if self.strict_extra_currency {
985 value.try_add_assign_tokens(msg.balance_remaining.tokens)?;
987 } else {
988 value.try_add_assign(&msg.balance_remaining)?;
990 }
991 }
992
993 if !mode.contains(SendMsgFlags::PAY_FEE_SEPARATELY) {
994 value.try_sub_assign_tokens(*self.action_fine)?;
996 value.try_sub_assign_tokens(self.compute_phase.gas_fees)?;
997 }
998 }
999
1000 let mut total = value.tokens;
1002 if mode.contains(SendMsgFlags::PAY_FEE_SEPARATELY) {
1003 total.try_add_assign(fees_total)?;
1004 } else {
1005 value.tokens.try_sub_assign(fees_total)?;
1006 }
1007
1008 Ok(total)
1010 }
1011}
1012
1013struct ActionFailed;
1014
1015impl From<anyhow::Error> for ActionFailed {
1016 #[inline]
1017 fn from(_: anyhow::Error) -> Self {
1018 Self
1019 }
1020}
1021
1022impl From<Error> for ActionFailed {
1023 #[inline]
1024 fn from(_: Error) -> Self {
1025 Self
1026 }
1027}
1028
1029#[derive(Debug, Clone, Copy)]
1030enum SendMsgResult {
1031 Sent,
1032 Rewrite(MessageRewrite),
1033}
1034
1035#[derive(Debug, Clone, Copy)]
1036enum MessageRewrite {
1037 StateInitToCell,
1038 BodyToCell,
1039}
1040
1041impl MessageRewrite {
1042 pub fn next(rewrite: Option<Self>) -> Option<Self> {
1043 match rewrite {
1044 None => Some(Self::StateInitToCell),
1045 Some(Self::StateInitToCell) => Some(Self::BodyToCell),
1046 Some(Self::BodyToCell) => None,
1047 }
1048 }
1049}
1050
1051fn load_state_init_as_slice<'a>(cs: &mut CellSlice<'a>) -> Result<CellSlice<'a>, Error> {
1052 let mut res_cs = *cs;
1053
1054 if cs.load_bit()? {
1056 if cs.load_bit()? {
1057 let state_root = cs.load_reference()?;
1059 if state_root.is_exotic() {
1060 return Err(Error::InvalidData);
1062 }
1063
1064 let mut cs = state_root.as_slice_allow_exotic();
1066 StateInit::load_from(&mut cs)?;
1067
1068 if !cs.is_empty() {
1070 return Err(Error::CellOverflow);
1071 }
1072 } else {
1073 StateInit::load_from(cs)?;
1077 }
1078 }
1079
1080 res_cs.skip_last(cs.size_bits(), cs.size_refs())?;
1081 Ok(res_cs)
1082}
1083
1084fn load_body_as_slice<'a>(cs: &mut CellSlice<'a>) -> Result<CellSlice<'a>, Error> {
1085 let res_cs = *cs;
1086
1087 if cs.load_bit()? {
1088 cs.skip_first(0, 1)?;
1090 } else {
1091 cs.load_remaining();
1093 }
1094
1095 Ok(res_cs)
1096}
1097
1098fn rewrite_state_init_to_cell(mut cs: CellSlice<'_>) -> CellBuilder {
1099 let prefix = cs.load_small_uint(2).unwrap();
1101 debug_assert_eq!(prefix, 0b10);
1102
1103 let cell = CellBuilder::build_from(cs).unwrap();
1105
1106 let mut b = CellBuilder::new();
1108 b.store_small_uint(0b11, 2).unwrap();
1109 b.store_reference(cell).unwrap();
1110
1111 b
1113}
1114
1115fn rewrite_body_to_cell(mut cs: CellSlice<'_>) -> CellBuilder {
1116 let prefix = cs.load_bit().unwrap();
1118 debug_assert!(!prefix);
1119
1120 let cell = CellBuilder::build_from(cs).unwrap();
1122
1123 let mut b = CellBuilder::new();
1125 b.store_bit_one().unwrap();
1126 b.store_reference(cell).unwrap();
1127
1128 b
1130}
1131
1132fn build_message(
1133 info: &RelaxedMsgInfo,
1134 state_init_cs: &CellSlice<'_>,
1135 body_cs: &CellSlice<'_>,
1136) -> Result<Lazy<OwnedMessage>, Error> {
1137 CellBuilder::build_from((info, state_init_cs, body_cs)).map(|cell| {
1138 unsafe { Lazy::from_raw_unchecked(cell) }
1140 })
1141}
1142
1143fn update_total_msg_stat(
1144 total_message_size: &mut StorageUsedShort,
1145 stats: CellTreeStats,
1146 root_bits: u16,
1147) {
1148 let bits_diff = VarUint56::new(stats.bit_count.saturating_add(root_bits as _));
1149 let cells_diff = VarUint56::new(stats.cell_count.saturating_add(1));
1150
1151 total_message_size.bits = total_message_size.bits.saturating_add(bits_diff);
1152 total_message_size.cells = total_message_size.cells.saturating_add(cells_diff);
1153}
1154
1155fn check_extra_balance(value: &ExtraCurrencyCollection, limit: usize) -> bool {
1156 for (i, entry) in value.as_dict().iter().enumerate() {
1157 if i > limit || entry.is_err() {
1158 return false;
1159 }
1160 }
1161 true
1162}
1163
1164fn normalize_extra_balance(
1165 value: ExtraCurrencyCollection,
1166 limit: usize,
1167) -> Result<ExtraCurrencyCollection, BalanceExtraError> {
1168 let mut result = value.clone();
1169 for (i, entry) in value.as_dict().iter().enumerate() {
1170 if i > limit {
1171 return Err(BalanceExtraError::OutOfLimit);
1172 }
1173 let (currency_id, other) = entry.map_err(BalanceExtraError::InvalidDict)?;
1174 if other.is_zero() {
1175 result
1176 .as_dict_mut()
1177 .remove(currency_id)
1178 .map_err(BalanceExtraError::InvalidDict)?;
1179 }
1180 }
1181 Ok(result)
1182}
1183
1184enum BalanceExtraError {
1185 OutOfLimit,
1186 InvalidDict(#[allow(unused)] Error),
1187}
1188
1189#[repr(i32)]
1190#[derive(Debug, thiserror::Error)]
1191enum ResultCode {
1192 #[error("invalid action list")]
1193 ActionListInvalid = 32,
1194 #[error("too many actions")]
1195 TooManyActions = 33,
1196 #[error("invalid or unsupported action")]
1197 ActionInvalid = 34,
1198 #[error("invalid source address")]
1199 InvalidSrcAddr = 35,
1200 #[error("invalid destination address")]
1201 InvalidDstAddr = 36,
1202 #[error("not enough balance (base currency)")]
1203 NotEnoughBalance = 37,
1204 #[error("not enough balance (extra currency)")]
1205 NotEnoughExtraBalance = 38,
1206 #[error("failed to fit message into cell")]
1207 FailedToFitMessage = 39,
1208 #[error("message exceeds limits")]
1209 MessageOutOfLimits = 40,
1210 #[error("library code not found")]
1211 NoLibCode = 41,
1212 #[error("failed to change libraries dict")]
1213 InvalidLibrariesDict = 42,
1214 #[error("too many library cells")]
1215 LibOutOfLimits = 43,
1216 #[error("too many extra currencies")]
1217 TooManyExtraCurrencies = 44,
1218 #[error("state exceeds limits")]
1219 StateOutOfLimits = 50,
1220}
1221
1222const MAX_MSG_EXTRA_CURRENCIES: usize = 2;
1224
1225#[cfg(test)]
1226mod tests {
1227 use std::collections::BTreeMap;
1228
1229 use tycho_asm_macros::tvmasm;
1230 use tycho_types::merkle::MerkleProof;
1231 use tycho_types::models::{
1232 Anycast, AuthorityMarksConfig, IntAddr, MessageLayout, MsgInfo, RelaxedIntMsgInfo,
1233 RelaxedMessage, StdAddr, VarAddr,
1234 };
1235 use tycho_types::num::{Uint9, VarUint248};
1236
1237 use super::*;
1238 use crate::ExecutorParams;
1239 use crate::tests::{make_custom_config, make_default_config, make_default_params};
1240
1241 const STUB_ADDR: StdAddr = StdAddr::new(0, HashBytes::ZERO);
1242 const OK_BALANCE: Tokens = Tokens::new(1_000_000_000);
1243 const OK_GAS: Tokens = Tokens::new(1_000_000);
1244
1245 fn stub_compute_phase(gas_fees: Tokens) -> ExecutedComputePhase {
1246 ExecutedComputePhase {
1247 success: true,
1248 msg_state_used: false,
1249 account_activated: false,
1250 gas_fees,
1251 gas_used: Default::default(),
1252 gas_limit: Default::default(),
1253 gas_credit: None,
1254 mode: 0,
1255 exit_code: 0,
1256 exit_arg: None,
1257 vm_steps: 0,
1258 vm_init_state_hash: Default::default(),
1259 vm_final_state_hash: Default::default(),
1260 }
1261 }
1262
1263 fn empty_action_phase() -> ActionPhase {
1264 ActionPhase {
1265 success: true,
1266 valid: true,
1267 no_funds: false,
1268 status_change: AccountStatusChange::Unchanged,
1269 total_fwd_fees: None,
1270 total_action_fees: None,
1271 result_code: 0,
1272 result_arg: None,
1273 total_actions: 0,
1274 special_actions: 0,
1275 skipped_actions: 0,
1276 messages_created: 0,
1277 action_list_hash: *Cell::empty_cell_ref().repr_hash(),
1278 total_message_size: Default::default(),
1279 }
1280 }
1281
1282 fn make_action_list<I: IntoIterator<Item: Store>>(actions: I) -> Cell {
1283 let mut root = Cell::default();
1284 for action in actions {
1285 root = CellBuilder::build_from((root, action)).unwrap();
1286 }
1287 root
1288 }
1289
1290 fn make_relaxed_message(
1291 info: impl Into<RelaxedMsgInfo>,
1292 init: Option<StateInit>,
1293 body: Option<CellBuilder>,
1294 ) -> Lazy<OwnedRelaxedMessage> {
1295 let body = match &body {
1296 None => Cell::empty_cell_ref().as_slice_allow_exotic(),
1297 Some(cell) => cell.as_full_slice(),
1298 };
1299 Lazy::new(&RelaxedMessage {
1300 info: info.into(),
1301 init,
1302 body,
1303 layout: None,
1304 })
1305 .unwrap()
1306 .cast_into()
1307 }
1308
1309 fn compute_full_stats(msg: &Lazy<OwnedMessage>, params: &ExecutorParams) -> StorageUsedShort {
1310 let msg = 'cell: {
1311 if params.strict_extra_currency {
1312 let mut parsed = msg.load().unwrap();
1313 if let MsgInfo::Int(int) = &mut parsed.info
1314 && !int.value.other.is_empty()
1315 {
1316 int.value.other = ExtraCurrencyCollection::new();
1317 break 'cell CellBuilder::build_from(parsed).unwrap();
1318 }
1319 }
1320 msg.inner().clone()
1321 };
1322
1323 let stats = {
1324 let mut stats = ExtStorageStat::with_limits(StorageStatLimits::UNLIMITED);
1325 assert!(stats.add_cell(msg.as_ref()));
1326 stats.stats()
1327 };
1328 StorageUsedShort {
1329 cells: VarUint56::new(stats.cell_count),
1330 bits: VarUint56::new(stats.bit_count),
1331 }
1332 }
1333
1334 fn original_balance(
1335 state: &ExecutorState<'_>,
1336 compute_phase: &ExecutedComputePhase,
1337 ) -> CurrencyCollection {
1338 state
1339 .balance
1340 .clone()
1341 .checked_add(&compute_phase.gas_fees.into())
1342 .unwrap()
1343 }
1344
1345 #[test]
1346 fn empty_actions() -> Result<()> {
1347 let params = make_default_params();
1348 let config = make_default_config();
1349 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
1350
1351 let compute_phase = stub_compute_phase(OK_GAS);
1352 let prev_total_fees = state.total_fees;
1353 let prev_balance = state.balance.clone();
1354 let prev_end_lt = state.end_lt;
1355
1356 let ActionPhaseFull {
1357 action_phase,
1358 action_fine,
1359 state_exceeds_limits,
1360 bounce,
1361 } = state.action_phase(ActionPhaseContext {
1362 received_message: None,
1363 original_balance: original_balance(&state, &compute_phase),
1364 new_state: StateInit::default(),
1365 actions: Cell::empty_cell(),
1366 compute_phase: &compute_phase,
1367 inspector: None,
1368 })?;
1369
1370 assert_eq!(action_phase, empty_action_phase());
1371 assert_eq!(action_fine, Tokens::ZERO);
1372 assert!(!state_exceeds_limits);
1373 assert!(!bounce);
1374 assert_eq!(state.total_fees, prev_total_fees);
1375 assert_eq!(state.balance, prev_balance);
1376 assert_eq!(state.end_lt, prev_end_lt);
1377 Ok(())
1378 }
1379
1380 #[test]
1381 fn too_many_actions() -> Result<()> {
1382 let params = make_default_params();
1383 let config = make_default_config();
1384 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
1385
1386 let compute_phase = stub_compute_phase(OK_GAS);
1387 let prev_total_fees = state.total_fees;
1388 let prev_balance = state.balance.clone();
1389 let prev_end_lt = state.end_lt;
1390
1391 let actions = make_action_list(
1392 std::iter::repeat_with(|| OutAction::SetCode {
1393 new_code: Cell::empty_cell(),
1394 })
1395 .take(300),
1396 );
1397
1398 let ActionPhaseFull {
1399 action_phase,
1400 action_fine,
1401 state_exceeds_limits,
1402 bounce,
1403 } = state.action_phase(ActionPhaseContext {
1404 received_message: None,
1405 original_balance: original_balance(&state, &compute_phase),
1406 new_state: StateInit::default(),
1407 actions: actions.clone(),
1408 compute_phase: &compute_phase,
1409 inspector: None,
1410 })?;
1411
1412 assert_eq!(action_phase, ActionPhase {
1413 success: false,
1414 valid: false,
1415 result_code: ResultCode::TooManyActions as i32,
1416 result_arg: Some(256),
1417 action_list_hash: *actions.repr_hash(),
1418 ..empty_action_phase()
1419 });
1420 assert_eq!(action_fine, Tokens::ZERO);
1421 assert!(!state_exceeds_limits);
1422 assert!(!bounce);
1423 assert_eq!(state.total_fees, prev_total_fees);
1424 assert_eq!(state.balance, prev_balance);
1425 assert_eq!(state.end_lt, prev_end_lt);
1426 Ok(())
1427 }
1428
1429 #[test]
1430 fn invalid_action_list() -> Result<()> {
1431 let params = make_default_params();
1432 let config = make_default_config();
1433 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
1434
1435 let compute_phase = stub_compute_phase(OK_GAS);
1436 let prev_total_fees = state.total_fees;
1437 let prev_balance = state.balance.clone();
1438 let prev_end_lt = state.end_lt;
1439
1440 let actions = CellBuilder::build_from((
1441 CellBuilder::build_from(MerkleProof::default())?,
1442 OutAction::SetCode {
1443 new_code: Cell::default(),
1444 },
1445 ))?;
1446
1447 let ActionPhaseFull {
1448 action_phase,
1449 action_fine,
1450 state_exceeds_limits,
1451 bounce,
1452 } = state.action_phase(ActionPhaseContext {
1453 received_message: None,
1454 original_balance: original_balance(&state, &compute_phase),
1455 new_state: StateInit::default(),
1456 actions: actions.clone(),
1457 compute_phase: &compute_phase,
1458 inspector: None,
1459 })?;
1460
1461 assert_eq!(action_phase, ActionPhase {
1462 success: false,
1463 valid: false,
1464 result_code: ResultCode::ActionListInvalid as i32,
1465 result_arg: Some(1),
1466 action_list_hash: *actions.repr_hash(),
1467 ..empty_action_phase()
1468 });
1469 assert_eq!(action_fine, Tokens::ZERO);
1470 assert!(!state_exceeds_limits);
1471 assert!(!bounce);
1472 assert_eq!(state.total_fees, prev_total_fees);
1473 assert_eq!(state.balance, prev_balance);
1474 assert_eq!(state.end_lt, prev_end_lt);
1475 Ok(())
1476 }
1477
1478 #[test]
1479 fn invalid_action() -> Result<()> {
1480 let params = make_default_params();
1481 let config = make_default_config();
1482 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
1483
1484 let compute_phase = stub_compute_phase(OK_GAS);
1485 let prev_total_fees = state.total_fees;
1486 let prev_balance = state.balance.clone();
1487 let prev_end_lt = state.end_lt;
1488
1489 let set_code_action = {
1490 let mut b = CellBuilder::new();
1491 OutAction::SetCode {
1492 new_code: Cell::empty_cell(),
1493 }
1494 .store_into(&mut b, Cell::empty_context())?;
1495 b
1496 };
1497 let invalid_action = {
1498 let mut b = CellBuilder::new();
1499 b.store_u32(0xdeafbeaf)?;
1500 b
1501 };
1502
1503 let actions = make_action_list([
1504 set_code_action.as_full_slice(),
1505 set_code_action.as_full_slice(),
1506 invalid_action.as_full_slice(),
1507 set_code_action.as_full_slice(),
1508 set_code_action.as_full_slice(),
1509 ]);
1510
1511 let ActionPhaseFull {
1512 action_phase,
1513 action_fine,
1514 state_exceeds_limits,
1515 bounce,
1516 } = state.action_phase(ActionPhaseContext {
1517 received_message: None,
1518 original_balance: original_balance(&state, &compute_phase),
1519 new_state: StateInit::default(),
1520 actions: actions.clone(),
1521 compute_phase: &compute_phase,
1522 inspector: None,
1523 })?;
1524
1525 assert_eq!(action_phase, ActionPhase {
1526 success: false,
1527 valid: false,
1528 result_code: ResultCode::ActionInvalid as i32,
1529 result_arg: Some(2),
1530 action_list_hash: *actions.repr_hash(),
1531 total_actions: 5,
1532 ..empty_action_phase()
1533 });
1534 assert_eq!(action_fine, Tokens::ZERO);
1535 assert!(!state_exceeds_limits);
1536 assert!(!bounce);
1537 assert_eq!(state.total_fees, prev_total_fees);
1538 assert_eq!(state.balance, prev_balance);
1539 assert_eq!(state.end_lt, prev_end_lt);
1540 Ok(())
1541 }
1542
1543 #[test]
1544 fn strict_reserve_extra_currency() -> Result<()> {
1545 let mut params = make_default_params();
1546 params.strict_extra_currency = true;
1547 let config = make_default_config();
1548 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
1549 let prev_balance = state.balance.clone();
1550
1551 let compute_phase = stub_compute_phase(OK_GAS);
1552 let prev_total_fees = state.total_fees;
1553 let prev_end_lt = state.end_lt;
1554
1555 let actions = make_action_list([OutAction::ReserveCurrency {
1556 mode: ReserveCurrencyFlags::empty(),
1557 value: CurrencyCollection {
1558 tokens: Tokens::ZERO,
1559 other: BTreeMap::from_iter([(123u32, VarUint248::new(10))]).try_into()?,
1560 },
1561 }]);
1562
1563 let ActionPhaseFull {
1564 action_phase,
1565 action_fine,
1566 state_exceeds_limits,
1567 bounce,
1568 } = state.action_phase(ActionPhaseContext {
1569 received_message: None,
1570 original_balance: original_balance(&state, &compute_phase),
1571 new_state: StateInit::default(),
1572 actions: actions.clone(),
1573 compute_phase: &compute_phase,
1574 inspector: None,
1575 })?;
1576
1577 assert_eq!(action_phase, ActionPhase {
1578 success: false,
1579 valid: true,
1580 result_code: ResultCode::ActionInvalid as i32,
1581 result_arg: Some(0),
1582 action_list_hash: *actions.repr_hash(),
1583 total_actions: 1,
1584 ..empty_action_phase()
1585 });
1586 assert_eq!(action_fine, Tokens::ZERO);
1587 assert!(!state_exceeds_limits);
1588 assert!(!bounce);
1589 assert_eq!(state.total_fees, prev_total_fees);
1590 assert_eq!(state.balance, prev_balance);
1591 assert_eq!(state.end_lt, prev_end_lt);
1592 Ok(())
1593 }
1594
1595 #[test]
1596 fn send_single_message() -> Result<()> {
1597 let params = make_default_params();
1598 let config = make_default_config();
1599 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
1600
1601 let compute_phase = stub_compute_phase(OK_GAS);
1602 let prev_total_fees = state.total_fees;
1603 let prev_balance = state.balance.clone();
1604 let prev_end_lt = state.end_lt;
1605
1606 let msg_value = Tokens::new(500_000_000);
1607
1608 let actions = make_action_list([OutAction::SendMsg {
1609 mode: SendMsgFlags::empty(),
1610 out_msg: make_relaxed_message(
1611 RelaxedIntMsgInfo {
1612 dst: STUB_ADDR.into(),
1613 value: msg_value.into(),
1614 ..Default::default()
1615 },
1616 None,
1617 None,
1618 ),
1619 }]);
1620
1621 let ActionPhaseFull {
1622 action_phase,
1623 action_fine,
1624 state_exceeds_limits,
1625 bounce,
1626 } = state.action_phase(ActionPhaseContext {
1627 received_message: None,
1628 original_balance: original_balance(&state, &compute_phase),
1629 new_state: StateInit::default(),
1630 actions: actions.clone(),
1631 compute_phase: &compute_phase,
1632 inspector: None,
1633 })?;
1634
1635 assert_eq!(action_fine, Tokens::ZERO);
1636 assert!(!state_exceeds_limits);
1637 assert!(!bounce);
1638
1639 assert_eq!(state.out_msgs.len(), 1);
1640 assert_eq!(state.end_lt, prev_end_lt + 1);
1641 let last_msg = state.out_msgs.last().unwrap();
1642
1643 let msg_info = {
1644 let msg = last_msg.load()?;
1645 assert!(msg.init.is_none());
1646 assert_eq!(msg.body, Default::default());
1647 match msg.info {
1648 MsgInfo::Int(info) => info,
1649 e => panic!("unexpected msg info {e:?}"),
1650 }
1651 };
1652 assert_eq!(msg_info.src, STUB_ADDR.into());
1653 assert_eq!(msg_info.dst, STUB_ADDR.into());
1654 assert!(msg_info.ihr_disabled);
1655 assert!(!msg_info.bounce);
1656 assert!(!msg_info.bounced);
1657 assert_eq!(msg_info.created_at, params.block_unixtime);
1658 assert_eq!(msg_info.created_lt, prev_end_lt);
1659
1660 let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
1661 let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
1662
1663 assert_eq!(msg_info.value, (msg_value - expected_fwd_fees).into());
1664 assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
1665 assert_eq!(msg_info.ihr_fee, Tokens::ZERO);
1666
1667 assert_eq!(action_phase, ActionPhase {
1668 total_fwd_fees: Some(expected_fwd_fees),
1669 total_action_fees: Some(expected_first_frac),
1670 total_actions: 1,
1671 messages_created: 1,
1672 action_list_hash: *actions.repr_hash(),
1673 total_message_size: compute_full_stats(last_msg, ¶ms),
1674 ..empty_action_phase()
1675 });
1676
1677 assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
1678 assert_eq!(state.balance.other, prev_balance.other);
1679 assert_eq!(state.balance.tokens, prev_balance.tokens - msg_value);
1680
1681 Ok(())
1682 }
1683
1684 #[test]
1685 fn send_all_balance() -> Result<()> {
1686 let params = make_default_params();
1687 let config = make_default_config();
1688 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
1689
1690 let compute_phase = stub_compute_phase(OK_GAS);
1691 let prev_total_fees = state.total_fees;
1692 let prev_balance = state.balance.clone();
1693 let prev_end_lt = state.end_lt;
1694
1695 let actions = make_action_list([OutAction::SendMsg {
1696 mode: SendMsgFlags::ALL_BALANCE,
1697 out_msg: make_relaxed_message(
1698 RelaxedIntMsgInfo {
1699 dst: STUB_ADDR.into(),
1700 value: CurrencyCollection::ZERO,
1701 ..Default::default()
1702 },
1703 None,
1704 None,
1705 ),
1706 }]);
1707
1708 let ActionPhaseFull {
1709 action_phase,
1710 action_fine,
1711 state_exceeds_limits,
1712 bounce,
1713 } = state.action_phase(ActionPhaseContext {
1714 received_message: None,
1715 original_balance: original_balance(&state, &compute_phase),
1716 new_state: StateInit::default(),
1717 actions: actions.clone(),
1718 compute_phase: &compute_phase,
1719 inspector: None,
1720 })?;
1721
1722 assert_eq!(action_fine, Tokens::ZERO);
1723 assert!(!state_exceeds_limits);
1724 assert!(!bounce);
1725
1726 assert_eq!(state.out_msgs.len(), 1);
1727 assert_eq!(state.end_lt, prev_end_lt + 1);
1728 let last_msg = state.out_msgs.last().unwrap();
1729
1730 let msg_info = {
1731 let msg = last_msg.load()?;
1732 assert!(msg.init.is_none());
1733 assert_eq!(msg.body, Default::default());
1734 match msg.info {
1735 MsgInfo::Int(info) => info,
1736 e => panic!("unexpected msg info {e:?}"),
1737 }
1738 };
1739 assert_eq!(msg_info.src, STUB_ADDR.into());
1740 assert_eq!(msg_info.dst, STUB_ADDR.into());
1741 assert!(msg_info.ihr_disabled);
1742 assert!(!msg_info.bounce);
1743 assert!(!msg_info.bounced);
1744 assert_eq!(msg_info.created_at, params.block_unixtime);
1745 assert_eq!(msg_info.created_lt, prev_end_lt);
1746
1747 let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
1748 let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
1749
1750 assert_eq!(
1751 msg_info.value,
1752 (prev_balance.tokens - expected_fwd_fees).into()
1753 );
1754 assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
1755 assert_eq!(msg_info.ihr_fee, Tokens::ZERO);
1756
1757 assert_eq!(action_phase, ActionPhase {
1758 total_fwd_fees: Some(expected_fwd_fees),
1759 total_action_fees: Some(expected_first_frac),
1760 total_actions: 1,
1761 messages_created: 1,
1762 action_list_hash: *actions.repr_hash(),
1763 total_message_size: compute_full_stats(last_msg, ¶ms),
1764 ..empty_action_phase()
1765 });
1766
1767 assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
1768 assert_eq!(state.balance, CurrencyCollection::ZERO);
1769
1770 Ok(())
1771 }
1772
1773 #[test]
1774 fn strict_send_all_balance() -> Result<()> {
1775 let mut params = make_default_params();
1776 params.strict_extra_currency = true;
1777 let config = make_default_config();
1778
1779 let mut state =
1780 ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, CurrencyCollection {
1781 tokens: OK_BALANCE,
1782 other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1783 });
1784
1785 let compute_phase = stub_compute_phase(OK_GAS);
1786 let prev_total_fees = state.total_fees;
1787 let prev_balance = state.balance.clone();
1788 let prev_end_lt = state.end_lt;
1789
1790 let sent_value = CurrencyCollection {
1791 tokens: Tokens::ZERO,
1792 other: BTreeMap::from_iter([(0u32, VarUint248::new(1))]).try_into()?,
1793 };
1794
1795 let actions = make_action_list([OutAction::SendMsg {
1796 mode: SendMsgFlags::ALL_BALANCE,
1797 out_msg: make_relaxed_message(
1798 RelaxedIntMsgInfo {
1799 dst: STUB_ADDR.into(),
1800 value: sent_value.clone(),
1801 ..Default::default()
1802 },
1803 None,
1804 None,
1805 ),
1806 }]);
1807
1808 let ActionPhaseFull {
1809 action_phase,
1810 action_fine,
1811 state_exceeds_limits,
1812 bounce,
1813 } = state.action_phase(ActionPhaseContext {
1814 received_message: None,
1815 original_balance: original_balance(&state, &compute_phase),
1816 new_state: StateInit::default(),
1817 actions: actions.clone(),
1818 compute_phase: &compute_phase,
1819 inspector: None,
1820 })?;
1821
1822 assert_eq!(action_fine, Tokens::ZERO);
1823 assert!(!state_exceeds_limits);
1824 assert!(!bounce);
1825
1826 assert_eq!(state.out_msgs.len(), 1);
1827 assert_eq!(state.end_lt, prev_end_lt + 1);
1828 let last_msg = state.out_msgs.last().unwrap();
1829
1830 let msg_info = {
1831 let msg = last_msg.load()?;
1832 assert!(msg.init.is_none());
1833 assert_eq!(msg.body, Default::default());
1834 match msg.info {
1835 MsgInfo::Int(info) => info,
1836 e => panic!("unexpected msg info {e:?}"),
1837 }
1838 };
1839 assert_eq!(msg_info.src, STUB_ADDR.into());
1840 assert_eq!(msg_info.dst, STUB_ADDR.into());
1841 assert!(msg_info.ihr_disabled);
1842 assert!(!msg_info.bounce);
1843 assert!(!msg_info.bounced);
1844 assert_eq!(msg_info.created_at, params.block_unixtime);
1845 assert_eq!(msg_info.created_lt, prev_end_lt);
1846
1847 let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
1848 let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
1849
1850 assert_eq!(msg_info.value, CurrencyCollection {
1851 tokens: prev_balance.tokens - expected_fwd_fees,
1852 other: sent_value.other.clone(),
1853 });
1854 assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
1855 assert_eq!(msg_info.ihr_fee, Tokens::ZERO);
1856
1857 assert_eq!(action_phase, ActionPhase {
1858 total_fwd_fees: Some(expected_fwd_fees),
1859 total_action_fees: Some(expected_first_frac),
1860 total_actions: 1,
1861 messages_created: 1,
1862 action_list_hash: *actions.repr_hash(),
1863 total_message_size: compute_full_stats(last_msg, ¶ms),
1864 ..empty_action_phase()
1865 });
1866
1867 assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
1868 assert_eq!(state.balance.tokens, Tokens::ZERO);
1869 assert_eq!(
1870 state.balance.other,
1871 prev_balance.other.checked_sub(&sent_value.other)?
1872 );
1873
1874 Ok(())
1875 }
1876
1877 #[test]
1878 fn strict_send_all_balance_destroy() -> Result<()> {
1879 struct TestCase {
1880 balance: CurrencyCollection,
1881 to_send: CurrencyCollection,
1882 expected_end_status: AccountStatus,
1883 }
1884
1885 let mut params = make_default_params();
1886 params.strict_extra_currency = true;
1887 let config = make_default_config();
1888
1889 for TestCase {
1890 balance,
1891 to_send,
1892 expected_end_status,
1893 } in [
1894 TestCase {
1896 balance: OK_BALANCE.into(),
1897 to_send: CurrencyCollection::ZERO,
1898 expected_end_status: AccountStatus::NotExists,
1899 },
1900 TestCase {
1902 balance: CurrencyCollection {
1903 tokens: OK_BALANCE,
1904 other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1905 },
1906 to_send: CurrencyCollection::ZERO,
1907 expected_end_status: AccountStatus::Uninit,
1908 },
1909 TestCase {
1911 balance: CurrencyCollection {
1912 tokens: OK_BALANCE,
1913 other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1914 },
1915 to_send: CurrencyCollection::ZERO,
1916 expected_end_status: AccountStatus::Uninit,
1917 },
1918 TestCase {
1920 balance: CurrencyCollection {
1921 tokens: OK_BALANCE,
1922 other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1923 },
1924 to_send: CurrencyCollection {
1925 tokens: OK_BALANCE,
1926 other: BTreeMap::from_iter([(0u32, VarUint248::new(1))]).try_into()?,
1927 },
1928 expected_end_status: AccountStatus::Uninit,
1929 },
1930 TestCase {
1932 balance: CurrencyCollection {
1933 tokens: OK_BALANCE,
1934 other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1935 },
1936 to_send: CurrencyCollection {
1937 tokens: OK_BALANCE,
1938 other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1939 },
1940 expected_end_status: AccountStatus::NotExists,
1941 },
1942 ] {
1943 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, balance);
1944
1945 let compute_phase = stub_compute_phase(OK_GAS);
1946 let prev_total_fees = state.total_fees;
1947 let prev_balance = state.balance.clone();
1948 let prev_end_lt = state.end_lt;
1949
1950 let actions = make_action_list([OutAction::SendMsg {
1951 mode: SendMsgFlags::ALL_BALANCE | SendMsgFlags::DELETE_IF_EMPTY,
1952 out_msg: make_relaxed_message(
1953 RelaxedIntMsgInfo {
1954 dst: STUB_ADDR.into(),
1955 value: to_send.clone(),
1956 ..Default::default()
1957 },
1958 None,
1959 None,
1960 ),
1961 }]);
1962
1963 let ActionPhaseFull {
1964 action_phase,
1965 action_fine,
1966 state_exceeds_limits,
1967 bounce,
1968 } = state.action_phase(ActionPhaseContext {
1969 received_message: None,
1970 original_balance: original_balance(&state, &compute_phase),
1971 new_state: StateInit::default(),
1972 actions: actions.clone(),
1973 compute_phase: &compute_phase,
1974 inspector: None,
1975 })?;
1976
1977 assert_eq!(action_fine, Tokens::ZERO);
1978 assert!(!state_exceeds_limits);
1979 assert!(!bounce);
1980
1981 assert_eq!(state.end_status, expected_end_status);
1982 assert_eq!(state.out_msgs.len(), 1);
1983 assert_eq!(state.end_lt, prev_end_lt + 1);
1984 let last_msg = state.out_msgs.last().unwrap();
1985
1986 let msg_info = {
1987 let msg = last_msg.load()?;
1988 assert!(msg.init.is_none());
1989 assert_eq!(msg.body, Default::default());
1990 match msg.info {
1991 MsgInfo::Int(info) => info,
1992 e => panic!("unexpected msg info {e:?}"),
1993 }
1994 };
1995 assert_eq!(msg_info.src, STUB_ADDR.into());
1996 assert_eq!(msg_info.dst, STUB_ADDR.into());
1997 assert!(msg_info.ihr_disabled);
1998 assert!(!msg_info.bounce);
1999 assert!(!msg_info.bounced);
2000 assert_eq!(msg_info.created_at, params.block_unixtime);
2001 assert_eq!(msg_info.created_lt, prev_end_lt);
2002
2003 let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
2004 let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
2005
2006 assert_eq!(msg_info.value, CurrencyCollection {
2007 tokens: prev_balance.tokens - expected_fwd_fees,
2008 other: to_send.other.clone(),
2009 });
2010 assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
2011 assert_eq!(msg_info.ihr_fee, Tokens::ZERO);
2012
2013 assert_eq!(action_phase, ActionPhase {
2014 total_fwd_fees: Some(expected_fwd_fees),
2015 total_action_fees: Some(expected_first_frac),
2016 total_actions: 1,
2017 messages_created: 1,
2018 action_list_hash: *actions.repr_hash(),
2019 total_message_size: compute_full_stats(last_msg, ¶ms),
2020 status_change: AccountStatusChange::Deleted,
2021 ..empty_action_phase()
2022 });
2023
2024 assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
2025 assert_eq!(state.balance.tokens, Tokens::ZERO);
2026 assert_eq!(
2027 state.balance.other,
2028 prev_balance.other.checked_sub(&to_send.other)?
2029 );
2030 }
2031
2032 Ok(())
2033 }
2034
2035 #[test]
2036 fn send_all_not_reserved() -> Result<()> {
2037 let params = make_default_params();
2038 let config = make_default_config();
2039 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
2040
2041 let compute_phase = stub_compute_phase(OK_GAS);
2042 let prev_total_fees = state.total_fees;
2043 let prev_end_lt = state.end_lt;
2044
2045 let expected_balance = CurrencyCollection::from(state.balance.tokens / 4);
2046 let actions = make_action_list([
2047 OutAction::ReserveCurrency {
2048 mode: ReserveCurrencyFlags::empty(),
2049 value: expected_balance.clone(),
2050 },
2051 OutAction::SendMsg {
2052 mode: SendMsgFlags::ALL_BALANCE,
2053 out_msg: make_relaxed_message(
2054 RelaxedIntMsgInfo {
2055 dst: STUB_ADDR.into(),
2056 value: CurrencyCollection::ZERO,
2057 ..Default::default()
2058 },
2059 None,
2060 None,
2061 ),
2062 },
2063 ]);
2064
2065 let ActionPhaseFull {
2066 action_phase,
2067 action_fine,
2068 state_exceeds_limits,
2069 bounce,
2070 } = state.action_phase(ActionPhaseContext {
2071 received_message: None,
2072 original_balance: original_balance(&state, &compute_phase),
2073 new_state: StateInit::default(),
2074 actions: actions.clone(),
2075 compute_phase: &compute_phase,
2076 inspector: None,
2077 })?;
2078
2079 assert_eq!(state.out_msgs.len(), 1);
2080 assert_eq!(state.end_lt, prev_end_lt + 1);
2081 let last_msg = state.out_msgs.last().unwrap();
2082
2083 let msg_info = {
2084 let msg = last_msg.load()?;
2085 assert!(msg.init.is_none());
2086 assert_eq!(msg.body, Default::default());
2087 match msg.info {
2088 MsgInfo::Int(info) => info,
2089 e => panic!("unexpected msg info {e:?}"),
2090 }
2091 };
2092 assert_eq!(msg_info.src, STUB_ADDR.into());
2093 assert_eq!(msg_info.dst, STUB_ADDR.into());
2094 assert!(msg_info.ihr_disabled);
2095 assert!(!msg_info.bounce);
2096 assert!(!msg_info.bounced);
2097 assert_eq!(msg_info.created_at, params.block_unixtime);
2098 assert_eq!(msg_info.created_lt, prev_end_lt);
2099
2100 let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
2101 let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
2102
2103 assert_eq!(
2104 msg_info.value,
2105 (OK_BALANCE * 3 / 4 - expected_fwd_fees).into()
2106 );
2107 assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
2108 assert_eq!(msg_info.ihr_fee, Tokens::ZERO);
2109
2110 assert_eq!(action_phase, ActionPhase {
2111 total_fwd_fees: Some(expected_fwd_fees),
2112 total_action_fees: Some(expected_first_frac),
2113 total_actions: 2,
2114 messages_created: 1,
2115 special_actions: 1,
2116 action_list_hash: *actions.repr_hash(),
2117 total_message_size: compute_full_stats(last_msg, ¶ms),
2118 ..empty_action_phase()
2119 });
2120 assert_eq!(action_fine, Tokens::ZERO);
2121 assert!(!state_exceeds_limits);
2122 assert!(!bounce);
2123
2124 assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
2125 assert_eq!(state.balance, expected_balance);
2126 Ok(())
2127 }
2128
2129 #[test]
2130 fn cant_send_authority_marks_by_all_balance() -> Result<()> {
2131 let params = ExecutorParams {
2132 authority_marks_enabled: true,
2133 strict_extra_currency: false,
2134 ..make_default_params()
2135 };
2136
2137 let config = make_custom_config(|config| {
2138 config.set_authority_marks_config(&AuthorityMarksConfig {
2139 authority_addresses: Dict::new(),
2140 black_mark_id: 100,
2141 white_mark_id: 101,
2142 })?;
2143 Ok(())
2144 });
2145
2146 let mut state = ExecutorState::new_active(
2147 ¶ms,
2148 &config,
2149 &STUB_ADDR,
2150 CurrencyCollection {
2151 tokens: OK_BALANCE,
2152 other: BTreeMap::from_iter([
2153 (100u32, VarUint248::new(500)), (101u32, VarUint248::new(500)),
2155 (200u32, VarUint248::new(500)),
2156 ])
2157 .try_into()?,
2158 },
2159 Cell::default(),
2160 tvmasm!("ACCEPT"),
2161 );
2162
2163 let compute_phase = stub_compute_phase(OK_GAS);
2164
2165 let prev_end_lt = state.end_lt;
2166 let prev_balance = state.balance.clone();
2167
2168 let actions = make_action_list([OutAction::SendMsg {
2169 mode: SendMsgFlags::ALL_BALANCE,
2170 out_msg: make_relaxed_message(
2171 RelaxedIntMsgInfo {
2172 dst: STUB_ADDR.into(),
2173 value: CurrencyCollection {
2174 tokens: Tokens::new(100_000_000),
2175 other: ExtraCurrencyCollection::new(),
2176 },
2177 ..Default::default()
2178 },
2179 None,
2180 None,
2181 ),
2182 }]);
2183
2184 let ActionPhaseFull {
2185 action_phase,
2186 action_fine,
2187 state_exceeds_limits,
2188 bounce,
2189 } = state.action_phase(ActionPhaseContext {
2190 received_message: None,
2191 original_balance: original_balance(&state, &compute_phase),
2192 new_state: StateInit::default(),
2193 actions,
2194 compute_phase: &compute_phase,
2195 inspector: None,
2196 })?;
2197
2198 assert!(!action_phase.success);
2199 assert!(action_phase.valid);
2200 assert_eq!(
2201 action_phase.result_code,
2202 ResultCode::NotEnoughExtraBalance as i32
2203 );
2204 assert_eq!(action_phase.messages_created, 0);
2205 assert_eq!(action_phase.total_actions, 1);
2206 assert_eq!(action_fine, Tokens::ZERO);
2207 assert!(!state_exceeds_limits);
2208 assert!(!bounce);
2209
2210 assert_eq!(state.out_msgs.len(), 0);
2211 assert_eq!(state.end_lt, prev_end_lt);
2212
2213 assert_eq!(state.balance, prev_balance);
2214 Ok(())
2215 }
2216
2217 #[test]
2218 fn authority_addresses_can_send_marks() -> Result<()> {
2219 let params = ExecutorParams {
2220 authority_marks_enabled: true,
2221 ..make_default_params()
2222 };
2223 let config = make_custom_config(|config| {
2224 config.set_authority_marks_config(&AuthorityMarksConfig {
2225 authority_addresses: Dict::try_from_btree(&BTreeMap::from_iter([(
2226 HashBytes::ZERO,
2227 (),
2228 )]))?,
2229 black_mark_id: 100,
2230 white_mark_id: 101,
2231 })?;
2232 Ok(())
2233 });
2234
2235 let mut state = ExecutorState::new_active(
2236 ¶ms,
2237 &config,
2238 &StdAddr::new(-1, HashBytes::ZERO),
2239 CurrencyCollection {
2240 tokens: OK_BALANCE,
2241 other: BTreeMap::from_iter([
2242 (100u32, VarUint248::new(1000)), (101u32, VarUint248::new(100)),
2244 ])
2245 .try_into()?,
2246 },
2247 Cell::default(),
2248 tvmasm!("ACCEPT"),
2249 );
2250
2251 let compute_phase = stub_compute_phase(OK_GAS);
2252 let prev_balance = state.balance.clone();
2253
2254 let actions_marks = make_action_list([OutAction::SendMsg {
2256 mode: SendMsgFlags::PAY_FEE_SEPARATELY,
2257 out_msg: make_relaxed_message(
2258 RelaxedIntMsgInfo {
2259 dst: STUB_ADDR.into(),
2260 value: CurrencyCollection {
2261 tokens: Tokens::ZERO,
2262 other: BTreeMap::from_iter([(100u32, VarUint248::new(10))]).try_into()?,
2263 },
2264 ..Default::default()
2265 },
2266 None,
2267 None,
2268 ),
2269 }]);
2270
2271 let ActionPhaseFull {
2272 action_phase: action_phase_normal,
2273 action_fine: action_fine_normal,
2274 state_exceeds_limits: state_exceeds_limits_normal,
2275 bounce: bounce_normal,
2276 } = state.action_phase(ActionPhaseContext {
2277 received_message: None,
2278 original_balance: original_balance(&state, &compute_phase),
2279 new_state: StateInit::default(),
2280 actions: actions_marks.clone(),
2281 compute_phase: &compute_phase,
2282 inspector: None,
2283 })?;
2284
2285 assert!(action_phase_normal.success);
2286 assert!(action_phase_normal.valid);
2287 assert_eq!(action_phase_normal.result_code, 0);
2288 assert_eq!(action_phase_normal.messages_created, 1);
2289 assert_eq!(action_fine_normal, Tokens::ZERO);
2290
2291 assert_ne!(state.balance.tokens, prev_balance.tokens); assert_eq!(
2293 state.balance.other,
2294 BTreeMap::from_iter([
2295 (100u32, VarUint248::new(990)), (101u32, VarUint248::new(100)),
2297 ])
2298 .try_into()?
2299 );
2300 assert!(!state_exceeds_limits_normal);
2301 assert!(!bounce_normal);
2302
2303 assert_eq!(state.out_msgs.len(), 1);
2304
2305 Ok(())
2306 }
2307
2308 #[test]
2309 fn cant_send_authority_marks_directly() -> Result<()> {
2310 let params = ExecutorParams {
2311 authority_marks_enabled: true,
2312 ..make_default_params()
2313 };
2314
2315 let config = make_custom_config(|config| {
2316 config.set_authority_marks_config(&AuthorityMarksConfig {
2317 authority_addresses: Dict::new(),
2318 black_mark_id: 100,
2319 white_mark_id: 101,
2320 })?;
2321 Ok(())
2322 });
2323
2324 let mut state = ExecutorState::new_active(
2325 ¶ms,
2326 &config,
2327 &STUB_ADDR,
2328 CurrencyCollection {
2329 tokens: OK_BALANCE,
2330 other: BTreeMap::from_iter([
2331 (100u32, VarUint248::new(500)), (101u32, VarUint248::new(500)),
2333 ])
2334 .try_into()?,
2335 },
2336 Cell::default(),
2337 tvmasm!("ACCEPT"),
2338 );
2339
2340 let compute_phase = stub_compute_phase(OK_GAS);
2341 let prev_balance = state.balance.clone();
2342 let prev_total_fees = state.total_fees;
2343 let prev_end_lt = state.end_lt;
2344
2345 let actions_black = make_action_list([OutAction::SendMsg {
2347 mode: SendMsgFlags::PAY_FEE_SEPARATELY,
2348 out_msg: make_relaxed_message(
2349 RelaxedIntMsgInfo {
2350 dst: STUB_ADDR.into(),
2351 value: CurrencyCollection {
2352 tokens: Tokens::ZERO,
2353 other: BTreeMap::from_iter([(100u32, VarUint248::new(10))]).try_into()?,
2354 },
2355 ..Default::default()
2356 },
2357 None,
2358 None,
2359 ),
2360 }]);
2361
2362 let ActionPhaseFull {
2363 action_phase: action_phase_black,
2364 action_fine: action_fine_black,
2365 state_exceeds_limits: state_exceeds_limits_black,
2366 bounce: bounce_black,
2367 } = state.action_phase(ActionPhaseContext {
2368 received_message: None,
2369 original_balance: original_balance(&state, &compute_phase),
2370 new_state: StateInit::default(),
2371 actions: actions_black.clone(),
2372 compute_phase: &compute_phase,
2373 inspector: None,
2374 })?;
2375
2376 assert_eq!(action_phase_black, ActionPhase {
2377 success: false,
2378 valid: true,
2379 no_funds: true,
2380 result_code: ResultCode::NotEnoughExtraBalance as i32,
2381 result_arg: Some(0),
2382 total_actions: 1,
2383 action_list_hash: *actions_black.repr_hash(),
2384 ..empty_action_phase()
2385 });
2386
2387 assert_eq!(action_fine_black, Tokens::ZERO);
2388 assert!(!state_exceeds_limits_black);
2389 assert!(!bounce_black);
2390
2391 assert_eq!(state.total_fees, prev_total_fees);
2392 assert_eq!(state.balance, prev_balance);
2393 assert_eq!(state.end_lt, prev_end_lt);
2394 assert!(state.out_msgs.is_empty());
2395
2396 let actions_normal = make_action_list([OutAction::SendMsg {
2398 mode: SendMsgFlags::PAY_FEE_SEPARATELY,
2399 out_msg: make_relaxed_message(
2400 RelaxedIntMsgInfo {
2401 dst: STUB_ADDR.into(),
2402 value: CurrencyCollection {
2403 tokens: Tokens::ZERO,
2404 other: ExtraCurrencyCollection::new(),
2405 },
2406 ..Default::default()
2407 },
2408 None,
2409 None,
2410 ),
2411 }]);
2412
2413 let ActionPhaseFull {
2414 action_phase: action_phase_normal,
2415 action_fine: action_fine_normal,
2416 state_exceeds_limits: state_exceeds_limits_normal,
2417 bounce: bounce_normal,
2418 } = state.action_phase(ActionPhaseContext {
2419 received_message: None,
2420 original_balance: original_balance(&state, &compute_phase),
2421 new_state: StateInit::default(),
2422 actions: actions_normal.clone(),
2423 compute_phase: &compute_phase,
2424 inspector: None,
2425 })?;
2426
2427 assert!(action_phase_normal.success);
2428 assert!(action_phase_normal.valid);
2429 assert_eq!(action_phase_normal.result_code, 0);
2430 assert_eq!(action_phase_normal.messages_created, 1);
2431 assert_eq!(action_fine_normal, Tokens::ZERO);
2432 assert!(!state_exceeds_limits_normal);
2433 assert!(!bounce_normal);
2434
2435 assert_eq!(state.out_msgs.len(), 1);
2436
2437 Ok(())
2438 }
2439
2440 #[test]
2441 fn set_code() -> Result<()> {
2442 let params = make_default_params();
2443 let config = make_default_config();
2444
2445 let orig_data = CellBuilder::build_from(u32::MIN)?;
2446 let final_data = CellBuilder::build_from(u32::MAX)?;
2447
2448 let temp_code = Boc::decode(tvmasm!("NOP NOP"))?;
2449 let final_code = Boc::decode(tvmasm!("NOP"))?;
2450
2451 let mut state = ExecutorState::new_active(
2452 ¶ms,
2453 &config,
2454 &STUB_ADDR,
2455 OK_BALANCE,
2456 orig_data,
2457 tvmasm!("ACCEPT"),
2458 );
2459
2460 let compute_phase = stub_compute_phase(OK_GAS);
2461 let prev_total_fees = state.total_fees;
2462 let prev_balance = state.balance.clone();
2463
2464 let actions = make_action_list([
2465 OutAction::SetCode {
2466 new_code: temp_code,
2467 },
2468 OutAction::SetCode {
2469 new_code: final_code.clone(),
2470 },
2471 ]);
2472
2473 let AccountState::Active(mut new_state) = state.state.clone() else {
2474 panic!("unexpected account state");
2475 };
2476 new_state.data = Some(final_data.clone());
2477
2478 let ActionPhaseFull {
2479 action_phase,
2480 action_fine,
2481 state_exceeds_limits,
2482 bounce,
2483 } = state.action_phase(ActionPhaseContext {
2484 received_message: None,
2485 original_balance: original_balance(&state, &compute_phase),
2486 new_state,
2487 actions: actions.clone(),
2488 compute_phase: &compute_phase,
2489 inspector: None,
2490 })?;
2491
2492 assert_eq!(action_phase, ActionPhase {
2493 total_actions: 2,
2494 special_actions: 2,
2495 action_list_hash: *actions.repr_hash(),
2496 ..empty_action_phase()
2497 });
2498 assert_eq!(action_fine, Tokens::ZERO);
2499 assert!(!state_exceeds_limits);
2500 assert!(!bounce);
2501 assert_eq!(state.end_status, AccountStatus::Active);
2502 assert_eq!(
2503 state.state,
2504 AccountState::Active(StateInit {
2505 code: Some(final_code),
2506 data: Some(final_data),
2507 ..Default::default()
2508 })
2509 );
2510 assert_eq!(state.total_fees, prev_total_fees);
2511 assert_eq!(state.balance, prev_balance);
2512 Ok(())
2513 }
2514
2515 #[test]
2516 fn invalid_dst_addr() -> Result<()> {
2517 let params = make_default_params();
2518 let config = make_default_config();
2519
2520 let targets = [
2521 IntAddr::Std(StdAddr::new(123, HashBytes::ZERO)),
2523 IntAddr::Std({
2525 let mut addr = STUB_ADDR;
2526 let mut b = CellBuilder::new();
2527 b.store_u16(0xaabb)?;
2528 addr.anycast = Some(Box::new(Anycast::from_slice(&b.as_data_slice())?));
2529 addr
2530 }),
2531 IntAddr::Var(VarAddr {
2533 anycast: None,
2534 address_len: Uint9::new(80),
2535 workchain: 0,
2536 address: vec![0; 10],
2537 }),
2538 ];
2539
2540 for dst in targets {
2541 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
2542
2543 let compute_phase = stub_compute_phase(OK_GAS);
2544 let prev_total_fees = state.total_fees;
2545 let prev_balance = state.balance.clone();
2546 let prev_end_lt = state.end_lt;
2547
2548 let actions = make_action_list([OutAction::SendMsg {
2549 mode: SendMsgFlags::ALL_BALANCE,
2550 out_msg: make_relaxed_message(
2551 RelaxedIntMsgInfo {
2552 dst,
2553 ..Default::default()
2554 },
2555 None,
2556 None,
2557 ),
2558 }]);
2559
2560 let ActionPhaseFull {
2561 action_phase,
2562 action_fine,
2563 state_exceeds_limits,
2564 bounce,
2565 } = state.action_phase(ActionPhaseContext {
2566 received_message: None,
2567 original_balance: original_balance(&state, &compute_phase),
2568 new_state: StateInit::default(),
2569 actions: actions.clone(),
2570 compute_phase: &compute_phase,
2571 inspector: None,
2572 })?;
2573
2574 assert_eq!(action_phase, ActionPhase {
2575 success: false,
2576 total_actions: 1,
2577 messages_created: 0,
2578 result_code: ResultCode::InvalidDstAddr as _,
2579 result_arg: Some(0),
2580 action_list_hash: *actions.repr_hash(),
2581 ..empty_action_phase()
2582 });
2583 assert_eq!(action_fine, Tokens::ZERO);
2584 assert!(!state_exceeds_limits);
2585 assert!(!bounce);
2586
2587 assert!(state.out_msgs.is_empty());
2588 assert_eq!(state.end_lt, prev_end_lt);
2589
2590 assert_eq!(state.total_fees, prev_total_fees);
2591 assert_eq!(state.balance, prev_balance);
2592 }
2593 Ok(())
2594 }
2595
2596 #[test]
2597 fn cant_pay_fwd_fee() -> Result<()> {
2598 let params = make_default_params();
2599 let config = make_default_config();
2600 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, Tokens::new(50000));
2601
2602 let compute_phase = stub_compute_phase(OK_GAS);
2603 let prev_balance = state.balance.clone();
2604 let prev_total_fee = state.total_fees;
2605 let prev_end_lt = state.end_lt;
2606
2607 let actions = make_action_list([OutAction::SendMsg {
2608 mode: SendMsgFlags::PAY_FEE_SEPARATELY,
2609 out_msg: make_relaxed_message(
2610 RelaxedIntMsgInfo {
2611 value: CurrencyCollection::ZERO,
2612 dst: STUB_ADDR.into(),
2613 ..Default::default()
2614 },
2615 None,
2616 Some({
2617 let mut b = CellBuilder::new();
2618 b.store_reference(Cell::empty_cell())?;
2619 b.store_reference(CellBuilder::build_from(0xdeafbeafu32)?)?;
2620 b
2621 }),
2622 ),
2623 }]);
2624
2625 let ActionPhaseFull {
2626 action_phase,
2627 action_fine,
2628 state_exceeds_limits,
2629 bounce,
2630 } = state.action_phase(ActionPhaseContext {
2631 received_message: None,
2632 original_balance: original_balance(&state, &compute_phase),
2633 new_state: StateInit::default(),
2634 actions: actions.clone(),
2635 compute_phase: &compute_phase,
2636 inspector: None,
2637 })?;
2638
2639 assert_eq!(action_phase, ActionPhase {
2640 success: false,
2641 no_funds: true,
2642 result_code: ResultCode::NotEnoughBalance as _,
2643 result_arg: Some(0),
2644 total_actions: 1,
2645 total_action_fees: Some(prev_balance.tokens),
2646 action_list_hash: *actions.repr_hash(),
2647 ..empty_action_phase()
2648 });
2649 assert_eq!(action_fine, prev_balance.tokens);
2650 assert!(!state_exceeds_limits);
2651 assert!(!bounce);
2652
2653 assert_eq!(state.balance, CurrencyCollection::ZERO);
2654 assert_eq!(
2655 state.total_fees,
2656 prev_total_fee + action_phase.total_action_fees.unwrap_or_default()
2657 );
2658 assert!(state.out_msgs.is_empty());
2659 assert_eq!(state.end_lt, prev_end_lt);
2660 Ok(())
2661 }
2662
2663 #[test]
2664 fn rewrite_message() -> Result<()> {
2665 let params = make_default_params();
2666 let config = make_default_config();
2667 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
2668
2669 let compute_phase = stub_compute_phase(OK_GAS);
2670 let prev_balance = state.balance.clone();
2671 let prev_total_fee = state.total_fees;
2672 let prev_end_lt = state.end_lt;
2673
2674 let msg_body = {
2675 let mut b = CellBuilder::new();
2676 b.store_zeros(600)?;
2677 b.store_reference(Cell::empty_cell())?;
2678 b
2679 };
2680
2681 let actions = make_action_list([OutAction::SendMsg {
2682 mode: SendMsgFlags::PAY_FEE_SEPARATELY,
2683 out_msg: make_relaxed_message(
2684 RelaxedIntMsgInfo {
2685 value: CurrencyCollection::ZERO,
2686 dst: STUB_ADDR.into(),
2687 ..Default::default()
2688 },
2689 None,
2690 Some(msg_body.clone()),
2691 ),
2692 }]);
2693
2694 let ActionPhaseFull {
2695 action_phase,
2696 action_fine,
2697 state_exceeds_limits,
2698 bounce,
2699 } = state.action_phase(ActionPhaseContext {
2700 received_message: None,
2701 original_balance: original_balance(&state, &compute_phase),
2702 new_state: StateInit::default(),
2703 actions: actions.clone(),
2704 compute_phase: &compute_phase,
2705 inspector: None,
2706 })?;
2707
2708 assert_eq!(state.out_msgs.len(), 1);
2709 let last_msg = state.out_msgs.last().unwrap();
2710 let msg = last_msg.load()?;
2711 assert_eq!(
2712 msg.layout,
2713 Some(MessageLayout {
2714 init_to_cell: false,
2715 body_to_cell: true,
2716 })
2717 );
2718 assert_eq!(msg.body.1, msg_body.build()?);
2719
2720 let MsgInfo::Int(info) = msg.info else {
2721 panic!("expected an internal message");
2722 };
2723
2724 let expected_fwd_fees = config.fwd_prices.compute_fwd_fee(CellTreeStats {
2725 bit_count: 600,
2726 cell_count: 2,
2727 });
2728 let first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
2729
2730 assert_eq!(action_phase, ActionPhase {
2731 total_actions: 1,
2732 messages_created: 1,
2733 total_fwd_fees: Some(expected_fwd_fees),
2734 total_action_fees: Some(first_frac),
2735 action_list_hash: *actions.repr_hash(),
2736 total_message_size: compute_full_stats(last_msg, ¶ms),
2737 ..empty_action_phase()
2738 });
2739 assert_eq!(action_fine, Tokens::ZERO);
2740 assert!(!state_exceeds_limits);
2741 assert!(!bounce);
2742
2743 assert_eq!(state.end_lt, prev_end_lt + 1);
2744 assert_eq!(
2745 state.total_fees,
2746 prev_total_fee + action_phase.total_action_fees.unwrap_or_default()
2747 );
2748 assert_eq!(state.balance.other, prev_balance.other);
2749 assert_eq!(
2750 state.balance.tokens,
2751 prev_balance.tokens - info.value.tokens - expected_fwd_fees
2752 );
2753 Ok(())
2754 }
2755
2756 #[test]
2757 fn change_lib() -> Result<()> {
2758 struct TestCase {
2759 libraries: Dict<HashBytes, SimpleLib>,
2760 target: Dict<HashBytes, SimpleLib>,
2761 changes: Vec<(ChangeLibraryMode, LibRef)>,
2762 diff: Vec<PublicLibraryChange>,
2763 }
2764
2765 fn make_lib(id: u32) -> Cell {
2766 CellBuilder::build_from(id).unwrap()
2767 }
2768
2769 fn make_lib_hash(id: u32) -> HashBytes {
2770 *CellBuilder::build_from(id).unwrap().repr_hash()
2771 }
2772
2773 fn make_libs<I>(items: I) -> Dict<HashBytes, SimpleLib>
2774 where
2775 I: IntoIterator<Item = (u32, bool)>,
2776 {
2777 let mut items = items
2778 .into_iter()
2779 .map(|(id, public)| {
2780 let root = make_lib(id);
2781 (*root.repr_hash(), SimpleLib { root, public })
2782 })
2783 .collect::<Vec<_>>();
2784 items.sort_by(|(a, _), (b, _)| a.cmp(b));
2785 Dict::try_from_sorted_slice(&items).unwrap()
2786 }
2787
2788 let params = make_default_params();
2789 let config = make_default_config();
2790
2791 let test_cases = vec![
2792 TestCase {
2794 libraries: Dict::new(),
2795 target: Dict::new(),
2796 changes: Vec::new(),
2797 diff: Vec::new(),
2798 },
2799 TestCase {
2801 libraries: Dict::new(),
2802 target: make_libs([(123, false)]),
2803 changes: vec![(ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123)))],
2804 diff: Vec::new(),
2805 },
2806 TestCase {
2807 libraries: Dict::new(),
2808 target: make_libs([(123, false), (234, false)]),
2809 changes: vec![
2810 (ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123))),
2811 (ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(234))),
2812 ],
2813 diff: Vec::new(),
2814 },
2815 TestCase {
2816 libraries: make_libs([(123, false)]),
2817 target: make_libs([(123, false)]),
2818 changes: vec![(ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123)))],
2819 diff: Vec::new(),
2820 },
2821 TestCase {
2822 libraries: make_libs([(123, false)]),
2823 target: make_libs([(123, false)]),
2824 changes: vec![(
2825 ChangeLibraryMode::ADD_PRIVATE,
2826 LibRef::Hash(make_lib_hash(123)),
2827 )],
2828 diff: Vec::new(),
2829 },
2830 TestCase {
2832 libraries: Dict::new(),
2833 target: make_libs([(123, true)]),
2834 changes: vec![(ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123)))],
2835 diff: vec![PublicLibraryChange::Add(make_lib(123))],
2836 },
2837 TestCase {
2838 libraries: Dict::new(),
2839 target: make_libs([(123, true), (234, true)]),
2840 changes: vec![
2841 (ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123))),
2842 (ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(234))),
2843 ],
2844 diff: vec![
2845 PublicLibraryChange::Add(make_lib(123)),
2846 PublicLibraryChange::Add(make_lib(234)),
2847 ],
2848 },
2849 TestCase {
2850 libraries: make_libs([(123, true)]),
2851 target: make_libs([(123, true)]),
2852 changes: vec![(ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123)))],
2853 diff: Vec::new(),
2854 },
2855 TestCase {
2856 libraries: make_libs([(123, true)]),
2857 target: make_libs([(123, true)]),
2858 changes: vec![(
2859 ChangeLibraryMode::ADD_PUBLIC,
2860 LibRef::Hash(make_lib_hash(123)),
2861 )],
2862 diff: Vec::new(),
2863 },
2864 TestCase {
2866 libraries: Dict::new(),
2867 target: Dict::new(),
2868 changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Cell(make_lib(123)))],
2870 diff: Vec::new(),
2871 },
2872 TestCase {
2873 libraries: Dict::new(),
2874 target: Dict::new(),
2875 changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Hash(make_lib_hash(123)))],
2877 diff: Vec::new(),
2878 },
2879 TestCase {
2880 libraries: make_libs([(123, false)]),
2881 target: Dict::new(),
2882 changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Cell(make_lib(123)))],
2883 diff: Vec::new(),
2884 },
2885 TestCase {
2886 libraries: make_libs([(123, false)]),
2887 target: Dict::new(),
2888 changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Hash(make_lib_hash(123)))],
2889 diff: Vec::new(),
2890 },
2891 TestCase {
2892 libraries: make_libs([(123, true)]),
2893 target: Dict::new(),
2894 changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Cell(make_lib(123)))],
2895 diff: vec![PublicLibraryChange::Remove(make_lib_hash(123))],
2896 },
2897 TestCase {
2899 libraries: make_libs([(123, false)]),
2900 target: make_libs([(123, true)]),
2901 changes: vec![(ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123)))],
2902 diff: vec![PublicLibraryChange::Add(make_lib(123))],
2903 },
2904 TestCase {
2905 libraries: make_libs([(123, false)]),
2906 target: make_libs([(123, true)]),
2907 changes: vec![(
2908 ChangeLibraryMode::ADD_PUBLIC,
2909 LibRef::Hash(make_lib_hash(123)),
2910 )],
2911 diff: vec![PublicLibraryChange::Add(make_lib(123))],
2912 },
2913 TestCase {
2914 libraries: make_libs([(123, true)]),
2915 target: make_libs([(123, false)]),
2916 changes: vec![(ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123)))],
2917 diff: vec![PublicLibraryChange::Remove(make_lib_hash(123))],
2918 },
2919 TestCase {
2920 libraries: make_libs([(123, true)]),
2921 target: make_libs([(123, false)]),
2922 changes: vec![(
2923 ChangeLibraryMode::ADD_PRIVATE,
2924 LibRef::Hash(make_lib_hash(123)),
2925 )],
2926 diff: vec![PublicLibraryChange::Remove(make_lib_hash(123))],
2927 },
2928 ];
2929
2930 for TestCase {
2931 libraries,
2932 target,
2933 changes,
2934 diff,
2935 } in test_cases
2936 {
2937 let mut state = ExecutorState::new_active(
2938 ¶ms,
2939 &config,
2940 &StdAddr::new(-1, HashBytes::ZERO),
2941 OK_BALANCE,
2942 Cell::empty_cell(),
2943 tvmasm!("ACCEPT"),
2944 );
2945
2946 let target_state_init = match &mut state.state {
2947 AccountState::Active(state_init) => {
2948 state_init.libraries = libraries;
2949 StateInit {
2950 libraries: target,
2951 ..state_init.clone()
2952 }
2953 }
2954 AccountState::Uninit | AccountState::Frozen(..) => panic!("invalid initial state"),
2955 };
2956
2957 let compute_phase = stub_compute_phase(OK_GAS);
2958 let prev_total_fees = state.total_fees;
2959 let prev_balance = state.balance.clone();
2960
2961 let change_count = changes.len();
2962 let actions = make_action_list(
2963 changes
2964 .into_iter()
2965 .map(|(mode, lib)| OutAction::ChangeLibrary { mode, lib }),
2966 );
2967
2968 let mut inspector = ExecutorInspector::default();
2969
2970 let ActionPhaseFull {
2971 action_phase,
2972 action_fine,
2973 state_exceeds_limits,
2974 bounce,
2975 } = state.action_phase(ActionPhaseContext {
2976 received_message: None,
2977 original_balance: original_balance(&state, &compute_phase),
2978 new_state: match state.state.clone() {
2979 AccountState::Active(state_init) => state_init,
2980 AccountState::Uninit | AccountState::Frozen(..) => Default::default(),
2981 },
2982 actions: actions.clone(),
2983 compute_phase: &compute_phase,
2984 inspector: Some(&mut inspector),
2985 })?;
2986
2987 assert_eq!(action_phase, ActionPhase {
2988 total_actions: change_count as u16,
2989 special_actions: change_count as u16,
2990 action_list_hash: *actions.repr_hash(),
2991 ..empty_action_phase()
2992 });
2993 assert_eq!(action_fine, Tokens::ZERO);
2994 assert!(!state_exceeds_limits);
2995 assert!(!bounce);
2996 assert_eq!(state.end_status, AccountStatus::Active);
2997 assert_eq!(state.state, AccountState::Active(target_state_init));
2998 assert_eq!(state.total_fees, prev_total_fees);
2999 assert_eq!(state.balance, prev_balance);
3000 assert_eq!(inspector.public_libs_diff, diff);
3001 }
3002 Ok(())
3003 }
3004}