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