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}