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
12 changes: 12 additions & 0 deletions prdoc/pr_10100.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
title: 'pallet_revive: Only enforce EIP-3607 for dispatchables'
doc:
- audience: Runtime Dev
description: |-
The EIP states that contract addresses cannot be the origin of a transaction. However, we are enforcing this rule for all contract execution (i.e all public function on the pallet). This is a problem for code that uses `pallet_revive` and explicitly wants to allow this.

This PR now only enforces this check for dispatchables so that all the `bare_*` functions are unaffected.

As a drive-by a regrouped the functions on the `Pallet` so that the public functions are no longer interleaved with the private ones. This got mixed up when we resolved some merge conflicts.
crates:
- name: pallet-revive
bump: patch
146 changes: 65 additions & 81 deletions substrate/frame/revive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,7 @@ pub mod pallet {
#[pallet::compact] storage_deposit_limit: BalanceOf<T>,
data: Vec<u8>,
) -> DispatchResultWithPostInfo {
Self::ensure_non_contract_if_signed(&origin)?;
let mut output = Self::bare_call(
origin,
dest,
Expand Down Expand Up @@ -1072,6 +1073,7 @@ pub mod pallet {
data: Vec<u8>,
salt: Option<[u8; 32]>,
) -> DispatchResultWithPostInfo {
Self::ensure_non_contract_if_signed(&origin)?;
let data_len = data.len() as u32;
let mut output = Self::bare_instantiate(
origin,
Expand Down Expand Up @@ -1136,6 +1138,7 @@ pub mod pallet {
data: Vec<u8>,
salt: Option<[u8; 32]>,
) -> DispatchResultWithPostInfo {
Self::ensure_non_contract_if_signed(&origin)?;
let code_len = code.len() as u32;
let data_len = data.len() as u32;
let mut output = Self::bare_instantiate(
Expand Down Expand Up @@ -1197,6 +1200,7 @@ pub mod pallet {
encoded_len: u32,
) -> DispatchResultWithPostInfo {
let origin = Self::ensure_eth_origin(origin)?;
Self::ensure_non_contract_if_signed(&origin)?;
let mut call = Call::<T>::eth_instantiate_with_code {
value,
gas_limit,
Expand Down Expand Up @@ -1267,6 +1271,7 @@ pub mod pallet {
encoded_len: u32,
) -> DispatchResultWithPostInfo {
let origin = Self::ensure_eth_origin(origin)?;
Self::ensure_non_contract_if_signed(&origin)?;
let mut call = Call::<T>::eth_call {
dest,
value,
Expand Down Expand Up @@ -1342,6 +1347,7 @@ pub mod pallet {
code: Vec<u8>,
#[pallet::compact] storage_deposit_limit: BalanceOf<T>,
) -> DispatchResult {
Self::ensure_non_contract_if_signed(&origin)?;
Self::bare_upload_code(origin, code, storage_deposit_limit).map(|_| ())
}

Expand Down Expand Up @@ -1403,6 +1409,7 @@ pub mod pallet {
#[pallet::call_index(7)]
#[pallet::weight(<T as Config>::WeightInfo::map_account())]
pub fn map_account(origin: OriginFor<T>) -> DispatchResult {
Self::ensure_non_contract_if_signed(&origin)?;
let origin = ensure_signed(origin)?;
T::AddressMapper::map(&origin)
}
Expand Down Expand Up @@ -1435,6 +1442,7 @@ pub mod pallet {
origin: OriginFor<T>,
call: Box<<T as Config>::RuntimeCall>,
) -> DispatchResultWithPostInfo {
Self::ensure_non_contract_if_signed(&origin)?;
let origin = ensure_signed(origin)?;
let unmapped_account =
T::AddressMapper::to_fallback_account_id(&T::AddressMapper::to_address(&origin));
Expand Down Expand Up @@ -1475,9 +1483,6 @@ impl<T: Config> Pallet<T> {
data: Vec<u8>,
exec_config: ExecConfig,
) -> ContractResult<ExecReturnValue, BalanceOf<T>> {
if let Err(contract_result) = Self::ensure_non_contract_if_signed(&origin) {
return contract_result;
}
let mut gas_meter = GasMeter::new(gas_limit);
let mut storage_deposit = Default::default();

Expand Down Expand Up @@ -1534,10 +1539,6 @@ impl<T: Config> Pallet<T> {
salt: Option<[u8; 32]>,
exec_config: ExecConfig,
) -> ContractResult<InstantiateReturnValue, BalanceOf<T>> {
// Enforce EIP-3607 for top-level signed origins: deny signed contract addresses.
if let Err(contract_result) = Self::ensure_non_contract_if_signed(&origin) {
return contract_result;
}
let mut gas_meter = GasMeter::new(gas_limit);
let mut storage_deposit = Default::default();
let try_instantiate = || {
Expand Down Expand Up @@ -2084,6 +2085,39 @@ impl<T: Config> Pallet<T> {
.map_err(ContractAccessError::StorageWriteFailed)
}

/// Pallet account, used to hold funds for contracts upload deposit.
pub fn account_id() -> T::AccountId {
use frame_support::PalletId;
use sp_runtime::traits::AccountIdConversion;
PalletId(*b"py/reviv").into_account_truncating()
}

/// The address of the validator that produced the current block.
pub fn block_author() -> H160 {
use frame_support::traits::FindAuthor;

let digest = <frame_system::Pallet<T>>::digest();
let pre_runtime_digests = digest.logs.iter().filter_map(|d| d.as_pre_runtime());

T::FindAuthor::find_author(pre_runtime_digests)
.map(|account_id| T::AddressMapper::to_address(&account_id))
.unwrap_or_default()
}

/// Returns the code at `address`.
///
/// This takes pre-compiles into account.
pub fn code(address: &H160) -> Vec<u8> {
use precompiles::{All, Precompiles};
if let Some(code) = <All<T>>::code(address.as_fixed_bytes()) {
return code.into()
}
AccountInfo::<T>::load_contract(&address)
.and_then(|contract| <PristineCode<T>>::get(contract.code_hash))
.map(|code| code.into())
.unwrap_or_default()
}

/// Uploads new code and returns the Vm binary contract blob and deposit amount collected.
fn try_upload_pvm_code(
origin: T::AccountId,
Expand Down Expand Up @@ -2121,80 +2155,6 @@ impl<T: Config> Pallet<T> {
T::FeeInfo::weight_to_fee(&weight, Combinator::Max).into()
}

/// Ensure the origin has no code deplyoyed if it is a signed origin.
fn ensure_non_contract_if_signed<ReturnValue>(
origin: &OriginFor<T>,
) -> Result<(), ContractResult<ReturnValue, BalanceOf<T>>> {
use crate::exec::is_precompile;
let Ok(who) = ensure_signed(origin.clone()) else { return Ok(()) };
let address = <T::AddressMapper as AddressMapper<T>>::to_address(&who);

// EIP_1052: precompile can never be used as EOA.
if is_precompile::<T, ContractBlob<T>>(&address) {
log::debug!(
target: crate::LOG_TARGET,
"EIP-3607: reject externally-signed tx from precompile account {:?}",
address
);
return Err(ContractResult {
result: Err(DispatchError::BadOrigin),
gas_consumed: Weight::default(),
gas_required: Weight::default(),
storage_deposit: Default::default(),
});
}

// Deployed code exists when hash is neither zero (no account) nor EMPTY_CODE_HASH
// (account exists but no code).
if <AccountInfo<T>>::is_contract(&address) {
log::debug!(
target: crate::LOG_TARGET,
"EIP-3607: reject externally-signed tx from contract account {:?}",
address
);
return Err(ContractResult {
result: Err(DispatchError::BadOrigin),
gas_consumed: Weight::default(),
gas_required: Weight::default(),
storage_deposit: Default::default(),
});
}
Ok(())
}

/// Pallet account, used to hold funds for contracts upload deposit.
pub fn account_id() -> T::AccountId {
use frame_support::PalletId;
use sp_runtime::traits::AccountIdConversion;
PalletId(*b"py/reviv").into_account_truncating()
}

/// The address of the validator that produced the current block.
pub fn block_author() -> H160 {
use frame_support::traits::FindAuthor;

let digest = <frame_system::Pallet<T>>::digest();
let pre_runtime_digests = digest.logs.iter().filter_map(|d| d.as_pre_runtime());

T::FindAuthor::find_author(pre_runtime_digests)
.map(|account_id| T::AddressMapper::to_address(&account_id))
.unwrap_or_default()
}

/// Returns the code at `address`.
///
/// This takes pre-compiles into account.
pub fn code(address: &H160) -> Vec<u8> {
use precompiles::{All, Precompiles};
if let Some(code) = <All<T>>::code(address.as_fixed_bytes()) {
return code.into()
}
AccountInfo::<T>::load_contract(&address)
.and_then(|contract| <PristineCode<T>>::get(contract.code_hash))
.map(|code| code.into())
.unwrap_or_default()
}

/// Transfer a deposit from some account to another.
///
/// `from` is usually the transaction origin and `to` a contract or
Expand Down Expand Up @@ -2321,6 +2281,30 @@ impl<T: Config> Pallet<T> {
_ => Err(BadOrigin.into()),
}
}

/// Ensure that the origin is neither a pre-compile nor a contract.
///
/// This enforces EIP-3607.
fn ensure_non_contract_if_signed(origin: &OriginFor<T>) -> DispatchResult {
let Some(address) = origin
.as_system_ref()
.and_then(|o| o.as_signed())
.map(<T::AddressMapper as AddressMapper<T>>::to_address)
else {
return Ok(())
};
if exec::is_precompile::<T, ContractBlob<T>>(&address) ||
<AccountInfo<T>>::is_contract(&address)
{
log::debug!(
target: crate::LOG_TARGET,
"EIP-3607: reject tx as pre-compile or account exist at {address:?}",
);
Err(DispatchError::BadOrigin)
} else {
Ok(())
}
}
}

/// The address used to call the runtime's pallets dispatchables
Expand Down
100 changes: 48 additions & 52 deletions substrate/frame/revive/src/tests/pvm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4942,74 +4942,70 @@ fn storage_deposit_from_hold_works() {
});
}

/// EIP-3607
/// Test that a top-level signed transaction that uses a contract address as the signer is rejected.
#[test]
fn reject_signed_tx_from_contract_address() {
fn eip3607_reject_tx_from_contract_or_precompile() {
let (binary, _code_hash) = compile_module("dummy").unwrap();

ExtBuilder::default().existential_deposit(200).build().execute_with(|| {
let _ = <Test as Config>::Currency::set_balance(&ALICE, 1_000_000);

let Contract { addr: contract_addr, account_id: contract_account_id, .. } =
// the origins from which we try to call a dispatchable
let Contract { addr: contract_addr, .. } =
builder::bare_instantiate(Code::Upload(binary)).build_and_unwrap_contract();
assert!(<AccountInfo<Test>>::is_contract(&contract_addr));
let blake2_addr = H160::from_low_u64_be(9);
let system_addr = H160::from_low_u64_be(0x900);
let addresses = [contract_addr, blake2_addr, system_addr];

assert!(AccountInfoOf::<Test>::contains_key(&contract_addr));
// used to test `dispatch_as_fallback_account`
let call = Box::new(RuntimeCall::Balances(pallet_balances::Call::transfer_all {
dest: EVE,
keep_alive: false,
}));

let call_result = builder::bare_call(BOB_ADDR)
.native_value(1)
.origin(RuntimeOrigin::signed(contract_account_id.clone()))
.build();
assert_err!(call_result.result, DispatchError::BadOrigin);
for address in addresses.iter() {
let origin = <Test as Config>::AddressMapper::to_fallback_account_id(address);

let instantiate_result = builder::bare_instantiate(Code::Upload(Vec::new()))
.origin(RuntimeOrigin::signed(contract_account_id))
.build();
assert_err!(instantiate_result.result, DispatchError::BadOrigin);
});
}
let result =
builder::call(BOB_ADDR).origin(RuntimeOrigin::signed(origin.clone())).build();
assert_err!(result, DispatchError::BadOrigin);

#[test]
fn reject_signed_tx_from_primitive_precompile_address() {
ExtBuilder::default().existential_deposit(200).build().execute_with(|| {
// blake2f precompile address
let precompile_addr = H160::from_low_u64_be(9);
let fake_account = <Test as Config>::AddressMapper::to_account_id(&precompile_addr);
let _ = <Test as Config>::Currency::set_balance(&ALICE, 1_000_000);
let _ = <Test as Config>::Currency::set_balance(&fake_account, 1_000_000);
let result = builder::eth_call(BOB_ADDR)
.origin(RuntimeOrigin::signed(origin.clone()))
.build();
assert_err!(result, DispatchError::BadOrigin);

let call_result = builder::bare_call(BOB_ADDR)
.native_value(1)
.origin(RuntimeOrigin::signed(fake_account.clone()))
.build();
assert_err!(call_result.result, DispatchError::BadOrigin);
let result = builder::instantiate(Default::default())
.origin(RuntimeOrigin::signed(origin.clone()))
.build();
assert_err!(result, DispatchError::BadOrigin);

let instantiate_result = builder::bare_instantiate(Code::Upload(Vec::new()))
.origin(RuntimeOrigin::signed(fake_account))
.build();
assert_err!(instantiate_result.result, DispatchError::BadOrigin);
});
}
let result = builder::eth_instantiate_with_code(Default::default())
.origin(RuntimeOrigin::signed(origin.clone()))
.build();
assert_err!(result, DispatchError::BadOrigin);

#[test]
fn reject_signed_tx_from_builtin_precompile_address() {
ExtBuilder::default().existential_deposit(200).build().execute_with(|| {
// system precompile address
let precompile_addr = H160::from_low_u64_be(0x900);
let fake_account = <Test as Config>::AddressMapper::to_account_id(&precompile_addr);
let _ = <Test as Config>::Currency::set_balance(&ALICE, 1_000_000);
let _ = <Test as Config>::Currency::set_balance(&fake_account, 1_000_000);
let result = builder::instantiate_with_code(Default::default())
.origin(RuntimeOrigin::signed(origin.clone()))
.build();
assert_err!(result, DispatchError::BadOrigin);

let call_result = builder::bare_call(BOB_ADDR)
.native_value(1)
.origin(RuntimeOrigin::signed(fake_account.clone()))
.build();
assert_err!(call_result.result, DispatchError::BadOrigin);
let result = <Pallet<Test>>::upload_code(
RuntimeOrigin::signed(origin.clone()),
Default::default(),
<BalanceOf<Test>>::MAX,
);
assert_err!(result, DispatchError::BadOrigin);

let instantiate_result = builder::bare_instantiate(Code::Upload(Vec::new()))
.origin(RuntimeOrigin::signed(fake_account))
.build();
assert_err!(instantiate_result.result, DispatchError::BadOrigin);
let result = <Pallet<Test>>::map_account(RuntimeOrigin::signed(origin.clone()));
assert_err!(result, DispatchError::BadOrigin);

let result = <Pallet<Test>>::dispatch_as_fallback_account(
RuntimeOrigin::signed(origin.clone()),
call.clone(),
);
assert_err!(result, DispatchError::BadOrigin);
}
});
}

Expand Down
Loading