pezkuwi_runtime_common/claims/
mod.rs

1// Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute
2// This file is part of Pezkuwi.
3
4// Pezkuwi is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Pezkuwi is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Pezkuwi.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Pezpallet to process claims from Ethereum addresses.
18
19#[cfg(not(feature = "std"))]
20use alloc::{format, string::String};
21use alloc::{vec, vec::Vec};
22use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
23use core::fmt::Debug;
24use pezframe_support::{
25	ensure,
26	traits::{Currency, Get, IsSubType, VestingSchedule},
27	weights::Weight,
28	DefaultNoBound,
29};
30use pezkuwi_primitives::ValidityError;
31pub use pezpallet::*;
32use pezsp_io::{crypto::secp256k1_ecdsa_recover, hashing::keccak_256};
33use pezsp_runtime::{
34	impl_tx_ext_default,
35	traits::{
36		AsSystemOriginSigner, AsTransactionAuthorizedOrigin, CheckedSub, DispatchInfoOf,
37		Dispatchable, TransactionExtension, Zero,
38	},
39	transaction_validity::{
40		InvalidTransaction, TransactionSource, TransactionValidity, TransactionValidityError,
41		ValidTransaction,
42	},
43	RuntimeDebug,
44};
45use scale_info::TypeInfo;
46use serde::{self, Deserialize, Deserializer, Serialize, Serializer};
47
48type CurrencyOf<T> = <<T as Config>::VestingSchedule as VestingSchedule<
49	<T as pezframe_system::Config>::AccountId,
50>>::Currency;
51type BalanceOf<T> = <CurrencyOf<T> as Currency<<T as pezframe_system::Config>::AccountId>>::Balance;
52
53pub trait WeightInfo {
54	fn claim() -> Weight;
55	fn mint_claim() -> Weight;
56	fn claim_attest() -> Weight;
57	fn attest() -> Weight;
58	fn move_claim() -> Weight;
59	fn prevalidate_attests() -> Weight;
60}
61
62pub struct TestWeightInfo;
63impl WeightInfo for TestWeightInfo {
64	fn claim() -> Weight {
65		Weight::zero()
66	}
67	fn mint_claim() -> Weight {
68		Weight::zero()
69	}
70	fn claim_attest() -> Weight {
71		Weight::zero()
72	}
73	fn attest() -> Weight {
74		Weight::zero()
75	}
76	fn move_claim() -> Weight {
77		Weight::zero()
78	}
79	fn prevalidate_attests() -> Weight {
80		Weight::zero()
81	}
82}
83
84/// The kind of statement an account needs to make for a claim to be valid.
85#[derive(
86	Encode,
87	Decode,
88	DecodeWithMemTracking,
89	Clone,
90	Copy,
91	Eq,
92	PartialEq,
93	RuntimeDebug,
94	TypeInfo,
95	Serialize,
96	Deserialize,
97	MaxEncodedLen,
98)]
99pub enum StatementKind {
100	/// Statement required to be made by non-SAFT holders.
101	Regular,
102	/// Statement required to be made by SAFT holders.
103	Saft,
104}
105
106impl StatementKind {
107	/// Convert this to the (English) statement it represents.
108	fn to_text(self) -> &'static [u8] {
109		match self {
110			StatementKind::Regular => {
111				&b"I hereby agree to the terms of the statement whose SHA-256 multihash is \
112				Qmc1XYqT6S39WNp2UeiRUrZichUWUPpGEThDE6dAb3f6Ny. (This may be found at the URL: \
113				https://statement.polkadot.network/regular.html)"[..]
114			},
115			StatementKind::Saft => {
116				&b"I hereby agree to the terms of the statement whose SHA-256 multihash is \
117				QmXEkMahfhHJPzT3RjkXiZVFi77ZeVeuxtAjhojGRNYckz. (This may be found at the URL: \
118				https://statement.polkadot.network/saft.html)"[..]
119			},
120		}
121	}
122}
123
124impl Default for StatementKind {
125	fn default() -> Self {
126		StatementKind::Regular
127	}
128}
129
130/// An Ethereum address (i.e. 20 bytes, used to represent an Ethereum account).
131///
132/// This gets serialized to the 0x-prefixed hex representation.
133#[derive(
134	Clone,
135	Copy,
136	PartialEq,
137	Eq,
138	Encode,
139	Decode,
140	DecodeWithMemTracking,
141	Default,
142	RuntimeDebug,
143	TypeInfo,
144	MaxEncodedLen,
145)]
146pub struct EthereumAddress(pub [u8; 20]);
147
148impl Serialize for EthereumAddress {
149	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
150	where
151		S: Serializer,
152	{
153		let hex: String = rustc_hex::ToHex::to_hex(&self.0[..]);
154		serializer.serialize_str(&format!("0x{}", hex))
155	}
156}
157
158impl<'de> Deserialize<'de> for EthereumAddress {
159	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
160	where
161		D: Deserializer<'de>,
162	{
163		let base_string = String::deserialize(deserializer)?;
164		let offset = if base_string.starts_with("0x") { 2 } else { 0 };
165		let s = &base_string[offset..];
166		if s.len() != 40 {
167			Err(serde::de::Error::custom(
168				"Bad length of Ethereum address (should be 42 including '0x')",
169			))?;
170		}
171		let raw: Vec<u8> = rustc_hex::FromHex::from_hex(s)
172			.map_err(|e| serde::de::Error::custom(format!("{:?}", e)))?;
173		let mut r = Self::default();
174		r.0.copy_from_slice(&raw);
175		Ok(r)
176	}
177}
178
179impl AsRef<[u8]> for EthereumAddress {
180	fn as_ref(&self) -> &[u8] {
181		&self.0[..]
182	}
183}
184
185#[derive(Encode, Decode, DecodeWithMemTracking, Clone, TypeInfo, MaxEncodedLen)]
186pub struct EcdsaSignature(pub [u8; 65]);
187
188impl PartialEq for EcdsaSignature {
189	fn eq(&self, other: &Self) -> bool {
190		&self.0[..] == &other.0[..]
191	}
192}
193
194impl core::fmt::Debug for EcdsaSignature {
195	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
196		write!(f, "EcdsaSignature({:?})", &self.0[..])
197	}
198}
199
200#[pezframe_support::pezpallet]
201pub mod pezpallet {
202	use super::*;
203	use pezframe_support::pezpallet_prelude::*;
204	use pezframe_system::pezpallet_prelude::*;
205
206	#[pezpallet::pezpallet]
207	pub struct Pezpallet<T>(_);
208
209	/// Configuration trait.
210	#[pezpallet::config]
211	pub trait Config: pezframe_system::Config {
212		/// The overarching event type.
213		#[allow(deprecated)]
214		type RuntimeEvent: From<Event<Self>>
215			+ IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
216		type VestingSchedule: VestingSchedule<Self::AccountId, Moment = BlockNumberFor<Self>>;
217		#[pezpallet::constant]
218		type Prefix: Get<&'static [u8]>;
219		type MoveClaimOrigin: EnsureOrigin<Self::RuntimeOrigin>;
220		type WeightInfo: WeightInfo;
221	}
222
223	#[pezpallet::event]
224	#[pezpallet::generate_deposit(pub(super) fn deposit_event)]
225	pub enum Event<T: Config> {
226		/// Someone claimed some DOTs.
227		Claimed { who: T::AccountId, ethereum_address: EthereumAddress, amount: BalanceOf<T> },
228	}
229
230	#[pezpallet::error]
231	pub enum Error<T> {
232		/// Invalid Ethereum signature.
233		InvalidEthereumSignature,
234		/// Ethereum address has no claim.
235		SignerHasNoClaim,
236		/// Account ID sending transaction has no claim.
237		SenderHasNoClaim,
238		/// There's not enough in the pot to pay out some unvested amount. Generally implies a
239		/// logic error.
240		PotUnderflow,
241		/// A needed statement was not included.
242		InvalidStatement,
243		/// The account already has a vested balance.
244		VestedBalanceExists,
245	}
246
247	#[pezpallet::storage]
248	pub type Claims<T: Config> = StorageMap<_, Identity, EthereumAddress, BalanceOf<T>>;
249
250	#[pezpallet::storage]
251	pub type Total<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
252
253	/// Vesting schedule for a claim.
254	/// First balance is the total amount that should be held for vesting.
255	/// Second balance is how much should be unlocked per block.
256	/// The block number is when the vesting should start.
257	#[pezpallet::storage]
258	pub type Vesting<T: Config> =
259		StorageMap<_, Identity, EthereumAddress, (BalanceOf<T>, BalanceOf<T>, BlockNumberFor<T>)>;
260
261	/// The statement kind that must be signed, if any.
262	#[pezpallet::storage]
263	pub type Signing<T> = StorageMap<_, Identity, EthereumAddress, StatementKind>;
264
265	/// Pre-claimed Ethereum accounts, by the Account ID that they are claimed to.
266	#[pezpallet::storage]
267	pub type Preclaims<T: Config> = StorageMap<_, Identity, T::AccountId, EthereumAddress>;
268
269	#[pezpallet::genesis_config]
270	#[derive(DefaultNoBound)]
271	pub struct GenesisConfig<T: Config> {
272		pub claims:
273			Vec<(EthereumAddress, BalanceOf<T>, Option<T::AccountId>, Option<StatementKind>)>,
274		pub vesting: Vec<(EthereumAddress, (BalanceOf<T>, BalanceOf<T>, BlockNumberFor<T>))>,
275	}
276
277	#[pezpallet::genesis_build]
278	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
279		fn build(&self) {
280			// build `Claims`
281			self.claims.iter().map(|(a, b, _, _)| (*a, *b)).for_each(|(a, b)| {
282				Claims::<T>::insert(a, b);
283			});
284			// build `Total`
285			Total::<T>::put(
286				self.claims
287					.iter()
288					.fold(Zero::zero(), |acc: BalanceOf<T>, &(_, b, _, _)| acc + b),
289			);
290			// build `Vesting`
291			self.vesting.iter().for_each(|(k, v)| {
292				Vesting::<T>::insert(k, v);
293			});
294			// build `Signing`
295			self.claims
296				.iter()
297				.filter_map(|(a, _, _, s)| Some((*a, (*s)?)))
298				.for_each(|(a, s)| {
299					Signing::<T>::insert(a, s);
300				});
301			// build `Preclaims`
302			self.claims.iter().filter_map(|(a, _, i, _)| Some((i.clone()?, *a))).for_each(
303				|(i, a)| {
304					Preclaims::<T>::insert(i, a);
305				},
306			);
307		}
308	}
309
310	#[pezpallet::hooks]
311	impl<T: Config> Hooks<BlockNumberFor<T>> for Pezpallet<T> {}
312
313	#[pezpallet::call]
314	impl<T: Config> Pezpallet<T> {
315		/// Make a claim to collect your DOTs.
316		///
317		/// The dispatch origin for this call must be _None_.
318		///
319		/// Unsigned Validation:
320		/// A call to claim is deemed valid if the signature provided matches
321		/// the expected signed message of:
322		///
323		/// > Ethereum Signed Message:
324		/// > (configured prefix string)(address)
325		///
326		/// and `address` matches the `dest` account.
327		///
328		/// Parameters:
329		/// - `dest`: The destination account to payout the claim.
330		/// - `ethereum_signature`: The signature of an ethereum signed message matching the format
331		///   described above.
332		///
333		/// <weight>
334		/// The weight of this call is invariant over the input parameters.
335		/// Weight includes logic to validate unsigned `claim` call.
336		///
337		/// Total Complexity: O(1)
338		/// </weight>
339		#[pezpallet::call_index(0)]
340		#[pezpallet::weight(T::WeightInfo::claim())]
341		pub fn claim(
342			origin: OriginFor<T>,
343			dest: T::AccountId,
344			ethereum_signature: EcdsaSignature,
345		) -> DispatchResult {
346			ensure_none(origin)?;
347
348			let data = dest.using_encoded(to_ascii_hex);
349			let signer = Self::eth_recover(&ethereum_signature, &data, &[][..])
350				.ok_or(Error::<T>::InvalidEthereumSignature)?;
351			ensure!(Signing::<T>::get(&signer).is_none(), Error::<T>::InvalidStatement);
352
353			Self::process_claim(signer, dest)?;
354			Ok(())
355		}
356
357		/// Mint a new claim to collect DOTs.
358		///
359		/// The dispatch origin for this call must be _Root_.
360		///
361		/// Parameters:
362		/// - `who`: The Ethereum address allowed to collect this claim.
363		/// - `value`: The number of DOTs that will be claimed.
364		/// - `vesting_schedule`: An optional vesting schedule for these DOTs.
365		///
366		/// <weight>
367		/// The weight of this call is invariant over the input parameters.
368		/// We assume worst case that both vesting and statement is being inserted.
369		///
370		/// Total Complexity: O(1)
371		/// </weight>
372		#[pezpallet::call_index(1)]
373		#[pezpallet::weight(T::WeightInfo::mint_claim())]
374		pub fn mint_claim(
375			origin: OriginFor<T>,
376			who: EthereumAddress,
377			value: BalanceOf<T>,
378			vesting_schedule: Option<(BalanceOf<T>, BalanceOf<T>, BlockNumberFor<T>)>,
379			statement: Option<StatementKind>,
380		) -> DispatchResult {
381			ensure_root(origin)?;
382
383			Total::<T>::mutate(|t| *t += value);
384			Claims::<T>::insert(who, value);
385			if let Some(vs) = vesting_schedule {
386				Vesting::<T>::insert(who, vs);
387			}
388			if let Some(s) = statement {
389				Signing::<T>::insert(who, s);
390			}
391			Ok(())
392		}
393
394		/// Make a claim to collect your DOTs by signing a statement.
395		///
396		/// The dispatch origin for this call must be _None_.
397		///
398		/// Unsigned Validation:
399		/// A call to `claim_attest` is deemed valid if the signature provided matches
400		/// the expected signed message of:
401		///
402		/// > Ethereum Signed Message:
403		/// > (configured prefix string)(address)(statement)
404		///
405		/// and `address` matches the `dest` account; the `statement` must match that which is
406		/// expected according to your purchase arrangement.
407		///
408		/// Parameters:
409		/// - `dest`: The destination account to payout the claim.
410		/// - `ethereum_signature`: The signature of an ethereum signed message matching the format
411		///   described above.
412		/// - `statement`: The identity of the statement which is being attested to in the
413		///   signature.
414		///
415		/// <weight>
416		/// The weight of this call is invariant over the input parameters.
417		/// Weight includes logic to validate unsigned `claim_attest` call.
418		///
419		/// Total Complexity: O(1)
420		/// </weight>
421		#[pezpallet::call_index(2)]
422		#[pezpallet::weight(T::WeightInfo::claim_attest())]
423		pub fn claim_attest(
424			origin: OriginFor<T>,
425			dest: T::AccountId,
426			ethereum_signature: EcdsaSignature,
427			statement: Vec<u8>,
428		) -> DispatchResult {
429			ensure_none(origin)?;
430
431			let data = dest.using_encoded(to_ascii_hex);
432			let signer = Self::eth_recover(&ethereum_signature, &data, &statement)
433				.ok_or(Error::<T>::InvalidEthereumSignature)?;
434			if let Some(s) = Signing::<T>::get(signer) {
435				ensure!(s.to_text() == &statement[..], Error::<T>::InvalidStatement);
436			}
437			Self::process_claim(signer, dest)?;
438			Ok(())
439		}
440
441		/// Attest to a statement, needed to finalize the claims process.
442		///
443		/// WARNING: Insecure unless your chain includes `PrevalidateAttests` as a
444		/// `TransactionExtension`.
445		///
446		/// Unsigned Validation:
447		/// A call to attest is deemed valid if the sender has a `Preclaim` registered
448		/// and provides a `statement` which is expected for the account.
449		///
450		/// Parameters:
451		/// - `statement`: The identity of the statement which is being attested to in the
452		///   signature.
453		///
454		/// <weight>
455		/// The weight of this call is invariant over the input parameters.
456		/// Weight includes logic to do pre-validation on `attest` call.
457		///
458		/// Total Complexity: O(1)
459		/// </weight>
460		#[pezpallet::call_index(3)]
461		#[pezpallet::weight((
462			T::WeightInfo::attest(),
463			DispatchClass::Normal,
464			Pays::No
465		))]
466		pub fn attest(origin: OriginFor<T>, statement: Vec<u8>) -> DispatchResult {
467			let who = ensure_signed(origin)?;
468			let signer = Preclaims::<T>::get(&who).ok_or(Error::<T>::SenderHasNoClaim)?;
469			if let Some(s) = Signing::<T>::get(signer) {
470				ensure!(s.to_text() == &statement[..], Error::<T>::InvalidStatement);
471			}
472			Self::process_claim(signer, who.clone())?;
473			Preclaims::<T>::remove(&who);
474			Ok(())
475		}
476
477		#[pezpallet::call_index(4)]
478		#[pezpallet::weight(T::WeightInfo::move_claim())]
479		pub fn move_claim(
480			origin: OriginFor<T>,
481			old: EthereumAddress,
482			new: EthereumAddress,
483			maybe_preclaim: Option<T::AccountId>,
484		) -> DispatchResultWithPostInfo {
485			T::MoveClaimOrigin::try_origin(origin).map(|_| ()).or_else(ensure_root)?;
486
487			Claims::<T>::take(&old).map(|c| Claims::<T>::insert(&new, c));
488			Vesting::<T>::take(&old).map(|c| Vesting::<T>::insert(&new, c));
489			Signing::<T>::take(&old).map(|c| Signing::<T>::insert(&new, c));
490			maybe_preclaim.map(|preclaim| {
491				Preclaims::<T>::mutate(&preclaim, |maybe_o| {
492					if maybe_o.as_ref().map_or(false, |o| o == &old) {
493						*maybe_o = Some(new)
494					}
495				})
496			});
497			Ok(Pays::No.into())
498		}
499	}
500
501	#[pezpallet::validate_unsigned]
502	impl<T: Config> ValidateUnsigned for Pezpallet<T> {
503		type Call = Call<T>;
504
505		fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
506			const PRIORITY: u64 = 100;
507
508			let (maybe_signer, maybe_statement) = match call {
509				// <weight>
510				// The weight of this logic is included in the `claim` dispatchable.
511				// </weight>
512				Call::claim { dest: account, ethereum_signature } => {
513					let data = account.using_encoded(to_ascii_hex);
514					(Self::eth_recover(&ethereum_signature, &data, &[][..]), None)
515				},
516				// <weight>
517				// The weight of this logic is included in the `claim_attest` dispatchable.
518				// </weight>
519				Call::claim_attest { dest: account, ethereum_signature, statement } => {
520					let data = account.using_encoded(to_ascii_hex);
521					(
522						Self::eth_recover(&ethereum_signature, &data, &statement),
523						Some(statement.as_slice()),
524					)
525				},
526				_ => return Err(InvalidTransaction::Call.into()),
527			};
528
529			let signer = maybe_signer.ok_or(InvalidTransaction::Custom(
530				ValidityError::InvalidEthereumSignature.into(),
531			))?;
532
533			let e = InvalidTransaction::Custom(ValidityError::SignerHasNoClaim.into());
534			ensure!(Claims::<T>::contains_key(&signer), e);
535
536			let e = InvalidTransaction::Custom(ValidityError::InvalidStatement.into());
537			match Signing::<T>::get(signer) {
538				None => ensure!(maybe_statement.is_none(), e),
539				Some(s) => ensure!(Some(s.to_text()) == maybe_statement, e),
540			}
541
542			Ok(ValidTransaction {
543				priority: PRIORITY,
544				requires: vec![],
545				provides: vec![("claims", signer).encode()],
546				longevity: TransactionLongevity::max_value(),
547				propagate: true,
548			})
549		}
550	}
551}
552
553/// Converts the given binary data into ASCII-encoded hex. It will be twice the length.
554fn to_ascii_hex(data: &[u8]) -> Vec<u8> {
555	let mut r = Vec::with_capacity(data.len() * 2);
556	let mut push_nibble = |n| r.push(if n < 10 { b'0' + n } else { b'a' - 10 + n });
557	for &b in data.iter() {
558		push_nibble(b / 16);
559		push_nibble(b % 16);
560	}
561	r
562}
563
564impl<T: Config> Pezpallet<T> {
565	// Constructs the message that Ethereum RPC's `personal_sign` and `eth_sign` would sign.
566	fn ethereum_signable_message(what: &[u8], extra: &[u8]) -> Vec<u8> {
567		let prefix = T::Prefix::get();
568		let mut l = prefix.len() + what.len() + extra.len();
569		let mut rev = Vec::new();
570		while l > 0 {
571			rev.push(b'0' + (l % 10) as u8);
572			l /= 10;
573		}
574		let mut v = b"\x19Ethereum Signed Message:\n".to_vec();
575		v.extend(rev.into_iter().rev());
576		v.extend_from_slice(prefix);
577		v.extend_from_slice(what);
578		v.extend_from_slice(extra);
579		v
580	}
581
582	// Attempts to recover the Ethereum address from a message signature signed by using
583	// the Ethereum RPC's `personal_sign` and `eth_sign`.
584	fn eth_recover(s: &EcdsaSignature, what: &[u8], extra: &[u8]) -> Option<EthereumAddress> {
585		let msg = keccak_256(&Self::ethereum_signable_message(what, extra));
586		let mut res = EthereumAddress::default();
587		res.0
588			.copy_from_slice(&keccak_256(&secp256k1_ecdsa_recover(&s.0, &msg).ok()?[..])[12..]);
589		Some(res)
590	}
591
592	fn process_claim(signer: EthereumAddress, dest: T::AccountId) -> pezsp_runtime::DispatchResult {
593		let balance_due = Claims::<T>::get(&signer).ok_or(Error::<T>::SignerHasNoClaim)?;
594
595		let new_total =
596			Total::<T>::get().checked_sub(&balance_due).ok_or(Error::<T>::PotUnderflow)?;
597
598		let vesting = Vesting::<T>::get(&signer);
599		if vesting.is_some() && T::VestingSchedule::vesting_balance(&dest).is_some() {
600			return Err(Error::<T>::VestedBalanceExists.into());
601		}
602
603		// We first need to deposit the balance to ensure that the account exists.
604		let _ = CurrencyOf::<T>::deposit_creating(&dest, balance_due);
605
606		// Check if this claim should have a vesting schedule.
607		if let Some(vs) = vesting {
608			// This can only fail if the account already has a vesting schedule,
609			// but this is checked above.
610			T::VestingSchedule::add_vesting_schedule(&dest, vs.0, vs.1, vs.2)
611				.expect("No other vesting schedule exists, as checked above; qed");
612		}
613
614		Total::<T>::put(new_total);
615		Claims::<T>::remove(&signer);
616		Vesting::<T>::remove(&signer);
617		Signing::<T>::remove(&signer);
618
619		// Let's deposit an event to let the outside world know this happened.
620		Self::deposit_event(Event::<T>::Claimed {
621			who: dest,
622			ethereum_address: signer,
623			amount: balance_due,
624		});
625
626		Ok(())
627	}
628}
629
630/// Validate `attest` calls prior to execution. Needed to avoid a DoS attack since they are
631/// otherwise free to place on chain.
632#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)]
633#[scale_info(skip_type_params(T))]
634pub struct PrevalidateAttests<T>(core::marker::PhantomData<fn(T)>);
635
636impl<T: Config> Debug for PrevalidateAttests<T>
637where
638	<T as pezframe_system::Config>::RuntimeCall: IsSubType<Call<T>>,
639{
640	#[cfg(feature = "std")]
641	fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
642		write!(f, "PrevalidateAttests")
643	}
644
645	#[cfg(not(feature = "std"))]
646	fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
647		Ok(())
648	}
649}
650
651impl<T: Config> PrevalidateAttests<T>
652where
653	<T as pezframe_system::Config>::RuntimeCall: IsSubType<Call<T>>,
654{
655	/// Create new `TransactionExtension` to check runtime version.
656	pub fn new() -> Self {
657		Self(core::marker::PhantomData)
658	}
659}
660
661impl<T: Config> TransactionExtension<T::RuntimeCall> for PrevalidateAttests<T>
662where
663	<T as pezframe_system::Config>::RuntimeCall: IsSubType<Call<T>>,
664	<<T as pezframe_system::Config>::RuntimeCall as Dispatchable>::RuntimeOrigin:
665		AsSystemOriginSigner<T::AccountId> + AsTransactionAuthorizedOrigin + Clone,
666{
667	const IDENTIFIER: &'static str = "PrevalidateAttests";
668	type Implicit = ();
669	type Pre = ();
670	type Val = ();
671
672	fn weight(&self, call: &T::RuntimeCall) -> Weight {
673		if let Some(Call::attest { .. }) = call.is_sub_type() {
674			T::WeightInfo::prevalidate_attests()
675		} else {
676			Weight::zero()
677		}
678	}
679
680	fn validate(
681		&self,
682		origin: <T::RuntimeCall as Dispatchable>::RuntimeOrigin,
683		call: &T::RuntimeCall,
684		_info: &DispatchInfoOf<T::RuntimeCall>,
685		_len: usize,
686		_self_implicit: Self::Implicit,
687		_inherited_implication: &impl Encode,
688		_source: TransactionSource,
689	) -> Result<
690		(ValidTransaction, Self::Val, <T::RuntimeCall as Dispatchable>::RuntimeOrigin),
691		TransactionValidityError,
692	> {
693		if let Some(Call::attest { statement: attested_statement }) = call.is_sub_type() {
694			let who = origin.as_system_origin_signer().ok_or(InvalidTransaction::BadSigner)?;
695			let signer = Preclaims::<T>::get(who)
696				.ok_or(InvalidTransaction::Custom(ValidityError::SignerHasNoClaim.into()))?;
697			if let Some(s) = Signing::<T>::get(signer) {
698				let e = InvalidTransaction::Custom(ValidityError::InvalidStatement.into());
699				ensure!(&attested_statement[..] == s.to_text(), e);
700			}
701		}
702		Ok((ValidTransaction::default(), (), origin))
703	}
704
705	impl_tx_ext_default!(T::RuntimeCall; prepare);
706}
707
708#[cfg(any(test, feature = "runtime-benchmarks"))]
709mod secp_utils {
710	use super::*;
711
712	pub fn public(secret: &libsecp256k1::SecretKey) -> libsecp256k1::PublicKey {
713		libsecp256k1::PublicKey::from_secret_key(secret)
714	}
715	pub fn eth(secret: &libsecp256k1::SecretKey) -> EthereumAddress {
716		let mut res = EthereumAddress::default();
717		res.0.copy_from_slice(&keccak_256(&public(secret).serialize()[1..65])[12..]);
718		res
719	}
720	pub fn sig<T: Config>(
721		secret: &libsecp256k1::SecretKey,
722		what: &[u8],
723		extra: &[u8],
724	) -> EcdsaSignature {
725		let msg = keccak_256(&super::Pezpallet::<T>::ethereum_signable_message(
726			&to_ascii_hex(what)[..],
727			extra,
728		));
729		let (sig, recovery_id) = libsecp256k1::sign(&libsecp256k1::Message::parse(&msg), secret);
730		let mut r = [0u8; 65];
731		r[0..64].copy_from_slice(&sig.serialize()[..]);
732		r[64] = recovery_id.serialize();
733		EcdsaSignature(r)
734	}
735}
736
737#[cfg(test)]
738mod mock;
739
740#[cfg(test)]
741mod tests;
742
743#[cfg(feature = "runtime-benchmarks")]
744mod benchmarking;