Skip to main content

pallet_revive/
address.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Functions that deal contract addresses.
19
20use crate::{Config, Error, HoldReason, OriginalAccount, ensure};
21use alloc::vec::Vec;
22use core::marker::PhantomData;
23use frame_support::traits::{
24	OnKilledAccount, OnNewAccount, fungible::MutateHold, tokens::Precision,
25};
26use sp_core::{Get, H160};
27use sp_io::hashing::keccak_256;
28use sp_runtime::{AccountId32, DispatchResult, Saturating};
29
30/// Map between the native chain account id `T` and an Ethereum [`H160`].
31///
32/// This trait exists only to emulate specialization for different concrete
33/// native account ids. **Not** to make the mapping user configurable. Hence
34/// the trait is `Sealed` and depending on your runtime configuration you need
35/// to pick either [`AccountId32Mapper`] or [`H160Mapper`]. Picking the wrong
36/// one will result in a compilation error. No footguns here.
37///
38/// Please note that we assume that the native account is at least 20 bytes and
39/// only implement this type for a `T` where this is the case. Luckily, this is the
40/// case for all existing runtimes as of right now. Reasoning is that this will allow
41/// us to reverse an address -> account_id mapping by just stripping the prefix.
42///
43/// We require the mapping to be reversible. Since we are potentially dealing with types of
44/// different sizes one direction of the mapping is necessarily lossy. This requires the mapping to
45/// make use of the [`OriginalAccount`] storage item to reverse the mapping.
46pub trait AddressMapper<T: Config>: private::Sealed {
47	/// Convert an account id to an ethereum address.
48	fn to_address(account_id: &T::AccountId) -> H160;
49
50	/// Convert an ethereum address to a native account id.
51	fn to_account_id(address: &H160) -> T::AccountId;
52
53	/// Same as [`Self::to_account_id`] but always returns the fallback account.
54	///
55	/// This skips the query into [`OriginalAccount`] and always returns the stateless
56	/// fallback account. This is useful when we know for a fact that the `address`
57	/// in question is originally a `H160`. This is usually only the case when we
58	/// generated a new contract address.
59	fn to_fallback_account_id(address: &H160) -> T::AccountId;
60
61	/// Create a stateful mapping for `account_id`
62	///
63	/// This will enable `to_account_id` to map back to the original
64	/// `account_id` instead of the fallback account id.
65	fn map(account_id: &T::AccountId) -> DispatchResult;
66
67	/// Map an account id without taking any deposit.
68	/// This is only useful for genesis configuration, or benchmarks.
69	fn map_no_deposit(account_id: &T::AccountId) -> DispatchResult {
70		Self::map(account_id)
71	}
72
73	/// Remove the mapping in order to reclaim the deposit.
74	///
75	/// There is no reason why one would unmap their `account_id` except
76	/// for reclaiming the deposit.
77	fn unmap(account_id: &T::AccountId) -> DispatchResult;
78
79	/// Returns true if the `account_id` is usable as an origin.
80	///
81	/// This means either the `account_id` doesn't require a stateful mapping
82	/// or a stateful mapping exists.
83	fn is_mapped(account_id: &T::AccountId) -> bool;
84
85	/// Returns true if the account is derived from an eth (secp256k1) key.
86	///
87	/// These accounts don't need a stateful mapping and never hold a mapping deposit.
88	fn is_eth_derived(account_id: &T::AccountId) -> bool;
89}
90
91mod private {
92	pub trait Sealed {}
93	impl<T> Sealed for super::AccountId32Mapper<T> {}
94	impl<T> Sealed for super::H160Mapper<T> {}
95	impl<T> Sealed for super::TestAccountMapper<T> {}
96}
97
98/// The mapper to be used if the account id is `AccountId32`.
99///
100/// It converts between addresses by either hash then truncate the last 12 bytes or
101/// suffixing them. To recover the original account id of a hashed and truncated account id we use
102/// [`OriginalAccount`] and will fall back to all `0xEE` if account was found. This means contracts
103/// and plain wallets controlled by an `secp256k1` always have a `0xEE` suffixed account.
104pub struct AccountId32Mapper<T>(PhantomData<T>);
105
106/// The mapper to be used if the account id is `H160`.
107///
108/// It just trivially returns its inputs and doesn't make use of any state.
109#[allow(dead_code)]
110pub struct H160Mapper<T>(PhantomData<T>);
111
112/// An account mapper that can be used for testing u64 account ids.
113pub struct TestAccountMapper<T>(PhantomData<T>);
114
115impl<T> AddressMapper<T> for AccountId32Mapper<T>
116where
117	T: Config<AccountId = AccountId32>,
118{
119	fn to_address(account_id: &AccountId32) -> H160 {
120		let account_bytes: &[u8; 32] = account_id.as_ref();
121		if Self::is_eth_derived(account_id) {
122			// this was originally an eth address
123			// we just strip the 0xEE suffix to get the original address
124			H160::from_slice(&account_bytes[..20])
125		} else {
126			// this is an (ed|sr)25510 derived address
127			// avoid truncating the public key by hashing it first
128			let account_hash = keccak_256(account_bytes);
129			H160::from_slice(&account_hash[12..])
130		}
131	}
132
133	fn to_account_id(address: &H160) -> AccountId32 {
134		<OriginalAccount<T>>::get(address).unwrap_or_else(|| Self::to_fallback_account_id(address))
135	}
136
137	fn to_fallback_account_id(address: &H160) -> AccountId32 {
138		let mut account_id = AccountId32::new([0xEE; 32]);
139		let account_bytes: &mut [u8; 32] = account_id.as_mut();
140		account_bytes[..20].copy_from_slice(address.as_bytes());
141		account_id
142	}
143
144	fn map(account_id: &T::AccountId) -> DispatchResult {
145		ensure!(!Self::is_mapped(account_id), <Error<T>>::AccountAlreadyMapped);
146
147		// each mapping entry stores the address (20 bytes) and the account id (32 bytes)
148		let deposit = T::DepositPerByte::get()
149			.saturating_mul(52u32.into())
150			.saturating_add(T::DepositPerItem::get());
151		T::Currency::hold(&HoldReason::AddressMapping.into(), account_id, deposit)?;
152
153		<OriginalAccount<T>>::insert(Self::to_address(account_id), account_id);
154		Ok(())
155	}
156
157	fn map_no_deposit(account_id: &T::AccountId) -> DispatchResult {
158		ensure!(!Self::is_mapped(account_id), <Error<T>>::AccountAlreadyMapped);
159		<OriginalAccount<T>>::insert(Self::to_address(account_id), account_id);
160		Ok(())
161	}
162
163	fn unmap(account_id: &T::AccountId) -> DispatchResult {
164		// will do nothing if address is not mapped so no check required
165		<OriginalAccount<T>>::remove(Self::to_address(account_id));
166		T::Currency::release_all(
167			&HoldReason::AddressMapping.into(),
168			account_id,
169			Precision::BestEffort,
170		)?;
171		Ok(())
172	}
173
174	fn is_mapped(account_id: &T::AccountId) -> bool {
175		Self::is_eth_derived(account_id) ||
176			<OriginalAccount<T>>::contains_key(Self::to_address(account_id))
177	}
178
179	/// This is a stateless check that just compares the last 12 bytes. Please note that it is
180	/// theoretically possible to create an ed25519 keypair that passes this filter. However,
181	/// this can't be used for an attack. It also won't happen by accident since everybody is
182	/// using sr25519 where this is not a valid public key.
183	fn is_eth_derived(account_id: &T::AccountId) -> bool {
184		let account_bytes: &[u8; 32] = account_id.as_ref();
185		&account_bytes[20..] == &[0xEE; 12]
186	}
187}
188
189impl<T> AddressMapper<T> for TestAccountMapper<T>
190where
191	T: Config<AccountId = u64>,
192{
193	fn to_address(account_id: &T::AccountId) -> H160 {
194		let mut bytes = [0u8; 20];
195		bytes[12..].copy_from_slice(&account_id.to_be_bytes());
196		H160::from(bytes)
197	}
198
199	fn to_account_id(address: &H160) -> T::AccountId {
200		Self::to_fallback_account_id(address)
201	}
202
203	fn to_fallback_account_id(address: &H160) -> T::AccountId {
204		u64::from_be_bytes(address.as_ref()[12..].try_into().unwrap())
205	}
206
207	fn map(_account_id: &T::AccountId) -> DispatchResult {
208		Ok(())
209	}
210
211	fn unmap(_account_id: &T::AccountId) -> DispatchResult {
212		Ok(())
213	}
214
215	fn is_mapped(_account_id: &T::AccountId) -> bool {
216		true
217	}
218
219	fn is_eth_derived(_account_id: &T::AccountId) -> bool {
220		false
221	}
222}
223
224impl<T> AddressMapper<T> for H160Mapper<T>
225where
226	T: Config,
227	crate::AccountIdOf<T>: AsRef<[u8; 20]> + From<H160>,
228{
229	fn to_address(account_id: &T::AccountId) -> H160 {
230		H160::from_slice(account_id.as_ref())
231	}
232
233	fn to_account_id(address: &H160) -> T::AccountId {
234		Self::to_fallback_account_id(address)
235	}
236
237	fn to_fallback_account_id(address: &H160) -> T::AccountId {
238		(*address).into()
239	}
240
241	fn map(_account_id: &T::AccountId) -> DispatchResult {
242		Ok(())
243	}
244
245	fn unmap(_account_id: &T::AccountId) -> DispatchResult {
246		Ok(())
247	}
248
249	fn is_mapped(_account_id: &T::AccountId) -> bool {
250		true
251	}
252
253	fn is_eth_derived(_account_id: &T::AccountId) -> bool {
254		true
255	}
256}
257
258/// Determine the address of a contract using CREATE semantics.
259pub fn create1(deployer: &H160, nonce: u64) -> H160 {
260	let mut list = rlp::RlpStream::new_list(2);
261	list.append(&deployer.as_bytes());
262	list.append(&nonce);
263	let hash = keccak_256(&list.out());
264	H160::from_slice(&hash[12..])
265}
266
267/// Determine the address of a contract using the CREATE2 semantics.
268pub fn create2(deployer: &H160, code: &[u8], input_data: &[u8], salt: &[u8; 32]) -> H160 {
269	let init_code_hash = {
270		let init_code: Vec<u8> = code.into_iter().chain(input_data).cloned().collect();
271		keccak_256(init_code.as_ref())
272	};
273	let mut bytes = [0; 85];
274	bytes[0] = 0xff;
275	bytes[1..21].copy_from_slice(deployer.as_bytes());
276	bytes[21..53].copy_from_slice(salt);
277	bytes[53..85].copy_from_slice(&init_code_hash);
278	let hash = keccak_256(&bytes);
279	H160::from_slice(&hash[12..])
280}
281
282pub struct AutoMapper<T>(PhantomData<T>);
283
284impl<T: Config> OnNewAccount<T::AccountId> for AutoMapper<T> {
285	fn on_new_account(who: &T::AccountId) {
286		if T::AutoMap::get() &&
287			!T::AddressMapper::is_eth_derived(who) &&
288			let Err(err) = T::AddressMapper::map_no_deposit(who)
289		{
290			log::warn!(
291				target: crate::LOG_TARGET,
292				"Failed to auto-map account {who:?}: {err:?}",
293			);
294		}
295	}
296}
297
298impl<T: Config> OnKilledAccount<T::AccountId> for AutoMapper<T> {
299	fn on_killed_account(who: &T::AccountId) {
300		if T::AutoMap::get() &&
301			!T::AddressMapper::is_eth_derived(who) &&
302			let Err(err) = T::AddressMapper::unmap(who)
303		{
304			log::warn!(
305				target: crate::LOG_TARGET,
306				"Failed to auto-unmap account {who:?}: {err:?}",
307			);
308		}
309	}
310}
311
312#[cfg(test)]
313mod test {
314	use super::*;
315	use crate::{
316		AddressMapper, Error, Pallet,
317		test_utils::*,
318		tests::{AutoMapFlag, ExtBuilder, RuntimeOrigin, Test},
319	};
320	use frame_support::{
321		assert_err,
322		dispatch::Pays,
323		traits::fungible::{InspectHold, Mutate},
324	};
325	use pretty_assertions::assert_eq;
326	use sp_core::{H160, hex2array};
327
328	#[test]
329	fn create1_works() {
330		assert_eq!(
331			create1(&ALICE_ADDR, 1u64),
332			H160(hex2array!("c851da37e4e8d3a20d8d56be2963934b4ad71c3b")),
333		)
334	}
335
336	#[test]
337	fn create2_works() {
338		assert_eq!(
339			create2(
340				&ALICE_ADDR,
341				&hex2array!("600060005560016000"),
342				&hex2array!("55"),
343				&hex2array!("1234567890123456789012345678901234567890123456789012345678901234")
344			),
345			H160(hex2array!("7f31e795e5836a19a8f919ab5a9de9a197ecd2b6")),
346		)
347	}
348
349	#[test]
350	fn fallback_map_works() {
351		assert!(<Test as Config>::AddressMapper::is_mapped(&ALICE));
352		assert_eq!(
353			ALICE_FALLBACK,
354			<Test as Config>::AddressMapper::to_fallback_account_id(&ALICE_ADDR)
355		);
356		assert_eq!(ALICE_ADDR, <Test as Config>::AddressMapper::to_address(&ALICE_FALLBACK));
357	}
358
359	#[test]
360	fn map_works() {
361		ExtBuilder::default().build().execute_with(|| {
362			<Test as Config>::Currency::set_balance(&EVE, 1_000_000);
363			// before mapping the fallback account is returned
364			assert!(!<Test as Config>::AddressMapper::is_mapped(&EVE));
365			assert_eq!(EVE_FALLBACK, <Test as Config>::AddressMapper::to_account_id(&EVE_ADDR));
366			assert_eq!(
367				<Test as Config>::Currency::balance_on_hold(
368					&HoldReason::AddressMapping.into(),
369					&EVE
370				),
371				0
372			);
373
374			// when mapped the full account id is returned
375			<Test as Config>::AddressMapper::map(&EVE).unwrap();
376			assert!(<Test as Config>::AddressMapper::is_mapped(&EVE));
377			assert_eq!(EVE, <Test as Config>::AddressMapper::to_account_id(&EVE_ADDR));
378			assert!(
379				<Test as Config>::Currency::balance_on_hold(
380					&HoldReason::AddressMapping.into(),
381					&EVE
382				) > 0
383			);
384		});
385	}
386
387	#[test]
388	fn map_fallback_account_fails() {
389		ExtBuilder::default().build().execute_with(|| {
390			assert!(<Test as Config>::AddressMapper::is_mapped(&ALICE));
391			// alice is an e suffixed account and hence cannot be mapped
392			assert_err!(
393				<Test as Config>::AddressMapper::map(&ALICE),
394				<Error<Test>>::AccountAlreadyMapped,
395			);
396			assert_eq!(
397				<Test as Config>::Currency::balance_on_hold(
398					&HoldReason::AddressMapping.into(),
399					&ALICE
400				),
401				0
402			);
403		});
404	}
405
406	#[test]
407	fn double_map_fails() {
408		ExtBuilder::default().build().execute_with(|| {
409			assert!(!<Test as Config>::AddressMapper::is_mapped(&EVE));
410			<Test as Config>::Currency::set_balance(&EVE, 1_000_000);
411			<Test as Config>::AddressMapper::map(&EVE).unwrap();
412			assert!(<Test as Config>::AddressMapper::is_mapped(&EVE));
413			let deposit = <Test as Config>::Currency::balance_on_hold(
414				&HoldReason::AddressMapping.into(),
415				&EVE,
416			);
417			assert_err!(
418				<Test as Config>::AddressMapper::map(&EVE),
419				<Error<Test>>::AccountAlreadyMapped,
420			);
421			assert!(<Test as Config>::AddressMapper::is_mapped(&EVE));
422			assert_eq!(
423				<Test as Config>::Currency::balance_on_hold(
424					&HoldReason::AddressMapping.into(),
425					&EVE
426				),
427				deposit
428			);
429		});
430	}
431
432	#[test]
433	fn unmap_works() {
434		ExtBuilder::default().build().execute_with(|| {
435			<Test as Config>::Currency::set_balance(&EVE, 1_000_000);
436			<Test as Config>::AddressMapper::map(&EVE).unwrap();
437			assert!(<Test as Config>::AddressMapper::is_mapped(&EVE));
438			assert!(
439				<Test as Config>::Currency::balance_on_hold(
440					&HoldReason::AddressMapping.into(),
441					&EVE
442				) > 0
443			);
444
445			<Test as Config>::AddressMapper::unmap(&EVE).unwrap();
446			assert!(!<Test as Config>::AddressMapper::is_mapped(&EVE));
447			assert_eq!(
448				<Test as Config>::Currency::balance_on_hold(
449					&HoldReason::AddressMapping.into(),
450					&EVE
451				),
452				0
453			);
454
455			// another unmap is a noop
456			<Test as Config>::AddressMapper::unmap(&EVE).unwrap();
457			assert!(!<Test as Config>::AddressMapper::is_mapped(&EVE));
458			assert_eq!(
459				<Test as Config>::Currency::balance_on_hold(
460					&HoldReason::AddressMapping.into(),
461					&EVE
462				),
463				0
464			);
465		});
466	}
467
468	#[test]
469	fn auto_mapper_maps_on_new_account() {
470		ExtBuilder::default().build().execute_with(|| {
471			AutoMapFlag::set(true);
472
473			assert!(!frame_system::Pallet::<Test>::account_exists(&EVE));
474			assert!(!<Test as Config>::AddressMapper::is_mapped(&EVE));
475			// Funding a new account triggers frame_system's OnNewAccount hook
476			<Test as Config>::Currency::set_balance(&EVE, 1_000_000);
477			assert!(<Test as Config>::AddressMapper::is_mapped(&EVE));
478			// no deposit taken
479			assert_eq!(
480				<Test as Config>::Currency::balance_on_hold(
481					&HoldReason::AddressMapping.into(),
482					&EVE
483				),
484				0
485			);
486		});
487	}
488
489	#[test]
490	fn auto_mapper_unmaps_on_killed_account() {
491		ExtBuilder::default().build().execute_with(|| {
492			AutoMapFlag::set(true);
493			<Test as Config>::Currency::set_balance(&EVE, 1_000_000);
494			assert!(<Test as Config>::AddressMapper::is_mapped(&EVE));
495
496			// Killing the account triggers frame_system's OnKilledAccount hook
497			<Test as Config>::Currency::set_balance(&EVE, 0);
498			assert!(!<Test as Config>::AddressMapper::is_mapped(&EVE));
499		});
500	}
501
502	#[test]
503	fn auto_mapper_noop_when_disabled() {
504		ExtBuilder::default().build().execute_with(|| {
505			AutoMapFlag::set(false);
506
507			assert!(!<Test as Config>::AddressMapper::is_mapped(&EVE));
508			<Test as Config>::Currency::set_balance(&EVE, 1_000_000);
509			assert!(!<Test as Config>::AddressMapper::is_mapped(&EVE));
510		});
511	}
512
513	#[test]
514	fn auto_mapper_ignores_eth_derived_accounts() {
515		ExtBuilder::default().build().execute_with(|| {
516			AutoMapFlag::set(true);
517
518			// ALICE is eth-derived and already considered mapped
519			assert!(<Test as Config>::AddressMapper::is_mapped(&ALICE));
520			// Funding an eth-derived account silently ignores the AccountAlreadyMapped error
521			<Test as Config>::Currency::set_balance(&ALICE, 1_000_000);
522			assert!(<Test as Config>::AddressMapper::is_mapped(&ALICE));
523		});
524	}
525
526	#[test]
527	#[cfg(not(feature = "runtime-benchmarks"))]
528	fn unmap_account_dispatchable_blocked_when_auto_map_enabled() {
529		use frame_support::assert_noop;
530		ExtBuilder::default().build().execute_with(|| {
531			AutoMapFlag::set(true);
532
533			assert_noop!(
534				Pallet::<Test>::unmap_account(RuntimeOrigin::signed(EVE)),
535				<Error<Test>>::AutoMappingEnabled,
536			);
537		});
538	}
539
540	#[test]
541	fn batch_map_accounts_empty_pays_yes() {
542		ExtBuilder::default().build().execute_with(|| {
543			let info =
544				Pallet::<Test>::batch_map_accounts(RuntimeOrigin::signed(ALICE), alloc::vec![])
545					.unwrap();
546
547			assert_eq!(info.pays_fee, Pays::Yes);
548		});
549	}
550
551	#[test]
552	fn batch_map_accounts_all_eth_derived_pays_yes() {
553		ExtBuilder::default().build().execute_with(|| {
554			// Eth-derived accounts are stateless mapped, so nothing useful happens
555			// and the caller is charged.
556			let info = Pallet::<Test>::batch_map_accounts(
557				RuntimeOrigin::signed(ALICE),
558				alloc::vec![ALICE, BOB, CHARLIE, DJANGO],
559			)
560			.unwrap();
561
562			assert_eq!(info.pays_fee, Pays::Yes);
563		});
564	}
565
566	#[test]
567	fn batch_map_accounts_pays_no_when_mostly_unmapped() {
568		ExtBuilder::default().build().execute_with(|| {
569			let unmapped: Vec<AccountId32> =
570				(10u8..19u8).map(|i| AccountId32::new([i; 32])).collect();
571			let mut accounts = unmapped.clone();
572			accounts.push(ALICE); // 1 eth-derived account, not counted as useful
573
574			// 9 of 10 (90%) become useful → free
575			let info =
576				Pallet::<Test>::batch_map_accounts(RuntimeOrigin::signed(ALICE), accounts).unwrap();
577
578			assert_eq!(info.pays_fee, Pays::No);
579
580			for a in &unmapped {
581				assert!(<Test as Config>::AddressMapper::is_mapped(a));
582
583				// map_no_deposit must not take a deposit
584				assert_eq!(
585					<Test as Config>::Currency::balance_on_hold(
586						&HoldReason::AddressMapping.into(),
587						a,
588					),
589					0
590				);
591			}
592		});
593	}
594
595	#[test]
596	fn batch_map_accounts_already_mapped_no_hold_pays_yes() {
597		ExtBuilder::default().build().execute_with(|| {
598			<Test as Config>::AddressMapper::map_no_deposit(&EVE).unwrap();
599			assert!(<Test as Config>::AddressMapper::is_mapped(&EVE));
600
601			assert_eq!(
602				<Test as Config>::Currency::balance_on_hold(
603					&HoldReason::AddressMapping.into(),
604					&EVE,
605				),
606				0
607			);
608
609			let info = Pallet::<Test>::batch_map_accounts(
610				RuntimeOrigin::signed(ALICE),
611				alloc::vec![EVE; 10],
612			)
613			.unwrap();
614
615			assert_eq!(info.pays_fee, Pays::Yes);
616		});
617	}
618
619	#[test]
620	fn batch_map_accounts_pays_yes_below_threshold() {
621		ExtBuilder::default().build().execute_with(|| {
622			// 1 unmapped non-eth-derived account + 9 eth-derived (= 10% useful)
623			let mut accounts: Vec<AccountId32> = alloc::vec![AccountId32::new([10u8; 32])];
624			for _ in 0..9 {
625				accounts.push(ALICE);
626			}
627
628			let info =
629				Pallet::<Test>::batch_map_accounts(RuntimeOrigin::signed(ALICE), accounts).unwrap();
630
631			assert_eq!(info.pays_fee, Pays::Yes);
632		});
633	}
634}