Skip to main content

r402_evm/
permit2.rs

1//! Canonical Uniswap Permit2 address, wire permissions, and client auto-approve.
2
3use alloy_primitives::{Address, address};
4use serde::{Deserialize, Serialize};
5
6use crate::chain::TokenAmount;
7
8/// Canonical Uniswap Permit2 contract address (same on all EVM chains via CREATE2).
9pub const PERMIT2_ADDRESS: Address = address!("0x000000000022D473030F116dDEE9F6B43aC78BA3");
10
11/// Permit2 token permissions — which token and how much.
12///
13/// Part of the `PermitWitnessTransferFrom` message structure that gets signed.
14#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
15pub struct Permit2TokenPermissions {
16    /// Token contract address.
17    pub token: Address,
18    /// Amount in smallest unit as decimal string (e.g., `"1000000"` for 1 USDC).
19    pub amount: TokenAmount,
20}
21
22#[cfg(feature = "client")]
23use std::future::Future;
24#[cfg(feature = "client")]
25use std::pin::Pin;
26
27#[cfg(feature = "client")]
28use alloy_primitives::{Bytes, U256};
29#[cfg(feature = "client")]
30use alloy_sol_types::{SolCall, sol};
31#[cfg(feature = "client")]
32use r402_core::error::ClientError;
33
34/// Abstraction for on-chain interactions needed by the Permit2 auto-approve flow.
35///
36/// Implement this trait to enable automatic Permit2 allowance management.
37/// When an approver is provided via a scheme client builder, the client will:
38///
39/// 1. **Before each Permit2 payment**, call [`check_permit2_allowance`](Self::check_permit2_allowance)
40///    to query the current ERC-20 allowance granted to the canonical Permit2 contract.
41/// 2. **If the allowance is insufficient**, call [`approve_permit2`](Self::approve_permit2)
42///    to send an on-chain `approve(Permit2, MAX)` transaction.
43/// 3. **Proceed with normal Permit2 EIP-712 signing** once allowance is confirmed.
44#[cfg(feature = "client")]
45pub trait Permit2Approver: Send + Sync {
46    /// Queries the current ERC-20 allowance that `owner` has granted to the
47    /// canonical Permit2 contract for the given `token`.
48    fn check_permit2_allowance(
49        &self,
50        token: Address,
51        owner: Address,
52    ) -> Pin<Box<dyn Future<Output = Result<U256, ClientError>> + Send + '_>>;
53
54    /// Sends an ERC-20 `approve(PERMIT2_ADDRESS, MAX_UINT256)` transaction
55    /// for `token` on behalf of `owner`, and waits for on-chain confirmation.
56    fn approve_permit2(
57        &self,
58        token: Address,
59        owner: Address,
60    ) -> Pin<Box<dyn Future<Output = Result<(), ClientError>> + Send + '_>>;
61}
62
63/// Built-in [`Permit2Approver`] backed by an Alloy provider.
64#[cfg(all(feature = "client", feature = "client-provider"))]
65pub(crate) struct BuiltinPermit2Approver<P> {
66    pub(crate) provider: P,
67}
68
69#[cfg(all(feature = "client", feature = "client-provider"))]
70impl<P> Permit2Approver for BuiltinPermit2Approver<P>
71where
72    P: alloy_provider::Provider + Send + Sync,
73{
74    fn check_permit2_allowance(
75        &self,
76        token: Address,
77        owner: Address,
78    ) -> Pin<Box<dyn Future<Output = Result<U256, ClientError>> + Send + '_>> {
79        Box::pin(async move {
80            let (_addr, calldata) = permit2_allowance_calldata(token, owner);
81            let tx = alloy_rpc_types_eth::TransactionRequest::default()
82                .to(token)
83                .input(calldata.into());
84            let result = self.provider.call(tx).await.map_err(|e| {
85                ClientError::PreConditionFailed(format!("Permit2 allowance check failed: {e}"))
86            })?;
87            Ok(U256::from_be_slice(&result))
88        })
89    }
90
91    fn approve_permit2(
92        &self,
93        token: Address,
94        _owner: Address,
95    ) -> Pin<Box<dyn Future<Output = Result<(), ClientError>> + Send + '_>> {
96        Box::pin(async move {
97            let calldata = IPermit2Approval::approveCall {
98                spender: PERMIT2_ADDRESS,
99                amount: U256::MAX,
100            }
101            .abi_encode();
102            let tx = alloy_rpc_types_eth::TransactionRequest::default()
103                .to(token)
104                .input(calldata.into());
105            let pending = self.provider.send_transaction(tx).await.map_err(|e| {
106                ClientError::PreConditionFailed(format!("Permit2 approve tx failed: {e}"))
107            })?;
108            let receipt = pending.get_receipt().await.map_err(|e| {
109                ClientError::PreConditionFailed(format!("Permit2 approve receipt failed: {e}"))
110            })?;
111            if !receipt.status() {
112                return Err(ClientError::PreConditionFailed(
113                    "Permit2 approve transaction reverted".into(),
114                ));
115            }
116            Ok(())
117        })
118    }
119}
120
121#[cfg(feature = "client")]
122sol! {
123    /// Minimal ERC-20 interface for client-side allowance checks and approvals.
124    #[allow(missing_docs, reason = "sol! generated interface")]
125    interface IPermit2Approval {
126        function allowance(address owner, address spender) external view returns (uint256);
127        function approve(address spender, uint256 amount) external returns (bool);
128    }
129}
130
131/// Returns the ABI-encoded calldata for checking a token's Permit2 allowance.
132#[cfg(feature = "client")]
133#[must_use]
134pub fn permit2_allowance_calldata(token: Address, owner: Address) -> (Address, Bytes) {
135    let call = IPermit2Approval::allowanceCall {
136        owner,
137        spender: PERMIT2_ADDRESS,
138    };
139    (token, call.abi_encode().into())
140}
141
142/// Returns the ABI-encoded calldata for approving the canonical Permit2
143/// contract to spend an unlimited amount of `token`.
144#[cfg(feature = "client")]
145#[must_use]
146pub fn permit2_approval_calldata(token: Address) -> (Address, Bytes) {
147    let call = IPermit2Approval::approveCall {
148        spender: PERMIT2_ADDRESS,
149        amount: U256::MAX,
150    };
151    (token, call.abi_encode().into())
152}