Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
07dd930
WIP: add dispatch_batch_all_evm to pallet dispatcher
F3Joule Apr 11, 2025
838d389
WIP: make wrapper extrinsic do single EVM call
F3Joule Apr 15, 2025
83f9c18
Refactor batch to single evm call wrap
F3Joule May 2, 2025
873d251
Merge remote-tracking branch 'origin/master' into feat/batch-evm
F3Joule May 2, 2025
deea0a8
Merge remote-tracking branch 'origin/master' into feat/batch-evm
F3Joule May 9, 2025
c697c1d
Implement pallet_evm::Call::call fail check in evm::runner
F3Joule May 19, 2025
84d3e8c
Merge remote-tracking branch 'origin/master' into feat/batch-evm
F3Joule May 19, 2025
388bbe8
Revert redundant changes
F3Joule May 19, 2025
27273c2
Add test for dispatch_evm_call
F3Joule May 19, 2025
66c88ec
Change test case not to fail
F3Joule May 19, 2025
6c40500
Add test for NotEvmCall error
F3Joule May 19, 2025
e33d6ee
Fix formatting
F3Joule May 19, 2025
a26795b
Fix tests build
F3Joule May 20, 2025
45db02f
Apply suggestions from code review
F3Joule May 30, 2025
46f65ae
Merge branch 'master' into feat/batch-evm
F3Joule May 30, 2025
51d5cdf
Complete tests after logic changes
F3Joule May 30, 2025
e9c6a4b
Merge remote-tracking branch 'origin/master' into feat/batch-evm
F3Joule Jun 13, 2025
c04a733
Revert unneeded changes after merge
F3Joule Jul 3, 2025
7213fa0
Merge branch 'master' into feat/batch-evm
F3Joule Jul 3, 2025
32546ab
Refactor dispatcher to store evm ExitReason instead of bool
F3Joule Jul 4, 2025
503b693
Fix tests
F3Joule Jul 4, 2025
ab3fa7d
Fix formatting
F3Joule Jul 4, 2025
82d6b26
Merge remote-tracking branch 'origin/master' into feat/batch-evm
F3Joule Jul 7, 2025
6863834
Clean up a code for a more neat look
F3Joule Jul 7, 2025
a416730
Fix code formatting
F3Joule Jul 7, 2025
47df584
Fix test
F3Joule Jul 8, 2025
705da1a
Merge branch 'master' into feat/batch-evm
F3Joule Jul 8, 2025
8f69cb5
Merge branch 'master' into feat/batch-evm
F3Joule Jul 11, 2025
de1d2f6
Add benchmark for dispatch_evm_call
F3Joule Jul 17, 2025
2dc39be
Merge remote-tracking branch 'origin/master' into feat/batch-evm
F3Joule Jul 17, 2025
a9618fa
Make EVM origin guard accept complete and truncated account ids
F3Joule Jul 17, 2025
018e7bd
Merge branch 'master' into feat/batch-evm
F3Joule Jul 22, 2025
cb41a0a
bump crates versions
F3Joule Jul 22, 2025
f653a25
Update pallets weights [ignore benchmarks]
F3Joule Jul 24, 2025
844d02a
Partially revert committed weights
F3Joule Jul 25, 2025
941d92c
Merge remote-tracking branch 'origin/master' into feat/batch-evm
F3Joule Jul 25, 2025
154c05e
Merge branch 'master' into feat/batch-evm
mrq1911 Jul 29, 2025
05e5a3a
Merge branch 'master' into feat/batch-evm
F3Joule Aug 12, 2025
452306a
Fix integration tests after merge
F3Joule Aug 12, 2025
0ecb44e
Merge branch 'master' into feat/batch-evm
mrq1911 Aug 13, 2025
b525d69
runtime 338
mrq1911 Aug 13, 2025
826b23a
lock
mrq1911 Aug 13, 2025
25d0583
Optimize EnsureAddressTruncated
F3Joule Aug 15, 2025
b396f0e
Merge branch 'master' into feat/batch-evm
F3Joule Aug 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 136 additions & 1 deletion integration-tests/src/dispatcher.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::evm::MockHandle;
use crate::polkadot_test_net::*;
use fp_evm::PrecompileSet;
use frame_support::dispatch::{GetDispatchInfo, PostDispatchInfo};
use frame_support::dispatch::{
extract_actual_pays_fee, extract_actual_weight, GetDispatchInfo, Pays, PostDispatchInfo,
};
use frame_support::{assert_err, assert_noop, assert_ok};
use hydradx_runtime::evm::precompiles::HydraDXPrecompiles;
use hydradx_runtime::evm::WethAssetId;
Expand All @@ -14,6 +16,7 @@ use sp_core::Encode;
use sp_core::Get;
use sp_core::{ByteArray, U256};
use sp_runtime::traits::SignedExtension;
use sp_runtime::DispatchErrorWithPostInfo;
use test_utils::last_events;
use xcm_emulator::TestExt;

Expand Down Expand Up @@ -438,3 +441,135 @@ fn dispatch_with_extra_gas_should_charge_extra_gas_when_calls_fail() {
);
});
}

#[test]
fn dispatch_evm_call_should_work_when_evm_call_succeeds() {
TestNet::reset();
Hydra::execute_with(|| {
// Arrange: Deploy a valid contract to interact with
let contract = crate::utils::contracts::deploy_contract("HydraToken", crate::contracts::deployer());
let invalid_target = EvmAddress::from_slice(&[0x11; 20]);

assert_ok!(hydradx_runtime::Tokens::set_balance(
hydradx_runtime::RuntimeOrigin::root(),
evm_account(),
WethAssetId::get(),
1_000_000_000_000_000_000u128,
0
));

// Helper function to create EVM calls with common parameters
let create_evm_call = |target| {
Box::new(RuntimeCall::EVM(pallet_evm::Call::call {
source: evm_address(),
target,
input: hex!["06fdde03"].to_vec(), // name() function selector
value: U256::zero(),
gas_limit: 100_000,
max_fee_per_gas: U256::from(233_460_000),
max_priority_fee_per_gas: None,
nonce: None,
access_list: vec![],
}))
};

// Create test cases with different targets
let call_succeed_returned = create_evm_call(contract);
let call_succeed_stopped = create_evm_call(invalid_target);

// Act: Dispatch the EVM calls
assert_ok!(Dispatcher::dispatch_evm_call(
evm_signed_origin(evm_address()),
call_succeed_returned
));
assert_ok!(Dispatcher::dispatch_evm_call(
evm_signed_origin(evm_address()),
call_succeed_stopped
));

// Assert: LastEvmCallFailed storage is clean
assert_eq!(Dispatcher::last_evm_call_failed(), false);
});
}

#[test]
fn dispatch_evm_call_should_fail_with_invalid_function_selector() {
TestNet::reset();
Hydra::execute_with(|| {
// Arrange
// Verify initial state
assert_eq!(Dispatcher::last_evm_call_failed(), false);

assert_ok!(hydradx_runtime::Tokens::set_balance(
hydradx_runtime::RuntimeOrigin::root(),
evm_account(),
WethAssetId::get(),
1_000_000_000_000_000_000u128,
0
));

// Deploy a contract to test with
let contract = crate::utils::contracts::deploy_contract("HydraToken", crate::contracts::deployer());

// Create an EVM call with an invalid function selector
let call = RuntimeCall::EVM(pallet_evm::Call::call {
source: evm_address(),
target: contract,
input: hex!["12345678"].to_vec(), // Invalid function selector
gas_limit: 100_000,
value: U256::zero(),
max_fee_per_gas: U256::from(233_460_000),
max_priority_fee_per_gas: None,
nonce: None,
access_list: vec![],
});
let call_data = call.get_dispatch_info();
let boxed_call = Box::new(call);

// Act
let result = Dispatcher::dispatch_evm_call(evm_signed_origin(evm_address()), boxed_call);

// Assert
// The dispatch should fail with EvmCallFailed error
assert_eq!(
result,
Err(DispatchErrorWithPostInfo {
post_info: PostDispatchInfo {
actual_weight: Some(extract_actual_weight(&result, &call_data)),
pays_fee: extract_actual_pays_fee(&result, &call_data),
},
error: pallet_dispatcher::Error::<Runtime>::EvmCallFailed.into(),
})
);

// Verify that LastEvmCallFailed storage is cleaned after execution
assert_eq!(Dispatcher::last_evm_call_failed(), false);
});
}

#[test]
fn dispatch_evm_call_should_fail_with_not_evm_call_error() {
TestNet::reset();
Hydra::execute_with(|| {
// Arrange: Create a non-EVM call
let call = RuntimeCall::Currencies(pallet_currencies::Call::transfer {
dest: BOB.into(),
currency_id: 1234,
amount: 100,
});
let boxed_call = Box::new(call.clone());

// Act & Assert: The dispatch should fail with NotEvmCall error
let result = Dispatcher::dispatch_evm_call(evm_signed_origin(evm_address()), boxed_call);
assert_eq!(
result,
Err(DispatchErrorWithPostInfo {
post_info: PostDispatchInfo {
actual_weight: None,
pays_fee: Pays::Yes,
},
error: pallet_dispatcher::Error::<Runtime>::NotEvmCall.into(),
})
);
})
}
10 changes: 6 additions & 4 deletions pallets/dispatcher/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ readme = "README.md"
codec = { workspace = true, features = ["derive", "max-encoded-len"] }
scale-info = { workspace = true }

pallet-evm = { workspace = true }
hydradx-traits = { workspace = true }

# primitives
sp-runtime = { workspace = true }
sp-std = { workspace = true }
Expand All @@ -26,12 +23,17 @@ sp-core = { workspace = true }
frame-support = { workspace = true }
frame-system = { workspace = true }

# HydraDX dependencies
hydradx-traits = { workspace = true }

# EVM dependencies
pallet-evm = { workspace = true }

# Optional imports for benchmarking
frame-benchmarking = { workspace = true, optional = true }

[dev-dependencies]
sp-io = { workspace = true }
hydradx-traits = { workspace = true }
orml-tokens = { workspace = true }
orml-traits = { workspace = true }
test-utils = { workspace = true }
Expand Down
65 changes: 65 additions & 0 deletions pallets/dispatcher/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ mod benchmarking;
pub mod weights;

use frame_support::dispatch::PostDispatchInfo;
use hydradx_traits::evm::MaybeEvmCall;
use pallet_evm::GasWeightMapping;
use sp_runtime::{traits::Dispatchable, DispatchResultWithInfo};
pub use weights::WeightInfo;
Expand Down Expand Up @@ -76,6 +77,9 @@ pub mod pallet {
+ From<frame_system::Call<Self>>
+ Parameter;

/// The trait to check whether RuntimeCall is [pallet_evm::Call::call].
type EvmCallIdentifier: MaybeEvmCall<<Self as Config>::RuntimeCall>;

type TreasuryManagerOrigin: EnsureOrigin<Self::RuntimeOrigin>;
type AaveManagerOrigin: EnsureOrigin<Self::RuntimeOrigin>;

Expand All @@ -101,6 +105,20 @@ pub mod pallet {
#[pallet::getter(fn extra_gas)]
pub type ExtraGas<T: Config> = StorageValue<_, u64, ValueQuery>;

#[pallet::storage]
#[pallet::whitelist_storage]
#[pallet::getter(fn last_evm_call_failed)]
pub type LastEvmCallFailed<T: Config> = StorageValue<_, bool, ValueQuery>;
Comment thread
F3Joule marked this conversation as resolved.
Outdated

#[pallet::error]
pub enum Error<T> {
/// The EVM call execution failed. This happens when the EVM returns an exit reason
/// other than `ExitSucceed(Returned)` or `ExitSucceed(Stopped)`.
EvmCallFailed,
/// The provided call is not an EVM call. This extrinsic only accepts `pallet_evm::Call::call`.
NotEvmCall,
Comment thread
F3Joule marked this conversation as resolved.
}

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
Expand Down Expand Up @@ -236,6 +254,45 @@ pub mod pallet {
}),
}
}

/// Execute a single EVM call.
/// This extrinsic will fail if the EVM call returns any other ExitReason than `ExitSucceed(Returned)` or `ExitSucceed(Stopped)`.
/// Look the [hydradx_runtime::evm::runner::WrapRunner] implementation for details.
///
/// Parameters:
/// - `origin`: Signed origin.
/// - `call`: presumably `pallet_evm::Call::call` as boxed `RuntimeCall`.
///
/// Emits `EvmCallFailed` event when failed.
#[pallet::call_index(4)]
#[pallet::weight(call.get_dispatch_info().weight)]
Comment thread
F3Joule marked this conversation as resolved.
Outdated
pub fn dispatch_evm_call(
origin: OriginFor<T>,
call: Box<<T as Config>::RuntimeCall>,
) -> DispatchResultWithPostInfo {
Comment thread
mrq1911 marked this conversation as resolved.
ensure!(T::EvmCallIdentifier::is_evm_call(&call), Error::<T>::NotEvmCall);

let (result, actual_weight) = Self::do_dispatch(origin, *call);
let post_info = PostDispatchInfo {
actual_weight,
pays_fee: Pays::Yes,
};

if Self::last_evm_call_failed() {
return Err(DispatchErrorWithPostInfo {
post_info,
error: Error::<T>::EvmCallFailed.into(),
});
}

match result {
Ok(_) => Ok(post_info),
Err(err) => Err(DispatchErrorWithPostInfo {
post_info,
error: err.error,
}),
}
}
}
}

Expand Down Expand Up @@ -271,4 +328,12 @@ impl<T: Config> Pallet<T> {
ExtraGas::<T>::set(new_value);
}
}

pub fn set_last_evm_call_failed(status: bool) {
Comment thread
F3Joule marked this conversation as resolved.
Outdated
if !status {
LastEvmCallFailed::<T>::kill();
} else {
LastEvmCallFailed::<T>::put(status);
}
Comment thread
F3Joule marked this conversation as resolved.
Outdated
}
}
9 changes: 9 additions & 0 deletions pallets/dispatcher/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ impl pallet_evm::GasWeightMapping for MockGasWeightMapping {
}
}

pub struct EvmCallIdentifier;
impl MaybeEvmCall<RuntimeCall> for EvmCallIdentifier {
fn is_evm_call(_call: &RuntimeCall) -> bool {
false
}
}

impl dispatcher::Config for Test {
type RuntimeCall = RuntimeCall;
type RuntimeEvent = RuntimeEvent;
Expand All @@ -112,6 +119,7 @@ impl dispatcher::Config for Test {
type TreasuryAccount = TreasuryAccount;
type DefaultAaveManagerAccount = TreasuryAccount;
type WeightInfo = ();
type EvmCallIdentifier = EvmCallIdentifier;
type GasWeightMapping = MockGasWeightMapping;
}

Expand Down Expand Up @@ -207,6 +215,7 @@ impl<T: Config> Inspect for DummyRegistry<T> {
}
}

use hydradx_traits::evm::MaybeEvmCall;
#[cfg(feature = "runtime-benchmarks")]
use hydradx_traits::Create as CreateRegistry;
#[cfg(feature = "runtime-benchmarks")]
Expand Down
2 changes: 1 addition & 1 deletion runtime/hydradx/src/evm/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use frame_support::traits::Get;
use frame_support::weights::Weight;
use hydradx_traits::AccountFeeCurrencyBalanceInCurrency;
use pallet_evm::runner::Runner;
use pallet_evm::{AccountProvider, AddressMapping, CallInfo, Config, CreateInfo, FeeCalculator, RunnerError};
use pallet_evm::{AccountProvider, AddressMapping, CallInfo, Config, CreateInfo, ExitReason, ExitSucceed, FeeCalculator, RunnerError};
use primitive_types::{H160, H256, U256};
use primitives::{AssetId, Balance};
use sp_runtime::traits::UniqueSaturatedInto;
Expand Down
13 changes: 11 additions & 2 deletions runtime/hydradx/src/governance/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ use frame_support::{
PalletId,
};
use frame_system::{EnsureRoot, EnsureRootWithSuccess};
use hydradx_traits::evm::MaybeEvmCall;
use pallet_collective::EnsureProportionAtLeast;
use primitives::constants::{currency::DOLLARS, time::DAYS};
use sp_arithmetic::Perbill;
Expand Down Expand Up @@ -236,13 +237,21 @@ parameter_types! {
pub const AaveManagerAccount: AccountId = AccountId::new(hex!("aa7e0000000000000000000000000000000aa7e0000000000000000000000000"));
}

pub struct EvmCallChecker;
impl MaybeEvmCall<RuntimeCall> for EvmCallChecker {
fn is_evm_call(call: &RuntimeCall) -> bool {
matches!(call, RuntimeCall::EVM(pallet_evm::Call::call { .. }))
}
}

impl pallet_dispatcher::Config for Runtime {
type WeightInfo = weights::pallet_dispatcher::HydraWeight<Runtime>;
type RuntimeCall = RuntimeCall;
type RuntimeEvent = RuntimeEvent;
type RuntimeCall = RuntimeCall;
type EvmCallIdentifier = EvmCallChecker;
type TreasuryManagerOrigin = EitherOf<EnsureRoot<AccountId>, Treasurer>;
type AaveManagerOrigin = EitherOf<EnsureRoot<AccountId>, EconomicParameters>;
type TreasuryAccount = TreasuryAccount;
type DefaultAaveManagerAccount = AaveManagerAccount;
type GasWeightMapping = evm::FixedHydraGasWeightMapping<Runtime>;
type WeightInfo = weights::pallet_dispatcher::HydraWeight<Runtime>;
}
4 changes: 4 additions & 0 deletions traits/src/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ pub trait EVM<EvmResult> {
fn view(context: CallContext, data: Vec<u8>, gas: u64) -> EvmResult;
}

pub trait MaybeEvmCall<RuntimeCall> {
fn is_evm_call(call: &RuntimeCall) -> bool;
}

/// ERC20 interface adapter
pub trait ERC20 {
type Balance;
Expand Down