arb_precompiles/
arbaggregator.rs

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