@@ -12,15 +12,27 @@ import {KeyType, Key, KeyLib} from "../src/libraries/KeyLib.sol";
12
12
import {TypedDataSignBuilder} from "./utils/TypedDataSignBuilder.sol " ;
13
13
import {FFISignTypedData} from "./utils/FFISignTypedData.sol " ;
14
14
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol " ;
15
+ import {Permit2Utils} from "./utils/Permit2Utils.sol " ;
16
+ import {IPermit2} from "../lib/permit2/src/interfaces/IPermit2.sol " ;
17
+ import {IAllowanceTransfer} from "../lib/permit2/src/interfaces/IAllowanceTransfer.sol " ;
18
+ import {PermitHash} from "../lib/permit2/src/libraries/PermitHash.sol " ;
19
+ import {ERC20ETH } from "../lib/erc20-eth/src/ERC20Eth.sol " ;
15
20
16
21
contract ERC7739Test is DelegationHandler , TokenHandler , ERC1271Handler , FFISignTypedData {
17
22
using TestKeyManager for TestKey;
18
23
using TypedDataSignBuilder for * ;
19
24
using KeyLib for Key;
25
+ using Permit2Utils for * ;
26
+ using PermitHash for IAllowanceTransfer.PermitSingle;
27
+
28
+ IAllowanceTransfer public permit2;
29
+ bytes4 private constant _1271_MAGIC_VALUE = 0x1626ba7e ;
20
30
21
31
function setUp () public {
22
32
setUpDelegation ();
23
33
setUpTokens ();
34
+ // Deploy permit2 for actual permit transfers
35
+ permit2 = IAllowanceTransfer (Permit2Utils.deployPermit2 ());
24
36
}
25
37
26
38
function test_signPersonalSign_matches_signWrappedPersonalSign_ffi () public {
@@ -112,4 +124,165 @@ contract ERC7739Test is DelegationHandler, TokenHandler, ERC1271Handler, FFISign
112
124
// Assert that the ffi generated signature is NOT the same as the locally generated signature
113
125
assertNotEq (signature, key.sign (typedDataSignDigest));
114
126
}
127
+
128
+ function test_signTypedSignData_permitSingleTransfer () public {
129
+ // Register a test key with the signer account
130
+ uint256 testPrivateKey = 0x123456 ;
131
+ TestKey memory testKey = TestKeyManager.withSeed (KeyType.Secp256k1, testPrivateKey);
132
+ vm.prank (address (signerAccount));
133
+ signerAccount.register (testKey.toKey ());
134
+
135
+ // Give the signer account some tokens to permit
136
+ uint256 initialBalance = 10000 ;
137
+ deal (address (tokenA), address (signerAccount), initialBalance);
138
+
139
+ // Approve permit2 to spend tokens on behalf of the signer account
140
+ vm.prank (address (signerAccount));
141
+ tokenA.approve (address (permit2), type (uint256 ).max);
142
+
143
+ // Create a PermitSingle for permit2 (using permit2's actual domain)
144
+ uint160 allowanceAmount = 1000 ;
145
+ IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer.PermitSingle ({
146
+ details: IAllowanceTransfer.PermitDetails ({
147
+ token: address (tokenA),
148
+ amount: allowanceAmount,
149
+ expiration: uint48 (block .timestamp + 1 hours),
150
+ nonce: 0
151
+ }),
152
+ spender: address (this ),
153
+ sigDeadline: block .timestamp + 1 hours
154
+ });
155
+
156
+ // Generate the permit2 domain separator and hash
157
+ bytes32 permit2DomainSeparator = permit2.DOMAIN_SEPARATOR ();
158
+ bytes32 permitHash = permitSingle.hash ();
159
+
160
+ // Build ERC-7739 TypedDataSign signature for permit2
161
+ string memory contentsDescr = "PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitSingle " ;
162
+ (string memory contentsName , string memory contentsType ) = mockERC7739Utils.decodeContentsDescr (contentsDescr);
163
+
164
+ bytes memory signerAccountDomainBytes = IERC5267 (address (signerAccount)).toDomainBytes ();
165
+ bytes32 typedDataSignDigest = permitHash.hashTypedDataSign (signerAccountDomainBytes, permit2DomainSeparator, contentsName, contentsType);
166
+
167
+ // Sign the digest with the test key
168
+ bytes memory signature = testKey.sign (typedDataSignDigest);
169
+
170
+ // Build the TypedDataSign signature for ERC-7739
171
+ bytes memory typedDataSignSignature = TypedDataSignBuilder.buildTypedDataSignSignature (signature, permit2DomainSeparator, permitHash, contentsDescr);
172
+
173
+ // Wrap the signature with keyHash and empty hook data
174
+ bytes memory wrappedSignature = abi.encode (testKey.toKeyHash (), typedDataSignSignature, bytes ("" ));
175
+
176
+ // Test by calling permit2.permit() which will internally call isValidSignature() on the signer account
177
+ // This should succeed without reverting, proving the signature is valid
178
+ permit2.permit (address (signerAccount), permitSingle, wrappedSignature);
179
+
180
+ // Verify the allowance was actually set
181
+ (uint160 allowance , uint48 expiration , uint48 nonce ) = permit2.allowance (address (signerAccount), address (tokenA), address (this ));
182
+ assertEq (allowance, allowanceAmount);
183
+ assertEq (expiration, uint48 (block .timestamp + 1 hours));
184
+ assertEq (nonce, 1 ); // nonce should be incremented after permit
185
+ }
186
+
187
+ function test_signTypedSignData_permitSingleTransfer_transferNative () public {
188
+ // Register a test key with the signer account
189
+ uint256 testPrivateKey = 0x123456 ;
190
+ TestKey memory testKey = TestKeyManager.withSeed (KeyType.Secp256k1, testPrivateKey);
191
+ vm.prank (address (signerAccount));
192
+ signerAccount.register (testKey.toKey ());
193
+
194
+ // Deploy ERC20ETH contract for native ETH transfers
195
+ ERC20ETH erc20Eth = new ERC20ETH ();
196
+
197
+ // Give the signer account some ETH
198
+ uint256 initialEthBalance = 10 ether ;
199
+ vm.deal (address (signerAccount), initialEthBalance);
200
+
201
+ // Approve ERC20ETH to spend native ETH on behalf of the signer account
202
+ vm.prank (address (signerAccount));
203
+ signerAccount.approveNative (address (erc20Eth), type (uint256 ).max);
204
+
205
+ // Create a PermitSingle for permit2 using ERC20ETH
206
+ uint160 allowanceAmount = 1 ether ;
207
+ IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer.PermitSingle ({
208
+ details: IAllowanceTransfer.PermitDetails ({
209
+ token: address (erc20Eth),
210
+ amount: allowanceAmount,
211
+ expiration: uint48 (block .timestamp + 1 hours),
212
+ nonce: 0
213
+ }),
214
+ spender: address (this ),
215
+ sigDeadline: block .timestamp + 1 hours
216
+ });
217
+
218
+ // Generate the permit2 domain separator and hash
219
+ bytes32 permit2DomainSeparator = permit2.DOMAIN_SEPARATOR ();
220
+ bytes32 permitHash = permitSingle.hash ();
221
+
222
+ // Build ERC-7739 TypedDataSign signature for permit2
223
+ string memory contentsDescr = "PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitSingle " ;
224
+ (string memory contentsName , string memory contentsType ) = mockERC7739Utils.decodeContentsDescr (contentsDescr);
225
+
226
+ bytes memory signerAccountDomainBytes = IERC5267 (address (signerAccount)).toDomainBytes ();
227
+ bytes32 typedDataSignDigest = permitHash.hashTypedDataSign (signerAccountDomainBytes, permit2DomainSeparator, contentsName, contentsType);
228
+
229
+ // Sign the digest with the test key
230
+ bytes memory signature = testKey.sign (typedDataSignDigest);
231
+
232
+ // Build the TypedDataSign signature for ERC-7739
233
+ bytes memory typedDataSignSignature = TypedDataSignBuilder.buildTypedDataSignSignature (signature, permit2DomainSeparator, permitHash, contentsDescr);
234
+
235
+ // Wrap the signature with keyHash and empty hook data
236
+ bytes memory wrappedSignature = abi.encode (testKey.toKeyHash (), typedDataSignSignature, bytes ("" ));
237
+
238
+ // Test by calling permit2.permit() which will internally call isValidSignature() on the signer account
239
+ // This should succeed without reverting, proving the signature is valid
240
+ permit2.permit (address (signerAccount), permitSingle, wrappedSignature);
241
+
242
+ // Verify the allowance was actually set
243
+ (uint160 allowance , uint48 expiration , uint48 nonce ) = permit2.allowance (address (signerAccount), address (erc20Eth), address (this ));
244
+ assertEq (allowance, allowanceAmount);
245
+ assertEq (expiration, uint48 (block .timestamp + 1 hours));
246
+ assertEq (nonce, 1 ); // nonce should be incremented after permit
247
+
248
+ // Now verify the spender can actually pull the ETH
249
+ address recipient = address (0xBEEF );
250
+ uint160 transferAmount = 0.5 ether ;
251
+
252
+ // Check initial ETH balances
253
+ uint256 signerEthBalanceBefore = address (signerAccount).balance;
254
+ uint256 recipientEthBalanceBefore = recipient.balance;
255
+
256
+ // Verify wrong spender cannot transfer ETH
257
+ address wrongSpender = address (0xBAD );
258
+ vm.expectRevert (); // Should revert with insufficient allowance
259
+ vm.prank (wrongSpender);
260
+ permit2.transferFrom (address (signerAccount), wrongSpender, 1 , address (erc20Eth));
261
+
262
+ // Verify correct spender cannot exceed allowance limit
263
+ vm.expectRevert (); // Should revert with insufficient allowance
264
+ permit2.transferFrom (address (signerAccount), recipient, allowanceAmount + 1 , address (erc20Eth));
265
+
266
+ // Transfer ETH using the permit
267
+ permit2.transferFrom (address (signerAccount), recipient, transferAmount, address (erc20Eth));
268
+
269
+ // Verify ETH balances changed correctly
270
+ assertEq (address (signerAccount).balance, signerEthBalanceBefore - transferAmount);
271
+ assertEq (recipient.balance, recipientEthBalanceBefore + transferAmount);
272
+
273
+ // Verify allowance was decremented
274
+ (uint160 allowanceAfter ,,) = permit2.allowance (address (signerAccount), address (erc20Eth), address (this ));
275
+ assertEq (allowanceAfter, allowanceAmount - transferAmount);
276
+
277
+ // Transfer remaining allowed ETH
278
+ permit2.transferFrom (address (signerAccount), recipient, allowanceAmount - transferAmount, address (erc20Eth));
279
+
280
+ // Verify final ETH balances
281
+ assertEq (address (signerAccount).balance, initialEthBalance - allowanceAmount);
282
+ assertEq (recipient.balance, allowanceAmount);
283
+
284
+ // Verify allowance is now zero
285
+ (uint160 finalAllowance ,,) = permit2.allowance (address (signerAccount), address (erc20Eth), address (this ));
286
+ assertEq (finalAllowance, 0 );
287
+ }
115
288
}
0 commit comments