arb_precompiles/
arbaggregator.rs

1use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
2use alloy_primitives::{Address, B256, U256};
3use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
4
5use crate::storage_slot::{
6    derive_subspace_key, map_slot, map_slot_b256, ARBOS_STATE_ADDRESS, CHAIN_OWNER_SUBSPACE,
7    L1_PRICING_SUBSPACE, ROOT_STORAGE_KEY,
8};
9
10/// ArbAggregator precompile address (0x6d).
11pub const ARBAGGREGATOR_ADDRESS: Address = Address::new([
12    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
13    0x00, 0x00, 0x00, 0x6d,
14]);
15
16/// Default batch poster address (the sequencer).
17const BATCH_POSTER_ADDRESS: Address = Address::new([
18    0xa4, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, 0x65, 0x71, 0x75, 0x65,
19    0x6e, 0x63, 0x65, 0x72,
20]);
21
22// Function selectors.
23const GET_PREFERRED_AGGREGATOR: [u8; 4] = [0x52, 0xf1, 0x07, 0x40];
24const GET_DEFAULT_AGGREGATOR: [u8; 4] = [0x87, 0x58, 0x83, 0xf2];
25const GET_BATCH_POSTERS: [u8; 4] = [0xe1, 0x05, 0x73, 0xa3];
26const ADD_BATCH_POSTER: [u8; 4] = [0xdf, 0x41, 0xe1, 0xe2];
27const GET_FEE_COLLECTOR: [u8; 4] = [0x9c, 0x2c, 0x5b, 0xb5];
28const SET_FEE_COLLECTOR: [u8; 4] = [0x29, 0x14, 0x97, 0x99];
29const GET_TX_BASE_FEE: [u8; 4] = [0x04, 0x97, 0x64, 0xaf];
30const SET_TX_BASE_FEE: [u8; 4] = [0x5b, 0xe6, 0x88, 0x8b];
31
32const SLOAD_GAS: u64 = 800;
33const SSTORE_GAS: u64 = 20_000;
34const SSTORE_ZERO_GAS: u64 = 5_000;
35const COPY_GAS: u64 = 3;
36
37// Batch poster table storage layout constants.
38const BATCH_POSTER_TABLE_KEY: &[u8] = &[0];
39const POSTER_ADDRS_KEY: &[u8] = &[0];
40const POSTER_INFO_KEY: &[u8] = &[1];
41const PAY_TO_OFFSET: u64 = 1;
42
43pub fn create_arbaggregator_precompile() -> DynPrecompile {
44    DynPrecompile::new_stateful(PrecompileId::custom("arbaggregator"), handler)
45}
46
47fn handler(mut input: PrecompileInput<'_>) -> PrecompileResult {
48    let gas_limit = input.gas;
49    let data = input.data;
50    if data.len() < 4 {
51        return Err(PrecompileError::other("input too short"));
52    }
53
54    let selector: [u8; 4] = [data[0], data[1], data[2], data[3]];
55
56    let result = match selector {
57        GET_PREFERRED_AGGREGATOR => {
58            // Deprecated view method: always returns (BatchPosterAddress, true).
59            // OpenArbosState (800) + argsCost (3) + resultCost (6) = 809.
60            let mut out = Vec::with_capacity(96);
61            out.extend_from_slice(&U256::from(0x40u64).to_be_bytes::<32>());
62            out.extend_from_slice(&U256::from(1u64).to_be_bytes::<32>());
63            let mut addr_word = [0u8; 32];
64            addr_word[12..32].copy_from_slice(BATCH_POSTER_ADDRESS.as_slice());
65            out.extend_from_slice(&addr_word);
66            Ok(PrecompileOutput::new(
67                (SLOAD_GAS + 9).min(gas_limit),
68                out.into(),
69            ))
70        }
71        GET_DEFAULT_AGGREGATOR => {
72            // Deprecated view method: always returns BatchPosterAddress.
73            // OpenArbosState (800) + resultCost (3) = 803.
74            let mut out = [0u8; 32];
75            out[12..32].copy_from_slice(BATCH_POSTER_ADDRESS.as_slice());
76            Ok(PrecompileOutput::new(
77                (SLOAD_GAS + COPY_GAS).min(gas_limit),
78                out.to_vec().into(),
79            ))
80        }
81        GET_TX_BASE_FEE => {
82            // Deprecated view method: always returns 0.
83            // OpenArbosState (800) + argsCost (3) + resultCost (3) = 806.
84            Ok(PrecompileOutput::new(
85                (SLOAD_GAS + 6).min(gas_limit),
86                U256::ZERO.to_be_bytes::<32>().to_vec().into(),
87            ))
88        }
89        SET_TX_BASE_FEE => {
90            // Deprecated write method: no-op.
91            // OpenArbosState (800) + argsCost (6) = 806.
92            Ok(PrecompileOutput::new(
93                (SLOAD_GAS + 6).min(gas_limit),
94                vec![].into(),
95            ))
96        }
97        GET_FEE_COLLECTOR => handle_get_fee_collector(&mut input),
98        SET_FEE_COLLECTOR => handle_set_fee_collector(&mut input),
99        GET_BATCH_POSTERS => handle_get_batch_posters(&mut input),
100        ADD_BATCH_POSTER => handle_add_batch_poster(&mut input),
101        _ => Err(PrecompileError::other("unknown ArbAggregator selector")),
102    };
103    crate::gas_check(gas_limit, result)
104}
105
106// ── helpers ──────────────────────────────────────────────────────────
107
108fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), PrecompileError> {
109    input
110        .internals_mut()
111        .load_account(ARBOS_STATE_ADDRESS)
112        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
113    Ok(())
114}
115
116fn sload_field(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
117    let val = input
118        .internals_mut()
119        .sload(ARBOS_STATE_ADDRESS, slot)
120        .map_err(|_| PrecompileError::other("sload failed"))?;
121    Ok(val.data)
122}
123
124fn sstore_field(
125    input: &mut PrecompileInput<'_>,
126    slot: U256,
127    value: U256,
128) -> Result<(), PrecompileError> {
129    input
130        .internals_mut()
131        .sstore(ARBOS_STATE_ADDRESS, slot, value)
132        .map_err(|_| PrecompileError::other("sstore failed"))?;
133    Ok(())
134}
135
136/// Derive the batch poster table sub-storage key.
137fn batch_poster_table_key() -> B256 {
138    let l1_pricing_key = derive_subspace_key(ROOT_STORAGE_KEY, L1_PRICING_SUBSPACE);
139    derive_subspace_key(l1_pricing_key.as_slice(), BATCH_POSTER_TABLE_KEY)
140}
141
142/// Derive the posterAddrs (AddressSet) sub-storage key.
143fn poster_addrs_key() -> B256 {
144    let bpt_key = batch_poster_table_key();
145    derive_subspace_key(bpt_key.as_slice(), POSTER_ADDRS_KEY)
146}
147
148/// Derive the poster info sub-storage key for a specific batch poster.
149fn poster_info_key(poster: Address) -> B256 {
150    let bpt_key = batch_poster_table_key();
151    let poster_info = derive_subspace_key(bpt_key.as_slice(), POSTER_INFO_KEY);
152    derive_subspace_key(poster_info.as_slice(), poster.as_slice())
153}
154
155/// Check if caller is a chain owner via the address set membership check.
156fn is_chain_owner(input: &mut PrecompileInput<'_>, addr: Address) -> Result<bool, PrecompileError> {
157    let owner_key = derive_subspace_key(ROOT_STORAGE_KEY, CHAIN_OWNER_SUBSPACE);
158    let by_address_key = derive_subspace_key(owner_key.as_slice(), &[0]);
159    let addr_b256 = B256::left_padding_from(addr.as_slice());
160    let slot = map_slot_b256(by_address_key.as_slice(), &addr_b256);
161    let val = sload_field(input, slot)?;
162    Ok(val != U256::ZERO)
163}
164
165/// GetFeeCollector reads the payTo address for a batch poster.
166fn handle_get_fee_collector(input: &mut PrecompileInput<'_>) -> PrecompileResult {
167    let data = input.data;
168    if data.len() < 36 {
169        return Err(PrecompileError::other("input too short"));
170    }
171
172    let gas_limit = input.gas;
173    let poster = Address::from_slice(&data[16..36]);
174    load_arbos(input)?;
175
176    let info_key = poster_info_key(poster);
177    let pay_to_slot = map_slot(info_key.as_slice(), PAY_TO_OFFSET);
178    let pay_to = sload_field(input, pay_to_slot)?;
179
180    // OAS(1) + OpenPoster IsMember(1) + payTo.Get(1) + argsCost(3) + resultCost(3).
181    Ok(PrecompileOutput::new(
182        (3 * SLOAD_GAS + 2 * COPY_GAS).min(gas_limit),
183        pay_to.to_be_bytes::<32>().to_vec().into(),
184    ))
185}
186
187/// SetFeeCollector sets the payTo address for a batch poster.
188/// Caller must be the batch poster, its current fee collector, or a chain owner.
189fn handle_set_fee_collector(input: &mut PrecompileInput<'_>) -> PrecompileResult {
190    let data = input.data;
191    if data.len() < 68 {
192        return Err(PrecompileError::other("input too short"));
193    }
194
195    let gas_limit = input.gas;
196    let poster = Address::from_slice(&data[16..36]);
197    let new_collector = Address::from_slice(&data[48..68]);
198    let caller = input.caller;
199
200    load_arbos(input)?;
201
202    // Read the current fee collector.
203    let info_key = poster_info_key(poster);
204    let pay_to_slot = map_slot(info_key.as_slice(), PAY_TO_OFFSET);
205    let old_collector_u256 = sload_field(input, pay_to_slot)?;
206    let old_collector_bytes = old_collector_u256.to_be_bytes::<32>();
207    let old_collector = Address::from_slice(&old_collector_bytes[12..32]);
208
209    // Verify authorization: caller must be poster, old fee collector, or chain owner.
210    if caller != poster && caller != old_collector {
211        let is_owner = is_chain_owner(input, caller)?;
212        if !is_owner {
213            return Err(PrecompileError::other(
214                "only a batch poster, its fee collector, or chain owner may change the fee collector",
215            ));
216        }
217    }
218
219    // Write the new fee collector.
220    let new_val = U256::from_be_slice(new_collector.as_slice());
221    sstore_field(input, pay_to_slot, new_val)?;
222
223    // OAS(1) + OpenPoster IsMember(1) + PayTo.Get(1) + SetPayTo(1 SSTORE) + argsCost(6).
224    // Owner check adds IsMember(1 SLOAD) only when caller is neither poster nor collector.
225    let mut gas_used = 3 * SLOAD_GAS + SSTORE_GAS + 2 * COPY_GAS;
226    if caller != poster && caller != old_collector {
227        gas_used += SLOAD_GAS;
228    }
229    Ok(PrecompileOutput::new(
230        gas_used.min(gas_limit),
231        vec![].into(),
232    ))
233}
234
235/// GetBatchPosters returns all batch poster addresses from the AddressSet.
236fn handle_get_batch_posters(input: &mut PrecompileInput<'_>) -> PrecompileResult {
237    let gas_limit = input.gas;
238    load_arbos(input)?;
239
240    let addrs_key = poster_addrs_key();
241    // AddressSet size is at offset 0.
242    let size_slot = map_slot(addrs_key.as_slice(), 0);
243    let size = sload_field(input, size_slot)?;
244    let count: u64 = size
245        .try_into()
246        .map_err(|_| PrecompileError::other("invalid address set size"))?;
247
248    const MAX_MEMBERS: u64 = 1024;
249    let count = count.min(MAX_MEMBERS);
250
251    // Read each member address from positions 1..=count.
252    let mut addresses = Vec::with_capacity(count as usize);
253    for i in 1..=count {
254        let member_slot = map_slot(addrs_key.as_slice(), i);
255        let val = sload_field(input, member_slot)?;
256        addresses.push(val);
257    }
258
259    // ABI-encode as dynamic address array: offset, length, then elements.
260    let mut out = Vec::with_capacity(64 + 32 * addresses.len());
261    out.extend_from_slice(&U256::from(32u64).to_be_bytes::<32>());
262    out.extend_from_slice(&U256::from(count).to_be_bytes::<32>());
263    for addr_val in &addresses {
264        out.extend_from_slice(&addr_val.to_be_bytes::<32>());
265    }
266
267    // resultCost = (2 + N) words for dynamic array encoding.
268    let gas_used = (2 + count) * SLOAD_GAS + (2 + count) * COPY_GAS;
269    Ok(PrecompileOutput::new(gas_used.min(gas_limit), out.into()))
270}
271
272/// AddBatchPoster adds a new batch poster. Caller must be a chain owner.
273fn handle_add_batch_poster(input: &mut PrecompileInput<'_>) -> PrecompileResult {
274    let data = input.data;
275    if data.len() < 36 {
276        return Err(PrecompileError::other("input too short"));
277    }
278
279    let gas_limit = input.gas;
280    let new_poster = Address::from_slice(&data[16..36]);
281    let caller = input.caller;
282    load_arbos(input)?;
283
284    // Verify caller is a chain owner.
285    if !is_chain_owner(input, caller)? {
286        return Err(PrecompileError::other("must be called by chain owner"));
287    }
288
289    let addrs_key = poster_addrs_key();
290
291    // Check if already a batch poster via byAddress sub-storage.
292    let by_address_key = derive_subspace_key(addrs_key.as_slice(), &[0]);
293    let addr_hash = B256::left_padding_from(new_poster.as_slice());
294    let member_slot = map_slot_b256(by_address_key.as_slice(), &addr_hash);
295    let existing = sload_field(input, member_slot)?;
296
297    if existing != U256::ZERO {
298        // Already a batch poster — no-op.
299        return Ok(PrecompileOutput::new(
300            (2 * SLOAD_GAS + COPY_GAS).min(gas_limit),
301            vec![].into(),
302        ));
303    }
304
305    // Read current size and increment.
306    let size_slot = map_slot(addrs_key.as_slice(), 0);
307    let size = sload_field(input, size_slot)?;
308    let size_u64: u64 = size
309        .try_into()
310        .map_err(|_| PrecompileError::other("invalid address set size"))?;
311    let new_size = size_u64 + 1;
312
313    // Store the new poster at position (1 + size) in the backing storage.
314    let new_pos_slot = map_slot(addrs_key.as_slice(), new_size);
315    let addr_as_u256 = U256::from_be_slice(new_poster.as_slice());
316    sstore_field(input, new_pos_slot, addr_as_u256)?;
317
318    // Store in byAddress mapping: byAddress[addr_hash] = 1-based position.
319    let slot_value = U256::from(new_size);
320    sstore_field(input, member_slot, slot_value)?;
321
322    // Increment size.
323    sstore_field(input, size_slot, U256::from(new_size))?;
324
325    // Initialize poster info: set payTo = newPoster (the poster pays itself initially).
326    let info_key = poster_info_key(new_poster);
327    let pay_to_slot = map_slot(info_key.as_slice(), PAY_TO_OFFSET);
328    sstore_field(input, pay_to_slot, addr_as_u256)?;
329
330    // IsMember(caller)(1) + ContainsPoster IsMember(1) + AddPoster[IsMember(1) +
331    // fundsDue.SetChecked(0)(5000) + payTo.Set(20000) + Add(IsMember(1) + size.Get(1) +
332    // byAddress.Set(20000) + backingStorage.Set(20000) + size.Increment Get(1)+Set(20000))]
333    // + argsCost(3).
334    let gas_used = 6 * SLOAD_GAS + SSTORE_ZERO_GAS + 4 * SSTORE_GAS + COPY_GAS;
335    Ok(PrecompileOutput::new(
336        gas_used.min(gas_limit),
337        vec![].into(),
338    ))
339}