referrerpolicy=no-referrer-when-downgrade

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