1use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
2use alloy_primitives::{keccak256, Address, Log, B256, U256};
3use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
4
5use crate::storage_slot::{
6 current_redeemer_slot, current_retryable_slot, derive_subspace_key, map_slot,
7 ARBOS_STATE_ADDRESS, RETRYABLES_SUBSPACE, ROOT_STORAGE_KEY,
8};
9
10pub const ARBRETRYABLETX_ADDRESS: Address = Address::new([
12 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
13 0x00, 0x00, 0x00, 0x6e,
14]);
15
16const REDEEM: [u8; 4] = [0xed, 0xa1, 0x12, 0x2c];
18const GET_LIFETIME: [u8; 4] = [0x81, 0xe6, 0xe0, 0x83];
19const GET_TIMEOUT: [u8; 4] = [0x9f, 0x10, 0x25, 0xc6];
20const KEEPALIVE: [u8; 4] = [0xf0, 0xb2, 0x1a, 0x41];
21const GET_BENEFICIARY: [u8; 4] = [0xba, 0x20, 0xdd, 0xa4];
22const CANCEL: [u8; 4] = [0xc4, 0xd2, 0x52, 0xf5];
23const GET_CURRENT_REDEEMER: [u8; 4] = [0xde, 0x4b, 0xa2, 0xb3];
24const SUBMIT_RETRYABLE: [u8; 4] = [0xc9, 0xf9, 0x5d, 0x32];
25
26const RETRYABLE_LIFETIME_SECONDS: u64 = 7 * 24 * 60 * 60;
28const RETRYABLE_REAP_PRICE: u64 = 58_000;
29
30const NUM_TRIES_OFFSET: u64 = 0;
32const FROM_OFFSET: u64 = 1;
33const TO_OFFSET: u64 = 2;
34const CALLVALUE_OFFSET: u64 = 3;
35const BENEFICIARY_OFFSET: u64 = 4;
36const TIMEOUT_OFFSET: u64 = 5;
37const TIMEOUT_WINDOWS_LEFT_OFFSET: u64 = 6;
38
39const TIMEOUT_QUEUE_KEY: &[u8] = &[0];
41
42const SLOAD_GAS: u64 = 800;
43const SSTORE_GAS: u64 = 20_000;
44const SSTORE_ZERO_GAS: u64 = 5_000;
45const COPY_GAS: u64 = 3;
46const TX_GAS: u64 = 21_000;
47const LOG_GAS: u64 = 375;
48const LOG_TOPIC_GAS: u64 = 375;
49const LOG_DATA_GAS: u64 = 8;
50
51const REDEEM_SCHEDULED_DATA_BYTES: u64 = 128;
53
54const REDEEM_SCHEDULED_EVENT_COST: u64 =
56 LOG_GAS + 4 * LOG_TOPIC_GAS + LOG_DATA_GAS * REDEEM_SCHEDULED_DATA_BYTES;
57
58pub fn ticket_created_topic() -> B256 {
65 keccak256("TicketCreated(bytes32)")
66}
67
68pub fn redeem_scheduled_topic() -> B256 {
71 keccak256("RedeemScheduled(bytes32,bytes32,uint64,uint64,address,uint256,uint256)")
72}
73
74pub fn create_arbretryabletx_precompile() -> DynPrecompile {
75 DynPrecompile::new_stateful(PrecompileId::custom("arbretryabletx"), handler)
76}
77
78fn handler(mut input: PrecompileInput<'_>) -> PrecompileResult {
79 let data = input.data;
80 if data.len() < 4 {
81 return Err(PrecompileError::other("input too short"));
82 }
83
84 let selector: [u8; 4] = [data[0], data[1], data[2], data[3]];
85 let gas_limit = input.gas;
86
87 let result = match selector {
88 GET_LIFETIME => {
89 let lifetime = U256::from(RETRYABLE_LIFETIME_SECONDS);
90 Ok(PrecompileOutput::new(
91 (SLOAD_GAS + COPY_GAS).min(gas_limit),
92 lifetime.to_be_bytes::<32>().to_vec().into(),
93 ))
94 }
95 GET_CURRENT_REDEEMER => {
96 let internals = input.internals_mut();
99 internals
100 .load_account(ARBOS_STATE_ADDRESS)
101 .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
102 let redeemer = internals
103 .sload(ARBOS_STATE_ADDRESS, current_redeemer_slot())
104 .map_err(|_| PrecompileError::other("sload failed"))?
105 .data;
106 Ok(PrecompileOutput::new(
107 (SLOAD_GAS + COPY_GAS).min(gas_limit),
108 redeemer.to_be_bytes::<32>().to_vec().into(),
109 ))
110 }
111 SUBMIT_RETRYABLE => {
112 Err(PrecompileError::other("not callable"))
114 }
115 GET_TIMEOUT => handle_get_timeout(&mut input),
116 GET_BENEFICIARY => handle_get_beneficiary(&mut input),
117 REDEEM => handle_redeem(&mut input),
118 KEEPALIVE => handle_keepalive(&mut input),
119 CANCEL => handle_cancel(&mut input),
120 _ => Err(PrecompileError::other("unknown ArbRetryableTx selector")),
121 };
122 crate::gas_check(gas_limit, result)
123}
124
125fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), PrecompileError> {
128 input
129 .internals_mut()
130 .load_account(ARBOS_STATE_ADDRESS)
131 .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
132 Ok(())
133}
134
135fn sload_field(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
136 let val = input
137 .internals_mut()
138 .sload(ARBOS_STATE_ADDRESS, slot)
139 .map_err(|_| PrecompileError::other("sload failed"))?;
140 Ok(val.data)
141}
142
143fn sstore_field(
144 input: &mut PrecompileInput<'_>,
145 slot: U256,
146 value: U256,
147) -> Result<(), PrecompileError> {
148 input
149 .internals_mut()
150 .sstore(ARBOS_STATE_ADDRESS, slot, value)
151 .map_err(|_| PrecompileError::other("sstore failed"))?;
152 Ok(())
153}
154
155fn ticket_storage_key(ticket_id: B256) -> B256 {
157 let retryables_key = derive_subspace_key(ROOT_STORAGE_KEY, RETRYABLES_SUBSPACE);
158 derive_subspace_key(retryables_key.as_slice(), ticket_id.as_slice())
159}
160
161fn open_retryable(
164 input: &mut PrecompileInput<'_>,
165 ticket_id: B256,
166 current_timestamp: u64,
167) -> Result<B256, PrecompileError> {
168 let ticket_key = ticket_storage_key(ticket_id);
169 let timeout_slot = map_slot(ticket_key.as_slice(), TIMEOUT_OFFSET);
170 let timeout = sload_field(input, timeout_slot)?;
171 let timeout_u64: u64 = timeout
172 .try_into()
173 .map_err(|_| PrecompileError::other("invalid timeout value"))?;
174
175 if timeout_u64 == 0 {
176 return Err(PrecompileError::other("retryable ticket not found"));
177 }
178 if timeout_u64 < current_timestamp {
179 return Err(PrecompileError::other("retryable ticket expired"));
180 }
181
182 Ok(ticket_key)
183}
184
185fn handle_get_timeout(input: &mut PrecompileInput<'_>) -> PrecompileResult {
188 let data = input.data;
189 if data.len() < 36 {
190 return Err(PrecompileError::other("input too short"));
191 }
192
193 let gas_limit = input.gas;
194 let ticket_id = B256::from_slice(&data[4..36]);
195
196 load_arbos(input)?;
197
198 let ticket_key = ticket_storage_key(ticket_id);
199
200 let timeout_slot = map_slot(ticket_key.as_slice(), TIMEOUT_OFFSET);
202 let timeout = sload_field(input, timeout_slot)?;
203 let timeout_u64: u64 = timeout.try_into().unwrap_or(0);
204
205 if timeout_u64 == 0 {
206 return Err(PrecompileError::other("retryable ticket not found"));
207 }
208
209 let windows_slot = map_slot(ticket_key.as_slice(), TIMEOUT_WINDOWS_LEFT_OFFSET);
211 let windows = sload_field(input, windows_slot)?;
212 let windows_u64: u64 = windows.try_into().unwrap_or(0);
213
214 let effective_timeout = timeout_u64 + windows_u64 * RETRYABLE_LIFETIME_SECONDS;
215
216 Ok(PrecompileOutput::new(
219 (4 * SLOAD_GAS + 2 * COPY_GAS).min(gas_limit),
220 U256::from(effective_timeout)
221 .to_be_bytes::<32>()
222 .to_vec()
223 .into(),
224 ))
225}
226
227fn timeout_queue_key() -> B256 {
229 let retryables_key = derive_subspace_key(ROOT_STORAGE_KEY, RETRYABLES_SUBSPACE);
230 derive_subspace_key(retryables_key.as_slice(), TIMEOUT_QUEUE_KEY)
231}
232
233fn queue_put(input: &mut PrecompileInput<'_>, value: B256) -> Result<(), PrecompileError> {
236 let queue_key = timeout_queue_key();
237
238 let put_offset_slot = map_slot(queue_key.as_slice(), 0);
240 let put_offset = sload_field(input, put_offset_slot)?;
241 let put_offset_u64: u64 = put_offset
242 .try_into()
243 .map_err(|_| PrecompileError::other("invalid queue put offset"))?;
244
245 let item_slot = map_slot(queue_key.as_slice(), put_offset_u64);
247 sstore_field(input, item_slot, U256::from_be_bytes(value.0))?;
248
249 sstore_field(input, put_offset_slot, U256::from(put_offset_u64 + 1))?;
251
252 Ok(())
253}
254
255fn handle_get_beneficiary(input: &mut PrecompileInput<'_>) -> PrecompileResult {
257 let data = input.data;
258 if data.len() < 36 {
259 return Err(PrecompileError::other("input too short"));
260 }
261
262 let gas_limit = input.gas;
263 let ticket_id = B256::from_slice(&data[4..36]);
264 let current_timestamp: u64 = input
265 .internals()
266 .block_timestamp()
267 .try_into()
268 .unwrap_or(u64::MAX);
269
270 load_arbos(input)?;
271
272 let ticket_key = open_retryable(input, ticket_id, current_timestamp)?;
273
274 let beneficiary_slot = map_slot(ticket_key.as_slice(), BENEFICIARY_OFFSET);
276 let beneficiary = sload_field(input, beneficiary_slot)?;
277
278 Ok(PrecompileOutput::new(
280 (3 * SLOAD_GAS + 2 * COPY_GAS).min(gas_limit),
281 beneficiary.to_be_bytes::<32>().to_vec().into(),
282 ))
283}
284
285fn handle_redeem(input: &mut PrecompileInput<'_>) -> PrecompileResult {
289 let data = input.data;
290 if data.len() < 36 {
291 return Err(PrecompileError::other("input too short"));
292 }
293
294 let gas_limit = input.gas;
295 let ticket_id = B256::from_slice(&data[4..36]);
296 let caller = input.caller;
297 let current_timestamp: u64 = input
298 .internals()
299 .block_timestamp()
300 .try_into()
301 .unwrap_or(u64::MAX);
302
303 let internals = input.internals_mut();
304
305 internals
307 .load_account(ARBOS_STATE_ADDRESS)
308 .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
309
310 let current_retryable = internals
312 .sload(ARBOS_STATE_ADDRESS, current_retryable_slot())
313 .map_err(|_| PrecompileError::other("sload failed"))?
314 .data;
315 if !current_retryable.is_zero()
316 && B256::from(current_retryable.to_be_bytes::<32>()) == ticket_id
317 {
318 return Err(PrecompileError::other("retryable cannot redeem itself"));
319 }
320
321 let ticket_key_pre = ticket_storage_key(ticket_id);
323 let (calldata_words, write_bytes, nonce) = {
324 let timeout_slot = map_slot(ticket_key_pre.as_slice(), TIMEOUT_OFFSET);
326 let timeout_check = internals
327 .sload(ARBOS_STATE_ADDRESS, timeout_slot)
328 .map_err(|_| PrecompileError::other("sload failed"))?
329 .data;
330 let timeout_u64: u64 = timeout_check.try_into().unwrap_or(0);
331 if timeout_u64 == 0 || timeout_u64 < current_timestamp {
332 return Err(PrecompileError::other(
333 "retryable ticket not found or expired",
334 ));
335 }
336
337 let calldata_sub = derive_subspace_key(ticket_key_pre.as_slice(), &[1]);
339 let calldata_size_slot = map_slot(calldata_sub.as_slice(), 0);
340 let calldata_size = internals
341 .sload(ARBOS_STATE_ADDRESS, calldata_size_slot)
342 .map_err(|_| PrecompileError::other("sload failed"))?
343 .data;
344 let calldata_size_u64: u64 = calldata_size.try_into().unwrap_or(0);
345 let cw = calldata_size_u64.div_ceil(32);
346 let nbytes = 6 * 32 + 32 + 32 * cw;
347 let wb = nbytes.div_ceil(32);
348
349 let num_tries_slot = map_slot(ticket_key_pre.as_slice(), NUM_TRIES_OFFSET);
351 let num_tries = internals
352 .sload(ARBOS_STATE_ADDRESS, num_tries_slot)
353 .map_err(|_| PrecompileError::other("sload failed"))?
354 .data;
355 let n: u64 = num_tries.try_into().unwrap_or(0);
356
357 (cw, wb, n)
358 };
359 let _ticket_key = ticket_key_pre;
360
361 let mut hash_input = [0u8; 64];
363 hash_input[..32].copy_from_slice(ticket_id.as_slice());
364 hash_input[32..].copy_from_slice(&U256::from(nonce).to_be_bytes::<32>());
365 let retry_tx_hash = keccak256(hash_input);
366
367 const PARAMS_SLOAD_GAS: u64 = 50; let retryable_size_gas = PARAMS_SLOAD_GAS.saturating_mul(write_bytes);
391
392 let make_tx_reads = 4 + calldata_words;
394
395 let gas_used_so_far = COPY_GAS + SLOAD_GAS + 2 * SLOAD_GAS + retryable_size_gas + SLOAD_GAS + SLOAD_GAS + SSTORE_GAS + make_tx_reads * SLOAD_GAS; let backlog_reservation = SLOAD_GAS + SSTORE_GAS; let future_gas_costs = REDEEM_SCHEDULED_EVENT_COST + COPY_GAS + backlog_reservation;
412 let gas_remaining = gas_limit.saturating_sub(gas_used_so_far);
413 if gas_remaining < future_gas_costs + TX_GAS {
414 return Err(PrecompileError::other(
415 "not enough gas to run redeem attempt",
416 ));
417 }
418 let gas_to_donate = gas_remaining - future_gas_costs;
419
420 let actual_backlog_cost = {
424 let current_backlog = crate::get_current_gas_backlog();
425 let new_backlog = current_backlog.saturating_sub(gas_to_donate);
426 let write_cost = if new_backlog == 0 {
427 SSTORE_ZERO_GAS } else {
429 SSTORE_GAS };
431 SLOAD_GAS + write_cost
432 };
433
434 let max_refund = U256::MAX;
436 let submission_fee_refund = U256::ZERO;
437
438 let topic0 = redeem_scheduled_topic();
440 let topic1 = ticket_id;
441 let topic2 = B256::from(retry_tx_hash);
442 let mut seq_bytes = [0u8; 32];
443 seq_bytes[24..32].copy_from_slice(&nonce.to_be_bytes());
444 let topic3 = B256::from(seq_bytes);
445
446 let mut event_data = Vec::with_capacity(128);
447 event_data.extend_from_slice(&U256::from(gas_to_donate).to_be_bytes::<32>());
448 event_data.extend_from_slice(&B256::left_padding_from(caller.as_slice()).0);
449 event_data.extend_from_slice(&max_refund.to_be_bytes::<32>());
450 event_data.extend_from_slice(&submission_fee_refund.to_be_bytes::<32>());
451
452 internals.log(Log::new_unchecked(
453 ARBRETRYABLETX_ADDRESS,
454 vec![topic0, topic1, topic2, topic3],
455 event_data.into(),
456 ));
457
458 let total_gas = gas_used_so_far
466 + REDEEM_SCHEDULED_EVENT_COST
467 + gas_to_donate
468 + actual_backlog_cost
469 + COPY_GAS;
470
471 Ok(PrecompileOutput::new(
472 total_gas.min(gas_limit),
473 retry_tx_hash.to_vec().into(),
474 ))
475}
476
477fn handle_keepalive(input: &mut PrecompileInput<'_>) -> PrecompileResult {
482 let data = input.data;
483 if data.len() < 36 {
484 return Err(PrecompileError::other("input too short"));
485 }
486
487 let gas_limit = input.gas;
488 let ticket_id = B256::from_slice(&data[4..36]);
489 let current_timestamp: u64 = input
490 .internals()
491 .block_timestamp()
492 .try_into()
493 .unwrap_or(u64::MAX);
494
495 load_arbos(input)?;
496
497 let ticket_key = open_retryable(input, ticket_id, current_timestamp)?;
499
500 let calldata_sub = derive_subspace_key(ticket_key.as_slice(), &[1]);
502 let calldata_size_slot = map_slot(calldata_sub.as_slice(), 0);
503 let calldata_size = sload_field(input, calldata_size_slot)?;
504 let calldata_size_u64: u64 = calldata_size.try_into().unwrap_or(0);
505
506 let timeout_slot = map_slot(ticket_key.as_slice(), TIMEOUT_OFFSET);
508 let timeout = sload_field(input, timeout_slot)?;
509 let timeout_u64: u64 = timeout
510 .try_into()
511 .map_err(|_| PrecompileError::other("invalid timeout"))?;
512
513 let windows_slot = map_slot(ticket_key.as_slice(), TIMEOUT_WINDOWS_LEFT_OFFSET);
514 let windows = sload_field(input, windows_slot)?;
515 let windows_u64: u64 = windows
516 .try_into()
517 .map_err(|_| PrecompileError::other("invalid windows"))?;
518
519 let effective_timeout = timeout_u64 + windows_u64 * RETRYABLE_LIFETIME_SECONDS;
520
521 let window_limit = current_timestamp + RETRYABLE_LIFETIME_SECONDS;
523 if effective_timeout > window_limit {
524 return Err(PrecompileError::other("timeout too far into the future"));
525 }
526
527 queue_put(input, ticket_id)?;
529
530 let new_windows = windows_u64 + 1;
532 sstore_field(input, windows_slot, U256::from(new_windows))?;
533
534 let new_timeout = effective_timeout + RETRYABLE_LIFETIME_SECONDS;
535
536 let calldata_words = calldata_size_u64.div_ceil(32);
541 let nbytes = 6 * 32 + 32 + 32 * calldata_words;
542 let update_cost = nbytes.div_ceil(32) * (SSTORE_GAS / 100);
543 let event_cost = LOG_GAS + 2 * LOG_TOPIC_GAS + LOG_DATA_GAS * 32;
544 let gas_used = 8 * SLOAD_GAS
545 + 3 * SSTORE_GAS
546 + 2 * COPY_GAS
547 + update_cost
548 + event_cost
549 + RETRYABLE_REAP_PRICE;
550
551 Ok(PrecompileOutput::new(
552 gas_used.min(gas_limit),
553 U256::from(new_timeout).to_be_bytes::<32>().to_vec().into(),
554 ))
555}
556
557fn handle_cancel(input: &mut PrecompileInput<'_>) -> PrecompileResult {
562 let data = input.data;
563 if data.len() < 36 {
564 return Err(PrecompileError::other("input too short"));
565 }
566
567 let gas_limit = input.gas;
568 let ticket_id = B256::from_slice(&data[4..36]);
569 let caller = input.caller;
570 let current_timestamp: u64 = input
571 .internals()
572 .block_timestamp()
573 .try_into()
574 .unwrap_or(u64::MAX);
575
576 load_arbos(input)?;
577
578 let ticket_key = open_retryable(input, ticket_id, current_timestamp)?;
580
581 let beneficiary_slot = map_slot(ticket_key.as_slice(), BENEFICIARY_OFFSET);
583 let beneficiary = sload_field(input, beneficiary_slot)?;
584
585 let caller_u256 = U256::from_be_slice(caller.as_slice());
587 if caller_u256 != beneficiary {
588 return Err(PrecompileError::other(
589 "only the beneficiary may cancel a retryable",
590 ));
591 }
592
593 let offsets = [
595 NUM_TRIES_OFFSET,
596 FROM_OFFSET,
597 TO_OFFSET,
598 CALLVALUE_OFFSET,
599 BENEFICIARY_OFFSET,
600 TIMEOUT_OFFSET,
601 TIMEOUT_WINDOWS_LEFT_OFFSET,
602 ];
603 for offset in offsets {
604 let slot = map_slot(ticket_key.as_slice(), offset);
605 sstore_field(input, slot, U256::ZERO)?;
606 }
607
608 let calldata_sub = derive_subspace_key(ticket_key.as_slice(), &[1]);
610 let calldata_size_slot = map_slot(calldata_sub.as_slice(), 0);
611 let calldata_size = sload_field(input, calldata_size_slot)?;
612 let calldata_size_u64: u64 = calldata_size.try_into().unwrap_or(0);
613 let calldata_words = calldata_size_u64.div_ceil(32);
614 if calldata_size_u64 > 0 {
615 for i in 0..calldata_words {
616 let word_slot = map_slot(calldata_sub.as_slice(), 1 + i);
617 sstore_field(input, word_slot, U256::ZERO)?;
618 }
619 sstore_field(input, calldata_size_slot, U256::ZERO)?;
620 }
621
622 let clear_bytes_cost = if calldata_size_u64 > 0 {
627 (calldata_words + 1) * SSTORE_ZERO_GAS
628 } else {
629 0
630 };
631 let event_cost = LOG_GAS + 2 * LOG_TOPIC_GAS;
632 let gas_used = 6 * SLOAD_GAS + 7 * SSTORE_ZERO_GAS + clear_bytes_cost + event_cost + COPY_GAS;
633
634 Ok(PrecompileOutput::new(
635 gas_used.min(gas_limit),
636 Vec::new().into(),
637 ))
638}