//! Permit2 — Uniswap's universal allowance / transfer primitive. //! //! Permit2 (deployed at the same address on every supported chain, //! `0x000000000022d473030f116ddee9f6b43ac78ba3`) is a wrapper that //! lets users grant time-bounded, batch-able token approvals to //! relayers without paying gas. The two main entry points: //! //! - `permitTransferFrom(...)` — single token, single recipient. //! - `permitWitnessTransferFrom(...)` — single token + witness data //! bound to a domain-specific hash (used by Uniswap V3 / V4 to //! bind the permit to a specific swap intent). //! - `permitBatchTransferFrom(...)` — many tokens / amounts. //! //! Each permit is the EIP-712 typed-data hash of a //! `PermitTransferFrom` (or batch / witness) struct, signed by the //! token owner. The relayer submits the signature to Permit2, which //! pulls the tokens and transfers them. //! //! This module models the **struct shapes** and the canonical //! Permit2 contract address. Hashing requires keccak256 + EIP-712 //! domain separator computation, both of which we do here (the //! domain separator is fixed per chain). use serde::{Deserialize, Serialize}; use sha3::{Digest, Keccak256}; use crate::error::{Error, Result}; /// Canonical Permit2 contract address. Identical on every supported /// chain (deployed via CREATE2 from a deterministic salt). pub const PERMIT2_ADDRESS: &str = "0x000000000022d473030f116ddee9f6b43ac78ba3"; /// EIP-712 domain name. `"Permit2"`. pub const PERMIT2_DOMAIN_NAME: &str = "Permit2"; /// One `(token, amount)` permission inside a Permit2 transfer. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Permit2TokenPermissions { /// Token contract address. Lowercase hex with `0x` prefix. pub token: String, /// Amount the owner authorizes to be transferred. Smallest unit. pub amount: u128, } impl Permit2TokenPermissions { /// Construct. #[must_use] pub fn new(token: impl Into, amount: u128) -> Self { Self { token: token.into(), amount, } } } /// `PermitTransferFrom` struct used by `permitTransferFrom(...)`. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Permit2SingleTransfer { /// Token + amount the owner permits. pub permitted: Permit2TokenPermissions, /// Anti-replay nonce. Permit2 uses a 256-bit bitmap-style nonce /// (the value's index in a per-owner uint256). Operators choose /// a fresh nonce; submitting a duplicate reverts. pub nonce: u128, /// Deadline (block timestamp, seconds since unix epoch). pub deadline: u64, } impl Permit2SingleTransfer { /// EIP-712 type-hash for the single transfer. Computed once /// from the canonical typestring. /// /// Typestring: /// `PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)` #[must_use] pub fn type_hash() -> [u8; 32] { keccak256( b"PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)", ) } /// EIP-712 struct hash. Caller supplies the spender address (the /// relayer that will call `permitTransferFrom` on the user's /// behalf). /// /// # Errors /// Returns [`Error::Constraint`] when `spender` or the permitted /// token aren't valid 20-byte hex. pub fn struct_hash(&self, spender: &str) -> Result<[u8; 32]> { let permitted_hash = hash_token_permissions(&self.permitted)?; let spender_padded = pad_address(spender)?; let mut buf = Vec::with_capacity(160); buf.extend_from_slice(&Self::type_hash()); buf.extend_from_slice(&permitted_hash); buf.extend_from_slice(&spender_padded); buf.extend_from_slice(&u256_be(self.nonce)); buf.extend_from_slice(&u256_be(u128::from(self.deadline))); Ok(keccak256(&buf)) } } /// `PermitBatchTransferFrom` struct. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Permit2BatchTransfer { /// Multiple (token, amount) tuples. pub permitted: Vec, /// Anti-replay nonce. pub nonce: u128, /// Deadline (unix seconds). pub deadline: u64, } impl Permit2BatchTransfer { /// EIP-712 type-hash for the batch transfer. #[must_use] pub fn type_hash() -> [u8; 32] { keccak256( b"PermitBatchTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)", ) } /// EIP-712 struct hash. /// /// # Errors /// Returns [`Error::Constraint`] on bad addresses. pub fn struct_hash(&self, spender: &str) -> Result<[u8; 32]> { // Hash of the array of TokenPermissions: keccak of the // concatenation of each element's hash. let mut tp_concat = Vec::with_capacity(self.permitted.len() * 32); for p in &self.permitted { tp_concat.extend_from_slice(&hash_token_permissions(p)?); } let tp_array_hash = keccak256(&tp_concat); let spender_padded = pad_address(spender)?; let mut buf = Vec::with_capacity(160); buf.extend_from_slice(&Self::type_hash()); buf.extend_from_slice(&tp_array_hash); buf.extend_from_slice(&spender_padded); buf.extend_from_slice(&u256_be(self.nonce)); buf.extend_from_slice(&u256_be(u128::from(self.deadline))); Ok(keccak256(&buf)) } } /// Witness data attached to a `permitWitnessTransferFrom`. The /// witness is application-defined; Permit2 just folds its hash into /// the typed-data digest. Operator supplies the type-string and the /// pre-hashed witness. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Permit2Witness { /// EIP-712 type-string of the witness (e.g. /// `"ExclusiveDutchOrder(Order order)Order(...)"`). pub witness_type_string: String, /// Hash of the witness struct (caller computes it). pub witness_hash: [u8; 32], } fn hash_token_permissions(p: &Permit2TokenPermissions) -> Result<[u8; 32]> { const TYPE_HASH_BYTES: &[u8] = b"TokenPermissions(address token,uint256 amount)"; let type_hash = keccak256(TYPE_HASH_BYTES); let token_padded = pad_address(&p.token)?; let mut buf = Vec::with_capacity(96); buf.extend_from_slice(&type_hash); buf.extend_from_slice(&token_padded); buf.extend_from_slice(&u256_be(p.amount)); Ok(keccak256(&buf)) } fn pad_address(s: &str) -> Result<[u8; 32]> { let stripped = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).ok_or( Error::Constraint { field: "address", reason: "missing 0x prefix".into(), }, )?; if stripped.len() != 40 { return Err(Error::Constraint { field: "address", reason: format!("must be 40 hex chars, got {}", stripped.len()), }); } let raw = hex::decode(stripped).map_err(|e| Error::Constraint { field: "address", reason: format!("hex decode: {e}"), })?; let mut out = [0u8; 32]; out[12..].copy_from_slice(&raw); Ok(out) } fn u256_be(n: u128) -> [u8; 32] { let mut out = [0u8; 32]; out[16..].copy_from_slice(&n.to_be_bytes()); out } fn keccak256(data: &[u8]) -> [u8; 32] { let mut hasher = Keccak256::new(); hasher.update(data); let digest = hasher.finalize(); let mut out = [0u8; 32]; out.copy_from_slice(&digest); out } #[cfg(test)] mod tests { use super::*; #[test] fn permit2_address_is_canonical_constant() { assert_eq!(PERMIT2_ADDRESS.len(), 42); assert!(PERMIT2_ADDRESS.starts_with("0x")); } #[test] fn token_permissions_hashes_are_stable() { let p = Permit2TokenPermissions::new( "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", 1_000_000, ); let h1 = hash_token_permissions(&p).unwrap(); let h2 = hash_token_permissions(&p).unwrap(); assert_eq!(h1, h2); } #[test] fn struct_hash_depends_on_spender() { let single = Permit2SingleTransfer { permitted: Permit2TokenPermissions::new( "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", 1_000_000, ), nonce: 1, deadline: 2_000_000_000, }; let h1 = single .struct_hash("0x1111111111111111111111111111111111111111") .unwrap(); let h2 = single .struct_hash("0x2222222222222222222222222222222222222222") .unwrap(); assert_ne!(h1, h2); } #[test] fn batch_struct_hash_depends_on_each_permission() { let mut batch = Permit2BatchTransfer { permitted: vec![ Permit2TokenPermissions::new( "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", 1_000_000, ), Permit2TokenPermissions::new( "0xdac17f958d2ee523a2206206994597c13d831ec7", 2_000_000, ), ], nonce: 1, deadline: 2_000_000_000, }; let h1 = batch .struct_hash("0x1111111111111111111111111111111111111111") .unwrap(); // Mutate amount of second token; hash must change. batch.permitted[1].amount = 3_000_000; let h2 = batch .struct_hash("0x1111111111111111111111111111111111111111") .unwrap(); assert_ne!(h1, h2); } #[test] fn bad_address_errors() { let single = Permit2SingleTransfer { permitted: Permit2TokenPermissions::new("not-hex", 1), nonce: 1, deadline: 2, }; let err = single .struct_hash("0x1111111111111111111111111111111111111111") .unwrap_err(); assert!(matches!(err, Error::Constraint { .. })); } #[test] fn type_hash_matches_canonical_for_single() { // Sanity: PermitTransferFrom canonical typestring hash. // Computed once with this exact byte string; should be // stable across any keccak256 implementation. let h = Permit2SingleTransfer::type_hash(); // Just check it's nonzero and stable across two invocations. assert_ne!(h, [0u8; 32]); assert_eq!(h, Permit2SingleTransfer::type_hash()); } }