#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::unused_unit)]
use circuit_runtime_types::{AccountIndex, EvmAddress};
use frame_support::{
ensure, log,
pallet_prelude::*,
traits::{Currency, ExistenceRequirement, IsType, OnKilledAccount},
transactional,
};
use frame_system::{ensure_signed, pallet_prelude::*};
use pallet_3vm_evm::AddressMapping as BaseAddressMapping;
use scale_codec::Encode;
use sp_core::crypto::AccountId32;
use sp_io::{
crypto::secp256k1_ecdsa_recover,
hashing::{blake2_256, keccak_256},
};
use sp_runtime::{
traits::{LookupError, Saturating, StaticLookup},
MultiAddress,
};
use sp_std::{marker::PhantomData, vec::Vec};
use t3rn_primitives::{attesters::ETH_SIGNED_MESSAGE_PREFIX, threevm::AddressMapping};
mod tests;
pub use pallet::*;
#[derive(Encode, Decode, Clone, TypeInfo)]
pub struct EcdsaSignature(pub [u8; 65]);
impl PartialEq for EcdsaSignature {
fn eq(&self, other: &Self) -> bool {
&self.0[..] == &other.0[..]
}
}
impl sp_std::fmt::Debug for EcdsaSignature {
fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result {
write!(f, "EcdsaSignature({:?})", &self.0[..])
}
}
pub fn to_ascii_hex(data: &[u8]) -> Vec<u8> {
let mut r = Vec::with_capacity(data.len() * 2);
let mut push_nibble = |n| r.push(if n < 10 { b'0' + n } else { b'a' - 10 + n });
for &b in data.iter() {
push_nibble(b / 16);
push_nibble(b % 16);
}
r
}
pub fn ethereum_signable_message(what: &[u8], extra: &[u8]) -> Vec<u8> {
let message_digest = keccak_256([what, extra].concat().as_slice());
[Ð_SIGNED_MESSAGE_PREFIX[..], &message_digest[..]].concat()
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
pub(crate) type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type Currency: Currency<Self::AccountId>;
type AddressMapping: AddressMapping<Self::AccountId>;
#[pallet::constant]
type ChainId: Get<u64>;
#[pallet::constant]
type NetworkTreasuryAccount: Get<Self::AccountId>;
#[pallet::constant]
type StorageDepositFee: Get<BalanceOf<Self>>;
}
#[pallet::event]
#[pallet::generate_deposit(fn deposit_event)]
pub enum Event<T: Config> {
ClaimAccount {
account_id: T::AccountId,
evm_address: EvmAddress,
},
}
#[pallet::error]
pub enum Error<T> {
AccountIdHasMapped,
EthAddressHasMapped,
BadSignature,
InvalidSignature,
PreImageAddressNotMatchingRecovered,
NonZeroRefCount,
}
#[pallet::storage]
#[pallet::getter(fn accounts)]
pub type Accounts<T: Config> =
StorageMap<_, Twox64Concat, EvmAddress, T::AccountId, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn evm_addresses)]
pub type EvmAddresses<T: Config> =
StorageMap<_, Twox64Concat, T::AccountId, EvmAddress, OptionQuery>;
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
#[transactional]
pub fn claim_eth_account(
origin: OriginFor<T>,
eth_address: EvmAddress,
eth_signature: EcdsaSignature,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
ensure!(
!EvmAddresses::<T>::contains_key(&who),
Error::<T>::AccountIdHasMapped
);
ensure!(
!Accounts::<T>::contains_key(eth_address),
Error::<T>::EthAddressHasMapped
);
let data = eth_address.0.as_slice();
let address = Self::eth_recover(ð_signature, &data, &[][..])
.ok_or(Error::<T>::BadSignature)?;
ensure!(
eth_address == address,
Error::<T>::PreImageAddressNotMatchingRecovered
);
let account_id = T::AddressMapping::into_account_id(ð_address);
if frame_system::Pallet::<T>::account_exists(&account_id) {
<T as Config>::Currency::transfer(
&account_id,
&who,
<T as Config>::Currency::free_balance(&account_id),
ExistenceRequirement::AllowDeath,
)?;
}
<T as Config>::Currency::transfer(
&who,
&T::NetworkTreasuryAccount::get(),
T::StorageDepositFee::get(),
ExistenceRequirement::KeepAlive,
)?;
Accounts::<T>::insert(eth_address, &who);
EvmAddresses::<T>::insert(&who, eth_address);
Self::deposit_event(Event::ClaimAccount {
account_id: who,
evm_address: eth_address,
});
Ok(Pays::No.into())
}
#[pallet::call_index(1)]
#[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
#[transactional]
pub fn claim_default_account(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
ensure!(
!EvmAddresses::<T>::contains_key(&who),
Error::<T>::AccountIdHasMapped
);
<T as Config>::Currency::transfer(
&who,
&T::NetworkTreasuryAccount::get(),
T::StorageDepositFee::get(),
ExistenceRequirement::KeepAlive,
)?;
let eth_address = T::AddressMapping::get_or_create_evm_address(&who);
Self::deposit_event(Event::ClaimAccount {
account_id: who,
evm_address: eth_address,
});
Ok(Pays::No.into())
}
#[pallet::call_index(2)]
#[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
#[transactional]
pub fn unclaim(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
T::AddressMapping::get_evm_address(&who)
.map(|eth_address| {
Accounts::<T>::remove(eth_address);
EvmAddresses::<T>::remove(&who);
})
.ok_or(Error::<T>::EthAddressHasMapped)?;
Ok(Pays::Yes.into())
}
}
}
impl<T: Config> Pallet<T> {
#[cfg(any(feature = "runtime-benchmarks", feature = "std"))]
pub fn eth_public(secret: &libsecp256k1::SecretKey) -> libsecp256k1::PublicKey {
libsecp256k1::PublicKey::from_secret_key(secret)
}
#[cfg(any(feature = "runtime-benchmarks", feature = "std"))]
pub fn eth_address(secret: &libsecp256k1::SecretKey) -> EvmAddress {
EvmAddress::from_slice(&keccak_256(&Self::eth_public(secret).serialize()[1..65])[12..])
}
#[cfg(any(feature = "runtime-benchmarks", feature = "std"))]
pub fn eth_sign(secret: &libsecp256k1::SecretKey, _who: &T::AccountId) -> EcdsaSignature {
let address = Self::eth_address(secret);
let what = address.0.as_slice();
let msg = keccak_256(&Self::ethereum_signable_message(&what, &[][..]));
let (sig, recovery_id) = libsecp256k1::sign(&libsecp256k1::Message::parse(&msg), secret);
let mut r = [0u8; 65];
r[0..64].copy_from_slice(&sig.serialize()[..]);
r[64] = recovery_id.serialize();
let signature = EcdsaSignature { 0: r };
signature
}
fn ethereum_signable_message(what: &[u8], extra: &[u8]) -> Vec<u8> {
let message_digest = keccak_256([what, extra].concat().as_slice());
[Ð_SIGNED_MESSAGE_PREFIX[..], &message_digest[..]].concat()
}
fn eth_recover(s: &EcdsaSignature, what: &[u8], extra: &[u8]) -> Option<EvmAddress> {
let msg = keccak_256(&Self::ethereum_signable_message(what, extra));
let mut res = EvmAddress::default();
res.0
.copy_from_slice(&keccak_256(&secp256k1_ecdsa_recover(&s.0, &msg).ok()?[..])[12..]);
Some(res)
}
}
fn account_to_default_evm_address(account_id: &impl Encode) -> EvmAddress {
EvmAddress::from_slice(&account_id.encode().as_slice()[12..])
}
fn create_default_substrate_address(address: &EvmAddress) -> AccountId32 {
let mut data: [u8; 32] = [0u8; 32];
data[0..4].copy_from_slice(b"evm:");
data[4..24].copy_from_slice(&address[..]);
AccountId32::from(data)
}
pub struct EvmAddressMapping<T>(sp_std::marker::PhantomData<T>);
impl<T: Config> AddressMapping<T::AccountId> for EvmAddressMapping<T>
where
T::AccountId: IsType<AccountId32>,
{
fn into_account_id(address: &EvmAddress) -> T::AccountId {
if let Some(acc) = Accounts::<T>::get(address) {
log::info!(
"AddressMapping::into_account_id - found matching account: {:?}",
acc
);
acc
} else {
create_default_substrate_address(address).into()
}
}
fn get_evm_address(account_id: &T::AccountId) -> Option<EvmAddress> {
EvmAddresses::<T>::get(account_id).or_else(|| {
let data: &[u8] = account_id.into_ref().as_ref();
if data.starts_with(b"evm:") {
Some(EvmAddress::from_slice(&data[4..24]))
} else {
None
}
})
}
fn get_or_create_evm_address(account_id: &T::AccountId) -> EvmAddress {
Self::get_evm_address(account_id).unwrap_or_else(|| {
let addr = account_to_default_evm_address(account_id);
Accounts::<T>::insert(&addr, &account_id);
EvmAddresses::<T>::insert(&account_id, &addr);
addr
})
}
fn get_default_evm_address(account_id: &T::AccountId) -> EvmAddress {
account_to_default_evm_address(account_id)
}
fn is_linked(account_id: &T::AccountId, evm: &EvmAddress) -> bool {
Self::get_evm_address(account_id).as_ref() == Some(evm)
|| &account_to_default_evm_address(account_id.into_ref()) == evm
}
}
impl<T: Config> BaseAddressMapping<T::AccountId> for EvmAddressMapping<T>
where
T::AccountId: IsType<AccountId32>,
{
fn into_account_id(address: EvmAddress) -> T::AccountId {
if let Some(acc) = Accounts::<T>::get(&address) {
log::info!("into_account_id - found matching account: {:?}", acc);
acc
} else {
create_default_substrate_address(&address).into()
}
}
}
pub struct CallKillAccount<T>(PhantomData<T>);
impl<T: Config> OnKilledAccount<T::AccountId> for CallKillAccount<T> {
fn on_killed_account(who: &T::AccountId) {
Accounts::<T>::remove(account_to_default_evm_address(who.into_ref()));
if let Some(evm_addr) = Pallet::<T>::evm_addresses(who) {
Accounts::<T>::remove(evm_addr);
EvmAddresses::<T>::remove(who);
}
}
}
impl<T: Config> StaticLookup for Pallet<T> {
type Source = MultiAddress<T::AccountId, AccountIndex>;
type Target = T::AccountId;
fn lookup(a: Self::Source) -> Result<Self::Target, LookupError> {
match a {
MultiAddress::Address20(i) => Ok(T::AddressMapping::into_account_id(
&EvmAddress::from_slice(&i),
)),
_ => Err(LookupError),
}
}
fn unlookup(a: Self::Target) -> Self::Source {
MultiAddress::Id(a)
}
}