use sha3::{Digest, Keccak256};
pub const PERMIT2_ADDRESS: [u8; 20] = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0xD4, 0x73, 0x03, 0x0F, 0x11, 0x6d, 0xDE, 0xE9, 0xF6, 0xB4,
0x3a, 0xC7, 0x8B, 0xA3,
];
const EIP712_DOMAIN_TYPE: &str =
"EIP712Domain(string name,uint256 chainId,address verifyingContract)";
const TOKEN_PERMISSIONS_TYPE: &str = "TokenPermissions(address token,uint256 amount)";
const GASLESS_ORDER_TYPE: &str = "GaslessCrossChainOrder(address originSettler,address user,uint256 nonce,uint256 originChainId,uint32 openDeadline,uint32 fillDeadline,bytes32 orderDataType,bytes orderData)";
const PERMIT_WITNESS_TYPE: &str = "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,GaslessCrossChainOrder witness)GaslessCrossChainOrder(address originSettler,address user,uint256 nonce,uint256 originChainId,uint32 openDeadline,uint32 fillDeadline,bytes32 orderDataType,bytes orderData)TokenPermissions(address token,uint256 amount)";
#[derive(Debug, Clone)]
pub struct GaslessOrder {
pub origin_settler: [u8; 20],
pub user: [u8; 20],
pub nonce: [u8; 32],
pub origin_chain_id: [u8; 32],
pub open_deadline: u32,
pub fill_deadline: u32,
pub order_data_type: [u8; 32],
pub order_data: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct Permit2Witness {
pub token: [u8; 20],
pub amount: [u8; 32],
pub spender: [u8; 20],
pub permit2_nonce: [u8; 32],
pub deadline: [u8; 32],
pub chain_id: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Erc7683Error {
Recovery,
WrongSigner,
}
fn keccak(data: &[u8]) -> [u8; 32] {
Keccak256::digest(data).into()
}
fn word_addr(a: &[u8; 20]) -> [u8; 32] {
let mut w = [0u8; 32];
w[12..].copy_from_slice(a);
w
}
fn word_u32(v: u32) -> [u8; 32] {
let mut w = [0u8; 32];
w[28..].copy_from_slice(&v.to_be_bytes());
w
}
fn word_u256(v: u64) -> [u8; 32] {
let mut w = [0u8; 32];
w[24..].copy_from_slice(&v.to_be_bytes());
w
}
pub fn eip712_digest(order: &GaslessOrder, w: &Permit2Witness) -> [u8; 32] {
let mut dom = Vec::with_capacity(32 * 4);
dom.extend_from_slice(&keccak(EIP712_DOMAIN_TYPE.as_bytes()));
dom.extend_from_slice(&keccak(b"Permit2"));
dom.extend_from_slice(&word_u256(w.chain_id));
dom.extend_from_slice(&word_addr(&PERMIT2_ADDRESS));
let domain_separator = keccak(&dom);
let mut tp = Vec::with_capacity(32 * 3);
tp.extend_from_slice(&keccak(TOKEN_PERMISSIONS_TYPE.as_bytes()));
tp.extend_from_slice(&word_addr(&w.token));
tp.extend_from_slice(&w.amount);
let token_permissions = keccak(&tp);
let mut wit = Vec::with_capacity(32 * 9);
wit.extend_from_slice(&keccak(GASLESS_ORDER_TYPE.as_bytes()));
wit.extend_from_slice(&word_addr(&order.origin_settler));
wit.extend_from_slice(&word_addr(&order.user));
wit.extend_from_slice(&order.nonce);
wit.extend_from_slice(&order.origin_chain_id);
wit.extend_from_slice(&word_u32(order.open_deadline));
wit.extend_from_slice(&word_u32(order.fill_deadline));
wit.extend_from_slice(&order.order_data_type);
wit.extend_from_slice(&keccak(&order.order_data));
let witness = keccak(&wit);
let mut sh = Vec::with_capacity(32 * 6);
sh.extend_from_slice(&keccak(PERMIT_WITNESS_TYPE.as_bytes()));
sh.extend_from_slice(&token_permissions);
sh.extend_from_slice(&word_addr(&w.spender));
sh.extend_from_slice(&w.permit2_nonce);
sh.extend_from_slice(&w.deadline);
sh.extend_from_slice(&witness);
let struct_hash = keccak(&sh);
let mut pre = Vec::with_capacity(2 + 64);
pre.extend_from_slice(&[0x19, 0x01]);
pre.extend_from_slice(&domain_separator);
pre.extend_from_slice(&struct_hash);
keccak(&pre)
}
pub fn recover_signer(
order: &GaslessOrder,
w: &Permit2Witness,
signature: &[u8; 65],
) -> Result<[u8; 20], Erc7683Error> {
use k256::ecdsa::{RecoveryId, Signature, VerifyingKey};
let v = signature[64];
let y_parity = if v >= 27 { v - 27 } else { v };
let recovery_id = RecoveryId::try_from(y_parity).map_err(|_| Erc7683Error::Recovery)?;
let mut sig_bytes = [0u8; 64];
sig_bytes.copy_from_slice(&signature[..64]);
let sig = Signature::from_bytes(&sig_bytes.into()).map_err(|_| Erc7683Error::Recovery)?;
let digest = eip712_digest(order, w);
let vk = VerifyingKey::recover_from_prehash(&digest, &sig, recovery_id)
.map_err(|_| Erc7683Error::Recovery)?;
let encoded = vk.to_sec1_point(false);
let addr_hash = keccak(&encoded.as_bytes()[1..]);
let mut addr = [0u8; 20];
addr.copy_from_slice(&addr_hash[12..32]);
Ok(addr)
}
pub fn verify_gasless_order(
order: &GaslessOrder,
w: &Permit2Witness,
signature: &[u8; 65],
) -> Result<[u8; 20], Erc7683Error> {
let recovered = recover_signer(order, w, signature)?;
if recovered == order.user {
Ok(recovered)
} else {
Err(Erc7683Error::WrongSigner)
}
}
#[cfg(test)]
mod tests {
use super::*;
use k256::ecdsa::{SigningKey, VerifyingKey};
use sha3::{Digest, Keccak256};
fn evm_address(sk: &SigningKey) -> [u8; 20] {
let vk = VerifyingKey::from(sk);
let enc = vk.to_sec1_point(false);
let h = Keccak256::digest(&enc.as_bytes()[1..]);
let mut a = [0u8; 20];
a.copy_from_slice(&h[12..32]);
a
}
#[test]
fn permit2_witness_round_trip_recovers_user() {
let sk = SigningKey::from_slice(&[0x42u8; 32]).unwrap();
let user = evm_address(&sk);
let order = GaslessOrder {
origin_settler: [0x11; 20],
user,
nonce: {
let mut n = [0u8; 32];
n[31] = 7;
n
},
origin_chain_id: {
let mut c = [0u8; 32];
c[30] = 0x0a; c
},
open_deadline: 1_900_000_000,
fill_deadline: 1_900_000_500,
order_data_type: [0xAB; 32],
order_data: b"opaque-order-data".to_vec(),
};
let w = Permit2Witness {
token: [0x22; 20],
amount: {
let mut a = [0u8; 32];
a[28..].copy_from_slice(&1_000_000u32.to_be_bytes());
a
},
spender: [0x33; 20],
permit2_nonce: [0x01; 32],
deadline: {
let mut d = [0u8; 32];
d[28..].copy_from_slice(&1_900_000_000u32.to_be_bytes());
d
},
chain_id: 8453,
};
let digest = eip712_digest(&order, &w);
let (sig, rid) = sk.sign_prehash_recoverable(&digest).unwrap();
let mut sig65 = sig.to_bytes().to_vec();
sig65.push(rid.to_byte());
let sig65: [u8; 65] = sig65.try_into().unwrap();
assert_eq!(verify_gasless_order(&order, &w, &sig65), Ok(user));
let mut bad = order.clone();
bad.fill_deadline += 1;
assert_eq!(
verify_gasless_order(&bad, &w, &sig65),
Err(Erc7683Error::WrongSigner)
);
}
#[test]
fn typehash_strings_are_alphabetically_ordered() {
let g = PERMIT_WITNESS_TYPE.find("GaslessCrossChainOrder(").unwrap();
let t = PERMIT_WITNESS_TYPE.find("TokenPermissions(").unwrap();
assert!(g < t, "referenced structs must be alphabetical");
}
}