tycho_execution/encoding/evm/approvals/
permit2.rs1use std::{str::FromStr, sync::Arc};
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::{
13 runtime::{Handle, Runtime},
14 task::block_in_place,
15};
16use tycho_common::Bytes;
17
18use crate::encoding::{
19 errors::EncodingError,
20 evm::{
21 encoding_utils::encode_input,
22 utils::{biguint_to_u256, bytes_to_address, get_client, get_runtime, EVMProvider},
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)]
39 runtime: Option<Arc<Runtime>>,
40}
41
42type Allowance = (U160, U48, U48);
45const PERMIT_EXPIRATION: u64 = 30 * 24 * 60 * 60;
47const PERMIT_SIG_EXPIRATION: u64 = 30 * 60;
49
50sol! {
51 #[derive(Debug)]
52 struct PermitSingle {
53 PermitDetails details;
54 address spender;
55 uint256 sigDeadline;
56 }
57
58 #[derive(Debug)]
59 struct PermitDetails {
60 address token;
61 uint160 amount;
62 uint48 expiration;
63 uint48 nonce;
64 }
65}
66
67impl TryFrom<&PermitSingle> for models::PermitSingle {
68 type Error = EncodingError;
69
70 fn try_from(sol: &PermitSingle) -> Result<Self, EncodingError> {
71 Ok(models::PermitSingle {
72 details: models::PermitDetails {
73 token: Bytes::from(sol.details.token.to_vec()),
74 amount: BigUint::from_bytes_be(&sol.details.amount.to_be_bytes::<20>()),
75 expiration: BigUint::from_bytes_be(
76 &sol.details
77 .expiration
78 .to_be_bytes::<6>(),
79 ),
80 nonce: BigUint::from_bytes_be(&sol.details.nonce.to_be_bytes::<6>()),
81 },
82 spender: Bytes::from(sol.spender.to_vec()),
83 sig_deadline: BigUint::from_bytes_be(&sol.sigDeadline.to_be_bytes::<32>()),
84 })
85 }
86}
87
88impl TryFrom<&models::PermitSingle> for PermitSingle {
89 type Error = EncodingError;
90
91 fn try_from(p: &models::PermitSingle) -> Result<Self, EncodingError> {
92 Ok(PermitSingle {
93 details: PermitDetails {
94 token: bytes_to_address(&p.details.token)?,
95 amount: U160::from(biguint_to_u256(&p.details.amount)),
96 expiration: U48::from(biguint_to_u256(&p.details.expiration)),
97 nonce: U48::from(biguint_to_u256(&p.details.nonce)),
98 },
99 spender: bytes_to_address(&p.spender)?,
100 sigDeadline: biguint_to_u256(&p.sig_deadline),
101 })
102 }
103}
104
105impl Permit2 {
106 pub fn new() -> Result<Self, EncodingError> {
107 let (handle, runtime) = get_runtime()?;
108 let client = block_in_place(|| handle.block_on(get_client()))?;
109 Ok(Self {
110 address: Address::from_str("0x000000000022D473030F116dDEE9F6B43aC78BA3")
111 .map_err(|_| EncodingError::FatalError("Permit2 address not valid".to_string()))?,
112 client,
113 runtime_handle: handle,
114 runtime,
115 })
116 }
117
118 fn get_existing_allowance(
120 &self,
121 owner: &Bytes,
122 spender: &Bytes,
123 token: &Bytes,
124 ) -> Result<Allowance, EncodingError> {
125 let args = (bytes_to_address(owner)?, bytes_to_address(token)?, bytes_to_address(spender)?);
126 let data = encode_input("allowance(address,address,address)", args.abi_encode());
127 let tx = TransactionRequest {
128 to: Some(TxKind::from(self.address)),
129 input: TransactionInput { input: Some(AlloyBytes::from(data)), data: None },
130 ..Default::default()
131 };
132
133 let output = block_in_place(|| {
134 self.runtime_handle
135 .block_on(async { self.client.call(tx).await })
136 });
137 match output {
138 Ok(response) => {
139 let allowance: Allowance = Allowance::abi_decode(&response).map_err(|_| {
140 EncodingError::FatalError(
141 "Failed to decode response for permit2 allowance".to_string(),
142 )
143 })?;
144 Ok(allowance)
145 }
146 Err(err) => Err(EncodingError::RecoverableError(format!(
147 "Call to permit2 allowance method failed with error: {err}"
148 ))),
149 }
150 }
151 pub fn get_permit(
153 &self,
154 spender: &Bytes,
155 owner: &Bytes,
156 token: &Bytes,
157 amount: &BigUint,
158 ) -> Result<models::PermitSingle, EncodingError> {
159 let current_time = Utc::now()
160 .naive_utc()
161 .and_utc()
162 .timestamp() as u64;
163
164 let (_, _, nonce) = self.get_existing_allowance(owner, spender, token)?;
165 let expiration = U48::from(current_time + PERMIT_EXPIRATION);
166 let sig_deadline = U256::from(current_time + PERMIT_SIG_EXPIRATION);
167 let amount = U160::from(biguint_to_u256(amount));
168
169 let details = PermitDetails { token: bytes_to_address(token)?, amount, expiration, nonce };
170
171 let permit_single = PermitSingle {
172 details,
173 spender: bytes_to_address(spender)?,
174 sigDeadline: sig_deadline,
175 };
176
177 models::PermitSingle::try_from(&permit_single)
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use std::str::FromStr;
184
185 use alloy::{
186 primitives::{Uint, B256},
187 signers::local::PrivateKeySigner,
188 };
189 use num_bigint::BigUint;
190 use tycho_common::models::Chain;
191
192 use super::*;
193 use crate::encoding::evm::encoding_utils::sign_permit;
194
195 impl PartialEq for PermitSingle {
198 fn eq(&self, other: &Self) -> bool {
199 if self.details != other.details {
200 return false;
201 }
202 if self.spender != other.spender {
203 return false;
204 }
205 true
206 }
207 }
208
209 impl PartialEq for PermitDetails {
210 fn eq(&self, other: &Self) -> bool {
211 if self.token != other.token {
212 return false;
213 }
214 if self.amount != other.amount {
215 return false;
216 }
217 if self.nonce != other.nonce {
219 return false;
220 }
221
222 true
223 }
224 }
225
226 fn eth_chain() -> Chain {
227 Chain::Ethereum
228 }
229
230 #[test]
231 fn test_get_existing_allowance() {
232 let manager = Permit2::new().unwrap();
233
234 let token = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
235 let owner = Bytes::from_str("0x2c6a3cd97c6283b95ac8c5a4459ebb0d5fd404f4").unwrap();
236 let spender = Bytes::from_str("0xba12222222228d8ba445958a75a0704d566bf2c8").unwrap();
237
238 let result = manager
239 .get_existing_allowance(&owner, &spender, &token)
240 .unwrap();
241 assert_eq!(
242 result,
243 (Uint::<160, 3>::from(0), Uint::<48, 1>::from(0), Uint::<48, 1>::from(0))
244 );
245 }
246
247 #[test]
248 fn test_get_permit() {
249 let permit2 = Permit2::new().expect("Failed to create Permit2");
250
251 let owner = Bytes::from_str("0x2c6a3cd97c6283b95ac8c5a4459ebb0d5fd404f4").unwrap();
252 let spender = Bytes::from_str("0xba12222222228d8ba445958a75a0704d566bf2c8").unwrap();
253 let token = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
254 let amount = BigUint::from(1000u64);
255
256 let permit = permit2
257 .get_permit(&spender, &owner, &token, &amount)
258 .unwrap();
259
260 let expected_details = models::PermitDetails {
261 token,
262 amount,
263 expiration: BigUint::from(Utc::now().timestamp() as u64 + PERMIT_EXPIRATION),
264 nonce: BigUint::from(0u64),
265 };
266 let expected_permit_single = models::PermitSingle {
267 details: expected_details,
268 spender: Bytes::from_str("0xba12222222228d8ba445958a75a0704d566bf2c8").unwrap(),
269 sig_deadline: BigUint::from(Utc::now().timestamp() as u64 + PERMIT_SIG_EXPIRATION),
270 };
271
272 assert_eq!(
273 permit, expected_permit_single,
274 "Decoded PermitSingle does not match expected values"
275 );
276 }
277
278 #[test]
283 #[cfg_attr(not(feature = "fork-tests"), ignore)]
284 fn test_permit() {
285 let anvil_account = Bytes::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266").unwrap();
286 let anvil_private_key =
287 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".to_string();
288
289 let pk = B256::from_str(&anvil_private_key)
290 .map_err(|_| {
291 EncodingError::FatalError(
292 "Failed to convert swapper private key to B256".to_string(),
293 )
294 })
295 .unwrap();
296 let signer = PrivateKeySigner::from_bytes(&pk)
297 .map_err(|_| {
298 EncodingError::FatalError("Failed to create signer from private key".to_string())
299 })
300 .unwrap();
301 let permit2 = Permit2::new().expect("Failed to create Permit2");
302
303 let token = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
304 let amount = BigUint::from(1000u64);
305
306 let approve_function_signature = "approve(address,uint256)";
308 let args = (permit2.address, biguint_to_u256(&BigUint::from(1000000u64)));
309 let data = encode_input(approve_function_signature, args.abi_encode());
310
311 let tx = TransactionRequest {
312 to: Some(TxKind::from(bytes_to_address(&token).unwrap())),
313 input: TransactionInput { input: Some(AlloyBytes::from(data)), data: None },
314 ..Default::default()
315 };
316 let receipt = block_in_place(|| {
317 permit2.runtime_handle.block_on(async {
318 let pending_tx = permit2
319 .client
320 .send_transaction(tx)
321 .await
322 .unwrap();
323 pending_tx.get_receipt().await.unwrap()
325 })
326 });
327 assert!(receipt.status(), "Approve transaction failed");
328
329 let spender = Bytes::from_str("0xba12222222228d8ba445958a75a0704d566bf2c8").unwrap();
330
331 let permit = permit2
332 .get_permit(&spender, &anvil_account, &token, &amount)
333 .unwrap();
334 let sol_permit: PermitSingle =
335 PermitSingle::try_from(&permit).expect("Failed to convert to PermitSingle");
336
337 let signature = sign_permit(eth_chain().id(), &permit, signer).unwrap();
338 let encoded =
339 (bytes_to_address(&anvil_account).unwrap(), sol_permit, signature.as_bytes().to_vec())
340 .abi_encode();
341
342 let function_signature =
343 "permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)";
344 let data = encode_input(function_signature, encoded.to_vec());
345
346 let tx = TransactionRequest {
347 to: Some(TxKind::from(permit2.address)),
348 input: TransactionInput { input: Some(AlloyBytes::from(data)), data: None },
349 gas: Some(10_000_000u64),
350 ..Default::default()
351 };
352
353 let result = permit2.runtime_handle.block_on(async {
354 let pending_tx = permit2
355 .client
356 .send_transaction(tx)
357 .await
358 .unwrap();
359 pending_tx.get_receipt().await.unwrap()
360 });
361 assert!(result.status(), "Permit transaction failed");
362
363 let (allowance_amount, _, nonce) = permit2
365 .get_existing_allowance(&anvil_account, &spender, &token)
366 .unwrap();
367 assert_eq!(allowance_amount, U160::from(biguint_to_u256(&amount)));
368 assert_eq!(nonce, U48::from(1));
369 }
370}