arb_precompiles/
arbownerpublic.rs

1use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
2use alloy_primitives::{Address, U256};
3use alloy_sol_types::SolInterface;
4use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
5
6use crate::{
7    interfaces::IArbOwnerPublic,
8    storage_slot::{
9        derive_subspace_key, map_slot, map_slot_b256, root_slot, subspace_slot,
10        ARBOS_STATE_ADDRESS, CHAIN_OWNER_SUBSPACE, FEATURES_SUBSPACE,
11        FILTERED_FUNDS_RECIPIENT_OFFSET, L1_PRICING_SUBSPACE,
12        NATIVE_TOKEN_ENABLED_FROM_TIME_OFFSET, NATIVE_TOKEN_SUBSPACE, PROGRAMS_SUBSPACE,
13        ROOT_STORAGE_KEY, TRANSACTION_FILTERER_SUBSPACE, TX_FILTERING_ENABLED_FROM_TIME_OFFSET,
14    },
15};
16
17/// ArbOwnerPublic precompile address (0x6b).
18pub const ARBOWNERPUBLIC_ADDRESS: Address = Address::new([
19    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
20    0x00, 0x00, 0x00, 0x6b,
21]);
22
23const INITIAL_MAX_FRAGMENT_COUNT: u8 = 2;
24// ArbOS version where MaxFragmentCount was introduced.
25const ARBOS_VERSION_STYLUS_CONTRACT_LIMIT: u64 = 60;
26// ArbOS version where collectTips storage flag was introduced.
27const ARBOS_VERSION_COLLECT_TIPS: u64 = 60;
28// collectTipsOffset in arbosState (root field offset 11).
29const COLLECT_TIPS_OFFSET: u64 = 11;
30
31// ArbOS state offsets (from arbosState).
32const NETWORK_FEE_ACCOUNT_OFFSET: u64 = 3;
33const INFRA_FEE_ACCOUNT_OFFSET: u64 = 6;
34const BROTLI_COMPRESSION_LEVEL_OFFSET: u64 = 7;
35const UPGRADE_VERSION_OFFSET: u64 = 1;
36const UPGRADE_TIMESTAMP_OFFSET: u64 = 2;
37
38// L1 pricing field for gas floor per token.
39const L1_GAS_FLOOR_PER_TOKEN: u64 = 12;
40
41const SLOAD_GAS: u64 = 800;
42const SSTORE_GAS: u64 = 20_000;
43const COPY_GAS: u64 = 3;
44
45pub fn create_arbownerpublic_precompile() -> DynPrecompile {
46    DynPrecompile::new_stateful(PrecompileId::custom("arbownerpublic"), handler)
47}
48
49fn handler(mut input: PrecompileInput<'_>) -> PrecompileResult {
50    let gas_limit = input.gas;
51    crate::init_precompile_gas(input.data.len());
52
53    let call = match IArbOwnerPublic::ArbOwnerPublicCalls::abi_decode(input.data) {
54        Ok(c) => c,
55        Err(_) => return crate::burn_all_revert(gas_limit),
56    };
57
58    use IArbOwnerPublic::ArbOwnerPublicCalls as Calls;
59    let result = match call {
60        Calls::getNetworkFeeAccount(_) => read_state_field(&mut input, NETWORK_FEE_ACCOUNT_OFFSET),
61        Calls::getInfraFeeAccount(_) => {
62            if crate::get_arbos_version() < 6 {
63                read_state_field(&mut input, NETWORK_FEE_ACCOUNT_OFFSET)
64            } else {
65                read_state_field(&mut input, INFRA_FEE_ACCOUNT_OFFSET)
66            }
67        }
68        Calls::getBrotliCompressionLevel(_) => {
69            read_state_field(&mut input, BROTLI_COMPRESSION_LEVEL_OFFSET)
70        }
71        Calls::getScheduledUpgrade(_) => handle_scheduled_upgrade(&mut input),
72        Calls::isChainOwner(c) => handle_is_chain_owner(&mut input, c.addr),
73        Calls::getAllChainOwners(_) => handle_get_all_members(&mut input),
74        Calls::rectifyChainOwner(c) => handle_rectify_chain_owner(&mut input, c.ownerToRectify),
75        Calls::isNativeTokenOwner(c) => {
76            handle_is_set_member(&mut input, NATIVE_TOKEN_SUBSPACE, c.addr)
77        }
78        Calls::isTransactionFilterer(c) => {
79            handle_is_set_member(&mut input, TRANSACTION_FILTERER_SUBSPACE, c.filterer)
80        }
81        Calls::getAllNativeTokenOwners(_) => {
82            handle_get_all_set_members(&mut input, NATIVE_TOKEN_SUBSPACE)
83        }
84        Calls::getAllTransactionFilterers(_) => {
85            handle_get_all_set_members(&mut input, TRANSACTION_FILTERER_SUBSPACE)
86        }
87        Calls::getNativeTokenManagementFrom(_) => {
88            read_state_field(&mut input, NATIVE_TOKEN_ENABLED_FROM_TIME_OFFSET)
89        }
90        Calls::getTransactionFilteringFrom(_) => {
91            read_state_field(&mut input, TX_FILTERING_ENABLED_FROM_TIME_OFFSET)
92        }
93        Calls::getFilteredFundsRecipient(_) => {
94            read_state_field(&mut input, FILTERED_FUNDS_RECIPIENT_OFFSET)
95        }
96        Calls::isCalldataPriceIncreaseEnabled(_) => {
97            load_arbos(&mut input)?;
98            let features_key = derive_subspace_key(ROOT_STORAGE_KEY, FEATURES_SUBSPACE);
99            let features_slot = map_slot(features_key.as_slice(), 0);
100            let features = sload_field(&mut input, features_slot)?;
101            let enabled = features & U256::from(1);
102            Ok(PrecompileOutput::new(
103                (2 * SLOAD_GAS + COPY_GAS).min(gas_limit),
104                enabled.to_be_bytes::<32>().to_vec().into(),
105            ))
106        }
107        Calls::getParentGasFloorPerToken(_) => {
108            load_arbos(&mut input)?;
109            let field_slot = subspace_slot(L1_PRICING_SUBSPACE, L1_GAS_FLOOR_PER_TOKEN);
110            let value = sload_field(&mut input, field_slot)?;
111            Ok(PrecompileOutput::new(
112                (2 * SLOAD_GAS + COPY_GAS).min(gas_limit),
113                value.to_be_bytes::<32>().to_vec().into(),
114            ))
115        }
116        Calls::getMaxStylusContractFragments(_) => handle_max_stylus_fragments(&mut input),
117        Calls::getCollectTips(_) => handle_get_collect_tips(&mut input),
118    };
119    crate::gas_check(gas_limit, result)
120}
121
122// ── helpers ──────────────────────────────────────────────────────────
123
124fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), PrecompileError> {
125    input
126        .internals_mut()
127        .load_account(ARBOS_STATE_ADDRESS)
128        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
129    Ok(())
130}
131
132fn sload_field(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
133    let val = input
134        .internals_mut()
135        .sload(ARBOS_STATE_ADDRESS, slot)
136        .map_err(|_| PrecompileError::other("sload failed"))?;
137    crate::charge_precompile_gas(SLOAD_GAS);
138    Ok(val.data)
139}
140
141fn sstore_field(
142    input: &mut PrecompileInput<'_>,
143    slot: U256,
144    value: U256,
145) -> Result<(), PrecompileError> {
146    input
147        .internals_mut()
148        .sstore(ARBOS_STATE_ADDRESS, slot, value)
149        .map_err(|_| PrecompileError::other("sstore failed"))?;
150    crate::charge_precompile_gas(SSTORE_GAS);
151    Ok(())
152}
153
154fn read_state_field(input: &mut PrecompileInput<'_>, offset: u64) -> PrecompileResult {
155    let gas_limit = input.gas;
156    load_arbos(input)?;
157
158    let value = sload_field(input, root_slot(offset))?;
159    Ok(PrecompileOutput::new(
160        (2 * SLOAD_GAS + COPY_GAS).min(gas_limit),
161        value.to_be_bytes::<32>().to_vec().into(),
162    ))
163}
164
165fn handle_scheduled_upgrade(input: &mut PrecompileInput<'_>) -> PrecompileResult {
166    let gas_limit = input.gas;
167    load_arbos(input)?;
168
169    let version = sload_field(input, root_slot(UPGRADE_VERSION_OFFSET))?;
170    let timestamp = sload_field(input, root_slot(UPGRADE_TIMESTAMP_OFFSET))?;
171
172    let mut out = Vec::with_capacity(64);
173    out.extend_from_slice(&version.to_be_bytes::<32>());
174    out.extend_from_slice(&timestamp.to_be_bytes::<32>());
175
176    // OAS(1) + version(1) + timestamp(1) = 3 sloads + resultCost = 2 words × 3 = 6.
177    Ok(PrecompileOutput::new(
178        (3 * SLOAD_GAS + 2 * COPY_GAS).min(gas_limit),
179        out.into(),
180    ))
181}
182
183fn handle_rectify_chain_owner(input: &mut PrecompileInput<'_>, addr: Address) -> PrecompileResult {
184    let gas_limit = input.gas;
185    load_arbos(input)?;
186
187    let set_key = derive_subspace_key(ROOT_STORAGE_KEY, CHAIN_OWNER_SUBSPACE);
188    let by_address_key = derive_subspace_key(set_key.as_slice(), &[0]);
189    let addr_hash = alloy_primitives::B256::left_padding_from(addr.as_slice());
190
191    // IsMember check
192    let member_slot = map_slot_b256(by_address_key.as_slice(), &addr_hash);
193    let slot_val = sload_field(input, member_slot)?;
194    if slot_val == U256::ZERO {
195        return Err(PrecompileError::other("not an owner"));
196    }
197
198    // Check if mapping is already correct
199    let slot_idx: u64 = slot_val
200        .try_into()
201        .map_err(|_| PrecompileError::other("invalid slot"))?;
202    let at_slot_key = map_slot(set_key.as_slice(), slot_idx);
203    let at_slot_val = sload_field(input, at_slot_key)?;
204    let size_slot = map_slot(set_key.as_slice(), 0);
205    let size: u64 = sload_field(input, size_slot)?
206        .try_into()
207        .map_err(|_| PrecompileError::other("invalid size"))?;
208
209    // Compare: backingStorage[slot] should store the address as U256
210    let addr_as_u256 = U256::from_be_slice(addr.as_slice());
211    if at_slot_val == addr_as_u256 && slot_idx <= size {
212        return Err(PrecompileError::other("already correctly mapped"));
213    }
214
215    // Clear byAddress mapping, then re-add
216    sstore_field(input, member_slot, U256::ZERO)?;
217
218    // Re-add using same logic as address_set_add in arbowner.rs
219    let new_size = size + 1;
220    let new_pos_slot = map_slot(set_key.as_slice(), new_size);
221    sstore_field(input, new_pos_slot, addr_as_u256)?;
222    sstore_field(input, member_slot, U256::from(new_size))?;
223    sstore_field(input, size_slot, U256::from(new_size))?;
224
225    // Emit ChainOwnerRectified(address) event
226    let topic0 = alloy_primitives::keccak256("ChainOwnerRectified(address)");
227    input
228        .internals_mut()
229        .log(alloy_primitives::Log::new_unchecked(
230            ARBOWNERPUBLIC_ADDRESS,
231            vec![topic0],
232            addr_hash.0.to_vec().into(),
233        ));
234
235    const SSTORE_ZERO_GAS: u64 = 5_000;
236    const RECTIFY_EVENT_GAS: u64 = 1_006; // LOG1 + 32 bytes data
237    let gas_used =
238        SLOAD_GAS + 7 * SLOAD_GAS + SSTORE_ZERO_GAS + 3 * SSTORE_GAS + RECTIFY_EVENT_GAS + COPY_GAS;
239    Ok(PrecompileOutput::new(
240        gas_used.min(gas_limit),
241        Vec::new().into(),
242    ))
243}
244
245fn handle_is_chain_owner(input: &mut PrecompileInput<'_>, addr: Address) -> PrecompileResult {
246    let gas_limit = input.gas;
247    load_arbos(input)?;
248
249    // Chain owners AddressSet: byAddress sub-storage at key [0].
250    let set_key = derive_subspace_key(ROOT_STORAGE_KEY, CHAIN_OWNER_SUBSPACE);
251    let by_address_key = derive_subspace_key(set_key.as_slice(), &[0]);
252
253    let addr_as_b256 = alloy_primitives::B256::left_padding_from(addr.as_slice());
254    let member_slot = map_slot_b256(by_address_key.as_slice(), &addr_as_b256);
255
256    let value = sload_field(input, member_slot)?;
257    let is_owner = value != U256::ZERO;
258
259    let result = if is_owner {
260        U256::from(1u64)
261    } else {
262        U256::ZERO
263    };
264
265    // OAS(1) + IsMember(1) = 2 sloads + argsCost(3) + resultCost(3).
266    Ok(PrecompileOutput::new(
267        (2 * SLOAD_GAS + 2 * COPY_GAS).min(gas_limit),
268        result.to_be_bytes::<32>().to_vec().into(),
269    ))
270}
271
272fn handle_get_all_members(input: &mut PrecompileInput<'_>) -> PrecompileResult {
273    let gas_limit = input.gas;
274    load_arbos(input)?;
275
276    // AddressSet: size at offset 0, members at offsets 1..=size in backing storage.
277    let set_key = derive_subspace_key(ROOT_STORAGE_KEY, CHAIN_OWNER_SUBSPACE);
278    let size_slot = map_slot(set_key.as_slice(), 0);
279    let size = sload_field(input, size_slot)?;
280    let count: u64 = size.try_into().unwrap_or(0);
281
282    // ABI: offset to dynamic array, array length, then elements.
283    let max_members = count.min(256); // Safety cap
284    let mut out = Vec::with_capacity(64 + max_members as usize * 32);
285    out.extend_from_slice(&U256::from(32u64).to_be_bytes::<32>());
286    out.extend_from_slice(&U256::from(count).to_be_bytes::<32>());
287
288    for i in 0..max_members {
289        let member_slot = map_slot(set_key.as_slice(), i + 1);
290        let addr_val = sload_field(input, member_slot)?;
291        out.extend_from_slice(&addr_val.to_be_bytes::<32>());
292    }
293
294    // resultCost = (2 + N) words for dynamic array encoding.
295    Ok(PrecompileOutput::new(
296        ((2 + max_members) * SLOAD_GAS + (2 + max_members) * COPY_GAS).min(gas_limit),
297        out.into(),
298    ))
299}
300
301fn handle_is_set_member(
302    input: &mut PrecompileInput<'_>,
303    subspace: &[u8],
304    addr: Address,
305) -> PrecompileResult {
306    let gas_limit = input.gas;
307    load_arbos(input)?;
308
309    let set_key = derive_subspace_key(ROOT_STORAGE_KEY, subspace);
310    let by_address_key = derive_subspace_key(set_key.as_slice(), &[0]);
311    let addr_hash = alloy_primitives::B256::left_padding_from(addr.as_slice());
312    let member_slot = map_slot_b256(by_address_key.as_slice(), &addr_hash);
313    let value = sload_field(input, member_slot)?;
314    let is_member = if value != U256::ZERO {
315        U256::from(1u64)
316    } else {
317        U256::ZERO
318    };
319
320    // OAS(1) + IsMember(1) = 2 sloads + argsCost(3) + resultCost(3).
321    Ok(PrecompileOutput::new(
322        (2 * SLOAD_GAS + 2 * COPY_GAS).min(gas_limit),
323        is_member.to_be_bytes::<32>().to_vec().into(),
324    ))
325}
326
327fn handle_get_all_set_members(
328    input: &mut PrecompileInput<'_>,
329    subspace: &[u8],
330) -> PrecompileResult {
331    let gas_limit = input.gas;
332    load_arbos(input)?;
333
334    let set_key = derive_subspace_key(ROOT_STORAGE_KEY, subspace);
335    let size_slot = map_slot(set_key.as_slice(), 0);
336    let size = sload_field(input, size_slot)?;
337    let count: u64 = size.try_into().unwrap_or(0);
338    let max_members = count.min(65536);
339
340    let mut out = Vec::with_capacity(64 + max_members as usize * 32);
341    out.extend_from_slice(&U256::from(32u64).to_be_bytes::<32>());
342    out.extend_from_slice(&U256::from(count).to_be_bytes::<32>());
343
344    for i in 0..max_members {
345        let member_slot = map_slot(set_key.as_slice(), i + 1);
346        let addr_val = sload_field(input, member_slot)?;
347        out.extend_from_slice(&addr_val.to_be_bytes::<32>());
348    }
349
350    // resultCost = (2 + N) words for dynamic array encoding.
351    Ok(PrecompileOutput::new(
352        ((2 + max_members) * SLOAD_GAS + (2 + max_members) * COPY_GAS).min(gas_limit),
353        out.into(),
354    ))
355}
356
357fn handle_max_stylus_fragments(input: &mut PrecompileInput<'_>) -> PrecompileResult {
358    let gas_limit = input.gas;
359    if crate::get_arbos_version() < ARBOS_VERSION_STYLUS_CONTRACT_LIMIT {
360        return Ok(PrecompileOutput::new(
361            (SLOAD_GAS + COPY_GAS).min(gas_limit),
362            vec![0u8; 32].into(),
363        ));
364    }
365    load_arbos(input)?;
366    let programs_key = derive_subspace_key(ROOT_STORAGE_KEY, PROGRAMS_SUBSPACE);
367    let params_key = derive_subspace_key(programs_key.as_slice(), &[0]);
368    let params_slot = map_slot(params_key.as_slice(), 0);
369    let val = sload_field(input, params_slot)?;
370    let bytes = val.to_be_bytes::<32>();
371    let mut count = bytes[29];
372    if count == 0 {
373        count = INITIAL_MAX_FRAGMENT_COUNT;
374    }
375    let mut out = [0u8; 32];
376    out[31] = count;
377    Ok(PrecompileOutput::new(
378        (SLOAD_GAS + COPY_GAS).min(gas_limit),
379        out.to_vec().into(),
380    ))
381}
382
383fn handle_get_collect_tips(input: &mut PrecompileInput<'_>) -> PrecompileResult {
384    let gas_limit = input.gas;
385    if crate::get_arbos_version() < ARBOS_VERSION_COLLECT_TIPS {
386        return Ok(PrecompileOutput::new(
387            COPY_GAS.min(gas_limit),
388            vec![0u8; 32].into(),
389        ));
390    }
391    load_arbos(input)?;
392    let value = sload_field(input, root_slot(COLLECT_TIPS_OFFSET))?;
393    let mut out = [0u8; 32];
394    if !value.is_zero() {
395        out[31] = 1;
396    }
397    Ok(PrecompileOutput::new(
398        (SLOAD_GAS + COPY_GAS).min(gas_limit),
399        out.to_vec().into(),
400    ))
401}