tycho_execution/encoding/evm/approvals/
permit2.rs1use 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#[derive(Clone)]
30pub struct Permit2 {
31 address: Address,
32 client: EVMProvider,
33 runtime_handle: Handle,
34 #[allow(dead_code)]
35 runtime: SafeRuntime,
36}
37
38type Allowance = (U160, U48, U48);
41const PERMIT_EXPIRATION: u64 = 30 * 24 * 60 * 60;
43const 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 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 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 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 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 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 #[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 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 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 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}