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:
@@ -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
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user