diff --git a/crates/anvil-polkadot/src/api_server/server.rs b/crates/anvil-polkadot/src/api_server/server.rs index a12f861acd951..c6ca08af41ec3 100644 --- a/crates/anvil-polkadot/src/api_server/server.rs +++ b/crates/anvil-polkadot/src/api_server/server.rs @@ -360,6 +360,9 @@ impl ApiServer { EthRequest::SetStorageAt(address, key, value) => { self.set_storage_at(address, key, value).to_rpc_result() } + EthRequest::SetImmutableStorageAt(address, data) => { + self.set_immutable_storage_at(address, data).to_rpc_result() + } EthRequest::SetChainId(chain_id) => self.set_chain_id(chain_id).to_rpc_result(), // --- Revert --- EthRequest::EvmSnapshot(()) => self.snapshot().await.to_rpc_result(), @@ -1327,6 +1330,49 @@ impl ApiServer { Ok(()) } + fn set_immutable_storage_at( + &self, + address: Address, + immutables: Vec, + ) -> Result<()> { + node_info!("anvil_setImmutableStorageAt"); + + let latest_block = self.latest_block(); + + // Convert ABI-encoded immutable values (big-endian) to PVM format (little-endian). + // + // ## Data Format + // + // Each element in `immutables` is a single ABI-encoded immutable value: + // - Value in big-endian format (standard for Solidity ABI encoding) + // - Padded to 32 bytes + // + // Example: For a contract with immutables `uint256 value` and `address addr`: + // immutables[0] = <32-byte big-endian uint256> + // immutables[1] = <12 zeros + 20-byte address> + // + // ## Conversion + // + // This method converts each immutable value (32-byte chunk) from big-endian to + // little-endian, since PVM (Polkadot Virtual Machine) expects immutable data in + // little-endian format. The conversion is done by reversing each 32-byte word. + // Then all converted values are concatenated in order. + let pvm_data: Vec = immutables + .into_iter() + .flat_map(|immutable| { + let mut word = [0u8; 32]; + let len = immutable.len().min(32); + word[..len].copy_from_slice(&immutable[..len]); + word.reverse(); + word + }) + .collect(); + + self.backend.inject_immutable_data(latest_block, address, pvm_data); + + Ok(()) + } + // ----- Wallet RPCs async fn sign(&self, address: Address, content: impl AsRef<[u8]>) -> Result { Ok(alloy_primitives::hex::encode_prefixed(self.wallet.sign(address, content.as_ref())?)) diff --git a/crates/anvil-polkadot/src/substrate_node/service/backend.rs b/crates/anvil-polkadot/src/substrate_node/service/backend.rs index 9f3f256a8a355..d24257f45d8a0 100644 --- a/crates/anvil-polkadot/src/substrate_node/service/backend.rs +++ b/crates/anvil-polkadot/src/substrate_node/service/backend.rs @@ -207,6 +207,11 @@ impl BackendWithOverlay { overrides.set_code_info(at, code_hash, code_info); } + pub fn inject_immutable_data(&self, at: Hash, address: Address, data: Vec) { + let mut overrides = self.overrides.lock(); + overrides.set_immutable_data(at, address, data); + } + pub fn inject_child_storage( &self, at: Hash, @@ -353,6 +358,21 @@ impl StorageOverrides { self.add(latest_block, changeset); } + fn set_immutable_data(&mut self, latest_block: Hash, address: Address, data: Vec) { + let mut changeset = BlockOverrides::default(); + + // SCALE-encode as BoundedVec: compact_length prefix followed by raw bytes + let mut encoded = codec::Compact(data.len() as u32).encode(); + encoded.extend_from_slice(&data); + + changeset.top.insert( + well_known_keys::immutable_data_of(H160::from_slice(address.as_slice())), + Some(encoded), + ); + + self.add(latest_block, changeset); + } + fn set_child_storage( &mut self, latest_block: Hash, diff --git a/crates/anvil-polkadot/src/substrate_node/service/storage.rs b/crates/anvil-polkadot/src/substrate_node/service/storage.rs index a6553cead52c8..4f60c8c62229f 100644 --- a/crates/anvil-polkadot/src/substrate_node/service/storage.rs +++ b/crates/anvil-polkadot/src/substrate_node/service/storage.rs @@ -90,4 +90,13 @@ pub mod well_known_keys { key } + + pub fn immutable_data_of(address: H160) -> Vec { + let mut key = Vec::new(); + key.extend_from_slice(&twox_128("Revive".as_bytes())); + key.extend_from_slice(&twox_128("ImmutableDataOf".as_bytes())); + key.extend_from_slice(&address.encode()); + + key + } } diff --git a/crates/anvil-polkadot/test-data/ImmutableStorage.json b/crates/anvil-polkadot/test-data/ImmutableStorage.json new file mode 100644 index 0000000000000..8dec933c2ac0c --- /dev/null +++ b/crates/anvil-polkadot/test-data/ImmutableStorage.json @@ -0,0 +1,75 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_addr", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "getImmutableAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getImmutableValue", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "immutableAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "immutableValue", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bin": "60c060405234801561000f575f5ffd5b5060405161038b38038061038b83398181016040528101906100319190610105565b81608081815250508073ffffffffffffffffffffffffffffffffffffffff1660a08173ffffffffffffffffffffffffffffffffffffffff16815250505050610143565b5f5ffd5b5f819050919050565b61008a81610078565b8114610094575f5ffd5b50565b5f815190506100a581610081565b92915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100d4826100ab565b9050919050565b6100e4816100ca565b81146100ee575f5ffd5b50565b5f815190506100ff816100db565b92915050565b5f5f6040838503121561011b5761011a610074565b5b5f61012885828601610097565b9250506020610139858286016100f1565b9150509250929050565b60805160a05161021b6101705f395f818160f0015261013a01525f818160c90152610116015261021b5ff3fe608060405234801561000f575f5ffd5b506004361061004a575f3560e01c8063304e1cb21461004e578063477198e21461006c57806382f155161461008a5780638d726ac7146100a8575b5f5ffd5b6100566100c6565b6040516100639190610174565b60405180910390f35b6100746100ed565b60405161008191906101cc565b60405180910390f35b610092610114565b60405161009f9190610174565b60405180910390f35b6100b0610138565b6040516100bd91906101cc565b60405180910390f35b5f7f0000000000000000000000000000000000000000000000000000000000000000905090565b5f7f0000000000000000000000000000000000000000000000000000000000000000905090565b7f000000000000000000000000000000000000000000000000000000000000000081565b7f000000000000000000000000000000000000000000000000000000000000000081565b5f819050919050565b61016e8161015c565b82525050565b5f6020820190506101875f830184610165565b92915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6101b68261018d565b9050919050565b6101c6816101ac565b82525050565b5f6020820190506101df5f8301846101bd565b9291505056fea264697066735822122040ab77b65e5147c48aee1cbca580ba83cdadcec862bf5c18d744a06874682ae064736f6c634300081e0033", + "bin-runtime": "608060405234801561000f575f5ffd5b506004361061004a575f3560e01c8063304e1cb21461004e578063477198e21461006c57806382f155161461008a5780638d726ac7146100a8575b5f5ffd5b6100566100c6565b6040516100639190610174565b60405180910390f35b6100746100ed565b60405161008191906101cc565b60405180910390f35b610092610114565b60405161009f9190610174565b60405180910390f35b6100b0610138565b6040516100bd91906101cc565b60405180910390f35b5f7f0000000000000000000000000000000000000000000000000000000000000000905090565b5f7f0000000000000000000000000000000000000000000000000000000000000000905090565b7f000000000000000000000000000000000000000000000000000000000000000081565b7f000000000000000000000000000000000000000000000000000000000000000081565b5f819050919050565b61016e8161015c565b82525050565b5f6020820190506101875f830184610165565b92915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6101b68261018d565b9050919050565b6101c6816101ac565b82525050565b5f6020820190506101df5f8301846101bd565b9291505056fea264697066735822122040ab77b65e5147c48aee1cbca580ba83cdadcec862bf5c18d744a06874682ae064736f6c634300081e0033", + "bin-pvm": "50564d00008809000000000000010700c25000c2000003044000000004809a08000000000e0000001c0000002a000000390000004b000000560000006800000063616c6c5f646174615f636f707963616c6c5f646174615f6c6f616463616c6c5f646174615f73697a65636f6e73756d655f616c6c5f6761736765745f696d6d757461626c655f646174617365616c5f72657475726e7365745f696d6d757461626c655f6461746176616c75655f7472616e73666572726564051102848b0463616c6c8497066465706c6f790688b8210287811a001f00b000bb00e4000d017b01a001ac01f1012202470258028502bf02cb02dd024903630368039703a103b70323043d0441046b0475048b049704a4042e0576069511f07b10087b158475010a02013d07040002510507501002095010043e02951180fe7b1078017b1570017b166801951580018411e049215801492150014921480149214001831740010a0701821750018218580182194801821a4001d49808d4a707d487075107143308100002838833070133090a0528ca00390604000297672098772095771f8477e09578c000d87807492138017b1820017b17280194777b17300133074095182001501006a7053307c00064685010088e068377646833090a01551640143308100002838833070133090a05286c951700013308c00050100a3904821718017b1738821710017b1730821708017b1728821700017b17209517e0003308e00050100c10048219f0008216f80014070000000001000000d39707d46707989820888801946893785208123308100002838833070133090a05018217e8007b17108217e0007b17188217207b17c0008217287b17c8008217307b17d000330780009518c0007b19308219387b19d80050100edc047b16b8008217307b17b0008217107b17a8003307a000c871088219187b19a000501010b704951780003308405010127103821798008218900082198800821a8000d49707d48a09d47909989920d48707977720d49707520781003307100002bea707330833097b1a380a951760330880005010142c038217783306100004821870821968821a603e072800043e082000043e091800043e0a1000049517403308a000501016fb02821758821850821948821a407b67387b68307b69287b6a2028c902821738330850101802058378330733090a0528f2fe33001a0a03019511a07b10587b15507b16489515608411e06416491638491630491620800033074095682049162850101cd2033907040002531704419517e08477e07b67186471837733080a010182671881771c51471655f182155147c76a728d195247b21c4e300b50101e225247e29871470c501020f1003308100002838833070133093300220a0501951160ff7b1098007b1590007b1688009515a0008411e04911784911704911684911608317600a0701821770821878821968821a60d49808d4a707d4870751070a33081000022811390704000256170314330810000201838833070133090a0501951740330840501024d4018217587b17188217507b17108217487b170882164050102666015012284101520749646b3a091000043a081800043a0a2000043a072800047b17387b1a307b18289518207b192064b750102ac002330820646750102ca803837833092033070a0533002e0a03280833002e0a0301951160ff7b1098007b1590007b1688009515a0008411e04911784911704911684911608317600a0701821770821878821968821a60d49808d4a707d4870751070a33081000022811390704000256170314330810000201838833070133090a0501951740330840501030fa008217587b17188217507b17108217487b17088216405010328c0050123467520744646a3a093000043a0838000438074000044911387b17307b18289518207b192064a7501036ec013308206467501038d402837833092033070a0533003a0a03280833003a0a03019511f87b10330750103c6efb9511f87b1033070150103e61fb821718821808d48707821910d49608d47808988820d49707977720d4870732029511e87b10107b15087b163306000002390500000251051c3307100004837783680a040139070000022003000002ac571082101082150882169511183200003908000002510835fd330710000483770a062828fd9511f07b10087b156489647533082064975010401b027c78017c797c7a027c7b03978808d4980897aa1097bb18d4ba0ad4a8087c79057c7a047c7b067c7c07979908d4a90997bb1097cc18d4cb0bd4b909979920d489027c79097c7a087c7b0a7c7c0b979908d4a90997bb1097cc18d4cb0bd4b9097c7a0d7c7b0c7c7c0e7c780f97aa08d4ba0a97cc10978818d4c808d4a808978820d498037c78117c7a107c7b127c7c13978808d4a80897bb1097cc18d4cb0bd4b8087c7a157c7b147c7c167c791797aa08d4ba0a97cc10979918d4c909d4a909979920d4890a7c78197c79187c7b1a7c7c1b978808d4980897bb1097cc18d4cb0bd4b8087c791d7c7b1c7c7c1e7c771f979908d4b90997cc10977718d4c707d49707977720d487076f776fa86f396f2a7b5a187b59107b58087b57821008821595111032009511d87b10207b15187b161082897b19088289087b19828510828618330820501042d3006f686f59821a6faa821b086fbb787b18787a10787908787898bc38787c1f98bc30787c1e98bc28787c1d98bc20787c1c98bc18787c1b98bc10787c1a98bb08787b1998ab38787b1798ab30787b1698ab28787b1598ab20787b1498ab18787b1398ab10787b1298aa08787a11989a38787a0f989a30787a0e989a28787a0d989a20787a0c989a18787a0b989a10787a0a9899087879099889387879079889307879069889287879059889207879049889187879039889107879029888087878018210208215188216109511283200838951092e8b7a11520a34330b010002aeb92cc8780883881f8488e0563800000220390a080002ae8a093d080800020183773308100002c887073200004969488488884844443422224942a98424894444910851a88e50aa88108944221121228024a9845223914422119144884422920891101191244922a432111292841042222449922485aaa44992948424214492aa492010884008a53422229224699224492844586a1292244908298410922485144aa5d288884892a4499224a11061a94948922421520821495248a1542a4d85949024499294a41012aa2124290d112a4995424a92244992244992244992244992244992244992244992244992244992244992a42a49292549494942554a529224499224499224499224499224499224499224499254929024414858480100" +} \ No newline at end of file diff --git a/crates/anvil-polkadot/test-data/ImmutableStorage.sol b/crates/anvil-polkadot/test-data/ImmutableStorage.sol new file mode 100644 index 0000000000000..32fa5a407ed70 --- /dev/null +++ b/crates/anvil-polkadot/test-data/ImmutableStorage.sol @@ -0,0 +1,19 @@ +pragma solidity ^0.8.0; + +contract ImmutableStorage { + uint256 public immutable immutableValue; + address public immutable immutableAddress; + + constructor(uint256 _value, address _addr) { + immutableValue = _value; + immutableAddress = _addr; + } + + function getImmutableValue() public view returns (uint256) { + return immutableValue; + } + + function getImmutableAddress() public view returns (address) { + return immutableAddress; + } +} diff --git a/crates/anvil-polkadot/tests/it/abi.rs b/crates/anvil-polkadot/tests/it/abi.rs index 0c0aedfc188ca..67489be13856d 100644 --- a/crates/anvil-polkadot/tests/it/abi.rs +++ b/crates/anvil-polkadot/tests/it/abi.rs @@ -23,3 +23,9 @@ sol!( SimpleStorageCaller, "test-data/SimpleStorageCaller.json" ); + +sol!( + #[derive(Debug)] + ImmutableStorage, + "test-data/ImmutableStorage.json" +); diff --git a/crates/anvil-polkadot/tests/it/state_injector.rs b/crates/anvil-polkadot/tests/it/state_injector.rs index c41522c23dc69..27a2f96b26e27 100644 --- a/crates/anvil-polkadot/tests/it/state_injector.rs +++ b/crates/anvil-polkadot/tests/it/state_injector.rs @@ -1,6 +1,6 @@ use crate::{ - abi::SimpleStorage, - utils::{ContractCode, TestNode, get_contract_code, unwrap_response}, + abi::{ImmutableStorage, SimpleStorage}, + utils::{ContractCode, TestNode, get_contract_code, get_contract_pvm_code, unwrap_response}, }; use alloy_eips::BlockId; use alloy_primitives::{Address, B256, Bytes, U256}; @@ -776,3 +776,116 @@ async fn test_set_storage() { assert_eq!(stored_value, 511); } } + +#[tokio::test(flavor = "multi_thread")] +async fn test_set_immutable_storage() { + use alloy_sol_types::SolValue; + + let anvil_node_config = AnvilNodeConfig::test_config(); + let substrate_node_config = SubstrateNodeConfig::new(&anvil_node_config); + let mut node = TestNode::new(anvil_node_config, substrate_node_config).await.unwrap(); + + let alith = Account::from(subxt_signer::eth::dev::alith()); + let alith_addr = Address::from(ReviveAddress::new(alith.address())); + let initial_value = U256::from(12345); + let initial_address = Address::from(ReviveAddress::new( + Account::from(subxt_signer::eth::dev::baltathar()).address(), + )); + + // Deploy PVM contract with ABI-encoded constructor args + let bytecode = get_contract_pvm_code("ImmutableStorage"); + let constructor_args = (initial_value, initial_address).abi_encode(); + let deployment_bytecode = [bytecode.as_slice(), constructor_args.as_slice()].concat(); + let tx_hash = node.deploy_contract(&deployment_bytecode, alith.address()).await; + unwrap_response::<()>(node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + + let contract_address = Address::from(ReviveAddress::new( + node.get_transaction_receipt(tx_hash).await.contract_address.unwrap(), + )); + + // Verify initial immutable value + let call_tx = TransactionRequest::default() + .from(alith_addr) + .to(contract_address) + .input(TransactionInput::both( + ImmutableStorage::getImmutableValueCall {}.abi_encode().into(), + )); + let result = unwrap_response::( + node.eth_rpc(EthRequest::EthCall(call_tx.into(), None, None, None)).await.unwrap(), + ) + .unwrap(); + assert_eq!( + ImmutableStorage::getImmutableValueCall::abi_decode_returns(&result.0).unwrap(), + initial_value + ); + + // Verify initial immutable address + let call_tx = TransactionRequest::default() + .from(alith_addr) + .to(contract_address) + .input(TransactionInput::both( + ImmutableStorage::getImmutableAddressCall {}.abi_encode().into(), + )); + let result = unwrap_response::( + node.eth_rpc(EthRequest::EthCall(call_tx.into(), None, None, None)).await.unwrap(), + ) + .unwrap(); + assert_eq!( + ImmutableStorage::getImmutableAddressCall::abi_decode_returns(&result.0).unwrap(), + initial_address + ); + + // Set new immutable values via anvil_setImmutableStorageAt + // Each immutable is passed as a separate ABI-encoded bytes value + let new_value = U256::from(99999); + let new_address = Address::from(ReviveAddress::new( + Account::from(subxt_signer::eth::dev::charleth()).address(), + )); + + let immutables = vec![ + Bytes::from(new_value.abi_encode()), + Bytes::from(new_address.abi_encode()), + ]; + + unwrap_response::<()>( + node.eth_rpc(EthRequest::SetImmutableStorageAt( + contract_address, + immutables, + )) + .await + .unwrap(), + ) + .unwrap(); + + // Verify new immutable value + let call_tx = TransactionRequest::default() + .from(alith_addr) + .to(contract_address) + .input(TransactionInput::both( + ImmutableStorage::getImmutableValueCall {}.abi_encode().into(), + )); + let result = unwrap_response::( + node.eth_rpc(EthRequest::EthCall(call_tx.into(), None, None, None)).await.unwrap(), + ) + .unwrap(); + assert_eq!( + ImmutableStorage::getImmutableValueCall::abi_decode_returns(&result.0).unwrap(), + new_value + ); + + // Verify new immutable address + let call_tx = TransactionRequest::default() + .from(alith_addr) + .to(contract_address) + .input(TransactionInput::both( + ImmutableStorage::getImmutableAddressCall {}.abi_encode().into(), + )); + let result = unwrap_response::( + node.eth_rpc(EthRequest::EthCall(call_tx.into(), None, None, None)).await.unwrap(), + ) + .unwrap(); + assert_eq!( + ImmutableStorage::getImmutableAddressCall::abi_decode_returns(&result.0).unwrap(), + new_address + ); +} diff --git a/crates/anvil-polkadot/tests/it/utils.rs b/crates/anvil-polkadot/tests/it/utils.rs index 0f220afebc4f7..a1eb4922180e9 100644 --- a/crates/anvil-polkadot/tests/it/utils.rs +++ b/crates/anvil-polkadot/tests/it/utils.rs @@ -461,22 +461,36 @@ pub struct ContractCode { pub runtime: Option>, } -pub fn get_contract_code(name: &str) -> ContractCode { +fn load_contract_json(name: &str) -> Value { let contract_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("test-data/{name}.json")); - let contract_json: Value = serde_json::from_reader(std::io::BufReader::new( + serde_json::from_reader(std::io::BufReader::new( std::fs::File::open(contract_path).unwrap(), )) - .unwrap(); + .unwrap() +} + +fn decode_hex_field(json: &Value, field: &str) -> Option> { + json.get(field) + .and_then(|v| v.as_str()) + .map(|s| hex::decode(s).unwrap()) +} + +pub fn get_contract_code(name: &str) -> ContractCode { + let contract_json = load_contract_json(name); - let init = hex::decode(contract_json.get("bin").unwrap().as_str().unwrap()).unwrap(); - let runtime = - contract_json.get("bin-runtime").map(|code| hex::decode(code.as_str().unwrap()).unwrap()); + let init = decode_hex_field(&contract_json, "bin").expect("missing 'bin' field"); + let runtime = decode_hex_field(&contract_json, "bin-runtime"); ContractCode { init, runtime } } +pub fn get_contract_pvm_code(name: &str) -> Vec { + let contract_json = load_contract_json(name); + decode_hex_field(&contract_json, "bin-pvm").expect("missing 'bin-pvm' field") +} + /// Gets contract code with constructor arguments encoded and appended to bytecode. /// Loads the ABI from the contract JSON file and encodes the constructor arguments. pub fn get_contract_code_with_args(name: &str, constructor_args: Vec) -> ContractCode { diff --git a/crates/anvil/core/src/eth/mod.rs b/crates/anvil/core/src/eth/mod.rs index 1cfc62ba238a9..b717f665b152c 100644 --- a/crates/anvil/core/src/eth/mod.rs +++ b/crates/anvil/core/src/eth/mod.rs @@ -428,6 +428,14 @@ pub enum EthRequest { B256, ), + /// Sets the immutable data associated with a contract address + #[serde(rename = "anvil_setImmutableStorageAt")] + SetImmutableStorageAt( + Address, + /// immutable values (each as ABI-encoded bytes) + Vec, + ), + /// Sets the coinbase address #[serde(rename = "anvil_setCoinbase", alias = "hardhat_setCoinbase", with = "sequence")] SetCoinbase(Address), diff --git a/crates/anvil/src/eth/api.rs b/crates/anvil/src/eth/api.rs index 06aa13dca1b02..b869601b5ff32 100644 --- a/crates/anvil/src/eth/api.rs +++ b/crates/anvil/src/eth/api.rs @@ -392,6 +392,11 @@ impl EthApi { EthRequest::SetStorageAt(addr, slot, val) => { self.anvil_set_storage_at(addr, slot, val).await.to_rpc_result() } + EthRequest::SetImmutableStorageAt(_, _) => { + return ResponseResult::Error(RpcError::invalid_params( + "anvil_setImmutableStorageAt is not supported on EVM anvil", + )); + } EthRequest::SetCoinbase(addr) => self.anvil_set_coinbase(addr).await.to_rpc_result(), EthRequest::EthCoinbase(()) => self.author().to_rpc_result(), EthRequest::SetChainId(id) => self.anvil_set_chain_id(id).await.to_rpc_result(),