Skip to content
Merged
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions substrate/frame/revive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ alloy-consensus = { workspace = true, default-features = true }
array-bytes = { workspace = true, default-features = true }
assert_matches = { workspace = true }
pretty_assertions = { workspace = true }
rstest = { workspace = true }
secp256k1 = { workspace = true, features = ["recovery"] }
serde_json = { workspace = true }
test-case = { workspace = true }
Expand Down
11 changes: 9 additions & 2 deletions substrate/frame/revive/src/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,25 @@ use sp_runtime::RuntimeDebug;
pub struct DebugSettings {
/// Whether to allow unlimited contract size.
allow_unlimited_contract_size: bool,
/// Whether to allow bypassing EIP-3607 (allowing transactions coming from contract accounts).
bypass_eip_3607: bool,
}

impl DebugSettings {
pub fn new(allow_unlimited_contract_size: bool) -> Self {
Self { allow_unlimited_contract_size }
pub fn new(allow_unlimited_contract_size: bool, bypass_eip_3607: bool) -> Self {
Self { allow_unlimited_contract_size, bypass_eip_3607 }
}

/// Returns true if unlimited contract size is allowed.
pub fn is_unlimited_contract_size_allowed<T: Config>() -> bool {
T::DebugEnabled::get() && DebugSettingsOf::<T>::get().allow_unlimited_contract_size
}

/// Returns true if transactions coming from contract accounts are allowed (bypassing EIP-3607)
pub fn bypass_eip_3607<T: Config>() -> bool {
T::DebugEnabled::get() && DebugSettingsOf::<T>::get().bypass_eip_3607
}

/// Write the debug settings to storage.
pub fn write_to_storage<T: Config>(&self) {
DebugSettingsOf::<T>::put(self);
Expand Down
2 changes: 1 addition & 1 deletion substrate/frame/revive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2376,7 +2376,7 @@ impl<T: Config> Pallet<T> {
return Ok(())
};
if exec::is_precompile::<T, ContractBlob<T>>(&address) ||
<AccountInfo<T>>::is_contract(&address)
(<AccountInfo<T>>::is_contract(&address) && !DebugSettings::bypass_eip_3607::<T>())
{
log::debug!(
target: crate::LOG_TARGET,
Expand Down
174 changes: 129 additions & 45 deletions substrate/frame/revive/src/tests/pvm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ use crate::{
tracing::trace,
weights::WeightInfo,
AccountInfo, AccountInfoOf, BalanceWithDust, Code, Combinator, Config, ContractInfo,
DeletionQueueCounter, Error, ExecConfig, HoldReason, Origin, Pallet, PristineCode,
StorageDeposit, H160,
DebugSettings, DeletionQueueCounter, Error, ExecConfig, HoldReason, Origin, Pallet,
PristineCode, StorageDeposit, H160,
};
use assert_matches::assert_matches;
use codec::Encode;
Expand All @@ -60,6 +60,7 @@ use frame_system::{EventRecord, Phase};
use pallet_revive_fixtures::compile_module;
use pallet_revive_uapi::{ReturnErrorCode as RuntimeReturnCode, ReturnFlags};
use pretty_assertions::{assert_eq, assert_ne};
use rstest::rstest;
use sp_core::U256;
use sp_io::hashing::blake2_256;
use sp_runtime::{
Expand Down Expand Up @@ -4944,71 +4945,154 @@ fn storage_deposit_from_hold_works() {
});
}

#[test]
fn eip3607_reject_tx_from_contract_or_precompile() {
#[rstest]
#[case(true)]
#[case(false)]
fn eip3607_reject_tx_from_contract_or_precompile(#[case] bypass_eip3607_for_contracts: bool) {
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 genesis_config = GenesisConfig::<Test> {
debug_settings: Some(DebugSettings::new(false, bypass_eip3607_for_contracts)),
..Default::default()
};

// 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];
ExtBuilder::default()
.genesis_config(Some(genesis_config))
.existential_deposit(200)
.build()
.execute_with(|| {
DebugFlag::set(bypass_eip3607_for_contracts);
let _ = <Test as Config>::Currency::set_balance(&ALICE, 1_000_000);

// used to test `dispatch_as_fallback_account`
let call = Box::new(RuntimeCall::Balances(pallet_balances::Call::transfer_all {
dest: EVE,
keep_alive: false,
}));
// 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 mut addresses = vec![blake2_addr, system_addr];

if !bypass_eip3607_for_contracts {
// If we are setting the bypass config to true, it will only take effect for
// contracts, not for precompiles. So if it's not true, we can also test for the
// contract.
addresses.push(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,
}));

for address in addresses.iter() {
let origin = <Test as Config>::AddressMapper::to_fallback_account_id(address);

let result =
builder::call(BOB_ADDR).origin(RuntimeOrigin::signed(origin.clone())).build();
assert_err!(result, DispatchError::BadOrigin);

let result = builder::eth_call(BOB_ADDR)
.origin(Origin::EthTransaction(origin.clone()).into())
.build();
assert_err!(result, DispatchError::BadOrigin);

let result = builder::instantiate(Default::default())
.origin(RuntimeOrigin::signed(origin.clone()))
.build();
assert_err!(result, DispatchError::BadOrigin);

let result = builder::eth_instantiate_with_code(Default::default())
.origin(Origin::EthTransaction(origin.clone()).into())
.build();
assert_err!(result, DispatchError::BadOrigin);

let result = builder::instantiate_with_code(Default::default())
.origin(RuntimeOrigin::signed(origin.clone()))
.build();
assert_err!(result, DispatchError::BadOrigin);

let result = <Pallet<Test>>::upload_code(
RuntimeOrigin::signed(origin.clone()),
Default::default(),
<BalanceOf<Test>>::MAX,
);
assert_err!(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);
}
});
}

#[test]
fn eip3607_allow_tx_from_contract_if_debug_setting_configured() {
let (binary, code_hash) = compile_module("dummy").unwrap();

let genesis_config = GenesisConfig::<Test> {
debug_settings: Some(DebugSettings::new(false, true)),
..Default::default()
};

ExtBuilder::default()
.genesis_config(Some(genesis_config))
.existential_deposit(200)
.build()
.execute_with(|| {
DebugFlag::set(true);

for address in addresses.iter() {
let origin = <Test as Config>::AddressMapper::to_fallback_account_id(address);
let _ = <Test as Config>::Currency::set_balance(&ALICE, 1_000_000);

// the origins from which we try to call a dispatchable
let Contract { addr: address, account_id } =
builder::bare_instantiate(Code::Upload(binary.clone())).build_and_unwrap_contract();
let _ = <Test as Config>::Currency::set_balance(&account_id, 10_000_000_000_000);

assert!(<AccountInfo<Test>>::is_contract(&address));

let origin = <Test as Config>::AddressMapper::to_fallback_account_id(&address);

let result =
builder::call(BOB_ADDR).origin(RuntimeOrigin::signed(origin.clone())).build();
assert_err!(result, DispatchError::BadOrigin);
assert_ok!(result);

let result = builder::eth_call(BOB_ADDR)
.origin(RuntimeOrigin::signed(origin.clone()))
.origin(Origin::EthTransaction(origin.clone()).into())
.build();
assert_err!(result, DispatchError::BadOrigin);
assert_ok!(result);

let result = builder::instantiate(Default::default())
let result = builder::instantiate(code_hash)
.origin(RuntimeOrigin::signed(origin.clone()))
.build();
assert_err!(result, DispatchError::BadOrigin);
assert_ok!(result);

let result = builder::eth_instantiate_with_code(Default::default())
.origin(RuntimeOrigin::signed(origin.clone()))
let result = builder::eth_instantiate_with_code(binary.clone())
.origin(Origin::EthTransaction(origin.clone()).into())
.build();
assert_err!(result, DispatchError::BadOrigin);
assert_ok!(result);

let result = builder::instantiate_with_code(Default::default())
.origin(RuntimeOrigin::signed(origin.clone()))
.build();
assert_err!(result, DispatchError::BadOrigin);

let result = <Pallet<Test>>::upload_code(
let result = <Pallet<Test>>::dispatch_as_fallback_account(
RuntimeOrigin::signed(origin.clone()),
Default::default(),
<BalanceOf<Test>>::MAX,
Box::new(RuntimeCall::Balances(pallet_balances::Call::transfer_all {
dest: EVE,
keep_alive: false,
})),
);
assert_err!(result, DispatchError::BadOrigin);

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

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

#[test]
Expand Down
2 changes: 1 addition & 1 deletion substrate/frame/revive/src/tests/sol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ fn eth_contract_too_large() {

// Initialize genesis config with allow_unlimited_contract_size
let genesis_config = GenesisConfig::<Test> {
debug_settings: Some(DebugSettings::new(allow_unlimited_contract_size)),
debug_settings: Some(DebugSettings::new(allow_unlimited_contract_size, false)),
..Default::default()
};

Expand Down
Loading