open-standards/openpay/crates/op-crypto-extra/src/permit2.rs
Transaction Science 68b8a2dbee ingest wave F: 3 crates · op-crypto-extra + smart-byte-acdc + eoc-memory
OpenPay (1 crate, 3751 LOC, 83 tests)
  op-crypto-extra      Extended crypto rails. Stablecoin catalog (USDC, USDT,
                       PYUSD, DAI, RLUSD, EURC, FDUSD — 26 deployments). L2
                       catalog (12 chains incl. Optimism, Arbitrum, Base,
                       zkSync Era, Polygon zkEVM, Linea, Scroll, Starknet).
                       CCTP v2 BurnMessage encode/decode + domain ids +
                       attestation. ERC-4337 UserOperation v0.7 +
                       PackedUserOperation. EIP-7702 set-EOA-code with RLP
                       signing preimage. Uniswap Permit2 single + batch
                       transfer EIP-712 hashes. EIP-2612 ERC-20 permit.
                       BOLT-11 bech32 invoices + LNURL decode. Bitcoin
                       PSBT v2 (BIP-370). Atomic-swap HTLC. Solana SPL /
                       Aleo / Aztec confidential-transfer sketches.

Smart Byte (1 crate, 2304 LOC, 30 tests)
  smart-byte-acdc      Authentic Chained Data Containers (KERI ecosystem).
                       Acdc + AcdcBuilder (targeted + untargeted) with SAID
                       derivation. JSON Schema 2020-12 validator with
                       oneOf partial-disclosure. TEL (RIP/VRT/ISS/REV)
                       with hash-chained verification. IPEX (apply/offer/
                       agree/grant/admit/spurn) chained by SAID. Edge
                       records (I2I/NI2I/DI2I) + EdgeGraph BFS traversal
                       with cycle detection. Selective disclosure with
                       full/none/subset plans + compact packing.
                       CredentialRegistry trait + InMemoryRegistry over
                       TEL. Composes with smart-byte-core + smart-byte-keri-witness.

EOC (1 crate, 1764 LOC, 16 tests)
  eoc-memory           Long-term memory primitives. Memory trait + MemoryKind +
                       EpisodeId. Episodic event log with TimeIndex range
                       queries. RDF-style Triple store with S/P/O indices.
                       Bounded attention-budget Scratchpad with
                       salience-weighted eviction. Rolling SummaryBuffer +
                       FIFO SlidingWindow. Dream-cycle consolidate()
                       (episodic → semantic). Ebbinghaus forgetting curve
                       with reinforcement. Retriever blending cosine
                       similarity + recency + saturating frequency.
                       MemGpt three-tier shape (Main/Recall/Archival).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-24 17:28:24 -04:00

297 lines
10 KiB
Rust

//! 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<String>, 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<Permit2TokenPermissions>,
/// 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());
}
}