From 2723803cb38ec7e0ad83ddd9b8f4e83bf97cd043 Mon Sep 17 00:00:00 2001 From: mrq Date: Sun, 5 Apr 2026 11:59:11 +0200 Subject: [PATCH 1/5] moved bound address check to predispatch --- runtime/hydradx/src/lib.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/runtime/hydradx/src/lib.rs b/runtime/hydradx/src/lib.rs index ba23e90ac..7bc925304 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -70,7 +70,7 @@ pub use sp_runtime::{ AccountIdConversion, BlakeTwo256, Block as BlockT, DispatchInfoOf, Dispatchable, PostDispatchInfoOf, UniqueSaturatedInto, }, - transaction_validity::{TransactionValidity, TransactionValidityError}, + transaction_validity::{InvalidTransaction, TransactionValidity, TransactionValidityError}, DispatchError, Permill, TransactionOutcome, }; @@ -457,7 +457,13 @@ impl fp_self_contained::SelfContainedCall for RuntimeCall { len: usize, ) -> Option> { match self { - RuntimeCall::Ethereum(call) => call.pre_dispatch_self_contained(info, dispatch_info, len), + RuntimeCall::Ethereum(call) => { + // don't allow on-chain EVM transactions from a bound address + if EVMAccounts::bound_account_id(*info).is_some() { + return Some(Err(TransactionValidityError::Invalid(InvalidTransaction::BadSigner))); + } + call.pre_dispatch_self_contained(info, dispatch_info, len) + } _ => None, } } @@ -839,10 +845,6 @@ impl_runtime_apis! { _ => (None, None), }; - // don't allow calling EVM RPC or Runtime API from a bound address - if !estimate && EVMAccounts::bound_account_id(from).is_some() { - return Err(pallet_evm_accounts::Error::::BoundAddressCannotBeUsed.into()) - }; ::Runner::call( from, @@ -922,10 +924,6 @@ impl_runtime_apis! { _ => (None, None), }; - // don't allow calling EVM RPC or Runtime API from a bound address - if !estimate && EVMAccounts::bound_account_id(from).is_some() { - return Err(pallet_evm_accounts::Error::::BoundAddressCannotBeUsed.into()) - }; // the address needs to have a permission to deploy smart contract if !EVMAccounts::can_deploy_contracts(from) { From be7a50d1736d976d6cde1d6bb2c7171fc51fc54f Mon Sep 17 00:00:00 2001 From: mrq Date: Sun, 5 Apr 2026 12:17:26 +0200 Subject: [PATCH 2/5] tests fixed --- integration-tests/src/evm.rs | 227 +++++++++++++++++++++++++++----- pallets/evm-accounts/src/lib.rs | 2 +- 2 files changed, 193 insertions(+), 36 deletions(-) diff --git a/integration-tests/src/evm.rs b/integration-tests/src/evm.rs index 3777ad31f..ce177f2f2 100644 --- a/integration-tests/src/evm.rs +++ b/integration-tests/src/evm.rs @@ -355,7 +355,7 @@ mod account_conversion { } #[test] - fn evm_call_from_runtime_rpc_should_not_be_accepted_from_bound_addresses() { + fn evm_rpc_call_should_be_accepted_from_bound_addresses() { TestNet::reset(); Hydra::execute_with(|| { @@ -368,41 +368,21 @@ mod account_conversion { let evm_address = EVMAccounts::evm_address(&Into::::into(ALICE)); - //Act & Assert - assert_noop!( - hydradx_runtime::Runtime::call( - evm_address, // from - DISPATCH_ADDR, // to - data, // data - U256::from(1000u64), - U256::from(100000u64), - None, - None, - None, - false, - None, - None, - ), - pallet_evm_accounts::Error::::BoundAddressCannotBeUsed - ); - }); - } - - #[test] - fn estimation_of_evm_call_should_be_accepted_even_from_bound_address() { - TestNet::reset(); - - Hydra::execute_with(|| { - //Arrange - let data = - hex!["4d0045544800d1820d45118d78d091e685490c674d7596e62d1f0000000000000000140000000f0000c16ff28623"] - .to_vec(); - - assert_ok!(EVMAccounts::bind_evm_address(RuntimeOrigin::signed(ALICE.into())),); - - let evm_address = EVMAccounts::evm_address(&Into::::into(ALICE)); + //Act & Assert - both estimate=false and estimate=true should work from RPC + assert_ok!(hydradx_runtime::Runtime::call( + evm_address, // from + DISPATCH_ADDR, // to + data.clone(), // data + U256::from(1000u64), + U256::from(100000u64), + None, + None, + None, + false, + None, + None, + )); - //Act & Assert assert_ok!(hydradx_runtime::Runtime::call( evm_address, // from DISPATCH_ADDR, // to @@ -419,6 +399,183 @@ mod account_conversion { }); } + #[test] + fn on_chain_evm_transaction_should_be_rejected_from_bound_address() { + use crate::utils::accounts::{alith_evm_account, alith_evm_address, alith_secret_key}; + use ethereum::{ + eip2930::TransactionSignature, EIP1559Transaction, EIP1559TransactionMessage, TransactionAction, + TransactionV2, + }; + use libsecp256k1::{sign, Message, SecretKey}; + use sp_runtime::transaction_validity::{InvalidTransaction, TransactionValidityError}; + + TestNet::reset(); + + Hydra::execute_with(|| { + //Arrange + let account = MockAccount::new(alith_evm_account()); + let evm_address = alith_evm_address(); + + // fund the account + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + account.address(), + WETH, + 1_000_000_000_000_000_000i128, + )); + + // simulate binding by inserting directly into storage + // (alith is a truncated account so bind_evm_address would fail) + let account_bytes: [u8; 32] = *account.address().as_ref(); + let mut last_12: [u8; 12] = [0u8; 12]; + last_12.copy_from_slice(&account_bytes[20..32]); + pallet_evm_accounts::AccountExtension::::insert(evm_address, last_12); + + // verify binding works + assert!(EVMAccounts::bound_account_id(evm_address).is_some()); + + // build a simple evm transaction + let chain_id = ::ChainId::get(); + let nonce = U256::zero(); + let (base_gas_price, _) = hydradx_runtime::DynamicEvmFee::min_gas_price(); + let max_fee_per_gas = base_gas_price * 10; + let max_priority_fee_per_gas = base_gas_price; + let gas_limit: u64 = 100_000; + + let tx_msg = EIP1559TransactionMessage { + chain_id, + nonce, + max_priority_fee_per_gas, + max_fee_per_gas, + gas_limit: gas_limit.into(), + action: TransactionAction::Call(DISPATCH_ADDR), + value: U256::zero(), + input: hex!("0107081337").to_vec(), + access_list: vec![], + }; + + let secret_key = SecretKey::parse(&alith_secret_key()).expect("valid secret key"); + let hash = tx_msg.hash(); + let mut hash_bytes = [0u8; 32]; + hash_bytes.copy_from_slice(&hash.0); + let message = Message::parse(&hash_bytes); + let (rs, v) = sign(&message, &secret_key); + let odd_y_parity = v.serialize() != 0; + let signature = + TransactionSignature::new(odd_y_parity, H256::from(rs.r.b32()), H256::from(rs.s.b32())) + .expect("valid signature"); + + let signed_tx = EIP1559Transaction { + chain_id, + nonce, + max_priority_fee_per_gas, + max_fee_per_gas, + gas_limit: gas_limit.into(), + action: TransactionAction::Call(DISPATCH_ADDR), + value: U256::zero(), + input: hex!("0107081337").to_vec(), + access_list: vec![], + signature, + }; + + let transaction = TransactionV2::EIP1559(signed_tx); + + //Act + let call = hydradx_runtime::RuntimeCall::Ethereum(pallet_ethereum::Call::transact { + transaction: transaction.into(), + }); + let ue = hydradx_runtime::HydraUncheckedExtrinsic::new_bare(call); + let result = hydradx_runtime::Executive::apply_extrinsic(ue); + + //Assert - transaction should be rejected with BadSigner + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + TransactionValidityError::Invalid(InvalidTransaction::BadSigner), + ); + }); + } + + #[test] + fn on_chain_evm_transaction_should_work_from_unbound_address() { + use crate::utils::accounts::{alith_evm_account, alith_secret_key}; + use ethereum::{ + eip2930::TransactionSignature, EIP1559Transaction, EIP1559TransactionMessage, TransactionAction, + TransactionV2, + }; + use libsecp256k1::{sign, Message, SecretKey}; + + TestNet::reset(); + + Hydra::execute_with(|| { + //Arrange + let account = MockAccount::new(alith_evm_account()); + + // fund the account but do NOT bind + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + account.address(), + WETH, + 1_000_000_000_000_000_000i128, + )); + + init_omnipool_with_oracle_for_block_10(); + + // build a simple evm transaction + let chain_id = ::ChainId::get(); + let nonce = U256::zero(); + let (base_gas_price, _) = hydradx_runtime::DynamicEvmFee::min_gas_price(); + let max_fee_per_gas = base_gas_price * 10; + let max_priority_fee_per_gas = base_gas_price; + let gas_limit: u64 = 1_000_000; + + let tx_msg = EIP1559TransactionMessage { + chain_id, + nonce, + max_priority_fee_per_gas, + max_fee_per_gas, + gas_limit: gas_limit.into(), + action: TransactionAction::Call(DISPATCH_ADDR), + value: U256::zero(), + input: hex!("0107081337").to_vec(), + access_list: vec![], + }; + + let secret_key = SecretKey::parse(&alith_secret_key()).expect("valid secret key"); + let hash = tx_msg.hash(); + let mut hash_bytes = [0u8; 32]; + hash_bytes.copy_from_slice(&hash.0); + let message = Message::parse(&hash_bytes); + let (rs, v) = sign(&message, &secret_key); + let odd_y_parity = v.serialize() != 0; + let signature = + TransactionSignature::new(odd_y_parity, H256::from(rs.r.b32()), H256::from(rs.s.b32())) + .expect("valid signature"); + + let signed_tx = EIP1559Transaction { + chain_id, + nonce, + max_priority_fee_per_gas, + max_fee_per_gas, + gas_limit: gas_limit.into(), + action: TransactionAction::Call(DISPATCH_ADDR), + value: U256::zero(), + input: hex!("0107081337").to_vec(), + access_list: vec![], + signature, + }; + + let transaction = TransactionV2::EIP1559(signed_tx); + + //Act & Assert - unbound address should pass pre_dispatch + crate::utils::executive::assert_executive_apply_unsigned_extrinsic( + hydradx_runtime::RuntimeCall::Ethereum(pallet_ethereum::Call::transact { + transaction: transaction.into(), + }), + ); + }); + } + #[test] fn claim_account_should_work_for_account_with_erc20_balance() { TestNet::reset(); diff --git a/pallets/evm-accounts/src/lib.rs b/pallets/evm-accounts/src/lib.rs index acbf14076..a0eb3d99d 100644 --- a/pallets/evm-accounts/src/lib.rs +++ b/pallets/evm-accounts/src/lib.rs @@ -134,7 +134,7 @@ pub mod pallet { /// Maps an EVM address to the last 12 bytes of a substrate account. #[pallet::storage] #[pallet::getter(fn account)] - pub(super) type AccountExtension = StorageMap<_, Blake2_128Concat, EvmAddress, AccountIdLast12Bytes>; + pub type AccountExtension = StorageMap<_, Blake2_128Concat, EvmAddress, AccountIdLast12Bytes>; /// Whitelisted addresses that are allowed to deploy smart contracts. #[pallet::storage] From 470e9ef3404fdfd14bbb6397471660761eb971c8 Mon Sep 17 00:00:00 2001 From: mrq Date: Sun, 5 Apr 2026 12:24:46 +0200 Subject: [PATCH 3/5] fmt --- integration-tests/src/evm.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/integration-tests/src/evm.rs b/integration-tests/src/evm.rs index ce177f2f2..d80510dc8 100644 --- a/integration-tests/src/evm.rs +++ b/integration-tests/src/evm.rs @@ -461,9 +461,8 @@ mod account_conversion { let message = Message::parse(&hash_bytes); let (rs, v) = sign(&message, &secret_key); let odd_y_parity = v.serialize() != 0; - let signature = - TransactionSignature::new(odd_y_parity, H256::from(rs.r.b32()), H256::from(rs.s.b32())) - .expect("valid signature"); + let signature = TransactionSignature::new(odd_y_parity, H256::from(rs.r.b32()), H256::from(rs.s.b32())) + .expect("valid signature"); let signed_tx = EIP1559Transaction { chain_id, @@ -548,9 +547,8 @@ mod account_conversion { let message = Message::parse(&hash_bytes); let (rs, v) = sign(&message, &secret_key); let odd_y_parity = v.serialize() != 0; - let signature = - TransactionSignature::new(odd_y_parity, H256::from(rs.r.b32()), H256::from(rs.s.b32())) - .expect("valid signature"); + let signature = TransactionSignature::new(odd_y_parity, H256::from(rs.r.b32()), H256::from(rs.s.b32())) + .expect("valid signature"); let signed_tx = EIP1559Transaction { chain_id, @@ -568,11 +566,11 @@ mod account_conversion { let transaction = TransactionV2::EIP1559(signed_tx); //Act & Assert - unbound address should pass pre_dispatch - crate::utils::executive::assert_executive_apply_unsigned_extrinsic( - hydradx_runtime::RuntimeCall::Ethereum(pallet_ethereum::Call::transact { + crate::utils::executive::assert_executive_apply_unsigned_extrinsic(hydradx_runtime::RuntimeCall::Ethereum( + pallet_ethereum::Call::transact { transaction: transaction.into(), - }), - ); + }, + )); }); } From a46e75c3a4cf1eab682db46d523ee064661f24f2 Mon Sep 17 00:00:00 2001 From: mrq Date: Sun, 5 Apr 2026 12:27:29 +0200 Subject: [PATCH 4/5] bumped version --- Cargo.lock | 6 +++--- integration-tests/Cargo.toml | 2 +- pallets/evm-accounts/Cargo.toml | 2 +- runtime/hydradx/Cargo.toml | 2 +- runtime/hydradx/src/lib.rs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 37ab8ce2d..f472c666b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "hydradx-runtime" -version = "405.0.0" +version = "406.0.0" dependencies = [ "alloy-primitives 0.7.7", "alloy-sol-types 0.7.7", @@ -10507,7 +10507,7 @@ dependencies = [ [[package]] name = "pallet-evm-accounts" -version = "1.6.0" +version = "1.6.1" dependencies = [ "frame-benchmarking", "frame-support", @@ -15646,7 +15646,7 @@ checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" [[package]] name = "runtime-integration-tests" -version = "1.75.1" +version = "1.75.2" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 1d0422bd6..dbfbb1768 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "runtime-integration-tests" -version = "1.75.1" +version = "1.75.2" description = "Integration tests" authors = ["GalacticCouncil"] edition = "2021" diff --git a/pallets/evm-accounts/Cargo.toml b/pallets/evm-accounts/Cargo.toml index d06c7fd27..9204ffbeb 100644 --- a/pallets/evm-accounts/Cargo.toml +++ b/pallets/evm-accounts/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-evm-accounts" -version = "1.6.0" +version = "1.6.1" authors = ["GalacticCouncil"] edition = "2021" license = "Apache-2.0" diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index 06a2d1516..13829311b 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-runtime" -version = "405.0.0" +version = "406.0.0" authors = ["GalacticCouncil"] edition = "2021" license = "Apache 2.0" diff --git a/runtime/hydradx/src/lib.rs b/runtime/hydradx/src/lib.rs index 7bc925304..116a72156 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -129,7 +129,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: Cow::Borrowed("hydradx"), impl_name: Cow::Borrowed("hydradx"), authoring_version: 1, - spec_version: 405, + spec_version: 406, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, From 09e8cf6b04e9b1e27b39936b264406214d5e689c Mon Sep 17 00:00:00 2001 From: mrq Date: Fri, 17 Apr 2026 15:46:46 +0200 Subject: [PATCH 5/5] fix: clean up leftover merge conflict markers Co-Authored-By: Claude Opus 4.7 (1M context) --- integration-tests/Cargo.toml | 4 ---- runtime/hydradx/Cargo.toml | 4 ---- 2 files changed, 8 deletions(-) diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 439a16945..0789ea310 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -1,10 +1,6 @@ [package] name = "runtime-integration-tests" -<<<<<<< eth_estimate_fix -version = "1.75.2" -======= version = "1.77.0" ->>>>>>> master description = "Integration tests" authors = ["GalacticCouncil"] edition = "2021" diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index 83924a8fe..454f4f488 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -1,10 +1,6 @@ [package] name = "hydradx-runtime" -<<<<<<< eth_estimate_fix -version = "406.0.0" -======= version = "409.0.0" ->>>>>>> master authors = ["GalacticCouncil"] edition = "2021" license = "Apache 2.0"