From e4c59473f22fe1624b59ac885d238da096513285 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 22 Aug 2024 19:20:35 +0300 Subject: [PATCH 01/37] Add Permit2Proxy --- test/solidity/Periphery/Permit2Proxy.t.sol | 45 ++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 test/solidity/Periphery/Permit2Proxy.t.sol diff --git a/test/solidity/Periphery/Permit2Proxy.t.sol b/test/solidity/Periphery/Permit2Proxy.t.sol new file mode 100644 index 000000000..d1c38a8fc --- /dev/null +++ b/test/solidity/Periphery/Permit2Proxy.t.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import { Test, console } from "forge-std/Test.sol"; +import { Permit2Proxy } from "lifi/Periphery/Permit2Proxy.sol"; +import { ISignatureTransfer } from "lifi/Interfaces/ISignatureTransfer.sol"; +import "forge-std/console.sol"; + +contract Permit2ProxyTest is Test { + Permit2Proxy public permit2proxy; + + function setUp() public { + permit2proxy = new Permit2Proxy(); + vm.createSelectFork(vm.envString("ETH_NODE_URI_MAINNET"), 20261175); + console.logAddress(address(permit2proxy)); + } + + function test_hardcoded_sig() public { + uint256 amount = 10 ** 18; + address token = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address owner = vm.addr( + 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 + ); + + ISignatureTransfer.PermitTransferFrom + memory transfer = ISignatureTransfer.PermitTransferFrom( + ISignatureTransfer.TokenPermissions(token, amount), + 0, + type(uint256).max + ); + + bytes + memory sig = hex"496bd11f1de6e3824f1d8032977c10f752ceb1bda1aec025b5b5a7956ffb0e182a0ae2d49de265ff334b47f7adf6233f5455b1d6d3a921ccdfcfbd4c2cab218e1b"; + console.logBytes(sig); + permit2proxy.diamondCallSingle( + address(0), + address(0), + keccak256(hex"deadbeef"), + hex"deadbeef", + owner, + transfer, + sig + ); + } +} From 797e772cfb4fb9aa17e8f259a0bb42d51e3e4fad Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 22 Aug 2024 19:24:50 +0300 Subject: [PATCH 02/37] Add Permit2Proxy --- src/Interfaces/IEIP712.sol | 6 + src/Interfaces/ISignatureTransfer.sol | 138 +++++++++++++++++++++ src/Periphery/Permit2Proxy.sol | 91 ++++++++++++++ test/solidity/Periphery/Permit2Proxy.t.sol | 2 +- 4 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 src/Interfaces/IEIP712.sol create mode 100644 src/Interfaces/ISignatureTransfer.sol create mode 100644 src/Periphery/Permit2Proxy.sol diff --git a/src/Interfaces/IEIP712.sol b/src/Interfaces/IEIP712.sol new file mode 100644 index 000000000..1a5be726b --- /dev/null +++ b/src/Interfaces/IEIP712.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +interface IEIP712 { + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/src/Interfaces/ISignatureTransfer.sol b/src/Interfaces/ISignatureTransfer.sol new file mode 100644 index 000000000..9b379e881 --- /dev/null +++ b/src/Interfaces/ISignatureTransfer.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import { IEIP712 } from "./IEIP712.sol"; + +/// @title SignatureTransfer +/// @notice Handles ERC20 token transfers through signature based actions +/// @dev Requires user's token approval on the Permit2 contract +interface ISignatureTransfer is IEIP712 { + /// @notice Thrown when the requested amount for a transfer is larger than the permissioned amount + /// @param maxAmount The maximum amount a spender can request to transfer + error InvalidAmount(uint256 maxAmount); + + /// @notice Thrown when the number of tokens permissioned to a spender does not match the number of tokens being transferred + /// @dev If the spender does not need to transfer the number of tokens permitted, the spender can request amount 0 to be transferred + error LengthMismatch(); + + /// @notice Emits an event when the owner successfully invalidates an unordered nonce. + event UnorderedNonceInvalidation( + address indexed owner, + uint256 word, + uint256 mask + ); + + /// @notice The token and amount details for a transfer signed in the permit transfer signature + struct TokenPermissions { + // ERC20 token address + address token; + // the maximum amount that can be spent + uint256 amount; + } + + /// @notice The signed permit message for a single token transfer + 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; + } + + /// @notice Specifies the recipient address and amount for batched transfers. + /// @dev Recipients and amounts correspond to the index of the signed token permissions array. + /// @dev Reverts if the requested amount is greater than the permitted signed amount. + struct SignatureTransferDetails { + // recipient address + address to; + // spender requested amount + uint256 requestedAmount; + } + + /// @notice Used to reconstruct the signed permit message for multiple token transfers + /// @dev Do not need to pass in spender address as it is required that it is msg.sender + /// @dev Note that a user still signs over a spender address + struct PermitBatchTransferFrom { + // the tokens and corresponding amounts permitted for a transfer + TokenPermissions[] permitted; + // a unique value for every token owner's signature to prevent signature replays + uint256 nonce; + // deadline on the permit signature + uint256 deadline; + } + + /// @notice A map from token owner address and a caller specified word index to a bitmap. Used to set bits in the bitmap to prevent against signature replay protection + /// @dev Uses unordered nonces so that permit messages do not need to be spent in a certain order + /// @dev The mapping is indexed first by the token owner, then by an index specified in the nonce + /// @dev It returns a uint256 bitmap + /// @dev The index, or wordPosition is capped at type(uint248).max + function nonceBitmap(address, uint256) external view returns (uint256); + + /// @notice Transfers a token using a signed permit message + /// @dev Reverts if the requested amount is greater than the permitted signed amount + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails The spender's requested transfer details for the permitted token + /// @param signature The signature to verify + function permitTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes calldata signature + ) external; + + /// @notice Transfers a token using a signed permit message + /// @notice Includes extra data provided by the caller to verify signature over + /// @dev The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition + /// @dev Reverts if the requested amount is greater than the permitted signed amount + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails The spender's requested transfer details for the permitted token + /// @param witness Extra data to include when checking the user signature + /// @param witnessTypeString The EIP-712 type definition for remaining string stub of the typehash + /// @param signature The signature to verify + function permitWitnessTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes32 witness, + string calldata witnessTypeString, + bytes calldata signature + ) external; + + /// @notice Transfers multiple tokens using a signed permit message + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails Specifies the recipient and requested amount for the token transfer + /// @param signature The signature to verify + function permitTransferFrom( + PermitBatchTransferFrom memory permit, + SignatureTransferDetails[] calldata transferDetails, + address owner, + bytes calldata signature + ) external; + + /// @notice Transfers multiple tokens using a signed permit message + /// @dev The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition + /// @notice Includes extra data provided by the caller to verify signature over + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails Specifies the recipient and requested amount for the token transfer + /// @param witness Extra data to include when checking the user signature + /// @param witnessTypeString The EIP-712 type definition for remaining string stub of the typehash + /// @param signature The signature to verify + function permitWitnessTransferFrom( + PermitBatchTransferFrom memory permit, + SignatureTransferDetails[] calldata transferDetails, + address owner, + bytes32 witness, + string calldata witnessTypeString, + bytes calldata signature + ) external; + + /// @notice Invalidates the bits specified in mask for the bitmap at the word position + /// @dev The wordPos is maxed at type(uint248).max + /// @param wordPos A number to index the nonceBitmap at + /// @param mask A bitmap masked against msg.sender's current bitmap at the word position + function invalidateUnorderedNonces(uint256 wordPos, uint256 mask) external; +} diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol new file mode 100644 index 000000000..e39f1cfed --- /dev/null +++ b/src/Periphery/Permit2Proxy.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { ISignatureTransfer } from "../Interfaces/ISignatureTransfer.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract Permit2Proxy { + ISignatureTransfer immutable PERMIT2 = + ISignatureTransfer(0x000000000022D473030F116dDEE9F6B43aC78BA3); + + error CallToDiamondFailed(bytes); + + // LIFI Specific Witness to verify + struct LIFICall { + address tokenReceiver; + address diamondAddress; + bytes32 diamondCalldataHash; + } + + string private constant WITNESS_TYPE_STRING = + "LIFICall witness)TokenPermissions(address token,uint256 amount)LIFICall(address tokenReceiver,address diamondAddress,bytes32 diamondCalldataHash)"; + bytes32 private WITNESS_TYPEHASH = + keccak256( + "LIFICall(address tokenReceiver,address diamondAddress,bytes32 diamondCalldataHash)" + ); + + function maxApproveERC20( + IERC20 assetId, + address spender, + uint256 amount + ) internal { + if (address(assetId) == address(0)) { + return; + } + + if (assetId.allowance(address(this), spender) < amount) { + SafeERC20.safeIncreaseAllowance(IERC20(assetId), spender, 0); + SafeERC20.safeIncreaseAllowance( + IERC20(assetId), + spender, + type(uint).max + ); + } + } + + function diamondCallSingle( + address _tokenReceiver, + address _diamondAddress, + bytes32 _diamondCalldataHash, + bytes calldata _diamondCalldata, + address _owner, + ISignatureTransfer.PermitTransferFrom calldata _permit, + bytes calldata _signature + ) external payable { + bytes32 witnessHash = keccak256( + abi.encode( + WITNESS_TYPEHASH, + LIFICall(_tokenReceiver, _diamondAddress, _diamondCalldataHash) + ) + ); + + PERMIT2.permitWitnessTransferFrom( + _permit, + ISignatureTransfer.SignatureTransferDetails({ + to: address(this), + requestedAmount: _permit.permitted.amount + }), + _owner, + witnessHash, + WITNESS_TYPE_STRING, + _signature + ); + + maxApproveERC20( + IERC20(_permit.permitted.token), + _diamondAddress, + _permit.permitted.amount + ); + + // call diamond with provided calldata + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory data) = _diamondAddress.call{ + value: msg.value + }(_diamondCalldata); + // throw error to make sure tx reverts if low-level call was unsuccessful + if (!success) { + revert CallToDiamondFailed(data); + } + } +} diff --git a/test/solidity/Periphery/Permit2Proxy.t.sol b/test/solidity/Periphery/Permit2Proxy.t.sol index d1c38a8fc..51e981cbf 100644 --- a/test/solidity/Periphery/Permit2Proxy.t.sol +++ b/test/solidity/Periphery/Permit2Proxy.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.17; import { Test, console } from "forge-std/Test.sol"; import { Permit2Proxy } from "lifi/Periphery/Permit2Proxy.sol"; From 48b97032e9263660e02ade7b097914761466ab53 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 22 Aug 2024 19:25:28 +0300 Subject: [PATCH 03/37] forge install: Permit2 --- .gitmodules | 3 +++ lib/Permit2 | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/Permit2 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/lib/Permit2 b/lib/Permit2 new file mode 160000 index 000000000..cc56ad0f3 --- /dev/null +++ b/lib/Permit2 @@ -0,0 +1 @@ +Subproject commit cc56ad0f3439c502c246fc5cfcc3db92bb8b7219 From 9795b16729cf542e78013bfc673b63e422903ece Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 23 Aug 2024 18:48:37 +0300 Subject: [PATCH 04/37] Get basic test working... --- remappings.txt | 3 +- src/Periphery/Permit2Proxy.sol | 50 +++++--- test/solidity/Periphery/Permit2Proxy.t.sol | 137 +++++++++++++++++---- 3 files changed, 151 insertions(+), 39 deletions(-) diff --git a/remappings.txt b/remappings.txt index cf75a7770..ed6769e68 100644 --- a/remappings.txt +++ b/remappings.txt @@ -9,7 +9,8 @@ celer-network/=lib/sgn-v2-contracts/ create3-factory/=lib/create3-factory/src/ solmate/=lib/solmate/src/ solady/=lib/solady/src/ - +permit2-test-utils/=lib/Permit2/test/utils/ +permit2/=lib/Permit2/src/ ds-test/=lib/ds-test/src/ forge-std/=lib/forge-std/src/ diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index e39f1cfed..2a23aa894 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -1,29 +1,42 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.17; -import { ISignatureTransfer } from "../Interfaces/ISignatureTransfer.sol"; +import { ISignatureTransfer } from "permit2/interfaces/ISignatureTransfer.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract Permit2Proxy { - ISignatureTransfer immutable PERMIT2 = - ISignatureTransfer(0x000000000022D473030F116dDEE9F6B43aC78BA3); + /// Storage /// - error CallToDiamondFailed(bytes); + 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)" + ); - // LIFI Specific Witness to verify + /// Types /// + + // @dev LIFI Specific Witness to verify struct LIFICall { address tokenReceiver; address diamondAddress; bytes32 diamondCalldataHash; } - string private constant WITNESS_TYPE_STRING = - "LIFICall witness)TokenPermissions(address token,uint256 amount)LIFICall(address tokenReceiver,address diamondAddress,bytes32 diamondCalldataHash)"; - bytes32 private WITNESS_TYPEHASH = - keccak256( - "LIFICall(address tokenReceiver,address diamondAddress,bytes32 diamondCalldataHash)" - ); + /// Errors /// + + error CallToDiamondFailed(bytes); + + /// Constructor /// + + constructor(ISignatureTransfer _permit2) { + PERMIT2 = _permit2; + } + + /// External Functions /// function maxApproveERC20( IERC20 assetId, @@ -47,16 +60,23 @@ contract Permit2Proxy { function diamondCallSingle( address _tokenReceiver, address _diamondAddress, - bytes32 _diamondCalldataHash, bytes calldata _diamondCalldata, address _owner, ISignatureTransfer.PermitTransferFrom calldata _permit, bytes calldata _signature ) external payable { - bytes32 witnessHash = keccak256( + LIFICall memory lifiCall = LIFICall( + _tokenReceiver, + _diamondAddress, + keccak256(_diamondCalldata) + ); + + bytes32 witness = keccak256( abi.encode( WITNESS_TYPEHASH, - LIFICall(_tokenReceiver, _diamondAddress, _diamondCalldataHash) + lifiCall.tokenReceiver, + lifiCall.diamondAddress, + lifiCall.diamondCalldataHash ) ); @@ -67,7 +87,7 @@ contract Permit2Proxy { requestedAmount: _permit.permitted.amount }), _owner, - witnessHash, + witness, WITNESS_TYPE_STRING, _signature ); diff --git a/test/solidity/Periphery/Permit2Proxy.t.sol b/test/solidity/Periphery/Permit2Proxy.t.sol index 51e981cbf..2632a6ce1 100644 --- a/test/solidity/Periphery/Permit2Proxy.t.sol +++ b/test/solidity/Periphery/Permit2Proxy.t.sol @@ -3,43 +3,134 @@ pragma solidity ^0.8.17; import { Test, console } from "forge-std/Test.sol"; import { Permit2Proxy } from "lifi/Periphery/Permit2Proxy.sol"; -import { ISignatureTransfer } from "lifi/Interfaces/ISignatureTransfer.sol"; +import { ISignatureTransfer } from "permit2/interfaces/ISignatureTransfer.sol"; +import { PermitHash } from "permit2/libraries/PermitHash.sol"; +import { ERC20 } from "../utils/TestBase.sol"; import "forge-std/console.sol"; contract Permit2ProxyTest is Test { - Permit2Proxy public permit2proxy; + using PermitHash for ISignatureTransfer.PermitTransferFrom; + address internal constant PERMIT2_ADDRESS = + 0x000000000022D473030F116dDEE9F6B43aC78BA3; + address internal constant LINK_ADDRESS = + 0x514910771AF9Ca656af840dff83E8264EcF986CA; + bytes32 internal PERMIT_WITH_WITNESS_TYPEHASH; + + Permit2Proxy internal permit2Proxy; + + ISignatureTransfer internal uniPermit2; + uint256 internal PRIVATE_KEY = 0x1234567890; + address internal USER; function setUp() public { - permit2proxy = new Permit2Proxy(); + uniPermit2 = ISignatureTransfer(PERMIT2_ADDRESS); + permit2Proxy = new Permit2Proxy(uniPermit2); + PERMIT_WITH_WITNESS_TYPEHASH = keccak256( + abi.encodePacked( + PermitHash._PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB, + permit2Proxy.WITNESS_TYPE_STRING() + ) + ); vm.createSelectFork(vm.envString("ETH_NODE_URI_MAINNET"), 20261175); - console.logAddress(address(permit2proxy)); + USER = vm.addr(PRIVATE_KEY); } - function test_hardcoded_sig() public { - uint256 amount = 10 ** 18; - address token = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - address owner = vm.addr( - 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 + function test_can_call_diamond_single() public { + // Token Permissions + ISignatureTransfer.TokenPermissions + memory tokenPermissions = ISignatureTransfer.TokenPermissions( + LINK_ADDRESS, // LINK + 100 ether + ); + bytes32 permit = getTokenPermissionsHash(tokenPermissions); + + // Witness + Permit2Proxy.LIFICall memory lifiCall = Permit2Proxy.LIFICall( + USER, + address(0x11f1), + keccak256(hex"d34db33f") ); + bytes32 witness = getWitnessHash(lifiCall); - ISignatureTransfer.PermitTransferFrom - memory transfer = ISignatureTransfer.PermitTransferFrom( - ISignatureTransfer.TokenPermissions(token, amount), + // PermitTransferWithWitness + bytes32 msgHash = getPermitWitnessTransferFromHash( + uniPermit2.DOMAIN_SEPARATOR(), + permit, + address(permit2Proxy), + 0, + type(uint256).max, + witness + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(PRIVATE_KEY, msgHash); + bytes memory sig = bytes.concat(r, s, bytes1(v)); + + deal(LINK_ADDRESS, USER, 10000 ether); + // Approve to Permit2 + vm.prank(USER); + ERC20(LINK_ADDRESS).approve(PERMIT2_ADDRESS, 100 ether); + + permit2Proxy.diamondCallSingle( + USER, + address(0x11f1), + hex"d34db33f", + USER, + ISignatureTransfer.PermitTransferFrom( + tokenPermissions, 0, type(uint256).max + ), + sig + ); + } + + function getTokenPermissionsHash( + ISignatureTransfer.TokenPermissions memory tokenPermissions + ) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + PermitHash._TOKEN_PERMISSIONS_TYPEHASH, + tokenPermissions.token, + tokenPermissions.amount + ) ); + } - bytes - memory sig = hex"496bd11f1de6e3824f1d8032977c10f752ceb1bda1aec025b5b5a7956ffb0e182a0ae2d49de265ff334b47f7adf6233f5455b1d6d3a921ccdfcfbd4c2cab218e1b"; - console.logBytes(sig); - permit2proxy.diamondCallSingle( - address(0), - address(0), - keccak256(hex"deadbeef"), - hex"deadbeef", - owner, - transfer, - sig + function getWitnessHash( + Permit2Proxy.LIFICall memory lifiCall + ) internal view returns (bytes32) { + return + keccak256( + abi.encode( + permit2Proxy.WITNESS_TYPEHASH(), + lifiCall.tokenReceiver, + lifiCall.diamondAddress, + lifiCall.diamondCalldataHash + ) + ); + } + + 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)); } } From 745c5c7b135b9458ea12e29c37f19e0338b33097 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 26 Aug 2024 14:32:17 +0300 Subject: [PATCH 05/37] Add more basic tests --- src/Periphery/Permit2Proxy.sol | 82 +++--- test/solidity/Periphery/Permit2Proxy.t.sol | 303 ++++++++++++++++++--- 2 files changed, 312 insertions(+), 73 deletions(-) diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index 2a23aa894..ef4040597 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -2,13 +2,16 @@ 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 "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -contract Permit2Proxy { +contract Permit2Proxy is TransferrableOwnership { /// Storage /// ISignatureTransfer public immutable PERMIT2; + mapping(address => bool) public diamondWhitelist; string public constant WITNESS_TYPE_STRING = "LIFICall witness)LIFICall(address tokenReceiver,address diamondAddress,bytes32 diamondCalldataHash)TokenPermissions(address token,uint256 amount)"; @@ -29,34 +32,23 @@ contract Permit2Proxy { /// Errors /// error CallToDiamondFailed(bytes); + error DiamondAddressNotWhitelisted(); + + /// Events /// + + event WhitelistUpdated(address[] addresses, bool[] values); /// Constructor /// - constructor(ISignatureTransfer _permit2) { + constructor( + address _owner, + ISignatureTransfer _permit2 + ) TransferrableOwnership(_owner) { PERMIT2 = _permit2; } /// External Functions /// - function maxApproveERC20( - IERC20 assetId, - address spender, - uint256 amount - ) internal { - if (address(assetId) == address(0)) { - return; - } - - if (assetId.allowance(address(this), spender) < amount) { - SafeERC20.safeIncreaseAllowance(IERC20(assetId), spender, 0); - SafeERC20.safeIncreaseAllowance( - IERC20(assetId), - spender, - type(uint).max - ); - } - } - function diamondCallSingle( address _tokenReceiver, address _diamondAddress, @@ -71,14 +63,7 @@ contract Permit2Proxy { keccak256(_diamondCalldata) ); - bytes32 witness = keccak256( - abi.encode( - WITNESS_TYPEHASH, - lifiCall.tokenReceiver, - lifiCall.diamondAddress, - lifiCall.diamondCalldataHash - ) - ); + bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, lifiCall)); PERMIT2.permitWitnessTransferFrom( _permit, @@ -92,20 +77,53 @@ contract Permit2Proxy { _signature ); - maxApproveERC20( + // maxApprove token to diamond if current allowance is insufficient + LibAsset.maxApproveERC20( IERC20(_permit.permitted.token), _diamondAddress, _permit.permitted.amount ); + _executeCalldata(_diamondAddress, _diamondCalldata); + } + + function _executeCalldata( + address diamondAddress, + bytes memory diamondCalldata + ) private { + // make sure diamondAddress is whitelisted + // this limits the usage of this Permit2Proxy contracts to only work with our diamond contracts + if (!diamondWhitelist[diamondAddress]) + revert DiamondAddressNotWhitelisted(); + // call diamond with provided calldata // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory data) = _diamondAddress.call{ + (bool success, bytes memory data) = diamondAddress.call{ value: msg.value - }(_diamondCalldata); + }(diamondCalldata); // throw error to make sure tx reverts if low-level call was unsuccessful if (!success) { revert CallToDiamondFailed(data); } } + + /// @notice Allows to update the whitelist of diamond contracts + /// @dev Admin function + /// @param addresses Addresses to be added (true) or removed (false) from whitelist + /// @param values Values for each address that should be updated + function updateWhitelist( + address[] calldata addresses, + bool[] calldata values + ) external onlyOwner { + for (uint i; i < addresses.length; ) { + // update whitelist address value + diamondWhitelist[addresses[i]] = values[i]; + + // gas-efficient way to increase the loop counter + unchecked { + ++i; + } + } + emit WhitelistUpdated(addresses, values); + } } diff --git a/test/solidity/Periphery/Permit2Proxy.t.sol b/test/solidity/Periphery/Permit2Proxy.t.sol index 2632a6ce1..ee1c6ee23 100644 --- a/test/solidity/Periphery/Permit2Proxy.t.sol +++ b/test/solidity/Periphery/Permit2Proxy.t.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.17; -import { Test, console } from "forge-std/Test.sol"; +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 "forge-std/console.sol"; +import { PolygonBridgeFacet } from "lifi/Facets/PolygonBridgeFacet.sol"; -contract Permit2ProxyTest is Test { +contract Permit2ProxyTest is TestBase { using PermitHash for ISignatureTransfer.PermitTransferFrom; address internal constant PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3; @@ -20,40 +20,261 @@ contract Permit2ProxyTest is Test { ISignatureTransfer internal uniPermit2; uint256 internal PRIVATE_KEY = 0x1234567890; - address internal USER; + address internal PERMIT2_USER; + + /// Errors /// + + error InvalidSigner(); + error InvalidNonce(); function setUp() public { + customBlockNumberForForking = 20261175; + initTestBase(); + uniPermit2 = ISignatureTransfer(PERMIT2_ADDRESS); - permit2Proxy = new Permit2Proxy(uniPermit2); + permit2Proxy = new Permit2Proxy(address(this), uniPermit2); PERMIT_WITH_WITNESS_TYPEHASH = keccak256( abi.encodePacked( PermitHash._PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB, permit2Proxy.WITNESS_TYPE_STRING() ) ); - vm.createSelectFork(vm.envString("ETH_NODE_URI_MAINNET"), 20261175); - USER = vm.addr(PRIVATE_KEY); + + address[] memory whitelist = new address[](1); + whitelist[0] = address(0x11f1); + bool[] memory allowed = new bool[](1); + allowed[0] = true; + permit2Proxy.updateWhitelist(whitelist, allowed); + PERMIT2_USER = vm.addr(PRIVATE_KEY); + vm.label(PERMIT2_USER, "Permit2 User"); + deal(LINK_ADDRESS, PERMIT2_USER, 10000 ether); + + // Infinite approve to Permit2 + vm.prank(PERMIT2_USER); + ERC20(LINK_ADDRESS).approve(PERMIT2_ADDRESS, type(uint256).max); } function test_can_call_diamond_single() public { + bytes memory diamondCalldata; + ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; + bytes memory signature; + ( + diamondCalldata, + permitTransferFrom, + , + signature + ) = _getPermitWitnessTransferFromParams(); + + // Execute + permit2Proxy.diamondCallSingle( + PERMIT2_USER, + address(0x11f1), + diamondCalldata, + PERMIT2_USER, + permitTransferFrom, + signature + ); + } + + function testRevert_cannot_call_diamond_single_with_same_signature_more_than_once() + public + { + deal(LINK_ADDRESS, PERMIT2_USER, 10000 ether); + bytes memory diamondCalldata; + ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; + bytes memory signature; + ( + diamondCalldata, + permitTransferFrom, + , + signature + ) = _getPermitWitnessTransferFromParams(); + + // Execute x2 + permit2Proxy.diamondCallSingle( + PERMIT2_USER, + address(0x11f1), + diamondCalldata, + PERMIT2_USER, + permitTransferFrom, + signature + ); + vm.expectRevert(InvalidNonce.selector); + permit2Proxy.diamondCallSingle( + PERMIT2_USER, + address(0x11f1), + diamondCalldata, + PERMIT2_USER, + permitTransferFrom, + signature + ); + } + + function testRevert_cannot_set_different_receiver_than_intended() public { + deal(LINK_ADDRESS, PERMIT2_USER, 10000 ether); + bytes memory diamondCalldata; + ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; + bytes memory signature; + ( + diamondCalldata, + permitTransferFrom, + , + signature + ) = _getPermitWitnessTransferFromParams(); + + address MALICIOUS_RECEIVER; + + // Execute + vm.expectRevert(InvalidSigner.selector); + permit2Proxy.diamondCallSingle( + MALICIOUS_RECEIVER, + address(0x11f1), + diamondCalldata, + PERMIT2_USER, + permitTransferFrom, + signature + ); + } + + function testRevert_cannot_set_different_diamond_address_than_intended() + public + { + deal(LINK_ADDRESS, PERMIT2_USER, 10000 ether); + bytes memory diamondCalldata; + ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; + bytes memory signature; + ( + diamondCalldata, + permitTransferFrom, + , + signature + ) = _getPermitWitnessTransferFromParams(); + + address MALICIOUS_CONTRACT; + + // Execute + vm.expectRevert(InvalidSigner.selector); + permit2Proxy.diamondCallSingle( + PERMIT2_USER, + MALICIOUS_CONTRACT, + diamondCalldata, + PERMIT2_USER, + permitTransferFrom, + signature + ); + } + + function testRevert_cannot_set_different_calldata_than_intended() public { + deal(LINK_ADDRESS, 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.diamondCallSingle( + PERMIT2_USER, + address(0x11f1), + MALICIOUS_CALLDATA, + PERMIT2_USER, + permitTransferFrom, + signature + ); + } + + function testRevert_cannot_use_signature_from_another_wallet() public { + deal(LINK_ADDRESS, 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.diamondCallSingle( + PERMIT2_USER, + address(0x11f1), + diamondCalldata, + PERMIT2_USER, + permitTransferFrom, + signature + ); + } + + function testRevert_cannot_transfer_more_tokens_than_intended() public { + deal(LINK_ADDRESS, 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.diamondCallSingle( + PERMIT2_USER, + address(0x11f1), + diamondCalldata, + PERMIT2_USER, + permitTransferFrom, + signature + ); + } + + /// Helper Functions /// + + 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( LINK_ADDRESS, // LINK 100 ether ); - bytes32 permit = getTokenPermissionsHash(tokenPermissions); + bytes32 permit = _getTokenPermissionsHash(tokenPermissions); // Witness + diamondCalldata = _getCalldataForBridging(); Permit2Proxy.LIFICall memory lifiCall = Permit2Proxy.LIFICall( - USER, + PERMIT2_USER, address(0x11f1), - keccak256(hex"d34db33f") + keccak256(diamondCalldata) ); - bytes32 witness = getWitnessHash(lifiCall); + bytes32 witness = _getWitnessHash(lifiCall); // PermitTransferWithWitness - bytes32 msgHash = getPermitWitnessTransferFromHash( + msgHash = _getPermitWitnessTransferFromHash( uniPermit2.DOMAIN_SEPARATOR(), permit, address(permit2Proxy), @@ -62,29 +283,36 @@ contract Permit2ProxyTest is Test { witness ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(PRIVATE_KEY, msgHash); - bytes memory sig = bytes.concat(r, s, bytes1(v)); - - deal(LINK_ADDRESS, USER, 10000 ether); - // Approve to Permit2 - vm.prank(USER); - ERC20(LINK_ADDRESS).approve(PERMIT2_ADDRESS, 100 ether); + signature = _signMsgHash(msgHash, PRIVATE_KEY); - permit2Proxy.diamondCallSingle( - USER, - address(0x11f1), - hex"d34db33f", - USER, - ISignatureTransfer.PermitTransferFrom( - tokenPermissions, - 0, - type(uint256).max - ), - sig + permitTransferFrom = ISignatureTransfer.PermitTransferFrom( + tokenPermissions, + 0, + type(uint256).max ); } - function getTokenPermissionsHash( + 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 @@ -97,21 +325,14 @@ contract Permit2ProxyTest is Test { ); } - function getWitnessHash( + function _getWitnessHash( Permit2Proxy.LIFICall memory lifiCall ) internal view returns (bytes32) { return - keccak256( - abi.encode( - permit2Proxy.WITNESS_TYPEHASH(), - lifiCall.tokenReceiver, - lifiCall.diamondAddress, - lifiCall.diamondCalldataHash - ) - ); + keccak256(abi.encode(permit2Proxy.WITNESS_TYPEHASH(), lifiCall)); } - function getPermitWitnessTransferFromHash( + function _getPermitWitnessTransferFromHash( bytes32 domainSeparator, bytes32 permit, address spender, From e18643630484cec7e4cda76cdc8643c36aab4706 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 26 Aug 2024 15:11:24 +0300 Subject: [PATCH 06/37] Remove superfluous receiver param --- src/Periphery/Permit2Proxy.sol | 14 ++-- test/solidity/Periphery/Permit2Proxy.t.sol | 80 +++++++--------------- 2 files changed, 34 insertions(+), 60 deletions(-) diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index ef4040597..21b3721b9 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -24,7 +24,6 @@ contract Permit2Proxy is TransferrableOwnership { // @dev LIFI Specific Witness to verify struct LIFICall { - address tokenReceiver; address diamondAddress; bytes32 diamondCalldataHash; } @@ -49,16 +48,21 @@ contract Permit2Proxy is TransferrableOwnership { /// External Functions /// + /// @notice Forwards a call to a whitelisted LIFI diamond + /// pulling tokens from the user using Uniswap Permit2 + /// @param _diamondAddress the diamond contract to execute the call + /// @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 diamondCallSingle( - address _tokenReceiver, address _diamondAddress, bytes calldata _diamondCalldata, - address _owner, + address _signer, ISignatureTransfer.PermitTransferFrom calldata _permit, bytes calldata _signature ) external payable { LIFICall memory lifiCall = LIFICall( - _tokenReceiver, _diamondAddress, keccak256(_diamondCalldata) ); @@ -71,7 +75,7 @@ contract Permit2Proxy is TransferrableOwnership { to: address(this), requestedAmount: _permit.permitted.amount }), - _owner, + _signer, witness, WITNESS_TYPE_STRING, _signature diff --git a/test/solidity/Periphery/Permit2Proxy.t.sol b/test/solidity/Periphery/Permit2Proxy.t.sol index ee1c6ee23..547ff477c 100644 --- a/test/solidity/Periphery/Permit2Proxy.t.sol +++ b/test/solidity/Periphery/Permit2Proxy.t.sol @@ -10,16 +10,20 @@ import { PolygonBridgeFacet } from "lifi/Facets/PolygonBridgeFacet.sol"; contract Permit2ProxyTest is TestBase { using PermitHash for ISignatureTransfer.PermitTransferFrom; + + /// Constants /// + address internal constant PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3; - address internal constant LINK_ADDRESS = - 0x514910771AF9Ca656af840dff83E8264EcF986CA; - bytes32 internal PERMIT_WITH_WITNESS_TYPEHASH; + uint256 internal PRIVATE_KEY = 0x1234567890; + address internal constant DIAMOND_ADDRESS = + 0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE; - Permit2Proxy internal permit2Proxy; + /// Storage /// + bytes32 internal PERMIT_WITH_WITNESS_TYPEHASH; + Permit2Proxy internal permit2Proxy; ISignatureTransfer internal uniPermit2; - uint256 internal PRIVATE_KEY = 0x1234567890; address internal PERMIT2_USER; /// Errors /// @@ -41,17 +45,17 @@ contract Permit2ProxyTest is TestBase { ); address[] memory whitelist = new address[](1); - whitelist[0] = address(0x11f1); + whitelist[0] = DIAMOND_ADDRESS; bool[] memory allowed = new bool[](1); allowed[0] = true; permit2Proxy.updateWhitelist(whitelist, allowed); PERMIT2_USER = vm.addr(PRIVATE_KEY); vm.label(PERMIT2_USER, "Permit2 User"); - deal(LINK_ADDRESS, PERMIT2_USER, 10000 ether); + deal(ADDRESS_USDC, PERMIT2_USER, 10000 ether); // Infinite approve to Permit2 vm.prank(PERMIT2_USER); - ERC20(LINK_ADDRESS).approve(PERMIT2_ADDRESS, type(uint256).max); + ERC20(ADDRESS_USDC).approve(PERMIT2_ADDRESS, type(uint256).max); } function test_can_call_diamond_single() public { @@ -67,8 +71,7 @@ contract Permit2ProxyTest is TestBase { // Execute permit2Proxy.diamondCallSingle( - PERMIT2_USER, - address(0x11f1), + DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, permitTransferFrom, @@ -79,7 +82,7 @@ contract Permit2ProxyTest is TestBase { function testRevert_cannot_call_diamond_single_with_same_signature_more_than_once() public { - deal(LINK_ADDRESS, PERMIT2_USER, 10000 ether); + deal(ADDRESS_USDC, PERMIT2_USER, 10000 ether); bytes memory diamondCalldata; ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; bytes memory signature; @@ -92,8 +95,7 @@ contract Permit2ProxyTest is TestBase { // Execute x2 permit2Proxy.diamondCallSingle( - PERMIT2_USER, - address(0x11f1), + DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, permitTransferFrom, @@ -101,34 +103,7 @@ contract Permit2ProxyTest is TestBase { ); vm.expectRevert(InvalidNonce.selector); permit2Proxy.diamondCallSingle( - PERMIT2_USER, - address(0x11f1), - diamondCalldata, - PERMIT2_USER, - permitTransferFrom, - signature - ); - } - - function testRevert_cannot_set_different_receiver_than_intended() public { - deal(LINK_ADDRESS, PERMIT2_USER, 10000 ether); - bytes memory diamondCalldata; - ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; - bytes memory signature; - ( - diamondCalldata, - permitTransferFrom, - , - signature - ) = _getPermitWitnessTransferFromParams(); - - address MALICIOUS_RECEIVER; - - // Execute - vm.expectRevert(InvalidSigner.selector); - permit2Proxy.diamondCallSingle( - MALICIOUS_RECEIVER, - address(0x11f1), + DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, permitTransferFrom, @@ -139,7 +114,7 @@ contract Permit2ProxyTest is TestBase { function testRevert_cannot_set_different_diamond_address_than_intended() public { - deal(LINK_ADDRESS, PERMIT2_USER, 10000 ether); + deal(ADDRESS_USDC, PERMIT2_USER, 10000 ether); bytes memory diamondCalldata; ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; bytes memory signature; @@ -155,7 +130,6 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(InvalidSigner.selector); permit2Proxy.diamondCallSingle( - PERMIT2_USER, MALICIOUS_CONTRACT, diamondCalldata, PERMIT2_USER, @@ -165,7 +139,7 @@ contract Permit2ProxyTest is TestBase { } function testRevert_cannot_set_different_calldata_than_intended() public { - deal(LINK_ADDRESS, PERMIT2_USER, 10000 ether); + deal(ADDRESS_USDC, PERMIT2_USER, 10000 ether); bytes memory diamondCalldata; ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; bytes memory signature; @@ -181,8 +155,7 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(InvalidSigner.selector); permit2Proxy.diamondCallSingle( - PERMIT2_USER, - address(0x11f1), + DIAMOND_ADDRESS, MALICIOUS_CALLDATA, PERMIT2_USER, permitTransferFrom, @@ -191,7 +164,7 @@ contract Permit2ProxyTest is TestBase { } function testRevert_cannot_use_signature_from_another_wallet() public { - deal(LINK_ADDRESS, PERMIT2_USER, 10000 ether); + deal(ADDRESS_USDC, PERMIT2_USER, 10000 ether); bytes memory diamondCalldata; ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; bytes32 msgHash; @@ -207,8 +180,7 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(InvalidSigner.selector); permit2Proxy.diamondCallSingle( - PERMIT2_USER, - address(0x11f1), + DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, permitTransferFrom, @@ -217,7 +189,7 @@ contract Permit2ProxyTest is TestBase { } function testRevert_cannot_transfer_more_tokens_than_intended() public { - deal(LINK_ADDRESS, PERMIT2_USER, 10000 ether); + deal(ADDRESS_USDC, PERMIT2_USER, 10000 ether); bytes memory diamondCalldata; ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; bytes32 msgHash; @@ -235,8 +207,7 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(InvalidSigner.selector); permit2Proxy.diamondCallSingle( - PERMIT2_USER, - address(0x11f1), + DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, permitTransferFrom, @@ -259,7 +230,7 @@ contract Permit2ProxyTest is TestBase { // Token Permissions ISignatureTransfer.TokenPermissions memory tokenPermissions = ISignatureTransfer.TokenPermissions( - LINK_ADDRESS, // LINK + ADDRESS_USDC, // LINK 100 ether ); bytes32 permit = _getTokenPermissionsHash(tokenPermissions); @@ -267,8 +238,7 @@ contract Permit2ProxyTest is TestBase { // Witness diamondCalldata = _getCalldataForBridging(); Permit2Proxy.LIFICall memory lifiCall = Permit2Proxy.LIFICall( - PERMIT2_USER, - address(0x11f1), + DIAMOND_ADDRESS, keccak256(diamondCalldata) ); bytes32 witness = _getWitnessHash(lifiCall); From 143d4c21f96f0424690486c6183ca1f50503c5ee Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 26 Aug 2024 15:39:54 +0300 Subject: [PATCH 07/37] Add utility method for getting a valid and working msgHash to sign --- src/Periphery/Permit2Proxy.sol | 134 ++++++++++++++++++--- test/solidity/Periphery/Permit2Proxy.t.sol | 47 +++++++- 2 files changed, 158 insertions(+), 23 deletions(-) diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index 21b3721b9..dbc37dc72 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -4,6 +4,7 @@ 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"; @@ -19,6 +20,7 @@ contract Permit2Proxy is TransferrableOwnership { keccak256( "LIFICall(address tokenReceiver,address diamondAddress,bytes32 diamondCalldataHash)" ); + bytes32 public immutable PERMIT_WITH_WITNESS_TYPEHASH; /// Types /// @@ -44,6 +46,13 @@ contract Permit2Proxy is TransferrableOwnership { ISignatureTransfer _permit2 ) TransferrableOwnership(_owner) { PERMIT2 = _permit2; + + PERMIT_WITH_WITNESS_TYPEHASH = keccak256( + abi.encodePacked( + PermitHash._PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB, + WITNESS_TYPE_STRING + ) + ); } /// External Functions /// @@ -91,26 +100,6 @@ contract Permit2Proxy is TransferrableOwnership { _executeCalldata(_diamondAddress, _diamondCalldata); } - function _executeCalldata( - address diamondAddress, - bytes memory diamondCalldata - ) private { - // make sure diamondAddress is whitelisted - // this limits the usage of this Permit2Proxy contracts to only work with our diamond contracts - if (!diamondWhitelist[diamondAddress]) - revert DiamondAddressNotWhitelisted(); - - // call diamond with provided calldata - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory data) = diamondAddress.call{ - value: msg.value - }(diamondCalldata); - // throw error to make sure tx reverts if low-level call was unsuccessful - if (!success) { - revert CallToDiamondFailed(data); - } - } - /// @notice Allows to update the whitelist of diamond contracts /// @dev Admin function /// @param addresses Addresses to be added (true) or removed (false) from whitelist @@ -130,4 +119,109 @@ contract Permit2Proxy is TransferrableOwnership { } emit WhitelistUpdated(addresses, values); } + + /// @notice utitlity method for constructing a valid Permit2 message hash + /// @param _diamondAddress the diamond address to call + /// @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( + address _diamondAddress, + 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( + _diamondAddress, + 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( + address diamondAddress, + bytes memory diamondCalldata + ) internal { + // make sure diamondAddress is whitelisted + // this limits the usage of this Permit2Proxy contracts to only work with our diamond contracts + if (!diamondWhitelist[diamondAddress]) + revert DiamondAddressNotWhitelisted(); + + // call diamond with provided calldata + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory data) = diamondAddress.call{ + value: msg.value + }(diamondCalldata); + // throw error to make sure tx reverts if low-level call was unsuccessful + if (!success) { + revert CallToDiamondFailed(data); + } + } } diff --git a/test/solidity/Periphery/Permit2Proxy.t.sol b/test/solidity/Periphery/Permit2Proxy.t.sol index 547ff477c..52ceae401 100644 --- a/test/solidity/Periphery/Permit2Proxy.t.sol +++ b/test/solidity/Periphery/Permit2Proxy.t.sol @@ -16,7 +16,7 @@ contract Permit2ProxyTest is TestBase { address internal constant PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3; uint256 internal PRIVATE_KEY = 0x1234567890; - address internal constant DIAMOND_ADDRESS = + address internal DIAMOND_ADDRESS = 0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE; /// Storage /// @@ -30,6 +30,7 @@ contract Permit2ProxyTest is TestBase { error InvalidSigner(); error InvalidNonce(); + error DiamondAddressNotWhitelisted(); function setUp() public { customBlockNumberForForking = 20261175; @@ -79,6 +80,46 @@ contract Permit2ProxyTest is TestBase { ); } + function test_can_generrate_a_valid_msg_hash_for_signing() public { + bytes32 msgHash; + bytes32 generatedMsgHash; + (, , msgHash, ) = _getPermitWitnessTransferFromParams(); + + generatedMsgHash = permit2Proxy.getPermit2MsgHash( + DIAMOND_ADDRESS, + _getCalldataForBridging(), + ADDRESS_USDC, + defaultUSDCAmount, + 0, + type(uint256).max + ); + + assertEq(msgHash, generatedMsgHash); + } + + function testRevery_cannot_call_unwhitelisted_diamond() public { + DIAMOND_ADDRESS = address(0x11f1); // Not whitelisted + bytes memory diamondCalldata; + ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; + bytes memory signature; + ( + diamondCalldata, + permitTransferFrom, + , + signature + ) = _getPermitWitnessTransferFromParams(); + + // Execute + vm.expectRevert(DiamondAddressNotWhitelisted.selector); + permit2Proxy.diamondCallSingle( + DIAMOND_ADDRESS, + diamondCalldata, + PERMIT2_USER, + permitTransferFrom, + signature + ); + } + function testRevert_cannot_call_diamond_single_with_same_signature_more_than_once() public { @@ -230,8 +271,8 @@ contract Permit2ProxyTest is TestBase { // Token Permissions ISignatureTransfer.TokenPermissions memory tokenPermissions = ISignatureTransfer.TokenPermissions( - ADDRESS_USDC, // LINK - 100 ether + ADDRESS_USDC, + defaultUSDCAmount ); bytes32 permit = _getTokenPermissionsHash(tokenPermissions); From 779bd8bfb4fe7e8460b98246052ac77279ada227 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 26 Aug 2024 15:47:18 +0300 Subject: [PATCH 08/37] Change name to be more specific --- src/Periphery/Permit2Proxy.sol | 2 +- test/solidity/Periphery/Permit2Proxy.t.sol | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index dbc37dc72..5fb008b27 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -64,7 +64,7 @@ contract Permit2Proxy is TransferrableOwnership { /// @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 diamondCallSingle( + function callDiamondUsingPermit2Single( address _diamondAddress, bytes calldata _diamondCalldata, address _signer, diff --git a/test/solidity/Periphery/Permit2Proxy.t.sol b/test/solidity/Periphery/Permit2Proxy.t.sol index 52ceae401..96a9596af 100644 --- a/test/solidity/Periphery/Permit2Proxy.t.sol +++ b/test/solidity/Periphery/Permit2Proxy.t.sol @@ -71,7 +71,7 @@ contract Permit2ProxyTest is TestBase { ) = _getPermitWitnessTransferFromParams(); // Execute - permit2Proxy.diamondCallSingle( + permit2Proxy.callDiamondUsingPermit2Single( DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, @@ -111,7 +111,7 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(DiamondAddressNotWhitelisted.selector); - permit2Proxy.diamondCallSingle( + permit2Proxy.callDiamondUsingPermit2Single( DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, @@ -135,7 +135,7 @@ contract Permit2ProxyTest is TestBase { ) = _getPermitWitnessTransferFromParams(); // Execute x2 - permit2Proxy.diamondCallSingle( + permit2Proxy.callDiamondUsingPermit2Single( DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, @@ -143,7 +143,7 @@ contract Permit2ProxyTest is TestBase { signature ); vm.expectRevert(InvalidNonce.selector); - permit2Proxy.diamondCallSingle( + permit2Proxy.callDiamondUsingPermit2Single( DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, @@ -170,7 +170,7 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(InvalidSigner.selector); - permit2Proxy.diamondCallSingle( + permit2Proxy.callDiamondUsingPermit2Single( MALICIOUS_CONTRACT, diamondCalldata, PERMIT2_USER, @@ -195,7 +195,7 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(InvalidSigner.selector); - permit2Proxy.diamondCallSingle( + permit2Proxy.callDiamondUsingPermit2Single( DIAMOND_ADDRESS, MALICIOUS_CALLDATA, PERMIT2_USER, @@ -220,7 +220,7 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(InvalidSigner.selector); - permit2Proxy.diamondCallSingle( + permit2Proxy.callDiamondUsingPermit2Single( DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, @@ -247,7 +247,7 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(InvalidSigner.selector); - permit2Proxy.diamondCallSingle( + permit2Proxy.callDiamondUsingPermit2Single( DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, From f36a67e2efb8f1a3f968b5d3e5ba65ad61f7168d Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 26 Aug 2024 16:00:06 +0300 Subject: [PATCH 09/37] Add Permit "v1" functionality --- src/Periphery/Permit2Proxy.sol | 47 +++++++++++++++++++++- test/solidity/Periphery/Permit2Proxy.t.sol | 22 +++++----- 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index 5fb008b27..94915daf2 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -7,6 +7,7 @@ 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"; contract Permit2Proxy is TransferrableOwnership { /// Storage /// @@ -57,6 +58,50 @@ contract Permit2Proxy is TransferrableOwnership { /// 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) + /// @param tokenAddress Address of the token to be bridged + /// @param owner Owner of the tokens 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 diamondAddress Address of the token to be bridged + /// @param diamondCalldata Address of the token to be bridged + function callDiamondWithEIP2612Signature( + address tokenAddress, + address owner, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s, + address diamondAddress, + bytes calldata diamondCalldata + ) public payable { + // call permit function of token contract to register approval using signature + ERC20Permit(tokenAddress).permit( + owner, + address(this), + amount, + deadline, + v, + r, + s + ); + + // deposit assets + LibAsset.transferFromERC20(tokenAddress, owner, address(this), amount); + + // maxApprove token to diamond if current allowance is insufficient + LibAsset.maxApproveERC20(IERC20(tokenAddress), diamondAddress, amount); + + // call our diamond to execute calldata + _executeCalldata(diamondAddress, diamondCalldata); + } + /// @notice Forwards a call to a whitelisted LIFI diamond /// pulling tokens from the user using Uniswap Permit2 /// @param _diamondAddress the diamond contract to execute the call @@ -64,7 +109,7 @@ contract Permit2Proxy is TransferrableOwnership { /// @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 callDiamondUsingPermit2Single( + function callDiamondWithPermit2SignatureSingle( address _diamondAddress, bytes calldata _diamondCalldata, address _signer, diff --git a/test/solidity/Periphery/Permit2Proxy.t.sol b/test/solidity/Periphery/Permit2Proxy.t.sol index 96a9596af..8157f670a 100644 --- a/test/solidity/Periphery/Permit2Proxy.t.sol +++ b/test/solidity/Periphery/Permit2Proxy.t.sol @@ -71,7 +71,7 @@ contract Permit2ProxyTest is TestBase { ) = _getPermitWitnessTransferFromParams(); // Execute - permit2Proxy.callDiamondUsingPermit2Single( + permit2Proxy.callDiamondWithPermit2SignatureSingle( DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, @@ -91,7 +91,7 @@ contract Permit2ProxyTest is TestBase { ADDRESS_USDC, defaultUSDCAmount, 0, - type(uint256).max + block.timestamp + 1000 ); assertEq(msgHash, generatedMsgHash); @@ -111,7 +111,7 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(DiamondAddressNotWhitelisted.selector); - permit2Proxy.callDiamondUsingPermit2Single( + permit2Proxy.callDiamondWithPermit2SignatureSingle( DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, @@ -135,7 +135,7 @@ contract Permit2ProxyTest is TestBase { ) = _getPermitWitnessTransferFromParams(); // Execute x2 - permit2Proxy.callDiamondUsingPermit2Single( + permit2Proxy.callDiamondWithPermit2SignatureSingle( DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, @@ -143,7 +143,7 @@ contract Permit2ProxyTest is TestBase { signature ); vm.expectRevert(InvalidNonce.selector); - permit2Proxy.callDiamondUsingPermit2Single( + permit2Proxy.callDiamondWithPermit2SignatureSingle( DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, @@ -170,7 +170,7 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(InvalidSigner.selector); - permit2Proxy.callDiamondUsingPermit2Single( + permit2Proxy.callDiamondWithPermit2SignatureSingle( MALICIOUS_CONTRACT, diamondCalldata, PERMIT2_USER, @@ -195,7 +195,7 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(InvalidSigner.selector); - permit2Proxy.callDiamondUsingPermit2Single( + permit2Proxy.callDiamondWithPermit2SignatureSingle( DIAMOND_ADDRESS, MALICIOUS_CALLDATA, PERMIT2_USER, @@ -220,7 +220,7 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(InvalidSigner.selector); - permit2Proxy.callDiamondUsingPermit2Single( + permit2Proxy.callDiamondWithPermit2SignatureSingle( DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, @@ -247,7 +247,7 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(InvalidSigner.selector); - permit2Proxy.callDiamondUsingPermit2Single( + permit2Proxy.callDiamondWithPermit2SignatureSingle( DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, @@ -290,7 +290,7 @@ contract Permit2ProxyTest is TestBase { permit, address(permit2Proxy), 0, - type(uint256).max, + block.timestamp + 1000, witness ); @@ -299,7 +299,7 @@ contract Permit2ProxyTest is TestBase { permitTransferFrom = ISignatureTransfer.PermitTransferFrom( tokenPermissions, 0, - type(uint256).max + block.timestamp + 1000 ); } From 48de4770cc5aa4f78e88a1518e25d02e5848feb0 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 26 Aug 2024 16:26:08 +0300 Subject: [PATCH 10/37] Add Permit "v1" tests --- test/solidity/Periphery/Permit2Proxy.t.sol | 217 +++++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/test/solidity/Periphery/Permit2Proxy.t.sol b/test/solidity/Periphery/Permit2Proxy.t.sol index 8157f670a..a17fb46c5 100644 --- a/test/solidity/Periphery/Permit2Proxy.t.sol +++ b/test/solidity/Periphery/Permit2Proxy.t.sol @@ -7,6 +7,7 @@ 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; @@ -26,6 +27,19 @@ contract Permit2ProxyTest is TestBase { 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(); @@ -59,6 +73,152 @@ contract Permit2ProxyTest is TestBase { 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, + PERMIT2_USER, + defaultUSDCAmount, + testdata.deadline, + testdata.v, + testdata.r, + testdata.s, + DIAMOND_ADDRESS, + testdata.diamondCalldata + ); + + vm.stopPrank(); + } + + function testRevertcannotUseEIP2612SignatureTwice() 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, + PERMIT2_USER, + defaultUSDCAmount, + testdata.deadline, + testdata.v, + testdata.r, + testdata.s, + DIAMOND_ADDRESS, + testdata.diamondCalldata + ); + + // expect call to revert if same signature is used twice + vm.expectRevert("EIP2612: invalid signature"); + permit2Proxy.callDiamondWithEIP2612Signature( + ADDRESS_USDC, + PERMIT2_USER, + defaultUSDCAmount, + testdata.deadline, + testdata.v, + testdata.r, + testdata.s, + DIAMOND_ADDRESS, + testdata.diamondCalldata + ); + + vm.stopPrank(); + } + + function testRevertCannotUseExpiredEIP2612Signature() 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, + PERMIT2_USER, + defaultUSDCAmount, + testdata.deadline, + testdata.v, + testdata.r, + testdata.s, + DIAMOND_ADDRESS, + testdata.diamondCalldata + ); + + vm.stopPrank(); + } + + function testRevertCannotUseInvalidEIP2612Signature() 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, + PERMIT2_USER, + defaultUSDCAmount, + testdata.deadline, + testdata.v + 1, // invalid v value + testdata.r, + testdata.s, + DIAMOND_ADDRESS, + testdata.diamondCalldata + ); + + vm.stopPrank(); + } + + /// Permit2 specific tests /// + function test_can_call_diamond_single() public { bytes memory diamondCalldata; ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; @@ -365,4 +525,61 @@ contract Permit2ProxyTest is TestBase { 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 + ) + ) + ) + ); + } } From d119ca9775533a16f22ac4b42c4232c45642a3f9 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 26 Aug 2024 18:41:23 +0300 Subject: [PATCH 11/37] Add missing comments --- config/permit2.json | 4 ++ deployments/_deployments_log_file.json | 26 +++++---- deployments/arbitrum.staging.json | 3 +- script/deploy/facets/DeployPermit2Proxy.s.sol | 53 +++++++++++++++++++ src/Periphery/Permit2Proxy.sol | 28 ++++++---- 5 files changed, 94 insertions(+), 20 deletions(-) create mode 100644 config/permit2.json create mode 100644 script/deploy/facets/DeployPermit2Proxy.s.sol diff --git a/config/permit2.json b/config/permit2.json new file mode 100644 index 000000000..66e819726 --- /dev/null +++ b/config/permit2.json @@ -0,0 +1,4 @@ +{ + "mainnet": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "arbitrum": "0x000000000022D473030F116dDEE9F6B43aC78BA3" +} \ No newline at end of file diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index c72d137a0..a02e0eb63 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -21793,16 +21793,6 @@ "VERIFIED": "true" } ], - "1.0.1": [ - { - "ADDRESS": "0x6e378C84e657C57b2a8d183CFf30ee5CC8989b61", - "OPTIMIZER_RUNS": "1000000", - "TIMESTAMP": "2024-08-14 18:28:47", - "CONSTRUCTOR_ARGS": "0x0000000000000000000000006ce9bf8cdab780416ad1fd87b318a077d2f50eac", - "SALT": "", - "VERIFIED": "true" - } - ], "1.0.1": [ { "ADDRESS": "0x6e378C84e657C57b2a8d183CFf30ee5CC8989b61", @@ -22407,5 +22397,21 @@ ] } } + }, + "Permit2Proxy": { + "arbitrum": { + "staging": { + "\u001b[31m[error] '@custom:version' string not found in src/Periphery/Permit2Proxy.sol\u001b[0m": [ + { + "ADDRESS": "0x442BBFD6a4641B2b710DFfa4754081eC7502a3F7", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2024-08-26 18:34:52", + "CONSTRUCTOR_ARGS": "0x00000000000000000000000011f1022ca6adef6400e5677528a80d49a069c00c000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3", + "SALT": "09072024", + "VERIFIED": "true" + } + ] + } + } } } diff --git a/deployments/arbitrum.staging.json b/deployments/arbitrum.staging.json index a6402499e..4a9f4e76d 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": "0x442BBFD6a4641B2b710DFfa4754081eC7502a3F7" } \ No newline at end of file diff --git a/script/deploy/facets/DeployPermit2Proxy.s.sol b/script/deploy/facets/DeployPermit2Proxy.s.sol new file mode 100644 index 000000000..14b54689f --- /dev/null +++ b/script/deploy/facets/DeployPermit2Proxy.s.sol @@ -0,0 +1,53 @@ +// 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) { + // get path of global config file + string memory globalConfigPath = string.concat( + root, + "/config/global.json" + ); + + // read file into json variable + string memory globalConfigJson = vm.readFile(globalConfigPath); + + // extract refundWallet address + address deployWalletAddress = globalConfigJson.readAddress( + ".deployerWallet" + ); + + // 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(deployWalletAddress, permit2Address); + } +} diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index 94915daf2..310ec95f8 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -9,6 +9,11 @@ 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 (Permit2-enabled) calls to our +/// diamond contract +/// @custom:version 1.0.0 contract Permit2Proxy is TransferrableOwnership { /// Storage /// @@ -58,9 +63,10 @@ contract Permit2Proxy is TransferrableOwnership { /// 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) + /// @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) /// @param tokenAddress Address of the token to be bridged /// @param owner Owner of the tokens to be bridged /// @param amount Amount of tokens to be bridged @@ -81,7 +87,7 @@ contract Permit2Proxy is TransferrableOwnership { address diamondAddress, bytes calldata diamondCalldata ) public payable { - // call permit function of token contract to register approval using signature + // call permit on token contract to register approval using signature ERC20Permit(tokenAddress).permit( owner, address(this), @@ -102,8 +108,9 @@ contract Permit2Proxy is TransferrableOwnership { _executeCalldata(diamondAddress, diamondCalldata); } - /// @notice Forwards a call to a whitelisted LIFI diamond - /// pulling tokens from the user using Uniswap Permit2 + /// @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 _diamondAddress the diamond contract to execute the call /// @param _diamondCalldata the calldata to execute /// @param _signer the signer giving permission to transfer tokens @@ -147,7 +154,8 @@ contract Permit2Proxy is TransferrableOwnership { /// @notice Allows to update the whitelist of diamond contracts /// @dev Admin function - /// @param addresses Addresses to be added (true) or removed (false) from whitelist + /// @param addresses Addresses to be added (true) or removed (false) from + /// whitelist /// @param values Values for each address that should be updated function updateWhitelist( address[] calldata addresses, @@ -255,7 +263,8 @@ contract Permit2Proxy is TransferrableOwnership { bytes memory diamondCalldata ) internal { // make sure diamondAddress is whitelisted - // this limits the usage of this Permit2Proxy contracts to only work with our diamond contracts + // this limits the usage of this Permit2Proxy contracts to only work + // with our diamond contracts if (!diamondWhitelist[diamondAddress]) revert DiamondAddressNotWhitelisted(); @@ -264,7 +273,8 @@ contract Permit2Proxy is TransferrableOwnership { (bool success, bytes memory data) = diamondAddress.call{ value: msg.value }(diamondCalldata); - // throw error to make sure tx reverts if low-level call was unsuccessful + // throw error to make sure tx reverts if low-level call was + // unsuccessful if (!success) { revert CallToDiamondFailed(data); } From 484c91817f26bdcb21a02b92bc93fab0a33698b0 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 26 Aug 2024 19:26:37 +0300 Subject: [PATCH 12/37] Flesh out demo script --- script/demoScripts/demoPermit2Proxy.ts | 44 ++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 script/demoScripts/demoPermit2Proxy.ts diff --git a/script/demoScripts/demoPermit2Proxy.ts b/script/demoScripts/demoPermit2Proxy.ts new file mode 100644 index 000000000..145ab661d --- /dev/null +++ b/script/demoScripts/demoPermit2Proxy.ts @@ -0,0 +1,44 @@ +import { http, createPublicClient, parseAbi } from 'viem' +import { arbitrum } from 'viem/chains' +import dotenv from 'dotenv' +dotenv.config() + +const PERMIT2_PROXY_ADDRESS = '0x442BBFD6a4641B2b710DFfa4754081eC7502a3F7' + +const main = async () => { + const abi = parseAbi([ + 'function getPermit2MsgHash(address,bytes,address,uint256,uint256,uint256)', + ]) + + const client = createPublicClient({ + chain: arbitrum, + transport: http(), + }) + + // Get calldata to bridge UNI from LIFI API + + // Get nonce + + // + + // Pass args and figure out msg hash + const msgHash = await client.readContract({ + address: PERMIT2_PROXY_ADDRESS, + abi, + functionName: 'getPermi2MsgHash', + args: [], + }) + + // Sign msg hash + + // Call proxy with signature +} + +main() + .then(() => { + console.log('Done!') + }) + .catch((err) => { + console.error(err) + process.exit(1) + }) From f582cbcd05202d0e33a2abfc57a306f02f041398 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 27 Aug 2024 15:07:41 +0300 Subject: [PATCH 13/37] Finish demo script --- package.json | 1 + script/demoScripts/demoPermit2Proxy.ts | 137 +++++++++++++++++++------ tsconfig.json | 12 ++- yarn.lock | 8 ++ 4 files changed, 122 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 01d99889d..4e42db18b 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@types/pino": "^7.0.5", "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^7.10.0", + "@uniswap/permit2-sdk": "^1.3.0", "cross-env": "^7.0.2", "dotenv": "^16.0.0", "eslint": "^8.11.0", diff --git a/script/demoScripts/demoPermit2Proxy.ts b/script/demoScripts/demoPermit2Proxy.ts index 145ab661d..f3c7986e2 100644 --- a/script/demoScripts/demoPermit2Proxy.ts +++ b/script/demoScripts/demoPermit2Proxy.ts @@ -1,44 +1,117 @@ -import { http, createPublicClient, parseAbi } from 'viem' +import { + http, + createPublicClient, + parseAbi, + Hex, + parseUnits, + serializeSignature, + createWalletClient, +} from 'viem' +import { privateKeyToAccount, sign } from 'viem/accounts' import { arbitrum } from 'viem/chains' -import dotenv from 'dotenv' -dotenv.config() +import { defineCommand, runMain } from 'citty' +const DIAMOND_ADDRESS = '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE' +const USDT_ADDRESS = '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9' const PERMIT2_PROXY_ADDRESS = '0x442BBFD6a4641B2b710DFfa4754081eC7502a3F7' +const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3' +const PRIVATE_KEY = `0x${process.env.PRIVATE_KEY}` -const main = async () => { - const abi = parseAbi([ - 'function getPermit2MsgHash(address,bytes,address,uint256,uint256,uint256)', - ]) +const main = defineCommand({ + meta: { + name: 'demo-permit2', + description: 'Demonstrate a Permit2 tx', + }, + args: { + signerKey: { + type: 'string', + description: 'Private key of signer', + }, + executorKey: { + type: 'string', + description: 'Private key of the executor', + }, + }, + async run({ args }) { + const SIGNER_PRIVATE_KEY = `0x${args.signerKey}` as Hex + const EXECUTOR_PRIVATE_KEY = `0x${args.executorKey}` as Hex - const client = createPublicClient({ - chain: arbitrum, - transport: http(), - }) + const permit2Abi = parseAbi([ + 'function nonceBitmap(address owner, uint256 index) external view returns (uint256 nonce)', + ]) + const permit2ProxyAbi = parseAbi([ + 'function getPermit2MsgHash(address,bytes,address,uint256,uint256,uint256) external view returns (bytes32)', + 'function callDiamondWithPermit2SignatureSingle(address,bytes,address,((address,uint256),uint256,uint256),bytes) external', + ]) - // Get calldata to bridge UNI from LIFI API + const client = createPublicClient({ + chain: arbitrum, + transport: http(), + }) - // Get nonce + // Account + const account = privateKeyToAccount(SIGNER_PRIVATE_KEY) - // + // Get calldata to bridge UNI 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' } } - // Pass args and figure out msg hash - const msgHash = await client.readContract({ - address: PERMIT2_PROXY_ADDRESS, - abi, - functionName: 'getPermi2MsgHash', - args: [], - }) + const lifiResp = await fetch(url, options) - // Sign msg hash + const calldata = (await lifiResp.json()).transactionRequest.data - // Call proxy with signature -} + // Get nonce + const nonce = await client.readContract({ + address: PERMIT2_ADDRESS, + abi: permit2Abi, + functionName: 'nonceBitmap', + args: [account.address, 0n], + }) -main() - .then(() => { - console.log('Done!') - }) - .catch((err) => { - console.error(err) - process.exit(1) - }) + // Get block + const block = await client.getBlock() + + // Pass args and figure out msg hash + const msgHash = await client.readContract({ + address: PERMIT2_PROXY_ADDRESS, + abi: permit2ProxyAbi, + functionName: 'getPermit2MsgHash', + args: [ + DIAMOND_ADDRESS, + calldata, + USDT_ADDRESS, + parseUnits('5', 6), + nonce, + block.timestamp + 1200n, + ], + }) + + console.log(msgHash) + + // Sign msg hash + const rsvSig = await sign({ hash: msgHash, privateKey: SIGNER_PRIVATE_KEY }) + const signature = serializeSignature(rsvSig) + + // Call proxy with signature + console.log(signature) + + const tokenPermissions = [USDT_ADDRESS, parseUnits('5', 6)] + const permit = [tokenPermissions, nonce, block.timestamp + 1200n] + + const executorAccount = privateKeyToAccount(EXECUTOR_PRIVATE_KEY) + const walletClient = createWalletClient({ + account: executorAccount, + chain: arbitrum, + transport: http(), + }) + const tx = await walletClient.writeContract({ + address: PERMIT2_PROXY_ADDRESS, + abi: permit2ProxyAbi, + functionName: 'callDiamondWithPermit2SignatureSingle', + args: [DIAMOND_ADDRESS, calldata, account.address, permit, signature], + }) + }, +}) + +runMain(main) 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 diff --git a/yarn.lock b/yarn.lock index 4b7829c2c..43ce87a74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2065,6 +2065,14 @@ immer "^9.0.7" lodash-es "^4.17.21" +"@uniswap/permit2-sdk@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@uniswap/permit2-sdk/-/permit2-sdk-1.3.0.tgz#b54124e570f0adbaca9d39b2de3054fd7d3798a1" + integrity sha512-LstYQWP47dwpQrgqBJ+ysFstne9LgI5FGiKHc2ewjj91MTY8Mq1reocu6U/VDncdR5ef30TUOcZ7gPExRY8r6Q== + dependencies: + ethers "^5.7.0" + tiny-invariant "^1.1.0" + "@uniswap/sdk@^3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@uniswap/sdk/-/sdk-3.0.3.tgz#8201c7c72215d0030cb99acc7e661eff895c18a9" From 9785369b0ea4902d7c9d6c0bc0bbaf764a8e84a0 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 27 Aug 2024 15:15:54 +0300 Subject: [PATCH 14/37] Cleanup and comments --- script/demoScripts/demoPermit2Proxy.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/script/demoScripts/demoPermit2Proxy.ts b/script/demoScripts/demoPermit2Proxy.ts index f3c7986e2..4eade89ab 100644 --- a/script/demoScripts/demoPermit2Proxy.ts +++ b/script/demoScripts/demoPermit2Proxy.ts @@ -26,16 +26,19 @@ const main = defineCommand({ 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 ABIs const permit2Abi = parseAbi([ 'function nonceBitmap(address owner, uint256 index) external view returns (uint256 nonce)', ]) @@ -44,24 +47,23 @@ const main = defineCommand({ 'function callDiamondWithPermit2SignatureSingle(address,bytes,address,((address,uint256),uint256,uint256),bytes) external', ]) + // Setup a READ-ONLY client const client = createPublicClient({ chain: arbitrum, transport: http(), }) - // Account + // Setup a signer account const account = privateKeyToAccount(SIGNER_PRIVATE_KEY) - // Get calldata to bridge UNI from LIFI API + // 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 nonce + // Get the nonce from the PERMIT2 contract const nonce = await client.readContract({ address: PERMIT2_ADDRESS, abi: permit2Abi, @@ -69,10 +71,10 @@ const main = defineCommand({ args: [account.address, 0n], }) - // Get block + // Get lastest block const block = await client.getBlock() - // Pass args and figure out msg hash + // Consturct a valid message hash to sign using Permit2Proxy's utility func const msgHash = await client.readContract({ address: PERMIT2_PROXY_ADDRESS, abi: permit2ProxyAbi, @@ -86,25 +88,26 @@ const main = defineCommand({ block.timestamp + 1200n, ], }) - console.log(msgHash) - // Sign msg hash + // Sign the message hash const rsvSig = await sign({ hash: msgHash, privateKey: SIGNER_PRIVATE_KEY }) const signature = serializeSignature(rsvSig) - - // Call proxy with signature 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, From 4cd03e02c2622ed973b808c10eb9951184d86b7d Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 27 Aug 2024 15:27:06 +0300 Subject: [PATCH 15/37] Fix log --- deployments/_deployments_log_file.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index a02e0eb63..a312f04ce 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -22401,7 +22401,7 @@ "Permit2Proxy": { "arbitrum": { "staging": { - "\u001b[31m[error] '@custom:version' string not found in src/Periphery/Permit2Proxy.sol\u001b[0m": [ + "1.0.0": [ { "ADDRESS": "0x442BBFD6a4641B2b710DFfa4754081eC7502a3F7", "OPTIMIZER_RUNS": "1000000", @@ -22414,4 +22414,4 @@ } } } -} +} \ No newline at end of file From f18dab6e88a46c0e7d298eb57ec00d86e5e97a08 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 27 Aug 2024 15:30:25 +0300 Subject: [PATCH 16/37] Remove extra files --- src/Interfaces/IEIP712.sol | 6 -- src/Interfaces/ISignatureTransfer.sol | 138 -------------------------- 2 files changed, 144 deletions(-) delete mode 100644 src/Interfaces/IEIP712.sol delete mode 100644 src/Interfaces/ISignatureTransfer.sol diff --git a/src/Interfaces/IEIP712.sol b/src/Interfaces/IEIP712.sol deleted file mode 100644 index 1a5be726b..000000000 --- a/src/Interfaces/IEIP712.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -interface IEIP712 { - function DOMAIN_SEPARATOR() external view returns (bytes32); -} diff --git a/src/Interfaces/ISignatureTransfer.sol b/src/Interfaces/ISignatureTransfer.sol deleted file mode 100644 index 9b379e881..000000000 --- a/src/Interfaces/ISignatureTransfer.sol +++ /dev/null @@ -1,138 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import { IEIP712 } from "./IEIP712.sol"; - -/// @title SignatureTransfer -/// @notice Handles ERC20 token transfers through signature based actions -/// @dev Requires user's token approval on the Permit2 contract -interface ISignatureTransfer is IEIP712 { - /// @notice Thrown when the requested amount for a transfer is larger than the permissioned amount - /// @param maxAmount The maximum amount a spender can request to transfer - error InvalidAmount(uint256 maxAmount); - - /// @notice Thrown when the number of tokens permissioned to a spender does not match the number of tokens being transferred - /// @dev If the spender does not need to transfer the number of tokens permitted, the spender can request amount 0 to be transferred - error LengthMismatch(); - - /// @notice Emits an event when the owner successfully invalidates an unordered nonce. - event UnorderedNonceInvalidation( - address indexed owner, - uint256 word, - uint256 mask - ); - - /// @notice The token and amount details for a transfer signed in the permit transfer signature - struct TokenPermissions { - // ERC20 token address - address token; - // the maximum amount that can be spent - uint256 amount; - } - - /// @notice The signed permit message for a single token transfer - 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; - } - - /// @notice Specifies the recipient address and amount for batched transfers. - /// @dev Recipients and amounts correspond to the index of the signed token permissions array. - /// @dev Reverts if the requested amount is greater than the permitted signed amount. - struct SignatureTransferDetails { - // recipient address - address to; - // spender requested amount - uint256 requestedAmount; - } - - /// @notice Used to reconstruct the signed permit message for multiple token transfers - /// @dev Do not need to pass in spender address as it is required that it is msg.sender - /// @dev Note that a user still signs over a spender address - struct PermitBatchTransferFrom { - // the tokens and corresponding amounts permitted for a transfer - TokenPermissions[] permitted; - // a unique value for every token owner's signature to prevent signature replays - uint256 nonce; - // deadline on the permit signature - uint256 deadline; - } - - /// @notice A map from token owner address and a caller specified word index to a bitmap. Used to set bits in the bitmap to prevent against signature replay protection - /// @dev Uses unordered nonces so that permit messages do not need to be spent in a certain order - /// @dev The mapping is indexed first by the token owner, then by an index specified in the nonce - /// @dev It returns a uint256 bitmap - /// @dev The index, or wordPosition is capped at type(uint248).max - function nonceBitmap(address, uint256) external view returns (uint256); - - /// @notice Transfers a token using a signed permit message - /// @dev Reverts if the requested amount is greater than the permitted signed amount - /// @param permit The permit data signed over by the owner - /// @param owner The owner of the tokens to transfer - /// @param transferDetails The spender's requested transfer details for the permitted token - /// @param signature The signature to verify - function permitTransferFrom( - PermitTransferFrom memory permit, - SignatureTransferDetails calldata transferDetails, - address owner, - bytes calldata signature - ) external; - - /// @notice Transfers a token using a signed permit message - /// @notice Includes extra data provided by the caller to verify signature over - /// @dev The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition - /// @dev Reverts if the requested amount is greater than the permitted signed amount - /// @param permit The permit data signed over by the owner - /// @param owner The owner of the tokens to transfer - /// @param transferDetails The spender's requested transfer details for the permitted token - /// @param witness Extra data to include when checking the user signature - /// @param witnessTypeString The EIP-712 type definition for remaining string stub of the typehash - /// @param signature The signature to verify - function permitWitnessTransferFrom( - PermitTransferFrom memory permit, - SignatureTransferDetails calldata transferDetails, - address owner, - bytes32 witness, - string calldata witnessTypeString, - bytes calldata signature - ) external; - - /// @notice Transfers multiple tokens using a signed permit message - /// @param permit The permit data signed over by the owner - /// @param owner The owner of the tokens to transfer - /// @param transferDetails Specifies the recipient and requested amount for the token transfer - /// @param signature The signature to verify - function permitTransferFrom( - PermitBatchTransferFrom memory permit, - SignatureTransferDetails[] calldata transferDetails, - address owner, - bytes calldata signature - ) external; - - /// @notice Transfers multiple tokens using a signed permit message - /// @dev The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition - /// @notice Includes extra data provided by the caller to verify signature over - /// @param permit The permit data signed over by the owner - /// @param owner The owner of the tokens to transfer - /// @param transferDetails Specifies the recipient and requested amount for the token transfer - /// @param witness Extra data to include when checking the user signature - /// @param witnessTypeString The EIP-712 type definition for remaining string stub of the typehash - /// @param signature The signature to verify - function permitWitnessTransferFrom( - PermitBatchTransferFrom memory permit, - SignatureTransferDetails[] calldata transferDetails, - address owner, - bytes32 witness, - string calldata witnessTypeString, - bytes calldata signature - ) external; - - /// @notice Invalidates the bits specified in mask for the bitmap at the word position - /// @dev The wordPos is maxed at type(uint248).max - /// @param wordPos A number to index the nonceBitmap at - /// @param mask A bitmap masked against msg.sender's current bitmap at the word position - function invalidateUnorderedNonces(uint256 wordPos, uint256 mask) external; -} From 0eaa7c867b979e59f8e720cc8cde7d231ce15f60 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 27 Aug 2024 15:32:01 +0300 Subject: [PATCH 17/37] Remove extra remapping --- remappings.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/remappings.txt b/remappings.txt index ed6769e68..d4a7a4506 100644 --- a/remappings.txt +++ b/remappings.txt @@ -9,7 +9,6 @@ celer-network/=lib/sgn-v2-contracts/ create3-factory/=lib/create3-factory/src/ solmate/=lib/solmate/src/ solady/=lib/solady/src/ -permit2-test-utils/=lib/Permit2/test/utils/ permit2/=lib/Permit2/src/ ds-test/=lib/ds-test/src/ forge-std/=lib/forge-std/src/ From a3e6f5dffdad0d273cc99997142ebacf736d3d3f Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 27 Aug 2024 15:33:41 +0300 Subject: [PATCH 18/37] Remove unneeded lib --- package.json | 1 - yarn.lock | 8 -------- 2 files changed, 9 deletions(-) diff --git a/package.json b/package.json index 4e42db18b..01d99889d 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "@types/pino": "^7.0.5", "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^7.10.0", - "@uniswap/permit2-sdk": "^1.3.0", "cross-env": "^7.0.2", "dotenv": "^16.0.0", "eslint": "^8.11.0", diff --git a/yarn.lock b/yarn.lock index 43ce87a74..4b7829c2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2065,14 +2065,6 @@ immer "^9.0.7" lodash-es "^4.17.21" -"@uniswap/permit2-sdk@^1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@uniswap/permit2-sdk/-/permit2-sdk-1.3.0.tgz#b54124e570f0adbaca9d39b2de3054fd7d3798a1" - integrity sha512-LstYQWP47dwpQrgqBJ+ysFstne9LgI5FGiKHc2ewjj91MTY8Mq1reocu6U/VDncdR5ef30TUOcZ7gPExRY8r6Q== - dependencies: - ethers "^5.7.0" - tiny-invariant "^1.1.0" - "@uniswap/sdk@^3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@uniswap/sdk/-/sdk-3.0.3.tgz#8201c7c72215d0030cb99acc7e661eff895c18a9" From cfd19775d2c467b3cbc44ad19a45662770efd72b Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 27 Aug 2024 15:50:48 +0300 Subject: [PATCH 19/37] Add official Permit2 addresses --- config/permit2.json | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/config/permit2.json b/config/permit2.json index 66e819726..c4ac60ac2 100644 --- a/config/permit2.json +++ b/config/permit2.json @@ -1,4 +1,31 @@ { "mainnet": "0x000000000022D473030F116dDEE9F6B43aC78BA3", - "arbitrum": "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 From e7395b226c41d506f2360ab79d2ae150f6abd077 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 29 Aug 2024 12:58:25 +0300 Subject: [PATCH 20/37] Allow only signer to call using EIP2612 --- src/Periphery/Permit2Proxy.sol | 11 +++--- test/solidity/Periphery/Permit2Proxy.t.sol | 41 +++++++++++++++++----- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index 310ec95f8..f04763700 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -68,7 +68,6 @@ contract Permit2Proxy is TransferrableOwnership { /// implement EIP2612) (in contrast to Permit2, calldata and diamondAddress /// are not signed by the user and could therefore be replaced) /// @param tokenAddress Address of the token to be bridged - /// @param owner Owner of the tokens 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) @@ -78,7 +77,6 @@ contract Permit2Proxy is TransferrableOwnership { /// @param diamondCalldata Address of the token to be bridged function callDiamondWithEIP2612Signature( address tokenAddress, - address owner, uint256 amount, uint256 deadline, uint8 v, @@ -89,7 +87,7 @@ contract Permit2Proxy is TransferrableOwnership { ) public payable { // call permit on token contract to register approval using signature ERC20Permit(tokenAddress).permit( - owner, + msg.sender, address(this), amount, deadline, @@ -99,7 +97,12 @@ contract Permit2Proxy is TransferrableOwnership { ); // deposit assets - LibAsset.transferFromERC20(tokenAddress, owner, address(this), amount); + LibAsset.transferFromERC20( + tokenAddress, + msg.sender, + address(this), + amount + ); // maxApprove token to diamond if current allowance is insufficient LibAsset.maxApproveERC20(IERC20(tokenAddress), diamondAddress, amount); diff --git a/test/solidity/Periphery/Permit2Proxy.t.sol b/test/solidity/Periphery/Permit2Proxy.t.sol index a17fb46c5..0fa1d99f3 100644 --- a/test/solidity/Periphery/Permit2Proxy.t.sol +++ b/test/solidity/Periphery/Permit2Proxy.t.sol @@ -97,7 +97,6 @@ contract Permit2ProxyTest is TestBase { // call Permit2Proxy with signature permit2Proxy.callDiamondWithEIP2612Signature( ADDRESS_USDC, - PERMIT2_USER, defaultUSDCAmount, testdata.deadline, testdata.v, @@ -110,7 +109,7 @@ contract Permit2ProxyTest is TestBase { vm.stopPrank(); } - function testRevertcannotUseEIP2612SignatureTwice() public { + function testRevert_cannot_use_eip2612_signature_twice() public { vm.startPrank(PERMIT2_USER); // get token-specific domainSeparator @@ -126,7 +125,6 @@ contract Permit2ProxyTest is TestBase { // call Permit2Proxy with signature permit2Proxy.callDiamondWithEIP2612Signature( ADDRESS_USDC, - PERMIT2_USER, defaultUSDCAmount, testdata.deadline, testdata.v, @@ -140,7 +138,6 @@ contract Permit2ProxyTest is TestBase { vm.expectRevert("EIP2612: invalid signature"); permit2Proxy.callDiamondWithEIP2612Signature( ADDRESS_USDC, - PERMIT2_USER, defaultUSDCAmount, testdata.deadline, testdata.v, @@ -153,7 +150,7 @@ contract Permit2ProxyTest is TestBase { vm.stopPrank(); } - function testRevertCannotUseExpiredEIP2612Signature() public { + function testRevert_cannot_use_expired_eip2612_signature() public { vm.startPrank(PERMIT2_USER); // get token-specific domainSeparator @@ -172,7 +169,6 @@ contract Permit2ProxyTest is TestBase { // call Permit2Proxy with signature permit2Proxy.callDiamondWithEIP2612Signature( ADDRESS_USDC, - PERMIT2_USER, defaultUSDCAmount, testdata.deadline, testdata.v, @@ -185,7 +181,7 @@ contract Permit2ProxyTest is TestBase { vm.stopPrank(); } - function testRevertCannotUseInvalidEIP2612Signature() public { + function testRevert_cannot_use_invalid_eip2612_signature() public { vm.startPrank(PERMIT2_USER); // get token-specific domainSeparator @@ -204,7 +200,6 @@ contract Permit2ProxyTest is TestBase { // call Permit2Proxy with signature permit2Proxy.callDiamondWithEIP2612Signature( ADDRESS_USDC, - PERMIT2_USER, defaultUSDCAmount, testdata.deadline, testdata.v + 1, // invalid v value @@ -217,6 +212,36 @@ contract Permit2ProxyTest is TestBase { 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, + DIAMOND_ADDRESS, + testdata.diamondCalldata + ); + + vm.stopPrank(); + } + /// Permit2 specific tests /// function test_can_call_diamond_single() public { From 5d772b0a79052e77fb646f4ef998e66aacc3ab43 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 29 Aug 2024 13:15:14 +0300 Subject: [PATCH 21/37] Bind Permit2Proxy to a single diamond --- src/Periphery/Permit2Proxy.sol | 61 ++++--------------- test/solidity/Periphery/Permit2Proxy.t.sol | 70 +--------------------- 2 files changed, 13 insertions(+), 118 deletions(-) diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index f04763700..afb333149 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -14,9 +14,10 @@ import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC2 /// @notice Proxy contract allowing gasless (Permit2-enabled) calls to our /// diamond contract /// @custom:version 1.0.0 -contract Permit2Proxy is TransferrableOwnership { +contract Permit2Proxy { /// Storage /// + address public immutable LIFI_DIAMOND; ISignatureTransfer public immutable PERMIT2; mapping(address => bool) public diamondWhitelist; @@ -47,10 +48,8 @@ contract Permit2Proxy is TransferrableOwnership { /// Constructor /// - constructor( - address _owner, - ISignatureTransfer _permit2 - ) TransferrableOwnership(_owner) { + constructor(address _lifiDiamond, ISignatureTransfer _permit2) { + LIFI_DIAMOND = _lifiDiamond; PERMIT2 = _permit2; PERMIT_WITH_WITNESS_TYPEHASH = keccak256( @@ -73,7 +72,6 @@ contract Permit2Proxy is TransferrableOwnership { /// @param v User signature (recovery ID) /// @param r User signature (ECDSA output) /// @param s User signature (ECDSA output) - /// @param diamondAddress Address of the token to be bridged /// @param diamondCalldata Address of the token to be bridged function callDiamondWithEIP2612Signature( address tokenAddress, @@ -82,7 +80,6 @@ contract Permit2Proxy is TransferrableOwnership { uint8 v, bytes32 r, bytes32 s, - address diamondAddress, bytes calldata diamondCalldata ) public payable { // call permit on token contract to register approval using signature @@ -105,29 +102,27 @@ contract Permit2Proxy is TransferrableOwnership { ); // maxApprove token to diamond if current allowance is insufficient - LibAsset.maxApproveERC20(IERC20(tokenAddress), diamondAddress, amount); + LibAsset.maxApproveERC20(IERC20(tokenAddress), LIFI_DIAMOND, amount); // call our diamond to execute calldata - _executeCalldata(diamondAddress, diamondCalldata); + _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 _diamondAddress the diamond contract to execute the call /// @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 callDiamondWithPermit2SignatureSingle( - address _diamondAddress, bytes calldata _diamondCalldata, address _signer, ISignatureTransfer.PermitTransferFrom calldata _permit, bytes calldata _signature ) external payable { LIFICall memory lifiCall = LIFICall( - _diamondAddress, + LIFI_DIAMOND, keccak256(_diamondCalldata) ); @@ -148,43 +143,20 @@ contract Permit2Proxy is TransferrableOwnership { // maxApprove token to diamond if current allowance is insufficient LibAsset.maxApproveERC20( IERC20(_permit.permitted.token), - _diamondAddress, + LIFI_DIAMOND, _permit.permitted.amount ); - _executeCalldata(_diamondAddress, _diamondCalldata); - } - - /// @notice Allows to update the whitelist of diamond contracts - /// @dev Admin function - /// @param addresses Addresses to be added (true) or removed (false) from - /// whitelist - /// @param values Values for each address that should be updated - function updateWhitelist( - address[] calldata addresses, - bool[] calldata values - ) external onlyOwner { - for (uint i; i < addresses.length; ) { - // update whitelist address value - diamondWhitelist[addresses[i]] = values[i]; - - // gas-efficient way to increase the loop counter - unchecked { - ++i; - } - } - emit WhitelistUpdated(addresses, values); + _executeCalldata(_diamondCalldata); } /// @notice utitlity method for constructing a valid Permit2 message hash - /// @param _diamondAddress the diamond address to call /// @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( - address _diamondAddress, bytes calldata _diamondCalldata, address _assetId, uint256 _amount, @@ -201,7 +173,7 @@ contract Permit2Proxy is TransferrableOwnership { // Witness Permit2Proxy.LIFICall memory lifiCall = LIFICall( - _diamondAddress, + LIFI_DIAMOND, keccak256(_diamondCalldata) ); bytes32 witness = _getWitnessHash(lifiCall); @@ -261,19 +233,10 @@ contract Permit2Proxy is TransferrableOwnership { keccak256(abi.encodePacked("\x19\x01", domainSeparator, dataHash)); } - function _executeCalldata( - address diamondAddress, - bytes memory diamondCalldata - ) internal { - // make sure diamondAddress is whitelisted - // this limits the usage of this Permit2Proxy contracts to only work - // with our diamond contracts - if (!diamondWhitelist[diamondAddress]) - revert DiamondAddressNotWhitelisted(); - + function _executeCalldata(bytes memory diamondCalldata) internal { // call diamond with provided calldata // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory data) = diamondAddress.call{ + (bool success, bytes memory data) = LIFI_DIAMOND.call{ value: msg.value }(diamondCalldata); // throw error to make sure tx reverts if low-level call was diff --git a/test/solidity/Periphery/Permit2Proxy.t.sol b/test/solidity/Periphery/Permit2Proxy.t.sol index 0fa1d99f3..64eff6226 100644 --- a/test/solidity/Periphery/Permit2Proxy.t.sol +++ b/test/solidity/Periphery/Permit2Proxy.t.sol @@ -51,7 +51,7 @@ contract Permit2ProxyTest is TestBase { initTestBase(); uniPermit2 = ISignatureTransfer(PERMIT2_ADDRESS); - permit2Proxy = new Permit2Proxy(address(this), uniPermit2); + permit2Proxy = new Permit2Proxy(DIAMOND_ADDRESS, uniPermit2); PERMIT_WITH_WITNESS_TYPEHASH = keccak256( abi.encodePacked( PermitHash._PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB, @@ -59,11 +59,6 @@ contract Permit2ProxyTest is TestBase { ) ); - address[] memory whitelist = new address[](1); - whitelist[0] = DIAMOND_ADDRESS; - bool[] memory allowed = new bool[](1); - allowed[0] = true; - permit2Proxy.updateWhitelist(whitelist, allowed); PERMIT2_USER = vm.addr(PRIVATE_KEY); vm.label(PERMIT2_USER, "Permit2 User"); deal(ADDRESS_USDC, PERMIT2_USER, 10000 ether); @@ -102,7 +97,6 @@ contract Permit2ProxyTest is TestBase { testdata.v, testdata.r, testdata.s, - DIAMOND_ADDRESS, testdata.diamondCalldata ); @@ -130,7 +124,6 @@ contract Permit2ProxyTest is TestBase { testdata.v, testdata.r, testdata.s, - DIAMOND_ADDRESS, testdata.diamondCalldata ); @@ -143,7 +136,6 @@ contract Permit2ProxyTest is TestBase { testdata.v, testdata.r, testdata.s, - DIAMOND_ADDRESS, testdata.diamondCalldata ); @@ -174,7 +166,6 @@ contract Permit2ProxyTest is TestBase { testdata.v, testdata.r, testdata.s, - DIAMOND_ADDRESS, testdata.diamondCalldata ); @@ -205,7 +196,6 @@ contract Permit2ProxyTest is TestBase { testdata.v + 1, // invalid v value testdata.r, testdata.s, - DIAMOND_ADDRESS, testdata.diamondCalldata ); @@ -235,7 +225,6 @@ contract Permit2ProxyTest is TestBase { testdata.v, testdata.r, testdata.s, - DIAMOND_ADDRESS, testdata.diamondCalldata ); @@ -257,7 +246,6 @@ contract Permit2ProxyTest is TestBase { // Execute permit2Proxy.callDiamondWithPermit2SignatureSingle( - DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, permitTransferFrom, @@ -271,7 +259,6 @@ contract Permit2ProxyTest is TestBase { (, , msgHash, ) = _getPermitWitnessTransferFromParams(); generatedMsgHash = permit2Proxy.getPermit2MsgHash( - DIAMOND_ADDRESS, _getCalldataForBridging(), ADDRESS_USDC, defaultUSDCAmount, @@ -282,29 +269,6 @@ contract Permit2ProxyTest is TestBase { assertEq(msgHash, generatedMsgHash); } - function testRevery_cannot_call_unwhitelisted_diamond() public { - DIAMOND_ADDRESS = address(0x11f1); // Not whitelisted - bytes memory diamondCalldata; - ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; - bytes memory signature; - ( - diamondCalldata, - permitTransferFrom, - , - signature - ) = _getPermitWitnessTransferFromParams(); - - // Execute - vm.expectRevert(DiamondAddressNotWhitelisted.selector); - permit2Proxy.callDiamondWithPermit2SignatureSingle( - DIAMOND_ADDRESS, - diamondCalldata, - PERMIT2_USER, - permitTransferFrom, - signature - ); - } - function testRevert_cannot_call_diamond_single_with_same_signature_more_than_once() public { @@ -321,7 +285,6 @@ contract Permit2ProxyTest is TestBase { // Execute x2 permit2Proxy.callDiamondWithPermit2SignatureSingle( - DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, permitTransferFrom, @@ -329,34 +292,6 @@ contract Permit2ProxyTest is TestBase { ); vm.expectRevert(InvalidNonce.selector); permit2Proxy.callDiamondWithPermit2SignatureSingle( - DIAMOND_ADDRESS, - diamondCalldata, - PERMIT2_USER, - permitTransferFrom, - signature - ); - } - - function testRevert_cannot_set_different_diamond_address_than_intended() - public - { - deal(ADDRESS_USDC, PERMIT2_USER, 10000 ether); - bytes memory diamondCalldata; - ISignatureTransfer.PermitTransferFrom memory permitTransferFrom; - bytes memory signature; - ( - diamondCalldata, - permitTransferFrom, - , - signature - ) = _getPermitWitnessTransferFromParams(); - - address MALICIOUS_CONTRACT; - - // Execute - vm.expectRevert(InvalidSigner.selector); - permit2Proxy.callDiamondWithPermit2SignatureSingle( - MALICIOUS_CONTRACT, diamondCalldata, PERMIT2_USER, permitTransferFrom, @@ -381,7 +316,6 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(InvalidSigner.selector); permit2Proxy.callDiamondWithPermit2SignatureSingle( - DIAMOND_ADDRESS, MALICIOUS_CALLDATA, PERMIT2_USER, permitTransferFrom, @@ -406,7 +340,6 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(InvalidSigner.selector); permit2Proxy.callDiamondWithPermit2SignatureSingle( - DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, permitTransferFrom, @@ -433,7 +366,6 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(InvalidSigner.selector); permit2Proxy.callDiamondWithPermit2SignatureSingle( - DIAMOND_ADDRESS, diamondCalldata, PERMIT2_USER, permitTransferFrom, From 2f8e953c13f7758f0f392953d7ef10822d50f21e Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 29 Aug 2024 13:20:25 +0300 Subject: [PATCH 22/37] Update deploy script --- script/deploy/facets/DeployPermit2Proxy.s.sol | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/script/deploy/facets/DeployPermit2Proxy.s.sol b/script/deploy/facets/DeployPermit2Proxy.s.sol index 14b54689f..4513f3b31 100644 --- a/script/deploy/facets/DeployPermit2Proxy.s.sol +++ b/script/deploy/facets/DeployPermit2Proxy.s.sol @@ -20,19 +20,17 @@ contract DeployScript is DeployScriptBase { } function getConstructorArgs() internal override returns (bytes memory) { - // get path of global config file - string memory globalConfigPath = string.concat( + string memory deployments = string.concat( root, - "/config/global.json" + "/deployments/", + network, + ".", + fileSuffix, + "json" ); + string memory deploymentsJSON = vm.readFile(deployments); - // read file into json variable - string memory globalConfigJson = vm.readFile(globalConfigPath); - - // extract refundWallet address - address deployWalletAddress = globalConfigJson.readAddress( - ".deployerWallet" - ); + address diamond = deploymentsJSON.readAddress(".LiFiDiamond"); // get path of permit2 config file string memory permit2ProxyConfig = string.concat( @@ -48,6 +46,6 @@ contract DeployScript is DeployScriptBase { string.concat(".", network) ); - return abi.encode(deployWalletAddress, permit2Address); + return abi.encode(diamond, permit2Address); } } From 3d39c319c1850064e974801f08cd71e19bfaa5cc Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 29 Aug 2024 13:56:05 +0300 Subject: [PATCH 23/37] Implement non-gasless Permit2 flow --- src/Periphery/Permit2Proxy.sol | 33 +++++- test/solidity/Periphery/Permit2Proxy.t.sol | 117 +++++++++++++++++++-- 2 files changed, 142 insertions(+), 8 deletions(-) diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index afb333149..225ce200f 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -108,6 +108,37 @@ contract Permit2Proxy { _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 _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, + _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 @@ -115,7 +146,7 @@ contract Permit2Proxy { /// @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 callDiamondWithPermit2SignatureSingle( + function callDiamondWithPermit2Witness( bytes calldata _diamondCalldata, address _signer, ISignatureTransfer.PermitTransferFrom calldata _permit, diff --git a/test/solidity/Periphery/Permit2Proxy.t.sol b/test/solidity/Periphery/Permit2Proxy.t.sol index 64eff6226..5ad586725 100644 --- a/test/solidity/Periphery/Permit2Proxy.t.sol +++ b/test/solidity/Periphery/Permit2Proxy.t.sol @@ -233,7 +233,50 @@ contract Permit2ProxyTest is TestBase { /// Permit2 specific tests /// - function test_can_call_diamond_single() public { + 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; @@ -245,7 +288,7 @@ contract Permit2ProxyTest is TestBase { ) = _getPermitWitnessTransferFromParams(); // Execute - permit2Proxy.callDiamondWithPermit2SignatureSingle( + permit2Proxy.callDiamondWithPermit2Witness( diamondCalldata, PERMIT2_USER, permitTransferFrom, @@ -284,14 +327,14 @@ contract Permit2ProxyTest is TestBase { ) = _getPermitWitnessTransferFromParams(); // Execute x2 - permit2Proxy.callDiamondWithPermit2SignatureSingle( + permit2Proxy.callDiamondWithPermit2Witness( diamondCalldata, PERMIT2_USER, permitTransferFrom, signature ); vm.expectRevert(InvalidNonce.selector); - permit2Proxy.callDiamondWithPermit2SignatureSingle( + permit2Proxy.callDiamondWithPermit2Witness( diamondCalldata, PERMIT2_USER, permitTransferFrom, @@ -315,7 +358,7 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(InvalidSigner.selector); - permit2Proxy.callDiamondWithPermit2SignatureSingle( + permit2Proxy.callDiamondWithPermit2Witness( MALICIOUS_CALLDATA, PERMIT2_USER, permitTransferFrom, @@ -339,7 +382,7 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(InvalidSigner.selector); - permit2Proxy.callDiamondWithPermit2SignatureSingle( + permit2Proxy.callDiamondWithPermit2Witness( diamondCalldata, PERMIT2_USER, permitTransferFrom, @@ -365,7 +408,7 @@ contract Permit2ProxyTest is TestBase { // Execute vm.expectRevert(InvalidSigner.selector); - permit2Proxy.callDiamondWithPermit2SignatureSingle( + permit2Proxy.callDiamondWithPermit2Witness( diamondCalldata, PERMIT2_USER, permitTransferFrom, @@ -375,6 +418,45 @@ contract Permit2ProxyTest is TestBase { /// 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); + + // PermitTransferFrom + msgHash = _getPermitTransferFromHash( + uniPermit2.DOMAIN_SEPARATOR(), + permit, + address(permit2Proxy), + 0, + block.timestamp + 1000 + ); + + signature = _signMsgHash(msgHash, PRIVATE_KEY); + + permitTransferFrom = ISignatureTransfer.PermitTransferFrom( + tokenPermissions, + 0, + block.timestamp + 1000 + ); + } + function _getPermitWitnessTransferFromParams() internal view @@ -460,6 +542,27 @@ contract Permit2ProxyTest is TestBase { 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, From af38e51f1376b81ba904a59bf67715ac353bc95d Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 29 Aug 2024 17:34:48 +0300 Subject: [PATCH 24/37] Redeploy to staging and update demo script --- deployments/_deployments_log_file.json | 10 +++++----- deployments/arbitrum.staging.json | 2 +- script/demoScripts/demoPermit2Proxy.ts | 11 +++++------ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index ea7d6657d..1828bb27c 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -22459,15 +22459,15 @@ "staging": { "1.0.0": [ { - "ADDRESS": "0x442BBFD6a4641B2b710DFfa4754081eC7502a3F7", + "ADDRESS": "0x30252Fd1C12d240F7d63F24e54390F796F2EAF37", "OPTIMIZER_RUNS": "1000000", - "TIMESTAMP": "2024-08-26 18:34:52", - "CONSTRUCTOR_ARGS": "0x00000000000000000000000011f1022ca6adef6400e5677528a80d49a069c00c000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3", + "TIMESTAMP": "2024-08-29 17:03:09", + "CONSTRUCTOR_ARGS": "0x000000000000000000000000d3b2b0ac0afdd0d166a495f5e9fca4ecc715a782000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3", "SALT": "09072024", - "VERIFIED": "true" + "VERIFIED": "false" } ] } } } -} \ No newline at end of file +} diff --git a/deployments/arbitrum.staging.json b/deployments/arbitrum.staging.json index 4a9f4e76d..5f517619f 100644 --- a/deployments/arbitrum.staging.json +++ b/deployments/arbitrum.staging.json @@ -45,5 +45,5 @@ "HopFacetOptimized": "0xf82135385765f1324257ffF74489F16382EBBb8A", "LiFuelFeeCollector": "0x94EA56D8049e93E0308B9c7d1418Baf6A7C68280", "TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70", - "Permit2Proxy": "0x442BBFD6a4641B2b710DFfa4754081eC7502a3F7" + "Permit2Proxy": "0x30252Fd1C12d240F7d63F24e54390F796F2EAF37" } \ No newline at end of file diff --git a/script/demoScripts/demoPermit2Proxy.ts b/script/demoScripts/demoPermit2Proxy.ts index 4eade89ab..67ac24b72 100644 --- a/script/demoScripts/demoPermit2Proxy.ts +++ b/script/demoScripts/demoPermit2Proxy.ts @@ -13,7 +13,7 @@ import { defineCommand, runMain } from 'citty' const DIAMOND_ADDRESS = '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE' const USDT_ADDRESS = '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9' -const PERMIT2_PROXY_ADDRESS = '0x442BBFD6a4641B2b710DFfa4754081eC7502a3F7' +const PERMIT2_PROXY_ADDRESS = '0x30252Fd1C12d240F7d63F24e54390F796F2EAF37' const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3' const PRIVATE_KEY = `0x${process.env.PRIVATE_KEY}` @@ -43,8 +43,8 @@ const main = defineCommand({ 'function nonceBitmap(address owner, uint256 index) external view returns (uint256 nonce)', ]) const permit2ProxyAbi = parseAbi([ - 'function getPermit2MsgHash(address,bytes,address,uint256,uint256,uint256) external view returns (bytes32)', - 'function callDiamondWithPermit2SignatureSingle(address,bytes,address,((address,uint256),uint256,uint256),bytes) external', + 'function getPermit2MsgHash(bytes,address,uint256,uint256,uint256) external view returns (bytes32)', + 'function callDiamondWithPermit2Witness(bytes,address,((address,uint256),uint256,uint256),bytes) external', ]) // Setup a READ-ONLY client @@ -80,7 +80,6 @@ const main = defineCommand({ abi: permit2ProxyAbi, functionName: 'getPermit2MsgHash', args: [ - DIAMOND_ADDRESS, calldata, USDT_ADDRESS, parseUnits('5', 6), @@ -111,8 +110,8 @@ const main = defineCommand({ const tx = await walletClient.writeContract({ address: PERMIT2_PROXY_ADDRESS, abi: permit2ProxyAbi, - functionName: 'callDiamondWithPermit2SignatureSingle', - args: [DIAMOND_ADDRESS, calldata, account.address, permit, signature], + functionName: 'callDiamondWithPermit2Witness', + args: [calldata, account.address, permit, signature], }) }, }) From 056a8c2c1f28c3a856c94f643c97f2551470d522 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 30 Aug 2024 13:45:46 +0300 Subject: [PATCH 25/37] Update comments and remove unneeded events/errors --- src/Periphery/Permit2Proxy.sol | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index 225ce200f..139186bfe 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -40,11 +40,6 @@ contract Permit2Proxy { /// Errors /// error CallToDiamondFailed(bytes); - error DiamondAddressNotWhitelisted(); - - /// Events /// - - event WhitelistUpdated(address[] addresses, bool[] values); /// Constructor /// @@ -65,7 +60,8 @@ contract Permit2Proxy { /// @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) + /// 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 @@ -84,7 +80,7 @@ contract Permit2Proxy { ) public payable { // call permit on token contract to register approval using signature ERC20Permit(tokenAddress).permit( - msg.sender, + msg.sender, // Ensure msg.sender is same wallet that signed permit address(this), amount, deadline, @@ -110,7 +106,9 @@ contract Permit2Proxy { /// @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 + /// 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 @@ -125,7 +123,7 @@ contract Permit2Proxy { to: address(this), requestedAmount: _permit.permitted.amount }), - msg.sender, + msg.sender, // Ensure msg.sender is same wallet that signed permit _signature ); From 1755f86a341a7c61615be32d6fd1143da3b80a0b Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 30 Aug 2024 13:57:35 +0300 Subject: [PATCH 26/37] Add utility methods for determining the next valid nonce --- src/Periphery/Permit2Proxy.sol | 79 ++++++++++++++++++++++ test/solidity/Periphery/Permit2Proxy.t.sol | 14 ++-- 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index 139186bfe..fa987d42d 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -274,4 +274,83 @@ contract Permit2Proxy { 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 index 5ad586725..5b5df609b 100644 --- a/test/solidity/Periphery/Permit2Proxy.t.sol +++ b/test/solidity/Periphery/Permit2Proxy.t.sol @@ -439,12 +439,15 @@ contract Permit2ProxyTest is TestBase { ); bytes32 permit = _getTokenPermissionsHash(tokenPermissions); + // Nonce + uint256 nonce = permit2Proxy.nextNonce(PERMIT2_USER); + // PermitTransferFrom msgHash = _getPermitTransferFromHash( uniPermit2.DOMAIN_SEPARATOR(), permit, address(permit2Proxy), - 0, + nonce, block.timestamp + 1000 ); @@ -452,7 +455,7 @@ contract Permit2ProxyTest is TestBase { permitTransferFrom = ISignatureTransfer.PermitTransferFrom( tokenPermissions, - 0, + nonce, block.timestamp + 1000 ); } @@ -483,12 +486,15 @@ contract Permit2ProxyTest is TestBase { ); bytes32 witness = _getWitnessHash(lifiCall); + // Nonce + uint256 nonce = permit2Proxy.nextNonce(PERMIT2_USER); + // PermitTransferWithWitness msgHash = _getPermitWitnessTransferFromHash( uniPermit2.DOMAIN_SEPARATOR(), permit, address(permit2Proxy), - 0, + nonce, block.timestamp + 1000, witness ); @@ -497,7 +503,7 @@ contract Permit2ProxyTest is TestBase { permitTransferFrom = ISignatureTransfer.PermitTransferFrom( tokenPermissions, - 0, + nonce, block.timestamp + 1000 ); } From 17e5da3ba9cf6390e1995d3478014e0274cfd2b4 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 30 Aug 2024 14:02:59 +0300 Subject: [PATCH 27/37] Redeploy and update demo script --- deployments/_deployments_log_file.json | 6 +++--- deployments/arbitrum.staging.json | 2 +- script/demoScripts/demoPermit2Proxy.ts | 16 +++++++--------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index 1828bb27c..3498f0c68 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -22459,12 +22459,12 @@ "staging": { "1.0.0": [ { - "ADDRESS": "0x30252Fd1C12d240F7d63F24e54390F796F2EAF37", + "ADDRESS": "0xA3C7a31a2A97b847D967e0B755921D084C46a742", "OPTIMIZER_RUNS": "1000000", - "TIMESTAMP": "2024-08-29 17:03:09", + "TIMESTAMP": "2024-08-30 14:01:34", "CONSTRUCTOR_ARGS": "0x000000000000000000000000d3b2b0ac0afdd0d166a495f5e9fca4ecc715a782000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3", "SALT": "09072024", - "VERIFIED": "false" + "VERIFIED": "true" } ] } diff --git a/deployments/arbitrum.staging.json b/deployments/arbitrum.staging.json index 5f517619f..be96f4622 100644 --- a/deployments/arbitrum.staging.json +++ b/deployments/arbitrum.staging.json @@ -45,5 +45,5 @@ "HopFacetOptimized": "0xf82135385765f1324257ffF74489F16382EBBb8A", "LiFuelFeeCollector": "0x94EA56D8049e93E0308B9c7d1418Baf6A7C68280", "TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70", - "Permit2Proxy": "0x30252Fd1C12d240F7d63F24e54390F796F2EAF37" + "Permit2Proxy": "0xA3C7a31a2A97b847D967e0B755921D084C46a742" } \ No newline at end of file diff --git a/script/demoScripts/demoPermit2Proxy.ts b/script/demoScripts/demoPermit2Proxy.ts index 67ac24b72..d27587796 100644 --- a/script/demoScripts/demoPermit2Proxy.ts +++ b/script/demoScripts/demoPermit2Proxy.ts @@ -13,7 +13,7 @@ import { defineCommand, runMain } from 'citty' const DIAMOND_ADDRESS = '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE' const USDT_ADDRESS = '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9' -const PERMIT2_PROXY_ADDRESS = '0x30252Fd1C12d240F7d63F24e54390F796F2EAF37' +const PERMIT2_PROXY_ADDRESS = '0xA3C7a31a2A97b847D967e0B755921D084C46a742' const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3' const PRIVATE_KEY = `0x${process.env.PRIVATE_KEY}` @@ -38,12 +38,10 @@ const main = defineCommand({ const SIGNER_PRIVATE_KEY = `0x${args.signerKey}` as Hex const EXECUTOR_PRIVATE_KEY = `0x${args.executorKey}` as Hex - // Setup the required ABIs - const permit2Abi = parseAbi([ - 'function nonceBitmap(address owner, uint256 index) external view returns (uint256 nonce)', - ]) + // 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', ]) @@ -65,10 +63,10 @@ const main = defineCommand({ // Get the nonce from the PERMIT2 contract const nonce = await client.readContract({ - address: PERMIT2_ADDRESS, - abi: permit2Abi, - functionName: 'nonceBitmap', - args: [account.address, 0n], + address: PERMIT2_PROXY_ADDRESS, + abi: permit2ProxyAbi, + functionName: 'nextNonce', + args: [account.address], }) // Get lastest block From 476ba04143ca67a627f9f4be8eb47de15eb912bf Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 30 Aug 2024 15:32:32 +0300 Subject: [PATCH 28/37] Add documentation --- docs/Permit2Proxy.md | 126 +++++++++++++++++++++++++++++++++++++++++++ docs/README.md | 1 + 2 files changed, 127 insertions(+) create mode 100644 docs/Permit2Proxy.md diff --git a/docs/Permit2Proxy.md b/docs/Permit2Proxy.md new file mode 100644 index 000000000..9938a4ece --- /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..0de0d4b03 100644 --- a/docs/README.md +++ b/docs/README.md @@ -56,3 +56,4 @@ - [FeeCollector](./FeeCollector.md) - [Receiver](./Receiver.md) - [RelayerCelerIM](./RelayerCelerIM.md) +- [Permi2Proxy](./Permit2Proxy.md) From 10b620493dc663dec8236cd31ebb87c5de827213 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 2 Sep 2024 10:10:05 +0300 Subject: [PATCH 29/37] Fixes --- docs/Permit2Proxy.md | 2 +- docs/README.md | 2 +- script/demoScripts/demoPermit2Proxy.ts | 4 ++-- src/Periphery/Permit2Proxy.sol | 7 ++++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/Permit2Proxy.md b/docs/Permit2Proxy.md index 9938a4ece..fa248bf27 100644 --- a/docs/Permit2Proxy.md +++ b/docs/Permit2Proxy.md @@ -103,7 +103,7 @@ function getPermit2MsgHash( ) external view returns (bytes32 msgHash) ``` -Permit2 nonces are non-sequential and are a bit complicated to work with the +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. diff --git a/docs/README.md b/docs/README.md index 0de0d4b03..9bf554527 100644 --- a/docs/README.md +++ b/docs/README.md @@ -56,4 +56,4 @@ - [FeeCollector](./FeeCollector.md) - [Receiver](./Receiver.md) - [RelayerCelerIM](./RelayerCelerIM.md) -- [Permi2Proxy](./Permit2Proxy.md) +- [Permit2Proxy](./Permit2Proxy.md) diff --git a/script/demoScripts/demoPermit2Proxy.ts b/script/demoScripts/demoPermit2Proxy.ts index d27587796..9a6731150 100644 --- a/script/demoScripts/demoPermit2Proxy.ts +++ b/script/demoScripts/demoPermit2Proxy.ts @@ -69,10 +69,10 @@ const main = defineCommand({ args: [account.address], }) - // Get lastest block + // Get latest block const block = await client.getBlock() - // Consturct a valid message hash to sign using Permit2Proxy's utility func + // Construct a valid message hash to sign using Permit2Proxy's utility func const msgHash = await client.readContract({ address: PERMIT2_PROXY_ADDRESS, abi: permit2ProxyAbi, diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index fa987d42d..f088babd8 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -11,8 +11,8 @@ import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC2 /// @title Permit2Proxy /// @author LI.FI (https://li.fi) -/// @notice Proxy contract allowing gasless (Permit2-enabled) calls to our -/// diamond contract +/// @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 /// @@ -31,7 +31,8 @@ contract Permit2Proxy { /// Types /// - // @dev LIFI Specific Witness to verify + // @dev LIFI Specific Witness which verifies the correct calldata and + // diamond address struct LIFICall { address diamondAddress; bytes32 diamondCalldataHash; From f5f566d0f17e212517b988e93d6a50ec88ea0fb1 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 3 Sep 2024 12:58:33 +0300 Subject: [PATCH 30/37] Change witness type to be consistent with the rest of the codebase --- src/Periphery/Permit2Proxy.sol | 12 ++++++------ test/solidity/Periphery/Permit2Proxy.t.sol | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index f088babd8..da12c330c 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -22,10 +22,10 @@ contract Permit2Proxy { mapping(address => bool) public diamondWhitelist; string public constant WITNESS_TYPE_STRING = - "LIFICall witness)LIFICall(address tokenReceiver,address diamondAddress,bytes32 diamondCalldataHash)TokenPermissions(address token,uint256 amount)"; + "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)" + "LiFiCall(address tokenReceiver,address diamondAddress,bytes32 diamondCalldataHash)" ); bytes32 public immutable PERMIT_WITH_WITNESS_TYPEHASH; @@ -33,7 +33,7 @@ contract Permit2Proxy { // @dev LIFI Specific Witness which verifies the correct calldata and // diamond address - struct LIFICall { + struct LiFiCall { address diamondAddress; bytes32 diamondCalldataHash; } @@ -151,7 +151,7 @@ contract Permit2Proxy { ISignatureTransfer.PermitTransferFrom calldata _permit, bytes calldata _signature ) external payable { - LIFICall memory lifiCall = LIFICall( + LiFiCall memory lifiCall = LiFiCall( LIFI_DIAMOND, keccak256(_diamondCalldata) ); @@ -202,7 +202,7 @@ contract Permit2Proxy { bytes32 permit = _getTokenPermissionsHash(tokenPermissions); // Witness - Permit2Proxy.LIFICall memory lifiCall = LIFICall( + Permit2Proxy.LiFiCall memory lifiCall = LiFiCall( LIFI_DIAMOND, keccak256(_diamondCalldata) ); @@ -235,7 +235,7 @@ contract Permit2Proxy { } function _getWitnessHash( - Permit2Proxy.LIFICall memory lifiCall + Permit2Proxy.LiFiCall memory lifiCall ) internal pure returns (bytes32) { return keccak256(abi.encode(WITNESS_TYPEHASH, lifiCall)); } diff --git a/test/solidity/Periphery/Permit2Proxy.t.sol b/test/solidity/Periphery/Permit2Proxy.t.sol index 5b5df609b..8647c5dbc 100644 --- a/test/solidity/Periphery/Permit2Proxy.t.sol +++ b/test/solidity/Periphery/Permit2Proxy.t.sol @@ -480,7 +480,7 @@ contract Permit2ProxyTest is TestBase { // Witness diamondCalldata = _getCalldataForBridging(); - Permit2Proxy.LIFICall memory lifiCall = Permit2Proxy.LIFICall( + Permit2Proxy.LiFiCall memory lifiCall = Permit2Proxy.LiFiCall( DIAMOND_ADDRESS, keccak256(diamondCalldata) ); @@ -542,7 +542,7 @@ contract Permit2ProxyTest is TestBase { } function _getWitnessHash( - Permit2Proxy.LIFICall memory lifiCall + Permit2Proxy.LiFiCall memory lifiCall ) internal view returns (bytes32) { return keccak256(abi.encode(permit2Proxy.WITNESS_TYPEHASH(), lifiCall)); From be50801cc412bc779d7a2421a2e00a0e515a994c Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 3 Sep 2024 13:02:19 +0300 Subject: [PATCH 31/37] Remove unneeded whitelist --- src/Periphery/Permit2Proxy.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Periphery/Permit2Proxy.sol b/src/Periphery/Permit2Proxy.sol index da12c330c..ea75f5375 100644 --- a/src/Periphery/Permit2Proxy.sol +++ b/src/Periphery/Permit2Proxy.sol @@ -19,7 +19,6 @@ contract Permit2Proxy { address public immutable LIFI_DIAMOND; ISignatureTransfer public immutable PERMIT2; - mapping(address => bool) public diamondWhitelist; string public constant WITNESS_TYPE_STRING = "LiFiCall witness)LiFiCall(address tokenReceiver,address diamondAddress,bytes32 diamondCalldataHash)TokenPermissions(address token,uint256 amount)"; From 2202a509d3d1f86a8768ee1d7f3e5ea6b8fe00dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Bl=C3=A4cker?= Date: Fri, 6 Sep 2024 08:38:16 +0700 Subject: [PATCH 32/37] test --- .github/workflows/enforceTestCoverage.yml | 39 +++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/.github/workflows/enforceTestCoverage.yml b/.github/workflows/enforceTestCoverage.yml index 5360b91a6..d62a01635 100644 --- a/.github/workflows/enforceTestCoverage.yml +++ b/.github/workflows/enforceTestCoverage.yml @@ -54,12 +54,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 +77,23 @@ jobs: TOTAL_BRANCHES_FOUND=0 TOTAL_BRANCHES_HIT=0 + # Initialize associative array to store file-specific coverage + declare -A FILE_COVERAGE_SUMMARY + # 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 +108,14 @@ 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) + FILE_COVERAGE_SUMMARY["$FILE_NAME"]="File: $FILE_NAME - Line Coverage: $LINE_COVERAGE_PERCENTAGE% ($FILE_LINES_HIT / $FILE_LINES_FOUND)" + fi + done < "$LCOV_FILE" # Calculate percentages with high precision @@ -118,7 +143,7 @@ jobs: exit 1 fi - # Output result_COVERAGE_REPORTs + # Output results echo "$LINE_COVERAGE_REPORT" echo "$FUNCTION_COVERAGE_REPORT" echo "$BRANCH_COVERAGE_REPORT" @@ -132,6 +157,11 @@ jobs: echo "RESULT_COVERAGE_REPORT=$RESULT_COVERAGE_REPORT" } >> "$GITHUB_ENV" + # Prepare per-file coverage report + for FILE in "${!FILE_COVERAGE_SUMMARY[@]}"; do + echo "${FILE_COVERAGE_SUMMARY[$FILE]}" + done > per_file_coverage_report.txt + - name: Comment with Coverage Summary in PR uses: mshick/add-pr-comment@v2.8.2 with: @@ -142,3 +172,8 @@ jobs: ${{ env.FUNCTION_COVERAGE_REPORT }} ${{ env.BRANCH_COVERAGE_REPORT }} ${{ env.RESULT_COVERAGE_REPORT }} + + ### Per File Coverage: + ```txt + $(cat per_file_coverage_report.txt) + ``` From 6f8640027d32f2cffd941a886015f1260b65c929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Bl=C3=A4cker?= Date: Fri, 6 Sep 2024 08:42:34 +0700 Subject: [PATCH 33/37] test --- .github/workflows/enforceTestCoverage.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/enforceTestCoverage.yml b/.github/workflows/enforceTestCoverage.yml index d62a01635..b6222036e 100644 --- a/.github/workflows/enforceTestCoverage.yml +++ b/.github/workflows/enforceTestCoverage.yml @@ -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 From 1aa3287d86c1c4f39aa8e32e77e647df2e27e69d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Bl=C3=A4cker?= Date: Fri, 6 Sep 2024 08:45:37 +0700 Subject: [PATCH 34/37] test --- .github/workflows/enforceTestCoverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/enforceTestCoverage.yml b/.github/workflows/enforceTestCoverage.yml index b6222036e..0231379ab 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: From c75b982d6cd2f7af7ec514adc2ef4fde0a79bb82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Bl=C3=A4cker?= Date: Fri, 6 Sep 2024 08:50:03 +0700 Subject: [PATCH 35/37] test --- .github/workflows/enforceTestCoverage.yml | 25 +++++++---------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/.github/workflows/enforceTestCoverage.yml b/.github/workflows/enforceTestCoverage.yml index 0231379ab..a350d48ac 100644 --- a/.github/workflows/enforceTestCoverage.yml +++ b/.github/workflows/enforceTestCoverage.yml @@ -112,10 +112,15 @@ jobs: esac # Check if the file is in the list of modified files - if [[ " ${MODIFIED_FILES[@]} " =~ "$FILE_NAME" ]]; then + 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) - FILE_COVERAGE_SUMMARY["$FILE_NAME"]="File: $FILE_NAME - Line Coverage: $LINE_COVERAGE_PERCENTAGE% ($FILE_LINES_HIT / $FILE_LINES_FOUND)" + + # Sanitize file name to prevent any potential issues with bad subscripts + FILE_NAME_CLEAN=$(echo "$FILE_NAME" | tr -cd '[:alnum:]._') + + # Update associative array with sanitized file name + FILE_COVERAGE_SUMMARY["$FILE_NAME_CLEAN"]="File: $FILE_NAME - Line Coverage: $LINE_COVERAGE_PERCENTAGE% ($FILE_LINES_HIT / $FILE_LINES_FOUND)" fi done < "$LCOV_FILE" @@ -163,19 +168,3 @@ jobs: for FILE in "${!FILE_COVERAGE_SUMMARY[@]}"; do echo "${FILE_COVERAGE_SUMMARY[$FILE]}" done > per_file_coverage_report.txt - - - 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 }} - - ### Per File Coverage: - ```txt - $(cat per_file_coverage_report.txt) - ``` From b27f9386bcae1d93cfddc38252fc6a61197b45eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Bl=C3=A4cker?= Date: Fri, 6 Sep 2024 08:58:02 +0700 Subject: [PATCH 36/37] test --- .github/workflows/enforceTestCoverage.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/enforceTestCoverage.yml b/.github/workflows/enforceTestCoverage.yml index a350d48ac..d6fbe84ab 100644 --- a/.github/workflows/enforceTestCoverage.yml +++ b/.github/workflows/enforceTestCoverage.yml @@ -119,6 +119,9 @@ jobs: # Sanitize file name to prevent any potential issues with bad subscripts FILE_NAME_CLEAN=$(echo "$FILE_NAME" | tr -cd '[:alnum:]._') + echo "FILE_NAME_CLEAN: $FILE_NAME_CLEAN" + echo "LINE_COVERAGE_PERCENTAGE: $LINE_COVERAGE_PERCENTAGE" + # Update associative array with sanitized file name FILE_COVERAGE_SUMMARY["$FILE_NAME_CLEAN"]="File: $FILE_NAME - Line Coverage: $LINE_COVERAGE_PERCENTAGE% ($FILE_LINES_HIT / $FILE_LINES_FOUND)" fi From 26fa4eac5457b01f9a2b8f608994a64edbeb3db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Bl=C3=A4cker?= Date: Fri, 6 Sep 2024 08:59:42 +0700 Subject: [PATCH 37/37] test --- .github/workflows/enforceTestCoverage.yml | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/.github/workflows/enforceTestCoverage.yml b/.github/workflows/enforceTestCoverage.yml index d6fbe84ab..6dc545511 100644 --- a/.github/workflows/enforceTestCoverage.yml +++ b/.github/workflows/enforceTestCoverage.yml @@ -79,8 +79,8 @@ jobs: TOTAL_BRANCHES_FOUND=0 TOTAL_BRANCHES_HIT=0 - # Initialize associative array to store file-specific coverage - declare -A FILE_COVERAGE_SUMMARY + # Clear the per-file coverage report file + echo "" > per_file_coverage_report.txt # Read through the lcov file while IFS= read -r line; do @@ -116,14 +116,8 @@ jobs: # Calculate line coverage percentage for the current file LINE_COVERAGE_PERCENTAGE=$(echo "scale=2; $FILE_LINES_HIT / $FILE_LINES_FOUND * 100" | bc) - # Sanitize file name to prevent any potential issues with bad subscripts - FILE_NAME_CLEAN=$(echo "$FILE_NAME" | tr -cd '[:alnum:]._') - - echo "FILE_NAME_CLEAN: $FILE_NAME_CLEAN" - echo "LINE_COVERAGE_PERCENTAGE: $LINE_COVERAGE_PERCENTAGE" - - # Update associative array with sanitized file name - FILE_COVERAGE_SUMMARY["$FILE_NAME_CLEAN"]="File: $FILE_NAME - Line Coverage: $LINE_COVERAGE_PERCENTAGE% ($FILE_LINES_HIT / $FILE_LINES_FOUND)" + # 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" @@ -166,8 +160,3 @@ jobs: echo "BRANCH_COVERAGE_REPORT=$BRANCH_COVERAGE_REPORT" echo "RESULT_COVERAGE_REPORT=$RESULT_COVERAGE_REPORT" } >> "$GITHUB_ENV" - - # Prepare per-file coverage report - for FILE in "${!FILE_COVERAGE_SUMMARY[@]}"; do - echo "${FILE_COVERAGE_SUMMARY[$FILE]}" - done > per_file_coverage_report.txt