1use anyhow::Result;
2use tycho_types::cell::{Cell, CellBuilder, CellFamily, Lazy, Store};
3use tycho_types::models::{
4 BouncePhase, ExecutedBouncePhase, MsgInfo, NoFundsBouncePhase, StorageUsedShort,
5};
6use tycho_types::num::Tokens;
7
8use crate::ExecutorState;
9use crate::phase::receive::ReceivedMessage;
10use crate::util::{
11 ExtStorageStat, StorageStatLimits, check_rewrite_dst_addr, new_varuint56_truncate,
12};
13
14pub struct BouncePhaseContext<'a> {
16 pub gas_fees: Tokens,
18 pub action_fine: Tokens,
20 pub received_message: &'a ReceivedMessage,
22}
23
24impl ExecutorState<'_> {
25 pub fn bounce_phase(&mut self, ctx: BouncePhaseContext<'_>) -> Result<BouncePhase> {
40 let mut info = ctx.received_message.root.parse::<MsgInfo>()?;
41 let MsgInfo::Int(int_msg_info) = &mut info else {
42 anyhow::bail!("bounce phase is defined only for internal messages");
43 };
44
45 std::mem::swap(&mut int_msg_info.src, &mut int_msg_info.dst);
47 if !check_rewrite_dst_addr(&self.config.workchains, &mut int_msg_info.dst) {
48 anyhow::bail!("invalid destination address in a bounced message");
52 }
53
54 let full_body = if self.params.full_body_in_bounced {
56 let (range, cell) = &ctx.received_message.body;
57 Some(if range.is_full(cell) {
58 cell.clone()
59 } else {
60 CellBuilder::build_from(range.apply_allow_exotic(cell))?
61 })
62 } else {
63 None
64 };
65
66 let mut msg_value = ctx.received_message.balance_remaining.clone();
68
69 if self.params.authority_marks_enabled
71 && let Some(marks) = &self.config.authority_marks
72 {
73 marks.remove_authority_marks_in(&mut msg_value)?;
74 }
75
76 let stats = 'stats: {
78 let mut stats = ExtStorageStat::with_limits(StorageStatLimits {
79 bit_count: self.config.size_limits.max_msg_bits,
80 cell_count: self.config.size_limits.max_msg_cells,
81 });
82
83 'valid: {
85 if let Some(extra_currencies) = msg_value.other.as_dict().root()
87 && !stats.add_cell(extra_currencies.as_ref())
88 {
89 break 'valid;
90 }
91
92 if let Some(body) = &full_body
94 && !stats.add_cell(body.as_ref())
95 {
96 break 'valid;
97 }
98
99 break 'stats stats.stats();
101 }
102
103 let stats = stats.stats();
106 return Ok(BouncePhase::NoFunds(NoFundsBouncePhase {
107 msg_size: StorageUsedShort {
108 bits: new_varuint56_truncate(stats.bit_count),
109 cells: new_varuint56_truncate(stats.cell_count),
110 },
111 req_fwd_fees: Tokens::MAX,
112 }));
113 };
114
115 let use_mc_prices = self.address.is_masterchain() || int_msg_info.dst.is_masterchain();
117 let prices = self.config.fwd_prices(use_mc_prices);
118
119 let mut fwd_fees = prices.compute_fwd_fee(stats);
120 let msg_size = StorageUsedShort {
121 cells: new_varuint56_truncate(stats.cell_count),
122 bits: new_varuint56_truncate(stats.bit_count),
123 };
124
125 msg_value.tokens = match msg_value
127 .tokens
128 .checked_sub(ctx.gas_fees)
129 .and_then(|t| t.checked_sub(ctx.action_fine))
130 {
131 Some(msg_balance) if msg_balance >= fwd_fees => msg_balance,
132 msg_balance => {
133 return Ok(BouncePhase::NoFunds(NoFundsBouncePhase {
134 msg_size,
135 req_fwd_fees: fwd_fees - msg_balance.unwrap_or_default(),
136 }));
137 }
138 };
139
140 self.balance.try_sub_assign(&msg_value)?;
142
143 msg_value.tokens -= fwd_fees;
145
146 let msg_fees = prices.get_first_part(fwd_fees);
148 fwd_fees -= msg_fees;
149 self.total_fees.try_add_assign(msg_fees)?;
150
151 int_msg_info.ihr_disabled = true;
153 int_msg_info.bounce = false;
154 int_msg_info.bounced = true;
155 int_msg_info.value = msg_value;
156 int_msg_info.ihr_fee = Tokens::ZERO;
157 int_msg_info.fwd_fee = fwd_fees;
158 int_msg_info.created_lt = self.end_lt;
159 int_msg_info.created_at = self.params.block_unixtime;
160
161 let msg = {
162 const ROOT_BODY_BITS: u16 = 256;
163 const BOUNCE_SELECTOR: u32 = u32::MAX;
164
165 let body_prefix = {
166 let (range, cell) = &ctx.received_message.body;
167 range.apply_allow_exotic(cell).get_prefix(ROOT_BODY_BITS, 0)
168 };
169
170 let c = Cell::empty_context();
171 let mut b = CellBuilder::new();
172 info.store_into(&mut b, c)?;
173 b.store_bit_zero()?; if b.has_capacity(body_prefix.size_bits() + 33, 0) {
176 b.store_bit_zero()?; b.store_u32(BOUNCE_SELECTOR)?;
178 b.store_slice_data(body_prefix)?;
179 if let Some(full_body) = full_body {
180 b.store_reference(full_body)?;
181 }
182 } else {
183 let child = {
184 let mut b = CellBuilder::new();
185 b.store_u32(BOUNCE_SELECTOR)?;
186 b.store_slice_data(body_prefix)?;
187 if let Some(full_body) = full_body {
188 b.store_reference(full_body)?;
189 }
190 b.build()?
191 };
192
193 b.store_bit_one()?; b.store_reference(child)?
195 }
196
197 unsafe { Lazy::from_raw_unchecked(b.build()?) }
199 };
200
201 self.out_msgs.push(msg);
203 self.end_lt += 1;
204
205 Ok(BouncePhase::Executed(ExecutedBouncePhase {
207 msg_size,
208 msg_fees,
209 fwd_fees,
210 }))
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use std::collections::BTreeMap;
217
218 use tycho_types::models::{AuthorityMarksConfig, CurrencyCollection, IntMsgInfo, StdAddr};
219 use tycho_types::num::VarUint248;
220 use tycho_types::prelude::*;
221
222 use super::*;
223 use crate::ExecutorParams;
224 use crate::tests::{
225 make_custom_config, make_default_config, make_default_params, make_message,
226 };
227
228 #[test]
229 fn bounce_with_enough_funds() {
230 let mut params = make_default_params();
231 params.full_body_in_bounced = false;
232
233 let config = make_default_config();
234
235 let src_addr = StdAddr::new(0, HashBytes([0; 32]));
236 let dst_addr = StdAddr::new(0, HashBytes([1; 32]));
237
238 let gas_fees = Tokens::new(100);
239 let action_fine = Tokens::new(200);
240
241 let mut state =
242 ExecutorState::new_uninit(¶ms, &config, &dst_addr, Tokens::new(1_000_000_000));
243 state.balance.tokens -= gas_fees;
244 state.balance.tokens -= action_fine;
245 let prev_balance = state.balance.clone();
246 let prev_total_fees = state.total_fees;
247 let prev_start_lt = state.start_lt;
248
249 let received_msg = state
250 .receive_in_msg(make_message(
251 IntMsgInfo {
252 src: src_addr.clone().into(),
253 dst: dst_addr.clone().into(),
254 value: Tokens::new(1_000_000_000).into(),
255 bounce: true,
256 created_lt: prev_start_lt + 1000,
257 ..Default::default()
258 },
259 None,
260 None,
261 ))
262 .unwrap();
263 assert_eq!(state.start_lt, prev_start_lt + 1000 + 1);
264 assert_eq!(state.end_lt, prev_start_lt + 1000 + 2);
265
266 let bounce_phase = state
267 .bounce_phase(BouncePhaseContext {
268 gas_fees,
269 action_fine,
270 received_message: &received_msg,
271 })
272 .unwrap();
273
274 let BouncePhase::Executed(bounce_phase) = bounce_phase else {
275 panic!("expected bounce phase to execute")
276 };
277
278 let full_fwd_fee = Tokens::new(config.fwd_prices.lump_price as _);
280 let collected_fees = config.fwd_prices.get_first_part(full_fwd_fee);
281 assert_eq!(state.total_fees, prev_total_fees + collected_fees);
282 assert_eq!(state.total_fees, prev_total_fees + bounce_phase.msg_fees);
283 assert_eq!(bounce_phase.fwd_fees, full_fwd_fee - collected_fees);
284
285 assert_eq!(state.out_msgs.len(), 1);
287 let bounced_msg = state.out_msgs.last().unwrap().load().unwrap();
288 assert!(bounced_msg.init.is_none());
289 assert_eq!(bounced_msg.body.0.size_bits(), 32);
290 assert_eq!(
291 CellSlice::apply(&bounced_msg.body)
292 .unwrap()
293 .load_u32()
294 .unwrap(),
295 u32::MAX
296 );
297
298 let MsgInfo::Int(bounced_msg_info) = bounced_msg.info else {
299 panic!("expected bounced internal message");
300 };
301 assert_eq!(state.balance.other, prev_balance.other);
302 assert!(bounced_msg_info.value.other.is_empty());
303 assert_eq!(
304 state.balance.tokens,
305 prev_balance.tokens - (received_msg.balance_remaining.tokens - gas_fees - action_fine)
306 );
307 assert_eq!(
308 bounced_msg_info.value.tokens,
309 received_msg.balance_remaining.tokens - gas_fees - action_fine - full_fwd_fee
310 );
311 assert!(bounced_msg_info.ihr_disabled);
312 assert!(!bounced_msg_info.bounce);
313 assert!(bounced_msg_info.bounced);
314 assert_eq!(bounced_msg_info.src, dst_addr.clone().into());
315 assert_eq!(bounced_msg_info.dst, src_addr.clone().into());
316 assert_eq!(bounced_msg_info.ihr_fee, Tokens::ZERO);
317 assert_eq!(bounced_msg_info.fwd_fee, bounce_phase.fwd_fees);
318 assert_eq!(bounced_msg_info.created_at, params.block_unixtime);
319 assert_eq!(bounced_msg_info.created_lt, prev_start_lt + 1000 + 2);
320
321 assert_eq!(bounce_phase.msg_size, StorageUsedShort {
323 bits: Default::default(),
324 cells: Default::default()
325 });
326
327 assert_eq!(state.end_lt, prev_start_lt + 1000 + 3);
329 }
330
331 #[test]
332 fn bounce_does_not_return_marks() {
333 let params = ExecutorParams {
334 authority_marks_enabled: true,
335 ..make_default_params()
336 };
337 let config = make_custom_config(|config| {
338 config.set_authority_marks_config(&AuthorityMarksConfig {
339 authority_addresses: BTreeMap::from_iter([(HashBytes::ZERO, ())]).try_into()?,
340 black_mark_id: 100,
341 white_mark_id: 101,
342 })?;
343 Ok(())
344 });
345
346 let src_addr = StdAddr::new(0, HashBytes([123; 32]));
347 let dst_addr = StdAddr::new(-1, HashBytes::ZERO);
348
349 let gas_fees = Tokens::new(100);
350 let action_fine = Tokens::new(200);
351
352 let mut state =
353 ExecutorState::new_uninit(¶ms, &config, &dst_addr, CurrencyCollection {
354 tokens: Tokens::new(1_000_000_000),
355 other: BTreeMap::from_iter([
356 (100u32, VarUint248::new(1000)), (101u32, VarUint248::new(100)),
358 ])
359 .try_into()
360 .unwrap(),
361 });
362 state.balance.tokens -= gas_fees;
363 state.balance.tokens -= action_fine;
364 let prev_balance = state.balance.clone();
365 let prev_total_fees = state.total_fees;
366 let prev_start_lt = state.start_lt;
367
368 let msg_balance = CurrencyCollection {
369 tokens: Tokens::new(1_000_000_000),
370 other: BTreeMap::from_iter([(100u32, VarUint248::new(1))])
371 .try_into()
372 .unwrap(),
373 };
374 let received_msg = state
375 .receive_in_msg(make_message(
376 IntMsgInfo {
377 src: src_addr.clone().into(),
378 dst: dst_addr.clone().into(),
379 value: msg_balance.clone(),
380 bounce: true,
381 created_lt: prev_start_lt + 1000,
382 ..Default::default()
383 },
384 None,
385 None,
386 ))
387 .unwrap();
388 assert_eq!(state.start_lt, prev_start_lt + 1000 + 1);
389 assert_eq!(state.end_lt, prev_start_lt + 1000 + 2);
390
391 let credit_phase = state.credit_phase(&received_msg).unwrap();
392 assert_eq!(credit_phase.credit, msg_balance);
393
394 let bounce_phase = state
395 .bounce_phase(BouncePhaseContext {
396 gas_fees,
397 action_fine,
398 received_message: &received_msg,
399 })
400 .unwrap();
401
402 let BouncePhase::Executed(bounce_phase) = bounce_phase else {
403 panic!("expected bounce phase to execute")
404 };
405
406 let full_fwd_fee = Tokens::new(config.mc_fwd_prices.lump_price as _);
408 let collected_fees = config.mc_fwd_prices.get_first_part(full_fwd_fee);
409 assert_eq!(state.total_fees, prev_total_fees + collected_fees);
410 assert_eq!(state.total_fees, prev_total_fees + bounce_phase.msg_fees);
411 assert_eq!(bounce_phase.fwd_fees, full_fwd_fee - collected_fees);
412
413 assert_eq!(state.out_msgs.len(), 1);
415 let bounced_msg = state.out_msgs.last().unwrap().load().unwrap();
416 assert!(bounced_msg.init.is_none());
417 assert_eq!(bounced_msg.body.0.size_bits(), 32);
418 assert_eq!(
419 CellSlice::apply(&bounced_msg.body)
420 .unwrap()
421 .load_u32()
422 .unwrap(),
423 u32::MAX
424 );
425
426 let MsgInfo::Int(bounced_msg_info) = bounced_msg.info else {
427 panic!("expected bounced internal message");
428 };
429 assert_eq!(
430 state.balance.other,
431 prev_balance.other.checked_add(&msg_balance.other).unwrap()
432 );
433 assert!(bounced_msg_info.value.other.is_empty());
434 assert_eq!(
435 state.balance.tokens,
436 prev_balance.tokens + gas_fees + action_fine
437 );
438 assert_eq!(
439 bounced_msg_info.value.tokens,
440 received_msg.balance_remaining.tokens - gas_fees - action_fine - full_fwd_fee
441 );
442 assert!(bounced_msg_info.ihr_disabled);
443 assert!(!bounced_msg_info.bounce);
444 assert!(bounced_msg_info.bounced);
445 assert_eq!(bounced_msg_info.src, dst_addr.clone().into());
446 assert_eq!(bounced_msg_info.dst, src_addr.clone().into());
447 assert_eq!(bounced_msg_info.ihr_fee, Tokens::ZERO);
448 assert_eq!(bounced_msg_info.fwd_fee, bounce_phase.fwd_fees);
449 assert_eq!(bounced_msg_info.created_at, params.block_unixtime);
450 assert_eq!(bounced_msg_info.created_lt, prev_start_lt + 1000 + 2);
451
452 assert_eq!(bounce_phase.msg_size, StorageUsedShort {
454 bits: Default::default(),
455 cells: Default::default()
456 });
457
458 assert_eq!(state.end_lt, prev_start_lt + 1000 + 3);
460 }
461
462 #[test]
463 fn bounce_with_no_funds() {
464 let mut params = make_default_params();
465 params.full_body_in_bounced = false;
466
467 let config = make_default_config();
468
469 let src_addr = StdAddr::new(0, HashBytes([0; 32]));
470 let dst_addr = StdAddr::new(0, HashBytes([1; 32]));
471
472 let mut state =
473 ExecutorState::new_uninit(¶ms, &config, &dst_addr, Tokens::new(1_000_000_001));
474 let prev_balance = state.balance.clone();
475 let prev_total_fees = state.total_fees;
476 let prev_start_lt = state.start_lt;
477
478 let received_msg = state
479 .receive_in_msg(make_message(
480 IntMsgInfo {
481 src: src_addr.clone().into(),
482 dst: dst_addr.clone().into(),
483 value: Tokens::new(1).into(),
484 bounce: true,
485 created_lt: prev_start_lt + 1000,
486 ..Default::default()
487 },
488 None,
489 None,
490 ))
491 .unwrap();
492 assert_eq!(state.start_lt, prev_start_lt + 1000 + 1);
493 assert_eq!(state.end_lt, prev_start_lt + 1000 + 2);
494
495 let bounce_phase = state
496 .bounce_phase(BouncePhaseContext {
497 gas_fees: Tokens::ZERO,
498 action_fine: Tokens::ZERO,
499 received_message: &received_msg,
500 })
501 .unwrap();
502
503 let BouncePhase::NoFunds(bounce_phase) = bounce_phase else {
504 panic!("expected bounce phase to execute")
505 };
506
507 assert_eq!(state.balance.other, prev_balance.other);
509 assert_eq!(state.balance.tokens, prev_balance.tokens);
510 assert_eq!(state.total_fees, prev_total_fees);
511
512 let full_fwd_fee = Tokens::new(config.fwd_prices.lump_price as _);
514 assert_eq!(
515 bounce_phase.req_fwd_fees,
516 full_fwd_fee - received_msg.balance_remaining.tokens
517 );
518
519 assert_eq!(bounce_phase.msg_size, StorageUsedShort {
521 bits: Default::default(),
522 cells: Default::default()
523 });
524
525 assert_eq!(state.out_msgs.len(), 0);
527
528 assert_eq!(state.end_lt, prev_start_lt + 1000 + 2);
530 }
531}