#![cfg_attr(not(feature = "std"), no_std)] use frame_support::{ ensure, pallet_prelude::*, traits::{ Currency, ExistenceRequirement, Get, RankedMembers, RankedMembersSwapHandler, VestingSchedule, }, DefaultNoBound, }; use frame_system::pallet_prelude::*; use serde::{self, Deserialize, Deserializer, Serialize, Serializer}; pub use pallet::*; use sp_io::{crypto::secp256k1_ecdsa_recover, hashing::keccak_256}; use sp_runtime::traits::{CheckedSub, CheckedDiv, BlockNumberProvider}; use sp_std::prelude::*; extern crate alloc; #[cfg(not(feature = "std"))] use alloc::{format, string::String}; pub mod weights; pub use crate::weights::WeightInfo; mod tests; mod mock; mod benchmarking; mod secp_utils; /// An ethereum address (i.e. 20 bytes, used to represent an Ethereum account). /// /// This gets serialized to the 0x-prefixed hex representation. #[derive(Clone, Copy, PartialEq, Eq, Encode, Decode, Default, RuntimeDebug, TypeInfo)] pub struct EthereumAddress(pub [u8; 20]); impl Serialize for EthereumAddress { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let hex: String = rustc_hex::ToHex::to_hex(&self.0[..]); serializer.serialize_str(&format!("0x{}", hex)) } } impl<'de> Deserialize<'de> for EthereumAddress { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let base_string = String::deserialize(deserializer)?; let offset = if base_string.starts_with("0x") { 2 } else { 0 }; let s = &base_string[offset..]; if s.len() != 40 { Err(serde::de::Error::custom( "Bad length of Ethereum address (should be 42 including `0x`)", ))?; } let raw: Vec = rustc_hex::FromHex::from_hex(s) .map_err(|e| serde::de::Error::custom(format!("{:?}", e)))?; let mut r = Self::default(); r.0.copy_from_slice(&raw); Ok(r) } } #[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[..]) } } type CurrencyOf = <>::VestingSchedule as VestingSchedule< ::AccountId >>::Currency; type BalanceOf = as Currency< ::AccountId> >::Balance; type RankOf = as RankedMembers>::Rank; type AccountIdOf = as RankedMembers>::AccountId; #[frame_support::pallet] pub mod pallet { use super::*; #[pallet::pallet] #[pallet::without_storage_info] pub struct Pallet(_); #[pallet::config] pub trait Config: frame_system::Config + pallet_ranked_collective::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; type VestingSchedule: VestingSchedule>; type BlockNumberProvider: BlockNumberProvider>; type MemberSwappedHandler: RankedMembersSwapHandler, RankOf>; #[pallet::constant] type Prefix: Get<&'static [u8]>; #[pallet::constant] type MaximumWithdrawAmount: Get>; #[pallet::constant] type VestingBlocks: Get; type WeightInfo: WeightInfo; } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event, I: 'static = ()> { Claimed { receiver: T::AccountId, donor: T::AccountId, amount: BalanceOf, rank: Option>, }, } #[pallet::error] pub enum Error { InvalidEthereumSignature, InvalidEthereumAddress, NoBalanceToClaim, AddressDecodingFailed, ArithmeticError, PotUnderflow, } #[pallet::storage] pub type Total, I: 'static = ()> = StorageValue<_, BalanceOf, ValueQuery>; #[pallet::genesis_config] #[derive(DefaultNoBound)] pub struct GenesisConfig, I: 'static = ()> { pub total: BalanceOf, pub members_and_ranks: Vec<(T::AccountId, u16)>, } #[pallet::genesis_build] impl, I: 'static> BuildGenesisConfig for GenesisConfig { fn build(&self) { let cult_accounts: Vec<_> = self .members_and_ranks .iter() .map(|(account_id, rank)| { assert!( pallet_ranked_collective::Pallet::::do_add_member_to_rank( account_id.clone(), *rank, false).is_ok(), "error during adding and promotion" ); account_id }) .collect(); assert!( self.members_and_ranks.len() == cult_accounts.len(), "duplicates in `members_and_ranks`" ); Total::::put(self.total); } } #[pallet::call] impl, I: 'static> Pallet { #[pallet::call_index(0)] #[pallet::weight(>::WeightInfo::claim())] pub fn claim( origin: OriginFor, ethereum_address: EthereumAddress, ethereum_signature: EcdsaSignature, ) -> DispatchResult { let who = ensure_signed(origin)?; let data = who.using_encoded(to_ascii_hex); let recovered_address = Self::recover_ethereum_address( ðereum_signature, &data, ).ok_or(Error::::InvalidEthereumSignature)?; ensure!(recovered_address == ethereum_address, Error::::InvalidEthereumAddress); Self::do_claim(who, ethereum_address) } } } fn to_ascii_hex(data: &[u8]) -> Vec { 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 } impl, I: 'static> Pallet { fn ethereum_signable_message(what: &[u8]) -> Vec { let prefix = T::Prefix::get(); let mut l = prefix.len() + what.len(); let mut rev = Vec::new(); while l > 0 { rev.push(b'0' + (l % 10) as u8); l /= 10; } let mut v = b"\x19Ethereum Signed Message:\n".to_vec(); v.extend(rev.into_iter().rev()); v.extend_from_slice(prefix); v.extend_from_slice(what); v } fn recover_ethereum_address(s: &EcdsaSignature, what: &[u8]) -> Option { let msg = keccak_256(&Self::ethereum_signable_message(what)); let mut res = EthereumAddress::default(); res.0 .copy_from_slice(&keccak_256(&secp256k1_ecdsa_recover(&s.0, &msg).ok()?[..])[12..]); Some(res) } fn into_account_id(address: EthereumAddress) -> Result { let mut data = [0u8; 32]; data[0..4].copy_from_slice(b"evm:"); data[4..24].copy_from_slice(&address.0[..]); let hash = sp_core::keccak_256(&data); T::AccountId::decode(&mut &hash[..]) } fn do_claim(receiver: T::AccountId, ethereum_address: EthereumAddress) -> DispatchResult { let donor = Self::into_account_id(ethereum_address).ok() .ok_or(Error::::AddressDecodingFailed)?; let balance_due = CurrencyOf::::free_balance(&donor); ensure!(balance_due >= CurrencyOf::::minimum_balance(), Error::::NoBalanceToClaim); let new_total = Total::::get() .checked_sub(&balance_due) .ok_or(Error::::PotUnderflow)?; CurrencyOf::::transfer( &donor, &receiver, balance_due, ExistenceRequirement::AllowDeath, )?; let max_amount = T::MaximumWithdrawAmount::get(); if balance_due > max_amount { let vesting_balance = balance_due .checked_sub(&max_amount) .ok_or(Error::::ArithmeticError)?; let vesting_blocks = T::VestingBlocks::get(); let per_block_balance = vesting_balance .checked_div(&vesting_blocks.into()) .ok_or(Error::::ArithmeticError)?; T::VestingSchedule::add_vesting_schedule( &receiver, vesting_balance, per_block_balance, T::BlockNumberProvider::current_block_number(), )?; } let rank = if let Some(rank) = as RankedMembers>::rank_of(&donor) { pallet_ranked_collective::Pallet::::do_remove_member_from_rank(&donor, rank)?; let new_rank = match as RankedMembers>::rank_of(&receiver) { Some(current_rank) if current_rank >= rank => current_rank, Some(current_rank) if current_rank < rank => { for _ in 0..rank - current_rank { pallet_ranked_collective::Pallet::::do_promote_member(receiver.clone(), None, false)?; } rank }, _ => { pallet_ranked_collective::Pallet::::do_add_member_to_rank(receiver.clone(), rank, false)?; rank }, }; >::MemberSwappedHandler::swapped(&donor, &receiver, new_rank); Some(new_rank) } else { None }; Total::::put(new_total); Self::deposit_event(Event::::Claimed { receiver, donor, amount: balance_due, rank, }); Ok(()) } }