#![cfg_attr(not(feature = "std"), no_std)]
mod benchmarking;
mod benchmarks;
mod mock;
mod secp_utils;
use codec::{Decode, Encode};
use eq_primitives::AccountGetter;
use eq_utils::log::eq_log;
use eq_utils::{eq_ensure, ok_or_error};
#[allow(unused_imports)]
use frame_support::{
debug, decl_error, decl_event, decl_module, decl_storage,
dispatch::IsSubType,
traits::{Currency, EnsureOrigin, Get, VestingSchedule},
weights::{DispatchClass, Pays, Weight},
};
use frame_system::{ensure_none, ensure_root, ensure_signed};
#[cfg(feature = "std")]
use serde::{self, Deserialize, Deserializer, Serialize, Serializer};
use sp_io::{crypto::secp256k1_ecdsa_recover, hashing::keccak_256};
#[cfg(feature = "std")]
use sp_runtime::traits::Zero;
use sp_runtime::{
traits::{CheckedSub, DispatchInfoOf, Saturating, SignedExtension},
transaction_validity::{
InvalidTransaction, TransactionLongevity, TransactionSource, TransactionValidity,
TransactionValidityError, ValidTransaction,
},
DispatchResult, RuntimeDebug,
};
use sp_std::{fmt::Debug, prelude::*};
pub trait WeightInfo {
fn claim(u: u32) -> Weight;
fn mint_claim(c: u32) -> Weight;
fn claim_attest(u: u32) -> Weight;
fn attest(u: u32) -> Weight;
fn validate_unsigned_claim(c: u32) -> Weight;
fn validate_unsigned_claim_attest(c: u32) -> Weight;
fn validate_prevalidate_attests(c: u32) -> Weight;
fn keccak256(i: u32) -> Weight;
fn eth_recover(i: u32) -> Weight;
}
type CurrencyOf<T> = <<T as Trait>::VestingSchedule as VestingSchedule<
<T as frame_system::Trait>::AccountId,
>>::Currency;
type BalanceOf<T> = <CurrencyOf<T> as Currency<<T as frame_system::Trait>::AccountId>>::Balance;
#[repr(u8)]
pub enum ValidityError {
InvalidEthereumSignature = 0,
SignerHasNoClaim = 1,
NoPermission = 2,
InvalidStatement = 3,
}
impl From<ValidityError> for u8 {
fn from(err: ValidityError) -> Self {
err as u8
}
}
pub trait Trait: frame_system::Trait {
type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
type WeightInfo: WeightInfo;
type VestingSchedule: VestingSchedule<Self::AccountId, Moment = Self::BlockNumber>;
type Prefix: Get<&'static [u8]>;
type MoveClaimOrigin: EnsureOrigin<Self::Origin>;
type VestingAccountGetter: AccountGetter<Self::AccountId>;
}
#[derive(Encode, Decode, Clone, Copy, Eq, PartialEq, RuntimeDebug)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub enum StatementKind {
Regular,
Saft,
}
impl Default for StatementKind {
fn default() -> Self {
StatementKind::Regular
}
}
#[derive(
Clone, Copy, PartialEq, PartialOrd, Ord, Eq, Encode, Decode, Default, RuntimeDebug, Hash,
)]
pub struct EthereumAddress([u8; 20]);
#[cfg(feature = "std")]
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))
}
}
#[cfg(feature = "std")]
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 {
debug::error!(
"{}:{}. Bad length of Ethereum address. Length: {:?}",
file!(),
line!(),
s.len()
);
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| {
debug::error!("{}:{}. Couldn't convert from hex.", file!(), line!());
serde::de::Error::custom(format!("{:?}", e))
})?;
let mut r = Self::default();
r.0.copy_from_slice(&raw);
Ok(r)
}
}
#[derive(Encode, Decode, Clone)]
pub struct EcdsaSignature(pub [u8; 65]);
impl AsRef<[u8; 65]> for EcdsaSignature {
fn as_ref(&self) -> &[u8; 65] {
&self.0
}
}
impl AsRef<[u8]> for EcdsaSignature {
fn as_ref(&self) -> &[u8] {
&self.0[..]
}
}
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[..])
}
}
decl_event!(
pub enum Event<T>
where
Balance = BalanceOf<T>,
AccountId = <T as frame_system::Trait>::AccountId,
{
Claimed(AccountId, EthereumAddress, Balance),
}
);
decl_error! {
pub enum Error for Module<T: Trait> {
InvalidEthereumSignature,
SignerHasNoClaim,
SenderHasNoClaim,
PotUnderflow,
InvalidStatement,
VestedBalanceExists,
}
}
decl_storage! {
trait Store for Module<T: Trait> as Claims {
Claims get(fn claims) build(|config: &GenesisConfig<T>| {
config.claims.iter().map(|(a, b, _, _)| (a.clone(), b.clone())).collect::<Vec<_>>()
}): map hasher(identity) EthereumAddress => Option<BalanceOf<T>>;
Total get(fn total) build(|config: &GenesisConfig<T>| {
config.claims.iter().fold(Zero::zero(), |acc: BalanceOf<T>, &(_, b, _, _)| acc + b)
}): BalanceOf<T>;
Vesting get(fn vesting) config():
map hasher(identity) EthereumAddress
=> Option<(BalanceOf<T>, BalanceOf<T>, T::BlockNumber)>;
Signing build(|config: &GenesisConfig<T>| {
config.claims.iter()
.filter_map(|(a, _, _, s)| Some((a.clone(), s.clone())))
.collect::<Vec<_>>()
}): map hasher(identity) EthereumAddress => bool;
Preclaims build(|config: &GenesisConfig<T>| {
config.claims.iter()
.filter_map(|(a, _, i, _)| Some((i.clone()?, a.clone())))
.collect::<Vec<_>>()
}): map hasher(identity) T::AccountId => Option<EthereumAddress>;
}
add_extra_genesis {
config(claims): Vec<(EthereumAddress, BalanceOf<T>, Option<T::AccountId>, bool)>;
}
}
decl_module! {
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
fn deposit_event() = default;
type Error = Error<T>;
const Prefix: &[u8] = T::Prefix::get();
#[weight = T::WeightInfo::claim(1)]
fn claim(origin, dest: T::AccountId, ethereum_signature: EcdsaSignature) {
ensure_none(origin)?;
let data = dest.using_encoded(to_ascii_hex);
let option_ethereum_address = Self::eth_recover(ðereum_signature, &data, &[][..]);
let signer = ok_or_error!(option_ethereum_address, Error::<T>::InvalidEthereumSignature,
"{}:{}. Invalid ethereum signature while recover. Dest: {:?}, signature: {:?}, data: {:?}.",
file!(), line!(), dest, ethereum_signature, data)?;
eq_ensure!(Signing::get(&signer) == false, Error::<T>::InvalidStatement,
"{}:{}. Cannot get signer. Who: {:?}.", file!(), line!(), signer);
Self::process_claim(signer, dest)?;
}
#[weight = T::WeightInfo::mint_claim(5_000)]
fn mint_claim(origin,
who: EthereumAddress,
value: BalanceOf<T>,
vesting_schedule: Option<(BalanceOf<T>, BalanceOf<T>, T::BlockNumber)>,
statement: bool,
) {
ensure_root(origin)?;
if vesting_schedule != None && value < vesting_schedule.unwrap().0 {
eq_log!(
"mint_claim error: value {:?} < vesting_schedule.locked {:?}",
value,
vesting_schedule.unwrap().0
);
eq_ensure!(false, Error::<T>::InvalidStatement,
"{}:{}. Amount to claim is less than vesting_schedule.locked. Amount to claim: {:?}, vesting_schedule: {:?}.",
file!(), line!(), value, vesting_schedule.unwrap().0);
}
<Total<T>>::mutate(|t| *t += value);
<Claims<T>>::insert(who, value);
if let Some(vs) = vesting_schedule {
<Vesting<T>>::insert(who, vs);
}
if statement {
Signing::insert(who, statement);
}
}
#[weight = T::WeightInfo::claim_attest(1)]
fn claim_attest(origin,
dest: T::AccountId,
ethereum_signature: EcdsaSignature,
statement: Vec<u8>,
) {
ensure_none(origin)?;
let data = dest.using_encoded(to_ascii_hex);
let option_ethereum_address = Self::eth_recover(ðereum_signature, &data, &statement);
let signer = ok_or_error!(option_ethereum_address, Error::<T>::InvalidEthereumSignature,
"{}:{}. Invalid ethereum signature while recover. Dest: {:?}, signature: {:?}, data: {:?}, statement: {:?}.",
file!(), line!(), dest, ethereum_signature, data, statement)?;
let s = Signing::get(signer);
if s {
eq_ensure!(get_statement_text() == &statement[..], Error::<T>::InvalidStatement,
"{}:{}. Get_statement_text() not equal to statement from params. Get statement text: {:?}, from params: {:?}.",
file!(), line!(), get_statement_text(), &statement[..]);
}
Self::process_claim(signer, dest)?;
}
#[weight = (T::WeightInfo::attest(1), DispatchClass::Normal, Pays::No)]
fn attest(origin, statement: Vec<u8>) {
let who = ensure_signed(origin)?;
let option_ethereum_address = Preclaims::<T>::get(&who);
let signer = ok_or_error!(option_ethereum_address, Error::<T>::SenderHasNoClaim,
"{}:{}. Sender not in Preclaims. Who: {:?}.", file!(), line!(), who)?;
let s = Signing::get(signer);
if s {
eq_ensure!(get_statement_text() == &statement[..], Error::<T>::InvalidStatement,
"{}:{}. Get_statement_text() not equal to statement from params. Get statement text: {:?}, from params: {:?}.",
file!(), line!(), get_statement_text(), &statement[..]);
}
Self::process_claim(signer, who.clone())?;
Preclaims::<T>::remove(&who);
}
#[weight = (
T::DbWeight::get().reads_writes(4, 4) + 100_000_000_000,
DispatchClass::Normal,
Pays::No
)]
fn move_claim(origin,
old: EthereumAddress,
new: EthereumAddress,
maybe_preclaim: Option<T::AccountId>,
) {
T::MoveClaimOrigin::try_origin(origin).map(|_| ()).or_else(ensure_root)?;
Claims::<T>::take(&old).map(|c| Claims::<T>::insert(&new, c));
Vesting::<T>::take(&old).map(|c| Vesting::<T>::insert(&new, c));
let s = Signing::take(&old);
Signing::insert(&new, s);
maybe_preclaim.map(|preclaim| Preclaims::<T>::mutate(&preclaim, |maybe_o|
if maybe_o.as_ref().map_or(false, |o| o == &old) { *maybe_o = Some(new) }
));
}
}
}
pub fn get_statement_text() -> &'static [u8] {
&b"I hereby agree to the terms of the statement whose SHA-256 multihash is \
Qmc1XYqT6S39WNp2UeiRUrZichUWUPpGEThDE6dAb3f6Ny. (This may be found at the URL: \
https://equilibrium.io/tokenswap/docs/token_swap_t&cs.pdf)"[..]
}
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
}
impl<T: Trait> Module<T> {
fn ethereum_signable_message(what: &[u8], extra: &[u8]) -> Vec<u8> {
let prefix = T::Prefix::get();
let mut l = prefix.len() + what.len() + extra.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.extend_from_slice(extra);
v
}
fn eth_recover(s: &EcdsaSignature, what: &[u8], extra: &[u8]) -> Option<EthereumAddress> {
let msg = keccak_256(&Self::ethereum_signable_message(what, extra));
let mut res = EthereumAddress::default();
res.0
.copy_from_slice(&keccak_256(&secp256k1_ecdsa_recover(&s.0, &msg).ok()?[..])[12..]);
Some(res)
}
fn process_claim(signer: EthereumAddress, dest: T::AccountId) -> DispatchResult {
let option_balance_of = <Claims<T>>::get(&signer);
let balance_due = ok_or_error!(
option_balance_of,
Error::<T>::SignerHasNoClaim,
"{}:{}. Signer has no claim. Address: {:?}.",
file!(),
line!(),
signer
)?;
let option_checked = Self::total().checked_sub(&balance_due);
let new_total = ok_or_error!(option_checked, Error::<T>::PotUnderflow,
"{}:{}. Not enough in the pot to pay out some unvested amount. Total: {:?}, balanceOf: {:?}, address: {:?}",
file!(), line!(), Self::total(), balance_due, signer)?;
let vesting = Vesting::<T>::get(&signer);
if vesting.is_some() && T::VestingSchedule::vesting_balance(&dest).is_some() {
return Err({
debug::error!("{}:{}. The account already has a vested balance. Who ID: {:?}, dest ethereum address: {:?}.",
file!(), line!(), dest, signer);
Error::<T>::VestedBalanceExists.into()
});
}
if let Some(vs) = vesting {
let initial_balance = balance_due.saturating_sub(vs.0);
CurrencyOf::<T>::deposit_creating(&dest, initial_balance);
let vesting_account_id = T::VestingAccountGetter::get_account_id();
#[allow(unused_must_use)]
{
CurrencyOf::<T>::deposit_creating(&vesting_account_id, vs.0);
}
T::VestingSchedule::add_vesting_schedule(&dest, vs.0, vs.1, vs.2)
.expect("No other vesting schedule exists, as checked above; qed");
} else {
CurrencyOf::<T>::deposit_creating(&dest, balance_due);
}
<Total<T>>::put(new_total);
<Claims<T>>::remove(&signer);
<Vesting<T>>::remove(&signer);
Signing::remove(&signer);
Self::deposit_event(RawEvent::Claimed(dest, signer, balance_due));
Ok(())
}
}
impl<T: Trait> sp_runtime::traits::ValidateUnsigned for Module<T> {
type Call = Call<T>;
fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
const PRIORITY: u64 = 100;
let (maybe_signer, maybe_statement) = match call {
Call::claim(account, ethereum_signature) => {
let data = account.using_encoded(to_ascii_hex);
(Self::eth_recover(ðereum_signature, &data, &[][..]), None)
}
Call::claim_attest(account, ethereum_signature, statement) => {
let data = account.using_encoded(to_ascii_hex);
(
Self::eth_recover(ðereum_signature, &data, &statement),
Some(statement.as_slice()),
)
}
_ => {
debug::error!("{}:{}. Call didn't match claim options", file!(), line!());
return Err(InvalidTransaction::Call.into());
}
};
let signer = ok_or_error!(
maybe_signer,
InvalidTransaction::Custom(ValidityError::InvalidEthereumSignature.into()),
"{}:{}. Invalid Ethereum signature. Signature: {:?}.",
file!(),
line!(),
maybe_signer
)?;
let e = InvalidTransaction::Custom(ValidityError::SignerHasNoClaim.into());
eq_ensure!(
<Claims<T>>::contains_key(&signer),
e,
"{}:{}. Signer has no claim. Who: {:?}.",
file!(),
line!(),
signer
);
let e = InvalidTransaction::Custom(ValidityError::InvalidStatement.into());
let s = Signing::get(signer);
if s {
eq_ensure!(Some(get_statement_text()) == maybe_statement, e,
"{}:{}. Get_statement_text() not equal to statement from params. Get statement text: {:?}, from params: {:?}.",
file!(), line!(), get_statement_text(), maybe_statement);
} else {
eq_ensure!(
maybe_statement.is_none(),
e,
"{}:{}. Statement is none",
file!(),
line!()
);
}
Ok(ValidTransaction {
priority: PRIORITY,
requires: vec![],
provides: vec![("claims", signer).encode()],
longevity: TransactionLongevity::max_value(),
propagate: true,
})
}
}
#[derive(Encode, Decode, Clone, Eq, PartialEq)]
pub struct PrevalidateAttests<T: Trait + Send + Sync>(sp_std::marker::PhantomData<T>)
where
<T as frame_system::Trait>::Call: IsSubType<Call<T>>;
impl<T: Trait + Send + Sync> Debug for PrevalidateAttests<T>
where
<T as frame_system::Trait>::Call: IsSubType<Call<T>>,
{
#[cfg(feature = "std")]
fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
write!(f, "PrevalidateAttests")
}
#[cfg(not(feature = "std"))]
fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
Ok(())
}
}
impl<T: Trait + Send + Sync> PrevalidateAttests<T>
where
<T as frame_system::Trait>::Call: IsSubType<Call<T>>,
{
pub fn new() -> Self {
Self(sp_std::marker::PhantomData)
}
}
impl<T: Trait + Send + Sync> SignedExtension for PrevalidateAttests<T>
where
<T as frame_system::Trait>::Call: IsSubType<Call<T>>,
{
type AccountId = T::AccountId;
type Call = <T as frame_system::Trait>::Call;
type AdditionalSigned = ();
type Pre = ();
const IDENTIFIER: &'static str = "PrevalidateAttests";
fn additional_signed(&self) -> Result<Self::AdditionalSigned, TransactionValidityError> {
Ok(())
}
fn validate(
&self,
who: &Self::AccountId,
call: &Self::Call,
_info: &DispatchInfoOf<Self::Call>,
_len: usize,
) -> TransactionValidity {
if let Some(local_call) = call.is_sub_type() {
if let Call::attest(attested_statement) = local_call {
let option_ethereum_address = Preclaims::<T>::get(who);
let signer = ok_or_error!(
option_ethereum_address,
InvalidTransaction::Custom(ValidityError::SignerHasNoClaim.into()),
"{}:{}. Signer has no claim. Who: {:?}.",
file!(),
line!(),
who
)?;
let s = Signing::get(signer);
if s {
let e = InvalidTransaction::Custom(ValidityError::InvalidStatement.into());
eq_ensure!(&attested_statement[..] == get_statement_text(), e,
"{}:{}. Get_statement_text() not equal to statement from call. Get statement text: {:?}, from call: {:?}.",
file!(), line!(), get_statement_text(), &attested_statement[..]);
}
}
}
Ok(ValidTransaction::default())
}
}