pallet_unified_accounts/
lib.rs

1// This file is part of Astar.
2
3// Copyright (C) Stake Technologies Pte.Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later
5
6// Astar is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// Astar is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with Astar. If not, see <http://www.gnu.org/licenses/>.
18
19//! # Pallet Unified Account
20//!
21//! A simple module for managing mappings (both ways) between different
22//! address schemes, inspired from Acala's evm-accounts pallet
23//! https://github.com/AcalaNetwork/Acala/tree/master/modules/evm-accounts
24//!
25//! - [`Config`]
26//! - [`Call`]
27//!
28//! ## Overview
29//!
30//! The Unified Accounts module provide functionality for native account holders to
31//! connect their evm address to have a unified experience across the different VMs.
32//! - Connect evm address you control
33//! - Connect default evm address
34//!
35//! ## Interface
36//!
37//! * `claim_evm_address`: Creates the double mappings for the provided evm address with caller
38//!    account id given that no prior mapping exists for both and signature provided is valid.
39//! * `claim_default_evm_address`: Creates the double mapping with default evm address given that
40//!    no prior mapping exists.
41//!
42//! ## Storage Fee
43//! User is also charged a storage fee [`AccountMappingStorageFee`](`crate::Config::AccountMappingStorageFee`)
44//! before mappings are created to prevent storage abuse.
45//!
46//! WARNINGS:
47//! * This pallet only handles transfer of native balance only, for the rest of native assets
48//!   hold by evm address like XC20, DAppStaking unclaimed rewards, etc should be transferred
49//!   manually beforehand by user himself otherwise FUNDS WILL BE LOST FOREVER.
50//! * Once mapping is created it cannot be changed.
51//!
52//! ## Traits
53//!
54//! * `UnifiedAddressMapper`: Interface to access pallet's mappings with defaults
55//!
56//! ## Implementations
57//!
58//! * [`StaticLookup`](sp_runtime::traits::StaticLookup): Lookup implementations for accepting H160
59//! * [`AddressMapping`](pallet_evm::AddressMapping): Wrapper over `UnifiedAddressMapper` for evm address mapping
60//!   to account id.
61//! * `KillAccountMapping`: [`OnKilledAccount`](frame_support::traits::OnKilledAccount) implementation to remove
62//!   the mappings from storage after account is reaped.
63
64#![cfg_attr(not(feature = "std"), no_std)]
65
66use astar_primitives::{
67    evm::{EvmAddress, UnifiedAddressMapper},
68    Balance,
69};
70use frame_support::{
71    pallet_prelude::*,
72    traits::{
73        fungible::{Inspect as FungibleInspect, Mutate as FungibleMutate},
74        tokens::{Fortitude::*, Precision::*, Preservation::*},
75        OnKilledAccount,
76    },
77};
78use frame_system::{ensure_signed, pallet_prelude::*};
79use pallet_evm::AddressMapping;
80use precompile_utils::keccak256;
81use sp_core::{H160, H256, U256};
82use sp_io::hashing::keccak_256;
83use sp_runtime::{
84    traits::{LookupError, StaticLookup, Zero},
85    MultiAddress,
86};
87use sp_std::marker::PhantomData;
88
89pub use pallet::*;
90
91pub mod weights;
92pub use weights::WeightInfo;
93
94#[cfg(feature = "runtime-benchmarks")]
95mod benchmarking;
96mod mock;
97mod tests;
98
99/// ECDSA Signature type, with last bit for recovering address
100type EvmSignature = [u8; 65];
101
102#[frame_support::pallet]
103pub mod pallet {
104    use super::*;
105
106    #[pallet::pallet]
107    pub struct Pallet<T>(PhantomData<T>);
108
109    #[pallet::config]
110    pub trait Config: frame_system::Config {
111        /// The Currency for managing evm address assets
112        type Currency: FungibleMutate<Self::AccountId, Balance = Balance>;
113        /// Default address conversion
114        type DefaultMappings: UnifiedAddressMapper<Self::AccountId>;
115        /// EVM chain id
116        #[pallet::constant]
117        type ChainId: Get<u64>;
118        /// The amount of currency needed for mappings to be added.
119        /// Two storage items with values sizes, sizeof(AccountId) and sizeof(H160)
120        /// respectively
121        #[pallet::constant]
122        type AccountMappingStorageFee: Get<Balance>;
123        /// Weight information for the extrinsics in this module
124        type WeightInfo: WeightInfo;
125    }
126
127    #[pallet::error]
128    pub enum Error<T> {
129        /// AccountId or EvmAddress already mapped
130        AlreadyMapped,
131        /// The signature is malformed
132        UnexpectedSignatureFormat,
133        /// The signature verification failed due to mismatch evm address
134        InvalidSignature,
135        /// Funds unavailable to claim account
136        FundsUnavailable,
137    }
138
139    #[pallet::event]
140    #[pallet::generate_deposit(pub(super) fn deposit_event)]
141    pub enum Event<T: Config> {
142        /// Evm Address claimed.
143        /// Double Mapping b/w native and evm address created
144        AccountClaimed {
145            account_id: T::AccountId,
146            evm_address: EvmAddress,
147        },
148    }
149
150    /// Native accounts for evm address
151    /// EvmToNative: EvmAddress => Option<AccountId>
152    #[pallet::storage]
153    pub type EvmToNative<T: Config> =
154        StorageMap<_, Blake2_128Concat, EvmAddress, T::AccountId, OptionQuery>;
155
156    /// Evm addresses for native accounts
157    /// NativeToEvm: AccountId => Option<EvmAddress>
158    #[pallet::storage]
159    pub type NativeToEvm<T: Config> =
160        StorageMap<_, Blake2_128Concat, T::AccountId, EvmAddress, OptionQuery>;
161
162    #[pallet::call]
163    impl<T: Config> Pallet<T> {
164        /// Claim account mapping between Substrate account and Evm address.
165        /// Ensure no prior mapping exists for evm address.
166        ///
167        /// - `evm_address`: The evm address to bind to the caller's account
168        /// - `signature`: A signature generated by the address to prove ownership
169        ///
170        /// WARNING:
171        /// - This extrinsic only handles transfer of native balance, if your EVM
172        /// address contains any other native assets like XC20, DAppStaking unclaimed rewards,
173        /// etc you need to transfer them before hand, otherwise FUNDS WILL BE LOST FOREVER.
174        /// - Once connected user cannot change their mapping EVER.
175        #[pallet::call_index(0)]
176        #[pallet::weight(T::WeightInfo::claim_evm_address())]
177        pub fn claim_evm_address(
178            origin: OriginFor<T>,
179            evm_address: EvmAddress,
180            signature: EvmSignature,
181        ) -> DispatchResult {
182            let who = ensure_signed(origin)?;
183            // make sure no prior mapping exists
184            ensure!(
185                !NativeToEvm::<T>::contains_key(&who),
186                Error::<T>::AlreadyMapped
187            );
188            ensure!(
189                !EvmToNative::<T>::contains_key(evm_address),
190                Error::<T>::AlreadyMapped
191            );
192
193            // recover evm address from signature
194            let address = Self::verify_signature(&who, &signature)
195                .ok_or(Error::<T>::UnexpectedSignatureFormat)?;
196
197            ensure!(evm_address == address, Error::<T>::InvalidSignature);
198
199            // charge the storage fee
200            Self::charge_storage_fee(&who)?;
201
202            // Check if the default account id already exists for this evm address
203            let default_account_id = T::DefaultMappings::to_default_account_id(&evm_address);
204            if frame_system::Pallet::<T>::account_exists(&default_account_id) {
205                // Transfer all the free native balance from old account id to the newly
206                // since this `default_account_id` will no longer be connected to evm address
207                // and users cannot access it.
208                // For the reset of the assets types (like XC20, etc) that should be handled by UI.
209                T::Currency::transfer(
210                    &default_account_id,
211                    &who,
212                    T::Currency::reducible_balance(&default_account_id, Expendable, Polite),
213                    Expendable,
214                )?;
215            }
216
217            // create double mappings for the pair
218            EvmToNative::<T>::insert(&evm_address, &who);
219            NativeToEvm::<T>::insert(&who, &evm_address);
220
221            Self::deposit_event(Event::AccountClaimed {
222                account_id: who,
223                evm_address,
224            });
225            Ok(())
226        }
227
228        /// Claim default evm address for given account id
229        /// Ensure no prior mapping exists for the account
230        ///
231        /// WARNINGS: Once connected user cannot change their mapping EVER.
232        #[pallet::call_index(1)]
233        #[pallet::weight(T::WeightInfo::claim_default_evm_address())]
234        pub fn claim_default_evm_address(origin: OriginFor<T>) -> DispatchResult {
235            let who = ensure_signed(origin)?;
236            // claim default evm address
237            let _ = Self::do_claim_default_evm_address(who)?;
238            Ok(())
239        }
240    }
241}
242
243impl<T: Config> Pallet<T> {
244    /// Claim the default evm address
245    fn do_claim_default_evm_address(account_id: T::AccountId) -> Result<EvmAddress, DispatchError> {
246        ensure!(
247            !NativeToEvm::<T>::contains_key(&account_id),
248            Error::<T>::AlreadyMapped
249        );
250        // get the default evm address
251        let evm_address = T::DefaultMappings::to_default_h160(&account_id);
252        // make sure default address is not already mapped, this should not
253        // happen but for sanity check.
254        ensure!(
255            !EvmToNative::<T>::contains_key(&evm_address),
256            Error::<T>::AlreadyMapped
257        );
258
259        Self::charge_storage_fee(&account_id)?;
260
261        // create double mappings for the pair with default evm address
262        EvmToNative::<T>::insert(&evm_address, &account_id);
263        NativeToEvm::<T>::insert(&account_id, &evm_address);
264
265        Self::deposit_event(Event::AccountClaimed {
266            account_id,
267            evm_address,
268        });
269        Ok(evm_address)
270    }
271
272    /// Charge the (exact) storage fee (politely) from the user and burn it
273    /// while preserving the account from being reaped.
274    fn charge_storage_fee(who: &T::AccountId) -> Result<Balance, DispatchError> {
275        let balance = T::Currency::reducible_balance(who, Preserve, Polite);
276        let fee = T::AccountMappingStorageFee::get();
277        ensure!(balance >= fee, Error::<T>::FundsUnavailable);
278        T::Currency::burn_from(
279            who,
280            T::AccountMappingStorageFee::get(),
281            Preserve,
282            Exact,
283            Polite,
284        )
285    }
286}
287
288/// EIP-712 compatible signature scheme for verifying ownership of EVM Address
289/// https://eips.ethereum.org/EIPS/eip-712
290///
291/// Raw Data = Domain Separator + Type Hash + keccak256(AccountId)
292impl<T: Config> Pallet<T> {
293    pub fn build_signing_payload(who: &T::AccountId) -> [u8; 32] {
294        let domain_separator = Self::build_domain_separator();
295        let args_hash = Self::build_args_hash(who);
296
297        let mut payload = b"\x19\x01".to_vec();
298        payload.extend_from_slice(&domain_separator);
299        payload.extend_from_slice(&args_hash);
300        keccak_256(&payload)
301    }
302
303    pub fn verify_signature(who: &T::AccountId, sig: &EvmSignature) -> Option<EvmAddress> {
304        let payload_hash = Self::build_signing_payload(who);
305
306        sp_io::crypto::secp256k1_ecdsa_recover(sig, &payload_hash)
307            .map(|pubkey| H160::from(H256::from_slice(&keccak_256(&pubkey))))
308            .ok()
309    }
310
311    fn build_domain_separator() -> [u8; 32] {
312        let mut domain =
313            keccak256!("EIP712Domain(string name,string version,uint256 chainId,bytes32 salt)")
314                .to_vec();
315        domain.extend_from_slice(&keccak256!("Astar EVM Claim")); // name
316        domain.extend_from_slice(&keccak256!("1")); // version
317        domain.extend_from_slice(&U256::from(T::ChainId::get()).to_big_endian()); // chain id
318        domain.extend_from_slice(
319            frame_system::Pallet::<T>::block_hash(BlockNumberFor::<T>::zero()).as_ref(),
320        ); // genesis block hash
321        keccak_256(domain.as_slice())
322    }
323
324    fn build_args_hash(account: &T::AccountId) -> [u8; 32] {
325        let mut args_hash = keccak256!("Claim(bytes substrateAddress)").to_vec();
326        args_hash.extend_from_slice(&keccak_256(&account.encode()));
327        keccak_256(args_hash.as_slice())
328    }
329}
330
331#[cfg(any(feature = "std", feature = "runtime-benchmarks"))]
332impl<T: Config> Pallet<T> {
333    /// Sign the given prehash with provided eth private key
334    pub fn eth_sign_prehash(prehash: &[u8; 32], secret: &libsecp256k1::SecretKey) -> [u8; 65] {
335        let (sig, recovery_id) = libsecp256k1::sign(&libsecp256k1::Message::parse(prehash), secret);
336        let mut r = [0u8; 65];
337        r[0..64].copy_from_slice(&sig.serialize()[..]);
338        r[64] = recovery_id.serialize();
339        r
340    }
341
342    /// Get the eth address for given eth private key
343    pub fn eth_address(secret: &libsecp256k1::SecretKey) -> EvmAddress {
344        EvmAddress::from_slice(
345            &sp_io::hashing::keccak_256(
346                &libsecp256k1::PublicKey::from_secret_key(secret).serialize()[1..65],
347            )[12..],
348        )
349    }
350}
351
352/// UnifiedAddressMapper implementation using pallet's mapping
353/// and default address scheme from pallet's config
354impl<T: Config> UnifiedAddressMapper<T::AccountId> for Pallet<T> {
355    fn to_account_id(evm_address: &EvmAddress) -> Option<T::AccountId> {
356        EvmToNative::<T>::get(evm_address)
357    }
358
359    fn to_default_account_id(evm_address: &EvmAddress) -> T::AccountId {
360        T::DefaultMappings::to_default_account_id(evm_address)
361    }
362
363    fn to_h160(account_id: &T::AccountId) -> Option<EvmAddress> {
364        NativeToEvm::<T>::get(account_id)
365    }
366
367    fn to_default_h160(account_id: &T::AccountId) -> EvmAddress {
368        T::DefaultMappings::to_default_h160(account_id)
369    }
370}
371
372/// AddressMapping wrapper implementation
373impl<T: Config> AddressMapping<T::AccountId> for Pallet<T> {
374    fn into_account_id(evm_address: H160) -> T::AccountId {
375        <Self as UnifiedAddressMapper<T::AccountId>>::to_account_id_or_default(&evm_address)
376            .into_address()
377    }
378}
379
380/// OnKilledAccount hooks implementation for removing storage mapping
381/// for killed accounts
382pub struct KillAccountMapping<T>(PhantomData<T>);
383impl<T: Config> OnKilledAccount<T::AccountId> for KillAccountMapping<T> {
384    fn on_killed_account(who: &T::AccountId) {
385        // remove mappings of account reaped
386        if let Some(evm_addr) = NativeToEvm::<T>::take(who) {
387            EvmToNative::<T>::remove(evm_addr);
388            NativeToEvm::<T>::remove(who);
389        }
390    }
391}
392
393/// A lookup implementation returning the `AccountId` from `MultiAddress::Address20` (EVM Address).
394impl<T: Config> StaticLookup for Pallet<T> {
395    type Source = MultiAddress<T::AccountId, ()>;
396    type Target = T::AccountId;
397
398    fn lookup(a: Self::Source) -> Result<Self::Target, LookupError> {
399        match a {
400            MultiAddress::Address20(i) => Ok(
401                <Self as UnifiedAddressMapper<T::AccountId>>::to_account_id_or_default(
402                    &EvmAddress::from_slice(&i),
403                )
404                .into_address(),
405            ),
406            _ => Err(LookupError),
407        }
408    }
409
410    fn unlookup(a: Self::Target) -> Self::Source {
411        MultiAddress::Id(a)
412    }
413}