arb_precompiles/
arbownerpublic.rs

1use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
2use alloy_primitives::{Address, U256};
3use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
4
5use crate::storage_slot::{
6    derive_subspace_key, map_slot, map_slot_b256, root_slot, subspace_slot, ARBOS_STATE_ADDRESS,
7    CHAIN_OWNER_SUBSPACE, FEATURES_SUBSPACE, FILTERED_FUNDS_RECIPIENT_OFFSET, L1_PRICING_SUBSPACE,
8    NATIVE_TOKEN_ENABLED_FROM_TIME_OFFSET, NATIVE_TOKEN_SUBSPACE, ROOT_STORAGE_KEY,
9    TRANSACTION_FILTERER_SUBSPACE, TX_FILTERING_ENABLED_FROM_TIME_OFFSET,
10};
11
12/// ArbOwnerPublic precompile address (0x6b).
13pub const ARBOWNERPUBLIC_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, 0x6b,
16]);
17
18// Function selectors.
19const GET_NETWORK_FEE_ACCOUNT: [u8; 4] = [0x3e, 0x7a, 0x47, 0xb1];
20const GET_INFRA_FEE_ACCOUNT: [u8; 4] = [0x74, 0x33, 0x16, 0x04];
21const GET_BROTLI_COMPRESSION_LEVEL: [u8; 4] = [0xb1, 0x9e, 0x6b, 0xef];
22const GET_SCHEDULED_UPGRADE: [u8; 4] = [0xed, 0x23, 0xfa, 0x57];
23const IS_CHAIN_OWNER: [u8; 4] = [0x26, 0xef, 0x69, 0x9d];
24const GET_ALL_CHAIN_OWNERS: [u8; 4] = [0x51, 0x6b, 0xaf, 0x03];
25const RECTIFY_CHAIN_OWNER: [u8; 4] = [0x18, 0x3b, 0xe5, 0xf2];
26const IS_NATIVE_TOKEN_OWNER: [u8; 4] = [0x40, 0xb6, 0x62, 0x08];
27const GET_ALL_NATIVE_TOKEN_OWNERS: [u8; 4] = [0xf5, 0xc8, 0x16, 0x7a];
28const GET_NATIVE_TOKEN_MANAGEMENT_FROM: [u8; 4] = [0xaa, 0x57, 0x87, 0x88];
29const GET_TRANSACTION_FILTERING_FROM: [u8; 4] = [0x7a, 0x86, 0xfe, 0x96];
30const IS_TRANSACTION_FILTERER: [u8; 4] = [0xa5, 0x3f, 0xef, 0x64];
31const GET_ALL_TRANSACTION_FILTERERS: [u8; 4] = [0x3d, 0xbb, 0x43, 0x98];
32const GET_FILTERED_FUNDS_RECIPIENT: [u8; 4] = [0x8b, 0x00, 0x16, 0x72];
33const IS_CALLDATA_PRICE_INCREASE_ENABLED: [u8; 4] = [0x7f, 0xe5, 0x5a, 0x2f];
34const GET_PARENT_GAS_FLOOR_PER_TOKEN: [u8; 4] = [0xee, 0x36, 0x03, 0x8e];
35const GET_MAX_STYLUS_CONTRACT_FRAGMENTS: [u8; 4] = [0xea, 0x25, 0x8c, 0x64];
36
37// ArbOS state offsets (from arbosState).
38const NETWORK_FEE_ACCOUNT_OFFSET: u64 = 3;
39const INFRA_FEE_ACCOUNT_OFFSET: u64 = 6;
40const BROTLI_COMPRESSION_LEVEL_OFFSET: u64 = 7;
41const UPGRADE_VERSION_OFFSET: u64 = 1;
42const UPGRADE_TIMESTAMP_OFFSET: u64 = 2;
43
44// L1 pricing field for gas floor per token.
45const L1_GAS_FLOOR_PER_TOKEN: u64 = 12;
46
47const SLOAD_GAS: u64 = 800;
48const COPY_GAS: u64 = 3;
49
50pub fn create_arbownerpublic_precompile() -> DynPrecompile {
51    DynPrecompile::new_stateful(PrecompileId::custom("arbownerpublic"), handler)
52}
53
54fn handler(mut input: PrecompileInput<'_>) -> PrecompileResult {
55    let gas_limit = input.gas;
56    let data = input.data;
57    if data.len() < 4 {
58        return Err(PrecompileError::other("input too short"));
59    }
60
61    let selector: [u8; 4] = [data[0], data[1], data[2], data[3]];
62
63    let result = match selector {
64        GET_NETWORK_FEE_ACCOUNT => read_state_field(&mut input, NETWORK_FEE_ACCOUNT_OFFSET),
65        // GetInfraFeeAccount: ArbOS >= 5
66        GET_INFRA_FEE_ACCOUNT => {
67            if let Some(r) = crate::check_method_version(5, 0) {
68                return r;
69            }
70            read_state_field(&mut input, INFRA_FEE_ACCOUNT_OFFSET)
71        }
72        // GetBrotliCompressionLevel: ArbOS >= 20
73        GET_BROTLI_COMPRESSION_LEVEL => {
74            if let Some(r) = crate::check_method_version(20, 0) {
75                return r;
76            }
77            read_state_field(&mut input, BROTLI_COMPRESSION_LEVEL_OFFSET)
78        }
79        // GetScheduledUpgrade: ArbOS >= 20
80        GET_SCHEDULED_UPGRADE => {
81            if let Some(r) = crate::check_method_version(20, 0) {
82                return r;
83            }
84            handle_scheduled_upgrade(&mut input)
85        }
86        IS_CHAIN_OWNER => handle_is_chain_owner(&mut input),
87        GET_ALL_CHAIN_OWNERS => handle_get_all_members(&mut input),
88        // RectifyChainOwner: ArbOS >= 11
89        RECTIFY_CHAIN_OWNER => {
90            if let Some(r) = crate::check_method_version(11, 0) {
91                return r;
92            }
93            let gas_cost = (SLOAD_GAS + COPY_GAS).min(input.gas);
94            Ok(PrecompileOutput::new(gas_cost, Vec::new().into()))
95        }
96        // IsNativeTokenOwner: ArbOS >= 41
97        IS_NATIVE_TOKEN_OWNER => {
98            if let Some(r) = crate::check_method_version(41, 0) {
99                return r;
100            }
101            handle_is_set_member(&mut input, NATIVE_TOKEN_SUBSPACE)
102        }
103        // IsTransactionFilterer: ArbOS >= 60 (TransactionFiltering)
104        IS_TRANSACTION_FILTERER => {
105            if let Some(r) = crate::check_method_version(60, 0) {
106                return r;
107            }
108            handle_is_set_member(&mut input, TRANSACTION_FILTERER_SUBSPACE)
109        }
110        // GetAllNativeTokenOwners: ArbOS >= 41
111        GET_ALL_NATIVE_TOKEN_OWNERS => {
112            if let Some(r) = crate::check_method_version(41, 0) {
113                return r;
114            }
115            handle_get_all_set_members(&mut input, NATIVE_TOKEN_SUBSPACE)
116        }
117        // GetAllTransactionFilterers: ArbOS >= 60 (TransactionFiltering)
118        GET_ALL_TRANSACTION_FILTERERS => {
119            if let Some(r) = crate::check_method_version(60, 0) {
120                return r;
121            }
122            handle_get_all_set_members(&mut input, TRANSACTION_FILTERER_SUBSPACE)
123        }
124        // GetNativeTokenManagementFrom: ArbOS >= 50
125        GET_NATIVE_TOKEN_MANAGEMENT_FROM => {
126            if let Some(r) = crate::check_method_version(50, 0) {
127                return r;
128            }
129            read_state_field(&mut input, NATIVE_TOKEN_ENABLED_FROM_TIME_OFFSET)
130        }
131        // GetTransactionFilteringFrom: ArbOS >= 60 (TransactionFiltering)
132        GET_TRANSACTION_FILTERING_FROM => {
133            if let Some(r) = crate::check_method_version(60, 0) {
134                return r;
135            }
136            read_state_field(&mut input, TX_FILTERING_ENABLED_FROM_TIME_OFFSET)
137        }
138        // GetFilteredFundsRecipient: ArbOS >= 60 (TransactionFiltering)
139        GET_FILTERED_FUNDS_RECIPIENT => {
140            if let Some(r) = crate::check_method_version(60, 0) {
141                return r;
142            }
143            read_state_field(&mut input, FILTERED_FUNDS_RECIPIENT_OFFSET)
144        }
145        // IsCalldataPriceIncreaseEnabled: ArbOS >= 40
146        IS_CALLDATA_PRICE_INCREASE_ENABLED => {
147            if let Some(r) = crate::check_method_version(40, 0) {
148                return r;
149            }
150            let gas_limit = input.gas;
151            load_arbos(&mut input)?;
152            let features_key = derive_subspace_key(ROOT_STORAGE_KEY, FEATURES_SUBSPACE);
153            let features_slot = map_slot(features_key.as_slice(), 0);
154            let features = sload_field(&mut input, features_slot)?;
155            let enabled = features & U256::from(1);
156            let gas_cost = (2 * SLOAD_GAS + COPY_GAS).min(gas_limit);
157            Ok(PrecompileOutput::new(
158                gas_cost,
159                enabled.to_be_bytes::<32>().to_vec().into(),
160            ))
161        }
162        // GetParentGasFloorPerToken: ArbOS >= 50
163        GET_PARENT_GAS_FLOOR_PER_TOKEN => {
164            if let Some(r) = crate::check_method_version(50, 0) {
165                return r;
166            }
167            let gas_limit = input.gas;
168            load_arbos(&mut input)?;
169            let field_slot = subspace_slot(L1_PRICING_SUBSPACE, L1_GAS_FLOOR_PER_TOKEN);
170            let value = sload_field(&mut input, field_slot)?;
171            Ok(PrecompileOutput::new(
172                (2 * SLOAD_GAS + COPY_GAS).min(gas_limit),
173                value.to_be_bytes::<32>().to_vec().into(),
174            ))
175        }
176        // GetMaxStylusContractFragments: ArbOS >= 60 (StylusContractLimit)
177        GET_MAX_STYLUS_CONTRACT_FRAGMENTS => {
178            if let Some(r) = crate::check_method_version(60, 0) {
179                return r;
180            }
181            // OAS(800) + Params() burn(100) + resultCost(3).
182            let gas_cost = (SLOAD_GAS + 100 + COPY_GAS).min(input.gas);
183            Ok(PrecompileOutput::new(gas_cost, vec![0u8; 32].into()))
184        }
185        _ => Err(PrecompileError::other("unknown ArbOwnerPublic selector")),
186    };
187    crate::gas_check(gas_limit, result)
188}
189
190// ── helpers ──────────────────────────────────────────────────────────
191
192fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), PrecompileError> {
193    input
194        .internals_mut()
195        .load_account(ARBOS_STATE_ADDRESS)
196        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
197    Ok(())
198}
199
200fn sload_field(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
201    let val = input
202        .internals_mut()
203        .sload(ARBOS_STATE_ADDRESS, slot)
204        .map_err(|_| PrecompileError::other("sload failed"))?;
205    Ok(val.data)
206}
207
208fn read_state_field(input: &mut PrecompileInput<'_>, offset: u64) -> PrecompileResult {
209    let gas_limit = input.gas;
210    load_arbos(input)?;
211
212    let value = sload_field(input, root_slot(offset))?;
213    Ok(PrecompileOutput::new(
214        (2 * SLOAD_GAS + COPY_GAS).min(gas_limit),
215        value.to_be_bytes::<32>().to_vec().into(),
216    ))
217}
218
219fn handle_scheduled_upgrade(input: &mut PrecompileInput<'_>) -> PrecompileResult {
220    let gas_limit = input.gas;
221    load_arbos(input)?;
222
223    let version = sload_field(input, root_slot(UPGRADE_VERSION_OFFSET))?;
224    let timestamp = sload_field(input, root_slot(UPGRADE_TIMESTAMP_OFFSET))?;
225
226    let mut out = Vec::with_capacity(64);
227    out.extend_from_slice(&version.to_be_bytes::<32>());
228    out.extend_from_slice(&timestamp.to_be_bytes::<32>());
229
230    // OAS(1) + version(1) + timestamp(1) = 3 sloads + resultCost = 2 words × 3 = 6.
231    Ok(PrecompileOutput::new(
232        (3 * SLOAD_GAS + 2 * COPY_GAS).min(gas_limit),
233        out.into(),
234    ))
235}
236
237fn handle_is_chain_owner(input: &mut PrecompileInput<'_>) -> PrecompileResult {
238    let data = input.data;
239    if data.len() < 36 {
240        return Err(PrecompileError::other("input too short"));
241    }
242
243    let gas_limit = input.gas;
244    let addr = Address::from_slice(&data[16..36]);
245    load_arbos(input)?;
246
247    // Chain owners AddressSet: byAddress sub-storage at key [0].
248    let set_key = derive_subspace_key(ROOT_STORAGE_KEY, CHAIN_OWNER_SUBSPACE);
249    let by_address_key = derive_subspace_key(set_key.as_slice(), &[0]);
250
251    let addr_as_b256 = alloy_primitives::B256::left_padding_from(addr.as_slice());
252    let member_slot = map_slot_b256(by_address_key.as_slice(), &addr_as_b256);
253
254    let value = sload_field(input, member_slot)?;
255    let is_owner = value != U256::ZERO;
256
257    let result = if is_owner {
258        U256::from(1u64)
259    } else {
260        U256::ZERO
261    };
262
263    // OAS(1) + IsMember(1) = 2 sloads + argsCost(3) + resultCost(3).
264    Ok(PrecompileOutput::new(
265        (2 * SLOAD_GAS + 2 * COPY_GAS).min(gas_limit),
266        result.to_be_bytes::<32>().to_vec().into(),
267    ))
268}
269
270fn handle_get_all_members(input: &mut PrecompileInput<'_>) -> PrecompileResult {
271    let gas_limit = input.gas;
272    load_arbos(input)?;
273
274    // AddressSet: size at offset 0, members at offsets 1..=size in backing storage.
275    let set_key = derive_subspace_key(ROOT_STORAGE_KEY, CHAIN_OWNER_SUBSPACE);
276    let size_slot = map_slot(set_key.as_slice(), 0);
277    let size = sload_field(input, size_slot)?;
278    let count: u64 = size.try_into().unwrap_or(0);
279
280    // ABI: offset to dynamic array, array length, then elements.
281    let max_members = count.min(256); // Safety cap
282    let mut out = Vec::with_capacity(64 + max_members as usize * 32);
283    out.extend_from_slice(&U256::from(32u64).to_be_bytes::<32>());
284    out.extend_from_slice(&U256::from(count).to_be_bytes::<32>());
285
286    for i in 0..max_members {
287        let member_slot = map_slot(set_key.as_slice(), i + 1);
288        let addr_val = sload_field(input, member_slot)?;
289        out.extend_from_slice(&addr_val.to_be_bytes::<32>());
290    }
291
292    // resultCost = (2 + N) words for dynamic array encoding.
293    Ok(PrecompileOutput::new(
294        ((2 + max_members) * SLOAD_GAS + (2 + max_members) * COPY_GAS).min(gas_limit),
295        out.into(),
296    ))
297}
298
299fn handle_is_set_member(input: &mut PrecompileInput<'_>, subspace: &[u8]) -> PrecompileResult {
300    let data = input.data;
301    if data.len() < 36 {
302        return Err(PrecompileError::other("input too short"));
303    }
304    let gas_limit = input.gas;
305    let addr = Address::from_slice(&data[16..36]);
306    load_arbos(input)?;
307
308    let set_key = derive_subspace_key(ROOT_STORAGE_KEY, subspace);
309    let by_address_key = derive_subspace_key(set_key.as_slice(), &[0]);
310    let addr_hash = alloy_primitives::B256::left_padding_from(addr.as_slice());
311    let member_slot = map_slot_b256(by_address_key.as_slice(), &addr_hash);
312    let value = sload_field(input, member_slot)?;
313    let is_member = if value != U256::ZERO {
314        U256::from(1u64)
315    } else {
316        U256::ZERO
317    };
318
319    // OAS(1) + IsMember(1) = 2 sloads + argsCost(3) + resultCost(3).
320    Ok(PrecompileOutput::new(
321        (2 * SLOAD_GAS + 2 * COPY_GAS).min(gas_limit),
322        is_member.to_be_bytes::<32>().to_vec().into(),
323    ))
324}
325
326fn handle_get_all_set_members(
327    input: &mut PrecompileInput<'_>,
328    subspace: &[u8],
329) -> PrecompileResult {
330    let gas_limit = input.gas;
331    load_arbos(input)?;
332
333    let set_key = derive_subspace_key(ROOT_STORAGE_KEY, subspace);
334    let size_slot = map_slot(set_key.as_slice(), 0);
335    let size = sload_field(input, size_slot)?;
336    let count: u64 = size.try_into().unwrap_or(0);
337    let max_members = count.min(65536);
338
339    let mut out = Vec::with_capacity(64 + max_members as usize * 32);
340    out.extend_from_slice(&U256::from(32u64).to_be_bytes::<32>());
341    out.extend_from_slice(&U256::from(count).to_be_bytes::<32>());
342
343    for i in 0..max_members {
344        let member_slot = map_slot(set_key.as_slice(), i + 1);
345        let addr_val = sload_field(input, member_slot)?;
346        out.extend_from_slice(&addr_val.to_be_bytes::<32>());
347    }
348
349    // resultCost = (2 + N) words for dynamic array encoding.
350    Ok(PrecompileOutput::new(
351        ((2 + max_members) * SLOAD_GAS + (2 + max_members) * COPY_GAS).min(gas_limit),
352        out.into(),
353    ))
354}