Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions contracts/extensions/Permit2Proxy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.30;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import "../interfaces/IPermit2TransferFrom.sol";
import "./ImmutableOwner.sol";

/* solhint-disable func-name-mixedcase */

contract Permit2Proxy is ImmutableOwner {
error Permit2ProxyBadSelector();

IPermit2TransferFrom private constant _PERMIT2 = IPermit2TransferFrom(0x000000000022D473030F116dDEE9F6B43aC78BA3);

constructor(address _immutableOwner) ImmutableOwner(_immutableOwner) {
if (Permit2Proxy.func_nZHTch.selector != IERC20.transferFrom.selector) revert Permit2ProxyBadSelector();
}

/// @notice Proxy transfer method for `Permit2.permitTransferFrom`. Selector must match `IERC20.transferFrom`
// keccak256("func_nZHTch(address,address,uint256,((address,uint256),uint256,uint256),bytes)") == 0x23b872dd (IERC20.transferFrom)
function func_nZHTch(
address from,
address to,
uint256 amount,
IPermit2TransferFrom.PermitTransferFrom calldata permit,
bytes calldata sig
) external onlyImmutableOwner {
_PERMIT2.permitTransferFrom(
permit,
IPermit2TransferFrom.SignatureTransferDetails({
to: to,
requestedAmount: amount
}),
from,
sig
);
}
}

/* solhint-enable func-name-mixedcase */
34 changes: 34 additions & 0 deletions contracts/interfaces/IPermit2TransferFrom.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

interface IPermit2TransferFrom {
struct TokenPermissions {
// ERC20 token address
address token;
// the maximum amount that can be spent
uint256 amount;
}

struct PermitTransferFrom {
TokenPermissions permitted;
// a unique value for every token owner's signature to prevent signature replays
uint256 nonce;
// deadline on the permit signature
uint256 deadline;
}

struct SignatureTransferDetails {
// recipient address
address to;
// spender requested amount
uint256 requestedAmount;
}

function permitTransferFrom(
PermitTransferFrom calldata permit,
SignatureTransferDetails calldata transferDetails,
address owner,
bytes calldata signature
) external;
}
21 changes: 21 additions & 0 deletions dev.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Development Guide

## Selector Bruteforce Tool

When developing proxy contracts that need specific function selectors (e.g., matching `IERC20.transferFrom` selector `0x23b872dd`), use the selector bruteforce tool from:

**https://github.com/1inch/smart-contract-helper-utils/src**

### Usage Example

To find a function name with custom suffix that produces the `transferFrom` selector:

```bash
python selector_bruteforce.py \
--target 0x23b872dd \
--params "address,address,uint256,((address,uint256),uint256,uint256),bytes" \
--prefix "func_" \
--fast
```

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.
37 changes: 37 additions & 0 deletions docs/extensions/Permit2Proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

## Permit2Proxy

A proxy contract that enables using Uniswap's Permit2 `permitTransferFrom` within the limit order protocol without the witness functionality.

### Functions list
- [constructor(_immutableOwner) public](#constructor)
- [func_nZHTch(from, to, amount, permit, sig) external](#func_nzhtch)

### Errors list
- [Permit2ProxyBadSelector()](#permit2proxybadselector)

### Functions
### constructor

```solidity
constructor(address _immutableOwner) public
```

### func_nZHTch

```solidity
function func_nZHTch(address from, address to, uint256 amount, struct IPermit2TransferFrom.PermitTransferFrom permit, bytes sig) external
```
Proxy transfer method for `Permit2.permitTransferFrom`. Selector must match `IERC20.transferFrom`

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.

### Errors
### Permit2ProxyBadSelector

```solidity
error Permit2ProxyBadSelector()
```

Thrown in the constructor if the function selector doesn't match `IERC20.transferFrom.selector`.

85 changes: 85 additions & 0 deletions test/Permit2Proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
const { constants, permit2Contract } = require('@1inch/solidity-utils');
const { SignatureTransfer, PERMIT2_ADDRESS } = require('@uniswap/permit2-sdk');
const { ether } = require('./helpers/utils');
const { signOrder, buildOrder, buildTakerTraits, buildMakerTraitsRFQ } = require('./helpers/orderUtils');
const { deploySwapTokens } = require('./helpers/fixtures');
const { nextPermit2Nonce } = require('./helpers/nonce');
const hre = require('hardhat');
const { ethers } = hre;

describe('Permit2Proxy', function () {
let addr, addr1;

before(async function () {
[addr, addr1] = await ethers.getSigners();
});

it('permit2 example (without witness)', async function () {
const { dai, weth, swap, chainId } = await deploySwapTokens();

await dai.mint(addr, ether('2000'));
await weth.connect(addr1).deposit({ value: ether('1') });
await dai.approve(swap, ether('2000'));
await weth.connect(addr1).approve(PERMIT2_ADDRESS, ether('1'));

const Permit2Proxy = await ethers.getContractFactory('Permit2Proxy');
const permit2Proxy = await Permit2Proxy.deploy(await swap.getAddress());
await permit2Proxy.waitForDeployment();

await permit2Contract();

const permit = {
permitted: {
token: await weth.getAddress(),
amount: ether('1'),
},
spender: await permit2Proxy.getAddress(),
nonce: nextPermit2Nonce(),
deadline: 0xffffffff,
};

const data = SignatureTransfer.getPermitData(
permit,
PERMIT2_ADDRESS,
chainId,
);

const sig = ethers.Signature.from(await addr1.signTypedData(data.domain, data.types, data.values));

const makerAssetSuffix = '0x' + permit2Proxy.interface.encodeFunctionData('func_nZHTch', [
constants.ZERO_ADDRESS, constants.ZERO_ADDRESS, 0,
{
permitted: {
token: permit.permitted.token,
amount: permit.permitted.amount,
},
nonce: permit.nonce,
deadline: permit.deadline,
},
sig.compactSerialized,
]).substring(202);

const order = buildOrder(
{
maker: addr1.address,
makerAsset: await permit2Proxy.getAddress(),
takerAsset: await dai.getAddress(),
makingAmount: ether('1'),
takingAmount: ether('2000'),
makerTraits: buildMakerTraitsRFQ(),
},
{
makerAssetSuffix,
},
);

const { r, yParityAndS: vs } = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr1));
const takerTraits = buildTakerTraits({
makingAmount: true,
extension: order.extension,
threshold: order.takingAmount,
});

await swap.fillOrderArgs(order, r, vs, order.makingAmount, takerTraits.traits, takerTraits.args);
});
});
6 changes: 3 additions & 3 deletions test/WitnessProxyExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const { SignatureTransfer, PERMIT2_ADDRESS } = require('@uniswap/permit2-sdk');
const { ether } = require('./helpers/utils');
const { signOrder, buildOrder, buildTakerTraits, buildMakerTraitsRFQ } = require('./helpers/orderUtils');
const { deploySwapTokens } = require('./helpers/fixtures');
const { nextPermit2Nonce } = require('./helpers/nonce');
const hre = require('hardhat');
const { ethers } = hre;

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

await dai.mint(addr, ether('2000'));
await weth.connect(addr1).deposit({ value: ether('1') });

await dai.approve(swap, ether('2000'));
await weth.connect(addr1).approve(PERMIT2_ADDRESS, ether('1'));

Expand All @@ -36,7 +36,7 @@ describe('WitnessProxyExample', function () {
amount: ether('1'),
},
spender: await permit2WitnessProxy.getAddress(),
nonce: 0,
nonce: nextPermit2Nonce(),
deadline: 0xffffffff,
};

Expand All @@ -49,7 +49,7 @@ describe('WitnessProxyExample', function () {
const data = SignatureTransfer.getPermitData(
permit,
PERMIT2_ADDRESS,
31337,
chainId,
witness,
);

Expand Down
14 changes: 14 additions & 0 deletions test/helpers/nonce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
let currentNonce = 0n;

/**
* Returns a unique nonce for Permit2 SignatureTransfer.
* Uses global state to ensure each call returns a different nonce.
* @returns {bigint} A unique nonce
*/
function nextPermit2Nonce () {
return currentNonce++;
}

module.exports = {
nextPermit2Nonce,
};
Loading