Skip to content

Commit a6148e1

Browse files
[PT1-347] Feature/permit2 without witness (#400)
* Add Permit2Proxy contract and IPermit2TransferFrom interface * Updated nonce generation in tests to ensure uniqueness and avoid conflicts.
1 parent 1e88b2e commit a6148e1

File tree

7 files changed

+236
-3
lines changed

7 files changed

+236
-3
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity 0.8.30;
4+
5+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6+
7+
import "../interfaces/IPermit2TransferFrom.sol";
8+
import "./ImmutableOwner.sol";
9+
10+
/* solhint-disable func-name-mixedcase */
11+
12+
contract Permit2Proxy is ImmutableOwner {
13+
error Permit2ProxyBadSelector();
14+
15+
IPermit2TransferFrom private constant _PERMIT2 = IPermit2TransferFrom(0x000000000022D473030F116dDEE9F6B43aC78BA3);
16+
17+
constructor(address _immutableOwner) ImmutableOwner(_immutableOwner) {
18+
if (Permit2Proxy.func_nZHTch.selector != IERC20.transferFrom.selector) revert Permit2ProxyBadSelector();
19+
}
20+
21+
/// @notice Proxy transfer method for `Permit2.permitTransferFrom`. Selector must match `IERC20.transferFrom`
22+
// keccak256("func_nZHTch(address,address,uint256,((address,uint256),uint256,uint256),bytes)") == 0x23b872dd (IERC20.transferFrom)
23+
function func_nZHTch(
24+
address from,
25+
address to,
26+
uint256 amount,
27+
IPermit2TransferFrom.PermitTransferFrom calldata permit,
28+
bytes calldata sig
29+
) external onlyImmutableOwner {
30+
_PERMIT2.permitTransferFrom(
31+
permit,
32+
IPermit2TransferFrom.SignatureTransferDetails({
33+
to: to,
34+
requestedAmount: amount
35+
}),
36+
from,
37+
sig
38+
);
39+
}
40+
}
41+
42+
/* solhint-enable func-name-mixedcase */
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.0;
4+
5+
interface IPermit2TransferFrom {
6+
struct TokenPermissions {
7+
// ERC20 token address
8+
address token;
9+
// the maximum amount that can be spent
10+
uint256 amount;
11+
}
12+
13+
struct PermitTransferFrom {
14+
TokenPermissions permitted;
15+
// a unique value for every token owner's signature to prevent signature replays
16+
uint256 nonce;
17+
// deadline on the permit signature
18+
uint256 deadline;
19+
}
20+
21+
struct SignatureTransferDetails {
22+
// recipient address
23+
address to;
24+
// spender requested amount
25+
uint256 requestedAmount;
26+
}
27+
28+
function permitTransferFrom(
29+
PermitTransferFrom calldata permit,
30+
SignatureTransferDetails calldata transferDetails,
31+
address owner,
32+
bytes calldata signature
33+
) external;
34+
}

dev.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Development Guide
2+
3+
## Selector Bruteforce Tool
4+
5+
When developing proxy contracts that need specific function selectors (e.g., matching `IERC20.transferFrom` selector `0x23b872dd`), use the selector bruteforce tool from:
6+
7+
**https://github.com/1inch/smart-contract-helper-utils/src**
8+
9+
### Usage Example
10+
11+
To find a function name with custom suffix that produces the `transferFrom` selector:
12+
13+
```bash
14+
python selector_bruteforce.py \
15+
--target 0x23b872dd \
16+
--params "address,address,uint256,((address,uint256),uint256,uint256),bytes" \
17+
--prefix "func_" \
18+
--fast
19+
```
20+
21+
This is used for contracts like `Permit2Proxy` and `Permit2WitnessProxy` where the proxy function must have the same selector as `transferFrom` to work with the limit order protocol's extension mechanism.

docs/extensions/Permit2Proxy.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
## Permit2Proxy
3+
4+
A proxy contract that enables using Uniswap's Permit2 `permitTransferFrom` within the limit order protocol without the witness functionality.
5+
6+
### Functions list
7+
- [constructor(_immutableOwner) public](#constructor)
8+
- [func_nZHTch(from, to, amount, permit, sig) external](#func_nzhtch)
9+
10+
### Errors list
11+
- [Permit2ProxyBadSelector()](#permit2proxybadselector)
12+
13+
### Functions
14+
### constructor
15+
16+
```solidity
17+
constructor(address _immutableOwner) public
18+
```
19+
20+
### func_nZHTch
21+
22+
```solidity
23+
function func_nZHTch(address from, address to, uint256 amount, struct IPermit2TransferFrom.PermitTransferFrom permit, bytes sig) external
24+
```
25+
Proxy transfer method for `Permit2.permitTransferFrom`. Selector must match `IERC20.transferFrom`
26+
27+
The function name `func_nZHTch` is chosen so that its selector equals `0x23b872dd` (same as `IERC20.transferFrom`), allowing it to be used as a maker asset in limit orders.
28+
29+
### Errors
30+
### Permit2ProxyBadSelector
31+
32+
```solidity
33+
error Permit2ProxyBadSelector()
34+
```
35+
36+
Thrown in the constructor if the function selector doesn't match `IERC20.transferFrom.selector`.
37+

test/Permit2Proxy.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
const { constants, permit2Contract } = require('@1inch/solidity-utils');
2+
const { SignatureTransfer, PERMIT2_ADDRESS } = require('@uniswap/permit2-sdk');
3+
const { ether } = require('./helpers/utils');
4+
const { signOrder, buildOrder, buildTakerTraits, buildMakerTraitsRFQ } = require('./helpers/orderUtils');
5+
const { deploySwapTokens } = require('./helpers/fixtures');
6+
const { nextPermit2Nonce } = require('./helpers/nonce');
7+
const hre = require('hardhat');
8+
const { ethers } = hre;
9+
10+
describe('Permit2Proxy', function () {
11+
let addr, addr1;
12+
13+
before(async function () {
14+
[addr, addr1] = await ethers.getSigners();
15+
});
16+
17+
it('permit2 example (without witness)', async function () {
18+
const { dai, weth, swap, chainId } = await deploySwapTokens();
19+
20+
await dai.mint(addr, ether('2000'));
21+
await weth.connect(addr1).deposit({ value: ether('1') });
22+
await dai.approve(swap, ether('2000'));
23+
await weth.connect(addr1).approve(PERMIT2_ADDRESS, ether('1'));
24+
25+
const Permit2Proxy = await ethers.getContractFactory('Permit2Proxy');
26+
const permit2Proxy = await Permit2Proxy.deploy(await swap.getAddress());
27+
await permit2Proxy.waitForDeployment();
28+
29+
await permit2Contract();
30+
31+
const permit = {
32+
permitted: {
33+
token: await weth.getAddress(),
34+
amount: ether('1'),
35+
},
36+
spender: await permit2Proxy.getAddress(),
37+
nonce: nextPermit2Nonce(),
38+
deadline: 0xffffffff,
39+
};
40+
41+
const data = SignatureTransfer.getPermitData(
42+
permit,
43+
PERMIT2_ADDRESS,
44+
chainId,
45+
);
46+
47+
const sig = ethers.Signature.from(await addr1.signTypedData(data.domain, data.types, data.values));
48+
49+
const makerAssetSuffix = '0x' + permit2Proxy.interface.encodeFunctionData('func_nZHTch', [
50+
constants.ZERO_ADDRESS, constants.ZERO_ADDRESS, 0,
51+
{
52+
permitted: {
53+
token: permit.permitted.token,
54+
amount: permit.permitted.amount,
55+
},
56+
nonce: permit.nonce,
57+
deadline: permit.deadline,
58+
},
59+
sig.compactSerialized,
60+
]).substring(202);
61+
62+
const order = buildOrder(
63+
{
64+
maker: addr1.address,
65+
makerAsset: await permit2Proxy.getAddress(),
66+
takerAsset: await dai.getAddress(),
67+
makingAmount: ether('1'),
68+
takingAmount: ether('2000'),
69+
makerTraits: buildMakerTraitsRFQ(),
70+
},
71+
{
72+
makerAssetSuffix,
73+
},
74+
);
75+
76+
const { r, yParityAndS: vs } = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr1));
77+
const takerTraits = buildTakerTraits({
78+
makingAmount: true,
79+
extension: order.extension,
80+
threshold: order.takingAmount,
81+
});
82+
83+
await swap.fillOrderArgs(order, r, vs, order.makingAmount, takerTraits.traits, takerTraits.args);
84+
});
85+
});

test/WitnessProxyExample.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const { SignatureTransfer, PERMIT2_ADDRESS } = require('@uniswap/permit2-sdk');
33
const { ether } = require('./helpers/utils');
44
const { signOrder, buildOrder, buildTakerTraits, buildMakerTraitsRFQ } = require('./helpers/orderUtils');
55
const { deploySwapTokens } = require('./helpers/fixtures');
6+
const { nextPermit2Nonce } = require('./helpers/nonce');
67
const hre = require('hardhat');
78
const { ethers } = hre;
89

@@ -20,7 +21,6 @@ describe('WitnessProxyExample', function () {
2021

2122
await dai.mint(addr, ether('2000'));
2223
await weth.connect(addr1).deposit({ value: ether('1') });
23-
2424
await dai.approve(swap, ether('2000'));
2525
await weth.connect(addr1).approve(PERMIT2_ADDRESS, ether('1'));
2626

@@ -36,7 +36,7 @@ describe('WitnessProxyExample', function () {
3636
amount: ether('1'),
3737
},
3838
spender: await permit2WitnessProxy.getAddress(),
39-
nonce: 0,
39+
nonce: nextPermit2Nonce(),
4040
deadline: 0xffffffff,
4141
};
4242

@@ -49,7 +49,7 @@ describe('WitnessProxyExample', function () {
4949
const data = SignatureTransfer.getPermitData(
5050
permit,
5151
PERMIT2_ADDRESS,
52-
31337,
52+
chainId,
5353
witness,
5454
);
5555

test/helpers/nonce.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
let currentNonce = 0n;
2+
3+
/**
4+
* Returns a unique nonce for Permit2 SignatureTransfer.
5+
* Uses global state to ensure each call returns a different nonce.
6+
* @returns {bigint} A unique nonce
7+
*/
8+
function nextPermit2Nonce () {
9+
return currentNonce++;
10+
}
11+
12+
module.exports = {
13+
nextPermit2Nonce,
14+
};

0 commit comments

Comments
 (0)