1use 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
30pub trait AddressMapper<T: Config>: private::Sealed {
47 fn to_address(account_id: &T::AccountId) -> H160;
49
50 fn to_account_id(address: &H160) -> T::AccountId;
52
53 fn to_fallback_account_id(address: &H160) -> T::AccountId;
60
61 fn map(account_id: &T::AccountId) -> DispatchResult;
66
67 fn map_no_deposit_unchecked(account_id: &T::AccountId) -> DispatchResult {
75 Self::map(account_id)
76 }
77
78 fn unmap(account_id: &T::AccountId) -> DispatchResult;
83
84 fn is_mapped(account_id: &T::AccountId) -> bool;
89
90 fn is_eth_derived(account_id: &T::AccountId) -> bool;
94}
95
96mod private {
97 pub trait Sealed {}
98 impl<T> Sealed for super::AccountId32Mapper<T> {}
99 impl<T> Sealed for super::H160Mapper<T> {}
100 impl<T> Sealed for super::TestAccountMapper<T> {}
101}
102
103pub struct AccountId32Mapper<T>(PhantomData<T>);
110
111#[allow(dead_code)]
115pub struct H160Mapper<T>(PhantomData<T>);
116
117pub struct TestAccountMapper<T>(PhantomData<T>);
119
120impl<T> AddressMapper<T> for AccountId32Mapper<T>
121where
122 T: Config<AccountId = AccountId32>,
123{
124 fn to_address(account_id: &AccountId32) -> H160 {
125 let account_bytes: &[u8; 32] = account_id.as_ref();
126 if Self::is_eth_derived(account_id) {
127 H160::from_slice(&account_bytes[..20])
130 } else {
131 let account_hash = keccak_256(account_bytes);
134 H160::from_slice(&account_hash[12..])
135 }
136 }
137
138 fn to_account_id(address: &H160) -> AccountId32 {
139 <OriginalAccount<T>>::get(address).unwrap_or_else(|| Self::to_fallback_account_id(address))
140 }
141
142 fn to_fallback_account_id(address: &H160) -> AccountId32 {
143 let mut account_id = AccountId32::new([0xEE; 32]);
144 let account_bytes: &mut [u8; 32] = account_id.as_mut();
145 account_bytes[..20].copy_from_slice(address.as_bytes());
146 account_id
147 }
148
149 fn map(account_id: &T::AccountId) -> DispatchResult {
150 ensure!(!Self::is_mapped(account_id), <Error<T>>::AccountAlreadyMapped);
151
152 let deposit = T::DepositPerByte::get()
154 .saturating_mul(52u32.into())
155 .saturating_add(T::DepositPerItem::get());
156 T::Currency::hold(&HoldReason::AddressMapping.into(), account_id, deposit)?;
157
158 <OriginalAccount<T>>::insert(Self::to_address(account_id), account_id);
159 Ok(())
160 }
161
162 fn map_no_deposit_unchecked(account_id: &T::AccountId) -> DispatchResult {
163 ensure!(!Self::is_mapped(account_id), <Error<T>>::AccountAlreadyMapped);
164 <OriginalAccount<T>>::insert(Self::to_address(account_id), account_id);
165 Ok(())
166 }
167
168 fn unmap(account_id: &T::AccountId) -> DispatchResult {
169 <OriginalAccount<T>>::remove(Self::to_address(account_id));
171 T::Currency::release_all(
172 &HoldReason::AddressMapping.into(),
173 account_id,
174 Precision::BestEffort,
175 )?;
176 Ok(())
177 }
178
179 fn is_mapped(account_id: &T::AccountId) -> bool {
180 Self::is_eth_derived(account_id) ||
181 <OriginalAccount<T>>::contains_key(Self::to_address(account_id))
182 }
183
184 fn is_eth_derived(account_id: &T::AccountId) -> bool {
189 let account_bytes: &[u8; 32] = account_id.as_ref();
190 &account_bytes[20..] == &[0xEE; 12]
191 }
192}
193
194impl<T> AddressMapper<T> for TestAccountMapper<T>
195where
196 T: Config<AccountId = u64>,
197{
198 fn to_address(account_id: &T::AccountId) -> H160 {
199 let mut bytes = [0u8; 20];
200 bytes[12..].copy_from_slice(&account_id.to_be_bytes());
201 H160::from(bytes)
202 }
203
204 fn to_account_id(address: &H160) -> T::AccountId {
205 Self::to_fallback_account_id(address)
206 }
207
208 fn to_fallback_account_id(address: &H160) -> T::AccountId {
209 u64::from_be_bytes(address.as_ref()[12..].try_into().unwrap())
210 }
211
212 fn map(_account_id: &T::AccountId) -> DispatchResult {
213 Ok(())
214 }
215
216 fn unmap(_account_id: &T::AccountId) -> DispatchResult {
217 Ok(())
218 }
219
220 fn is_mapped(_account_id: &T::AccountId) -> bool {
221 true
222 }
223
224 fn is_eth_derived(_account_id: &T::AccountId) -> bool {
225 false
226 }
227}
228
229impl<T> AddressMapper<T> for H160Mapper<T>
230where
231 T: Config,
232 crate::AccountIdOf<T>: AsRef<[u8; 20]> + From<H160>,
233{
234 fn to_address(account_id: &T::AccountId) -> H160 {
235 H160::from_slice(account_id.as_ref())
236 }
237
238 fn to_account_id(address: &H160) -> T::AccountId {
239 Self::to_fallback_account_id(address)
240 }
241
242 fn to_fallback_account_id(address: &H160) -> T::AccountId {
243 (*address).into()
244 }
245
246 fn map(_account_id: &T::AccountId) -> DispatchResult {
247 Ok(())
248 }
249
250 fn unmap(_account_id: &T::AccountId) -> DispatchResult {
251 Ok(())
252 }
253
254 fn is_mapped(_account_id: &T::AccountId) -> bool {
255 true
256 }
257
258 fn is_eth_derived(_account_id: &T::AccountId) -> bool {
259 true
260 }
261}
262
263pub fn create1(deployer: &H160, nonce: u64) -> H160 {
265 let mut list = rlp::RlpStream::new_list(2);
266 list.append(&deployer.as_bytes());
267 list.append(&nonce);
268 let hash = keccak_256(&list.out());
269 H160::from_slice(&hash[12..])
270}
271
272pub fn create2(deployer: &H160, code: &[u8], input_data: &[u8], salt: &[u8; 32]) -> H160 {
274 let init_code_hash = {
275 let init_code: Vec<u8> = code.into_iter().chain(input_data).cloned().collect();
276 keccak_256(init_code.as_ref())
277 };
278 let mut bytes = [0; 85];
279 bytes[0] = 0xff;
280 bytes[1..21].copy_from_slice(deployer.as_bytes());
281 bytes[21..53].copy_from_slice(salt);
282 bytes[53..85].copy_from_slice(&init_code_hash);
283 let hash = keccak_256(&bytes);
284 H160::from_slice(&hash[12..])
285}
286
287pub struct AutoMapper<T>(PhantomData<T>);
288
289impl<T: Config> OnNewAccount<T::AccountId> for AutoMapper<T> {
290 fn on_new_account(who: &T::AccountId) {
291 if T::AutoMap::get() &&
292 !T::AddressMapper::is_eth_derived(who) &&
293 let Err(err) = T::AddressMapper::map_no_deposit_unchecked(who)
294 {
295 log::warn!(
296 target: crate::LOG_TARGET,
297 "Failed to auto-map account {who:?}: {err:?}",
298 );
299 }
300 }
301}
302
303impl<T: Config> OnKilledAccount<T::AccountId> for AutoMapper<T> {
304 fn on_killed_account(who: &T::AccountId) {
305 if T::AutoMap::get() &&
306 !T::AddressMapper::is_eth_derived(who) &&
307 let Err(err) = T::AddressMapper::unmap(who)
308 {
309 log::warn!(
310 target: crate::LOG_TARGET,
311 "Failed to auto-unmap account {who:?}: {err:?}",
312 );
313 }
314 }
315}
316
317#[cfg(test)]
318mod test {
319 use super::*;
320 use crate::{
321 AddressMapper, Error, Pallet,
322 test_utils::*,
323 tests::{AutoMapFlag, ExtBuilder, RuntimeOrigin, Test},
324 };
325 use frame_support::{
326 assert_err,
327 dispatch::Pays,
328 traits::fungible::{InspectHold, Mutate},
329 };
330 use pretty_assertions::assert_eq;
331 use sp_core::{H160, hex2array};
332
333 #[test]
334 fn create1_works() {
335 assert_eq!(
336 create1(&ALICE_ADDR, 1u64),
337 H160(hex2array!("c851da37e4e8d3a20d8d56be2963934b4ad71c3b")),
338 )
339 }
340
341 #[test]
342 fn create2_works() {
343 assert_eq!(
344 create2(
345 &ALICE_ADDR,
346 &hex2array!("600060005560016000"),
347 &hex2array!("55"),
348 &hex2array!("1234567890123456789012345678901234567890123456789012345678901234")
349 ),
350 H160(hex2array!("7f31e795e5836a19a8f919ab5a9de9a197ecd2b6")),
351 )
352 }
353
354 #[test]
355 fn fallback_map_works() {
356 assert!(<Test as Config>::AddressMapper::is_mapped(&ALICE));
357 assert_eq!(
358 ALICE_FALLBACK,
359 <Test as Config>::AddressMapper::to_fallback_account_id(&ALICE_ADDR)
360 );
361 assert_eq!(ALICE_ADDR, <Test as Config>::AddressMapper::to_address(&ALICE_FALLBACK));
362 }
363
364 #[test]
365 fn map_works() {
366 ExtBuilder::default().build().execute_with(|| {
367 <Test as Config>::Currency::set_balance(&EVE, 1_000_000);
368 assert!(!<Test as Config>::AddressMapper::is_mapped(&EVE));
370 assert_eq!(EVE_FALLBACK, <Test as Config>::AddressMapper::to_account_id(&EVE_ADDR));
371 assert_eq!(
372 <Test as Config>::Currency::balance_on_hold(
373 &HoldReason::AddressMapping.into(),
374 &EVE
375 ),
376 0
377 );
378
379 <Test as Config>::AddressMapper::map(&EVE).unwrap();
381 assert!(<Test as Config>::AddressMapper::is_mapped(&EVE));
382 assert_eq!(EVE, <Test as Config>::AddressMapper::to_account_id(&EVE_ADDR));
383 assert!(
384 <Test as Config>::Currency::balance_on_hold(
385 &HoldReason::AddressMapping.into(),
386 &EVE
387 ) > 0
388 );
389 });
390 }
391
392 #[test]
393 fn map_fallback_account_fails() {
394 ExtBuilder::default().build().execute_with(|| {
395 assert!(<Test as Config>::AddressMapper::is_mapped(&ALICE));
396 assert_err!(
398 <Test as Config>::AddressMapper::map(&ALICE),
399 <Error<Test>>::AccountAlreadyMapped,
400 );
401 assert_eq!(
402 <Test as Config>::Currency::balance_on_hold(
403 &HoldReason::AddressMapping.into(),
404 &ALICE
405 ),
406 0
407 );
408 });
409 }
410
411 #[test]
412 fn double_map_fails() {
413 ExtBuilder::default().build().execute_with(|| {
414 assert!(!<Test as Config>::AddressMapper::is_mapped(&EVE));
415 <Test as Config>::Currency::set_balance(&EVE, 1_000_000);
416 <Test as Config>::AddressMapper::map(&EVE).unwrap();
417 assert!(<Test as Config>::AddressMapper::is_mapped(&EVE));
418 let deposit = <Test as Config>::Currency::balance_on_hold(
419 &HoldReason::AddressMapping.into(),
420 &EVE,
421 );
422 assert_err!(
423 <Test as Config>::AddressMapper::map(&EVE),
424 <Error<Test>>::AccountAlreadyMapped,
425 );
426 assert!(<Test as Config>::AddressMapper::is_mapped(&EVE));
427 assert_eq!(
428 <Test as Config>::Currency::balance_on_hold(
429 &HoldReason::AddressMapping.into(),
430 &EVE
431 ),
432 deposit
433 );
434 });
435 }
436
437 #[test]
438 fn unmap_works() {
439 ExtBuilder::default().build().execute_with(|| {
440 <Test as Config>::Currency::set_balance(&EVE, 1_000_000);
441 <Test as Config>::AddressMapper::map(&EVE).unwrap();
442 assert!(<Test as Config>::AddressMapper::is_mapped(&EVE));
443 assert!(
444 <Test as Config>::Currency::balance_on_hold(
445 &HoldReason::AddressMapping.into(),
446 &EVE
447 ) > 0
448 );
449
450 <Test as Config>::AddressMapper::unmap(&EVE).unwrap();
451 assert!(!<Test as Config>::AddressMapper::is_mapped(&EVE));
452 assert_eq!(
453 <Test as Config>::Currency::balance_on_hold(
454 &HoldReason::AddressMapping.into(),
455 &EVE
456 ),
457 0
458 );
459
460 <Test as Config>::AddressMapper::unmap(&EVE).unwrap();
462 assert!(!<Test as Config>::AddressMapper::is_mapped(&EVE));
463 assert_eq!(
464 <Test as Config>::Currency::balance_on_hold(
465 &HoldReason::AddressMapping.into(),
466 &EVE
467 ),
468 0
469 );
470 });
471 }
472
473 #[test]
474 fn auto_mapper_maps_on_new_account() {
475 ExtBuilder::default().build().execute_with(|| {
476 AutoMapFlag::set(true);
477
478 assert!(!frame_system::Pallet::<Test>::account_exists(&EVE));
479 assert!(!<Test as Config>::AddressMapper::is_mapped(&EVE));
480 <Test as Config>::Currency::set_balance(&EVE, 1_000_000);
482 assert!(<Test as Config>::AddressMapper::is_mapped(&EVE));
483 assert_eq!(
485 <Test as Config>::Currency::balance_on_hold(
486 &HoldReason::AddressMapping.into(),
487 &EVE
488 ),
489 0
490 );
491 });
492 }
493
494 #[test]
495 fn auto_mapper_unmaps_on_killed_account() {
496 ExtBuilder::default().build().execute_with(|| {
497 AutoMapFlag::set(true);
498 <Test as Config>::Currency::set_balance(&EVE, 1_000_000);
499 assert!(<Test as Config>::AddressMapper::is_mapped(&EVE));
500
501 <Test as Config>::Currency::set_balance(&EVE, 0);
503 assert!(!<Test as Config>::AddressMapper::is_mapped(&EVE));
504 });
505 }
506
507 #[test]
508 fn auto_mapper_noop_when_disabled() {
509 ExtBuilder::default().build().execute_with(|| {
510 AutoMapFlag::set(false);
511
512 assert!(!<Test as Config>::AddressMapper::is_mapped(&EVE));
513 <Test as Config>::Currency::set_balance(&EVE, 1_000_000);
514 assert!(!<Test as Config>::AddressMapper::is_mapped(&EVE));
515 });
516 }
517
518 #[test]
519 fn auto_mapper_ignores_eth_derived_accounts() {
520 ExtBuilder::default().build().execute_with(|| {
521 AutoMapFlag::set(true);
522
523 assert!(<Test as Config>::AddressMapper::is_mapped(&ALICE));
525 <Test as Config>::Currency::set_balance(&ALICE, 1_000_000);
527 assert!(<Test as Config>::AddressMapper::is_mapped(&ALICE));
528 });
529 }
530
531 #[test]
532 #[cfg(not(feature = "runtime-benchmarks"))]
533 fn unmap_account_dispatchable_blocked_when_auto_map_enabled() {
534 use frame_support::assert_noop;
535 ExtBuilder::default().build().execute_with(|| {
536 AutoMapFlag::set(true);
537
538 assert_noop!(
539 Pallet::<Test>::unmap_account(RuntimeOrigin::signed(EVE)),
540 <Error<Test>>::AutoMappingEnabled,
541 );
542 });
543 }
544
545 #[test]
546 fn batch_map_accounts_empty_pays_yes() {
547 ExtBuilder::default().build().execute_with(|| {
548 let info =
549 Pallet::<Test>::batch_map_accounts(RuntimeOrigin::signed(ALICE), alloc::vec![])
550 .unwrap();
551
552 assert_eq!(info.pays_fee, Pays::Yes);
553 });
554 }
555
556 #[test]
557 fn batch_map_accounts_all_eth_derived_pays_yes() {
558 ExtBuilder::default().build().execute_with(|| {
559 let info = Pallet::<Test>::batch_map_accounts(
562 RuntimeOrigin::signed(ALICE),
563 alloc::vec![ALICE, BOB, CHARLIE, DJANGO],
564 )
565 .unwrap();
566
567 assert_eq!(info.pays_fee, Pays::Yes);
568 });
569 }
570
571 #[test]
572 fn batch_map_accounts_pays_no_when_mostly_unmapped() {
573 ExtBuilder::default().build().execute_with(|| {
574 let unmapped: Vec<AccountId32> =
575 (10u8..19u8).map(|i| AccountId32::new([i; 32])).collect();
576 for a in &unmapped {
578 <Test as Config>::Currency::set_balance(a, 1_000_000);
579 }
580 let mut accounts = unmapped.clone();
581 accounts.push(ALICE); let info =
585 Pallet::<Test>::batch_map_accounts(RuntimeOrigin::signed(ALICE), accounts).unwrap();
586
587 assert_eq!(info.pays_fee, Pays::No);
588
589 for a in &unmapped {
590 assert!(<Test as Config>::AddressMapper::is_mapped(a));
591
592 assert_eq!(
594 <Test as Config>::Currency::balance_on_hold(
595 &HoldReason::AddressMapping.into(),
596 a,
597 ),
598 0
599 );
600 }
601 });
602 }
603
604 #[test]
605 fn batch_map_accounts_already_mapped_no_hold_pays_yes() {
606 ExtBuilder::default().build().execute_with(|| {
607 <Test as Config>::AddressMapper::map_no_deposit_unchecked(&EVE).unwrap();
608 assert!(<Test as Config>::AddressMapper::is_mapped(&EVE));
609
610 assert_eq!(
611 <Test as Config>::Currency::balance_on_hold(
612 &HoldReason::AddressMapping.into(),
613 &EVE,
614 ),
615 0
616 );
617
618 let info = Pallet::<Test>::batch_map_accounts(
619 RuntimeOrigin::signed(ALICE),
620 alloc::vec![EVE; 10],
621 )
622 .unwrap();
623
624 assert_eq!(info.pays_fee, Pays::Yes);
625 });
626 }
627
628 #[test]
629 fn batch_map_accounts_pays_yes_below_threshold() {
630 ExtBuilder::default().build().execute_with(|| {
631 let mut accounts: Vec<AccountId32> = alloc::vec![AccountId32::new([10u8; 32])];
633 <Test as Config>::Currency::set_balance(&accounts[0], 1_000_000);
634 for _ in 0..9 {
635 accounts.push(ALICE);
636 }
637
638 let info =
639 Pallet::<Test>::batch_map_accounts(RuntimeOrigin::signed(ALICE), accounts).unwrap();
640
641 assert_eq!(info.pays_fee, Pays::Yes);
642 });
643 }
644
645 #[test]
646 fn batch_map_accounts_pays_yes_mixed() {
647 ExtBuilder::default().build().execute_with(|| {
648 let existing: Vec<AccountId32> =
651 (10u8..27u8).map(|i| AccountId32::new([i; 32])).collect();
652 for a in &existing {
653 <Test as Config>::Currency::set_balance(a, 1_000_000);
654 }
655 let nonexistent = AccountId32::new([99u8; 32]);
656 let mut accounts = existing.clone();
657 accounts.push(nonexistent.clone());
658 accounts.push(ALICE); let info =
661 Pallet::<Test>::batch_map_accounts(RuntimeOrigin::signed(ALICE), accounts).unwrap();
662
663 assert_eq!(info.pays_fee, Pays::Yes);
664 for a in &existing {
665 assert!(
666 <Test as Config>::AddressMapper::is_mapped(a),
667 "existing accounts must still be mapped alongside non-existent or eth-derived entries",
668 );
669 }
670 assert!(
671 !<Test as Config>::AddressMapper::is_mapped(&nonexistent),
672 "non-existent accounts must be skipped, not mapped",
673 );
674 });
675 }
676
677 #[test]
678 fn batch_map_accounts_rejects_nonexistent_accounts() {
679 ExtBuilder::default().build().execute_with(|| {
680 let unknown = AccountId32::new([0xAB; 32]);
683 assert!(
684 !frame_system::Pallet::<Test>::account_exists(&unknown),
685 "unknown account must not pre-exist on chain",
686 );
687
688 let info = Pallet::<Test>::batch_map_accounts(
689 RuntimeOrigin::signed(ALICE),
690 alloc::vec![unknown.clone()],
691 )
692 .unwrap();
693
694 assert_eq!(
695 info.pays_fee,
696 Pays::Yes,
697 "non-existent accounts must not trigger the free path",
698 );
699 assert!(
700 !<Test as Config>::AddressMapper::is_mapped(&unknown),
701 "OriginalAccount must not be written for a non-existent account",
702 );
703 });
704 }
705}