1use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
2use alloy_primitives::{keccak256, Address, Log, B256, U256};
3use alloy_sol_types::{SolError, SolEvent, SolInterface};
4use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
5
6use crate::{
7 interfaces::IArbRetryableTx,
8 storage_slot::{
9 derive_subspace_key, map_slot, vector_length_slot, ARBOS_STATE_ADDRESS,
10 L2_PRICING_SUBSPACE, RETRYABLES_SUBSPACE, ROOT_STORAGE_KEY,
11 },
12};
13
14pub const ARBRETRYABLETX_ADDRESS: Address = Address::new([
16 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
17 0x00, 0x00, 0x00, 0x6e,
18]);
19
20const RETRYABLE_LIFETIME_SECONDS: u64 = 7 * 24 * 60 * 60;
22const RETRYABLE_REAP_PRICE: u64 = 58_000;
23
24const NUM_TRIES_OFFSET: u64 = 0;
26const FROM_OFFSET: u64 = 1;
27const TO_OFFSET: u64 = 2;
28const CALLVALUE_OFFSET: u64 = 3;
29const BENEFICIARY_OFFSET: u64 = 4;
30const TIMEOUT_OFFSET: u64 = 5;
31const TIMEOUT_WINDOWS_LEFT_OFFSET: u64 = 6;
32
33const TIMEOUT_QUEUE_KEY: &[u8] = &[0];
35
36const SLOAD_GAS: u64 = 800;
37const SSTORE_GAS: u64 = 20_000;
38const SSTORE_ZERO_GAS: u64 = 5_000;
39const SSTORE_RESET_GAS: u64 = 5_000;
40const COPY_GAS: u64 = 3;
41const TX_GAS: u64 = 21_000;
42const LOG_GAS: u64 = 375;
43const LOG_TOPIC_GAS: u64 = 375;
44const LOG_DATA_GAS: u64 = 8;
45
46const REDEEM_SCHEDULED_DATA_BYTES: u64 = 128;
48
49const REDEEM_SCHEDULED_EVENT_COST: u64 =
51 LOG_GAS + 4 * LOG_TOPIC_GAS + LOG_DATA_GAS * REDEEM_SCHEDULED_DATA_BYTES;
52
53pub fn ticket_created_topic() -> B256 {
54 IArbRetryableTx::TicketCreated::SIGNATURE_HASH
55}
56
57pub fn redeem_scheduled_topic() -> B256 {
58 IArbRetryableTx::RedeemScheduled::SIGNATURE_HASH
59}
60
61pub fn lifetime_extended_topic() -> B256 {
62 IArbRetryableTx::LifetimeExtended::SIGNATURE_HASH
63}
64
65pub fn canceled_topic() -> B256 {
66 IArbRetryableTx::Canceled::SIGNATURE_HASH
67}
68
69pub fn create_arbretryabletx_precompile() -> DynPrecompile {
70 DynPrecompile::new_stateful(PrecompileId::custom("arbretryabletx"), handler)
71}
72
73fn handler(mut input: PrecompileInput<'_>) -> PrecompileResult {
74 let gas_limit = input.gas;
75 crate::init_precompile_gas(input.data.len());
76
77 let call = match IArbRetryableTx::ArbRetryableTxCalls::abi_decode(input.data) {
78 Ok(c) => c,
79 Err(_) => return crate::burn_all_revert(gas_limit),
80 };
81
82 use IArbRetryableTx::ArbRetryableTxCalls as Calls;
83 let result = match call {
84 Calls::getLifetime(_) => {
85 let lifetime = U256::from(RETRYABLE_LIFETIME_SECONDS);
86 Ok(PrecompileOutput::new(
87 (SLOAD_GAS + COPY_GAS).min(gas_limit),
88 lifetime.to_be_bytes::<32>().to_vec().into(),
89 ))
90 }
91 Calls::getCurrentRedeemer(_) => {
92 let redeemer = crate::get_current_redeemer();
93 Ok(PrecompileOutput::new(
94 (SLOAD_GAS + COPY_GAS).min(gas_limit),
95 redeemer.to_be_bytes::<32>().to_vec().into(),
96 ))
97 }
98 Calls::submitRetryable(_) => {
99 let data = IArbRetryableTx::NotCallable {}.abi_encode();
100 return crate::sol_error_revert(data, gas_limit);
101 }
102 Calls::getTimeout(c) => handle_get_timeout(&mut input, c.ticketId),
103 Calls::getBeneficiary(c) => handle_get_beneficiary(&mut input, c.ticketId),
104 Calls::redeem(c) => handle_redeem(&mut input, c.ticketId),
105 Calls::keepalive(c) => handle_keepalive(&mut input, c.ticketId),
106 Calls::cancel(c) => handle_cancel(&mut input, c.ticketId),
107 };
108 crate::gas_check(gas_limit, result)
109}
110
111fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), PrecompileError> {
114 input
115 .internals_mut()
116 .load_account(ARBOS_STATE_ADDRESS)
117 .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
118 Ok(())
119}
120
121fn sload_field(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
122 let val = input
123 .internals_mut()
124 .sload(ARBOS_STATE_ADDRESS, slot)
125 .map_err(|_| PrecompileError::other("sload failed"))?;
126 crate::charge_precompile_gas(SLOAD_GAS);
127 Ok(val.data)
128}
129
130fn sstore_field(
131 input: &mut PrecompileInput<'_>,
132 slot: U256,
133 value: U256,
134) -> Result<(), PrecompileError> {
135 input
136 .internals_mut()
137 .sstore(ARBOS_STATE_ADDRESS, slot, value)
138 .map_err(|_| PrecompileError::other("sstore failed"))?;
139 Ok(())
140}
141
142fn ticket_storage_key(ticket_id: B256) -> B256 {
144 let retryables_key = derive_subspace_key(ROOT_STORAGE_KEY, RETRYABLES_SUBSPACE);
145 derive_subspace_key(retryables_key.as_slice(), ticket_id.as_slice())
146}
147
148fn open_retryable(
151 input: &mut PrecompileInput<'_>,
152 ticket_id: B256,
153 current_timestamp: u64,
154) -> Result<Option<B256>, PrecompileError> {
155 let ticket_key = ticket_storage_key(ticket_id);
156 let timeout_slot = map_slot(ticket_key.as_slice(), TIMEOUT_OFFSET);
157 let timeout = sload_field(input, timeout_slot)?;
158 let timeout_u64: u64 = timeout.try_into().unwrap_or(0);
159
160 if timeout_u64 == 0 || timeout_u64 < current_timestamp {
161 return Ok(None);
162 }
163
164 Ok(Some(ticket_key))
165}
166
167fn handle_get_timeout(input: &mut PrecompileInput<'_>, ticket_id: B256) -> PrecompileResult {
169 let gas_limit = input.gas;
170 let current_timestamp: u64 = input
171 .internals()
172 .block_timestamp()
173 .try_into()
174 .unwrap_or(u64::MAX);
175
176 load_arbos(input)?;
177
178 let ticket_key = ticket_storage_key(ticket_id);
179
180 let timeout_slot = map_slot(ticket_key.as_slice(), TIMEOUT_OFFSET);
181 let timeout = sload_field(input, timeout_slot)?;
182 let timeout_u64: u64 = timeout.try_into().unwrap_or(0);
183
184 if timeout_u64 == 0 || timeout_u64 < current_timestamp {
185 let data = IArbRetryableTx::NoTicketWithID {}.abi_encode();
186 return crate::sol_error_revert(data, gas_limit);
187 }
188
189 let windows_slot = map_slot(ticket_key.as_slice(), TIMEOUT_WINDOWS_LEFT_OFFSET);
191 let windows = sload_field(input, windows_slot)?;
192 let windows_u64: u64 = windows.try_into().unwrap_or(0);
193
194 let effective_timeout = timeout_u64 + windows_u64 * RETRYABLE_LIFETIME_SECONDS;
195
196 Ok(PrecompileOutput::new(
199 (4 * SLOAD_GAS + 2 * COPY_GAS).min(gas_limit),
200 U256::from(effective_timeout)
201 .to_be_bytes::<32>()
202 .to_vec()
203 .into(),
204 ))
205}
206
207fn timeout_queue_key() -> B256 {
209 let retryables_key = derive_subspace_key(ROOT_STORAGE_KEY, RETRYABLES_SUBSPACE);
210 derive_subspace_key(retryables_key.as_slice(), TIMEOUT_QUEUE_KEY)
211}
212
213fn queue_put(input: &mut PrecompileInput<'_>, value: B256) -> Result<(), PrecompileError> {
216 let queue_key = timeout_queue_key();
217
218 let put_offset_slot = map_slot(queue_key.as_slice(), 0);
220 let put_offset = sload_field(input, put_offset_slot)?;
221 let put_offset_u64: u64 = put_offset
222 .try_into()
223 .map_err(|_| PrecompileError::other("invalid queue put offset"))?;
224
225 let item_slot = map_slot(queue_key.as_slice(), put_offset_u64);
227 sstore_field(input, item_slot, U256::from_be_bytes(value.0))?;
228
229 sstore_field(input, put_offset_slot, U256::from(put_offset_u64 + 1))?;
231
232 Ok(())
233}
234
235fn handle_get_beneficiary(input: &mut PrecompileInput<'_>, ticket_id: B256) -> PrecompileResult {
236 let gas_limit = input.gas;
237 let current_timestamp: u64 = input
238 .internals()
239 .block_timestamp()
240 .try_into()
241 .unwrap_or(u64::MAX);
242
243 load_arbos(input)?;
244
245 let ticket_key = match open_retryable(input, ticket_id, current_timestamp)? {
246 Some(k) => k,
247 None => {
248 let data = IArbRetryableTx::NoTicketWithID {}.abi_encode();
249 return crate::sol_error_revert(data, gas_limit);
250 }
251 };
252
253 let beneficiary_slot = map_slot(ticket_key.as_slice(), BENEFICIARY_OFFSET);
254 let beneficiary = sload_field(input, beneficiary_slot)?;
255
256 Ok(PrecompileOutput::new(
258 (3 * SLOAD_GAS + 2 * COPY_GAS).min(gas_limit),
259 beneficiary.to_be_bytes::<32>().to_vec().into(),
260 ))
261}
262
263fn handle_redeem(input: &mut PrecompileInput<'_>, ticket_id: B256) -> PrecompileResult {
267 let gas_limit = input.gas;
268 let caller = input.caller;
269 let current_timestamp: u64 = input
270 .internals()
271 .block_timestamp()
272 .try_into()
273 .unwrap_or(u64::MAX);
274
275 {
277 let current_retryable = crate::get_current_retryable_id();
278 if !current_retryable.is_zero()
279 && B256::from(current_retryable.to_be_bytes::<32>()) == ticket_id
280 {
281 return Err(PrecompileError::other("retryable cannot redeem itself"));
282 }
283 }
284
285 let ticket_key_pre = ticket_storage_key(ticket_id);
287 let timeout_slot = map_slot(ticket_key_pre.as_slice(), TIMEOUT_OFFSET);
288 let timeout_val = sload_field(input, timeout_slot)?;
289 let timeout_u64: u64 = timeout_val.try_into().unwrap_or(0);
290
291 let (_calldata_words, write_bytes, calldata_raw_size) =
292 if timeout_u64 == 0 || timeout_u64 < current_timestamp {
293 (0u64, 0u64, 0u64)
294 } else {
295 let calldata_sub = derive_subspace_key(ticket_key_pre.as_slice(), &[1]);
296 let calldata_size_slot = map_slot(calldata_sub.as_slice(), 0);
297 let calldata_size = sload_field(input, calldata_size_slot)?;
298 let calldata_size_u64: u64 = calldata_size.try_into().unwrap_or(0);
299 let cw = calldata_size_u64.div_ceil(32);
300 let nbytes = 6 * 32 + 32 + 32 * cw;
301 let wb = nbytes.div_ceil(32);
302 (cw, wb, calldata_size_u64)
303 };
304
305 const PARAMS_SLOAD_GAS: u64 = 50;
306 let retryable_size_gas = PARAMS_SLOAD_GAS.saturating_mul(write_bytes);
307 crate::charge_precompile_gas(retryable_size_gas);
308
309 let timeout_val2 = sload_field(input, timeout_slot)?;
311 let timeout_u64_2: u64 = timeout_val2.try_into().unwrap_or(0);
312 if timeout_u64_2 == 0 || timeout_u64_2 < current_timestamp {
313 let data = IArbRetryableTx::NoTicketWithID {}.abi_encode();
314 return crate::sol_error_revert(data, gas_limit);
315 }
316
317 let num_tries_slot = map_slot(ticket_key_pre.as_slice(), NUM_TRIES_OFFSET);
318 let num_tries = sload_field(input, num_tries_slot)?;
319 crate::charge_precompile_gas(SSTORE_GAS);
320 let nonce: u64 = num_tries.try_into().unwrap_or(0);
321 let internals = input.internals_mut();
322 internals
323 .sstore(ARBOS_STATE_ADDRESS, num_tries_slot, U256::from(nonce + 1))
324 .map_err(|_| PrecompileError::other("sstore failed"))?;
325
326 let make_tx_reads = 5 + calldata_raw_size / 32;
328 crate::charge_precompile_gas(make_tx_reads * SLOAD_GAS);
329
330 let mut hash_input = [0u8; 64];
332 hash_input[..32].copy_from_slice(ticket_id.as_slice());
333 hash_input[32..].copy_from_slice(&U256::from(nonce).to_be_bytes::<32>());
334 let retry_tx_hash = keccak256(hash_input);
335
336 let backlog_reservation = compute_backlog_update_cost(input)?;
337
338 let gas_used_so_far = crate::get_precompile_gas();
339
340 let future_gas_costs = REDEEM_SCHEDULED_EVENT_COST + COPY_GAS + backlog_reservation;
341 let gas_remaining = gas_limit.saturating_sub(gas_used_so_far);
342 if gas_remaining < future_gas_costs + TX_GAS {
343 return Err(PrecompileError::other(
344 "not enough gas to run redeem attempt",
345 ));
346 }
347 let gas_to_donate = gas_remaining - future_gas_costs;
348
349 let actual_backlog_cost = compute_actual_backlog_cost(input)?;
350
351 let max_refund = U256::MAX;
352 let submission_fee_refund = U256::ZERO;
353
354 let topic0 = redeem_scheduled_topic();
356 let topic1 = ticket_id;
357 let topic2 = B256::from(retry_tx_hash);
358 let mut seq_bytes = [0u8; 32];
359 seq_bytes[24..32].copy_from_slice(&nonce.to_be_bytes());
360 let topic3 = B256::from(seq_bytes);
361
362 let mut event_data = Vec::with_capacity(128);
363 event_data.extend_from_slice(&U256::from(gas_to_donate).to_be_bytes::<32>());
364 event_data.extend_from_slice(&B256::left_padding_from(caller.as_slice()).0);
365 event_data.extend_from_slice(&max_refund.to_be_bytes::<32>());
366 event_data.extend_from_slice(&submission_fee_refund.to_be_bytes::<32>());
367
368 let internals = input.internals_mut();
369 internals.log(Log::new_unchecked(
370 ARBRETRYABLETX_ADDRESS,
371 vec![topic0, topic1, topic2, topic3],
372 event_data.into(),
373 ));
374
375 let total_gas = gas_used_so_far
377 + REDEEM_SCHEDULED_EVENT_COST
378 + gas_to_donate
379 + actual_backlog_cost
380 + COPY_GAS;
381
382 Ok(PrecompileOutput::new(
383 total_gas.min(gas_limit),
384 retry_tx_hash.to_vec().into(),
385 ))
386}
387
388fn compute_actual_backlog_cost(input: &mut PrecompileInput<'_>) -> Result<u64, PrecompileError> {
389 use arb_chainspec::arbos_version as arb_ver;
390 let arbos_version = crate::get_arbos_version();
391 if arbos_version >= arb_ver::ARBOS_VERSION_MULTI_GAS_CONSTRAINTS {
392 return Ok(arbos::l2_pricing::MULTI_CONSTRAINT_STATIC_BACKLOG_UPDATE_COST);
393 }
394 if arbos_version >= arb_ver::ARBOS_VERSION_MULTI_CONSTRAINT_FIX {
395 let len = read_gas_constraints_length_free(input)?;
396 if len > 0 {
397 return Ok(2 * SLOAD_GAS + len.saturating_mul(SLOAD_GAS + SSTORE_RESET_GAS));
398 }
399 }
400 Ok(SLOAD_GAS + SSTORE_GAS)
401}
402
403fn compute_backlog_update_cost(input: &mut PrecompileInput<'_>) -> Result<u64, PrecompileError> {
404 use arb_chainspec::arbos_version as arb_ver;
405 let arbos_version = crate::get_arbos_version();
406 if arbos_version >= arb_ver::ARBOS_VERSION_MULTI_GAS_CONSTRAINTS {
407 return Ok(arbos::l2_pricing::MULTI_CONSTRAINT_STATIC_BACKLOG_UPDATE_COST);
408 }
409
410 let mut result = 0u64;
411 if arbos_version >= arb_ver::ARBOS_VERSION_50 {
412 result += SLOAD_GAS;
413 }
414 if arbos_version >= arb_ver::ARBOS_VERSION_MULTI_CONSTRAINT_FIX {
415 let len = read_gas_constraints_length(input)?;
416 if len > 0 {
417 result += SLOAD_GAS;
418 result += len.saturating_mul(SLOAD_GAS + SSTORE_GAS);
419 return Ok(result);
420 }
421 }
422 result += SLOAD_GAS + SSTORE_GAS;
423 Ok(result)
424}
425
426fn read_gas_constraints_length_free(
427 input: &mut PrecompileInput<'_>,
428) -> Result<u64, PrecompileError> {
429 let l2_subspace_key = derive_subspace_key(ROOT_STORAGE_KEY, L2_PRICING_SUBSPACE);
430 let gas_constraints_subspace_key = derive_subspace_key(l2_subspace_key.as_slice(), &[0]);
431 let len_slot = vector_length_slot(&gas_constraints_subspace_key);
432 let val = input
433 .internals_mut()
434 .sload(ARBOS_STATE_ADDRESS, len_slot)
435 .map_err(|_| PrecompileError::other("sload failed"))?;
436 Ok(val.data.try_into().unwrap_or(0))
437}
438
439fn read_gas_constraints_length(input: &mut PrecompileInput<'_>) -> Result<u64, PrecompileError> {
440 let l2_subspace_key = derive_subspace_key(ROOT_STORAGE_KEY, L2_PRICING_SUBSPACE);
441 let gas_constraints_subspace_key = derive_subspace_key(l2_subspace_key.as_slice(), &[0]);
442 let len_slot = vector_length_slot(&gas_constraints_subspace_key);
443 let val = sload_field(input, len_slot)?;
444 Ok(val.try_into().unwrap_or(0))
445}
446
447fn handle_keepalive(input: &mut PrecompileInput<'_>, ticket_id: B256) -> PrecompileResult {
452 let gas_limit = input.gas;
453 let current_timestamp: u64 = input
454 .internals()
455 .block_timestamp()
456 .try_into()
457 .unwrap_or(u64::MAX);
458
459 load_arbos(input)?;
460
461 let ticket_key = match open_retryable(input, ticket_id, current_timestamp)? {
462 Some(k) => k,
463 None => {
464 let data = IArbRetryableTx::NoTicketWithID {}.abi_encode();
465 return crate::sol_error_revert(data, gas_limit);
466 }
467 };
468
469 let calldata_sub = derive_subspace_key(ticket_key.as_slice(), &[1]);
471 let calldata_size_slot = map_slot(calldata_sub.as_slice(), 0);
472 let calldata_size = sload_field(input, calldata_size_slot)?;
473 let calldata_size_u64: u64 = calldata_size.try_into().unwrap_or(0);
474
475 let timeout_slot = map_slot(ticket_key.as_slice(), TIMEOUT_OFFSET);
477 let timeout = sload_field(input, timeout_slot)?;
478 let timeout_u64: u64 = timeout
479 .try_into()
480 .map_err(|_| PrecompileError::other("invalid timeout"))?;
481
482 let windows_slot = map_slot(ticket_key.as_slice(), TIMEOUT_WINDOWS_LEFT_OFFSET);
483 let windows = sload_field(input, windows_slot)?;
484 let windows_u64: u64 = windows
485 .try_into()
486 .map_err(|_| PrecompileError::other("invalid windows"))?;
487
488 let effective_timeout = timeout_u64 + windows_u64 * RETRYABLE_LIFETIME_SECONDS;
489
490 let window_limit = current_timestamp + RETRYABLE_LIFETIME_SECONDS;
492 if effective_timeout > window_limit {
493 return Err(PrecompileError::other("timeout too far into the future"));
494 }
495
496 queue_put(input, ticket_id)?;
498
499 let new_windows = windows_u64 + 1;
501 sstore_field(input, windows_slot, U256::from(new_windows))?;
502
503 let new_timeout = effective_timeout + RETRYABLE_LIFETIME_SECONDS;
504
505 let topic0 = lifetime_extended_topic();
507 let mut event_data = Vec::with_capacity(32);
508 event_data.extend_from_slice(&U256::from(new_timeout).to_be_bytes::<32>());
509 input.internals_mut().log(Log::new_unchecked(
510 ARBRETRYABLETX_ADDRESS,
511 vec![topic0, ticket_id],
512 event_data.into(),
513 ));
514
515 let calldata_words = calldata_size_u64.div_ceil(32);
520 let nbytes = 6 * 32 + 32 + 32 * calldata_words;
521 let update_cost = nbytes.div_ceil(32) * (SSTORE_GAS / 100);
522 let event_cost = LOG_GAS + 2 * LOG_TOPIC_GAS + LOG_DATA_GAS * 32;
523 let gas_used = 8 * SLOAD_GAS
524 + 3 * SSTORE_GAS
525 + 2 * COPY_GAS
526 + update_cost
527 + event_cost
528 + RETRYABLE_REAP_PRICE;
529
530 Ok(PrecompileOutput::new(
531 gas_used.min(gas_limit),
532 U256::from(new_timeout).to_be_bytes::<32>().to_vec().into(),
533 ))
534}
535
536fn handle_cancel(input: &mut PrecompileInput<'_>, ticket_id: B256) -> PrecompileResult {
541 let gas_limit = input.gas;
542 let caller = input.caller;
543 let current_timestamp: u64 = input
544 .internals()
545 .block_timestamp()
546 .try_into()
547 .unwrap_or(u64::MAX);
548
549 load_arbos(input)?;
550
551 let ticket_key = match open_retryable(input, ticket_id, current_timestamp)? {
552 Some(k) => k,
553 None => {
554 let data = IArbRetryableTx::NoTicketWithID {}.abi_encode();
555 return crate::sol_error_revert(data, gas_limit);
556 }
557 };
558
559 let beneficiary_slot = map_slot(ticket_key.as_slice(), BENEFICIARY_OFFSET);
561 let beneficiary = sload_field(input, beneficiary_slot)?;
562
563 let caller_u256 = U256::from_be_slice(caller.as_slice());
565 if caller_u256 != beneficiary {
566 return Err(PrecompileError::other(
567 "only the beneficiary may cancel a retryable",
568 ));
569 }
570
571 let offsets = [
573 NUM_TRIES_OFFSET,
574 FROM_OFFSET,
575 TO_OFFSET,
576 CALLVALUE_OFFSET,
577 BENEFICIARY_OFFSET,
578 TIMEOUT_OFFSET,
579 TIMEOUT_WINDOWS_LEFT_OFFSET,
580 ];
581 for offset in offsets {
582 let slot = map_slot(ticket_key.as_slice(), offset);
583 sstore_field(input, slot, U256::ZERO)?;
584 }
585
586 let calldata_sub = derive_subspace_key(ticket_key.as_slice(), &[1]);
588 let calldata_size_slot = map_slot(calldata_sub.as_slice(), 0);
589 let calldata_size = sload_field(input, calldata_size_slot)?;
590 let calldata_size_u64: u64 = calldata_size.try_into().unwrap_or(0);
591 let calldata_words = calldata_size_u64.div_ceil(32);
592 if calldata_size_u64 > 0 {
593 for i in 0..calldata_words {
594 let word_slot = map_slot(calldata_sub.as_slice(), 1 + i);
595 sstore_field(input, word_slot, U256::ZERO)?;
596 }
597 sstore_field(input, calldata_size_slot, U256::ZERO)?;
598 }
599
600 input.internals_mut().log(Log::new_unchecked(
602 ARBRETRYABLETX_ADDRESS,
603 vec![canceled_topic(), ticket_id],
604 Default::default(),
605 ));
606
607 let clear_bytes_cost = if calldata_size_u64 > 0 {
610 (calldata_words + 1) * SSTORE_ZERO_GAS
611 } else {
612 0
613 };
614 let event_cost = LOG_GAS + 2 * LOG_TOPIC_GAS;
615 let gas_used = 6 * SLOAD_GAS + 7 * SSTORE_ZERO_GAS + clear_bytes_cost + event_cost + COPY_GAS;
616
617 Ok(PrecompileOutput::new(
618 gas_used.min(gas_limit),
619 Vec::new().into(),
620 ))
621}