314 lines
10 KiB
Rust
314 lines
10 KiB
Rust
#![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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
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<D>(deserializer: D) -> Result<Self, D::Error>
|
|
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<u8> = 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<T, I> = <<T as Config<I>>::VestingSchedule as VestingSchedule<
|
|
<T as frame_system::Config>::AccountId
|
|
>>::Currency;
|
|
|
|
type BalanceOf<T, I> = <CurrencyOf<T, I> as Currency<
|
|
<T as frame_system::Config>::AccountId>
|
|
>::Balance;
|
|
|
|
type RankOf<T, I> = <pallet_ranked_collective::Pallet::<T, I> as RankedMembers>::Rank;
|
|
type AccountIdOf<T, I> = <pallet_ranked_collective::Pallet::<T, I> as RankedMembers>::AccountId;
|
|
|
|
#[frame_support::pallet]
|
|
pub mod pallet {
|
|
use super::*;
|
|
|
|
#[pallet::pallet]
|
|
#[pallet::without_storage_info]
|
|
pub struct Pallet<T, I = ()>(_);
|
|
|
|
#[pallet::config]
|
|
pub trait Config<I: 'static = ()>: frame_system::Config + pallet_ranked_collective::Config<I> {
|
|
type RuntimeEvent: From<Event<Self, I>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
|
|
|
type VestingSchedule: VestingSchedule<Self::AccountId, Moment = BlockNumberFor<Self>>;
|
|
type BlockNumberProvider: BlockNumberProvider<BlockNumber = BlockNumberFor<Self>>;
|
|
type MemberSwappedHandler: RankedMembersSwapHandler<AccountIdOf<Self, I>, RankOf<Self, I>>;
|
|
|
|
#[pallet::constant]
|
|
type Prefix: Get<&'static [u8]>;
|
|
|
|
#[pallet::constant]
|
|
type MaximumWithdrawAmount: Get<BalanceOf<Self, I>>;
|
|
|
|
#[pallet::constant]
|
|
type VestingBlocks: Get<u32>;
|
|
|
|
type WeightInfo: WeightInfo;
|
|
}
|
|
|
|
#[pallet::event]
|
|
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
|
pub enum Event<T: Config<I>, I: 'static = ()> {
|
|
Claimed {
|
|
receiver: T::AccountId,
|
|
donor: T::AccountId,
|
|
amount: BalanceOf<T, I>,
|
|
rank: Option<RankOf<T, I>>,
|
|
},
|
|
}
|
|
|
|
#[pallet::error]
|
|
pub enum Error<T, I = ()> {
|
|
InvalidEthereumSignature,
|
|
InvalidEthereumAddress,
|
|
NoBalanceToClaim,
|
|
AddressDecodingFailed,
|
|
ArithmeticError,
|
|
PotUnderflow,
|
|
}
|
|
|
|
#[pallet::storage]
|
|
pub type Total<T: Config<I>, I: 'static = ()> = StorageValue<_, BalanceOf<T, I>, ValueQuery>;
|
|
|
|
#[pallet::genesis_config]
|
|
#[derive(DefaultNoBound)]
|
|
pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
|
|
pub total: BalanceOf<T, I>,
|
|
pub members_and_ranks: Vec<(T::AccountId, u16)>,
|
|
}
|
|
|
|
#[pallet::genesis_build]
|
|
impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
|
|
fn build(&self) {
|
|
let cult_accounts: Vec<_> = self
|
|
.members_and_ranks
|
|
.iter()
|
|
.map(|(account_id, rank)| {
|
|
assert!(
|
|
pallet_ranked_collective::Pallet::<T, I>::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::<T, I>::put(self.total);
|
|
}
|
|
}
|
|
|
|
#[pallet::call]
|
|
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
|
#[pallet::call_index(0)]
|
|
#[pallet::weight(<T as Config<I>>::WeightInfo::claim())]
|
|
pub fn claim(
|
|
origin: OriginFor<T>,
|
|
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::<T, I>::InvalidEthereumSignature)?;
|
|
|
|
ensure!(recovered_address == ethereum_address,
|
|
Error::<T, I>::InvalidEthereumAddress);
|
|
|
|
Self::do_claim(who, ethereum_address)
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
|
fn ethereum_signable_message(what: &[u8]) -> Vec<u8> {
|
|
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<EthereumAddress> {
|
|
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<T::AccountId, codec::Error> {
|
|
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::<T, I>::AddressDecodingFailed)?;
|
|
|
|
let balance_due = CurrencyOf::<T, I>::free_balance(&donor);
|
|
ensure!(balance_due >= CurrencyOf::<T, I>::minimum_balance(),
|
|
Error::<T, I>::NoBalanceToClaim);
|
|
|
|
let new_total = Total::<T, I>::get()
|
|
.checked_sub(&balance_due)
|
|
.ok_or(Error::<T, I>::PotUnderflow)?;
|
|
|
|
CurrencyOf::<T, I>::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::<T, I>::ArithmeticError)?;
|
|
|
|
let vesting_blocks = T::VestingBlocks::get();
|
|
let per_block_balance = vesting_balance
|
|
.checked_div(&vesting_blocks.into())
|
|
.ok_or(Error::<T, I>::ArithmeticError)?;
|
|
|
|
T::VestingSchedule::add_vesting_schedule(
|
|
&receiver,
|
|
vesting_balance,
|
|
per_block_balance,
|
|
T::BlockNumberProvider::current_block_number(),
|
|
)?;
|
|
}
|
|
|
|
let rank = if let Some(rank) = <pallet_ranked_collective::Pallet::<T, I> as RankedMembers>::rank_of(&donor) {
|
|
pallet_ranked_collective::Pallet::<T, I>::do_remove_member_from_rank(&donor, rank)?;
|
|
let new_rank = match <pallet_ranked_collective::Pallet::<T, I> 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::<T, I>::do_promote_member(receiver.clone(), None, false)?;
|
|
}
|
|
rank
|
|
},
|
|
_ => {
|
|
pallet_ranked_collective::Pallet::<T, I>::do_add_member_to_rank(receiver.clone(), rank, false)?;
|
|
rank
|
|
},
|
|
};
|
|
<T as pallet::Config<I>>::MemberSwappedHandler::swapped(&donor, &receiver, new_rank);
|
|
Some(new_rank)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Total::<T, I>::put(new_total);
|
|
Self::deposit_event(Event::<T, I>::Claimed {
|
|
receiver,
|
|
donor,
|
|
amount: balance_due,
|
|
rank,
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
}
|