diff --git a/.github/workflows/enforceTestCoverage.yml b/.github/workflows/enforceTestCoverage.yml index 5360b91a6..6dc545511 100644 --- a/.github/workflows/enforceTestCoverage.yml +++ b/.github/workflows/enforceTestCoverage.yml @@ -6,7 +6,7 @@ name: Enforce Min Test Coverage on: pull_request: - types: [opened, synchronize, reopened] + types: [opened, synchronize, reopened, ready_for_review] jobs: enforce-min-test-coverage: @@ -27,6 +27,8 @@ jobs: MIN_TEST_COVERAGE: 74 # = 74% line coverage steps: - uses: actions/checkout@v4.1.7 + with: + fetch-depth: 0 ##### Fetch all history for all branches - name: Set up Node.js uses: actions/setup-node@v4 @@ -54,12 +56,20 @@ jobs: echo "Coverage report successfully filtered" - - name: Generate Coverage Summary + # Step to get modified or added files in the src/ folder + - name: Get Modified Files + id: get_modified_files run: | + # Get list of modified or added files in src directory + MODIFIED_FILES=$(git diff --name-only --diff-filter=AM origin/main | grep '^src/.*\.sol$') + echo "modified_files=$MODIFIED_FILES" >> "$GITHUB_ENV" + - name: Generate Coverage Summary + run: | # Path to the lcov info file LCOV_FILE="lcov-filtered.info" + MODIFIED_FILES=(${MODIFIED_FILES}) # Initialize counters TOTAL_LINES_FOUND=0 @@ -69,14 +79,23 @@ jobs: TOTAL_BRANCHES_FOUND=0 TOTAL_BRANCHES_HIT=0 + # Clear the per-file coverage report file + echo "" > per_file_coverage_report.txt + # Read through the lcov file while IFS= read -r line; do case $line in + SF:*) + CURRENT_FILE=${line#SF:} + FILE_NAME=$(basename "$CURRENT_FILE") + ;; LF:*) TOTAL_LINES_FOUND=$((TOTAL_LINES_FOUND + ${line#LF:})) + FILE_LINES_FOUND=${line#LF:} ;; LH:*) TOTAL_LINES_HIT=$((TOTAL_LINES_HIT + ${line#LH:})) + FILE_LINES_HIT=${line#LH:} ;; FNF:*) TOTAL_FUNCTIONS_FOUND=$((TOTAL_FUNCTIONS_FOUND + ${line#FNF:})) @@ -91,6 +110,16 @@ jobs: TOTAL_BRANCHES_HIT=$((TOTAL_BRANCHES_HIT + ${line#BRH:})) ;; esac + + # Check if the file is in the list of modified files + if [[ " ${MODIFIED_FILES[@]} " =~ " $FILE_NAME " ]]; then + # Calculate line coverage percentage for the current file + LINE_COVERAGE_PERCENTAGE=$(echo "scale=2; $FILE_LINES_HIT / $FILE_LINES_FOUND * 100" | bc) + + # Append the per-file coverage to the report file + echo "File: $FILE_NAME - Line Coverage: $LINE_COVERAGE_PERCENTAGE% ($FILE_LINES_HIT / $FILE_LINES_FOUND)" >> per_file_coverage_report.txt + fi + done < "$LCOV_FILE" # Calculate percentages with high precision @@ -118,7 +147,7 @@ jobs: exit 1 fi - # Output result_COVERAGE_REPORTs + # Output results echo "$LINE_COVERAGE_REPORT" echo "$FUNCTION_COVERAGE_REPORT" echo "$BRANCH_COVERAGE_REPORT" @@ -131,14 +160,3 @@ jobs: echo "BRANCH_COVERAGE_REPORT=$BRANCH_COVERAGE_REPORT" echo "RESULT_COVERAGE_REPORT=$RESULT_COVERAGE_REPORT" } >> "$GITHUB_ENV" - - - name: Comment with Coverage Summary in PR - uses: mshick/add-pr-comment@v2.8.2 - with: - repo-token: ${{ secrets.GIT_ACTIONS_BOT_PAT_CLASSIC }} - message: | - ## Test Coverage Report - ${{ env.LINE_COVERAGE_REPORT }} - ${{ env.FUNCTION_COVERAGE_REPORT }} - ${{ env.BRANCH_COVERAGE_REPORT }} - ${{ env.RESULT_COVERAGE_REPORT }} diff --git a/.gitmodules b/.gitmodules index 916ca16ca..ce1a6a0ef 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,6 @@ [submodule "lib/solady"] path = lib/solady url = https://github.com/Vectorized/solady +[submodule "lib/Permit2"] + path = lib/Permit2 + url = https://github.com/Uniswap/Permit2 diff --git a/config/permit2.json b/config/permit2.json new file mode 100644 index 000000000..c4ac60ac2 --- /dev/null +++ b/config/permit2.json @@ -0,0 +1,31 @@ +{ + "mainnet": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "arbitrum": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "aurora": "", + "avalanche": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "base": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "blast": "0x000000000022d473030f116ddee9f6b43ac78ba3", + "boba": "", + "bsc": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "celo": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "fantom": "", + "fraxtal": "", + "fuse": "", + "gnosis": "", + "gravity": "", + "immutablezkevm": "", + "linea": "", + "mantle": "", + "metis": "", + "mode": "", + "moonbeam": "", + "moonriver": "", + "optimism": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "polygon": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "polygonzkevm": "", + "rootstock": "", + "scroll": "", + "sei": "", + "taiko": "", + "zksync": "0x0000000000225e31d15943971f47ad3022f714fa" +} \ No newline at end of file diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index e5141b66d..3498f0c68 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -22453,5 +22453,21 @@ ] } } + }, + "Permit2Proxy": { + "arbitrum": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xA3C7a31a2A97b847D967e0B755921D084C46a742", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2024-08-30 14:01:34", + "CONSTRUCTOR_ARGS": "0x000000000000000000000000d3b2b0ac0afdd0d166a495f5e9fca4ecc715a782000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3", + "SALT": "09072024", + "VERIFIED": "true" + } + ] + } + } } } diff --git a/deployments/arbitrum.staging.json b/deployments/arbitrum.staging.json index a6402499e..be96f4622 100644 --- a/deployments/arbitrum.staging.json +++ b/deployments/arbitrum.staging.json @@ -44,5 +44,6 @@ "CircleBridgeFacet": "0xa73a8BC8d36472269138c3233e24D0Ee0c344bd8", "HopFacetOptimized": "0xf82135385765f1324257ffF74489F16382EBBb8A", "LiFuelFeeCollector": "0x94EA56D8049e93E0308B9c7d1418Baf6A7C68280", - "TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70" + "TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70", + "Permit2Proxy": "0xA3C7a31a2A97b847D967e0B755921D084C46a742" } \ No newline at end of file diff --git a/docs/Permit2Proxy.md b/docs/Permit2Proxy.md new file mode 100644 index 000000000..fa248bf27 --- /dev/null +++ b/docs/Permit2Proxy.md @@ -0,0 +1,126 @@ +# Permit2 Proxy + +## Description + +Periphery contract which enables gasless and semi-gasless transaction flows +enabled through ERC20 Permit and Uniswap's Permit2 + +## How To Use + +The contract has a number of methods for making gasless and semi-gasless calls +as well as a few helpful utility methods. + +The following methods are available: + +This method is used to execute a transaction where the approval is granted +using an ERC20 Permit signature. It can only be called by the signer in order +to prevent front-running attacks. + +```solidity +/// @notice Allows to bridge tokens through a LI.FI diamond contract using +/// an EIP2612 gasless permit (only works with tokenAddresses that +/// implement EIP2612) (in contrast to Permit2, calldata and diamondAddress +/// are not signed by the user and could therefore be replaced by the user) +/// Can only be called by the permit signer to prevent front-running. +/// @param tokenAddress Address of the token to be bridged +/// @param amount Amount of tokens to be bridged +/// @param deadline Transaction must be completed before this timestamp +/// @param v User signature (recovery ID) +/// @param r User signature (ECDSA output) +/// @param s User signature (ECDSA output) +/// @param diamondCalldata Address of the token to be bridged +function callDiamondWithEIP2612Signature( + address tokenAddress, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s, + bytes calldata diamondCalldata +) public payable +```` + +This method is used to execute a transaction where the approval is granted via +Uniswap's Permit2 contract. It can only be called by the signer in order to +prevent front-running attacks. + +```solidity +/// @notice Allows to bridge tokens of one type through a LI.FI diamond +/// contract using Uniswap's Permit2 contract and a user signature +/// that verifies allowance. The calldata can be changed by the +/// user. Can only be called by the permit signer to prevent +/// front-running. +/// @param _diamondCalldata the calldata to execute +/// @param _permit the Uniswap Permit2 parameters +/// @param _signature the signature giving approval to transfer tokens +function callDiamondWithPermit2( + bytes calldata _diamondCalldata, + ISignatureTransfer.PermitTransferFrom calldata _permit, + bytes calldata _signature +) external payable +``` + +This method enables a gasless flow by allowing a user to sign a Uniswap Permit2 +message hash which includes a "witness" type. This extra type restricts which +calldata can be called during execution and cannot be changed. Anyone with the +signature can execute the transaction on behalf of the signer. + +```solidity +/// @notice Allows to bridge tokens of one type through a LI.FI diamond +/// contract using Uniswap's Permit2 contract and a user signature +/// that verifies allowance, diamondAddress and diamondCalldata +/// @param _diamondCalldata the calldata to execute +/// @param _signer the signer giving permission to transfer tokens +/// @param _permit the Uniswap Permit2 parameters +/// @param _signature the signature giving approval to transfer tokens +function callDiamondWithPermit2Witness( + bytes calldata _diamondCalldata, + address _signer, + ISignatureTransfer.PermitTransferFrom calldata _permit, + bytes calldata _signature +) external payable +``` + +There are a few utility methods to make it easier to generate the necessary +signature for the gasless flow. + +Calling this method will return a valid message hash that can then be signed +in order to be executed later by another wallet. + +```solidity +/// @notice utitlity method for constructing a valid Permit2 message hash +/// @param _diamondCalldata the calldata to execute +/// @param _assetId the address of the token to approve +/// @param _amount amount of tokens to approve +/// @param _nonce the nonce to use +/// @param _deadline the expiration deadline +function getPermit2MsgHash( + bytes calldata _diamondCalldata, + address _assetId, + uint256 _amount, + uint256 _nonce, + uint256 _deadline +) external view returns (bytes32 msgHash) +``` + +Permit2 nonces are non-sequential and are a bit complicated to work with. The +following utility methods allow you to fetch the next valid nonce or sequence +of nonces for use when generating Permit2 signatures. + +```solidity +/// @notice Finds the next valid nonce for a user, starting from 0. +/// @param owner The owner of the nonces +/// @return nonce The first valid nonce starting from 0 +function nextNonce(address owner) external view returns (uint256 nonce) + +/// @notice Finds the next valid nonce for a user, after from a given nonce. +/// @dev This can be helpful if you're signing multiple nonces in a row and +/// need the next nonce to sign but the start one is still valid. +/// @param owner The owner of the nonces +/// @param start The nonce to start from +/// @return nonce The first valid nonce after the given nonce +function nextNonceAfter( + address owner, + uint256 start +) external view returns (uint256 nonce) +``` diff --git a/docs/README.md b/docs/README.md index 55948f4ab..9bf554527 100644 --- a/docs/README.md +++ b/docs/README.md @@ -56,3 +56,4 @@ - [FeeCollector](./FeeCollector.md) - [Receiver](./Receiver.md) - [RelayerCelerIM](./RelayerCelerIM.md) +- [Permit2Proxy](./Permit2Proxy.md) diff --git a/lib/Permit2 b/lib/Permit2 new file mode 160000 index 000000000..cc56ad0f3 --- /dev/null +++ b/lib/Permit2 @@ -0,0 +1 @@ +Subproject commit cc56ad0f3439c502c246fc5cfcc3db92bb8b7219 diff --git a/remappings.txt b/remappings.txt index cf75a7770..d4a7a4506 100644 --- a/remappings.txt +++ b/remappings.txt @@ -9,7 +9,7 @@ celer-network/=lib/sgn-v2-contracts/ create3-factory/=lib/create3-factory/src/ solmate/=lib/solmate/src/ solady/=lib/solady/src/ - +permit2/=lib/Permit2/src/ ds-test/=lib/ds-test/src/ forge-std/=lib/forge-std/src/ diff --git a/script/demoScripts/demoPermit2Proxy.ts b/script/demoScripts/demoPermit2Proxy.ts new file mode 100644 index 000000000..9a6731150 --- /dev/null +++ b/script/demoScripts/demoPermit2Proxy.ts @@ -0,0 +1,117 @@ +import { + http, + createPublicClient, + parseAbi, + Hex, + parseUnits, + serializeSignature, + createWalletClient, +} from 'viem' +import { privateKeyToAccount, sign } from 'viem/accounts' +import { arbitrum } from 'viem/chains' +import { defineCommand, runMain } from 'citty' + +const DIAMOND_ADDRESS = '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE' +const USDT_ADDRESS = '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9' +const PERMIT2_PROXY_ADDRESS = '0xA3C7a31a2A97b847D967e0B755921D084C46a742' +const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3' +const PRIVATE_KEY = `0x${process.env.PRIVATE_KEY}` + +const main = defineCommand({ + meta: { + name: 'demo-permit2', + description: 'Demonstrate a Permit2 tx', + }, + args: { + signerKey: { + type: 'string', + description: 'Private key of signer', + required: true, + }, + executorKey: { + type: 'string', + description: 'Private key of the executor', + required: true, + }, + }, + async run({ args }) { + const SIGNER_PRIVATE_KEY = `0x${args.signerKey}` as Hex + const EXECUTOR_PRIVATE_KEY = `0x${args.executorKey}` as Hex + + // Setup the required ABI + const permit2ProxyAbi = parseAbi([ + 'function getPermit2MsgHash(bytes,address,uint256,uint256,uint256) external view returns (bytes32)', + 'function nextNonce(address owner) external view returns (uint256)', + 'function callDiamondWithPermit2Witness(bytes,address,((address,uint256),uint256,uint256),bytes) external', + ]) + + // Setup a READ-ONLY client + const client = createPublicClient({ + chain: arbitrum, + transport: http(), + }) + + // Setup a signer account + const account = privateKeyToAccount(SIGNER_PRIVATE_KEY) + + // Get calldata to bridge USDT from LIFI API + const url = + 'https://li.quest/v1/quote?fromChain=ARB&toChain=POL&fromToken=0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9&toToken=0xc2132D05D31c914a87C6611C10748AEb04B58e8F&fromAddress=0xb9c0dE368BECE5e76B52545a8E377a4C118f597B&toAddress=0xb9c0dE368BECE5e76B52545a8E377a4C118f597B&fromAmount=5000000' + const options = { method: 'GET', headers: { accept: 'application/json' } } + const lifiResp = await fetch(url, options) + const calldata = (await lifiResp.json()).transactionRequest.data + + // Get the nonce from the PERMIT2 contract + const nonce = await client.readContract({ + address: PERMIT2_PROXY_ADDRESS, + abi: permit2ProxyAbi, + functionName: 'nextNonce', + args: [account.address], + }) + + // Get latest block + const block = await client.getBlock() + + // Construct a valid message hash to sign using Permit2Proxy's utility func + const msgHash = await client.readContract({ + address: PERMIT2_PROXY_ADDRESS, + abi: permit2ProxyAbi, + functionName: 'getPermit2MsgHash', + args: [ + calldata, + USDT_ADDRESS, + parseUnits('5', 6), + nonce, + block.timestamp + 1200n, + ], + }) + console.log(msgHash) + + // Sign the message hash + const rsvSig = await sign({ hash: msgHash, privateKey: SIGNER_PRIVATE_KEY }) + const signature = serializeSignature(rsvSig) + console.log(signature) + + // Setup the parameters for the executor to call + const tokenPermissions = [USDT_ADDRESS, parseUnits('5', 6)] + const permit = [tokenPermissions, nonce, block.timestamp + 1200n] + + // Instantiate the executor account and a WRITE enabled client + const executorAccount = privateKeyToAccount(EXECUTOR_PRIVATE_KEY) + const walletClient = createWalletClient({ + account: executorAccount, + chain: arbitrum, + transport: http(), + }) + + // Execute using the Permit2 Proxy + const tx = await walletClient.writeContract({ + address: PERMIT2_PROXY_ADDRESS, + abi: permit2ProxyAbi, + functionName: 'callDiamondWithPermit2Witness', + args: [calldata, account.address, permit, signature], + }) + }, +}) + +runMain(main) diff --git a/script/deploy/facets/DeployPermit2Proxy.s.sol b/script/deploy/facets/DeployPermit2Proxy.s.sol new file mode 100644 index 000000000..4513f3b31 --- /dev/null +++ b/script/deploy/facets/DeployPermit2Proxy.s.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { DeployScriptBase } from "./utils/DeployScriptBase.sol"; +import { Permit2Proxy } from "lifi/Periphery/Permit2Proxy.sol"; +import { stdJson } from "forge-std/Script.sol"; + +contract DeployScript is DeployScriptBase { + using stdJson for string; + + constructor() DeployScriptBase("Permit2Proxy") {} + + function run() + public + returns (Permit2Proxy deployed, bytes memory constructorArgs) + { + constructorArgs = getConstructorArgs(); + + deployed = Permit2Proxy(deploy(type(Permit2Proxy).creationCode)); + } + + function getConstructorArgs() internal override returns (bytes memory) { + string memory deployments = string.concat( + root, + "/deployments/", + network, + ".", + fileSuffix, + "json" + ); + string memory deploymentsJSON = vm.readFile(deployments); + + address diamond = deploymentsJSON.readAddress(".LiFiDiamond"); + + // get path of permit2 config file + string memory permit2ProxyConfig = string.concat( + root, + "/config/permit2.json" + ); + + // read file into json variable + string memory permit2ProxyConfigJSON = vm.readFile(permit2ProxyConfig); + + // extract wrapped token address for the given network + address permit2Address = permit2ProxyConfigJSON.readAddress( + string.concat(".", network) + ); + + return abi.encode(diamond, permit2Address); + } +} diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol new file mode 100644 index 000000000..ea75f5375 --- /dev/null +++ b/src/Periphery/Permit2Proxy.sol @@ -0,0 +1,356 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { ISignatureTransfer } from "permit2/interfaces/ISignatureTransfer.sol"; +import { TransferrableOwnership } from "lifi/Helpers/TransferrableOwnership.sol"; +import { LibAsset, IERC20 } from "lifi/Libraries/LibAsset.sol"; +import { PermitHash } from "permit2/libraries/PermitHash.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; + +/// @title Permit2Proxy +/// @author LI.FI (https://li.fi) +/// @notice Proxy contract allowing gasless calls via Permit2 as well as making +/// token approvals via ERC20 Permit (EIP-2612) to our diamond contract +/// @custom:version 1.0.0 +contract Permit2Proxy { + /// Storage /// + + address public immutable LIFI_DIAMOND; + ISignatureTransfer public immutable PERMIT2; + + string public constant WITNESS_TYPE_STRING = + "LiFiCall witness)LiFiCall(address tokenReceiver,address diamondAddress,bytes32 diamondCalldataHash)TokenPermissions(address token,uint256 amount)"; + bytes32 public constant WITNESS_TYPEHASH = + keccak256( + "LiFiCall(address tokenReceiver,address diamondAddress,bytes32 diamondCalldataHash)" + ); + bytes32 public immutable PERMIT_WITH_WITNESS_TYPEHASH; + + /// Types /// + + // @dev LIFI Specific Witness which verifies the correct calldata and + // diamond address + struct LiFiCall { + address diamondAddress; + bytes32 diamondCalldataHash; + } + + /// Errors /// + + error CallToDiamondFailed(bytes); + + /// Constructor /// + + constructor(address _lifiDiamond, ISignatureTransfer _permit2) { + LIFI_DIAMOND = _lifiDiamond; + PERMIT2 = _permit2; + + PERMIT_WITH_WITNESS_TYPEHASH = keccak256( + abi.encodePacked( + PermitHash._PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB, + WITNESS_TYPE_STRING + ) + ); + } + + /// External Functions /// + + /// @notice Allows to bridge tokens through a LI.FI diamond contract using + /// an EIP2612 gasless permit (only works with tokenAddresses that + /// implement EIP2612) (in contrast to Permit2, calldata and diamondAddress + /// are not signed by the user and could therefore be replaced by the user) + /// Can only be called by the permit signer to prevent front-running. + /// @param tokenAddress Address of the token to be bridged + /// @param amount Amount of tokens to be bridged + /// @param deadline Transaction must be completed before this timestamp + /// @param v User signature (recovery ID) + /// @param r User signature (ECDSA output) + /// @param s User signature (ECDSA output) + /// @param diamondCalldata Address of the token to be bridged + function callDiamondWithEIP2612Signature( + address tokenAddress, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s, + bytes calldata diamondCalldata + ) public payable { + // call permit on token contract to register approval using signature + ERC20Permit(tokenAddress).permit( + msg.sender, // Ensure msg.sender is same wallet that signed permit + address(this), + amount, + deadline, + v, + r, + s + ); + + // deposit assets + LibAsset.transferFromERC20( + tokenAddress, + msg.sender, + address(this), + amount + ); + + // maxApprove token to diamond if current allowance is insufficient + LibAsset.maxApproveERC20(IERC20(tokenAddress), LIFI_DIAMOND, amount); + + // call our diamond to execute calldata + _executeCalldata(diamondCalldata); + } + + /// @notice Allows to bridge tokens of one type through a LI.FI diamond + /// contract using Uniswap's Permit2 contract and a user signature + /// that verifies allowance. The calldata can be changed by the + /// user. Can only be called by the permit signer to prevent + /// front-running. + /// @param _diamondCalldata the calldata to execute + /// @param _permit the Uniswap Permit2 parameters + /// @param _signature the signature giving approval to transfer tokens + function callDiamondWithPermit2( + bytes calldata _diamondCalldata, + ISignatureTransfer.PermitTransferFrom calldata _permit, + bytes calldata _signature + ) external payable { + PERMIT2.permitTransferFrom( + _permit, + ISignatureTransfer.SignatureTransferDetails({ + to: address(this), + requestedAmount: _permit.permitted.amount + }), + msg.sender, // Ensure msg.sender is same wallet that signed permit + _signature + ); + + // maxApprove token to diamond if current allowance is insufficient + LibAsset.maxApproveERC20( + IERC20(_permit.permitted.token), + LIFI_DIAMOND, + _permit.permitted.amount + ); + + _executeCalldata(_diamondCalldata); + } + + /// @notice Allows to bridge tokens of one type through a LI.FI diamond + /// contract using Uniswap's Permit2 contract and a user signature + /// that verifies allowance, diamondAddress and diamondCalldata + /// @param _diamondCalldata the calldata to execute + /// @param _signer the signer giving permission to transfer tokens + /// @param _permit the Uniswap Permit2 parameters + /// @param _signature the signature giving approval to transfer tokens + function callDiamondWithPermit2Witness( + bytes calldata _diamondCalldata, + address _signer, + ISignatureTransfer.PermitTransferFrom calldata _permit, + bytes calldata _signature + ) external payable { + LiFiCall memory lifiCall = LiFiCall( + LIFI_DIAMOND, + keccak256(_diamondCalldata) + ); + + bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, lifiCall)); + + PERMIT2.permitWitnessTransferFrom( + _permit, + ISignatureTransfer.SignatureTransferDetails({ + to: address(this), + requestedAmount: _permit.permitted.amount + }), + _signer, + witness, + WITNESS_TYPE_STRING, + _signature + ); + + // maxApprove token to diamond if current allowance is insufficient + LibAsset.maxApproveERC20( + IERC20(_permit.permitted.token), + LIFI_DIAMOND, + _permit.permitted.amount + ); + + _executeCalldata(_diamondCalldata); + } + + /// @notice utitlity method for constructing a valid Permit2 message hash + /// @param _diamondCalldata the calldata to execute + /// @param _assetId the address of the token to approve + /// @param _amount amount of tokens to approve + /// @param _nonce the nonce to use + /// @param _deadline the expiration deadline + function getPermit2MsgHash( + bytes calldata _diamondCalldata, + address _assetId, + uint256 _amount, + uint256 _nonce, + uint256 _deadline + ) external view returns (bytes32 msgHash) { + // Token Permissions + ISignatureTransfer.TokenPermissions + memory tokenPermissions = ISignatureTransfer.TokenPermissions( + _assetId, + _amount + ); + bytes32 permit = _getTokenPermissionsHash(tokenPermissions); + + // Witness + Permit2Proxy.LiFiCall memory lifiCall = LiFiCall( + LIFI_DIAMOND, + keccak256(_diamondCalldata) + ); + bytes32 witness = _getWitnessHash(lifiCall); + + // PermitTransferWithWitness + msgHash = _getPermitWitnessTransferFromHash( + PERMIT2.DOMAIN_SEPARATOR(), + permit, + address(this), + _nonce, + _deadline, + witness + ); + } + + /// Internal Functions /// + + function _getTokenPermissionsHash( + ISignatureTransfer.TokenPermissions memory tokenPermissions + ) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + PermitHash._TOKEN_PERMISSIONS_TYPEHASH, + tokenPermissions.token, + tokenPermissions.amount + ) + ); + } + + function _getWitnessHash( + Permit2Proxy.LiFiCall memory lifiCall + ) internal pure returns (bytes32) { + return keccak256(abi.encode(WITNESS_TYPEHASH, lifiCall)); + } + + function _getPermitWitnessTransferFromHash( + bytes32 domainSeparator, + bytes32 permit, + address spender, + uint256 nonce, + uint256 deadline, + bytes32 witness + ) internal view returns (bytes32) { + bytes32 dataHash = keccak256( + abi.encode( + PERMIT_WITH_WITNESS_TYPEHASH, + permit, + spender, + nonce, + deadline, + witness + ) + ); + + return + keccak256(abi.encodePacked("\x19\x01", domainSeparator, dataHash)); + } + + function _executeCalldata(bytes memory diamondCalldata) internal { + // call diamond with provided calldata + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory data) = LIFI_DIAMOND.call{ + value: msg.value + }(diamondCalldata); + // throw error to make sure tx reverts if low-level call was + // unsuccessful + if (!success) { + revert CallToDiamondFailed(data); + } + } + + /// The following code was adapted from https://github.com/flood-protocol/permit2-nonce-finder/blob/7a4ac8a58d0b499308000b75ddb2384834f31fac/src/Permit2NonceFinder.sol + /// Provides utility functions for determining the next valid Permit2 nonce + + /// @notice Finds the next valid nonce for a user, starting from 0. + /// @param owner The owner of the nonces + /// @return nonce The first valid nonce starting from 0 + function nextNonce(address owner) external view returns (uint256 nonce) { + nonce = _nextNonce(owner, 0, 0); + } + + /// @notice Finds the next valid nonce for a user, after from a given nonce. + /// @dev This can be helpful if you're signing multiple nonces in a row and need the next nonce to sign but the start one is still valid. + /// @param owner The owner of the nonces + /// @param start The nonce to start from + /// @return nonce The first valid nonce after the given nonce + function nextNonceAfter( + address owner, + uint256 start + ) external view returns (uint256 nonce) { + uint248 word = uint248(start >> 8); + uint8 pos = uint8(start); + if (pos == type(uint8).max) { + // If the position is 255, we need to move to the next word + word++; + pos = 0; + } else { + // Otherwise, we just move to the next position + pos++; + } + nonce = _nextNonce(owner, word, pos); + } + + /// @notice Finds the next valid nonce for a user, starting from a given word and position. + /// @param owner The owner of the nonces + /// @param word Word to start looking from + /// @param pos Position inside the word to start looking from + function _nextNonce( + address owner, + uint248 word, + uint8 pos + ) internal view returns (uint256 nonce) { + while (true) { + uint256 bitmap = PERMIT2.nonceBitmap(owner, word); + + // Check if the bitmap is completely full + if (bitmap == type(uint256).max) { + // If so, move to the next word + ++word; + pos = 0; + continue; + } + if (pos != 0) { + // If the position is not 0, we need to shift the bitmap to ignore the bits before position + bitmap = bitmap >> pos; + } + // Find the first zero bit in the bitmap + while (bitmap & 1 == 1) { + bitmap = bitmap >> 1; + ++pos; + } + + return _nonceFromWordAndPos(word, pos); + } + } + + /// @notice Constructs a nonce from a word and a position inside the word + /// @param word The word containing the nonce + /// @param pos The position of the nonce inside the word + /// @return nonce The nonce constructed from the word and position + function _nonceFromWordAndPos( + uint248 word, + uint8 pos + ) internal pure returns (uint256 nonce) { + // The last 248 bits of the word are the nonce bits + nonce = uint256(word) << 8; + // The first 8 bits of the word are the position inside the word + nonce |= pos; + } +} diff --git a/test/solidity/Periphery/Permit2Proxy.t.sol b/test/solidity/Periphery/Permit2Proxy.t.sol new file mode 100644 index 000000000..8647c5dbc --- /dev/null +++ b/test/solidity/Periphery/Permit2Proxy.t.sol @@ -0,0 +1,651 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { Test, TestBase, DSTest, ILiFi, console, ERC20 } from "../utils/TestBase.sol"; +import { Permit2Proxy } from "lifi/Periphery/Permit2Proxy.sol"; +import { ISignatureTransfer } from "permit2/interfaces/ISignatureTransfer.sol"; +import { PermitHash } from "permit2/libraries/PermitHash.sol"; +import { ERC20 } from "../utils/TestBase.sol"; +import { PolygonBridgeFacet } from "lifi/Facets/PolygonBridgeFacet.sol"; +import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; + +contract Permit2ProxyTest is TestBase { + using PermitHash for ISignatureTransfer.PermitTransferFrom; + + /// Constants /// + + address internal constant PERMIT2_ADDRESS = + 0x000000000022D473030F116dDEE9F6B43aC78BA3; + uint256 internal PRIVATE_KEY = 0x1234567890; + address internal DIAMOND_ADDRESS = + 0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE; + + /// Storage /// + + bytes32 internal PERMIT_WITH_WITNESS_TYPEHASH; + Permit2Proxy internal permit2Proxy; + ISignatureTransfer internal uniPermit2; + address internal PERMIT2_USER; + + /// Types /// + + struct TestDataEIP2612 { + address tokenAddress; + address userWallet; + uint256 nonce; + uint256 deadline; + bytes diamondCalldata; + uint8 v; + bytes32 r; + bytes32 s; + } + + /// Errors /// + + error InvalidSigner(); + error InvalidNonce(); + error DiamondAddressNotWhitelisted(); + + function setUp() public { + customBlockNumberForForking = 20261175; + initTestBase(); + + uniPermit2 = ISignatureTransfer(PERMIT2_ADDRESS); + permit2Proxy = new Permit2Proxy(DIAMOND_ADDRESS, uniPermit2); + PERMIT_WITH_WITNESS_TYPEHASH = keccak256( + abi.encodePacked( + PermitHash._PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB, + permit2Proxy.WITNESS_TYPE_STRING() + ) + ); + + PERMIT2_USER = vm.addr(PRIVATE_KEY); + vm.label(PERMIT2_USER, "Permit2 User"); + deal(ADDRESS_USDC, PERMIT2_USER, 10000 ether); + + // Infinite approve to Permit2 + vm.prank(PERMIT2_USER); + ERC20(ADDRESS_USDC).approve(PERMIT2_ADDRESS, type(uint256).max); + } + + /// Tests /// + + /// EIP2612 (native permit) related test cases /// + + function test_can_execute_calldata_using_eip2612_signature_usdc() public { + vm.startPrank(PERMIT2_USER); + + // get token-specific domainSeparator + bytes32 domainSeparator = ERC20Permit(ADDRESS_USDC).DOMAIN_SEPARATOR(); + + // // using USDC on ETH for testing (implements EIP2612) + TestDataEIP2612 memory testdata = _getTestDataEIP2612( + ADDRESS_USDC, + domainSeparator, + block.timestamp + 1000 + ); + + // expect LifiTransferStarted event to be emitted by our diamond contract + vm.expectEmit(true, true, true, true, DIAMOND_ADDRESS); + emit LiFiTransferStarted(bridgeData); + + // call Permit2Proxy with signature + permit2Proxy.callDiamondWithEIP2612Signature( + ADDRESS_USDC, + defaultUSDCAmount, + testdata.deadline, + testdata.v, + testdata.r, + testdata.s, + testdata.diamondCalldata + ); + + vm.stopPrank(); + } + + function testRevert_cannot_use_eip2612_signature_twice() public { + vm.startPrank(PERMIT2_USER); + + // get token-specific domainSeparator + bytes32 domainSeparator = ERC20Permit(ADDRESS_USDC).DOMAIN_SEPARATOR(); + + // using USDC on ETH for testing (implements EIP2612) + TestDataEIP2612 memory testdata = _getTestDataEIP2612( + ADDRESS_USDC, + domainSeparator, + block.timestamp + 1000 + ); + + // call Permit2Proxy with signature + permit2Proxy.callDiamondWithEIP2612Signature( + ADDRESS_USDC, + defaultUSDCAmount, + testdata.deadline, + testdata.v, + testdata.r, + testdata.s, + testdata.diamondCalldata + ); + + // expect call to revert if same signature is used twice + vm.expectRevert("EIP2612: invalid signature"); + permit2Proxy.callDiamondWithEIP2612Signature( + ADDRESS_USDC, + defaultUSDCAmount, + testdata.deadline, + testdata.v, + testdata.r, + testdata.s, + testdata.diamondCalldata + ); + + vm.stopPrank(); + } + + function testRevert_cannot_use_expired_eip2612_signature() public { + vm.startPrank(PERMIT2_USER); + + // get token-specific domainSeparator + bytes32 domainSeparator = ERC20Permit(ADDRESS_USDC).DOMAIN_SEPARATOR(); + + // // using USDC on ETH for testing (implements EIP2612) + TestDataEIP2612 memory testdata = _getTestDataEIP2612( + ADDRESS_USDC, + domainSeparator, + block.timestamp - 1 // deadline in the past + ); + + // expect call to revert since signature deadline is in the past + vm.expectRevert("FiatTokenV2: permit is expired"); + + // call Permit2Proxy with signature + permit2Proxy.callDiamondWithEIP2612Signature( + ADDRESS_USDC, + defaultUSDCAmount, + testdata.deadline, + testdata.v, + testdata.r, + testdata.s, + testdata.diamondCalldata + ); + + vm.stopPrank(); + } + + function testRevert_cannot_use_invalid_eip2612_signature() public { + vm.startPrank(PERMIT2_USER); + + // get token-specific domainSeparator + bytes32 domainSeparator = ERC20Permit(ADDRESS_USDC).DOMAIN_SEPARATOR(); + + // // using USDC on ETH for testing (implements EIP2612) + TestDataEIP2612 memory testdata = _getTestDataEIP2612( + ADDRESS_USDC, + domainSeparator, + block.timestamp + ); + + // expect call to revert since signature deadline is in the past + vm.expectRevert("EIP2612: invalid signature"); + + // call Permit2Proxy with signature + permit2Proxy.callDiamondWithEIP2612Signature( + ADDRESS_USDC, + defaultUSDCAmount, + testdata.deadline, + testdata.v + 1, // invalid v value + testdata.r, + testdata.s, + testdata.diamondCalldata + ); + + vm.stopPrank(); + } + + function testRevert_sign_and_call_using_different_addresses() public { + vm.startPrank(USER_SENDER); + + // get token-specific domainSeparator + bytes32 domainSeparator = ERC20Permit(ADDRESS_USDC).DOMAIN_SEPARATOR(); + + // // using USDC on ETH for testing (implements EIP2612) + TestDataEIP2612 memory testdata = _getTestDataEIP2612( + ADDRESS_USDC, + domainSeparator, + block.timestamp + ); + + // expect call to revert since signature deadline is in the past + vm.expectRevert("EIP2612: invalid signature"); + // call Permit2Proxy with signature + permit2Proxy.callDiamondWithEIP2612Signature( + ADDRESS_USDC, + defaultUSDCAmount, + testdata.deadline, + testdata.v, + testdata.r, + testdata.s, + testdata.diamondCalldata + ); + + vm.stopPrank(); + } + + /// Permit2 specific tests /// + + function test_can_call_diamond_with_permit2() public { + bytes memory diamondCalldata; + ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; + bytes memory signature; + ( + diamondCalldata, + permitTransferFrom, + , + signature + ) = _getPermitTransferFromParams(); + + // Execute + vm.prank(PERMIT2_USER); + permit2Proxy.callDiamondWithPermit2( + diamondCalldata, + permitTransferFrom, + signature + ); + } + + function testRevert_cannot_call_diamond_with_permit2_using_different_addresses() + public + { + bytes memory diamondCalldata; + ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; + bytes memory signature; + ( + diamondCalldata, + permitTransferFrom, + , + signature + ) = _getPermitTransferFromParams(); + + // Execute + vm.prank(USER_SENDER); + vm.expectRevert(InvalidSigner.selector); + permit2Proxy.callDiamondWithPermit2( + diamondCalldata, + permitTransferFrom, + signature + ); + } + + function test_can_call_diamond_with_permit2_plus_witness() public { + bytes memory diamondCalldata; + ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; + bytes memory signature; + ( + diamondCalldata, + permitTransferFrom, + , + signature + ) = _getPermitWitnessTransferFromParams(); + + // Execute + permit2Proxy.callDiamondWithPermit2Witness( + diamondCalldata, + PERMIT2_USER, + permitTransferFrom, + signature + ); + } + + function test_can_generrate_a_valid_msg_hash_for_signing() public { + bytes32 msgHash; + bytes32 generatedMsgHash; + (, , msgHash, ) = _getPermitWitnessTransferFromParams(); + + generatedMsgHash = permit2Proxy.getPermit2MsgHash( + _getCalldataForBridging(), + ADDRESS_USDC, + defaultUSDCAmount, + 0, + block.timestamp + 1000 + ); + + assertEq(msgHash, generatedMsgHash); + } + + function testRevert_cannot_call_diamond_single_with_same_signature_more_than_once() + public + { + deal(ADDRESS_USDC, PERMIT2_USER, 10000 ether); + bytes memory diamondCalldata; + ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; + bytes memory signature; + ( + diamondCalldata, + permitTransferFrom, + , + signature + ) = _getPermitWitnessTransferFromParams(); + + // Execute x2 + permit2Proxy.callDiamondWithPermit2Witness( + diamondCalldata, + PERMIT2_USER, + permitTransferFrom, + signature + ); + vm.expectRevert(InvalidNonce.selector); + permit2Proxy.callDiamondWithPermit2Witness( + diamondCalldata, + PERMIT2_USER, + permitTransferFrom, + signature + ); + } + + function testRevert_cannot_set_different_calldata_than_intended() public { + deal(ADDRESS_USDC, PERMIT2_USER, 10000 ether); + bytes memory diamondCalldata; + ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; + bytes memory signature; + ( + diamondCalldata, + permitTransferFrom, + , + signature + ) = _getPermitWitnessTransferFromParams(); + + bytes memory MALICIOUS_CALLDATA; + + // Execute + vm.expectRevert(InvalidSigner.selector); + permit2Proxy.callDiamondWithPermit2Witness( + MALICIOUS_CALLDATA, + PERMIT2_USER, + permitTransferFrom, + signature + ); + } + + function testRevert_cannot_use_signature_from_another_wallet() public { + deal(ADDRESS_USDC, PERMIT2_USER, 10000 ether); + bytes memory diamondCalldata; + ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; + bytes32 msgHash; + ( + diamondCalldata, + permitTransferFrom, + msgHash, + + ) = _getPermitWitnessTransferFromParams(); + + bytes memory signature = _signMsgHash(msgHash, 987654321); + + // Execute + vm.expectRevert(InvalidSigner.selector); + permit2Proxy.callDiamondWithPermit2Witness( + diamondCalldata, + PERMIT2_USER, + permitTransferFrom, + signature + ); + } + + function testRevert_cannot_transfer_more_tokens_than_intended() public { + deal(ADDRESS_USDC, PERMIT2_USER, 10000 ether); + bytes memory diamondCalldata; + ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; + bytes32 msgHash; + ( + diamondCalldata, + permitTransferFrom, + msgHash, + + ) = _getPermitWitnessTransferFromParams(); + + bytes memory signature = _signMsgHash(msgHash, 987654321); + + permitTransferFrom.permitted.amount = 500 ether; + + // Execute + vm.expectRevert(InvalidSigner.selector); + permit2Proxy.callDiamondWithPermit2Witness( + diamondCalldata, + PERMIT2_USER, + permitTransferFrom, + signature + ); + } + + /// Helper Functions /// + + function _getPermitTransferFromParams() + internal + view + returns ( + bytes memory diamondCalldata, + ISignatureTransfer.PermitTransferFrom memory permitTransferFrom, + bytes32 msgHash, + bytes memory signature + ) + { + // Calldata + diamondCalldata = _getCalldataForBridging(); + + // Token Permissions + ISignatureTransfer.TokenPermissions + memory tokenPermissions = ISignatureTransfer.TokenPermissions( + ADDRESS_USDC, + defaultUSDCAmount + ); + bytes32 permit = _getTokenPermissionsHash(tokenPermissions); + + // Nonce + uint256 nonce = permit2Proxy.nextNonce(PERMIT2_USER); + + // PermitTransferFrom + msgHash = _getPermitTransferFromHash( + uniPermit2.DOMAIN_SEPARATOR(), + permit, + address(permit2Proxy), + nonce, + block.timestamp + 1000 + ); + + signature = _signMsgHash(msgHash, PRIVATE_KEY); + + permitTransferFrom = ISignatureTransfer.PermitTransferFrom( + tokenPermissions, + nonce, + block.timestamp + 1000 + ); + } + + function _getPermitWitnessTransferFromParams() + internal + view + returns ( + bytes memory diamondCalldata, + ISignatureTransfer.PermitTransferFrom memory permitTransferFrom, + bytes32 msgHash, + bytes memory signature + ) + { + // Token Permissions + ISignatureTransfer.TokenPermissions + memory tokenPermissions = ISignatureTransfer.TokenPermissions( + ADDRESS_USDC, + defaultUSDCAmount + ); + bytes32 permit = _getTokenPermissionsHash(tokenPermissions); + + // Witness + diamondCalldata = _getCalldataForBridging(); + Permit2Proxy.LiFiCall memory lifiCall = Permit2Proxy.LiFiCall( + DIAMOND_ADDRESS, + keccak256(diamondCalldata) + ); + bytes32 witness = _getWitnessHash(lifiCall); + + // Nonce + uint256 nonce = permit2Proxy.nextNonce(PERMIT2_USER); + + // PermitTransferWithWitness + msgHash = _getPermitWitnessTransferFromHash( + uniPermit2.DOMAIN_SEPARATOR(), + permit, + address(permit2Proxy), + nonce, + block.timestamp + 1000, + witness + ); + + signature = _signMsgHash(msgHash, PRIVATE_KEY); + + permitTransferFrom = ISignatureTransfer.PermitTransferFrom( + tokenPermissions, + nonce, + block.timestamp + 1000 + ); + } + + function _signMsgHash( + bytes32 msgHash, + uint256 privateKey + ) internal pure returns (bytes memory signature) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, msgHash); + signature = bytes.concat(r, s, bytes1(v)); + } + + function _getCalldataForBridging() + private + view + returns (bytes memory diamondCalldata) + { + bytes4 selector = PolygonBridgeFacet + .startBridgeTokensViaPolygonBridge + .selector; + + diamondCalldata = abi.encodeWithSelector(selector, bridgeData); + } + + function _getTokenPermissionsHash( + ISignatureTransfer.TokenPermissions memory tokenPermissions + ) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + PermitHash._TOKEN_PERMISSIONS_TYPEHASH, + tokenPermissions.token, + tokenPermissions.amount + ) + ); + } + + function _getWitnessHash( + Permit2Proxy.LiFiCall memory lifiCall + ) internal view returns (bytes32) { + return + keccak256(abi.encode(permit2Proxy.WITNESS_TYPEHASH(), lifiCall)); + } + + function _getPermitTransferFromHash( + bytes32 domainSeparator, + bytes32 permit, + address spender, + uint256 nonce, + uint256 deadline + ) internal pure returns (bytes32) { + bytes32 dataHash = keccak256( + abi.encode( + PermitHash._PERMIT_TRANSFER_FROM_TYPEHASH, + permit, + spender, + nonce, + deadline + ) + ); + + return + keccak256(abi.encodePacked("\x19\x01", domainSeparator, dataHash)); + } + + function _getPermitWitnessTransferFromHash( + bytes32 domainSeparator, + bytes32 permit, + address spender, + uint256 nonce, + uint256 deadline, + bytes32 witness + ) internal view returns (bytes32) { + bytes32 dataHash = keccak256( + abi.encode( + PERMIT_WITH_WITNESS_TYPEHASH, + permit, + spender, + nonce, + deadline, + witness + ) + ); + + return + keccak256(abi.encodePacked("\x19\x01", domainSeparator, dataHash)); + } + + function _getTestDataEIP2612( + address tokenAddress, + bytes32 domainSeparator, + uint256 deadline + ) internal view returns (TestDataEIP2612 memory testdata) { + testdata.tokenAddress = tokenAddress; + testdata.userWallet = PERMIT2_USER; + testdata.nonce = ERC20Permit(tokenAddress).nonces(testdata.userWallet); + testdata.deadline = deadline; + + // generate approval data to be signed by user + bytes32 digest = _generateEIP2612MsgHash( + testdata.userWallet, + address(permit2Proxy), + defaultUSDCAmount, + testdata.nonce, + testdata.deadline, + domainSeparator + ); + + // sign digest and return signature + (testdata.v, testdata.r, testdata.s) = vm.sign(PRIVATE_KEY, digest); + + // get calldata for bridging (simple USDC bridging via PolygonBridge) + testdata.diamondCalldata = _getCalldataForBridging(); + } + + function _generateEIP2612MsgHash( + address owner, + address spender, + uint256 amount, + uint256 nonce, + uint256 deadline, + bytes32 domainSeparator + ) internal pure returns (bytes32 digest) { + digest = keccak256( + abi.encodePacked( + "\x19\x01", + // Domain separator + domainSeparator, + // Permit struct + keccak256( + abi.encode( + keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ), + owner, + spender, + amount, + nonce, + deadline + ) + ) + ) + ); + } +} diff --git a/tsconfig.json b/tsconfig.json index 5fa2df4a1..5f60ec360 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es2020", "module": "commonjs", "strict": true, "esModuleInterop": true, @@ -8,8 +8,12 @@ "forceConsistentCasingInFileNames": true, "outDir": "dist", "resolveJsonModule": true, - "lib": ["es2015"], - "types": ["node"] + "lib": [ + "es2020" + ], + "types": [ + "node" + ] }, "include": [ "hardhat.config.ts", @@ -19,4 +23,4 @@ "typechain/**/*", "config" ] -} +} \ No newline at end of file