diff --git a/Cargo.lock b/Cargo.lock index e693930b78..4d600a1b5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6074,6 +6074,7 @@ dependencies = [ "pallet-evm-precompile-bn128", "pallet-evm-precompile-dapp-staking-v3", "pallet-evm-precompile-dispatch", + "pallet-evm-precompile-dispatch-lockdrop", "pallet-evm-precompile-ed25519", "pallet-evm-precompile-modexp", "pallet-evm-precompile-sha3fips", @@ -8035,6 +8036,32 @@ dependencies = [ "pallet-evm", ] +[[package]] +name = "pallet-evm-precompile-dispatch-lockdrop" +version = "0.1.0" +dependencies = [ + "astar-primitives", + "ethers", + "fp-evm", + "frame-support", + "frame-system", + "hex-literal", + "libsecp256k1", + "log", + "pallet-balances", + "pallet-evm", + "pallet-evm-precompile-dispatch", + "pallet-timestamp", + "pallet-utility", + "parity-scale-codec", + "precompile-utils", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-evm-precompile-ed25519" version = "2.0.0-dev" @@ -13242,6 +13269,7 @@ dependencies = [ "pallet-evm-precompile-bn128", "pallet-evm-precompile-dapp-staking-v3", "pallet-evm-precompile-dispatch", + "pallet-evm-precompile-dispatch-lockdrop", "pallet-evm-precompile-ed25519", "pallet-evm-precompile-modexp", "pallet-evm-precompile-sha3fips", diff --git a/Cargo.toml b/Cargo.toml index 30f78873e0..bd32202d60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -299,6 +299,7 @@ pallet-evm-precompile-xvm = { path = "./precompiles/xvm", default-features = fal pallet-evm-precompile-dapps-staking = { path = "./precompiles/dapps-staking", default-features = false } pallet-evm-precompile-dapp-staking-v3 = { path = "./precompiles/dapp-staking-v3", default-features = false } pallet-evm-precompile-unified-accounts = { path = "./precompiles/unified-accounts", default-features = false } +pallet-evm-precompile-dispatch-lockdrop = { path = "./precompiles/dispatch-lockdrop", default-features = false } pallet-chain-extension-xvm = { path = "./chain-extensions/xvm", default-features = false } pallet-chain-extension-assets = { path = "./chain-extensions/pallet-assets", default-features = false } diff --git a/precompiles/dispatch-lockdrop/Cargo.toml b/precompiles/dispatch-lockdrop/Cargo.toml new file mode 100644 index 0000000000..b2ca7f5e20 --- /dev/null +++ b/precompiles/dispatch-lockdrop/Cargo.toml @@ -0,0 +1,58 @@ +[package] +name = "pallet-evm-precompile-dispatch-lockdrop" +description = "Evm Precompile to dispatch calls for lockdrop accounts" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +fp-evm = { workspace = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +hex-literal = { workspace = true } +libsecp256k1 = { workspace = true, features = ["hmac", "static-context"] } +log = { workspace = true } +pallet-evm = { workspace = true } +pallet-evm-precompile-dispatch = { workspace = true } +parity-scale-codec = { workspace = true } +precompile-utils = { workspace = true } +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +[dev-dependencies] +astar-primitives = { workspace = true } +ethers = { workspace = true } +frame-system = { workspace = true } +pallet-balances = { workspace = true } +pallet-timestamp = { workspace = true } +pallet-utility = { workspace = true } +precompile-utils = { workspace = true, features = ["testing"] } +scale-info = { workspace = true } +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +[features] +default = ["std"] +std = [ + "log/std", + "libsecp256k1/std", + "parity-scale-codec/std", + "scale-info/std", + "sp-std/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "frame-support/std", + "frame-system/std", + "astar-primitives/std", + "precompile-utils/std", + "pallet-evm/std", + "pallet-balances/std", + "pallet-timestamp/std", +] diff --git a/precompiles/dispatch-lockdrop/DispatchLockdrop.sol b/precompiles/dispatch-lockdrop/DispatchLockdrop.sol new file mode 100644 index 0000000000..f464b5c3da --- /dev/null +++ b/precompiles/dispatch-lockdrop/DispatchLockdrop.sol @@ -0,0 +1,20 @@ +pragma solidity ^0.8.0; + +/** + * @title Dispatch Lockdrop calls interface. + */ + +/// Interface to dispatch lockdrop calls precompiled contract +/// Pre-deployed at the address 0x0000000000000000000000000000000000005007 +interface RescueLockdrop { + /** + * @dev Dispatch lockdrop call + * @param call - SCALE-encoded call arguments + * @param pubkey - full ECDSA pubkey 64 bytes + * @return boolean confirming whether the call got successfully dispatched + */ + function dispatch_lockdrop_call( + bytes calldata call, + bytes calldata pubkey + ) external returns (bool); +} \ No newline at end of file diff --git a/precompiles/dispatch-lockdrop/src/lib.rs b/precompiles/dispatch-lockdrop/src/lib.rs new file mode 100644 index 0000000000..ee38bd0938 --- /dev/null +++ b/precompiles/dispatch-lockdrop/src/lib.rs @@ -0,0 +1,130 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use core::marker::PhantomData; +use fp_evm::PrecompileHandle; +use frame_support::pallet_prelude::IsType; +use frame_support::weights::Weight; +use frame_support::{codec::DecodeLimit as _, traits::Get}; +use frame_support::{ + dispatch::{Dispatchable, GetDispatchInfo, PostDispatchInfo}, + traits::ConstU32, +}; +use frame_system::Config; +use pallet_evm::GasWeightMapping; +use pallet_evm_precompile_dispatch::DispatchValidateT; +use precompile_utils::prelude::{revert, BoundedBytes, RuntimeHelper, UnboundedBytes}; +use precompile_utils::EvmResult; +use sp_core::{crypto::AccountId32, H160, H256}; +use sp_io::hashing::keccak_256; +use sp_std::vec::Vec; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +pub const LOG_TARGET: &str = "precompile::dispatch-lockdrop"; + +// ECDSA PublicKey +type ECDSAPublic = ConstU32<64>; + +// `DecodeLimit` specifies the max depth a call can use when decoding, as unbounded depth +// can be used to overflow the stack. +// Default value is 8, which is the same as in XCM call decoding. +pub struct DispatchLockdrop>( + PhantomData<(Runtime, DispatchValidator, DecodeLimit)>, +); + +#[precompile_utils::precompile] +impl + DispatchLockdrop +where + Runtime: pallet_evm::Config, + ::RuntimeOrigin: From>, + Runtime::RuntimeCall: Dispatchable + GetDispatchInfo, + ::AccountId: IsType, + ::AccountId: From<[u8; 32]>, + DispatchValidator: + DispatchValidateT<::AccountId, ::RuntimeCall>, + DecodeLimit: Get, +{ + #[precompile::public("dispatch_lockdrop_call(bytes,bytes)")] + fn dispatch_lockdrop_call( + handle: &mut impl PrecompileHandle, + call: UnboundedBytes, + pubkey: BoundedBytes, + ) -> EvmResult { + log::trace!( + target: LOG_TARGET, + "raw arguments: call: {:?}, pubkey: {:?}", + call, + pubkey + ); + + let caller: H160 = handle.context().caller.into(); + let input: Vec = call.into(); + + // Record a fixed amount of weight to ensure there is no free execution + handle.record_cost(Runtime::GasWeightMapping::weight_to_gas( + Weight::from_parts(1_000_000_000u64, 0), + ))?; + + // Ensure that the caller matches the public key + if caller != Self::get_evm_address_from_pubkey(pubkey.as_bytes()) { + let message: &str = "caller does not match the public key"; + log::trace!(target: LOG_TARGET, "{}", message); + return Err(revert(message)); + } + + // Derive the account id from the public key + let origin = Self::get_account_id_from_pubkey(pubkey.as_bytes()) + .ok_or(revert("could not derive AccountId from pubkey"))?; + + // Decode the call + let call = Runtime::RuntimeCall::decode_with_depth_limit(DecodeLimit::get(), &mut &*input) + .map_err(|_| revert("could not decode call"))?; + + // Validate the call - ensure that the call is allowed in filter + DispatchValidator::validate_before_dispatch(&origin, &call) + .map_or_else(|| Ok(()), |_| Err(revert("invalid Call")))?; + + // Dispatch the call and handle the cost + RuntimeHelper::::try_dispatch::( + handle, + Some(origin).into(), + call, + )?; + + Ok(true) + } + + fn get_account_id_from_pubkey(pubkey: &[u8]) -> Option<::AccountId> { + libsecp256k1::PublicKey::parse_slice(pubkey, None) + .map(|k| sp_io::hashing::blake2_256(k.serialize_compressed().as_ref()).into()) + .ok() + } + + fn get_evm_address_from_pubkey(pubkey: &[u8]) -> H160 { + H160::from(H256::from_slice(&keccak_256(pubkey))) + } +} diff --git a/precompiles/dispatch-lockdrop/src/mock.rs b/precompiles/dispatch-lockdrop/src/mock.rs new file mode 100644 index 0000000000..e830752153 --- /dev/null +++ b/precompiles/dispatch-lockdrop/src/mock.rs @@ -0,0 +1,243 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use super::*; +use std::marker::PhantomData; + +use fp_evm::{IsPrecompileResult, Precompile}; +use frame_support::{construct_runtime, parameter_types, traits::ConstU64, weights::Weight}; +pub use pallet_evm::{ + AddressMapping, EnsureAddressNever, EnsureAddressRoot, PrecompileResult, PrecompileSet, +}; +use sp_core::{keccak_256, H160, H256}; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, ConstU32, IdentityLookup}, + AccountId32, +}; + +use frame_support::traits::Contains; + +use astar_primitives::precompiles::DispatchFilterValidate; +pub type AccountId = AccountId32; +pub type Balance = u128; +pub type BlockNumber = u64; +pub type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +pub type Block = frame_system::mocking::MockBlock; +pub const PRECOMPILE_ADDRESS: H160 = H160::repeat_byte(0x7B); + +pub const ONE: u128 = 1_000_000_000_000_000_000; +pub const ALICE: AccountId32 = AccountId32::new([1u8; 32]); +pub const DUMMY: AccountId32 = AccountId32::new([2u8; 32]); + +pub fn alice_secret() -> libsecp256k1::SecretKey { + libsecp256k1::SecretKey::parse(&keccak_256(b"Alice")).unwrap() +} + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0)); +} + +impl frame_system::Config for TestRuntime { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type RuntimeCall = RuntimeCall; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId32; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +pub struct WhitelistedCalls; + +impl Contains for WhitelistedCalls { + fn contains(call: &RuntimeCall) -> bool { + match call { + RuntimeCall::Balances(pallet_balances::Call::transfer { .. }) => true, + RuntimeCall::System(frame_system::Call::remark { .. }) => true, + RuntimeCall::Utility(_) => true, + _ => false, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct TestPrecompileSet(PhantomData); + +impl PrecompileSet for TestPrecompileSet +where + R: pallet_evm::Config, + DispatchLockdrop>: Precompile, +{ + fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { + match handle.code_address() { + a if a == PRECOMPILE_ADDRESS => Some(DispatchLockdrop::< + R, + DispatchFilterValidate, + >::execute(handle)), + _ => None, + } + } + + fn is_precompile(&self, address: H160, _gas: u64) -> IsPrecompileResult { + IsPrecompileResult::Answer { + is_precompile: address == PRECOMPILE_ADDRESS, + extra_cost: 0, + } + } +} + +parameter_types! { + pub const ExistentialDeposit: u128 = 1; +} + +impl pallet_balances::Config for TestRuntime { + type MaxReserves = (); + type ReserveIdentifier = (); + type MaxLocks = (); + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type HoldIdentifier = (); + type FreezeIdentifier = (); + type MaxHolds = ConstU32<0>; + type MaxFreezes = ConstU32<0>; +} + +parameter_types! { + pub const MinimumPeriod: u64 = 5; +} + +impl pallet_timestamp::Config for TestRuntime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +parameter_types! { + pub const PrecompilesValue: TestPrecompileSet = + TestPrecompileSet(PhantomData); + pub WeightPerGas: Weight = Weight::from_parts(1, 0); +} + +pub type PrecompileCall = DispatchLockdropCall< + TestRuntime, + DispatchFilterValidate<::RuntimeCall, WhitelistedCalls>, + ConstU32<8>, +>; + +pub struct AddressMapper; +impl AddressMapping for AddressMapper { + fn into_account_id(account: H160) -> astar_primitives::AccountId { + let mut account_id = [0u8; 32]; + account_id[0..20].clone_from_slice(&account.as_bytes()); + + account_id + .try_into() + .expect("H160 is 20 bytes long so it must fit into 32 bytes; QED") + } +} + +impl pallet_evm::Config for TestRuntime { + type FeeCalculator = (); + type GasWeightMapping = pallet_evm::FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type CallOrigin = EnsureAddressRoot; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = AddressMapper; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type Runner = pallet_evm::runner::stack::Runner; + type PrecompilesType = TestPrecompileSet; + type PrecompilesValue = PrecompilesValue; + type Timestamp = Timestamp; + type ChainId = ChainId; + type OnChargeTransaction = (); + type BlockGasLimit = (); + type BlockHashMapping = pallet_evm::SubstrateBlockHashMapping; + type FindAuthor = (); + type OnCreate = (); + type WeightInfo = (); + type GasLimitPovSizeRatio = ConstU64<4>; +} + +impl pallet_utility::Config for TestRuntime { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type PalletsOrigin = OriginCaller; + type WeightInfo = (); +} + +parameter_types! { + // 2 storage items with value size 20 and 32 + pub const AccountMappingStorageFee: u128 = 0; + pub ChainId: u64 = 1024; +} + +// Configure a mock runtime to test the pallet. +construct_runtime!( + pub enum TestRuntime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Evm: pallet_evm, + Balances : pallet_balances, + Timestamp: pallet_timestamp, + Utility: pallet_utility, + } +); + +#[derive(Default)] +pub(crate) struct ExtBuilder; + +impl ExtBuilder { + pub(crate) fn build(self) -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::default() + .build_storage::() + .expect("Frame system builds valid default genesis config"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} diff --git a/precompiles/dispatch-lockdrop/src/tests.rs b/precompiles/dispatch-lockdrop/src/tests.rs new file mode 100644 index 0000000000..5a9cd53624 --- /dev/null +++ b/precompiles/dispatch-lockdrop/src/tests.rs @@ -0,0 +1,323 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use core::str::from_utf8; +use frame_support::dispatch::GetDispatchInfo; +use frame_support::traits::Currency; +use libsecp256k1::PublicKeyFormat; +use sp_core::crypto::{AccountId32, Ss58Codec}; + +use crate::mock::*; + +use astar_primitives::evm::EvmAddress; +use hex_literal::hex; +use parity_scale_codec::Encode; +use precompile_utils::testing::*; +use sp_core::{ecdsa, Pair}; + +fn precompiles() -> TestPrecompileSet { + PrecompilesValue::get() +} + +#[test] +fn dispatch_calls_on_behalf_of_lockdrop_works() { + ExtBuilder::default().build().execute_with(|| { + // Transfer balance to Alice + let call = RuntimeCall::Balances(pallet_balances::Call::transfer { + dest: ALICE, + value: 15 * ONE, + }); + // Sanity check - Alice holds no Balance + assert_eq!(Balances::free_balance(ALICE), 0); + + // Get Alice EVM address based on the Public Key + let alice_eth = crate::tests::eth_address(&alice_secret()); + // Get derived AccountId from the Blake2b hash of the compressed ECDSA Public key + let account_id = account_id(&alice_secret()); + // Fund this account (fund the lockdrop account) + let _ = Balances::deposit_creating(&account_id, ONE * 20); + // Get the full 64 bytes ECDSA Public key + let pubkey = crate::tests::public_key_full(&alice_secret()); + + precompiles() + .prepare_test( + alice_eth, + PRECOMPILE_ADDRESS, + PrecompileCall::dispatch_lockdrop_call { + call: call.encode().into(), + pubkey: pubkey.into(), + }, + ) + .expect_no_logs() + .execute_returns(true); + + // Get Balance of ALICE in pallet balances + assert_eq!(Balances::free_balance(ALICE), 15 * ONE); + }); +} + +#[test] +fn proper_gas_is_charged() { + ExtBuilder::default().build().execute_with(|| { + let call = RuntimeCall::Balances(pallet_balances::Call::transfer { + dest: ALICE, + value: 15 * ONE, + }); + + // Dispatch a call and ensure gas is charged properly + // Expected gas is the constant weight of 1_000_000_000 and the weight of the call + // In mock one unit of ref_time us charged 1 + let expected_gas = 1_000_000_000u64 + call.get_dispatch_info().weight.ref_time(); + + // Get Alice EVM address based on the Public Key + let alice_eth = crate::tests::eth_address(&alice_secret()); + // Get derived AccountId from the Blake2b hash of the compressed ECDSA Public key + let account_id = account_id(&alice_secret()); + // Fund this account (fund the lockdrop account) + let _ = Balances::deposit_creating(&account_id, ONE * 20); + // Get the full 64 bytes ECDSA Public key + let pubkey = crate::tests::public_key_full(&alice_secret()); + + precompiles() + .prepare_test( + alice_eth, + PRECOMPILE_ADDRESS, + PrecompileCall::dispatch_lockdrop_call { + call: call.encode().into(), + pubkey: pubkey.into(), + }, + ) + .expect_cost(expected_gas) + .execute_returns(true); + }); +} + +#[test] +fn pubkey_does_not_match_caller_address() { + ExtBuilder::default().build().execute_with(|| { + // Transfer balance to Alice + let call = RuntimeCall::Balances(pallet_balances::Call::transfer { + dest: ALICE, + value: 15 * ONE, + }); + // Sanity check - Alice holds no Balance + assert_eq!(Balances::free_balance(ALICE), 0); + + // Get Alice EVM address based on the Public Key + let alice_eth = crate::tests::eth_address(&alice_secret()); + // Dummy AccountId to sign the EIP712 payload with + let account_id = DUMMY; + // Fund this dummy account + let _ = Balances::deposit_creating(&account_id, ONE * 20); + // Create a dummy pubkey + let pubkey = [10u8; 64]; + + precompiles() + .prepare_test( + alice_eth, + PRECOMPILE_ADDRESS, + PrecompileCall::dispatch_lockdrop_call { + call: call.encode().into(), + pubkey: pubkey.into(), + }, + ) + .expect_no_logs() + .execute_reverts(|output| output == b"caller does not match the public key"); + + // Get Balance of ALICE in pallet balances and ensure it has not received any funds + assert_eq!(Balances::free_balance(ALICE), 0); + }); +} + +#[test] +fn pubkey_derive_to_proper_ss58() { + ExtBuilder::default().build().execute_with(|| { + // Transfer balance to Alice + let call = RuntimeCall::Balances(pallet_balances::Call::transfer { + dest: ALICE, + value: 15 * ONE, + }); + // Sanity check - Alice holds no Balance + assert_eq!(Balances::free_balance(ALICE), 0); + + // The seed "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + // should resolve to the SS58 address "5EGynCAEvv8NLeHx8vDMvb8hTcEcMYUMWCDQEEncNEfNWB2W" + // If we fund this account, it will be able to dispatch the Transfer call + let pair = ecdsa::Pair::from_seed(&hex!( + "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + )); + let pubkey = crate::tests::public_key_full_from_compressed(pair.public().as_ref()); + let alice_eth = EvmAddress::from_slice(&sp_io::hashing::keccak_256(&pubkey)[12..]); + let account_id = + AccountId::from_ss58check("5EGynCAEvv8NLeHx8vDMvb8hTcEcMYUMWCDQEEncNEfNWB2W").unwrap(); + // Fund this account + let _ = Balances::deposit_creating(&account_id, ONE * 20); + + precompiles() + .prepare_test( + alice_eth, + PRECOMPILE_ADDRESS, + PrecompileCall::dispatch_lockdrop_call { + call: call.encode().into(), + pubkey: pubkey.into(), + }, + ) + .expect_no_logs() + .execute_returns(true); + + // Assert that the call (Transfer) was successful + assert_eq!(Balances::free_balance(ALICE), 15 * ONE); + }); +} + +#[test] +fn decode_limit_too_high() { + ExtBuilder::default().build().execute_with(|| { + let mut nested_call = + RuntimeCall::System(frame_system::Call::remark { remark: Vec::new() }); + + // More than 8 depth + for _ in 0..9 { + nested_call = RuntimeCall::Utility(pallet_utility::Call::as_derivative { + index: 0, + call: Box::new(nested_call), + }); + } + + // Get Alice EVM address based on the Public Key + let alice_eth = crate::tests::eth_address(&alice_secret()); + // Get derived AccountId from the Blake2b hash of the compressed ECDSA Public key + let account_id = account_id(&alice_secret()); + // Fund this account (fund the lockdrop account) + let _ = Balances::deposit_creating(&account_id, ONE * 20); + // Get the full 64 bytes ECDSA Public key + let pubkey = crate::tests::public_key_full(&alice_secret()); + + precompiles() + .prepare_test( + alice_eth, + PRECOMPILE_ADDRESS, + PrecompileCall::dispatch_lockdrop_call { + call: nested_call.encode().into(), + pubkey: pubkey.into(), + }, + ) + .expect_no_logs() + .execute_reverts(|output| from_utf8(output).unwrap().contains("could not decode call")); + }); +} + +#[test] +fn decode_limit_ok() { + ExtBuilder::default().build().execute_with(|| { + let mut nested_call = + RuntimeCall::System(frame_system::Call::remark { remark: Vec::new() }); + + for _ in 0..8 { + nested_call = RuntimeCall::Utility(pallet_utility::Call::as_derivative { + index: 0, + call: Box::new(nested_call), + }); + } + + // Get Alice EVM address based on the Public Key + let alice_eth = crate::tests::eth_address(&alice_secret()); + // Get derived AccountId from the Blake2b hash of the compressed ECDSA Public key + let account_id = account_id(&alice_secret()); + // Fund this account (fund the lockdrop account) + let _ = Balances::deposit_creating(&account_id, ONE * 20); + // Get the full 64 bytes ECDSA Public key + let pubkey = crate::tests::public_key_full(&alice_secret()); + + precompiles() + .prepare_test( + alice_eth, + PRECOMPILE_ADDRESS, + PrecompileCall::dispatch_lockdrop_call { + call: nested_call.encode().into(), + pubkey: pubkey.into(), + }, + ) + .expect_no_logs() + .execute_returns(true); + }); +} + +#[test] +fn only_whitelisted_calls_can_be_dispatched() { + ExtBuilder::default().build().execute_with(|| { + // Transfer balance to Alice + let call = RuntimeCall::System(frame_system::Call::remark_with_event { + remark: b"Hello World".to_vec(), + }); + + // Get Alice EVM address based on the Public Key + let alice_eth = crate::tests::eth_address(&alice_secret()); + // Get derived AccountId from the Blake2b hash of the compressed ECDSA Public key + let account_id = crate::tests::account_id(&alice_secret()); + // Fund this account (fund the lockdrop account) + let _ = Balances::deposit_creating(&account_id, ONE * 20); + // Get the full 64 bytes ECDSA Public key + let pubkey = crate::tests::public_key_full(&alice_secret()); + + precompiles() + .prepare_test( + alice_eth, + PRECOMPILE_ADDRESS, + PrecompileCall::dispatch_lockdrop_call { + call: call.encode().into(), + pubkey: pubkey.into(), + }, + ) + .expect_no_logs() + .execute_reverts(|output| output == b"invalid Call"); + }); +} + +fn account_id(secret: &libsecp256k1::SecretKey) -> AccountId32 { + sp_io::hashing::blake2_256( + ecdsa::Public::from_full( + &libsecp256k1::PublicKey::from_secret_key(secret).serialize()[1..65], + ) + .unwrap() + .as_ref(), + ) + .into() +} + +fn eth_address(secret: &libsecp256k1::SecretKey) -> EvmAddress { + EvmAddress::from_slice( + &sp_io::hashing::keccak_256( + &libsecp256k1::PublicKey::from_secret_key(secret).serialize()[1..65], + )[12..], + ) +} + +fn public_key_full_from_compressed(pubkey: &[u8]) -> [u8; 64] { + libsecp256k1::PublicKey::parse_slice(pubkey, Some(PublicKeyFormat::Compressed)) + .unwrap() + .serialize()[1..65] + .try_into() + .unwrap() +} + +fn public_key_full(secret: &libsecp256k1::SecretKey) -> [u8; 64] { + libsecp256k1::PublicKey::from_secret_key(secret).serialize()[1..65] + .try_into() + .unwrap() +} diff --git a/runtime/local/Cargo.toml b/runtime/local/Cargo.toml index 9199907389..eee3b06e59 100644 --- a/runtime/local/Cargo.toml +++ b/runtime/local/Cargo.toml @@ -76,6 +76,7 @@ pallet-dapps-staking = { workspace = true } pallet-dynamic-evm-base-fee = { workspace = true } pallet-evm-precompile-assets-erc20 = { workspace = true } pallet-evm-precompile-dapp-staking-v3 = { workspace = true } +pallet-evm-precompile-dispatch-lockdrop = { workspace = true } pallet-evm-precompile-sr25519 = { workspace = true } pallet-evm-precompile-substrate-ecdsa = { workspace = true } pallet-evm-precompile-unified-accounts = { workspace = true } @@ -147,6 +148,7 @@ std = [ "pallet-evm-precompile-substrate-ecdsa/std", "pallet-evm-precompile-unified-accounts/std", "pallet-evm-precompile-xvm/std", + "pallet-evm-precompile-dispatch-lockdrop/std", "pallet-grandpa/std", "pallet-insecure-randomness-collective-flip/std", "pallet-preimage/std", diff --git a/runtime/local/src/precompiles.rs b/runtime/local/src/precompiles.rs index d207068f4b..d2a299127d 100644 --- a/runtime/local/src/precompiles.rs +++ b/runtime/local/src/precompiles.rs @@ -20,12 +20,14 @@ use crate::{RuntimeCall, UnifiedAccounts}; use astar_primitives::precompiles::DispatchFilterValidate; +use frame_support::traits::ConstU32; use frame_support::{parameter_types, traits::Contains}; use pallet_evm_precompile_assets_erc20::Erc20AssetsPrecompileSet; use pallet_evm_precompile_blake2::Blake2F; use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing}; use pallet_evm_precompile_dapp_staking_v3::DappStakingV3Precompile; use pallet_evm_precompile_dispatch::Dispatch; +use pallet_evm_precompile_dispatch_lockdrop::DispatchLockdrop; use pallet_evm_precompile_ed25519::Ed25519Verify; use pallet_evm_precompile_modexp::Modexp; use pallet_evm_precompile_sha3fips::Sha3FIPS256; @@ -64,6 +66,30 @@ impl Contains for WhitelistedCalls { } } } + +/// Filter that only allows whitelisted runtime call to pass through dispatch-lockdrop precompile +pub struct WhitelistedLockdropCalls; + +impl Contains for WhitelistedLockdropCalls { + fn contains(t: &RuntimeCall) -> bool { + match t { + RuntimeCall::Utility(pallet_utility::Call::batch { calls }) + | RuntimeCall::Utility(pallet_utility::Call::batch_all { calls }) => calls + .iter() + .all(|call| WhitelistedLockdropCalls::contains(call)), + RuntimeCall::DappStaking(pallet_dapp_staking_v3::Call::unbond_and_unstake { + .. + }) => true, + RuntimeCall::DappStaking(pallet_dapp_staking_v3::Call::withdraw_unbonded { + .. + }) => true, + RuntimeCall::Balances(pallet_balances::Call::transfer { .. }) => true, + RuntimeCall::Assets(pallet_assets::Call::transfer { .. }) => true, + _ => false, + } + } +} + /// The PrecompileSet installed in the Local runtime. #[precompile_utils::precompile_name_from_address] pub type LocalPrecompilesSetAt = ( @@ -116,6 +142,16 @@ pub type LocalPrecompilesSetAt = ( UnifiedAccountsPrecompile, (CallableByContract, CallableByPrecompile), >, + PrecompileAt< + AddressU64<20487>, + DispatchLockdrop< + R, + DispatchFilterValidate, + ConstU32<8>, + >, + // Not callable from smart contract nor precompiled, only EOA accounts + (), + >, ); pub type LocalPrecompiles = PrecompileSetBuilder< @@ -123,8 +159,8 @@ pub type LocalPrecompiles = PrecompileSetBuilder< ( // Skip precompiles if out of range. PrecompilesInRangeInclusive< - // We take range as last precompile index, UPDATE this once new prcompile is added - (AddressU64<1>, AddressU64<20486>), + // We take range as last precompile index, UPDATE this once new precompile is added + (AddressU64<1>, AddressU64<20487>), LocalPrecompilesSetAt, >, // Prefixed precompile sets (XC20) diff --git a/runtime/shibuya/Cargo.toml b/runtime/shibuya/Cargo.toml index 4eafb194fa..ed86ae74d8 100644 --- a/runtime/shibuya/Cargo.toml +++ b/runtime/shibuya/Cargo.toml @@ -106,6 +106,7 @@ pallet-dynamic-evm-base-fee = { workspace = true } pallet-ethereum-checked = { workspace = true } pallet-evm-precompile-assets-erc20 = { workspace = true } pallet-evm-precompile-dapp-staking-v3 = { workspace = true } +pallet-evm-precompile-dispatch-lockdrop = { workspace = true } pallet-evm-precompile-sr25519 = { workspace = true } pallet-evm-precompile-substrate-ecdsa = { workspace = true } pallet-evm-precompile-unified-accounts = { workspace = true } @@ -191,6 +192,7 @@ std = [ "pallet-evm-precompile-xcm/std", "pallet-evm-precompile-xvm/std", "pallet-evm-precompile-unified-accounts/std", + "pallet-evm-precompile-dispatch-lockdrop/std", "pallet-dapp-staking-v3/std", "dapp-staking-v3-runtime-api/std", "pallet-inflation/std", diff --git a/runtime/shibuya/src/precompiles.rs b/runtime/shibuya/src/precompiles.rs index c9f34a9a9d..8153f03146 100644 --- a/runtime/shibuya/src/precompiles.rs +++ b/runtime/shibuya/src/precompiles.rs @@ -20,12 +20,14 @@ use crate::{RuntimeCall, UnifiedAccounts, Xvm}; use astar_primitives::precompiles::DispatchFilterValidate; +use frame_support::traits::ConstU32; use frame_support::{parameter_types, traits::Contains}; use pallet_evm_precompile_assets_erc20::Erc20AssetsPrecompileSet; use pallet_evm_precompile_blake2::Blake2F; use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing}; use pallet_evm_precompile_dapp_staking_v3::DappStakingV3Precompile; use pallet_evm_precompile_dispatch::Dispatch; +use pallet_evm_precompile_dispatch_lockdrop::DispatchLockdrop; use pallet_evm_precompile_ed25519::Ed25519Verify; use pallet_evm_precompile_modexp::Modexp; use pallet_evm_precompile_sha3fips::Sha3FIPS256; @@ -65,6 +67,30 @@ impl Contains for WhitelistedCalls { } } } + +/// Filter that only allows whitelisted runtime call to pass through dispatch-lockdrop precompile +pub struct WhitelistedLockdropCalls; + +impl Contains for WhitelistedLockdropCalls { + fn contains(t: &RuntimeCall) -> bool { + match t { + RuntimeCall::Utility(pallet_utility::Call::batch { calls }) + | RuntimeCall::Utility(pallet_utility::Call::batch_all { calls }) => calls + .iter() + .all(|call| WhitelistedLockdropCalls::contains(call)), + RuntimeCall::DappStaking(pallet_dapp_staking_v3::Call::unbond_and_unstake { + .. + }) => true, + RuntimeCall::DappStaking(pallet_dapp_staking_v3::Call::withdraw_unbonded { + .. + }) => true, + RuntimeCall::Balances(pallet_balances::Call::transfer { .. }) => true, + RuntimeCall::Assets(pallet_assets::Call::transfer { .. }) => true, + _ => false, + } + } +} + /// The PrecompileSet installed in the Shibuya runtime. #[precompile_utils::precompile_name_from_address] pub type ShibuyaPrecompilesSetAt = ( @@ -125,6 +151,16 @@ pub type ShibuyaPrecompilesSetAt = ( UnifiedAccountsPrecompile, (CallableByContract, CallableByPrecompile), >, + PrecompileAt< + AddressU64<20487>, + DispatchLockdrop< + R, + DispatchFilterValidate, + ConstU32<8>, + >, + // Not callable from smart contract nor precompiled, only EOA accounts + (), + >, ); pub type ShibuyaPrecompiles = PrecompileSetBuilder<