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, PROGRAMS_DATA_KEY,
7 PROGRAMS_PARAMS_KEY, PROGRAMS_SUBSPACE, ROOT_STORAGE_KEY,
8};
9
10pub const ARBWASM_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, 0x71,
14]);
15
16const STYLUS_VERSION: [u8; 4] = [0xf2, 0x8a, 0x04, 0x99];
18const INK_PRICE: [u8; 4] = [0xeb, 0xf5, 0xd2, 0x51];
19const MAX_STACK_DEPTH: [u8; 4] = [0x19, 0x4a, 0xa2, 0x8e];
20const FREE_PAGES: [u8; 4] = [0xb6, 0x9d, 0xb8, 0x5e];
21const PAGE_GAS: [u8; 4] = [0x96, 0x76, 0xa4, 0x67];
22const PAGE_RAMP: [u8; 4] = [0x56, 0xc1, 0x80, 0x1c];
23const PAGE_LIMIT: [u8; 4] = [0x20, 0xf0, 0x02, 0xea];
24const MIN_INIT_GAS: [u8; 4] = [0x5b, 0x19, 0x32, 0x87];
25const INIT_COST_SCALAR: [u8; 4] = [0x67, 0x46, 0x27, 0x93];
26const EXPIRY_DAYS: [u8; 4] = [0xee, 0xe2, 0x2a, 0xa3];
27const KEEPALIVE_DAYS: [u8; 4] = [0xe7, 0xfb, 0x85, 0x75];
28const BLOCK_CACHE_SIZE: [u8; 4] = [0xd2, 0xfb, 0xa3, 0xc5];
29const ACTIVATE_PROGRAM: [u8; 4] = [0x72, 0x93, 0x80, 0x88];
30const CODEHASH_KEEPALIVE: [u8; 4] = [0xe7, 0xf6, 0x2c, 0x15];
31const CODEHASH_VERSION: [u8; 4] = [0xb4, 0xb7, 0xc5, 0xf5];
32const CODEHASH_ASM_SIZE: [u8; 4] = [0x5f, 0xd3, 0x5d, 0xea];
33const PROGRAM_VERSION: [u8; 4] = [0x70, 0x46, 0x7c, 0x7c];
34const PROGRAM_INIT_GAS: [u8; 4] = [0x8e, 0x15, 0xc4, 0x17];
35const PROGRAM_MEMORY_FOOTPRINT: [u8; 4] = [0x95, 0x48, 0xea, 0xb0];
36const PROGRAM_TIME_LEFT: [u8; 4] = [0x63, 0x5b, 0x36, 0x42];
37
38const SLOAD_GAS: u64 = 800;
39const COPY_GAS: u64 = 3;
40
41const INITIAL_PAGE_RAMP: u64 = 620674314;
43
44const MIN_INIT_GAS_UNITS: u64 = 128;
45const MIN_CACHED_GAS_UNITS: u64 = 32;
46const COST_SCALAR_PERCENT: u64 = 2;
47
48pub fn create_arbwasm_precompile() -> DynPrecompile {
49 DynPrecompile::new_stateful(PrecompileId::custom("arbwasm"), handler)
50}
51
52fn handler(mut input: PrecompileInput<'_>) -> PrecompileResult {
53 if let Some(result) =
55 crate::check_precompile_version(arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS)
56 {
57 return result;
58 }
59
60 let data = input.data;
61 if data.len() < 4 {
62 return Err(PrecompileError::other("input too short"));
63 }
64
65 let selector: [u8; 4] = [data[0], data[1], data[2], data[3]];
66
67 let result = match selector {
68 STYLUS_VERSION => {
69 let params = load_params_word(&mut input)?;
70 let version = u16::from_be_bytes([params[0], params[1]]);
71 ok_u256(SLOAD_GAS + COPY_GAS, U256::from(version))
72 }
73 INK_PRICE => {
74 let params = load_params_word(&mut input)?;
75 let ink_price = (params[2] as u32) << 16 | (params[3] as u32) << 8 | params[4] as u32;
76 ok_u256(SLOAD_GAS + COPY_GAS, U256::from(ink_price))
77 }
78 MAX_STACK_DEPTH => {
79 let params = load_params_word(&mut input)?;
80 let depth = u32::from_be_bytes([params[5], params[6], params[7], params[8]]);
81 ok_u256(SLOAD_GAS + COPY_GAS, U256::from(depth))
82 }
83 FREE_PAGES => {
84 let params = load_params_word(&mut input)?;
85 let pages = u16::from_be_bytes([params[9], params[10]]);
86 ok_u256(SLOAD_GAS + COPY_GAS, U256::from(pages))
87 }
88 PAGE_GAS => {
89 let params = load_params_word(&mut input)?;
90 let gas = u16::from_be_bytes([params[11], params[12]]);
91 ok_u256(SLOAD_GAS + COPY_GAS, U256::from(gas))
92 }
93 PAGE_RAMP => {
94 load_arbos(&mut input)?;
97 ok_u256(COPY_GAS, U256::from(INITIAL_PAGE_RAMP))
98 }
99 PAGE_LIMIT => {
100 let params = load_params_word(&mut input)?;
101 let limit = u16::from_be_bytes([params[13], params[14]]);
102 ok_u256(SLOAD_GAS + COPY_GAS, U256::from(limit))
103 }
104 MIN_INIT_GAS => {
105 if let Some(result) = crate::check_method_version(
107 arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS_CHARGING_FIXES,
108 0,
109 ) {
110 return result;
111 }
112 let params = load_params_word(&mut input)?;
113 let min_init = params[15] as u64;
114 let min_cached = params[16] as u64;
115 let init = min_init.saturating_mul(MIN_INIT_GAS_UNITS);
116 let cached = min_cached.saturating_mul(MIN_CACHED_GAS_UNITS);
117 ok_two_u256(SLOAD_GAS + COPY_GAS, U256::from(init), U256::from(cached))
118 }
119 INIT_COST_SCALAR => {
120 let params = load_params_word(&mut input)?;
121 let scalar = params[17] as u64;
122 ok_u256(
123 SLOAD_GAS + COPY_GAS,
124 U256::from(scalar.saturating_mul(COST_SCALAR_PERCENT)),
125 )
126 }
127 EXPIRY_DAYS => {
128 let params = load_params_word(&mut input)?;
129 let days = u16::from_be_bytes([params[19], params[20]]);
130 ok_u256(SLOAD_GAS + COPY_GAS, U256::from(days))
131 }
132 KEEPALIVE_DAYS => {
133 let params = load_params_word(&mut input)?;
134 let days = u16::from_be_bytes([params[21], params[22]]);
135 ok_u256(SLOAD_GAS + COPY_GAS, U256::from(days))
136 }
137 BLOCK_CACHE_SIZE => {
138 let params = load_params_word(&mut input)?;
139 let size = u16::from_be_bytes([params[23], params[24]]);
140 ok_u256(SLOAD_GAS + COPY_GAS, U256::from(size))
141 }
142 CODEHASH_VERSION => {
144 let codehash = extract_bytes32(input.data)?;
145 let (params_word, program_word) = load_params_and_program(&mut input, codehash)?;
146 let params_version = u16::from_be_bytes([params_word[0], params_word[1]]);
147 let program = parse_program(&program_word, ¶ms_word);
148 validate_active_program(&program, params_version)?;
149 ok_u256(2 * SLOAD_GAS + COPY_GAS, U256::from(program.version))
150 }
151 CODEHASH_ASM_SIZE => {
152 let codehash = extract_bytes32(input.data)?;
153 let (params_word, program_word) = load_params_and_program(&mut input, codehash)?;
154 let params_version = u16::from_be_bytes([params_word[0], params_word[1]]);
155 let program = parse_program(&program_word, ¶ms_word);
156 validate_active_program(&program, params_version)?;
157 let asm_size = program.asm_estimate_kb.saturating_mul(1024);
158 ok_u256(2 * SLOAD_GAS + COPY_GAS, U256::from(asm_size))
159 }
160 PROGRAM_VERSION => {
162 let address = extract_address(input.data)?;
163 let codehash = get_account_codehash(&mut input, address)?;
164 let (params_word, program_word) = load_params_and_program(&mut input, codehash)?;
165 let params_version = u16::from_be_bytes([params_word[0], params_word[1]]);
166 let program = parse_program(&program_word, ¶ms_word);
167 validate_active_program(&program, params_version)?;
168 ok_u256(3 * SLOAD_GAS + COPY_GAS, U256::from(program.version))
169 }
170 PROGRAM_INIT_GAS => {
171 let address = extract_address(input.data)?;
172 let codehash = get_account_codehash(&mut input, address)?;
173 let (params_word, program_word) = load_params_and_program(&mut input, codehash)?;
174 let params_version = u16::from_be_bytes([params_word[0], params_word[1]]);
175 let program = parse_program(&program_word, ¶ms_word);
176 validate_active_program(&program, params_version)?;
177
178 let min_init = params_word[15] as u64;
179 let min_cached = params_word[16] as u64;
180 let init_cost_scalar = params_word[17] as u64;
181 let cached_cost_scalar = params_word[18] as u64;
182
183 let init_base = min_init.saturating_mul(MIN_INIT_GAS_UNITS);
184 let init_dyno =
185 (program.init_cost as u64).saturating_mul(init_cost_scalar * COST_SCALAR_PERCENT);
186 let mut init_gas = init_base.saturating_add(div_ceil(init_dyno, 100));
187
188 let cached_base = min_cached.saturating_mul(MIN_CACHED_GAS_UNITS);
189 let cached_dyno = (program.cached_cost as u64)
190 .saturating_mul(cached_cost_scalar * COST_SCALAR_PERCENT);
191 let cached_gas = cached_base.saturating_add(div_ceil(cached_dyno, 100));
192
193 if params_version > 1 {
194 init_gas = init_gas.saturating_add(cached_gas);
195 }
196
197 ok_two_u256(
198 3 * SLOAD_GAS + COPY_GAS,
199 U256::from(init_gas),
200 U256::from(cached_gas),
201 )
202 }
203 PROGRAM_MEMORY_FOOTPRINT => {
204 let address = extract_address(input.data)?;
205 let codehash = get_account_codehash(&mut input, address)?;
206 let (params_word, program_word) = load_params_and_program(&mut input, codehash)?;
207 let params_version = u16::from_be_bytes([params_word[0], params_word[1]]);
208 let program = parse_program(&program_word, ¶ms_word);
209 validate_active_program(&program, params_version)?;
210 ok_u256(3 * SLOAD_GAS + COPY_GAS, U256::from(program.footprint))
211 }
212 PROGRAM_TIME_LEFT => {
213 let address = extract_address(input.data)?;
214 let codehash = get_account_codehash(&mut input, address)?;
215 let (params_word, program_word) = load_params_and_program(&mut input, codehash)?;
216 let params_version = u16::from_be_bytes([params_word[0], params_word[1]]);
217 let program = parse_program(&program_word, ¶ms_word);
218 validate_active_program(&program, params_version)?;
219
220 let expiry_days = u16::from_be_bytes([params_word[19], params_word[20]]);
221 let expiry_seconds = (expiry_days as u64) * 24 * 3600;
222 let time_left = expiry_seconds.saturating_sub(program.age_seconds);
223 ok_u256(3 * SLOAD_GAS + COPY_GAS, U256::from(time_left))
224 }
225 ACTIVATE_PROGRAM => {
227 let _ = &mut input;
228 Err(PrecompileError::other(
229 "Stylus activation not yet supported",
230 ))
231 }
232 CODEHASH_KEEPALIVE => {
233 let _ = &mut input;
234 Err(PrecompileError::other("Stylus keepalive not yet supported"))
235 }
236 _ => Err(PrecompileError::other("unknown ArbWasm selector")),
237 };
238 crate::gas_check(input.gas, result)
239}
240
241fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), PrecompileError> {
244 input
245 .internals_mut()
246 .load_account(ARBOS_STATE_ADDRESS)
247 .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
248 Ok(())
249}
250
251fn sload_field(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
252 let val = input
253 .internals_mut()
254 .sload(ARBOS_STATE_ADDRESS, slot)
255 .map_err(|_| PrecompileError::other("sload failed"))?;
256 Ok(val.data)
257}
258
259fn load_params_word(input: &mut PrecompileInput<'_>) -> Result<[u8; 32], PrecompileError> {
261 load_arbos(input)?;
262 let programs_key = derive_subspace_key(ROOT_STORAGE_KEY, PROGRAMS_SUBSPACE);
263 let params_key = derive_subspace_key(programs_key.as_slice(), PROGRAMS_PARAMS_KEY);
264 let slot = map_slot(params_key.as_slice(), 0);
265 let value = sload_field(input, slot)?;
266 Ok(value.to_be_bytes::<32>())
267}
268
269fn load_params_and_program(
271 input: &mut PrecompileInput<'_>,
272 codehash: B256,
273) -> Result<([u8; 32], [u8; 32]), PrecompileError> {
274 load_arbos(input)?;
275 let programs_key = derive_subspace_key(ROOT_STORAGE_KEY, PROGRAMS_SUBSPACE);
276
277 let params_key = derive_subspace_key(programs_key.as_slice(), PROGRAMS_PARAMS_KEY);
279 let params_slot = map_slot(params_key.as_slice(), 0);
280 let params_value = sload_field(input, params_slot)?;
281
282 let data_key = derive_subspace_key(programs_key.as_slice(), PROGRAMS_DATA_KEY);
284 let program_slot = map_slot_b256(data_key.as_slice(), &codehash);
285 let program_value = sload_field(input, program_slot)?;
286
287 Ok((
288 params_value.to_be_bytes::<32>(),
289 program_value.to_be_bytes::<32>(),
290 ))
291}
292
293fn get_account_codehash(
295 input: &mut PrecompileInput<'_>,
296 address: Address,
297) -> Result<B256, PrecompileError> {
298 let account = input
299 .internals_mut()
300 .load_account(address)
301 .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
302 Ok(account.data.info.code_hash)
303}
304
305struct ProgramInfo {
307 version: u16,
308 init_cost: u16,
309 cached_cost: u16,
310 footprint: u16,
311 asm_estimate_kb: u32,
312 age_seconds: u64,
313}
314
315const ARBITRUM_START_TIME: u64 = 1622243344;
317
318fn parse_program(data: &[u8; 32], params_word: &[u8; 32]) -> ProgramInfo {
319 let version = u16::from_be_bytes([data[0], data[1]]);
320 let init_cost = u16::from_be_bytes([data[2], data[3]]);
321 let cached_cost = u16::from_be_bytes([data[4], data[5]]);
322 let footprint = u16::from_be_bytes([data[6], data[7]]);
323 let activated_at = (data[8] as u32) << 16 | (data[9] as u32) << 8 | data[10] as u32;
324 let asm_estimate_kb = (data[11] as u32) << 16 | (data[12] as u32) << 8 | data[13] as u32;
325
326 let _ = params_word;
327 let age_seconds = hours_to_age(block_timestamp(), activated_at);
328
329 ProgramInfo {
330 version,
331 init_cost,
332 cached_cost,
333 footprint,
334 asm_estimate_kb,
335 age_seconds,
336 }
337}
338
339fn block_timestamp() -> u64 {
341 crate::get_block_timestamp()
342}
343
344fn hours_to_age(time: u64, hours: u32) -> u64 {
345 let seconds = (hours as u64).saturating_mul(3600);
346 let activated_at = ARBITRUM_START_TIME.saturating_add(seconds);
347 time.saturating_sub(activated_at)
348}
349
350fn validate_active_program(
352 program: &ProgramInfo,
353 params_version: u16,
354) -> Result<(), PrecompileError> {
355 if program.version == 0 {
356 return Err(PrecompileError::other("program not activated"));
357 }
358 if program.version != params_version {
359 return Err(PrecompileError::other("program needs upgrade"));
360 }
361 Ok(())
362}
363
364fn extract_bytes32(data: &[u8]) -> Result<B256, PrecompileError> {
366 if data.len() < 36 {
367 return Err(PrecompileError::other("calldata too short for bytes32 arg"));
368 }
369 let mut bytes = [0u8; 32];
370 bytes.copy_from_slice(&data[4..36]);
371 Ok(B256::from(bytes))
372}
373
374fn extract_address(data: &[u8]) -> Result<Address, PrecompileError> {
376 if data.len() < 36 {
377 return Err(PrecompileError::other("calldata too short for address arg"));
378 }
379 let mut bytes = [0u8; 20];
381 bytes.copy_from_slice(&data[16..36]);
382 Ok(Address::from(bytes))
383}
384
385fn ok_u256(gas_cost: u64, value: U256) -> PrecompileResult {
386 Ok(PrecompileOutput::new(
387 gas_cost,
388 value.to_be_bytes::<32>().to_vec().into(),
389 ))
390}
391
392fn ok_two_u256(gas_cost: u64, a: U256, b: U256) -> PrecompileResult {
393 let mut out = Vec::with_capacity(64);
394 out.extend_from_slice(&a.to_be_bytes::<32>());
395 out.extend_from_slice(&b.to_be_bytes::<32>());
396 Ok(PrecompileOutput::new(gas_cost, out.into()))
397}
398
399fn div_ceil(a: u64, b: u64) -> u64 {
400 a.div_ceil(b)
401}