use alloy_primitives::{Address, B256, Bytes, U256};
use alloy_sol_types::{Eip712Domain, SolStruct, eip712_domain, sol};
use r402_core::error::ClientError;
use crate::chain::TokenAmount;
use crate::exact::PERMIT2_ADDRESS;
use crate::exact::client::SignerLike;
use crate::upto::Eip2612SignedPermit;
sol! {
#[derive(Debug)]
struct Eip2612TypedData {
address owner;
address spender;
uint256 value;
uint256 nonce;
uint256 deadline;
}
}
#[derive(Debug, Clone)]
pub struct Eip2612SigningParams {
pub chain_id: u64,
pub asset_address: Address,
pub token_name: String,
pub token_version: String,
pub owner: Address,
pub value: U256,
pub nonce: U256,
pub deadline: U256,
}
pub async fn sign_eip2612_permit<S: SignerLike + Sync>(
signer: &S,
params: &Eip2612SigningParams,
) -> Result<Eip2612SignedPermit, ClientError> {
let domain = build_token_domain(params);
let typed = Eip2612TypedData {
owner: params.owner,
spender: PERMIT2_ADDRESS,
value: params.value,
nonce: params.nonce,
deadline: params.deadline,
};
let hash: B256 = typed.eip712_signing_hash(&domain);
let signature = signer
.sign_hash(&hash)
.await
.map_err(|e| ClientError::Signing(format!("{e:?}")))?;
Ok(Eip2612SignedPermit {
from: params.owner,
asset: params.asset_address,
spender: PERMIT2_ADDRESS,
value: TokenAmount::from(params.value),
deadline: TokenAmount::from(params.deadline),
signature: Bytes::from(signature.as_bytes().to_vec()),
})
}
fn build_token_domain(params: &Eip2612SigningParams) -> Eip712Domain {
eip712_domain! {
name: params.token_name.clone(),
version: params.token_version.clone(),
chain_id: params.chain_id,
verifying_contract: params.asset_address,
}
}
#[cfg(test)]
mod tests {
use alloy_primitives::address;
use alloy_signer_local::PrivateKeySigner;
use super::*;
fn fixture() -> Eip2612SigningParams {
Eip2612SigningParams {
chain_id: 8453,
asset_address: address!("833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"),
token_name: "USD Coin".into(),
token_version: "2".into(),
owner: address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
value: U256::from(1_000_000_u64),
nonce: U256::from(0u64),
deadline: U256::from(1_700_000_000_u64),
}
}
#[tokio::test]
async fn sign_returns_canonical_65_byte_signature() {
let signer = PrivateKeySigner::random();
let mut params = fixture();
params.owner = signer.address();
let permit = sign_eip2612_permit(&signer, ¶ms)
.await
.expect("signing succeeds");
assert_eq!(permit.signature.len(), Eip2612SignedPermit::SIGNATURE_LEN);
assert_eq!(permit.from, signer.address());
assert_eq!(permit.asset, params.asset_address);
assert_eq!(permit.spender, PERMIT2_ADDRESS);
assert_eq!(permit.value.0, params.value);
assert_eq!(permit.deadline.0, params.deadline);
}
#[tokio::test]
async fn signing_is_deterministic_for_fixed_inputs() {
let signer = PrivateKeySigner::random();
let mut params = fixture();
params.owner = signer.address();
let a = sign_eip2612_permit(&signer, ¶ms).await.unwrap();
let b = sign_eip2612_permit(&signer, ¶ms).await.unwrap();
assert_eq!(a.signature, b.signature);
}
#[tokio::test]
async fn signing_uses_canonical_permit2_spender() {
let signer = PrivateKeySigner::random();
let mut params = fixture();
params.owner = signer.address();
let permit = sign_eip2612_permit(&signer, ¶ms).await.unwrap();
assert_eq!(permit.spender, PERMIT2_ADDRESS);
}
}