arb_precompiles/
arbaggregator.rs

1use std::sync::Arc;
2
3use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
4use alloy_primitives::{Address, U256};
5use alloy_sol_types::SolInterface;
6use arb_context::ArbPrecompileCtx;
7use arb_storage::{
8    ARBOS_STATE_ADDRESS, STORAGE_READ_GAS, STORAGE_WRITE_GAS, STORAGE_WRITE_ZERO_GAS, write_cost,
9};
10use revm::precompile::{PrecompileId, PrecompileOutput, PrecompileResult};
11
12use crate::{ArbPrecompileError, interfaces::IArbAggregator};
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 = STORAGE_READ_GAS;
27const SSTORE_GAS: u64 = STORAGE_WRITE_GAS;
28const SSTORE_ZERO_GAS: u64 = STORAGE_WRITE_ZERO_GAS;
29const COPY_GAS: u64 = 3;
30
31pub fn create_arbaggregator_precompile(ctx: Arc<ArbPrecompileCtx>) -> DynPrecompile {
32    DynPrecompile::new_stateful(PrecompileId::custom("arbaggregator"), move |input| {
33        handler(input, &ctx)
34    })
35}
36
37fn handler(mut input: PrecompileInput<'_>, ctx: &ArbPrecompileCtx) -> PrecompileResult {
38    let mut gas_used = 0u64;
39    let gas_limit = input.gas;
40    crate::init_precompile_gas(&mut gas_used, ctx, input.data.len());
41
42    let call = match IArbAggregator::ArbAggregatorCalls::abi_decode(input.data) {
43        Ok(c) => c,
44        Err(_) => return crate::burn_all_revert(gas_limit),
45    };
46    if let Some(r) = crate::reject_nonpayable_value(input.value, input.data, gas_limit, &[]) {
47        return r;
48    }
49    if let Some(r) = crate::reject_static_write(
50        input.is_static,
51        input.data,
52        gas_limit,
53        &[
54            [0xdf, 0x41, 0xe1, 0xe2],
55            [0x29, 0x14, 0x97, 0x99],
56            [0x5b, 0xe6, 0x88, 0x8b],
57        ],
58    ) {
59        return r;
60    }
61    if let Some(r) = crate::reject_delegate_nonpure(
62        input.target_address != input.bytecode_address,
63        input.data,
64        gas_limit,
65        &[],
66    ) {
67        return r;
68    }
69
70    use IArbAggregator::ArbAggregatorCalls as Calls;
71    let result = match call {
72        Calls::getPreferredAggregator(_) => {
73            let mut out = Vec::with_capacity(64);
74            let mut addr_word = [0u8; 32];
75            addr_word[12..32].copy_from_slice(BATCH_POSTER_ADDRESS.as_slice());
76            out.extend_from_slice(&addr_word);
77            out.extend_from_slice(&U256::from(1u64).to_be_bytes::<32>());
78            crate::charge_computation(&mut gas_used, ctx, 2 * COPY_GAS);
79            Ok(PrecompileOutput::new(gas_used.min(gas_limit), out.into()))
80        }
81        Calls::getDefaultAggregator(_) => {
82            let mut out = [0u8; 32];
83            out[12..32].copy_from_slice(BATCH_POSTER_ADDRESS.as_slice());
84            crate::charge_computation(&mut gas_used, ctx, COPY_GAS);
85            Ok(PrecompileOutput::new(
86                gas_used.min(gas_limit),
87                out.to_vec().into(),
88            ))
89        }
90        Calls::getTxBaseFee(_) => {
91            // 1-arg + 1-result-word: init covered the arg copy; body adds the
92            // result-copy as computation.
93            crate::charge_computation(&mut gas_used, ctx, COPY_GAS);
94            Ok(PrecompileOutput::new(
95                gas_used.min(gas_limit),
96                U256::ZERO.to_be_bytes::<32>().to_vec().into(),
97            ))
98        }
99        Calls::setTxBaseFee(_) => {
100            // 2-arg no-op returning empty: init already charged both arg words.
101            Ok(PrecompileOutput::new(
102                gas_used.min(gas_limit),
103                vec![].into(),
104            ))
105        }
106        Calls::getFeeCollector(c) => {
107            handle_get_fee_collector(&mut input, &mut gas_used, c.batchPoster, ctx)
108        }
109        Calls::setFeeCollector(c) => handle_set_fee_collector(
110            &mut input,
111            &mut gas_used,
112            c.batchPoster,
113            c.newFeeCollector,
114            ctx,
115        ),
116        Calls::getBatchPosters(_) => handle_get_batch_posters(&mut input, &mut gas_used, ctx),
117        Calls::addBatchPoster(c) => {
118            handle_add_batch_poster(&mut input, &mut gas_used, c.newBatchPoster, ctx)
119        }
120    };
121    crate::gas_check(ctx, gas_limit, gas_used, result)
122}
123
124fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), ArbPrecompileError> {
125    input
126        .internals_mut()
127        .load_account(ARBOS_STATE_ADDRESS)
128        .map_err(ArbPrecompileError::fatal)?;
129    Ok(())
130}
131
132fn handle_get_fee_collector(
133    input: &mut PrecompileInput<'_>,
134    gas_used: &mut u64,
135    poster: Address,
136    ctx: &ArbPrecompileCtx,
137) -> PrecompileResult {
138    let gas_limit = input.gas;
139    load_arbos(input)?;
140    let internals = input.internals_mut();
141    let arb_state = ctx
142        .block
143        .arbos_state(internals)
144        .map_err(ArbPrecompileError::fatal)?;
145    let bpt = arb_state.l1_pricing_state.batch_poster_table();
146
147    let poster_state = match bpt.open_poster(internals, poster, false) {
148        Ok(state) => state,
149        Err(_) => {
150            crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
151            return Err(ArbPrecompileError::empty_revert(*gas_used).into());
152        }
153    };
154    let pay_to = poster_state
155        .pay_to(internals)
156        .map_err(ArbPrecompileError::fatal)?;
157    crate::charge_storage_read(gas_used, ctx, 2 * SLOAD_GAS);
158    crate::charge_computation(gas_used, ctx, COPY_GAS);
159    Ok(PrecompileOutput::new(
160        (*gas_used).min(gas_limit),
161        U256::from_be_slice(pay_to.as_slice())
162            .to_be_bytes::<32>()
163            .to_vec()
164            .into(),
165    ))
166}
167
168/// Caller must be the batch poster, its current fee collector, or a chain owner.
169fn handle_set_fee_collector(
170    input: &mut PrecompileInput<'_>,
171    gas_used: &mut u64,
172    poster: Address,
173    new_collector: Address,
174    ctx: &ArbPrecompileCtx,
175) -> PrecompileResult {
176    let gas_limit = input.gas;
177    let caller = input.caller;
178    load_arbos(input)?;
179    let internals = input.internals_mut();
180    let arb_state = ctx
181        .block
182        .arbos_state(internals)
183        .map_err(ArbPrecompileError::fatal)?;
184    let bpt = arb_state.l1_pricing_state.batch_poster_table();
185
186    let poster_state = match bpt.open_poster(internals, poster, false) {
187        Ok(state) => state,
188        Err(_) => {
189            crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
190            return Err(ArbPrecompileError::empty_revert(*gas_used).into());
191        }
192    };
193    let old_collector = poster_state
194        .pay_to(internals)
195        .map_err(ArbPrecompileError::fatal)?;
196    crate::charge_storage_read(gas_used, ctx, 2 * SLOAD_GAS);
197
198    if caller != poster && caller != old_collector {
199        let is_owner = arb_state
200            .chain_owners
201            .is_member(internals, caller)
202            .map_err(ArbPrecompileError::fatal)?;
203        crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
204        if !is_owner {
205            return Err(ArbPrecompileError::empty_revert(*gas_used).into());
206        }
207    }
208
209    poster_state
210        .set_pay_to(internals, new_collector)
211        .map_err(ArbPrecompileError::fatal)?;
212    crate::charge_storage_write(gas_used, ctx, write_cost(new_collector.is_zero()));
213
214    Ok(PrecompileOutput::new(
215        (*gas_used).min(gas_limit),
216        vec![].into(),
217    ))
218}
219
220fn handle_get_batch_posters(
221    input: &mut PrecompileInput<'_>,
222    gas_used: &mut u64,
223    ctx: &ArbPrecompileCtx,
224) -> PrecompileResult {
225    let gas_limit = input.gas;
226    load_arbos(input)?;
227    let internals = input.internals_mut();
228    let arb_state = ctx
229        .block
230        .arbos_state(internals)
231        .map_err(ArbPrecompileError::fatal)?;
232    let bpt = arb_state.l1_pricing_state.batch_poster_table();
233
234    const MAX_MEMBERS: u64 = 65_536;
235    let posters = bpt
236        .all_posters_capped(internals, MAX_MEMBERS)
237        .map_err(ArbPrecompileError::fatal)?;
238    let count = posters.len() as u64;
239
240    let mut out = Vec::with_capacity(64 + posters.len() * 32);
241    out.extend_from_slice(&U256::from(32u64).to_be_bytes::<32>());
242    out.extend_from_slice(&U256::from(count).to_be_bytes::<32>());
243    for addr in &posters {
244        let mut word = [0u8; 32];
245        word[12..32].copy_from_slice(addr.as_slice());
246        out.extend_from_slice(&word);
247    }
248
249    crate::charge_storage_read(gas_used, ctx, (1 + count) * SLOAD_GAS);
250    crate::charge_computation(gas_used, ctx, (2 + count) * COPY_GAS);
251    Ok(PrecompileOutput::new(
252        (*gas_used).min(gas_limit),
253        out.into(),
254    ))
255}
256
257/// Caller must be a chain owner.
258fn handle_add_batch_poster(
259    input: &mut PrecompileInput<'_>,
260    gas_used: &mut u64,
261    new_poster: Address,
262    ctx: &ArbPrecompileCtx,
263) -> PrecompileResult {
264    let gas_limit = input.gas;
265    let caller = input.caller;
266    load_arbos(input)?;
267    let internals = input.internals_mut();
268    let arb_state = ctx
269        .block
270        .arbos_state(internals)
271        .map_err(ArbPrecompileError::fatal)?;
272
273    let is_owner = arb_state
274        .chain_owners
275        .is_member(internals, caller)
276        .map_err(ArbPrecompileError::fatal)?;
277    crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
278    if !is_owner {
279        return Err(ArbPrecompileError::empty_revert(*gas_used).into());
280    }
281
282    let bpt = arb_state.l1_pricing_state.batch_poster_table();
283    let already = bpt
284        .contains_poster(internals, new_poster)
285        .map_err(ArbPrecompileError::fatal)?;
286    crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
287
288    if already {
289        return Ok(PrecompileOutput::new(
290            (*gas_used).min(gas_limit),
291            vec![].into(),
292        ));
293    }
294
295    bpt.add_poster(internals, new_poster, new_poster)
296        .map_err(ArbPrecompileError::fatal)?;
297
298    let addr_write = write_cost(new_poster.is_zero());
299    crate::charge_storage_read(gas_used, ctx, 4 * SLOAD_GAS);
300    crate::charge_storage_write(
301        gas_used,
302        ctx,
303        SSTORE_ZERO_GAS + addr_write + SSTORE_GAS + addr_write + SSTORE_GAS,
304    );
305    Ok(PrecompileOutput::new(
306        (*gas_used).min(gas_limit),
307        vec![].into(),
308    ))
309}