From 7414f2d73986e5ca9c071fd7618f8a5aab0dfd2a Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Fri, 12 Jul 2024 17:41:45 +0100 Subject: [PATCH] demo: add permit2 flow --- src/IEIP712.sol | 6 ++ src/ISignatureTransfer.sol | 134 +++++++++++++++++++++++++++++++++++++ src/Orders.sol | 63 +++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 src/IEIP712.sol create mode 100644 src/ISignatureTransfer.sol diff --git a/src/IEIP712.sol b/src/IEIP712.sol new file mode 100644 index 0000000..48c5c0f --- /dev/null +++ b/src/IEIP712.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IEIP712 { + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/src/ISignatureTransfer.sol b/src/ISignatureTransfer.sol new file mode 100644 index 0000000..71ccb8e --- /dev/null +++ b/src/ISignatureTransfer.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +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/Orders.sol b/src/Orders.sol index aa02e52..3e91f2f 100644 --- a/src/Orders.sol +++ b/src/Orders.sol @@ -2,6 +2,9 @@ pragma solidity ^0.8.24; import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {ISignatureTransfer} from "./ISignatureTransfer.sol"; + +address constant permit2 = address(0); /// @notice Tokens sent by the swapper as inputs to the order /// @dev From ERC-7683 @@ -54,6 +57,36 @@ abstract contract OrderDestination { // emit emit Filled(outputs); } + + function fillPermit2( + uint32[] memory chainIds, + ISignatureTransfer.PermitBatchTransferFrom memory permit, + ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, + bytes calldata signature + ) external { + // generate Input structs from the TransferDetails + Output[] memory outputs = _generateOutputs(chainIds, permit.permitted, transferDetails); + + // via permit2 contract, transfer all tokens to this contract && check the permit deadline + ISignatureTransfer(permit2).permitTransferFrom(permit, transferDetails, msg.sender, signature); + + // emit + emit Filled(outputs); + } + + function _generateOutputs( + uint32[] memory chainIds, + ISignatureTransfer.TokenPermissions[] memory permitted, + ISignatureTransfer.SignatureTransferDetails[] memory transferDetails + ) internal view returns (Output[] memory outputs) { + require(chainIds.length == transferDetails.length); + outputs = new Output[](transferDetails.length); + // generate an Input for each permitted transfer + for (uint256 i; i < transferDetails.length; i++) { + outputs[i] = + Output(permitted[i].token, transferDetails[i].requestedAmount, transferDetails[i].to, chainIds[i]); + } + } } /// @notice Contract capable of registering initiation of intent-based Orders. @@ -110,6 +143,36 @@ abstract contract OrderOrigin { } } + function initiatePermit2( + Output[] memory outputs, + ISignatureTransfer.PermitBatchTransferFrom memory permit, + ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, + bytes calldata signature + ) external { + // generate Input structs from the TransferDetails + Input[] memory inputs = _generateInputs(permit.permitted, transferDetails); + + // via permit2 contract, transfer all tokens to this contract && check the permit deadline + ISignatureTransfer(permit2).permitTransferFrom(permit, transferDetails, msg.sender, signature); + + // emit + emit Order(permit.deadline, inputs, outputs); + } + + function _generateInputs( + ISignatureTransfer.TokenPermissions[] memory permitted, + ISignatureTransfer.SignatureTransferDetails[] memory transferDetails + ) internal view returns (Input[] memory inputs) { + inputs = new Input[](transferDetails.length); + // generate an Input for each permitted transfer + for (uint256 i; i < transferDetails.length; i++) { + // ensure the tokens are being transferred to this address + require(transferDetails[i].to == address(this)); + // construct an Input + inputs[i] = Input(permitted[i].token, transferDetails[i].requestedAmount); + } + } + /// @notice Transfer the entire balance of ERC20 tokens to the recipient. /// @dev Called by the Builder within the same block as users' `initiate` transactions /// to claim the `inputs`.