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::TransactionEnvMut for ArbTransaction {
147    fn set_gas_limit(&mut self, gas_limit: u64) {
148        self.0.gas_limit = gas_limit;
149    }
150
151    fn set_nonce(&mut self, nonce: u64) {
152        self.0.nonce = nonce;
153    }
154
155    fn set_access_list(&mut self, access_list: AccessList) {
156        self.0.access_list = access_list;
157    }
158}
159
160impl crate::build::ArbTransactionEnv for ArbTransaction {
161    fn set_gas_price(&mut self, gas_price: u128) {
162        self.0.gas_price = gas_price;
163    }
164    fn set_gas_priority_fee(&mut self, fee: Option<u128>) {
165        self.0.gas_priority_fee = fee;
166    }
167    fn set_value(&mut self, value: alloy_primitives::U256) {
168        self.0.value = value;
169    }
170}
171
172impl FromRecoveredTx<TransactionSigned> for ArbTransaction {
173    fn from_recovered_tx(tx: &TransactionSigned, sender: Address) -> Self {
174        ArbTransaction(TxEnv::from_recovered_tx(tx, sender))
175    }
176}
177
178impl FromTxWithEncoded<TransactionSigned> for ArbTransaction {
179    fn from_encoded_tx(tx: &TransactionSigned, sender: Address, encoded: Bytes) -> Self {
180        ArbTransaction(TxEnv::from_encoded_tx(tx, sender, encoded))
181    }
182}
183
184/// Convert an ArbTransactionSigned into a TxEnv for EVM execution.
185fn arb_tx_to_tx_env(tx: &ArbTransactionSigned, sender: Address) -> TxEnv {
186    use alloy_consensus::Typed2718;
187    let arb_type = ArbTxType::from_u8(Typed2718::ty(tx)).ok();
188    let is_system_tx = matches!(
189        arb_type,
190        Some(ArbTxType::ArbitrumInternalTx | ArbTxType::ArbitrumDepositTx)
191    );
192    let is_submit_retryable = arb_type == Some(ArbTxType::ArbitrumSubmitRetryableTx);
193
194    let mut env = TxEnv::default();
195    // Set tx_type for standard EVM types so revm correctly handles access
196    // list gas in intrinsic calculation. Arb custom types (0x64+) must remain
197    // Legacy (0) — revm doesn't understand them and would apply wrong gas rules
198    // (e.g., non-Legacy warming behavior, unknown type validation).
199    let raw_type = Typed2718::ty(tx);
200    env.tx_type = if raw_type < 0x64 { raw_type } else { 0 };
201    env.caller = sender;
202    env.gas_limit = tx.gas_limit();
203    env.nonce = tx.nonce();
204    env.chain_id = tx.chain_id();
205    env.kind = tx.to().map_or(
206        revm::primitives::TxKind::Create,
207        revm::primitives::TxKind::Call,
208    );
209    env.data = tx.input().clone();
210
211    if is_system_tx {
212        env.gas_price = 0;
213        env.gas_priority_fee = Some(0);
214        env.value = U256::ZERO;
215        if env.gas_limit == 0 {
216            env.gas_limit = 1_000_000;
217        }
218    } else if is_submit_retryable {
219        env.gas_price = 0;
220        env.gas_priority_fee = Some(0);
221        env.value = U256::ZERO;
222    } else {
223        env.gas_price = tx.max_fee_per_gas();
224        env.gas_priority_fee = tx.max_priority_fee_per_gas();
225        env.value = tx.value();
226    }
227
228    if let Some(al) = tx.access_list() {
229        env.access_list = al.clone();
230    }
231
232    // EIP-7702: propagate signed authorization list so revm processes
233    // delegations. Without this, 7702 txs execute with an empty list and
234    // revm rejects them with "empty authorization list".
235    if let Some(auths) = tx.authorization_list() {
236        use alloy_consensus::transaction::Either;
237        env.authorization_list = auths.iter().map(|a| Either::Left(a.clone())).collect();
238    }
239
240    env
241}
242
243impl FromRecoveredTx<ArbTransactionSigned> for ArbTransaction {
244    fn from_recovered_tx(tx: &ArbTransactionSigned, sender: Address) -> Self {
245        ArbTransaction(arb_tx_to_tx_env(tx, sender))
246    }
247}
248
249impl FromTxWithEncoded<ArbTransactionSigned> for ArbTransaction {
250    fn from_encoded_tx(tx: &ArbTransactionSigned, sender: Address, _encoded: Bytes) -> Self {
251        ArbTransaction(arb_tx_to_tx_env(tx, sender))
252    }
253}