Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion Cargo.lock

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

225 changes: 190 additions & 35 deletions integration-tests/src/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(|| {
Expand All @@ -368,41 +368,21 @@ mod account_conversion {

let evm_address = EVMAccounts::evm_address(&Into::<AccountId>::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::<Runtime>::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::<AccountId>::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
Expand All @@ -419,6 +399,181 @@ 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::<Runtime>::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 = <hydradx_runtime::Runtime as pallet_evm::Config>::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 = <hydradx_runtime::Runtime as pallet_evm::Config>::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();
Expand Down
2 changes: 1 addition & 1 deletion pallets/evm-accounts/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion pallets/evm-accounts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: Config> = StorageMap<_, Blake2_128Concat, EvmAddress, AccountIdLast12Bytes>;
pub type AccountExtension<T: Config> = StorageMap<_, Blake2_128Concat, EvmAddress, AccountIdLast12Bytes>;

Comment on lines +137 to 138
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing AccountExtension from pub(super) to pub exposes the raw storage map as part of the pallet’s Rust API, making it easier for other runtime code to write to it directly and bypass invariants/events (inc_sufficients, Bound event, etc.). If this is only needed for integration tests, prefer a narrowly-scoped test helper (e.g., a function behind a dedicated cfg(feature = "testing")/cfg(test) or a helper in the integration test harness that writes storage by key) to avoid broadening the pallet’s public surface area.

Suggested change
pub type AccountExtension<T: Config> = StorageMap<_, Blake2_128Concat, EvmAddress, AccountIdLast12Bytes>;
pub(super) type AccountExtension<T: Config> = StorageMap<_, Blake2_128Concat, EvmAddress, AccountIdLast12Bytes>;
#[cfg(any(test, feature = "testing"))]
impl<T: Config> Pallet<T> {
/// Test-only helper for seeding account-extension storage without exposing the
/// raw storage map in the pallet's public production API.
pub fn insert_account_extension_for_testing(evm_address: EvmAddress, account_extension: AccountIdLast12Bytes) {
AccountExtension::<T>::insert(evm_address, account_extension);
}
}

Copilot uses AI. Check for mistakes.
/// Whitelisted addresses that are allowed to deploy smart contracts.
#[pallet::storage]
Expand Down
18 changes: 8 additions & 10 deletions runtime/hydradx/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -457,7 +457,13 @@ impl fp_self_contained::SelfContainedCall for RuntimeCall {
len: usize,
) -> Option<Result<(), TransactionValidityError>> {
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)));
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InvalidTransaction::BadSigner is typically reserved for signature/account mismatches. Here the signature can be valid, but the sender is rejected solely because the address is bound; using BadSigner can be misleading for clients and any error mapping/telemetry. Consider using a more semantically correct validity error (e.g., InvalidTransaction::Call or a Custom code) to represent the bound-address restriction distinctly.

Suggested change
return Some(Err(TransactionValidityError::Invalid(InvalidTransaction::BadSigner)));
return Some(Err(TransactionValidityError::Invalid(InvalidTransaction::Call)));

Copilot uses AI. Check for mistakes.
}
call.pre_dispatch_self_contained(info, dispatch_info, len)
}
_ => None,
}
}
Expand Down Expand Up @@ -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::<Runtime>::BoundAddressCannotBeUsed.into())
};

<Runtime as pallet_evm::Config>::Runner::call(
from,
Expand Down Expand Up @@ -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::<Runtime>::BoundAddressCannotBeUsed.into())
};

// the address needs to have a permission to deploy smart contract
if !EVMAccounts::can_deploy_contracts(from) {
Expand Down
Loading