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

@@ -39,16 +39,19 @@ type Allowance = (U160, U48, U48); // (amount, expiration, nonce)
const PERMIT_EXPIRATION: u64 = 30 * 24 * 60 * 60;
/// Expiration period for signatures, set to 30 minutes (in seconds).
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! {
#[derive(PartialEq, Debug)]
#[derive(Debug)]
struct PermitSingle {
PermitDetails details;
address spender;
uint256 sigDeadline;
}
#[derive(PartialEq, Debug)]
#[derive(Debug)]
struct PermitDetails {
address token;
uint160 amount;
@@ -62,7 +65,7 @@ impl Permit2 {
pub fn new(signer: PrivateKeySigner, chain_id: ChainId) -> Result<Self, EncodingError> {
let runtime = Runtime::new()
.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 {
address: Address::from_str("0x000000000022D473030F116dDEE9F6B43aC78BA3")
.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.
fn get_allowance_data(
fn get_existing_allowance(
&self,
owner: &Bytes,
spender: &Bytes,
@@ -120,26 +123,32 @@ impl UserApprovalsManager for Permit2 {
for approval in approvals {
let (_, _, nonce) =
self.get_allowance_data(&approval.owner, &approval.spender, &approval.token)?;
let expiration = current_time + PERMIT_EXPIRATION;
let sig_deadline = current_time + PERMIT_SIG_EXPIRATION;
self.get_existing_allowance(&approval.owner, &approval.spender, &approval.token)?;
let expiration = U48::from(current_time + PERMIT_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 {
token: bytes_to_address(&approval.token)?,
amount: U160::from(biguint_to_u256(&approval.amount)),
expiration: U48::from(expiration),
amount,
expiration,
nonce,
};
let permit_single = PermitSingle {
details,
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! {
name: "Permit",
name: "Permit2",
chain_id: self.chain_id,
verifying_contract: self.address,
};
@@ -153,8 +162,9 @@ impl UserApprovalsManager for Permit2 {
e
))
})?;
encoded.extend(signature.as_bytes());
let encoded =
(bytes_to_address(&approval.owner)?, permit_single, signature.as_bytes().to_vec())
.abi_encode();
encoded_approvals.push(encoded);
}
@@ -170,6 +180,38 @@ mod tests {
use num_bigint::BigUint;
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]
fn test_get_allowance_data() {
let signer = PrivateKeySigner::random();
@@ -180,7 +222,7 @@ mod tests {
let spender = Bytes::from_str("0xba12222222228d8ba445958a75a0704d566bf2c8").unwrap();
let result = manager
.get_allowance_data(&owner, &spender, &token)
.get_existing_allowance(&owner, &spender, &token)
.unwrap();
assert_eq!(
result,
@@ -210,12 +252,8 @@ mod tests {
let encoded = &encoded_approvals[0];
// Calculate the PermitSingle ABI-encoded length
let permit_details_length = 32 + 32 + 32 + 32; // token + amount + expiration + nonce
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");
// Remove prefix and owner (first 64 bytes) and signature (last 65 bytes)
let permit_single_encoded = &encoded[64..encoded.len() - 65];
let decoded_permit_single = PermitSingle::abi_decode(permit_single_encoded, false)
.expect("Failed to decode PermitSingle");
@@ -237,4 +275,87 @@ mod tests {
"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));
}
}