referrerpolicy=no-referrer-when-downgrade

pallet_assets_precompiles/
permit.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! ERC20Permit pallet for signature-based approvals (EIP-2612).
19//!
20//! This pallet stores permit-related state (nonces) and provides EIP-712
21//! signature verification for gasless approvals.
22//!
23//! # Security Notes
24//!
25//! - **Nonce management**: Use `use_permit` (not `verify_permit`) to atomically verify and consume
26//!   permits. This prevents replay attacks.
27//! - **Deadline validation**: Permits are validated against UNIX timestamps.
28//! - **Domain separation**: Each verifying contract has its own domain separator.
29//! - **Signature malleability**: The `s` value is checked to be in the lower half of the secp256k1
30//!   curve order to prevent signature malleability attacks.
31
32use alloc::vec::Vec;
33use frame_support::{pallet_prelude::*, traits::UnixTime};
34use pallet_revive::precompiles::H160;
35use sp_core::{H256, U256};
36use sp_io::{crypto::secp256k1_ecdsa_recover, hashing::keccak_256};
37
38pub use pallet::*;
39
40/// EIP-712 type hash for the domain separator.
41/// keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
42pub(crate) const DOMAIN_TYPEHASH: [u8; 32] = const_crypto::sha3::Keccak256::new()
43	.update(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
44	.finalize();
45
46/// EIP-712 type hash for Permit.
47/// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
48///
49/// Computed at compile time from the canonical string, eliminating any risk of a
50/// copy-paste error in a hand-written byte array.
51pub(crate) const PERMIT_TYPEHASH: [u8; 32] = const_crypto::sha3::Keccak256::new()
52	.update(b"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
53	.finalize();
54
55/// Half of the secp256k1 curve order (n/2).
56/// Used to ensure `s` is in the lower half to prevent signature malleability.
57/// n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
58/// n/2 = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0
59///
60/// TODO: Replace usages with `sp_core::ecdsa::is_signature_normalized` once
61/// paritytech/polkadot-sdk#5841 lands.
62pub(crate) const SECP256K1_N_DIV_2: [u8; 32] = [
63	0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
64	0x5D, 0x57, 0x6E, 0x73, 0x57, 0xA4, 0x50, 0x1D, 0xDF, 0xE9, 0x2F, 0x46, 0x68, 0x1B, 0x20, 0xA0,
65];
66
67/// Encoded length constants for EIP-712 encoding.
68/// Domain separator: typehash(32) + name_hash(32) + version_hash(32) + chainId(32) +
69/// verifyingContract(32) = 160 bytes
70pub(crate) const DOMAIN_SEPARATOR_ENCODED_LEN: usize = 32 * 5;
71/// Permit struct: typehash(32) + owner(32) + spender(32) + value(32) + nonce(32) + deadline(32) =
72/// 192 bytes
73pub(crate) const PERMIT_STRUCT_ENCODED_LEN: usize = 32 * 6;
74/// Digest prefix: \x19\x01(2) + domain_separator(32) + struct_hash(32) = 66 bytes
75pub(crate) const DIGEST_PREFIX_LEN: usize = 2 + 32 + 32;
76
77#[frame_support::pallet]
78pub mod pallet {
79	use super::*;
80
81	#[pallet::config]
82	pub trait Config: frame_system::Config + pallet_timestamp::Config {
83		/// The chain ID used in EIP-712 domain separator.
84		#[pallet::constant]
85		type ChainId: Get<u64>;
86
87		/// Weight information for permit operations.
88		type WeightInfo: crate::weights::WeightInfo;
89	}
90
91	#[pallet::pallet]
92	pub struct Pallet<T>(_);
93
94	/// Nonces for permit signatures.
95	/// Mapping: (verifying_contract, owner_address) => nonce
96	///
97	/// Uses Blake2_128Concat for the first key to prevent storage collision attacks
98	/// when the verifying_contract address could be influenced by an attacker.
99	///
100	/// Note: EIP-2612 specifies uint256 nonce. We store as U256 for compatibility.
101	#[pallet::storage]
102	pub type Nonces<T: Config> = StorageDoubleMap<
103		_,
104		Blake2_128Concat,
105		H160, // verifying contract address (precompile address)
106		Blake2_128Concat,
107		H160, // owner ethereum address
108		U256, // nonce (EIP-2612 uses uint256)
109		ValueQuery,
110	>;
111
112	/// Error types for the permit pallet.
113	#[pallet::error]
114	pub enum Error<T> {
115		/// The permit signature is invalid.
116		InvalidSignature,
117		/// The signer does not match the owner.
118		SignerMismatch,
119		/// The permit has expired (deadline passed).
120		PermitExpired,
121		/// The signature's `s` value is too high (malleability protection).
122		SignatureSValueTooHigh,
123		/// The signature's `v` value is invalid.
124		InvalidVValue,
125		/// Nonce overflow - account has used too many permits.
126		NonceOverflow,
127		/// The owner address is invalid (e.g., zero address).
128		InvalidOwner,
129		/// The spender address is invalid (e.g., zero address).
130		InvalidSpender,
131	}
132
133	impl<T: Config> Pallet<T> {
134		/// Get the current nonce for an owner on a specific verifying contract.
135		pub fn nonce(verifying_contract: &H160, owner: &H160) -> U256 {
136			Nonces::<T>::get(verifying_contract, owner)
137		}
138
139		/// Increment the nonce for an owner on a specific verifying contract.
140		/// Returns the new nonce value, or an error if overflow would occur.
141		pub fn increment_nonce(verifying_contract: &H160, owner: &H160) -> Result<U256, Error<T>> {
142			Nonces::<T>::try_mutate(verifying_contract, owner, |nonce| {
143				*nonce = nonce.checked_add(U256::one()).ok_or(Error::<T>::NonceOverflow)?;
144				Ok(*nonce)
145			})
146		}
147
148		/// Compute the EIP-712 domain separator for a given verifying contract.
149		///
150		/// DOMAIN_SEPARATOR = keccak256(abi.encode(
151		///   keccak256("EIP712Domain(string name,string version,uint256 chainId,address
152		/// verifyingContract)"),
153		///   keccak256(name),
154		///   keccak256("1"),
155		///   chainId,
156		///   verifyingContract
157		/// ))
158		///
159		/// The `name` parameter should be the token name per EIP-2612 specification.
160		pub fn compute_domain_separator(verifying_contract: &H160, name: &[u8]) -> H256 {
161			let name_hash = keccak_256(name);
162			let version_hash = keccak_256(b"1");
163			let chain_id = T::ChainId::get();
164
165			// Encode: typehash || name_hash || version_hash || chainId || verifyingContract
166			let mut data = Vec::with_capacity(DOMAIN_SEPARATOR_ENCODED_LEN);
167			data.extend_from_slice(&DOMAIN_TYPEHASH);
168			data.extend_from_slice(&name_hash);
169			data.extend_from_slice(&version_hash);
170			// Pad chain_id to 32 bytes (big-endian)
171			data.extend_from_slice(&[0u8; 24]);
172			data.extend_from_slice(&chain_id.to_be_bytes());
173			// Pad address to 32 bytes
174			data.extend_from_slice(&[0u8; 12]);
175			data.extend_from_slice(verifying_contract.as_bytes());
176
177			H256(keccak_256(&data))
178		}
179
180		/// Compute the EIP-712 struct hash for a permit.
181		///
182		/// structHash = keccak256(abi.encode(
183		///   PERMIT_TYPEHASH,
184		///   owner,
185		///   spender,
186		///   value,
187		///   nonce,
188		///   deadline
189		/// ))
190		pub fn permit_struct_hash(
191			owner: &H160,
192			spender: &H160,
193			value: &[u8; 32], // U256 as bytes (big-endian)
194			nonce: &U256,
195			deadline: &[u8; 32], // U256 as bytes (big-endian)
196		) -> H256 {
197			let mut data = Vec::with_capacity(PERMIT_STRUCT_ENCODED_LEN);
198			data.extend_from_slice(&PERMIT_TYPEHASH);
199			// owner (padded to 32 bytes)
200			data.extend_from_slice(&[0u8; 12]);
201			data.extend_from_slice(owner.as_bytes());
202			// spender (padded to 32 bytes)
203			data.extend_from_slice(&[0u8; 12]);
204			data.extend_from_slice(spender.as_bytes());
205			// value (already 32 bytes)
206			data.extend_from_slice(value);
207			// nonce (convert U256 to 32 bytes big-endian)
208			data.extend_from_slice(&nonce.to_big_endian());
209			// deadline (already 32 bytes)
210			data.extend_from_slice(deadline);
211
212			H256(keccak_256(&data))
213		}
214
215		/// Compute the final EIP-712 digest to be signed.
216		///
217		/// digest = keccak256("\x19\x01" || domainSeparator || structHash)
218		///
219		/// The `name` parameter should be the token name per EIP-2612 specification.
220		pub fn permit_digest(
221			verifying_contract: &H160,
222			name: &[u8],
223			owner: &H160,
224			spender: &H160,
225			value: &[u8; 32],
226			nonce: &U256,
227			deadline: &[u8; 32],
228		) -> [u8; 32] {
229			let domain_separator = Self::compute_domain_separator(verifying_contract, name);
230			let struct_hash = Self::permit_struct_hash(owner, spender, value, nonce, deadline);
231
232			let mut data = Vec::with_capacity(DIGEST_PREFIX_LEN);
233			data.extend_from_slice(&[0x19, 0x01]);
234			data.extend_from_slice(domain_separator.as_bytes());
235			data.extend_from_slice(struct_hash.as_bytes());
236
237			keccak_256(&data)
238		}
239
240		/// Check if the signature's `s` value is in the lower half of the curve order.
241		///
242		/// This prevents signature malleability attacks where an attacker can
243		/// create a second valid signature by flipping `s` to `n - s`.
244		///
245		/// TODO: Replace with `sp_core::ecdsa::is_signature_normalized` once
246		/// paritytech/polkadot-sdk#5841 lands.
247		fn is_s_value_valid(s: &[u8; 32]) -> bool {
248			for i in 0..32 {
249				if s[i] < SECP256K1_N_DIV_2[i] {
250					return true;
251				}
252				if s[i] > SECP256K1_N_DIV_2[i] {
253					return false;
254				}
255			}
256			// s == SECP256K1_N_DIV_2, which is valid
257			true
258		}
259
260		/// Recover the signer address from an ECDSA signature.
261		///
262		/// Returns `Ok(address)` if the signature is valid, `Err` otherwise.
263		///
264		/// This function also validates that the `s` value is in the lower half
265		/// of the curve order to prevent signature malleability.
266		pub fn ecrecover(
267			digest: &[u8; 32],
268			v: u8,
269			r: &[u8; 32],
270			s: &[u8; 32],
271		) -> Result<H160, Error<T>> {
272			// Check signature malleability: s must be in lower half of curve order
273			if !Self::is_s_value_valid(s) {
274				return Err(Error::<T>::SignatureSValueTooHigh);
275			}
276
277			// Convert v to recovery_id (Ethereum v is 27 or 28, recovery_id is 0 or 1)
278			let recovery_id = v.checked_sub(27).ok_or(Error::<T>::InvalidVValue)?;
279			if recovery_id > 1 {
280				return Err(Error::<T>::InvalidVValue);
281			}
282
283			// Build signature in format expected by secp256k1_ecdsa_recover: [r, s, recovery_id]
284			let mut sig = [0u8; 65];
285			sig[0..32].copy_from_slice(r);
286			sig[32..64].copy_from_slice(s);
287			sig[64] = recovery_id;
288
289			// Recover uncompressed public key (64 bytes)
290			let pubkey =
291				secp256k1_ecdsa_recover(&sig, digest).map_err(|_| Error::<T>::InvalidSignature)?;
292
293			// Convert public key to Ethereum address: keccak256(pubkey)[12..]
294			let hash = keccak_256(&pubkey);
295			let mut address = H160::zero();
296			address.0.copy_from_slice(&hash[12..]);
297
298			Ok(address)
299		}
300
301		/// Verify a permit signature without consuming it.
302		///
303		/// **WARNING**: This function does NOT increment the nonce. Using this
304		/// function alone will leave the permit vulnerable to replay attacks.
305		/// Use `use_permit` instead for production code.
306		///
307		/// This function is provided for cases where you need to verify a permit
308		/// in a read-only context or need to separate verification from consumption.
309		///
310		/// The `name` parameter should be the token name per EIP-2612 specification.
311		fn do_verify_permit(
312			verifying_contract: &H160,
313			name: &[u8],
314			owner: &H160,
315			spender: &H160,
316			value: &[u8; 32],
317			deadline: &[u8; 32],
318			v: u8,
319			r: &[u8; 32],
320			s: &[u8; 32],
321		) -> Result<(), Error<T>> {
322			// EIP-2612: owner and spender cannot be the zero address
323			if owner.is_zero() {
324				return Err(Error::<T>::InvalidOwner);
325			}
326			if spender.is_zero() {
327				return Err(Error::<T>::InvalidSpender);
328			}
329
330			// Validate deadline against current timestamp.
331			// EIP-2612 specifies deadlines in UNIX seconds. We use the `UnixTime`
332			// trait which returns a `core::time::Duration` — its `as_secs()` method
333			// gives us seconds regardless of pallet_timestamp's internal resolution
334			// (which stores milliseconds, converted via `Duration::from_millis` in
335			// pallet_timestamp's `UnixTime` implementation).
336			let now_seconds = <pallet_timestamp::Pallet<T> as UnixTime>::now().as_secs();
337			let deadline_u256 = U256::from_big_endian(deadline);
338			let now_u256 = U256::from(now_seconds);
339
340			if deadline_u256 < now_u256 {
341				return Err(Error::<T>::PermitExpired);
342			}
343
344			let nonce = Self::nonce(verifying_contract, owner);
345			let digest = Self::permit_digest(
346				verifying_contract,
347				name,
348				owner,
349				spender,
350				value,
351				&nonce,
352				deadline,
353			);
354
355			let recovered = Self::ecrecover(&digest, v, r, s)?;
356
357			if &recovered != owner {
358				return Err(Error::<T>::SignerMismatch);
359			}
360
361			Ok(())
362		}
363
364		/// Verify and consume a permit signature atomically.
365		///
366		/// This is the recommended function for production use. It:
367		/// 1. Validates the deadline against the current timestamp
368		/// 2. Verifies the signature matches the owner
369		/// 3. Increments the nonce to prevent replay attacks
370		///
371		/// The `name` parameter should be the token name per EIP-2612 specification.
372		///
373		/// After this function returns `Ok(())`, the permit cannot be used again.
374		pub fn use_permit(
375			verifying_contract: &H160,
376			name: &[u8],
377			owner: &H160,
378			spender: &H160,
379			value: &[u8; 32],
380			deadline: &[u8; 32],
381			v: u8,
382			r: &[u8; 32],
383			s: &[u8; 32],
384		) -> Result<(), Error<T>> {
385			// Verify the permit first
386			Self::do_verify_permit(
387				verifying_contract,
388				name,
389				owner,
390				spender,
391				value,
392				deadline,
393				v,
394				r,
395				s,
396			)?;
397
398			// Consume the permit by incrementing the nonce
399			// This prevents the same permit from being used again
400			Self::increment_nonce(verifying_contract, owner)?;
401
402			Ok(())
403		}
404	}
405
406	#[cfg(test)]
407	impl<T: Config> Pallet<T> {
408		/// Test-only entry point that exposes [`do_verify_permit`] without consuming the nonce.
409		///
410		/// Use this in unit tests to exercise signature verification in isolation.
411		/// Production callers must use [`use_permit`], which atomically verifies and
412		/// increments the nonce to prevent replay attacks.
413		pub fn verify_permit(
414			verifying_contract: &H160,
415			name: &[u8],
416			owner: &H160,
417			spender: &H160,
418			value: &[u8; 32],
419			deadline: &[u8; 32],
420			v: u8,
421			r: &[u8; 32],
422			s: &[u8; 32],
423		) -> Result<(), Error<T>> {
424			Self::do_verify_permit(
425				verifying_contract,
426				name,
427				owner,
428				spender,
429				value,
430				deadline,
431				v,
432				r,
433				s,
434			)
435		}
436	}
437}