arb_precompiles/
arbfilteredtxmanager.rs

1use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
2use alloy_primitives::{Address, Log, B256, U256};
3use alloy_sol_types::{SolEvent, SolInterface};
4use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
5
6use crate::{
7    interfaces::IArbFilteredTxManager,
8    storage_slot::{
9        derive_subspace_key, map_slot_b256, ARBOS_STATE_ADDRESS, FILTERED_TX_STATE_ADDRESS,
10        ROOT_STORAGE_KEY, TRANSACTION_FILTERER_SUBSPACE,
11    },
12};
13
14/// ArbFilteredTransactionsManager precompile address (0x74).
15pub const ARBFILTEREDTXMANAGER_ADDRESS: Address = Address::new([
16    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
17    0x00, 0x00, 0x00, 0x74,
18]);
19
20const SLOAD_GAS: u64 = 800;
21const SSTORE_GAS: u64 = 20_000;
22const COPY_GAS: u64 = 3;
23
24/// Sentinel value stored for filtered tx hashes.
25const PRESENT_VALUE: U256 = U256::from_limbs([1, 0, 0, 0]);
26
27pub fn create_arbfilteredtxmanager_precompile() -> DynPrecompile {
28    DynPrecompile::new_stateful(PrecompileId::custom("arbfilteredtxmanager"), handler)
29}
30
31fn handler(mut input: PrecompileInput<'_>) -> PrecompileResult {
32    if let Some(result) = crate::check_precompile_version(
33        arb_chainspec::arbos_version::ARBOS_VERSION_TRANSACTION_FILTERING,
34    ) {
35        return result;
36    }
37
38    let gas_limit = input.gas;
39    crate::init_precompile_gas(input.data.len());
40
41    let call =
42        match IArbFilteredTxManager::ArbFilteredTransactionsManagerCalls::abi_decode(input.data) {
43            Ok(c) => c,
44            Err(_) => return crate::burn_all_revert(gas_limit),
45        };
46
47    use IArbFilteredTxManager::ArbFilteredTransactionsManagerCalls as Calls;
48    let result = match call {
49        Calls::addFilteredTransaction(c) => handle_add_filtered_tx(&mut input, c.txHash),
50        Calls::deleteFilteredTransaction(c) => handle_delete_filtered_tx(&mut input, c.txHash),
51        Calls::isTransactionFiltered(c) => handle_is_tx_filtered(&mut input, c.txHash),
52    };
53    crate::gas_check(gas_limit, result)
54}
55
56// ── helpers ──────────────────────────────────────────────────────────
57
58fn load_accounts(input: &mut PrecompileInput<'_>) -> Result<(), PrecompileError> {
59    input
60        .internals_mut()
61        .load_account(ARBOS_STATE_ADDRESS)
62        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
63    input
64        .internals_mut()
65        .load_account(FILTERED_TX_STATE_ADDRESS)
66        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
67    Ok(())
68}
69
70fn sload_arbos(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
71    let val = input
72        .internals_mut()
73        .sload(ARBOS_STATE_ADDRESS, slot)
74        .map_err(|_| PrecompileError::other("sload failed"))?;
75    Ok(val.data)
76}
77
78fn sload_filtered(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
79    let val = input
80        .internals_mut()
81        .sload(FILTERED_TX_STATE_ADDRESS, slot)
82        .map_err(|_| PrecompileError::other("sload failed"))?;
83    Ok(val.data)
84}
85
86fn sstore_filtered(
87    input: &mut PrecompileInput<'_>,
88    slot: U256,
89    value: U256,
90) -> Result<(), PrecompileError> {
91    input
92        .internals_mut()
93        .sstore(FILTERED_TX_STATE_ADDRESS, slot, value)
94        .map_err(|_| PrecompileError::other("sstore failed"))?;
95    Ok(())
96}
97
98/// Compute the storage slot for a tx hash in the filtered transactions account.
99/// The filtered tx storage uses an empty storageKey, so: map_slot_b256(&[], &tx_hash).
100fn filtered_tx_slot(tx_hash: &B256) -> U256 {
101    map_slot_b256(&[], tx_hash)
102}
103
104/// Check if caller is a transaction filterer via the TransactionFilterers address set.
105fn is_transaction_filterer(
106    input: &mut PrecompileInput<'_>,
107    addr: Address,
108) -> Result<bool, PrecompileError> {
109    // TransactionFilterers is at subspace [11] in ArbOS state.
110    // byAddress sub-storage is at [0] within the address set.
111    let filterer_key = derive_subspace_key(ROOT_STORAGE_KEY, TRANSACTION_FILTERER_SUBSPACE);
112    let by_address_key = derive_subspace_key(filterer_key.as_slice(), &[0]);
113    let addr_hash = B256::left_padding_from(addr.as_slice());
114    let slot = map_slot_b256(by_address_key.as_slice(), &addr_hash);
115    let val = sload_arbos(input, slot)?;
116    Ok(val != U256::ZERO)
117}
118
119fn handle_is_tx_filtered(input: &mut PrecompileInput<'_>, tx_hash: B256) -> PrecompileResult {
120    let gas_limit = input.gas;
121    load_accounts(input)?;
122
123    let slot = filtered_tx_slot(&tx_hash);
124    let value = sload_filtered(input, slot)?;
125    let is_filtered = if value == PRESENT_VALUE {
126        U256::from(1u64)
127    } else {
128        U256::ZERO
129    };
130
131    Ok(PrecompileOutput::new(
132        (SLOAD_GAS + COPY_GAS).min(gas_limit),
133        is_filtered.to_be_bytes::<32>().to_vec().into(),
134    ))
135}
136
137fn handle_add_filtered_tx(input: &mut PrecompileInput<'_>, tx_hash: B256) -> PrecompileResult {
138    let gas_limit = input.gas;
139    let caller = input.caller;
140    load_accounts(input)?;
141
142    if !is_transaction_filterer(input, caller)? {
143        return Err(PrecompileError::other(
144            "caller is not a transaction filterer",
145        ));
146    }
147
148    let slot = filtered_tx_slot(&tx_hash);
149    sstore_filtered(input, slot, PRESENT_VALUE)?;
150
151    input.internals_mut().log(Log::new_unchecked(
152        ARBFILTEREDTXMANAGER_ADDRESS,
153        vec![
154            IArbFilteredTxManager::FilteredTransactionAdded::SIGNATURE_HASH,
155            tx_hash,
156        ],
157        Default::default(),
158    ));
159
160    let gas_used = 2 * SLOAD_GAS + SSTORE_GAS + COPY_GAS;
161    Ok(PrecompileOutput::new(
162        gas_used.min(gas_limit),
163        vec![].into(),
164    ))
165}
166
167fn handle_delete_filtered_tx(input: &mut PrecompileInput<'_>, tx_hash: B256) -> PrecompileResult {
168    let gas_limit = input.gas;
169    let caller = input.caller;
170    load_accounts(input)?;
171
172    if !is_transaction_filterer(input, caller)? {
173        return Err(PrecompileError::other(
174            "caller is not a transaction filterer",
175        ));
176    }
177
178    let slot = filtered_tx_slot(&tx_hash);
179    sstore_filtered(input, slot, U256::ZERO)?;
180
181    input.internals_mut().log(Log::new_unchecked(
182        ARBFILTEREDTXMANAGER_ADDRESS,
183        vec![
184            IArbFilteredTxManager::FilteredTransactionDeleted::SIGNATURE_HASH,
185            tx_hash,
186        ],
187        Default::default(),
188    ));
189
190    let gas_used = 2 * SLOAD_GAS + SSTORE_GAS + COPY_GAS;
191    Ok(PrecompileOutput::new(
192        gas_used.min(gas_limit),
193        vec![].into(),
194    ))
195}