diff --git a/precompiles/src/lib.rs b/precompiles/src/lib.rs index fe098221e0..f52343b2b6 100644 --- a/precompiles/src/lib.rs +++ b/precompiles/src/lib.rs @@ -165,6 +165,32 @@ where Self(Default::default()) } + /// Read-only and cryptographic precompiles may run in a borrowed frame. + /// Every other address in this set signs as `context.caller`. + fn accepts_foreign_frame(address: H160) -> bool { + address == hash(1) + || address == hash(2) + || address == hash(3) + || address == hash(4) + || address == hash(5) + || address == hash(7) + || address == hash(8) + || address == hash(9) + || address == hash(1024) + || address == hash(1025) + || address == hash(Ed25519Verify::::INDEX) + || address == hash(Sr25519Verify::::INDEX) + || address == hash(MetagraphPrecompile::::INDEX) + || address == hash(UidLookupPrecompile::::INDEX) + || address == hash(StorageQueryPrecompile::::INDEX) + || address == hash(AddressMappingPrecompile::::INDEX) + || address == hash(SchedulerPrecompile::::INDEX) + || address == hash(DrandPrecompile::::INDEX) + || address == hash(TimestampPrecompile::::INDEX) + || address == hash(RuntimeConfigurationPrecompile::::INDEX) + || address == hash(PrecompileRegistry::::INDEX) + } + pub fn used_addresses() -> [H160; 33] { [ hash(1), @@ -247,7 +273,19 @@ where <::Lookup as StaticLookup>::Source: From, { fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { - match handle.code_address() { + let code_address = handle.code_address(); + if !Self::used_addresses().contains(&code_address) { + return None; + } + if !Self::accepts_foreign_frame(code_address) && code_address != handle.context().address { + return Some(Err(PrecompileFailure::Error { + exit_status: ExitError::Other( + "Cannot be called with DELEGATECALL or CALLCODE".into(), + ), + })); + } + + match code_address { // Ethereum precompiles : a if a == hash(1) => Some(ECRecover::execute(handle)), a if a == hash(2) => Some(Sha256::execute(handle)), @@ -381,9 +419,172 @@ fn parse_slice(data: &[u8], from: usize, to: usize) -> Result<&[u8], PrecompileF #[cfg(test)] mod address_and_selector_tests { use super::*; - use crate::mock::{Runtime, selector_u32}; + use crate::mock::{ + AccountId, Runtime, abi_word, execute_precompile, new_test_ext, selector_u32, + }; use alloc::collections::BTreeSet; use codec::Encode; + use fp_evm::{Context, ExitSucceed, PrecompileOutput}; + use precompile_utils::solidity::encode_with_selector; + use precompile_utils::testing::MockHandle; + use sp_core::{H256, Pair, ed25519}; + + const FRAME_ERROR: &str = "Cannot be called with DELEGATECALL or CALLCODE"; + const SHA256_EMPTY: [u8; 32] = [ + 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, + 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, + 0xb8, 0x55, + ]; + + fn execute_with_foreign_frame(code_address: H160, input: Vec) -> Option { + let mut handle = MockHandle::new( + code_address, + Context { + address: H160::from_low_u64_be(0xDEAD), + caller: H160::from_low_u64_be(0xBEEF), + apparent_value: U256::zero(), + }, + ); + handle.input = input; + Precompiles::::new().execute(&mut handle) + } + + fn frame_error() -> Option { + Some(Err(PrecompileFailure::Error { + exit_status: ExitError::Other(FRAME_ERROR.into()), + })) + } + + #[test] + fn precompile_set_rejects_mismatched_frame() { + new_test_ext().execute_with(|| { + let code_address = hash(6); + let caller = H160::from_low_u64_be(0xBEEF); + let frame_address = H160::from_low_u64_be(0xDEAD); + let mut handle = MockHandle::new( + code_address, + Context { + address: frame_address, + caller, + apparent_value: U256::zero(), + }, + ); + + assert_eq!( + Precompiles::::new().execute(&mut handle), + frame_error() + ); + }); + } + + #[test] + fn precompile_set_rejects_foreign_frame_for_signed_dispatch_precompiles() { + new_test_ext().execute_with(|| { + for address in [ + hash(6), + hash(BalanceTransferPrecompile::::INDEX), + hash(StakingPrecompile::::INDEX), + hash(StakingPrecompileV2::::INDEX), + hash(SubnetPrecompile::::INDEX), + hash(NeuronPrecompile::::INDEX), + hash(AlphaPrecompile::::INDEX), + hash(CrowdloanPrecompile::::INDEX), + hash(LeasingPrecompile::::INDEX), + hash(VotingPowerPrecompile::::INDEX), + hash(ProxyPrecompile::::INDEX), + hash(BalancePrecompile::::INDEX), + ] { + assert_eq!( + execute_with_foreign_frame(address, alloc::vec::Vec::new()), + frame_error(), + "signed-dispatch precompile {address:?} must reject a foreign frame" + ); + } + }); + } + + #[test] + fn precompile_set_accepts_matching_frame() { + new_test_ext().execute_with(|| { + let result = execute_precompile( + &Precompiles::::new(), + hash(6), + H160::from_low_u64_be(0xBEEF), + alloc::vec::Vec::new(), + U256::zero(), + ); + assert_ne!(result, frame_error()); + }); + } + + #[test] + fn precompile_set_allows_foreign_frame_for_view_precompile() { + new_test_ext().execute_with(|| { + let result = execute_with_foreign_frame( + hash(TimestampPrecompile::::INDEX), + alloc::vec::Vec::new(), + ); + assert_ne!(result, frame_error()); + assert!(result.is_some()); + }); + } + + #[test] + fn precompile_set_executes_sha256_with_foreign_frame() { + new_test_ext().execute_with(|| { + assert_eq!( + execute_with_foreign_frame(hash(2), alloc::vec::Vec::new()), + Some(Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: SHA256_EMPTY.to_vec(), + })) + ); + }); + } + + #[test] + fn precompile_set_executes_ed25519_verify_with_foreign_frame() { + new_test_ext().execute_with(|| { + let pair = ed25519::Pair::from_seed(&[1u8; 32]); + let message = [7u8; 32]; + let signature = pair.sign(&message); + let input = encode_with_selector( + selector_u32("verify(bytes32,bytes32,bytes32,bytes32)"), + ( + H256::from(message), + H256::from(pair.public().0), + H256::from_slice(&signature.0[..32]), + H256::from_slice(&signature.0[32..]), + ), + ); + + assert_eq!( + execute_with_foreign_frame(hash(Ed25519Verify::::INDEX), input), + Some(Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: abi_word(U256::one()), + })) + ); + }); + } + + #[test] + fn precompile_set_returns_none_for_unknown_address() { + new_test_ext().execute_with(|| { + let unknown = H160::from_low_u64_be(0x1111); + let frame_address = H160::from_low_u64_be(0xDEAD); + let mut handle = MockHandle::new( + unknown, + Context { + address: frame_address, + caller: H160::from_low_u64_be(0xBEEF), + apparent_value: U256::zero(), + }, + ); + + assert_eq!(Precompiles::::new().execute(&mut handle), None); + }); + } #[test] fn precompile_addresses_are_unique_and_new_addresses_are_locked() { diff --git a/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts b/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts index ad2af9bc32..de0e2f2315 100644 --- a/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts +++ b/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts @@ -33,7 +33,6 @@ import { sudoSetLockReductionInterval, tao, waitForFinalizedBlocks, - waitUntilBlockFinalized, } from "../../utils"; const DEPLOYED_BYTECODE_PREFIX = "0x60806040523480156"; @@ -295,29 +294,13 @@ describeSuite({ const alphaInPool = await contractForCall.getContractStake(netuid); expect(alphaInPool).toEqual(BigInt(0)); - const depositAlphaTx = await contractForCall.depositAlpha(netuid, tao(10).toString(), hotkey.publicKey); - const depositReceipt = await depositAlphaTx.wait(); - if (!depositReceipt) throw new Error("Missing depositAlpha receipt"); - expect(depositReceipt.status).toEqual(1); - // Wait for the deposit's own block to finalize rather than a - // fixed block count: when GRANDPA lags best by more than 2 - // blocks, the finalized-state stake reads below see the - // pre-deposit stake and the toBeLessThan assertion flakes. - await waitUntilBlockFinalized(api, depositReceipt.blockNumber); - - const stakeAfterDeposit = await getStake(api, hotkeySs58, walletSs58, netuid); - expect(stakeAfterDeposit).toBeLessThan(stakeBeforeDeposit); - - const contractStake = await getStake(api, hotkeySs58, convertH160ToSS58(contractAddress), netuid); - expect(contractStake).toBeGreaterThan(BigInt(0)); - - const alphaBalanceOnContract = await contractForCall.alphaBalance(ethWallet.address, netuid); - expect(tao(10) - alphaBalanceOnContract).toBeLessThan(BigInt(1000)); - - const stakeFromContract = BigInt( - await stakingPrecompile.getStake(hotkey.publicKey, contractPublicKey, netuid) + await expect( + contractForCall.depositAlpha(netuid, tao(10).toString(), hotkey.publicKey) + ).rejects.toThrow(/user deposit alpha call failed/); + expect(await getStake(api, hotkeySs58, walletSs58, netuid)).toEqual(stakeBeforeDeposit); + expect(await stakingPrecompile.getStake(hotkey.publicKey, contractPublicKey, netuid)).toEqual( + BigInt(0) ); - expect(stakeFromContract).toEqual(await contractForCall.getContractStake(netuid)); }, });