Skip to main content

tycho_execution/encoding/evm/approvals/
permit2.rs

1use std::str::FromStr;
2
3use alloy::{
4    core::sol,
5    primitives::{aliases::U48, Address, Bytes as AlloyBytes, TxKind, U160, U256},
6    providers::Provider,
7    rpc::types::{TransactionInput, TransactionRequest},
8    sol_types::SolValue,
9};
10use chrono::Utc;
11use num_bigint::BigUint;
12use tokio::runtime::Handle;
13use tycho_common::Bytes;
14
15use crate::encoding::{
16    errors::EncodingError,
17    evm::{
18        encoding_utils::encode_input,
19        utils::{
20            biguint_to_u256, bytes_to_address, create_encoding_runtime, get_client,
21            on_blocking_thread, EVMProvider, SafeRuntime,
22        },
23    },
24    models,
25};
26
27/// Struct for managing Permit2 operations, including encoding approvals and fetching allowance
28/// data.
29#[derive(Clone)]
30pub struct Permit2 {
31    address: Address,
32    client: EVMProvider,
33    runtime_handle: Handle,
34    #[allow(dead_code)]
35    runtime: SafeRuntime,
36}
37
38/// Type alias for representing allowance data as a tuple of (amount, expiration, nonce). Used for
39/// decoding
40type Allowance = (U160, U48, U48);
41/// Expiration period for permits, set to 30 days (in seconds).
42const PERMIT_EXPIRATION: u64 = 30 * 24 * 60 * 60;
43/// Expiration period for signatures, set to 30 minutes (in seconds).
44const PERMIT_SIG_EXPIRATION: u64 = 30 * 60;
45
46sol! {
47     #[derive(Debug)]
48    struct PermitSingle {
49        PermitDetails details;
50        address spender;
51        uint256 sigDeadline;
52    }
53
54    #[derive(Debug)]
55    struct PermitDetails {
56        address token;
57        uint160 amount;
58        uint48 expiration;
59        uint48 nonce;
60    }
61}
62
63impl TryFrom<&PermitSingle> for models::PermitSingle {
64    type Error = EncodingError;
65
66    fn try_from(sol: &PermitSingle) -> Result<Self, EncodingError> {
67        Ok(models::PermitSingle::new(
68            models::PermitDetails::new(
69                Bytes::from(sol.details.token.to_vec()),
70                BigUint::from_bytes_be(&sol.details.amount.to_be_bytes::<20>()),
71                BigUint::from_bytes_be(
72                    &sol.details
73                        .expiration
74                        .to_be_bytes::<6>(),
75                ),
76                BigUint::from_bytes_be(&sol.details.nonce.to_be_bytes::<6>()),
77            ),
78            Bytes::from(sol.spender.to_vec()),
79            BigUint::from_bytes_be(&sol.sigDeadline.to_be_bytes::<32>()),
80        ))
81    }
82}
83
84impl TryFrom<&models::PermitSingle> for PermitSingle {
85    type Error = EncodingError;
86
87    fn try_from(p: &models::PermitSingle) -> Result<Self, EncodingError> {
88        Ok(PermitSingle {
89            details: PermitDetails {
90                token: bytes_to_address(p.details().token())?,
91                amount: U160::from(biguint_to_u256(p.details().amount())),
92                expiration: U48::from(biguint_to_u256(p.details().expiration())),
93                nonce: U48::from(biguint_to_u256(p.details().nonce())),
94            },
95            spender: bytes_to_address(p.spender())?,
96            sigDeadline: biguint_to_u256(p.sig_deadline()),
97        })
98    }
99}
100
101impl Permit2 {
102    pub fn new() -> Result<Self, EncodingError> {
103        let (handle, runtime) = create_encoding_runtime()?;
104        let client = on_blocking_thread(|| handle.block_on(get_client()))??;
105        Ok(Self {
106            address: Address::from_str("0x000000000022D473030F116dDEE9F6B43aC78BA3")
107                .map_err(|_| EncodingError::FatalError("Permit2 address not valid".to_string()))?,
108            client,
109            runtime_handle: handle,
110            runtime,
111        })
112    }
113
114    /// Fetches allowance data for a specific owner, spender, and token.
115    fn get_existing_allowance(
116        &self,
117        owner: &Bytes,
118        spender: &Bytes,
119        token: &Bytes,
120    ) -> Result<Allowance, EncodingError> {
121        let args = (bytes_to_address(owner)?, bytes_to_address(token)?, bytes_to_address(spender)?);
122        let data = encode_input("allowance(address,address,address)", args.abi_encode());
123        let tx = TransactionRequest {
124            to: Some(TxKind::from(self.address)),
125            input: TransactionInput { input: Some(AlloyBytes::from(data)), data: None },
126            ..Default::default()
127        };
128
129        let output = on_blocking_thread(|| {
130            self.runtime_handle
131                .block_on(async { self.client.call(tx).await })
132        })?;
133        match output {
134            Ok(response) => {
135                let allowance: Allowance = Allowance::abi_decode(&response).map_err(|_| {
136                    EncodingError::FatalError(
137                        "Failed to decode response for permit2 allowance".to_string(),
138                    )
139                })?;
140                Ok(allowance)
141            }
142            Err(err) => Err(EncodingError::RecoverableError(format!(
143                "Call to permit2 allowance method failed with error: {err}"
144            ))),
145        }
146    }
147    /// Creates permit single
148    pub fn get_permit(
149        &self,
150        spender: &Bytes,
151        owner: &Bytes,
152        token: &Bytes,
153        amount: &BigUint,
154    ) -> Result<models::PermitSingle, EncodingError> {
155        let current_time = Utc::now()
156            .naive_utc()
157            .and_utc()
158            .timestamp() as u64;
159
160        let (_, _, nonce) = self.get_existing_allowance(owner, spender, token)?;
161        let expiration = U48::from(current_time + PERMIT_EXPIRATION);
162        let sig_deadline = U256::from(current_time + PERMIT_SIG_EXPIRATION);
163        let amount = U160::from(biguint_to_u256(amount));
164
165        let details = PermitDetails { token: bytes_to_address(token)?, amount, expiration, nonce };
166
167        let permit_single = PermitSingle {
168            details,
169            spender: bytes_to_address(spender)?,
170            sigDeadline: sig_deadline,
171        };
172
173        models::PermitSingle::try_from(&permit_single)
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use std::str::FromStr;
180
181    use alloy::{
182        primitives::{Address, Uint, B256},
183        signers::{local::PrivateKeySigner, Signature, SignerSync},
184        sol_types::{eip712_domain, SolStruct, SolValue},
185    };
186    use num_bigint::BigUint;
187    use tycho_common::models::Chain;
188
189    use super::*;
190
191    // These two implementations are to avoid comparing the expiration and sig_deadline fields
192    // because they are timestamps
193    impl PartialEq for PermitSingle {
194        fn eq(&self, other: &Self) -> bool {
195            if self.details != other.details {
196                return false;
197            }
198            if self.spender != other.spender {
199                return false;
200            }
201            true
202        }
203    }
204
205    impl PartialEq for PermitDetails {
206        fn eq(&self, other: &Self) -> bool {
207            if self.token != other.token {
208                return false;
209            }
210            if self.amount != other.amount {
211                return false;
212            }
213            // Compare `nonce`
214            if self.nonce != other.nonce {
215                return false;
216            }
217
218            true
219        }
220    }
221
222    fn eth_chain() -> Chain {
223        Chain::Ethereum
224    }
225
226    #[test]
227    fn test_get_existing_allowance() {
228        let manager = Permit2::new().unwrap();
229
230        let token = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
231        let owner = Bytes::from_str("0x2c6a3cd97c6283b95ac8c5a4459ebb0d5fd404f4").unwrap();
232        let spender = Bytes::from_str("0xba12222222228d8ba445958a75a0704d566bf2c8").unwrap();
233
234        let result = manager
235            .get_existing_allowance(&owner, &spender, &token)
236            .unwrap();
237        assert_eq!(
238            result,
239            (Uint::<160, 3>::from(0), Uint::<48, 1>::from(0), Uint::<48, 1>::from(0))
240        );
241    }
242
243    #[test]
244    fn test_get_permit() {
245        let permit2 = Permit2::new().expect("Failed to create Permit2");
246
247        let owner = Bytes::from_str("0x2c6a3cd97c6283b95ac8c5a4459ebb0d5fd404f4").unwrap();
248        let spender = Bytes::from_str("0xba12222222228d8ba445958a75a0704d566bf2c8").unwrap();
249        let token = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
250        let amount = BigUint::from(1000u64);
251
252        let permit = permit2
253            .get_permit(&spender, &owner, &token, &amount)
254            .unwrap();
255
256        let expected_details = models::PermitDetails::new(
257            token,
258            amount,
259            BigUint::from(Utc::now().timestamp() as u64 + PERMIT_EXPIRATION),
260            BigUint::from(0u64),
261        );
262        let expected_permit_single = models::PermitSingle::new(
263            expected_details,
264            Bytes::from_str("0xba12222222228d8ba445958a75a0704d566bf2c8").unwrap(),
265            BigUint::from(Utc::now().timestamp() as u64 + PERMIT_SIG_EXPIRATION),
266        );
267
268        assert_eq!(
269            permit, expected_permit_single,
270            "Decoded PermitSingle does not match expected values"
271        );
272    }
273
274    /// Signs a Permit2 `PermitSingle` struct using the EIP-712 signing scheme.
275    ///
276    /// This function constructs an EIP-712 domain specific to the Permit2 contract and computes the
277    /// hash of the provided `PermitSingle`. It then uses the given `PrivateKeySigner` to produce
278    /// a cryptographic signature of the permit.
279    fn sign_permit(
280        chain_id: u64,
281        permit_single: &models::PermitSingle,
282        signer: PrivateKeySigner,
283    ) -> Result<Signature, EncodingError> {
284        let permit2_address = Address::from_str("0x000000000022D473030F116dDEE9F6B43aC78BA3")
285            .map_err(|_| EncodingError::FatalError("Permit2 address not valid".to_string()))?;
286        let domain = eip712_domain! {
287            name: "Permit2",
288            chain_id: chain_id,
289            verifying_contract: permit2_address,
290        };
291        let permit_single: PermitSingle = PermitSingle::try_from(permit_single)?;
292        let hash = permit_single.eip712_signing_hash(&domain);
293        signer
294            .sign_hash_sync(&hash)
295            .map_err(|e| {
296                EncodingError::FatalError(format!(
297                    "Failed to sign permit2 approval with error: {e}"
298                ))
299            })
300    }
301
302    /// This test actually calls the permit method on the Permit2 contract to verify the encoded
303    /// data works. It requires an Anvil fork, so please run with the following command: anvil
304    /// --fork-url <RPC-URL> And set up the following env var as RPC_URL=127.0.0.1:8545
305    /// Use an account from anvil to fill the anvil_account and anvil_private_key variables
306    #[test]
307    #[cfg_attr(not(feature = "fork-tests"), ignore)]
308    fn test_permit() {
309        let anvil_account = Bytes::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266").unwrap();
310        let anvil_private_key =
311            "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".to_string();
312
313        let pk = B256::from_str(&anvil_private_key)
314            .map_err(|_| {
315                EncodingError::FatalError(
316                    "Failed to convert swapper private key to B256".to_string(),
317                )
318            })
319            .unwrap();
320        let signer = PrivateKeySigner::from_bytes(&pk)
321            .map_err(|_| {
322                EncodingError::FatalError("Failed to create signer from private key".to_string())
323            })
324            .unwrap();
325        let permit2 = Permit2::new().expect("Failed to create Permit2");
326
327        let token = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
328        let amount = BigUint::from(1000u64);
329
330        // Approve token allowance for permit2 contract
331        let approve_function_signature = "approve(address,uint256)";
332        let args = (permit2.address, biguint_to_u256(&BigUint::from(1000000u64)));
333        let data = encode_input(approve_function_signature, args.abi_encode());
334
335        let tx = TransactionRequest {
336            to: Some(TxKind::from(bytes_to_address(&token).unwrap())),
337            input: TransactionInput { input: Some(AlloyBytes::from(data)), data: None },
338            ..Default::default()
339        };
340        let receipt = on_blocking_thread(|| {
341            permit2.runtime_handle.block_on(async {
342                let pending_tx = permit2
343                    .client
344                    .send_transaction(tx)
345                    .await
346                    .unwrap();
347                // Wait for the transaction to be mined
348                pending_tx.get_receipt().await.unwrap()
349            })
350        })
351        .unwrap();
352        assert!(receipt.status(), "Approve transaction failed");
353
354        let spender = Bytes::from_str("0xba12222222228d8ba445958a75a0704d566bf2c8").unwrap();
355
356        let permit = permit2
357            .get_permit(&spender, &anvil_account, &token, &amount)
358            .unwrap();
359        let sol_permit: PermitSingle =
360            PermitSingle::try_from(&permit).expect("Failed to convert to PermitSingle");
361
362        let signature = sign_permit(eth_chain().id(), &permit, signer).unwrap();
363        let encoded =
364            (bytes_to_address(&anvil_account).unwrap(), sol_permit, signature.as_bytes().to_vec())
365                .abi_encode();
366
367        let function_signature =
368            "permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)";
369        let data = encode_input(function_signature, encoded.to_vec());
370
371        let tx = TransactionRequest {
372            to: Some(TxKind::from(permit2.address)),
373            input: TransactionInput { input: Some(AlloyBytes::from(data)), data: None },
374            gas: Some(10_000_000u64),
375            ..Default::default()
376        };
377
378        let result = permit2.runtime_handle.block_on(async {
379            let pending_tx = permit2
380                .client
381                .send_transaction(tx)
382                .await
383                .unwrap();
384            pending_tx.get_receipt().await.unwrap()
385        });
386        assert!(result.status(), "Permit transaction failed");
387
388        // Assert that the allowance was set correctly in the permit2 contract
389        let (allowance_amount, _, nonce) = permit2
390            .get_existing_allowance(&anvil_account, &spender, &token)
391            .unwrap();
392        assert_eq!(allowance_amount, U160::from(biguint_to_u256(&amount)));
393        assert_eq!(nonce, U48::from(1));
394    }
395}