Skip to content

Commit 5407c6e

Browse files
0xRVERobert van Eerdewijkgithub-actions[bot]athei
authored andcommitted
EIP-3607 added check to make sure a contract account cannot transfer funds as an EOA account (#9717)
fixes #9570 --------- Co-authored-by: Robert van Eerdewijk <[email protected]> Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Alexander Theißen <[email protected]>
1 parent 6f40cfb commit 5407c6e

6 files changed

Lines changed: 159 additions & 16 deletions

File tree

prdoc/pr_9717.prdoc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
title: EIP-3607 added check to make sure a contract account cannot transfer funds
2+
as an EOA account
3+
doc:
4+
- audience: Runtime Dev
5+
description: fixes https://github.com/paritytech/polkadot-sdk/issues/9570
6+
crates:
7+
- name: pallet-revive
8+
bump: patch
9+
- name: pallet-assets
10+
bump: patch

substrate/frame/assets/src/precompiles.rs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -352,8 +352,8 @@ mod test {
352352
hex::const_decode_to_array(b"0000000000000000000000000000000001200000").unwrap(),
353353
);
354354

355-
let from = 1;
356-
let to = 2;
355+
let from = 123456789;
356+
let to = 987654321;
357357

358358
Balances::make_free_balance_be(&from, 100);
359359
Balances::make_free_balance_be(&to, 100);
@@ -367,7 +367,7 @@ mod test {
367367
IERC20::transferCall { to: to_addr.0.into(), value: U256::from(10) }.abi_encode();
368368

369369
pallet_revive::Pallet::<Test>::bare_call(
370-
RuntimeOrigin::signed(1),
370+
RuntimeOrigin::signed(from),
371371
H160::from(asset_addr),
372372
0u32.into(),
373373
Weight::MAX,
@@ -396,14 +396,16 @@ mod test {
396396
let asset_addr =
397397
hex::const_decode_to_array(b"0000000000000000000000000000000001200000").unwrap();
398398

399-
Balances::make_free_balance_be(&1, 100);
400-
assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, 1, true, 1));
401-
assert_ok!(Assets::mint(RuntimeOrigin::signed(1), asset_id, 1, 1000));
399+
let owner = 123456789;
400+
401+
Balances::make_free_balance_be(&owner, 100);
402+
assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, owner, true, 1));
403+
assert_ok!(Assets::mint(RuntimeOrigin::signed(owner), asset_id, owner, 1000));
402404

403405
let data = IERC20::totalSupplyCall {}.abi_encode();
404406

405407
let data = pallet_revive::Pallet::<Test>::bare_call(
406-
RuntimeOrigin::signed(1),
408+
RuntimeOrigin::signed(owner),
407409
H160::from(asset_addr),
408410
0u32.into(),
409411
Weight::MAX,
@@ -426,15 +428,17 @@ mod test {
426428
let asset_addr =
427429
hex::const_decode_to_array(b"0000000000000000000000000000000001200000").unwrap();
428430

429-
Balances::make_free_balance_be(&1, 100);
430-
assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, 1, true, 1));
431-
assert_ok!(Assets::mint(RuntimeOrigin::signed(1), asset_id, 1, 1000));
431+
let owner = 123456789;
432432

433-
let account = <Test as pallet_revive::Config>::AddressMapper::to_address(&1).0.into();
433+
assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, owner, true, 1));
434+
assert_ok!(Assets::mint(RuntimeOrigin::signed(owner), asset_id, owner, 1000));
435+
436+
let account =
437+
<Test as pallet_revive::Config>::AddressMapper::to_address(&owner).0.into();
434438
let data = IERC20::balanceOfCall { account }.abi_encode();
435439

436440
let data = pallet_revive::Pallet::<Test>::bare_call(
437-
RuntimeOrigin::signed(1),
441+
RuntimeOrigin::signed(owner),
438442
H160::from(asset_addr),
439443
0u32.into(),
440444
Weight::MAX,
@@ -460,9 +464,9 @@ mod test {
460464
hex::const_decode_to_array(b"0000000000000000000000000000000001200000").unwrap(),
461465
);
462466

463-
let owner = 1;
464-
let spender = 2;
465-
let other = 3;
467+
let owner = 123456789;
468+
let spender = 987654321;
469+
let other = 1122334455;
466470

467471
Balances::make_free_balance_be(&owner, 100);
468472
Balances::make_free_balance_be(&spender, 100);

substrate/frame/revive/src/exec.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2189,6 +2189,16 @@ where
21892189
}
21902190
}
21912191

2192+
/// Returns true if the address has a precompile contract, else false.
2193+
pub fn is_precompile<T: Config, E: Executable<T>>(address: &H160) -> bool
2194+
where
2195+
BalanceOf<T>: Into<U256> + TryFrom<U256>,
2196+
MomentOf<T>: Into<U256>,
2197+
T::Hash: frame_support::traits::IsType<H256>,
2198+
{
2199+
<AllPrecompiles<T>>::get::<Stack<'_, T, E>>(address.as_fixed_bytes()).is_some()
2200+
}
2201+
21922202
mod sealing {
21932203
use super::*;
21942204

substrate/frame/revive/src/lib.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,6 +1193,9 @@ where
11931193
storage_deposit_limit: DepositLimit<BalanceOf<T>>,
11941194
data: Vec<u8>,
11951195
) -> ContractResult<ExecReturnValue, BalanceOf<T>> {
1196+
if let Err(contract_result) = Self::ensure_non_contract_if_signed(&origin) {
1197+
return contract_result;
1198+
}
11961199
let mut gas_meter = GasMeter::new(gas_limit);
11971200
let mut storage_deposit = Default::default();
11981201

@@ -1250,6 +1253,10 @@ where
12501253
salt: Option<[u8; 32]>,
12511254
bump_nonce: BumpNonce,
12521255
) -> ContractResult<InstantiateReturnValue, BalanceOf<T>> {
1256+
// Enforce EIP-3607 for top-level signed origins: deny signed contract addresses.
1257+
if let Err(contract_result) = Self::ensure_non_contract_if_signed(&origin) {
1258+
return contract_result;
1259+
}
12531260
let mut gas_meter = GasMeter::new(gas_limit);
12541261
let mut storage_deposit = Default::default();
12551262
let unchecked_deposit_limit = storage_deposit_limit.is_unchecked();
@@ -1727,6 +1734,47 @@ where
17271734
.saturating_mul(T::NativeToEthRatio::get().into())
17281735
.saturating_add(dust.into())
17291736
}
1737+
1738+
/// Ensure the origin has no code deplyoyed if it is a signed origin.
1739+
fn ensure_non_contract_if_signed<ReturnValue>(
1740+
origin: &OriginFor<T>,
1741+
) -> Result<(), ContractResult<ReturnValue, BalanceOf<T>>> {
1742+
use crate::exec::is_precompile;
1743+
let Ok(who) = ensure_signed(origin.clone()) else { return Ok(()) };
1744+
let address = <T::AddressMapper as AddressMapper<T>>::to_address(&who);
1745+
1746+
// EIP_1052: precompile can never be used as EOA.
1747+
if is_precompile::<T, ContractBlob<T>>(&address) {
1748+
log::debug!(
1749+
target: crate::LOG_TARGET,
1750+
"EIP-3607: reject externally-signed tx from precompile account {:?}",
1751+
address
1752+
);
1753+
return Err(ContractResult {
1754+
result: Err(DispatchError::BadOrigin),
1755+
gas_consumed: Weight::default(),
1756+
gas_required: Weight::default(),
1757+
storage_deposit: Default::default(),
1758+
});
1759+
}
1760+
1761+
// Deployed code exists when hash is neither zero (no account) nor EMPTY_CODE_HASH
1762+
// (account exists but no code).
1763+
if <AccountInfo<T>>::is_contract(&address) {
1764+
log::debug!(
1765+
target: crate::LOG_TARGET,
1766+
"EIP-3607: reject externally-signed tx from contract account {:?}",
1767+
address
1768+
);
1769+
return Err(ContractResult {
1770+
result: Err(DispatchError::BadOrigin),
1771+
gas_consumed: Weight::default(),
1772+
gas_required: Weight::default(),
1773+
storage_deposit: Default::default(),
1774+
});
1775+
}
1776+
Ok(())
1777+
}
17301778
}
17311779

17321780
impl<T: Config> Pallet<T> {

substrate/frame/revive/src/storage.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ impl<T: Config> From<ContractInfo<T>> for AccountType<T> {
152152

153153
impl<T: Config> AccountInfo<T> {
154154
/// Returns true if the account is a contract.
155-
fn is_contract(address: &H160) -> bool {
155+
pub fn is_contract(address: &H160) -> bool {
156156
let Some(info) = <AccountInfoOf<T>>::get(address) else { return false };
157157
matches!(info.account_type, AccountType::Contract(_))
158158
}

substrate/frame/revive/src/tests/pvm.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4992,3 +4992,74 @@ fn return_data_limit_is_enforced() {
49924992
}
49934993
});
49944994
}
4995+
4996+
/// EIP-3607
4997+
/// Test that a top-level signed transaction that uses a contract address as the signer is rejected.
4998+
#[test]
4999+
fn reject_signed_tx_from_contract_address() {
5000+
let (binary, _code_hash) = compile_module("dummy").unwrap();
5001+
5002+
ExtBuilder::default().existential_deposit(200).build().execute_with(|| {
5003+
let _ = <Test as Config>::Currency::set_balance(&ALICE, 1_000_000);
5004+
5005+
let Contract { addr: contract_addr, account_id: contract_account_id, .. } =
5006+
builder::bare_instantiate(Code::Upload(binary)).build_and_unwrap_contract();
5007+
5008+
assert!(AccountInfoOf::<Test>::contains_key(&contract_addr));
5009+
5010+
let call_result = builder::bare_call(BOB_ADDR)
5011+
.native_value(1)
5012+
.origin(RuntimeOrigin::signed(contract_account_id.clone()))
5013+
.build();
5014+
assert_err!(call_result.result, DispatchError::BadOrigin);
5015+
5016+
let instantiate_result = builder::bare_instantiate(Code::Upload(Vec::new()))
5017+
.origin(RuntimeOrigin::signed(contract_account_id))
5018+
.build();
5019+
assert_err!(instantiate_result.result, DispatchError::BadOrigin);
5020+
});
5021+
}
5022+
5023+
#[test]
5024+
fn reject_signed_tx_from_primitive_precompile_address() {
5025+
ExtBuilder::default().existential_deposit(200).build().execute_with(|| {
5026+
// blake2f precompile address
5027+
let precompile_addr = H160::from_low_u64_be(9);
5028+
let fake_account = <Test as Config>::AddressMapper::to_account_id(&precompile_addr);
5029+
let _ = <Test as Config>::Currency::set_balance(&ALICE, 1_000_000);
5030+
let _ = <Test as Config>::Currency::set_balance(&fake_account, 1_000_000);
5031+
5032+
let call_result = builder::bare_call(BOB_ADDR)
5033+
.native_value(1)
5034+
.origin(RuntimeOrigin::signed(fake_account.clone()))
5035+
.build();
5036+
assert_err!(call_result.result, DispatchError::BadOrigin);
5037+
5038+
let instantiate_result = builder::bare_instantiate(Code::Upload(Vec::new()))
5039+
.origin(RuntimeOrigin::signed(fake_account))
5040+
.build();
5041+
assert_err!(instantiate_result.result, DispatchError::BadOrigin);
5042+
});
5043+
}
5044+
5045+
#[test]
5046+
fn reject_signed_tx_from_builtin_precompile_address() {
5047+
ExtBuilder::default().existential_deposit(200).build().execute_with(|| {
5048+
// system precompile address
5049+
let precompile_addr = H160::from_low_u64_be(0x900);
5050+
let fake_account = <Test as Config>::AddressMapper::to_account_id(&precompile_addr);
5051+
let _ = <Test as Config>::Currency::set_balance(&ALICE, 1_000_000);
5052+
let _ = <Test as Config>::Currency::set_balance(&fake_account, 1_000_000);
5053+
5054+
let call_result = builder::bare_call(BOB_ADDR)
5055+
.native_value(1)
5056+
.origin(RuntimeOrigin::signed(fake_account.clone()))
5057+
.build();
5058+
assert_err!(call_result.result, DispatchError::BadOrigin);
5059+
5060+
let instantiate_result = builder::bare_instantiate(Code::Upload(Vec::new()))
5061+
.origin(RuntimeOrigin::signed(fake_account))
5062+
.build();
5063+
assert_err!(instantiate_result.result, DispatchError::BadOrigin);
5064+
});
5065+
}

0 commit comments

Comments
 (0)