arb_precompiles/
arbwasm.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, PROGRAMS_DATA_KEY,
7    PROGRAMS_PARAMS_KEY, PROGRAMS_SUBSPACE, ROOT_STORAGE_KEY,
8};
9
10/// ArbWasm precompile address (0x71).
11pub 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
16// Function selectors — view methods returning Stylus program config.
17const 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
41/// Initial page ramp constant (not stored in packed params).
42const 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    // ArbWasm requires ArbOS >= 30 (Stylus).
54    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            // Page ramp is a constant, not stored in packed params.
95            // Still load the account for consistency.
96            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            // Requires ArbOS >= 32 (StylusChargingFixes).
106            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        // Program queries by codehash.
143        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, &params_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, &params_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 queries by address (need to get codehash from account).
161        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, &params_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, &params_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, &params_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, &params_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        // State-modifying.
226        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
241// ── Helpers ──────────────────────────────────────────────────────────
242
243fn 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
259/// Load the packed StylusParams word (slot 0) from storage.
260fn 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
269/// Load both the params word and a program entry by codehash.
270fn 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    // Params
278    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    // Program data
283    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
293/// Get the code hash for an account address.
294fn 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
305/// Parsed program entry from a storage word.
306struct 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
315/// Arbitrum start time.
316const 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
339/// Get the current block timestamp from the thread-local.
340fn 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
350/// Validate that a program is active (version matches and not expired).
351fn 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
364/// Extract a bytes32 argument from calldata (after 4-byte selector).
365fn 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
374/// Extract an address argument from calldata (after 4-byte selector).
375fn 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    // Address is right-aligned in 32-byte word.
380    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}