Skip to content
Merged
Show file tree
Hide file tree
Changes from 41 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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion integration-tests/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "runtime-integration-tests"
version = "1.50.0"
version = "1.51.0"
description = "Integration tests"
authors = ["GalacticCouncil"]
edition = "2021"
Expand Down
224 changes: 223 additions & 1 deletion integration-tests/src/dispatcher.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
use crate::evm::{create_dispatch_handle, gas_price};
use crate::polkadot_test_net::*;
use fp_evm::PrecompileSet;
use frame_support::assert_ok;
use frame_support::dispatch::GetDispatchInfo;
use frame_support::dispatch::{
extract_actual_pays_fee, extract_actual_weight, GetDispatchInfo, Pays, PostDispatchInfo,
};
use hydradx_runtime::evm::precompiles::HydraDXPrecompiles;
use hydradx_runtime::evm::WethAssetId;
use hydradx_runtime::*;
use orml_traits::MultiCurrency;
use pallet_evm::{ExitReason, ExitSucceed};
use pallet_transaction_payment::ChargeTransactionPayment;
use precompile_utils::prelude::PrecompileOutput;
use primitives::EvmAddress;
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 @@ -425,3 +433,217 @@ 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(|| {
// Verify that LastEvmCallExitReason storage is cleaned before execution
assert_eq!(Dispatcher::last_evm_call_exit_reason(), None);

// Arrange: Deploy a valid contract to interact with
let contract = crate::utils::contracts::deploy_contract("HydraToken", crate::contracts::deployer());
let stop_code_contract = crate::utils::contracts::deploy_contract_code(
hex!["608080604052346013576067908160188239f35b5f80fdfe6004361015600b575f80fd5b5f3560e01c6306fdde0314601d575f80fd5b34602d575f366003190112602d57005b5f80fdfea264697066735822122072cd2025c9922b7f29b4174f1e2d766386a8ecbaab35dc5921cda0fa301dcb3e64736f6c634300081e0033"].to_vec(),
crate::contracts::deployer(),
); // name() function selector returns "stopped"

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: 1_000_000,
max_fee_per_gas: gas_price(),
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(stop_code_contract);

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

// Verify that LastEvmCallExitReason storage has expected Returned value
assert_eq!(
Dispatcher::last_evm_call_exit_reason(),
Some(ExitReason::Succeed(ExitSucceed::Returned))
);

assert_ok!(Dispatcher::dispatch_evm_call(
evm_signed_origin(evm_address()),
call_succeed_stopped
));

// Verify that LastEvmCallExitReason storage has expected Stopped value
assert_eq!(
Dispatcher::last_evm_call_exit_reason(),
Some(ExitReason::Succeed(ExitSucceed::Stopped))
);

// Produce the next block and ensure the key is gone at the next block
hydradx_run_to_next_block();
assert_eq!(
Dispatcher::last_evm_call_exit_reason(),
None,
"Storage key should stay empty in subsequent blocks"
);
});
}

#[test]
fn dispatch_evm_call_should_fail_with_invalid_function_selector() {
TestNet::reset();
Hydra::execute_with(|| {
// Verify that LastEvmCallExitReason storage is cleaned before execution
assert_eq!(Dispatcher::last_evm_call_exit_reason(), None);

// Arrange
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: 1_000_000,
value: U256::zero(),
max_fee_per_gas: gas_price(),
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 LastEvmCallExitReason storage is cleaned after faulty execution
assert_eq!(Dispatcher::last_evm_call_exit_reason(), None);
});
}

#[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(),
})
);
})
}

#[test]
fn dispatch_evm_call_via_precompile_should_work() {
TestNet::reset();
Hydra::execute_with(|| {
// Arrange
let stop_code_contract = crate::utils::contracts::deploy_contract_code(
hex!["608080604052346013576067908160188239f35b5f80fdfe6004361015600b575f80fd5b5f3560e01c6306fdde0314601d575f80fd5b34602d575f366003190112602d57005b5f80fdfea264697066735822122072cd2025c9922b7f29b4174f1e2d766386a8ecbaab35dc5921cda0fa301dcb3e64736f6c634300081e0033"].to_vec(),
crate::contracts::deployer(),
); // name() function selector returns "stopped"

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

let inner_runtime_call = RuntimeCall::EVM(pallet_evm::Call::call {
source: evm_address(),
target: stop_code_contract,
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![],
});

let outer_call = RuntimeCall::Dispatcher(pallet_dispatcher::Call::dispatch_evm_call {
call: Box::new(inner_runtime_call),
});

// SCALE‑encode the entire outer call for precompile
let data = outer_call.encode();

// Build a mocked EVM precompile handle which basically simulates a MetaMask
// transaction calling the Frontier dispatch precompile (`DISPATCH_ADDR`)
// from the default test EVM account.
let mut handle = create_dispatch_handle(data);

// Execute all HydraDX precompiles (this includes the standard
// Frontier Dispatch precompile wired at `DISPATCH_ADDR`).
let precompiles = HydraDXPrecompiles::<hydradx_runtime::Runtime>::new();
let result = precompiles.execute(&mut handle);

// The dispatch precompile should succeed and stop.
assert_eq!(
result.unwrap(),
Ok(PrecompileOutput {
exit_status: ExitSucceed::Stopped,
output: Default::default(),
})
);
});
}
1 change: 1 addition & 0 deletions integration-tests/src/polkadot_test_net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,7 @@ pub fn hydradx_run_to_next_block() {
hydradx_runtime::MultiTransactionPayment::on_finalize(b);
hydradx_runtime::CircuitBreaker::on_finalize(b);
hydradx_runtime::DCA::on_finalize(b);
hydradx_runtime::Dispatcher::on_finalize(b);
hydradx_runtime::EmaOracle::on_finalize(b);
hydradx_runtime::EVM::on_finalize(b);
hydradx_runtime::Ethereum::on_finalize(b);
Expand Down
13 changes: 7 additions & 6 deletions pallets/dispatcher/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pallet-dispatcher"
version = "1.1.1"
version = "1.2.0"
authors = ['GalacticCouncil']
edition = "2021"
license = "Apache-2.0"
Expand All @@ -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 All @@ -50,7 +52,6 @@ std = [
'orml-tokens/std',
'orml-traits/std',
"pallet-evm/std",
"hydradx-traits/std",
]

runtime-benchmarks = [
Expand Down
8 changes: 8 additions & 0 deletions pallets/dispatcher/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,13 @@ benchmarks! {

}: _(RawOrigin::Signed(caller), Box::new(call), 50_000)

dispatch_evm_call {
let n in 1 .. 10_000;
let remark = sp_std::vec![1u8; n as usize];

let call: <T as pallet::Config>::RuntimeCall = frame_system::Call::remark { remark }.into();
let caller: T::AccountId = account("caller", 0, 1);
}: _(RawOrigin::Signed(caller), Box::new(call))

impl_benchmark_test_suite!(Pallet, crate::mock::ExtBuilder::default().build(), crate::mock::Test);
}
Loading
Loading