arb_evm/
transaction.rs

1use alloy_consensus::Transaction;
2use alloy_eips::eip2930::AccessList;
3use alloy_evm::tx::{FromRecoveredTx, FromTxWithEncoded, IntoTxEnv};
4use alloy_primitives::{Address, Bytes, U256};
5use arb_primitives::{ArbTransactionSigned, tx_types::ArbTxType};
6use reth_ethereum_primitives::TransactionSigned;
7use revm::context::TxEnv;
8
9/// Helper for building Arbitrum-specific TxEnv values.
10///
11/// Handles Arbitrum-specific conversion rules:
12/// - Internal/Deposit txs get 1M gas if zero, gas_price=0
13/// - SubmitRetryable txs use gas_price=0 (no coinbase tips)
14/// - Retry txs preserve value for ETH transfers
15#[derive(Clone, Debug, Default, PartialEq, Eq)]
16pub struct ArbTransaction(pub TxEnv);
17
18impl ArbTransaction {
19    /// Create an ArbTransaction from the raw components of an Arbitrum tx.
20    pub fn from_parts(
21        sender: Address,
22        tx_type: ArbTxType,
23        gas_limit: u64,
24        gas_price: u128,
25        value: U256,
26        to: revm::primitives::TxKind,
27        data: alloy_primitives::Bytes,
28        nonce: u64,
29        chain_id: Option<u64>,
30    ) -> Self {
31        let mut tx = TxEnv {
32            caller: sender,
33            gas_limit,
34            ..Default::default()
35        };
36
37        // Internal/Deposit txs get minimum 1M gas
38        if matches!(
39            tx_type,
40            ArbTxType::ArbitrumInternalTx | ArbTxType::ArbitrumDepositTx
41        ) && gas_limit == 0
42        {
43            tx.gas_limit = 1_000_000;
44        }
45
46        tx.gas_priority_fee = Some(0);
47
48        match tx_type {
49            ArbTxType::ArbitrumDepositTx | ArbTxType::ArbitrumInternalTx => {
50                tx.value = U256::ZERO;
51                tx.gas_price = 0;
52            }
53            ArbTxType::ArbitrumSubmitRetryableTx => {
54                tx.value = U256::ZERO;
55                tx.gas_price = 0;
56            }
57            _ => {
58                tx.value = value;
59                tx.gas_price = gas_price;
60            }
61        }
62
63        tx.kind = to;
64        tx.data = data;
65        tx.nonce = nonce;
66        tx.chain_id = chain_id;
67
68        ArbTransaction(tx)
69    }
70
71    /// Unwrap into the inner TxEnv.
72    pub fn into_inner(self) -> TxEnv {
73        self.0
74    }
75}
76
77impl From<ArbTransaction> for TxEnv {
78    fn from(arb_tx: ArbTransaction) -> Self {
79        arb_tx.0
80    }
81}
82
83impl IntoTxEnv<ArbTransaction> for ArbTransaction {
84    fn into_tx_env(self) -> ArbTransaction {
85        self
86    }
87}
88
89impl revm::context_interface::Transaction for ArbTransaction {
90    type AccessListItem<'a>
91        = <TxEnv as revm::context_interface::Transaction>::AccessListItem<'a>
92    where
93        Self: 'a;
94    type Authorization<'a>
95        = <TxEnv as revm::context_interface::Transaction>::Authorization<'a>
96    where
97        Self: 'a;
98
99    fn tx_type(&self) -> u8 {
100        revm::context_interface::Transaction::tx_type(&self.0)
101    }
102    fn caller(&self) -> Address {
103        revm::context_interface::Transaction::caller(&self.0)
104    }
105    fn gas_limit(&self) -> u64 {
106        revm::context_interface::Transaction::gas_limit(&self.0)
107    }
108    fn value(&self) -> U256 {
109        revm::context_interface::Transaction::value(&self.0)
110    }
111    fn input(&self) -> &alloy_primitives::Bytes {
112        revm::context_interface::Transaction::input(&self.0)
113    }
114    fn nonce(&self) -> u64 {
115        revm::context_interface::Transaction::nonce(&self.0)
116    }
117    fn kind(&self) -> alloy_primitives::TxKind {
118        revm::context_interface::Transaction::kind(&self.0)
119    }
120    fn chain_id(&self) -> Option<u64> {
121        revm::context_interface::Transaction::chain_id(&self.0)
122    }
123    fn gas_price(&self) -> u128 {
124        revm::context_interface::Transaction::gas_price(&self.0)
125    }
126    fn access_list(&self) -> Option<impl Iterator<Item = Self::AccessListItem<'_>>> {
127        revm::context_interface::Transaction::access_list(&self.0)
128    }
129    fn blob_versioned_hashes(&self) -> &[alloy_primitives::B256] {
130        revm::context_interface::Transaction::blob_versioned_hashes(&self.0)
131    }
132    fn max_fee_per_blob_gas(&self) -> u128 {
133        revm::context_interface::Transaction::max_fee_per_blob_gas(&self.0)
134    }
135    fn authorization_list_len(&self) -> usize {
136        revm::context_interface::Transaction::authorization_list_len(&self.0)
137    }
138    fn authorization_list(&self) -> impl Iterator<Item = Self::Authorization<'_>> {
139        revm::context_interface::Transaction::authorization_list(&self.0)
140    }
141    fn max_priority_fee_per_gas(&self) -> Option<u128> {
142        revm::context_interface::Transaction::max_priority_fee_per_gas(&self.0)
143    }
144}
145
146impl reth_evm::TransactionEnv for ArbTransaction {
147    fn set_gas_limit(&mut self, gas_limit: u64) {
148        self.0.gas_limit = gas_limit;
149    }
150
151    fn nonce(&self) -> u64 {
152        self.0.nonce
153    }
154
155    fn set_nonce(&mut self, nonce: u64) {
156        self.0.nonce = nonce;
157    }
158
159    fn set_access_list(&mut self, access_list: AccessList) {
160        self.0.access_list = access_list;
161    }
162}
163
164impl crate::build::ArbTransactionEnv for ArbTransaction {
165    fn set_gas_price(&mut self, gas_price: u128) {
166        self.0.gas_price = gas_price;
167    }
168    fn set_gas_priority_fee(&mut self, fee: Option<u128>) {
169        self.0.gas_priority_fee = fee;
170    }
171    fn set_value(&mut self, value: alloy_primitives::U256) {
172        self.0.value = value;
173    }
174}
175
176impl FromRecoveredTx<TransactionSigned> for ArbTransaction {
177    fn from_recovered_tx(tx: &TransactionSigned, sender: Address) -> Self {
178        ArbTransaction(TxEnv::from_recovered_tx(tx, sender))
179    }
180}
181
182impl FromTxWithEncoded<TransactionSigned> for ArbTransaction {
183    fn from_encoded_tx(tx: &TransactionSigned, sender: Address, encoded: Bytes) -> Self {
184        ArbTransaction(TxEnv::from_encoded_tx(tx, sender, encoded))
185    }
186}
187
188/// Convert an ArbTransactionSigned into a TxEnv for EVM execution.
189fn arb_tx_to_tx_env(tx: &ArbTransactionSigned, sender: Address) -> TxEnv {
190    use alloy_consensus::Typed2718;
191    let arb_type = ArbTxType::from_u8(Typed2718::ty(tx)).ok();
192    let is_system_tx = matches!(
193        arb_type,
194        Some(ArbTxType::ArbitrumInternalTx | ArbTxType::ArbitrumDepositTx)
195    );
196    let is_submit_retryable = arb_type == Some(ArbTxType::ArbitrumSubmitRetryableTx);
197
198    let mut env = TxEnv::default();
199    // Set tx_type for standard EVM types so revm correctly handles access
200    // list gas in intrinsic calculation. Arb custom types (0x64+) must remain
201    // Legacy (0) — revm doesn't understand them and would apply wrong gas rules
202    // (e.g., non-Legacy warming behavior, unknown type validation).
203    let raw_type = Typed2718::ty(tx);
204    env.tx_type = if raw_type < 0x64 { raw_type } else { 0 };
205    env.caller = sender;
206    env.gas_limit = tx.gas_limit();
207    env.nonce = tx.nonce();
208    env.chain_id = tx.chain_id();
209    env.kind = tx.to().map_or(
210        revm::primitives::TxKind::Create,
211        revm::primitives::TxKind::Call,
212    );
213    env.data = tx.input().clone();
214
215    if is_system_tx {
216        env.gas_price = 0;
217        env.gas_priority_fee = Some(0);
218        env.value = U256::ZERO;
219        if env.gas_limit == 0 {
220            env.gas_limit = 1_000_000;
221        }
222    } else if is_submit_retryable {
223        env.gas_price = 0;
224        env.gas_priority_fee = Some(0);
225        env.value = U256::ZERO;
226    } else {
227        env.gas_price = tx.max_fee_per_gas();
228        env.gas_priority_fee = tx.max_priority_fee_per_gas();
229        env.value = tx.value();
230    }
231
232    if let Some(al) = tx.access_list() {
233        env.access_list = al.clone();
234    }
235
236    // EIP-7702: propagate signed authorization list so revm processes
237    // delegations. Without this, 7702 txs execute with an empty list and
238    // revm rejects them with "empty authorization list".
239    if let Some(auths) = tx.authorization_list() {
240        use alloy_consensus::transaction::Either;
241        env.authorization_list = auths.iter().map(|a| Either::Left(a.clone())).collect();
242    }
243
244    env
245}
246
247impl FromRecoveredTx<ArbTransactionSigned> for ArbTransaction {
248    fn from_recovered_tx(tx: &ArbTransactionSigned, sender: Address) -> Self {
249        ArbTransaction(arb_tx_to_tx_env(tx, sender))
250    }
251}
252
253impl FromTxWithEncoded<ArbTransactionSigned> for ArbTransaction {
254    fn from_encoded_tx(tx: &ArbTransactionSigned, sender: Address, _encoded: Bytes) -> Self {
255        ArbTransaction(arb_tx_to_tx_env(tx, sender))
256    }
257}