arb_precompiles/
arbnativetokenmanager.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_b256, ARBOS_STATE_ADDRESS, NATIVE_TOKEN_SUBSPACE,
7    ROOT_STORAGE_KEY,
8};
9
10/// ArbNativeTokenManager precompile address (0x73).
11pub const ARBNATIVETOKENMANAGER_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, 0x73,
14]);
15
16// Function selectors.
17const MINT_NATIVE_TOKEN: [u8; 4] = [0xf2, 0xe2, 0x34, 0x70];
18const BURN_NATIVE_TOKEN: [u8; 4] = [0xa7, 0x54, 0x40, 0x2b];
19
20const SLOAD_GAS: u64 = 800;
21const COPY_GAS: u64 = 3;
22
23/// Gas cost for mint/burn: WarmStorageReadCost + CallValueTransferGas.
24const MINT_BURN_GAS: u64 = 100 + 9000;
25
26pub fn create_arbnativetokenmanager_precompile() -> DynPrecompile {
27    DynPrecompile::new_stateful(PrecompileId::custom("arbnativetokenmanager"), handler)
28}
29
30fn handler(mut input: PrecompileInput<'_>) -> PrecompileResult {
31    // ArbNativeTokenManager requires ArbOS >= 41.
32    if let Some(result) =
33        crate::check_precompile_version(arb_chainspec::arbos_version::ARBOS_VERSION_41)
34    {
35        return result;
36    }
37
38    let data = input.data;
39    if data.len() < 4 {
40        return Err(PrecompileError::other("input too short"));
41    }
42
43    let selector: [u8; 4] = [data[0], data[1], data[2], data[3]];
44
45    let result = match selector {
46        MINT_NATIVE_TOKEN => handle_mint(&mut input),
47        BURN_NATIVE_TOKEN => handle_burn(&mut input),
48        _ => Err(PrecompileError::other("unknown selector")),
49    };
50    crate::gas_check(input.gas, result)
51}
52
53// ── helpers ──────────────────────────────────────────────────────────
54
55fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), PrecompileError> {
56    input
57        .internals_mut()
58        .load_account(ARBOS_STATE_ADDRESS)
59        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
60    Ok(())
61}
62
63fn sload_arbos(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
64    let val = input
65        .internals_mut()
66        .sload(ARBOS_STATE_ADDRESS, slot)
67        .map_err(|_| PrecompileError::other("sload failed"))?;
68    Ok(val.data)
69}
70
71/// Check if caller is a native token owner via the NativeTokenOwners address set.
72fn is_native_token_owner(
73    input: &mut PrecompileInput<'_>,
74    addr: Address,
75) -> Result<bool, PrecompileError> {
76    // NativeTokenOwners is at subspace [10] in ArbOS state.
77    // byAddress sub-storage is at [0] within the address set.
78    let owner_key = derive_subspace_key(ROOT_STORAGE_KEY, NATIVE_TOKEN_SUBSPACE);
79    let by_address_key = derive_subspace_key(owner_key.as_slice(), &[0]);
80    let addr_hash = B256::left_padding_from(addr.as_slice());
81    let slot = map_slot_b256(by_address_key.as_slice(), &addr_hash);
82    let val = sload_arbos(input, slot)?;
83    Ok(val != U256::ZERO)
84}
85
86/// Mint native tokens to the caller's account.
87fn handle_mint(input: &mut PrecompileInput<'_>) -> PrecompileResult {
88    let data = input.data;
89    if data.len() < 36 {
90        return Err(PrecompileError::other("input too short"));
91    }
92
93    let gas_limit = input.gas;
94    let amount = U256::from_be_slice(&data[4..36]);
95    let caller = input.caller;
96    load_arbos(input)?;
97
98    if !is_native_token_owner(input, caller)? {
99        return Err(PrecompileError::other("caller is not a native token owner"));
100    }
101
102    // Add balance to the caller.
103    input
104        .internals_mut()
105        .balance_incr(caller, amount)
106        .map_err(|e| PrecompileError::other(format!("balance_incr: {e:?}")))?;
107
108    let gas_used = (SLOAD_GAS + MINT_BURN_GAS + COPY_GAS).min(gas_limit);
109    Ok(PrecompileOutput::new(gas_used, vec![].into()))
110}
111
112/// Burn native tokens from the caller's account.
113fn handle_burn(input: &mut PrecompileInput<'_>) -> PrecompileResult {
114    let data = input.data;
115    if data.len() < 36 {
116        return Err(PrecompileError::other("input too short"));
117    }
118
119    let gas_limit = input.gas;
120    let amount = U256::from_be_slice(&data[4..36]);
121    let caller = input.caller;
122    load_arbos(input)?;
123
124    if !is_native_token_owner(input, caller)? {
125        return Err(PrecompileError::other("caller is not a native token owner"));
126    }
127
128    // Check balance sufficiency.
129    let acct = input
130        .internals_mut()
131        .load_account(caller)
132        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
133    let current_balance = acct.data.info.balance;
134
135    if current_balance < amount {
136        return Err(PrecompileError::other("burn amount exceeds balance"));
137    }
138
139    // Set new balance.
140    let new_balance = current_balance - amount;
141    input
142        .internals_mut()
143        .set_balance(caller, new_balance)
144        .map_err(|e| PrecompileError::other(format!("set_balance: {e:?}")))?;
145
146    let gas_used = (SLOAD_GAS + MINT_BURN_GAS + COPY_GAS).min(gas_limit);
147    Ok(PrecompileOutput::new(gas_used, vec![].into()))
148}