Skip to content

Commit

Permalink
Merge pull request #128 from 1inch/feature/SC-1077-Extend-safePermit
Browse files Browse the repository at this point in the history
[SC-1077] Add support for ERC-7597 permit
  • Loading branch information
ZumZoom authored Mar 19, 2024
2 parents 7048aeb + c36f1eb commit fc1e783
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 40 deletions.
13 changes: 13 additions & 0 deletions contracts/interfaces/IERC7597Permit.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

interface IERC7597Permit {
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
bytes memory signature
) external;
}
10 changes: 7 additions & 3 deletions contracts/libraries/SafeERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import "../interfaces/IDaiLikePermit.sol";
import "../interfaces/IPermit2.sol";
import "../interfaces/IERC7597Permit.sol";
import "../interfaces/IWETH.sol";
import "../libraries/RevertReasonForwarder.sol";

Expand Down Expand Up @@ -301,6 +302,7 @@ library SafeERC20 {
bytes4 permitSelector = IERC20Permit.permit.selector;
bytes4 daiPermitSelector = IDaiLikePermit.permit.selector;
bytes4 permit2Selector = IPermit2.permit.selector;
bytes4 erc7597PermitSelector = IERC7597Permit.permit.selector;
assembly ("memory-safe") { // solhint-disable-line no-inline-assembly
let ptr := mload(0x40)

Expand Down Expand Up @@ -389,10 +391,12 @@ library SafeERC20 {
// IPermit2.permit(address owner, PermitSingle calldata permitSingle, bytes calldata signature)
success := call(gas(), _PERMIT2, 0, ptr, 0x164, 0, 0)
}
// Unknown
// Dynamic length
default {
mstore(ptr, _PERMIT_LENGTH_ERROR)
revert(ptr, 4)
mstore(ptr, erc7597PermitSelector)
calldatacopy(add(ptr, 0x04), permit.offset, permit.length) // copy permit calldata
// IERC7597Permit.permit(address owner, address spender, uint256 value, uint256 deadline, bytes memory signature)
success := call(gas(), token, 0, ptr, add(permit.length, 4), 0, 0)
}
}
}
Expand Down
80 changes: 80 additions & 0 deletions contracts/tests/mocks/USDCLikePermitMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol";
import { ERC20, ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";

contract USDCLikePermitMock is ERC20Permit {
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
bytes32
public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;

error InvalidSignature();
error PermitExpired();

constructor(
string memory name,
string memory symbol,
address initialAccount,
uint256 initialBalance
) payable ERC20(name, symbol) ERC20Permit(name){
_mint(initialAccount, initialBalance);
}

/**
* @notice Update allowance with a signed permit
* @dev EOA wallet signatures should be packed in the order of r, s, v.
* @param owner Token owner's address (Authorizer)
* @param spender Spender's address
* @param value Amount of allowance
* @param deadline The time at which the signature expires (unix time), or max uint256 value to signal no expiration
* @param signature Signature bytes signed by an EOA wallet or a contract wallet
*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
bytes memory signature
) external {
if (deadline != type(uint256).max && deadline < block.timestamp) {
revert PermitExpired();
}

bytes32 typedDataHash = _hashTypedDataV4(
keccak256(
abi.encode(
PERMIT_TYPEHASH,
owner,
spender,
value,
_useNonce(owner),
deadline
)
)
);
if (!_isValidERC1271SignatureNow(owner, typedDataHash, signature)) {
revert InvalidSignature();
}
_approve(owner, spender, value);
}

function _isValidERC1271SignatureNow(
address signer,
bytes32 digest,
bytes memory signature
) internal view returns (bool) {
(bool success, bytes memory result) = signer.staticcall(
abi.encodeWithSelector(
IERC1271.isValidSignature.selector,
digest,
signature
)
);
return (success &&
result.length >= 32 &&
abi.decode(result, (bytes32)) ==
bytes32(IERC1271.isValidSignature.selector));
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@1inch/solidity-utils",
"version": "3.8.3",
"version": "4.0.0",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"exports": {
Expand Down
33 changes: 32 additions & 1 deletion src/permit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ethers } from 'hardhat';
import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers';
import { AllowanceTransfer, PERMIT2_ADDRESS } from '@uniswap/permit2-sdk';
import { bytecode as permit2Bytecode } from './permit2.json';
import { DaiLikePermitMock, ERC20Permit } from '../typechain-types';
import { DaiLikePermitMock, ERC20Permit, USDCLikePermitMock } from '../typechain-types';

export const TypedDataVersion = SignTypedDataVersion.V4;
export const defaultDeadline = constants.MAX_UINT256;
Expand Down Expand Up @@ -202,6 +202,37 @@ export async function getPermitLikeDai(
return compact ? compressPermit(permitCall) : decompressPermit(compressPermit(permitCall), constants.ZERO_ADDRESS, holder.address, spender);
}

export async function getPermitLikeUSDC(
owner: string, // contract with isValidSignature function
signer: Wallet | SignerWithAddress,
permitContract: USDCLikePermitMock,
tokenVersion: string,
chainId: number,
spender: string,
value: string,
deadline = defaultDeadline.toString(),
): Promise<string> {
const nonce = await permitContract.nonces(owner);
const name = await permitContract.name();
const data = buildData(
name,
tokenVersion,
chainId,
await permitContract.getAddress(),
owner,
spender,
value,
nonce.toString(),
deadline,
);

const signature = await signer.signTypedData(data.domain, data.types, data.message);
const { v, r, s } = Signature.from(signature);
const signatureBytes = ethers.solidityPacked(['bytes32', 'bytes32', 'uint8'], [r, s, v]);

return cutSelector(permitContract.interface.encodeFunctionData('permit(address,address,uint256,uint256,bytes)', [owner, spender, value, deadline, signatureBytes]));
}

export function withTarget(target: bigint | string, data: bigint | string): string {
return target.toString() + trim0x(data);
}
Expand Down
51 changes: 16 additions & 35 deletions test/Permitable.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from '../src/expect';
import { defaultDeadline, buildData, buildDataLikeDai, getPermit, getPermit2, getPermitLikeDai, permit2Contract, cutSelector } from '../src/permit';
import { defaultDeadline, buildData, buildDataLikeDai, getPermit, getPermit2, getPermitLikeDai, getPermitLikeUSDC, permit2Contract, cutSelector } from '../src/permit';
import { constants } from '../src/prelude';
import { ethers } from 'hardhat';
import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers';
Expand All @@ -19,14 +19,18 @@ describe('Permitable', function () {
const PermitableMockFactory = await ethers.getContractFactory('PermitableMock');
const ERC20PermitMockFactory = await ethers.getContractFactory('ERC20PermitMock');
const DaiLikePermitMockFactory = await ethers.getContractFactory('DaiLikePermitMock');
const USDCLikePermitMockFactory = await ethers.getContractFactory('USDCLikePermitMock');
const SafeERC20Factory = await ethers.getContractFactory('SafeERC20');
const IsValidSignatureMockFactory = await ethers.getContractFactory('ERC1271WalletMock');

const chainId = Number((await ethers.provider.getNetwork()).chainId);
const permitableMock = await PermitableMockFactory.deploy();
const erc20PermitMock = await ERC20PermitMockFactory.deploy('USDC', 'USDC', signer1, 100n);
const daiLikePermitMock = await DaiLikePermitMockFactory.deploy('DAI', 'DAI', signer1, 100n);
const usdcLikePermitMock = await USDCLikePermitMockFactory.deploy('USDCP', 'USDCP', signer1, 100n);
const safeERC20 = await SafeERC20Factory.attach(permitableMock);
return { permitableMock, erc20PermitMock, daiLikePermitMock, safeERC20, chainId };
const isValidSignatureMock = await IsValidSignatureMockFactory.deploy(signer1);
return { permitableMock, erc20PermitMock, daiLikePermitMock, usdcLikePermitMock, safeERC20, isValidSignatureMock, chainId };
}

it('should be permitted for IERC20Permit', async function () {
Expand Down Expand Up @@ -174,41 +178,18 @@ describe('Permitable', function () {
);
});

it('should be wrong permit length', async function () {
const { permitableMock, erc20PermitMock, safeERC20, chainId } = await loadFixture(deployTokens);
it('should be permitted for IERC7597Permit', async function () {
const { permitableMock, usdcLikePermitMock, isValidSignatureMock, chainId } = await loadFixture(deployTokens);

const name = await erc20PermitMock.name();
const nonce = await erc20PermitMock.nonces(signer1);
const data = buildData(
name,
'1',
chainId,
await erc20PermitMock.getAddress(),
signer1.address,
signer2.address,
value.toString(),
nonce.toString(),
);
const signature = await signer1.signTypedData(data.domain, data.types, data.message);
const { v, r, s } = ethers.Signature.from(signature);
const owner = await isValidSignatureMock.getAddress();

const permit =
'0x' +
cutSelector(
erc20PermitMock.interface.encodeFunctionData('permit', [
signer1.address,
signer1.address,
value,
defaultDeadline,
v,
r,
s,
]),
).substring(64);

await expect(permitableMock.mockPermit(erc20PermitMock, permit)).to.be.revertedWithCustomError(
safeERC20,
'SafePermitBadLength',
const permit = await getPermitLikeUSDC(
owner, signer1, usdcLikePermitMock, '1', chainId, await permitableMock.getAddress(), value.toString()
);

await permitableMock.mockPermit(usdcLikePermitMock, permit);

expect(await usdcLikePermitMock.nonces(owner)).to.be.equal(1);
expect(await usdcLikePermitMock.allowance(owner, permitableMock)).to.be.equal(value);
});
});

0 comments on commit fc1e783

Please sign in to comment.