fix: Correct encoding of the approvals

- Add data validations similar to the ones done in the Permit2 SDK
- Fix Domain main (!!!) It's Permit2 not Permit
- Return the whole function signature data (owner, permit_single, signature) encoded

test improvements:
- Don't compare the timestamps, this was making the test fail sometimes and pass other times
- Add a test to run on an anvil fork and actually call permit2 contract to double check the encoded data works

misc:
Rename get_allowance_data -> get_existing_allowance

--- don't change below this line ---
ENG-4063 Took 5 hours 19 minutes


Took 11 seconds
This commit is contained in:
Diana Carvalho
2025-01-23 11:21:06 +00:00
parent ce9ae49e6f
commit 04e925fe81
2 changed files with 143 additions and 21 deletions

View File

@@ -26,6 +26,7 @@ rstest = "0.24.0"
[features] [features]
default = ["evm"] default = ["evm"]
evm = ["alloy", "alloy-sol-types", "alloy-primitives"] evm = ["alloy", "alloy-sol-types", "alloy-primitives"]
fork-tests = []
[profile.bench] [profile.bench]
debug = true debug = true

View File

@@ -39,16 +39,19 @@ type Allowance = (U160, U48, U48); // (amount, expiration, nonce)
const PERMIT_EXPIRATION: u64 = 30 * 24 * 60 * 60; const PERMIT_EXPIRATION: u64 = 30 * 24 * 60 * 60;
/// Expiration period for signatures, set to 30 minutes (in seconds). /// Expiration period for signatures, set to 30 minutes (in seconds).
const PERMIT_SIG_EXPIRATION: u64 = 30 * 60; const PERMIT_SIG_EXPIRATION: u64 = 30 * 60;
const MAX_UINT48: U48 = U48::MAX;
const MAX_UINT160: U160 = U160::MAX;
const MAX_UINT256: U256 = U256::MAX;
sol! { sol! {
#[derive(PartialEq, Debug)] #[derive(Debug)]
struct PermitSingle { struct PermitSingle {
PermitDetails details; PermitDetails details;
address spender; address spender;
uint256 sigDeadline; uint256 sigDeadline;
} }
#[derive(PartialEq, Debug)] #[derive(Debug)]
struct PermitDetails { struct PermitDetails {
address token; address token;
uint160 amount; uint160 amount;
@@ -62,7 +65,7 @@ impl Permit2 {
pub fn new(signer: PrivateKeySigner, chain_id: ChainId) -> Result<Self, EncodingError> { pub fn new(signer: PrivateKeySigner, chain_id: ChainId) -> Result<Self, EncodingError> {
let runtime = Runtime::new() let runtime = Runtime::new()
.map_err(|_| EncodingError::FatalError("Failed to create runtime".to_string()))?; .map_err(|_| EncodingError::FatalError("Failed to create runtime".to_string()))?;
let client = runtime.block_on(get_client()); let client = runtime.block_on(get_client())?;
Ok(Self { Ok(Self {
address: Address::from_str("0x000000000022D473030F116dDEE9F6B43aC78BA3") address: Address::from_str("0x000000000022D473030F116dDEE9F6B43aC78BA3")
.map_err(|_| EncodingError::FatalError("Permit2 address not valid".to_string()))?, .map_err(|_| EncodingError::FatalError("Permit2 address not valid".to_string()))?,
@@ -74,7 +77,7 @@ impl Permit2 {
} }
/// Fetches allowance data for a specific owner, spender, and token. /// Fetches allowance data for a specific owner, spender, and token.
fn get_allowance_data( fn get_existing_allowance(
&self, &self,
owner: &Bytes, owner: &Bytes,
spender: &Bytes, spender: &Bytes,
@@ -120,26 +123,32 @@ impl UserApprovalsManager for Permit2 {
for approval in approvals { for approval in approvals {
let (_, _, nonce) = let (_, _, nonce) =
self.get_allowance_data(&approval.owner, &approval.spender, &approval.token)?; self.get_existing_allowance(&approval.owner, &approval.spender, &approval.token)?;
let expiration = current_time + PERMIT_EXPIRATION; let expiration = U48::from(current_time + PERMIT_EXPIRATION);
let sig_deadline = current_time + PERMIT_SIG_EXPIRATION; let sig_deadline = U256::from(current_time + PERMIT_SIG_EXPIRATION);
let amount = U160::from(biguint_to_u256(&approval.amount));
// validations
assert!(MAX_UINT256 > sig_deadline, "signature deadline out of range");
assert!(MAX_UINT48 > nonce, "nonce out of range");
assert!(MAX_UINT160 > amount, "amount out of range");
assert!(MAX_UINT48 > expiration, "expiration out of range");
let details = PermitDetails { let details = PermitDetails {
token: bytes_to_address(&approval.token)?, token: bytes_to_address(&approval.token)?,
amount: U160::from(biguint_to_u256(&approval.amount)), amount,
expiration: U48::from(expiration), expiration,
nonce, nonce,
}; };
let permit_single = PermitSingle { let permit_single = PermitSingle {
details, details,
spender: bytes_to_address(&approval.spender)?, spender: bytes_to_address(&approval.spender)?,
sigDeadline: U256::from(sig_deadline), sigDeadline: sig_deadline,
}; };
let mut encoded = permit_single.abi_encode();
let domain = eip712_domain! { let domain = eip712_domain! {
name: "Permit", name: "Permit2",
chain_id: self.chain_id, chain_id: self.chain_id,
verifying_contract: self.address, verifying_contract: self.address,
}; };
@@ -153,8 +162,9 @@ impl UserApprovalsManager for Permit2 {
e e
)) ))
})?; })?;
let encoded =
encoded.extend(signature.as_bytes()); (bytes_to_address(&approval.owner)?, permit_single, signature.as_bytes().to_vec())
.abi_encode();
encoded_approvals.push(encoded); encoded_approvals.push(encoded);
} }
@@ -170,6 +180,38 @@ mod tests {
use num_bigint::BigUint; use num_bigint::BigUint;
use super::*; use super::*;
// These two implementations are to avoid comparing the expiration and sig_deadline fields
// because they are timestamps
impl PartialEq for PermitSingle {
fn eq(&self, other: &Self) -> bool {
if self.details != other.details {
return false;
}
if self.spender != other.spender {
return false;
}
true
}
}
impl PartialEq for PermitDetails {
fn eq(&self, other: &Self) -> bool {
if self.token != other.token {
return false;
}
if self.amount != other.amount {
return false;
}
// Compare `nonce`
if self.nonce != other.nonce {
return false;
}
true
}
}
#[test] #[test]
fn test_get_allowance_data() { fn test_get_allowance_data() {
let signer = PrivateKeySigner::random(); let signer = PrivateKeySigner::random();
@@ -180,7 +222,7 @@ mod tests {
let spender = Bytes::from_str("0xba12222222228d8ba445958a75a0704d566bf2c8").unwrap(); let spender = Bytes::from_str("0xba12222222228d8ba445958a75a0704d566bf2c8").unwrap();
let result = manager let result = manager
.get_allowance_data(&owner, &spender, &token) .get_existing_allowance(&owner, &spender, &token)
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
result, result,
@@ -210,12 +252,8 @@ mod tests {
let encoded = &encoded_approvals[0]; let encoded = &encoded_approvals[0];
// Calculate the PermitSingle ABI-encoded length // Remove prefix and owner (first 64 bytes) and signature (last 65 bytes)
let permit_details_length = 32 + 32 + 32 + 32; // token + amount + expiration + nonce let permit_single_encoded = &encoded[64..encoded.len() - 65];
let permit_single_length = permit_details_length + 32 + 32; // details + spender + sigDeadline
let (permit_single_encoded, signature_encoded) = encoded.split_at(permit_single_length);
assert_eq!(signature_encoded.len(), 65, "Expected 65 bytes for signature");
let decoded_permit_single = PermitSingle::abi_decode(permit_single_encoded, false) let decoded_permit_single = PermitSingle::abi_decode(permit_single_encoded, false)
.expect("Failed to decode PermitSingle"); .expect("Failed to decode PermitSingle");
@@ -237,4 +275,87 @@ mod tests {
"Decoded PermitSingle does not match expected values" "Decoded PermitSingle does not match expected values"
); );
} }
/// This test actually calls the permit method on the Permit2 contract to verify the encoded
/// data works. It requires an Anvil fork, so please run with the following command: anvil
/// --fork-url <RPC-URL> And set up the following env var as ETH_RPC_URL=127.0.0.1:8545
/// Use an account from anvil to fill the anvil_account and anvil_private_key variables
#[test]
#[cfg_attr(not(feature = "fork-tests"), ignore)]
fn test_permit() {
let anvil_account = Bytes::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266").unwrap();
let anvil_private_key =
B256::from_str("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")
.unwrap();
let signer =
PrivateKeySigner::from_bytes(&anvil_private_key).expect("Failed to create signer");
let permit2 = Permit2::new(signer, 1).expect("Failed to create Permit2");
let token = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let amount = BigUint::from(1000u64);
// Approve token allowance for permit2 contract
let approve_function_signature = "approve(address,uint256)";
let args = (permit2.address, biguint_to_u256(&BigUint::from(1000000u64)));
let data = encode_input(approve_function_signature, args.abi_encode());
let tx = TransactionRequest {
to: Some(TxKind::from(bytes_to_address(&token).unwrap())),
input: TransactionInput { input: Some(AlloyBytes::from(data)), data: None },
..Default::default()
};
let receipt = permit2.runtime.block_on(async {
let pending_tx = permit2
.client
.send_transaction(tx)
.await
.unwrap();
// Wait for the transaction to be mined
pending_tx.get_receipt().await.unwrap()
});
assert!(receipt.status(), "Approve transaction failed");
let spender = Bytes::from_str("0xba12222222228d8ba445958a75a0704d566bf2c8").unwrap();
let approvals = vec![Approval {
owner: anvil_account.clone(),
spender: spender.clone(),
token: token.clone(),
amount: amount.clone(),
}];
let encoded_approvals = permit2
.encode_approvals(approvals)
.unwrap();
let encoded = &encoded_approvals[0];
let function_signature =
"permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)";
let data = encode_input(function_signature, encoded.to_vec());
let tx = TransactionRequest {
to: Some(TxKind::from(permit2.address)),
input: TransactionInput { input: Some(AlloyBytes::from(data)), data: None },
gas: Some(10_000_000u64),
..Default::default()
};
let result = permit2.runtime.block_on(async {
let pending_tx = permit2
.client
.send_transaction(tx)
.await
.unwrap();
pending_tx.get_receipt().await.unwrap()
});
assert!(result.status(), "Permit transaction failed");
// Assert that the allowance was set correctly in the permit2 contract
let (allowance_amount, _, nonce) = permit2
.get_existing_allowance(&anvil_account, &spender, &token)
.unwrap();
assert_eq!(allowance_amount, U160::from(biguint_to_u256(&amount)));
assert_eq!(nonce, U48::from(1));
}
} }