Skip to main content

chains_sdk/ethereum/
permit2.rs

1//! Uniswap Permit2 — Universal token approval and transfer signatures.
2//!
3//! Implements EIP-712 typed data for Uniswap's Permit2 contract, which
4//! provides a universal, gas-efficient token approval system beyond
5//! the basic EIP-2612 permit.
6//!
7//! Supports:
8//! - `PermitSingle` / `PermitBatch` — gasless approval signatures
9//! - `PermitTransferFrom` — one-time signed transfer authorizations
10//! - `SignatureTransfer` — witness-extended transfers
11//!
12//! # Example
13//! ```no_run
14//! use chains_sdk::ethereum::permit2::*;
15//!
16//! let permit = PermitSingle {
17//!     token: [0xAA; 20],
18//!     amount: uint160_from_u128(1_000_000),
19//!     expiration: 1_700_000_000,
20//!     nonce: 0,
21//!     spender: [0xBB; 20],
22//!     sig_deadline: uint256_from_u64(1_700_000_000),
23//! };
24//! let hash = permit.struct_hash().unwrap();
25//! ```
26
27use crate::error::SignerError;
28use crate::ethereum::keccak256;
29
30/// Uniswap Permit2 contract address (same on all chains).
31pub const PERMIT2_ADDRESS: [u8; 20] = [
32    0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0xd4, 0x73, 0x03, 0x0f, 0x11, 0x6d, 0xde, 0xe9, 0xf6, 0xb4,
33    0x3a, 0xc7, 0x8b, 0xa3,
34];
35
36/// Maximum valid value for a uint48 field.
37pub const MAX_U48: u64 = (1u64 << 48) - 1;
38
39/// A raw uint160 value encoded as 20-byte big-endian.
40pub type Uint160 = [u8; 20];
41/// A raw uint256 value encoded as 32-byte big-endian.
42pub type Uint256 = [u8; 32];
43
44// ═══════════════════════════════════════════════════════════════════
45// Type Hashes
46// ═══════════════════════════════════════════════════════════════════
47
48/// `keccak256("TokenPermissions(address token,uint256 amount)")`
49fn token_permissions_typehash() -> [u8; 32] {
50    keccak256(b"TokenPermissions(address token,uint256 amount)")
51}
52
53/// `keccak256("PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)")`
54fn permit_details_typehash() -> [u8; 32] {
55    keccak256(b"PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)")
56}
57
58/// `keccak256("PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)")`
59fn permit_single_typehash() -> [u8; 32] {
60    keccak256(b"PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)")
61}
62
63/// `keccak256("PermitBatch(PermitDetails[] details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)")`
64fn permit_batch_typehash() -> [u8; 32] {
65    keccak256(b"PermitBatch(PermitDetails[] details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)")
66}
67
68/// `keccak256("PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)")`
69fn permit_transfer_from_typehash() -> [u8; 32] {
70    keccak256(b"PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)")
71}
72
73/// `keccak256("PermitBatchTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)")`
74fn permit_batch_transfer_from_typehash() -> [u8; 32] {
75    keccak256(b"PermitBatchTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)")
76}
77
78// ═══════════════════════════════════════════════════════════════════
79// PermitSingle (Allowance-based)
80// ═══════════════════════════════════════════════════════════════════
81
82/// A single-token allowance permit.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct PermitSingle {
85    /// Token address to approve.
86    pub token: [u8; 20],
87    /// Approval amount (`uint160`) encoded as 20-byte big-endian.
88    pub amount: Uint160,
89    /// Approval expiration timestamp (`uint48`).
90    pub expiration: u64,
91    /// Per-token nonce for replay protection (`uint48`).
92    pub nonce: u64,
93    /// Address being granted the allowance.
94    pub spender: [u8; 20],
95    /// Signature deadline (`uint256`).
96    pub sig_deadline: Uint256,
97}
98
99impl PermitSingle {
100    /// Compute the PermitDetails struct hash.
101    fn details_hash(&self) -> Result<[u8; 32], SignerError> {
102        let mut data = Vec::with_capacity(160);
103        data.extend_from_slice(&permit_details_typehash());
104        data.extend_from_slice(&pad_address(&self.token));
105        data.extend_from_slice(&pad_uint160(&self.amount));
106        data.extend_from_slice(&pad_u48(self.expiration, "expiration")?);
107        data.extend_from_slice(&pad_u48(self.nonce, "nonce")?);
108        Ok(keccak256(&data))
109    }
110
111    /// Compute the EIP-712 struct hash for this permit.
112    pub fn struct_hash(&self) -> Result<[u8; 32], SignerError> {
113        let mut data = Vec::with_capacity(128);
114        data.extend_from_slice(&permit_single_typehash());
115        data.extend_from_slice(&self.details_hash()?);
116        data.extend_from_slice(&pad_address(&self.spender));
117        data.extend_from_slice(&self.sig_deadline);
118        Ok(keccak256(&data))
119    }
120
121    /// Compute the full EIP-712 signing hash.
122    ///
123    /// `keccak256("\x19\x01" || domainSeparator || structHash)`
124    pub fn signing_hash(&self, domain_separator: &[u8; 32]) -> Result<[u8; 32], SignerError> {
125        Ok(eip712_hash(domain_separator, &self.struct_hash()?))
126    }
127}
128
129// ═══════════════════════════════════════════════════════════════════
130// PermitBatch (Allowance-based, multiple tokens)
131// ═══════════════════════════════════════════════════════════════════
132
133/// Details for one token in a batch permit.
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct PermitDetails {
136    /// Token address.
137    pub token: [u8; 20],
138    /// Approval amount (`uint160`) encoded as 20-byte big-endian.
139    pub amount: Uint160,
140    /// Expiration timestamp (`uint48`).
141    pub expiration: u64,
142    /// Per-token nonce (`uint48`).
143    pub nonce: u64,
144}
145
146/// A multi-token allowance permit.
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct PermitBatch {
149    /// Token approval details.
150    pub details: Vec<PermitDetails>,
151    /// Address being granted the allowance.
152    pub spender: [u8; 20],
153    /// Signature deadline (`uint256`).
154    pub sig_deadline: Uint256,
155}
156
157impl PermitBatch {
158    /// Compute the struct hash.
159    pub fn struct_hash(&self) -> Result<[u8; 32], SignerError> {
160        let mut details_hashes = Vec::with_capacity(self.details.len() * 32);
161        for d in &self.details {
162            let mut h = Vec::with_capacity(160);
163            h.extend_from_slice(&permit_details_typehash());
164            h.extend_from_slice(&pad_address(&d.token));
165            h.extend_from_slice(&pad_uint160(&d.amount));
166            h.extend_from_slice(&pad_u48(d.expiration, "expiration")?);
167            h.extend_from_slice(&pad_u48(d.nonce, "nonce")?);
168            details_hashes.extend_from_slice(&keccak256(&h));
169        }
170        let details_array_hash = keccak256(&details_hashes);
171
172        let mut data = Vec::with_capacity(128);
173        data.extend_from_slice(&permit_batch_typehash());
174        data.extend_from_slice(&details_array_hash);
175        data.extend_from_slice(&pad_address(&self.spender));
176        data.extend_from_slice(&self.sig_deadline);
177        Ok(keccak256(&data))
178    }
179
180    /// Compute the full EIP-712 signing hash.
181    pub fn signing_hash(&self, domain_separator: &[u8; 32]) -> Result<[u8; 32], SignerError> {
182        Ok(eip712_hash(domain_separator, &self.struct_hash()?))
183    }
184}
185
186// ═══════════════════════════════════════════════════════════════════
187// PermitTransferFrom (Signature-based transfers)
188// ═══════════════════════════════════════════════════════════════════
189
190/// A single-token signature transfer permit.
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct PermitTransferFrom {
193    /// Token address.
194    pub token: [u8; 20],
195    /// Maximum transfer amount (`uint256`).
196    pub amount: Uint256,
197    /// Unique nonce (`uint256`, unordered nonce bitmap model).
198    pub nonce: Uint256,
199    /// Signature deadline (`uint256`).
200    pub deadline: Uint256,
201    /// Address allowed to execute the transfer.
202    pub spender: [u8; 20],
203}
204
205impl PermitTransferFrom {
206    /// Compute the TokenPermissions struct hash.
207    fn token_permissions_hash(&self) -> [u8; 32] {
208        let mut data = Vec::with_capacity(96);
209        data.extend_from_slice(&token_permissions_typehash());
210        data.extend_from_slice(&pad_address(&self.token));
211        data.extend_from_slice(&self.amount);
212        keccak256(&data)
213    }
214
215    /// Compute the EIP-712 struct hash.
216    #[must_use]
217    pub fn struct_hash(&self) -> [u8; 32] {
218        let mut data = Vec::with_capacity(160);
219        data.extend_from_slice(&permit_transfer_from_typehash());
220        data.extend_from_slice(&self.token_permissions_hash());
221        data.extend_from_slice(&pad_address(&self.spender));
222        data.extend_from_slice(&self.nonce);
223        data.extend_from_slice(&self.deadline);
224        keccak256(&data)
225    }
226
227    /// Compute the full EIP-712 signing hash.
228    #[must_use]
229    pub fn signing_hash(&self, domain_separator: &[u8; 32]) -> [u8; 32] {
230        eip712_hash(domain_separator, &self.struct_hash())
231    }
232}
233
234/// A batch signature transfer permit.
235#[derive(Debug, Clone, PartialEq, Eq)]
236pub struct PermitBatchTransferFrom {
237    /// Permitted tokens and amounts.
238    pub permitted: Vec<TokenPermissions>,
239    /// Unique nonce (`uint256`).
240    pub nonce: Uint256,
241    /// Signature deadline (`uint256`).
242    pub deadline: Uint256,
243    /// Address allowed to execute the transfer.
244    pub spender: [u8; 20],
245}
246
247/// Token and amount pair for batch transfers.
248#[derive(Debug, Clone, PartialEq, Eq)]
249pub struct TokenPermissions {
250    /// Token address.
251    pub token: [u8; 20],
252    /// Transfer amount (`uint256`).
253    pub amount: Uint256,
254}
255
256impl PermitBatchTransferFrom {
257    /// Compute the EIP-712 struct hash.
258    #[must_use]
259    pub fn struct_hash(&self) -> [u8; 32] {
260        let mut perms_hashes = Vec::with_capacity(self.permitted.len() * 32);
261        for p in &self.permitted {
262            let mut h = Vec::with_capacity(96);
263            h.extend_from_slice(&token_permissions_typehash());
264            h.extend_from_slice(&pad_address(&p.token));
265            h.extend_from_slice(&p.amount);
266            perms_hashes.extend_from_slice(&keccak256(&h));
267        }
268        let perms_array_hash = keccak256(&perms_hashes);
269
270        let mut data = Vec::with_capacity(160);
271        data.extend_from_slice(&permit_batch_transfer_from_typehash());
272        data.extend_from_slice(&perms_array_hash);
273        data.extend_from_slice(&pad_address(&self.spender));
274        data.extend_from_slice(&self.nonce);
275        data.extend_from_slice(&self.deadline);
276        keccak256(&data)
277    }
278
279    /// Compute the full EIP-712 signing hash.
280    #[must_use]
281    pub fn signing_hash(&self, domain_separator: &[u8; 32]) -> [u8; 32] {
282        eip712_hash(domain_separator, &self.struct_hash())
283    }
284}
285
286// ═══════════════════════════════════════════════════════════════════
287// Domain Separator
288// ═══════════════════════════════════════════════════════════════════
289
290/// Compute the Permit2 EIP-712 domain separator.
291///
292/// `keccak256(abi.encode(EIP712_DOMAIN_TYPEHASH, name_hash, chain_id, permit2_address))`
293#[must_use]
294pub fn permit2_domain_separator(chain_id: Uint256) -> [u8; 32] {
295    let type_hash =
296        keccak256(b"EIP712Domain(string name,uint256 chainId,address verifyingContract)");
297    let name_hash = keccak256(b"Permit2");
298
299    let mut data = Vec::with_capacity(128);
300    data.extend_from_slice(&type_hash);
301    data.extend_from_slice(&name_hash);
302    data.extend_from_slice(&chain_id);
303    data.extend_from_slice(&pad_address(&PERMIT2_ADDRESS));
304    keccak256(&data)
305}
306
307// ═══════════════════════════════════════════════════════════════════
308// ABI Encoding for Permit2 contract calls
309// ═══════════════════════════════════════════════════════════════════
310
311/// ABI-encode `permit(address owner, PermitSingle permitSingle, bytes signature)`.
312///
313/// Function selector: `permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)`
314pub fn encode_permit_single_call(
315    owner: &[u8; 20],
316    permit: &PermitSingle,
317    signature: &[u8],
318) -> Result<Vec<u8>, SignerError> {
319    use crate::ethereum::abi::{AbiValue, Function};
320
321    let func =
322        Function::new("permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)");
323    Ok(func.encode(&[
324        AbiValue::Address(*owner),
325        AbiValue::Tuple(vec![
326            AbiValue::Tuple(vec![
327                AbiValue::Address(permit.token),
328                AbiValue::Uint256(pad_uint160(&permit.amount)),
329                AbiValue::Uint256(pad_u48(permit.expiration, "expiration")?),
330                AbiValue::Uint256(pad_u48(permit.nonce, "nonce")?),
331            ]),
332            AbiValue::Address(permit.spender),
333            AbiValue::Uint256(permit.sig_deadline),
334        ]),
335        AbiValue::Bytes(signature.to_vec()),
336    ]))
337}
338
339/// ABI-encode `transferFrom(address from, address to, uint160 amount, address token)`.
340#[must_use]
341pub fn encode_transfer_from(
342    from: &[u8; 20],
343    to: &[u8; 20],
344    amount: Uint160,
345    token: &[u8; 20],
346) -> Vec<u8> {
347    use crate::ethereum::abi::{AbiValue, Function};
348    let func = Function::new("transferFrom(address,address,uint160,address)");
349    func.encode(&[
350        AbiValue::Address(*from),
351        AbiValue::Address(*to),
352        AbiValue::Uint256(pad_uint160(&amount)),
353        AbiValue::Address(*token),
354    ])
355}
356
357// ═══════════════════════════════════════════════════════════════════
358// Helpers
359// ═══════════════════════════════════════════════════════════════════
360
361/// Convert a u128 value to a canonical uint160 (20-byte) representation.
362#[must_use]
363pub fn uint160_from_u128(value: u128) -> Uint160 {
364    let mut out = [0u8; 20];
365    out[4..].copy_from_slice(&value.to_be_bytes());
366    out
367}
368
369/// Convert a u64 value to a canonical uint256 (32-byte) representation.
370#[must_use]
371pub fn uint256_from_u64(value: u64) -> Uint256 {
372    let mut out = [0u8; 32];
373    out[24..].copy_from_slice(&value.to_be_bytes());
374    out
375}
376
377fn pad_address(addr: &[u8; 20]) -> [u8; 32] {
378    let mut buf = [0u8; 32];
379    buf[12..32].copy_from_slice(addr);
380    buf
381}
382
383fn pad_uint160(val: &Uint160) -> [u8; 32] {
384    let mut buf = [0u8; 32];
385    buf[12..32].copy_from_slice(val);
386    buf
387}
388
389fn pad_u48(val: u64, field: &str) -> Result<[u8; 32], SignerError> {
390    if val > MAX_U48 {
391        return Err(SignerError::ParseError(format!(
392            "Permit2 {field} exceeds uint48 range"
393        )));
394    }
395    Ok(uint256_from_u64(val))
396}
397
398fn eip712_hash(domain_separator: &[u8; 32], struct_hash: &[u8; 32]) -> [u8; 32] {
399    let mut data = Vec::with_capacity(66);
400    data.push(0x19);
401    data.push(0x01);
402    data.extend_from_slice(domain_separator);
403    data.extend_from_slice(struct_hash);
404    keccak256(&data)
405}
406
407// ═══════════════════════════════════════════════════════════════════
408// Tests
409// ═══════════════════════════════════════════════════════════════════
410
411#[cfg(test)]
412#[allow(clippy::unwrap_used, clippy::expect_used)]
413mod tests {
414    use super::*;
415    use crate::ethereum::abi;
416
417    const TOKEN_A: [u8; 20] = [0xAA; 20];
418    const TOKEN_B: [u8; 20] = [0xBB; 20];
419    const SPENDER: [u8; 20] = [0xCC; 20];
420    const OWNER: [u8; 20] = [0xDD; 20];
421    const DEADLINE: u64 = 1_700_000_000;
422
423    fn amount160(v: u128) -> Uint160 {
424        uint160_from_u128(v)
425    }
426
427    fn amount256(v: u64) -> Uint256 {
428        uint256_from_u64(v)
429    }
430
431    #[test]
432    fn test_permit_single_struct_hash_deterministic() {
433        let p = PermitSingle {
434            token: TOKEN_A,
435            amount: amount160(1000),
436            expiration: DEADLINE,
437            nonce: 0,
438            spender: SPENDER,
439            sig_deadline: amount256(DEADLINE),
440        };
441        assert_eq!(p.struct_hash().unwrap(), p.struct_hash().unwrap());
442    }
443
444    #[test]
445    fn test_permit_single_rejects_u48_overflow() {
446        let p = PermitSingle {
447            token: TOKEN_A,
448            amount: amount160(1000),
449            expiration: MAX_U48 + 1,
450            nonce: 0,
451            spender: SPENDER,
452            sig_deadline: amount256(DEADLINE),
453        };
454        assert!(p.struct_hash().is_err());
455    }
456
457    #[test]
458    fn test_permit_single_different_amounts() {
459        let p1 = PermitSingle {
460            token: TOKEN_A,
461            amount: amount160(1000),
462            expiration: DEADLINE,
463            nonce: 0,
464            spender: SPENDER,
465            sig_deadline: amount256(DEADLINE),
466        };
467        let p2 = PermitSingle {
468            token: TOKEN_A,
469            amount: amount160(2000),
470            expiration: DEADLINE,
471            nonce: 0,
472            spender: SPENDER,
473            sig_deadline: amount256(DEADLINE),
474        };
475        assert_ne!(p1.struct_hash().unwrap(), p2.struct_hash().unwrap());
476    }
477
478    #[test]
479    fn test_permit_single_signing_hash() {
480        let p = PermitSingle {
481            token: TOKEN_A,
482            amount: amount160(1000),
483            expiration: DEADLINE,
484            nonce: 0,
485            spender: SPENDER,
486            sig_deadline: amount256(DEADLINE),
487        };
488        let ds = permit2_domain_separator(amount256(1));
489        let hash = p.signing_hash(&ds).unwrap();
490        assert_ne!(hash, [0u8; 32]);
491    }
492
493    #[test]
494    fn test_permit_batch_struct_hash() {
495        let p = PermitBatch {
496            details: vec![
497                PermitDetails {
498                    token: TOKEN_A,
499                    amount: amount160(100),
500                    expiration: DEADLINE,
501                    nonce: 0,
502                },
503                PermitDetails {
504                    token: TOKEN_B,
505                    amount: amount160(200),
506                    expiration: DEADLINE,
507                    nonce: 1,
508                },
509            ],
510            spender: SPENDER,
511            sig_deadline: amount256(DEADLINE),
512        };
513        assert_ne!(p.struct_hash().unwrap(), [0u8; 32]);
514    }
515
516    #[test]
517    fn test_permit_transfer_struct_hash() {
518        let p = PermitTransferFrom {
519            token: TOKEN_A,
520            amount: amount256(5000),
521            nonce: amount256(42),
522            deadline: amount256(DEADLINE),
523            spender: SPENDER,
524        };
525        assert_ne!(p.struct_hash(), [0u8; 32]);
526    }
527
528    #[test]
529    fn test_permit_batch_transfer_struct_hash() {
530        let p = PermitBatchTransferFrom {
531            permitted: vec![
532                TokenPermissions {
533                    token: TOKEN_A,
534                    amount: amount256(100),
535                },
536                TokenPermissions {
537                    token: TOKEN_B,
538                    amount: amount256(200),
539                },
540            ],
541            nonce: amount256(0),
542            deadline: amount256(DEADLINE),
543            spender: SPENDER,
544        };
545        assert_ne!(p.struct_hash(), [0u8; 32]);
546    }
547
548    #[test]
549    fn test_domain_separator_different_chains() {
550        assert_ne!(
551            permit2_domain_separator(amount256(1)),
552            permit2_domain_separator(amount256(137))
553        );
554    }
555
556    #[test]
557    fn test_encode_permit_single_call_selector() {
558        let p = PermitSingle {
559            token: TOKEN_A,
560            amount: amount160(1000),
561            expiration: DEADLINE,
562            nonce: 0,
563            spender: SPENDER,
564            sig_deadline: amount256(DEADLINE),
565        };
566        let data = encode_permit_single_call(&OWNER, &p, &[0xAA; 65]).unwrap();
567        assert!(data.len() > 4);
568    }
569
570    #[test]
571    fn test_encode_transfer_from_selector() {
572        let data = encode_transfer_from(&OWNER, &SPENDER, amount160(1000), &TOKEN_A);
573        let expected = abi::function_selector("transferFrom(address,address,uint160,address)");
574        assert_eq!(&data[..4], &expected);
575    }
576
577    #[test]
578    fn test_pad_address() {
579        let addr = [0xAA; 20];
580        let padded = pad_address(&addr);
581        assert!(padded[..12].iter().all(|b| *b == 0));
582        assert_eq!(&padded[12..], &addr);
583    }
584
585    #[test]
586    fn test_pad_u48() {
587        let padded = pad_u48(42, "test").unwrap();
588        assert_eq!(padded[31], 42);
589        assert!(padded[..24].iter().all(|b| *b == 0));
590    }
591
592    #[test]
593    fn test_permit2_address_hex() {
594        let hex = PERMIT2_ADDRESS
595            .iter()
596            .map(|b| format!("{b:02x}"))
597            .collect::<String>();
598        assert_eq!(hex, "000000000022d473030f116ddee9f6b43ac78ba3");
599    }
600}