Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SC-1077] Add support for ERC-7597 permit #128

Merged
merged 4 commits into from
Mar 19, 2024
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
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);
});
});
Loading