polkadot_runtime_common/claims/
mod.rs

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