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 let stats = 'stats: {
71 let mut stats = ExtStorageStat::with_limits(StorageStatLimits {
72 bit_count: self.config.size_limits.max_msg_bits,
73 cell_count: self.config.size_limits.max_msg_cells,
74 });
75
76 'valid: {
78 if let Some(extra_currencies) = msg_value.other.as_dict().root() {
80 if !stats.add_cell(extra_currencies.as_ref()) {
81 break 'valid;
82 }
83 }
84
85 if let Some(body) = &full_body {
87 if !stats.add_cell(body.as_ref()) {
88 break 'valid;
89 }
90 }
91
92 break 'stats stats.stats();
94 }
95
96 let stats = stats.stats();
99 return Ok(BouncePhase::NoFunds(NoFundsBouncePhase {
100 msg_size: StorageUsedShort {
101 bits: new_varuint56_truncate(stats.bit_count),
102 cells: new_varuint56_truncate(stats.cell_count),
103 },
104 req_fwd_fees: Tokens::MAX,
105 }));
106 };
107
108 let use_mc_prices = self.address.is_masterchain() || int_msg_info.dst.is_masterchain();
110 let prices = self.config.fwd_prices(use_mc_prices);
111
112 let mut fwd_fees = prices.compute_fwd_fee(stats);
113 let msg_size = StorageUsedShort {
114 cells: new_varuint56_truncate(stats.cell_count),
115 bits: new_varuint56_truncate(stats.bit_count),
116 };
117
118 msg_value.tokens = match msg_value
120 .tokens
121 .checked_sub(ctx.gas_fees)
122 .and_then(|t| t.checked_sub(ctx.action_fine))
123 {
124 Some(msg_balance) if msg_balance >= fwd_fees => msg_balance,
125 msg_balance => {
126 return Ok(BouncePhase::NoFunds(NoFundsBouncePhase {
127 msg_size,
128 req_fwd_fees: fwd_fees - msg_balance.unwrap_or_default(),
129 }));
130 }
131 };
132
133 self.balance.try_sub_assign(&msg_value)?;
135
136 msg_value.tokens -= fwd_fees;
138
139 let msg_fees = prices.get_first_part(fwd_fees);
141 fwd_fees -= msg_fees;
142 self.total_fees.try_add_assign(msg_fees)?;
143
144 int_msg_info.ihr_disabled = true;
146 int_msg_info.bounce = false;
147 int_msg_info.bounced = true;
148 int_msg_info.value = msg_value;
149 int_msg_info.ihr_fee = Tokens::ZERO;
150 int_msg_info.fwd_fee = fwd_fees;
151 int_msg_info.created_lt = self.end_lt;
152 int_msg_info.created_at = self.params.block_unixtime;
153
154 let msg = {
155 const ROOT_BODY_BITS: u16 = 256;
156 const BOUNCE_SELECTOR: u32 = u32::MAX;
157
158 let body_prefix = {
159 let (range, cell) = &ctx.received_message.body;
160 range.apply_allow_exotic(cell).get_prefix(ROOT_BODY_BITS, 0)
161 };
162
163 let c = Cell::empty_context();
164 let mut b = CellBuilder::new();
165 info.store_into(&mut b, c)?;
166 b.store_bit_zero()?; if b.has_capacity(body_prefix.size_bits() + 33, 0) {
169 b.store_bit_zero()?; b.store_u32(BOUNCE_SELECTOR)?;
171 b.store_slice_data(body_prefix)?;
172 if let Some(full_body) = full_body {
173 b.store_reference(full_body)?;
174 }
175 } else {
176 let child = {
177 let mut b = CellBuilder::new();
178 b.store_u32(BOUNCE_SELECTOR)?;
179 b.store_slice_data(body_prefix)?;
180 if let Some(full_body) = full_body {
181 b.store_reference(full_body)?;
182 }
183 b.build()?
184 };
185
186 b.store_bit_one()?; b.store_reference(child)?
188 }
189
190 unsafe { Lazy::from_raw_unchecked(b.build()?) }
192 };
193
194 self.out_msgs.push(msg);
196 self.end_lt += 1;
197
198 Ok(BouncePhase::Executed(ExecutedBouncePhase {
200 msg_size,
201 msg_fees,
202 fwd_fees,
203 }))
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use tycho_types::models::{IntMsgInfo, StdAddr};
210 use tycho_types::prelude::*;
211
212 use super::*;
213 use crate::tests::{make_default_config, make_default_params, make_message};
214
215 #[test]
216 fn bounce_with_enough_funds() {
217 let mut params = make_default_params();
218 params.full_body_in_bounced = false;
219
220 let config = make_default_config();
221
222 let src_addr = StdAddr::new(0, HashBytes([0; 32]));
223 let dst_addr = StdAddr::new(0, HashBytes([1; 32]));
224
225 let gas_fees = Tokens::new(100);
226 let action_fine = Tokens::new(200);
227
228 let mut state =
229 ExecutorState::new_uninit(¶ms, &config, &dst_addr, Tokens::new(1_000_000_000));
230 state.balance.tokens -= gas_fees;
231 state.balance.tokens -= action_fine;
232 let prev_balance = state.balance.clone();
233 let prev_total_fees = state.total_fees;
234 let prev_start_lt = state.start_lt;
235
236 let received_msg = state
237 .receive_in_msg(make_message(
238 IntMsgInfo {
239 src: src_addr.clone().into(),
240 dst: dst_addr.clone().into(),
241 value: Tokens::new(1_000_000_000).into(),
242 bounce: true,
243 created_lt: prev_start_lt + 1000,
244 ..Default::default()
245 },
246 None,
247 None,
248 ))
249 .unwrap();
250 assert_eq!(state.start_lt, prev_start_lt + 1000 + 1);
251 assert_eq!(state.end_lt, prev_start_lt + 1000 + 2);
252
253 let bounce_phase = state
254 .bounce_phase(BouncePhaseContext {
255 gas_fees,
256 action_fine,
257 received_message: &received_msg,
258 })
259 .unwrap();
260
261 let BouncePhase::Executed(bounce_phase) = bounce_phase else {
262 panic!("expected bounce phase to execute")
263 };
264
265 let full_fwd_fee = Tokens::new(config.fwd_prices.lump_price as _);
267 let collected_fees = config.fwd_prices.get_first_part(full_fwd_fee);
268 assert_eq!(state.total_fees, prev_total_fees + collected_fees);
269 assert_eq!(state.total_fees, prev_total_fees + bounce_phase.msg_fees);
270 assert_eq!(bounce_phase.fwd_fees, full_fwd_fee - collected_fees);
271
272 assert_eq!(state.out_msgs.len(), 1);
274 let bounced_msg = state.out_msgs.last().unwrap().load().unwrap();
275 assert!(bounced_msg.init.is_none());
276 assert_eq!(bounced_msg.body.0.size_bits(), 32);
277 assert_eq!(
278 CellSlice::apply(&bounced_msg.body)
279 .unwrap()
280 .load_u32()
281 .unwrap(),
282 u32::MAX
283 );
284
285 let MsgInfo::Int(bounced_msg_info) = bounced_msg.info else {
286 panic!("expected bounced internal message");
287 };
288 assert_eq!(state.balance.other, prev_balance.other);
289 assert!(bounced_msg_info.value.other.is_empty());
290 assert_eq!(
291 state.balance.tokens,
292 prev_balance.tokens - (received_msg.balance_remaining.tokens - gas_fees - action_fine)
293 );
294 assert_eq!(
295 bounced_msg_info.value.tokens,
296 received_msg.balance_remaining.tokens - gas_fees - action_fine - full_fwd_fee
297 );
298 assert!(bounced_msg_info.ihr_disabled);
299 assert!(!bounced_msg_info.bounce);
300 assert!(bounced_msg_info.bounced);
301 assert_eq!(bounced_msg_info.src, dst_addr.clone().into());
302 assert_eq!(bounced_msg_info.dst, src_addr.clone().into());
303 assert_eq!(bounced_msg_info.ihr_fee, Tokens::ZERO);
304 assert_eq!(bounced_msg_info.fwd_fee, bounce_phase.fwd_fees);
305 assert_eq!(bounced_msg_info.created_at, params.block_unixtime);
306 assert_eq!(bounced_msg_info.created_lt, prev_start_lt + 1000 + 2);
307
308 assert_eq!(bounce_phase.msg_size, StorageUsedShort {
310 bits: Default::default(),
311 cells: Default::default()
312 });
313
314 assert_eq!(state.end_lt, prev_start_lt + 1000 + 3);
316 }
317
318 #[test]
319 fn bounce_with_no_funds() {
320 let mut params = make_default_params();
321 params.full_body_in_bounced = false;
322
323 let config = make_default_config();
324
325 let src_addr = StdAddr::new(0, HashBytes([0; 32]));
326 let dst_addr = StdAddr::new(0, HashBytes([1; 32]));
327
328 let mut state =
329 ExecutorState::new_uninit(¶ms, &config, &dst_addr, Tokens::new(1_000_000_001));
330 let prev_balance = state.balance.clone();
331 let prev_total_fees = state.total_fees;
332 let prev_start_lt = state.start_lt;
333
334 let received_msg = state
335 .receive_in_msg(make_message(
336 IntMsgInfo {
337 src: src_addr.clone().into(),
338 dst: dst_addr.clone().into(),
339 value: Tokens::new(1).into(),
340 bounce: true,
341 created_lt: prev_start_lt + 1000,
342 ..Default::default()
343 },
344 None,
345 None,
346 ))
347 .unwrap();
348 assert_eq!(state.start_lt, prev_start_lt + 1000 + 1);
349 assert_eq!(state.end_lt, prev_start_lt + 1000 + 2);
350
351 let bounce_phase = state
352 .bounce_phase(BouncePhaseContext {
353 gas_fees: Tokens::ZERO,
354 action_fine: Tokens::ZERO,
355 received_message: &received_msg,
356 })
357 .unwrap();
358
359 let BouncePhase::NoFunds(bounce_phase) = bounce_phase else {
360 panic!("expected bounce phase to execute")
361 };
362
363 assert_eq!(state.balance.other, prev_balance.other);
365 assert_eq!(state.balance.tokens, prev_balance.tokens);
366 assert_eq!(state.total_fees, prev_total_fees);
367
368 let full_fwd_fee = Tokens::new(config.fwd_prices.lump_price as _);
370 assert_eq!(
371 bounce_phase.req_fwd_fees,
372 full_fwd_fee - received_msg.balance_remaining.tokens
373 );
374
375 assert_eq!(bounce_phase.msg_size, StorageUsedShort {
377 bits: Default::default(),
378 cells: Default::default()
379 });
380
381 assert_eq!(state.out_msgs.len(), 0);
383
384 assert_eq!(state.end_lt, prev_start_lt + 1000 + 2);
386 }
387}