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
10pub 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
16const 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
22const 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
37const 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 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 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 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 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
106fn 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
136fn 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
142fn 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
148fn 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
155fn 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
165fn 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 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
187fn 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 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 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 let new_val = U256::from_be_slice(new_collector.as_slice());
221 sstore_field(input, pay_to_slot, new_val)?;
222
223 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
235fn 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 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 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 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 let gas_used = (2 + count) * SLOAD_GAS + (2 + count) * COPY_GAS;
269 Ok(PrecompileOutput::new(gas_used.min(gas_limit), out.into()))
270}
271
272fn 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 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 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 return Ok(PrecompileOutput::new(
300 (2 * SLOAD_GAS + COPY_GAS).min(gas_limit),
301 vec![].into(),
302 ));
303 }
304
305 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 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 let slot_value = U256::from(new_size);
320 sstore_field(input, member_slot, slot_value)?;
321
322 sstore_field(input, size_slot, U256::from(new_size))?;
324
325 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 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}