arb_precompiles/
arbfilteredtxmanager.rs

1use std::sync::Arc;
2
3use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
4use alloy_primitives::{Address, B256, Log, U256};
5use alloy_sol_types::{SolEvent, SolInterface};
6use arb_context::ArbPrecompileCtx;
7use arb_storage::{ARBOS_STATE_ADDRESS, FILTERED_TX_STATE_ADDRESS};
8use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
9
10use crate::{ArbPrecompileError, interfaces::IArbFilteredTxManager};
11
12/// ArbFilteredTransactionsManager precompile address (0x74).
13pub const ARBFILTEREDTXMANAGER_ADDRESS: Address = Address::new([
14    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
15    0x00, 0x00, 0x00, 0x74,
16]);
17
18const SLOAD_GAS: u64 = 800;
19const SSTORE_GAS: u64 = 20_000;
20const SSTORE_CLEAR_GAS: u64 = 5_000;
21const COPY_GAS: u64 = 3;
22const LOG_GAS: u64 = 375 + 2 * 375;
23
24pub fn create_arbfilteredtxmanager_precompile(ctx: Arc<ArbPrecompileCtx>) -> DynPrecompile {
25    DynPrecompile::new_stateful(PrecompileId::custom("arbfilteredtxmanager"), move |input| {
26        handler(input, &ctx)
27    })
28}
29
30fn handler(mut input: PrecompileInput<'_>, ctx: &ArbPrecompileCtx) -> PrecompileResult {
31    if let Some(result) = crate::check_precompile_version(
32        ctx,
33        arb_chainspec::arbos_version::ARBOS_VERSION_TRANSACTION_FILTERING,
34    ) {
35        return result;
36    }
37
38    let gas_limit = input.gas;
39
40    // Value, read-only and delegate rejections are handled inside the inner
41    // handlers so the free-access wrapper's gas override still applies.
42
43    // Free-access wrapper: 2 SLOAD membership check (1600), gas overridden to 0
44    // for filterers. Inner per-dim contributions are snapshotted and discarded.
45    let mg_snapshot = ctx.snapshot_precompile_multi_gas();
46    let mut wrapper_gas_used = 0u64;
47    crate::charge_storage_read(&mut wrapper_gas_used, ctx, SLOAD_GAS);
48    let caller = input.caller;
49    load_accounts(&mut input)?;
50    let is_filterer = {
51        let internals = input.internals_mut();
52        let arb_state = ctx
53            .block
54            .arbos_state(internals)
55            .map_err(ArbPrecompileError::fatal)?;
56        let res = arb_state
57            .transaction_filterers
58            .is_member(internals, caller)
59            .map_err(ArbPrecompileError::fatal)?;
60        crate::charge_storage_read(&mut wrapper_gas_used, ctx, SLOAD_GAS);
61        res
62    };
63    let wrapper_gas = wrapper_gas_used;
64
65    let call =
66        match IArbFilteredTxManager::ArbFilteredTransactionsManagerCalls::abi_decode(input.data) {
67            Ok(c) => c,
68            Err(_) => {
69                // The free-access wrapper already ran (membership check); a bad
70                // selector reverts with the wrapper's gas, not the whole limit.
71                let final_gas = if is_filterer {
72                    0
73                } else {
74                    wrapper_gas.min(gas_limit)
75                };
76                // For the filterer-free path, also discard the wrapper's dim
77                // contributions so the receipt and the backlog match.
78                if is_filterer {
79                    ctx.restore_precompile_multi_gas(mg_snapshot);
80                }
81                return Ok(PrecompileOutput::new_reverted(
82                    final_gas,
83                    Default::default(),
84                ));
85            }
86        };
87
88    let mut gas_used = 0u64;
89    crate::init_precompile_gas(&mut gas_used, ctx, input.data.len());
90    use IArbFilteredTxManager::ArbFilteredTransactionsManagerCalls as Calls;
91    let inner_result = match call {
92        Calls::addFilteredTransaction(c) => {
93            handle_add_filtered_tx(&mut input, &mut gas_used, c.txHash, ctx)
94        }
95        Calls::deleteFilteredTransaction(c) => {
96            handle_delete_filtered_tx(&mut input, &mut gas_used, c.txHash, ctx)
97        }
98        Calls::isTransactionFiltered(c) => {
99            handle_is_tx_filtered(&mut input, &mut gas_used, c.txHash, ctx)
100        }
101    };
102
103    // Override inner gas: 0 for filterer, else the wrapper SLOADs.
104    let final_gas = if is_filterer {
105        0
106    } else {
107        wrapper_gas.min(gas_limit)
108    };
109    ctx.restore_precompile_multi_gas(mg_snapshot);
110    if !is_filterer {
111        // Re-record the two wrapper SLOADs after the snapshot restore.
112        ctx.add_precompile_multi_gas(
113            arb_primitives::multigas::ResourceKind::StorageAccessRead,
114            2 * SLOAD_GAS,
115        );
116    }
117    match inner_result {
118        Ok(_) if gas_used > gas_limit => Ok(PrecompileOutput::new_reverted(
119            final_gas,
120            Default::default(),
121        )),
122        Ok(mut output) => {
123            output.gas_used = final_gas;
124            Ok(output)
125        }
126        Err(PrecompileError::Other(_)) => Ok(PrecompileOutput::new_reverted(
127            final_gas,
128            Default::default(),
129        )),
130        Err(e) => Err(e),
131    }
132}
133
134// ── helpers ──────────────────────────────────────────────────────────
135
136fn load_accounts(input: &mut PrecompileInput<'_>) -> Result<(), ArbPrecompileError> {
137    input
138        .internals_mut()
139        .load_account(ARBOS_STATE_ADDRESS)
140        .map_err(ArbPrecompileError::fatal)?;
141    input
142        .internals_mut()
143        .load_account(FILTERED_TX_STATE_ADDRESS)
144        .map_err(ArbPrecompileError::fatal)?;
145    Ok(())
146}
147
148/// Check if caller is a transaction filterer via the TransactionFilterers address set.
149fn is_transaction_filterer(
150    input: &mut PrecompileInput<'_>,
151    gas_used: &mut u64,
152    addr: Address,
153    ctx: &ArbPrecompileCtx,
154) -> Result<bool, ArbPrecompileError> {
155    let internals = input.internals_mut();
156    let arb_state = ctx
157        .block
158        .arbos_state(internals)
159        .map_err(ArbPrecompileError::fatal)?;
160    let is_member = arb_state
161        .transaction_filterers
162        .is_member(internals, addr)
163        .map_err(ArbPrecompileError::fatal)?;
164    crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
165    Ok(is_member)
166}
167
168fn handle_is_tx_filtered(
169    input: &mut PrecompileInput<'_>,
170    gas_used: &mut u64,
171    tx_hash: B256,
172    ctx: &ArbPrecompileCtx,
173) -> PrecompileResult {
174    let gas_limit = input.gas;
175    // A view method rejects call value and DELEGATECALL.
176    if !input.value.is_zero() || input.target_address != input.bytecode_address {
177        return Err(ArbPrecompileError::empty_revert(*gas_used).into());
178    }
179    load_accounts(input)?;
180
181    let internals = input.internals_mut();
182    let arb_state = ctx
183        .block
184        .arbos_state(internals)
185        .map_err(ArbPrecompileError::fatal)?;
186    let is_filtered_bool = arb_state
187        .filtered_transactions
188        .is_filtered(internals, tx_hash)
189        .map_err(ArbPrecompileError::fatal)?;
190    crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
191
192    let is_filtered = if is_filtered_bool {
193        U256::from(1u64)
194    } else {
195        U256::ZERO
196    };
197
198    crate::charge_computation(gas_used, ctx, COPY_GAS);
199    Ok(PrecompileOutput::new(
200        (*gas_used).min(gas_limit),
201        is_filtered.to_be_bytes::<32>().to_vec().into(),
202    ))
203}
204
205fn handle_add_filtered_tx(
206    input: &mut PrecompileInput<'_>,
207    gas_used: &mut u64,
208    tx_hash: B256,
209    ctx: &ArbPrecompileCtx,
210) -> PrecompileResult {
211    let gas_limit = input.gas;
212    let caller = input.caller;
213    // Value, read-only and delegate context revert via the wrapper's gas.
214    if !input.value.is_zero() || input.is_static || input.target_address != input.bytecode_address {
215        return Err(ArbPrecompileError::empty_revert(*gas_used).into());
216    }
217    load_accounts(input)?;
218
219    if !is_transaction_filterer(input, gas_used, caller, ctx)? {
220        return Err(ArbPrecompileError::empty_revert(*gas_used).into());
221    }
222
223    {
224        let internals = input.internals_mut();
225        let arb_state = ctx
226            .block
227            .arbos_state(internals)
228            .map_err(ArbPrecompileError::fatal)?;
229        arb_state
230            .filtered_transactions
231            .set(internals, tx_hash, true)
232            .map_err(ArbPrecompileError::fatal)?;
233        crate::charge_storage_write(gas_used, ctx, SSTORE_GAS);
234    }
235
236    input.internals_mut().log(Log::new_unchecked(
237        ARBFILTEREDTXMANAGER_ADDRESS,
238        vec![
239            IArbFilteredTxManager::FilteredTransactionAdded::SIGNATURE_HASH,
240            tx_hash,
241        ],
242        Default::default(),
243    ));
244    crate::charge_history_growth(gas_used, ctx, LOG_GAS);
245
246    Ok(PrecompileOutput::new(
247        (*gas_used).min(gas_limit),
248        vec![].into(),
249    ))
250}
251
252fn handle_delete_filtered_tx(
253    input: &mut PrecompileInput<'_>,
254    gas_used: &mut u64,
255    tx_hash: B256,
256    ctx: &ArbPrecompileCtx,
257) -> PrecompileResult {
258    let gas_limit = input.gas;
259    let caller = input.caller;
260    // Value, read-only and delegate context revert via the wrapper's gas.
261    if !input.value.is_zero() || input.is_static || input.target_address != input.bytecode_address {
262        return Err(ArbPrecompileError::empty_revert(*gas_used).into());
263    }
264    load_accounts(input)?;
265
266    if !is_transaction_filterer(input, gas_used, caller, ctx)? {
267        return Err(ArbPrecompileError::empty_revert(*gas_used).into());
268    }
269
270    {
271        let internals = input.internals_mut();
272        let arb_state = ctx
273            .block
274            .arbos_state(internals)
275            .map_err(ArbPrecompileError::fatal)?;
276        arb_state
277            .filtered_transactions
278            .set(internals, tx_hash, false)
279            .map_err(ArbPrecompileError::fatal)?;
280        crate::charge_storage_write(gas_used, ctx, SSTORE_CLEAR_GAS);
281    }
282
283    input.internals_mut().log(Log::new_unchecked(
284        ARBFILTEREDTXMANAGER_ADDRESS,
285        vec![
286            IArbFilteredTxManager::FilteredTransactionDeleted::SIGNATURE_HASH,
287            tx_hash,
288        ],
289        Default::default(),
290    ));
291    crate::charge_history_growth(gas_used, ctx, LOG_GAS);
292
293    Ok(PrecompileOutput::new(
294        (*gas_used).min(gas_limit),
295        vec![].into(),
296    ))
297}