Skip to content

Commit 84a3981

Browse files
codybornsnreynolds
andauthored
Permit2 with Native (#227)
* Test for PermitTransferFrom * permitWitnessTransferFrom test * remove verification lib * Move signPermitTransfer to TestKeyManager * try permit2 import * rm accidental change * Use permit2 submodule * Update test util * Add 7739 support * Refactor test logic * WIP * test passing * WIP * 7739 with nativeETH permit2 * Update comments * Respond to comments --------- Co-authored-by: Sara Reynolds <[email protected]>
1 parent a87011a commit 84a3981

File tree

6 files changed

+380
-1
lines changed

6 files changed

+380
-1
lines changed

.gitmodules

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,9 @@
1313
[submodule "lib/account-abstraction"]
1414
path = lib/account-abstraction
1515
url = https://github.com/eth-infinitism/account-abstraction
16+
[submodule "lib/erc20-eth"]
17+
path = lib/erc20-eth
18+
url = https://github.com/Uniswap/ERC20-eth
19+
[submodule "lib/permit2"]
20+
path = lib/permit2
21+
url = https://github.com/Uniswap/permit2

lib/erc20-eth

Submodule erc20-eth added at 17d7b9c

lib/permit2

Submodule permit2 added at cc56ad0

test/ERC7739.t.sol

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,27 @@ import {KeyType, Key, KeyLib} from "../src/libraries/KeyLib.sol";
1212
import {TypedDataSignBuilder} from "./utils/TypedDataSignBuilder.sol";
1313
import {FFISignTypedData} from "./utils/FFISignTypedData.sol";
1414
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";
1520

1621
contract ERC7739Test is DelegationHandler, TokenHandler, ERC1271Handler, FFISignTypedData {
1722
using TestKeyManager for TestKey;
1823
using TypedDataSignBuilder for *;
1924
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;
2030

2131
function setUp() public {
2232
setUpDelegation();
2333
setUpTokens();
34+
// Deploy permit2 for actual permit transfers
35+
permit2 = IAllowanceTransfer(Permit2Utils.deployPermit2());
2436
}
2537

2638
function test_signPersonalSign_matches_signWrappedPersonalSign_ffi() public {
@@ -112,4 +124,165 @@ contract ERC7739Test is DelegationHandler, TokenHandler, ERC1271Handler, FFISign
112124
// Assert that the ffi generated signature is NOT the same as the locally generated signature
113125
assertNotEq(signature, key.sign(typedDataSignDigest));
114126
}
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+
}
115288
}

test/ERC7914.t.sol

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,24 @@ import {DelegationHandler} from "./utils/DelegationHandler.sol";
55
import {IERC7914} from "../src/interfaces/IERC7914.sol";
66
import {ERC7914} from "../src/ERC7914.sol";
77
import {BaseAuthorization} from "../src/BaseAuthorization.sol";
8+
import {IPermit2} from "../lib/permit2/src/interfaces/IPermit2.sol";
9+
import {ISignatureTransfer} from "../lib/permit2/src/interfaces/ISignatureTransfer.sol";
10+
import {ERC20ETH} from "../lib/erc20-eth/src/ERC20Eth.sol";
11+
import {IAllowanceTransfer} from "../lib/permit2/src/interfaces/IAllowanceTransfer.sol";
12+
import {Permit2Utils} from "./utils/Permit2Utils.sol";
13+
import {TestKeyManager, TestKey} from "./utils/TestKeyManager.sol";
14+
import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol";
15+
import {TypedDataSignBuilder} from "./utils/TypedDataSignBuilder.sol";
16+
import {KeyType} from "../src/libraries/KeyLib.sol";
17+
import {FFISignTypedData} from "./utils/FFISignTypedData.sol";
18+
import {ERC1271Handler} from "./utils/ERC1271Handler.sol";
19+
import {PermitSingle, PermitDetails} from "./utils/MockERC1271VerifyingContract.sol";
20+
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
21+
22+
contract ERC7914Test is DelegationHandler, ERC1271Handler, FFISignTypedData {
23+
using Permit2Utils for *;
24+
using TestKeyManager for TestKey;
825

9-
contract ERC7914Test is DelegationHandler {
1026
event TransferFromNative(address indexed from, address indexed to, uint256 value);
1127
event ApproveNative(address indexed owner, address indexed spender, uint256 value);
1228
event ApproveNativeTransient(address indexed owner, address indexed spender, uint256 value);
@@ -16,6 +32,79 @@ contract ERC7914Test is DelegationHandler {
1632
address bob = makeAddr("bob");
1733
address recipient = makeAddr("recipient");
1834

35+
struct Permit2TestSetup {
36+
ERC20ETH erc20Eth;
37+
IPermit2 permit2;
38+
uint256 spendAmount;
39+
uint256 totalAmount;
40+
}
41+
42+
function _setupPermit2Test() internal returns (Permit2TestSetup memory setup) {
43+
setup.erc20Eth = new ERC20ETH();
44+
setup.permit2 = IPermit2(Permit2Utils.deployPermit2());
45+
setup.spendAmount = 1 ether;
46+
setup.totalAmount = 2 ether;
47+
48+
vm.deal(address(signerAccount), setup.totalAmount);
49+
vm.prank(address(signerAccount));
50+
signerAccount.approveNative(address(setup.erc20Eth), type(uint256).max);
51+
}
52+
53+
function _createPermit(address token, uint256 amount) internal view returns (ISignatureTransfer.PermitTransferFrom memory) {
54+
return ISignatureTransfer.PermitTransferFrom({
55+
permitted: ISignatureTransfer.TokenPermissions({
56+
token: token,
57+
amount: amount
58+
}),
59+
nonce: 0,
60+
deadline: block.timestamp + 1 hours
61+
});
62+
}
63+
64+
function _testPermit2Transfer(
65+
IPermit2 permit2,
66+
ISignatureTransfer.PermitTransferFrom memory permit,
67+
bytes memory sig,
68+
uint256 spendAmount,
69+
uint256 totalAmount,
70+
bool isWitness,
71+
bytes32 witness,
72+
string memory witnessTypeString
73+
) internal {
74+
// Test invalid transfer (too much)
75+
ISignatureTransfer.SignatureTransferDetails memory invalidTransfer =
76+
ISignatureTransfer.SignatureTransferDetails({
77+
to: bob,
78+
requestedAmount: spendAmount + 1
79+
});
80+
81+
vm.expectRevert(abi.encodeWithSelector(ISignatureTransfer.InvalidAmount.selector, spendAmount));
82+
vm.prank(bob);
83+
if (isWitness) {
84+
permit2.permitWitnessTransferFrom(permit, invalidTransfer, address(signerAccount), witness, witnessTypeString, sig);
85+
} else {
86+
permit2.permitTransferFrom(permit, invalidTransfer, address(signerAccount), sig);
87+
}
88+
89+
// Test valid transfer
90+
ISignatureTransfer.SignatureTransferDetails memory validTransfer =
91+
ISignatureTransfer.SignatureTransferDetails({
92+
to: bob,
93+
requestedAmount: spendAmount
94+
});
95+
96+
vm.prank(bob);
97+
if (isWitness) {
98+
permit2.permitWitnessTransferFrom(permit, validTransfer, address(signerAccount), witness, witnessTypeString, sig);
99+
} else {
100+
permit2.permitTransferFrom(permit, validTransfer, address(signerAccount), sig);
101+
}
102+
103+
// Verify the transfer
104+
assertEq(bob.balance, spendAmount);
105+
assertEq(address(signerAccount).balance, totalAmount - spendAmount);
106+
}
107+
19108
function setUp() public {
20109
setUpDelegation();
21110
}
@@ -246,4 +335,38 @@ contract ERC7914Test is DelegationHandler {
246335
assertEq(address(signerAccount).balance, signerAccountBalanceBefore);
247336
}
248337
}
338+
339+
// Test that a permit2 signature can be used to transfer native ETH
340+
// using the ERC20-eth contract
341+
function test_permit2SignatureTransferNative() public {
342+
Permit2TestSetup memory setup = _setupPermit2Test();
343+
ISignatureTransfer.PermitTransferFrom memory permit = _createPermit(address(setup.erc20Eth), setup.spendAmount);
344+
345+
bytes memory sig;
346+
{
347+
(bytes32 appDomainSeparator, , bytes32 contentsHash) = Permit2Utils.getPermit2Fixtures(permit, bob, address(setup.permit2));
348+
349+
bytes32 msgHash = MessageHashUtils.toTypedDataHash(appDomainSeparator, contentsHash);
350+
sig = signerTestKey.sign(msgHash);
351+
}
352+
_testPermit2Transfer(setup.permit2, permit, sig, setup.spendAmount, setup.totalAmount, false, bytes32(0), "");
353+
}
354+
355+
// Test that a permit2 witness signature can be used to transfer native ETH
356+
// using the ERC20-eth contract
357+
function test_permit2WitnessSignatureTransferNative() public {
358+
Permit2TestSetup memory setup = _setupPermit2Test();
359+
Permit2Utils.MockWitness memory witnessData = Permit2Utils.MockWitness(10000000, address(5), true);
360+
bytes32 witness = keccak256(abi.encode(witnessData));
361+
ISignatureTransfer.PermitTransferFrom memory permit = _createPermit(address(setup.erc20Eth), setup.spendAmount);
362+
363+
bytes memory sig;
364+
{
365+
(bytes32 appDomainSeparator, , bytes32 contentsHash) = Permit2Utils.getPermit2WitnessFixtures(permit, witness, bob, address(setup.permit2));
366+
367+
bytes32 msgHash = MessageHashUtils.toTypedDataHash(appDomainSeparator, contentsHash);
368+
sig = signerTestKey.sign(msgHash);
369+
}
370+
_testPermit2Transfer(setup.permit2, permit, sig, setup.spendAmount, setup.totalAmount, true, witness, Permit2Utils.WITNESS_TYPE_STRING);
371+
}
249372
}

test/utils/Permit2Utils.sol

Lines changed: 75 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)