use alloy_primitives::{Address, B256, U256, keccak256};
use alloy_signer::Signer as _;
use alloy_signer_local::PrivateKeySigner;
use crate::{
error::CowError,
permit::types::{Erc20PermitInfo, PermitHookData, PermitInfo},
};
pub const PERMIT_GAS_LIMIT: u64 = 100_000;
fn abi_address(a: Address) -> [u8; 32] {
let mut buf = [0u8; 32];
buf[12..].copy_from_slice(a.as_slice());
buf
}
const fn abi_u256(v: U256) -> [u8; 32] {
v.to_be_bytes()
}
fn abi_u64(v: u64) -> [u8; 32] {
let mut buf = [0u8; 32];
buf[24..].copy_from_slice(&v.to_be_bytes());
buf
}
fn domain_type_hash() -> B256 {
keccak256(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
}
#[must_use]
pub fn permit_type_hash() -> B256 {
keccak256(b"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
}
#[must_use]
pub fn permit_domain_separator(name: &str, version: &str, chain_id: u64, token: Address) -> B256 {
let name_hash = keccak256(name.as_bytes());
let version_hash = keccak256(version.as_bytes());
let mut buf = [0u8; 5 * 32];
buf[0..32].copy_from_slice(domain_type_hash().as_slice());
buf[32..64].copy_from_slice(name_hash.as_slice());
buf[64..96].copy_from_slice(version_hash.as_slice());
buf[96..128].copy_from_slice(&abi_u64(chain_id));
buf[128..160].copy_from_slice(&abi_address(token));
keccak256(buf)
}
#[must_use]
pub fn permit_digest(domain_sep: B256, info: &PermitInfo) -> B256 {
let mut struct_buf = [0u8; 6 * 32];
struct_buf[0..32].copy_from_slice(permit_type_hash().as_slice());
struct_buf[32..64].copy_from_slice(&abi_address(info.owner));
struct_buf[64..96].copy_from_slice(&abi_address(info.spender));
struct_buf[96..128].copy_from_slice(&abi_u256(info.value));
struct_buf[128..160].copy_from_slice(&abi_u256(info.nonce));
struct_buf[160..192].copy_from_slice(&abi_u64(info.deadline));
let struct_hash = keccak256(struct_buf);
let mut digest_buf = [0u8; 66];
digest_buf[0] = 0x19;
digest_buf[1] = 0x01;
digest_buf[2..34].copy_from_slice(domain_sep.as_slice());
digest_buf[34..66].copy_from_slice(struct_hash.as_slice());
keccak256(digest_buf)
}
pub async fn sign_permit(
info: &PermitInfo,
erc20_info: &Erc20PermitInfo,
signer: &PrivateKeySigner,
) -> Result<[u8; 65], CowError> {
let domain_sep = permit_domain_separator(
&erc20_info.name,
&erc20_info.version,
erc20_info.chain_id,
info.token_address,
);
let digest = permit_digest(domain_sep, info);
let sig = signer.sign_hash(&digest).await.map_err(|e| CowError::Signing(e.to_string()))?;
let mut out = [0u8; 65];
out.copy_from_slice(&sig.as_bytes());
Ok(out)
}
#[must_use]
pub fn build_permit_calldata(info: &PermitInfo, signature: [u8; 65]) -> Vec<u8> {
let selector: [u8; 4] = {
let h = keccak256(b"permit(address,address,uint256,uint256,uint256,uint8,bytes32,bytes32)");
[h[0], h[1], h[2], h[3]]
};
let mut r = [0u8; 32];
let mut s = [0u8; 32];
r.copy_from_slice(&signature[0..32]);
s.copy_from_slice(&signature[32..64]);
let v: u8 = signature[64];
let mut calldata = Vec::with_capacity(4 + 8 * 32);
calldata.extend_from_slice(&selector);
calldata.extend_from_slice(&abi_address(info.owner));
calldata.extend_from_slice(&abi_address(info.spender));
calldata.extend_from_slice(&abi_u256(info.value));
calldata.extend_from_slice(&abi_u256(info.nonce));
calldata.extend_from_slice(&abi_u64(info.deadline));
let mut v_word = [0u8; 32];
v_word[31] = v;
calldata.extend_from_slice(&v_word);
calldata.extend_from_slice(&r);
calldata.extend_from_slice(&s);
calldata
}
pub async fn build_permit_hook(
info: &PermitInfo,
erc20_info: &Erc20PermitInfo,
signer: &PrivateKeySigner,
) -> Result<PermitHookData, CowError> {
let signature = sign_permit(info, erc20_info, signer).await?;
let calldata = build_permit_calldata(info, signature);
Ok(PermitHookData { target: info.token_address, calldata, gas_limit: PERMIT_GAS_LIMIT })
}
#[cfg(test)]
mod tests {
use alloy_primitives::address;
use super::*;
#[test]
fn permit_type_hash_is_stable() {
let h = permit_type_hash();
assert_ne!(h, B256::ZERO);
assert_eq!(h, permit_type_hash());
}
#[test]
fn domain_separator_is_deterministic() {
let token = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let a = permit_domain_separator("USD Coin", "2", 1, token);
let b = permit_domain_separator("USD Coin", "2", 1, token);
assert_eq!(a, b);
let c = permit_domain_separator("USD Coin", "2", 5, token);
assert_ne!(a, c);
}
#[test]
fn calldata_has_correct_length() {
let info = PermitInfo {
token_address: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
owner: address!("1111111111111111111111111111111111111111"),
spender: address!("2222222222222222222222222222222222222222"),
value: U256::from(1_000_000u64),
nonce: U256::ZERO,
deadline: 9_999_999_999u64,
};
let sig = [0u8; 65];
let cd = build_permit_calldata(&info, sig);
assert_eq!(cd.len(), 4 + 8 * 32);
}
#[test]
fn permit_digest_is_deterministic() {
let token = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let domain_sep = permit_domain_separator("USD Coin", "2", 1, token);
let info = PermitInfo {
token_address: token,
owner: address!("1111111111111111111111111111111111111111"),
spender: address!("2222222222222222222222222222222222222222"),
value: U256::from(1_000_000u64),
nonce: U256::ZERO,
deadline: 9_999_999_999u64,
};
let d1 = permit_digest(domain_sep, &info);
let d2 = permit_digest(domain_sep, &info);
assert_eq!(d1, d2);
assert_ne!(d1, B256::ZERO);
}
#[test]
fn permit_digest_changes_with_different_nonce() {
let token = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let domain_sep = permit_domain_separator("USD Coin", "2", 1, token);
let info1 = PermitInfo {
token_address: token,
owner: address!("1111111111111111111111111111111111111111"),
spender: address!("2222222222222222222222222222222222222222"),
value: U256::from(1_000_000u64),
nonce: U256::ZERO,
deadline: 9_999_999_999u64,
};
let d1 = permit_digest(domain_sep, &info1);
let info2 = PermitInfo { nonce: U256::from(1u64), ..info1 };
let d2 = permit_digest(domain_sep, &info2);
assert_ne!(d1, d2);
}
#[test]
fn domain_separator_different_token_produces_different_result() {
let token1 = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let token2 = address!("dAC17F958D2ee523a2206206994597C13D831ec7");
let a = permit_domain_separator("USD Coin", "2", 1, token1);
let b = permit_domain_separator("Tether USD", "2", 1, token2);
assert_ne!(a, b);
}
#[test]
fn calldata_starts_with_permit_selector() {
let info = PermitInfo {
token_address: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
owner: address!("1111111111111111111111111111111111111111"),
spender: address!("2222222222222222222222222222222222222222"),
value: U256::from(1_000_000u64),
nonce: U256::ZERO,
deadline: 9_999_999_999u64,
};
let sig = [0u8; 65];
let cd = build_permit_calldata(&info, sig);
let expected_selector =
keccak256(b"permit(address,address,uint256,uint256,uint256,uint8,bytes32,bytes32)");
assert_eq!(&cd[..4], &expected_selector[..4]);
}
#[tokio::test]
async fn sign_permit_produces_65_bytes() {
let signer = PrivateKeySigner::random();
let info = PermitInfo {
token_address: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
owner: signer.address(),
spender: address!("2222222222222222222222222222222222222222"),
value: U256::from(1_000_000u64),
nonce: U256::ZERO,
deadline: 9_999_999_999u64,
};
let erc20_info =
Erc20PermitInfo { name: "USD Coin".into(), version: "2".into(), chain_id: 1 };
let sig = sign_permit(&info, &erc20_info, &signer).await.unwrap();
assert_eq!(sig.len(), 65);
}
#[tokio::test]
async fn build_permit_hook_returns_correct_data() {
let signer = PrivateKeySigner::random();
let info = PermitInfo {
token_address: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
owner: signer.address(),
spender: address!("2222222222222222222222222222222222222222"),
value: U256::from(1_000_000u64),
nonce: U256::ZERO,
deadline: 9_999_999_999u64,
};
let erc20_info =
Erc20PermitInfo { name: "USD Coin".into(), version: "2".into(), chain_id: 1 };
let hook = build_permit_hook(&info, &erc20_info, &signer).await.unwrap();
assert_eq!(hook.target, info.token_address);
assert_eq!(hook.gas_limit, PERMIT_GAS_LIMIT);
assert_eq!(hook.calldata.len(), 260); }
}