Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c67ee0e
Support upload_code via eth_sendRawTransaction
marian-radu Oct 29, 2025
ff85c0c
WIP: add eth_substrate_call extrinsic.
marian-radu Oct 30, 2025
67a3eaf
Add test for uploading contract code via RUNTIME_PALLETS_ADDR magic a…
marian-radu Oct 31, 2025
4d03947
Add test for removing contract code via RUNTIME_PALLETS_ADDR.
marian-radu Oct 31, 2025
07003da
Merge branch 'master' into 180-failed-to-instantiate-contract-someout…
marian-radu Oct 31, 2025
5968242
Add a unit test for unmapping an account using an Ethereum transaction.
marian-radu Nov 3, 2025
8fb515a
Filter the calls allowed via eth_substrate.
marian-radu Nov 3, 2025
1efda77
Add benchmark eth_substrate_call_upload_code.
marian-radu Nov 4, 2025
1826022
Changed eth_substrate_call parameter type to RuntimeCall.
marian-radu Nov 5, 2025
fa3189b
Allow eth_substrate_call only in development.
marian-radu Nov 5, 2025
53f9657
Add eth_substrate_call overhead to post_info.actual_weight.
marian-radu Nov 5, 2025
7eda58c
Small refactor
marian-radu Nov 5, 2025
0a05cee
Merge branch 'master' into 180-failed-to-instantiate-contract-someout…
marian-radu Nov 6, 2025
543249e
Address review comments.
marian-radu Nov 6, 2025
d1610b6
Adjust eth_substrate_call weight.
marian-radu Nov 6, 2025
ddc4607
Tweak a few comments.
marian-radu Nov 6, 2025
f9d0d8c
Update from github-actions[bot] running command 'prdoc --audience run…
github-actions[bot] Nov 6, 2025
e98001e
Small optimization.
marian-radu Nov 6, 2025
e89ccac
Add unit tests for eth_substrate_call
marian-radu Nov 6, 2025
f71dc30
Merge branch 'master' into 180-failed-to-instantiate-contract-someout…
marian-radu Nov 6, 2025
4e43194
Fix TOML formatting and remove redundant local variable.
marian-radu Nov 7, 2025
cba5dfb
Merge branch 'master' into 180-failed-to-instantiate-contract-someout…
marian-radu Nov 7, 2025
7fdbf8d
Move debug enabled check inside the ethereum_context block.
marian-radu Nov 7, 2025
43bb605
Add parameter transaction_encoded to eth_substrate_call and remove wi…
marian-radu Nov 7, 2025
0e9a36a
Remove development requirement for eth_substrate_call.
marian-radu Nov 7, 2025
70c277a
Update prdoc.
marian-radu Nov 7, 2025
24e5792
Update from github-actions[bot] running command 'bench --runtime dev …
github-actions[bot] Nov 7, 2025
e86bfa3
Address review comments.
marian-radu Nov 8, 2025
4026943
Fixed the actual_weight computation in eth_substrate_call.
marian-radu Nov 8, 2025
0727aba
Address review comments.
marian-radu Nov 10, 2025
fa8463d
Merge branch 'master' into 180-failed-to-instantiate-contract-someout…
marian-radu Nov 10, 2025
47789a6
Fix typo and tweak test.
marian-radu Nov 10, 2025
b4e5563
Merge branch 'master' into 180-failed-to-instantiate-contract-someout…
marian-radu Nov 10, 2025
88c4fb8
Update prdoc.
marian-radu Nov 10, 2025
e911c31
Merge branch 'master' into 180-failed-to-instantiate-contract-someout…
marian-radu Nov 10, 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
18 changes: 18 additions & 0 deletions prdoc/pr_10159.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
title: Add eth_substrate_call extrinsic.
doc:
- audience: Runtime Dev
description: |-
Fixes https://github.com/paritytech/contract-issues/issues/180

This PR introduces `eth_substrate_call`, a new extrinsic in pallet-revive that enables Substrate runtime calls from Ethereum transactions. This allows tools like Hardhat to invoke Substrate extrinsics (e.g., `upload_code`) via the Ethereum RPC.

Includes:
- New eth-rpc test validating the functionality
- Benchmark measuring the extrinsic overhead
crates:
- name: pallet-revive
bump: patch
- name: pallet-revive-eth-rpc
bump: patch
- name: revive-dev-runtime
bump: patch
4 changes: 3 additions & 1 deletion substrate/bin/node/runtime/src/genesis_config_presets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ pub fn kitchensink_genesis(
min_create_bond: 10 * DOLLARS,
min_join_bond: 1 * DOLLARS,
},
revive: ReviveConfig { mapped_accounts: endowed_accounts.iter().filter(|x| ! is_eth_derived(x)).cloned().collect() },
revive: ReviveConfig {
mapped_accounts: endowed_accounts.iter().filter(|x| ! is_eth_derived(x)).cloned().collect(),
},
})
}

Expand Down
1 change: 1 addition & 0 deletions substrate/frame/revive/rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ tokio = { workspace = true, features = ["full"] }
env_logger = { workspace = true }
pallet-revive-fixtures = { workspace = true, default-features = true }
pretty_assertions = { workspace = true }
sp-io = { workspace = true, default-features = true }
static_init = { workspace = true }
substrate-cli-test-utils = { workspace = true }

Expand Down
60 changes: 60 additions & 0 deletions substrate/frame/revive/rpc/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -815,3 +815,63 @@ async fn test_mixed_evm_substrate_transactions() -> anyhow::Result<()> {

Ok(())
}

#[tokio::test]
async fn test_runtime_pallets_address_upload_code() -> anyhow::Result<()> {
let _lock = SHARED_RESOURCES.write();
let client = Arc::new(SharedResources::client().await);
let (node_client, node_rpc_client, _) =
client::connect(SharedResources::node_rpc_url()).await?;

let (bytecode, _) = pallet_revive_fixtures::compile_module("dummy")?;
let signer = Account::default();

// Helper function to get substrate block hash from EVM block number
let get_substrate_block_hash = |block_number: U256| {
let rpc_client = node_rpc_client.clone();
async move {
rpc_client
.request::<sp_core::H256>("chain_getBlockHash", rpc_params![block_number])
.await
}
};

// Step 1: Encode the Substrate upload_code call
let upload_call = subxt::dynamic::tx(
"Revive",
"upload_code",
vec![
subxt::dynamic::Value::from_bytes(&bytecode),
subxt::dynamic::Value::u128(u128::max_value()), // storage_deposit_limit
],
);
let encoded_call = node_client.tx().call_data(&upload_call)?;

// Step 2: Send the encoded call to RUNTIME_PALLETS_ADDR
let tx = TransactionBuilder::new(&client)
.signer(signer.clone())
.to(pallet_revive::RUNTIME_PALLETS_ADDR)
.input(encoded_call.clone())
.send()
.await?;

// Step 3: Wait for receipt
let receipt = tx.wait_for_receipt().await?;

// Step 4: Verify transaction was successful
assert_eq!(
receipt.status.unwrap_or(U256::zero()),
U256::one(),
"Transaction should be successful"
);

// Step 5: Verify the code was actually uploaded
let code_hash = H256(sp_io::hashing::keccak_256(&bytecode));
let query = subxt_client::storage().revive().pristine_code(code_hash);
let block_hash: sp_core::H256 = get_substrate_block_hash(receipt.block_number).await?;
let stored_code = node_client.storage().at(block_hash).fetch(&query).await?;
assert!(stored_code.is_some(), "Code with hash {code_hash:?} should exist in storage");
assert_eq!(stored_code.unwrap(), bytecode, "Stored code should match the uploaded bytecode");

Ok(())
}
12 changes: 12 additions & 0 deletions substrate/frame/revive/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,18 @@ mod benchmarks {
Ok(())
}

// `c`: Size of the RLP encoded Ethereum transaction in bytes.
#[benchmark(pov_mode = Measured)]
fn eth_substrate_call(c: Linear<0, { 100 * 1024 }>) -> Result<(), BenchmarkError> {
let caller = whitelisted_caller();
T::Currency::set_balance(&caller, caller_funding::<T>());
let origin = Origin::EthTransaction(caller);
let dispatchable = frame_system::Call::remark { remark: vec![] }.into();
#[extrinsic_call]
_(origin, Box::new(dispatchable), vec![42u8; c as usize]);
Ok(())
}
Comment thread
marian-radu marked this conversation as resolved.

// This constructs a contract that is maximal expensive to instrument.
// It creates a maximum number of metering blocks per byte.
// `c`: Size of the code in bytes.
Expand Down
2 changes: 1 addition & 1 deletion substrate/frame/revive/src/evm/block_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use sp_core::{H256, U256};

/// Details needed to reconstruct the receipt info in the RPC
/// layer without losing accuracy.
#[derive(Encode, Decode, TypeInfo, Clone, Debug, PartialEq, Eq)]
#[derive(Encode, Decode, TypeInfo, Clone, Debug, Default, PartialEq, Eq)]
pub struct ReceiptGasInfo {
/// The amount of gas used for this specific transaction alone.
pub gas_used: U256,
Expand Down
5 changes: 3 additions & 2 deletions substrate/frame/revive/src/evm/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use crate::{
extract_code_and_data, BalanceOf, CallOf, Config, GenericTransaction, Pallet, Weight, Zero,
LOG_TARGET, RUNTIME_PALLETS_ADDR, U256,
};
use alloc::vec::Vec;
use alloc::{boxed::Box, vec::Vec};
use codec::DecodeLimit;
use frame_support::MAX_EXTRINSIC_DEPTH;
use sp_core::Get;
Expand Down Expand Up @@ -115,7 +115,8 @@ where
return Err(InvalidTransaction::Call)
}

call
crate::Call::eth_substrate_call::<T> { call: Box::new(call), transaction_encoded }
.into()
} else {
let call = crate::Call::eth_call::<T> {
dest,
Expand Down
9 changes: 8 additions & 1 deletion substrate/frame/revive/src/evm/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,13 @@ mod test {
UncheckedExtrinsicBuilder::call_with(RUNTIME_PALLETS_ADDR).data(remark.encode());
let (_, call, _, _, _, _) = builder.check().unwrap();

assert_eq!(call, remark);
match call {
RuntimeCall::Contracts(crate::Call::eth_substrate_call {
call: inner_call, ..
}) => {
assert_eq!(*inner_call, remark);
},
_ => assert!(false, "Call does not match."),
Comment thread
marian-radu marked this conversation as resolved.
Outdated
}
}
}
47 changes: 47 additions & 0 deletions substrate/frame/revive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1334,6 +1334,53 @@ pub mod pallet {
})
}

/// Executes a Substrate runtime call from an Ethereum transaction.
///
/// This dispatchable is intended to be called **only** through the EVM compatibility layer.
///
/// # Parameters
///
/// * `origin`: Must be an [`Origin::EthTransaction`] origin.
/// * `call`: The Substrate runtime call to execute.
/// * `transaction_encoded`: The RLP encoding of the Ethereum transaction,
#[pallet::call_index(12)]
#[pallet::weight(T::WeightInfo::eth_substrate_call(transaction_encoded.len() as u32).saturating_add(call.get_dispatch_info().call_weight))]
pub fn eth_substrate_call(
origin: OriginFor<T>,
call: Box<<T as Config>::RuntimeCall>,
transaction_encoded: Vec<u8>,
) -> DispatchResultWithPostInfo {
// Note that the inner dispatch uses `RawOrigin::Signed`, which cannot
// re-enter `eth_substrate_call` (which requires `Origin::EthTransaction`).
Comment thread
marian-radu marked this conversation as resolved.
let signer = Self::ensure_eth_signed(origin)?;
let weight_overhead =
T::WeightInfo::eth_substrate_call(transaction_encoded.len() as u32);

block_storage::with_ethereum_context::<T>(transaction_encoded, || {
let call_weight = call.get_dispatch_info().call_weight;
let mut call_result = call.dispatch(RawOrigin::Signed(signer).into());

// Add extrinsic_overhead to the actual weight in PostDispatchInfo
match &mut call_result {
Ok(post_info) | Err(DispatchErrorWithPostInfo { post_info, .. }) => {
post_info.actual_weight = Some(
post_info
.actual_weight
.unwrap_or_else(|| call_weight)
.saturating_add(weight_overhead),
);
},
}
Comment thread
pgherveou marked this conversation as resolved.

// Return zero EVM gas (Substrate dispatch, not EVM contract call).
// Actual weight is in `post_info.actual_weight`.
block_storage::EthereumCallResult {
receipt_gas_info: ReceiptGasInfo::default(),
result: call_result,
}
})
}

/// Upload new `code` without instantiating a contract from it.
///
/// If the code does not already exist a deposit is reserved from the caller
Expand Down
80 changes: 77 additions & 3 deletions substrate/frame/revive/src/tests/sol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,18 @@ use crate::{
assert_refcount,
call_builder::VmBinaryModule,
debug::DebugSettings,
test_utils::{builder::Contract, ALICE, ALICE_ADDR},
test_utils::{builder::Contract, ALICE, ALICE_ADDR, BOB},
tests::{
builder,
test_utils::{contract_base_deposit, ensure_stored, get_contract},
AllowEvmBytecode, DebugFlag, ExtBuilder, RuntimeOrigin, Test,
},
Code, Config, Error, GenesisConfig, Pallet, PristineCode,
Code, Config, Error, GenesisConfig, Origin, Pallet, PristineCode,
};
use alloy_core::sol_types::{SolCall, SolInterface};
use frame_support::{assert_err, assert_ok, traits::fungible::Mutate};
use frame_support::{
assert_err, assert_noop, assert_ok, dispatch::GetDispatchInfo, traits::fungible::Mutate,
};
use pallet_revive_fixtures::{compile_module_with_type, Fibonacci, FixtureType};
use pretty_assertions::assert_eq;
use test_case::test_case;
Expand Down Expand Up @@ -302,3 +304,75 @@ fn dust_work_with_child_calls(fixture_type: FixtureType) {
assert_eq!(crate::Pallet::<Test>::evm_balance(&addr), value);
});
}

#[test]
fn eth_substrate_call_dispatches_successfully() {
use frame_support::traits::fungible::Inspect;
ExtBuilder::default().build().execute_with(|| {
let _ = <Test as Config>::Currency::set_balance(&ALICE, 1000);
let _ = <Test as Config>::Currency::set_balance(&BOB, 100);

let transfer_call =
crate::tests::RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death {
dest: BOB,
value: 50,
});

assert_ok!(Pallet::<Test>::eth_substrate_call(
Origin::EthTransaction(ALICE).into(),
Box::new(transfer_call),
vec![]
));

// Verify balance changed
assert_eq!(<Test as Config>::Currency::balance(&ALICE), 950);
assert_eq!(<Test as Config>::Currency::balance(&BOB), 150);
});
}

#[test]
fn eth_substrate_call_requires_eth_origin() {
ExtBuilder::default().build().execute_with(|| {
let inner_call = frame_system::Call::remark { remark: vec![] };

// Should fail with non-EthTransaction origin
assert_noop!(
Pallet::<Test>::eth_substrate_call(
RuntimeOrigin::signed(ALICE),
Box::new(inner_call.into()),
vec![]
),
sp_runtime::traits::BadOrigin
);
});
}

#[test]
fn eth_substrate_call_tracks_weight_correctly() {
use crate::weights::WeightInfo;
ExtBuilder::default().build().execute_with(|| {
let _ = <Test as Config>::Currency::set_balance(&ALICE, 1000);

let inner_call = frame_system::Call::remark { remark: vec![0u8; 100] };
let transaction_encoded = vec![];
let transaction_encoded_len = transaction_encoded.len() as u32;

let result = Pallet::<Test>::eth_substrate_call(
Origin::EthTransaction(ALICE).into(),
Box::new(inner_call.clone().into()),
transaction_encoded,
);

assert_ok!(result);
let post_info = result.unwrap();

let overhead = <Test as Config>::WeightInfo::eth_substrate_call(transaction_encoded_len);
let expected_weight = overhead.saturating_add(inner_call.get_dispatch_info().call_weight);
assert!(
expected_weight == post_info.actual_weight.unwrap(),
"expected_weight ({}) should be == actual_weight ({})",
expected_weight,
post_info.actual_weight.unwrap(),
);
Comment thread
marian-radu marked this conversation as resolved.
});
}
Loading
Loading