diff --git a/contracts/Create2Factory.sol b/contracts/Create2Factory.sol new file mode 100644 index 000000000..a6d43481b --- /dev/null +++ b/contracts/Create2Factory.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol"; +import { Lockable } from "./Lockable.sol"; + +/** + * @title Create2Factory + * @notice Deploys a new contract via create2 at a deterministic address and then atomically initializes the contract + * @dev Contracts designed to be deployed at deterministic addresses should initialize via a non-constructor + * initializer to maintain bytecode across different chains. + * @custom:security-contact bugs@across.to + */ +contract Create2Factory is Lockable { + /// @notice Emitted when the initialization to a newly deployed contract fails + error InitializationFailed(); + + /** + * @notice Deploys a new contract via create2 at a deterministic address and then atomically initializes the contract + * @param amount The amount of ETH to send with the deployment. If this is not zero then the contract must have a payable constructor + * @param salt The salt to use for the create2 deployment. Must not have been used before for the bytecode + * @param bytecode The bytecode of the contract to deploy + * @param initializationCode The initialization code to call on the deployed contract + */ + function deploy( + uint256 amount, + bytes32 salt, + bytes calldata bytecode, + bytes calldata initializationCode + ) external nonReentrant returns (address) { + address deployedAddress = Create2.deploy(amount, salt, bytecode); + (bool success, ) = deployedAddress.call(initializationCode); + if (!success) revert InitializationFailed(); + return deployedAddress; + } +} diff --git a/contracts/SpokePoolV3Periphery.sol b/contracts/SpokePoolV3Periphery.sol index 1a0b3acd2..32790dbe2 100644 --- a/contracts/SpokePoolV3Periphery.sol +++ b/contracts/SpokePoolV3Periphery.sol @@ -1,4 +1,4 @@ -//SPDX-License-Identifier: Unlicense +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -7,68 +7,137 @@ import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IER import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { MultiCaller } from "@uma/core/contracts/common/implementation/MultiCaller.sol"; import { Lockable } from "./Lockable.sol"; +import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; +import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import { V3SpokePoolInterface } from "./interfaces/V3SpokePoolInterface.sol"; import { IERC20Auth } from "./external/interfaces/IERC20Auth.sol"; import { WETH9Interface } from "./external/interfaces/WETH9Interface.sol"; +import { IPermit2 } from "./external/interfaces/IPermit2.sol"; +import { PeripherySigningLib } from "./libraries/PeripherySigningLib.sol"; +import { SpokePoolV3PeripheryProxyInterface, SpokePoolV3PeripheryInterface } from "./interfaces/SpokePoolV3PeripheryInterface.sol"; + +/** + * @title SpokePoolPeripheryProxy + * @notice User should only call SpokePoolV3Periphery contract functions that require approvals through this + * contract. This is purposefully a simple passthrough contract so that the user only approves this contract to + * pull its assets because the SpokePoolV3Periphery contract can be used to call + * any calldata on any exchange that the user wants to. By separating the contract that is approved to spend + * user funds from the contract that executes arbitrary calldata, the SpokePoolPeriphery does not + * need to validate the calldata that gets executed. + * @dev If this proxy didn't exist and users instead approved and interacted directly with the SpokePoolV3Periphery + * then users would run the unneccessary risk that another user could instruct the Periphery contract to steal + * any approved tokens that the user had left outstanding. + */ +contract SpokePoolPeripheryProxy is SpokePoolV3PeripheryProxyInterface, Lockable, MultiCaller { + using SafeERC20 for IERC20; + using Address for address; + + // Flag set for one time initialization. + bool private initialized; + + // The SpokePoolPeriphery should be deterministically deployed at the same address across all networks, + // so this contract should also be able to be deterministically deployed at the same address across all networks + // since the periphery address is the only initializer argument. + SpokePoolV3Periphery public spokePoolPeriphery; + + error InvalidPeriphery(); + error ContractInitialized(); + + /** + * @notice Construct a new Proxy contract. + * @dev Is empty and all of the state variables are initialized in the initialize function + * to allow for deployment at a deterministic address via create2, which requires that the bytecode + * across different networks is the same. Constructor parameters affect the bytecode so we can only + * add parameters here that are consistent across networks. + */ + constructor() {} + + /** + * @notice Initialize the SpokePoolPeripheryProxy contract. + * @param _spokePoolPeriphery Address of the SpokePoolPeriphery contract that this proxy will call. + */ + function initialize(SpokePoolV3Periphery _spokePoolPeriphery) external nonReentrant { + if (initialized) revert ContractInitialized(); + initialized = true; + if (!address(_spokePoolPeriphery).isContract()) revert InvalidPeriphery(); + spokePoolPeriphery = _spokePoolPeriphery; + } + + /** + * @notice Swaps tokens on this chain via specified router before submitting Across deposit atomically. + * Caller can specify their slippage tolerance for the swap and Across deposit params. + * @dev If swapToken or acrossInputToken are the native token for this chain then this function might fail. + * the assumption is that this function will handle only ERC20 tokens. + * @param swapAndDepositData Specifies the params we need to perform a swap on a generic exchange. + */ + function swapAndBridge(SpokePoolV3PeripheryInterface.SwapAndDepositData calldata swapAndDepositData) + external + override + nonReentrant + { + _callSwapAndBridge(swapAndDepositData); + } + + /** + * @notice Calls swapAndBridge on the spoke pool periphery contract. + * @param swapAndDepositData The data outlining the conditions for the swap and across deposit when calling the periphery contract. + */ + function _callSwapAndBridge(SpokePoolV3PeripheryInterface.SwapAndDepositData calldata swapAndDepositData) internal { + // Load relevant variables on the stack. + IERC20 _swapToken = IERC20(swapAndDepositData.swapToken); + uint256 _swapTokenAmount = swapAndDepositData.swapTokenAmount; + + _swapToken.safeTransferFrom(msg.sender, address(this), _swapTokenAmount); + _swapToken.forceApprove(address(spokePoolPeriphery), _swapTokenAmount); + spokePoolPeriphery.swapAndBridge(swapAndDepositData); + } +} /** * @title SpokePoolV3Periphery - * @notice Contract for performing more complex interactions with an AcrossV3 spoke pool deployment. - * @dev Variables which may be immutable are not marked as immutable, nor defined in the constructor, so that this contract may be deployed deterministically. + * @notice Contract for performing more complex interactions with an Across spoke pool deployment. + * @dev Variables which may be immutable are not marked as immutable, nor defined in the constructor, so that this + * contract may be deployed deterministically at the same address across different networks. * @custom:security-contact bugs@across.to */ -contract SpokePoolV3Periphery is Lockable, MultiCaller { +contract SpokePoolV3Periphery is SpokePoolV3PeripheryInterface, Lockable, MultiCaller, EIP712 { using SafeERC20 for IERC20; using Address for address; - // This contract performs a low level call with arbirary data to an external contract. This is a large attack - // surface and we should whitelist which function selectors are allowed to be called on the exchange. - mapping(bytes4 => bool) public allowedSelectors; - // Across SpokePool we'll submit deposits to with acrossInputToken as the input token. V3SpokePoolInterface public spokePool; - // Exchange address or router where the swapping will happen. - address public exchange; - // Wrapped native token contract address. - WETH9Interface internal wrappedNativeToken; + WETH9Interface public wrappedNativeToken; + + // Canonical Permit2 contract address. + IPermit2 public permit2; + + // Address of the proxy contract that users should interact with to call this contract. + // Force users to call through this contract to make sure they don't leave any approvals/permits + // outstanding on this contract that could be abused because this contract executes arbitrary + // calldata. + address public proxy; + + // Nonce for this contract to use for EIP1271 "signatures". + uint48 private eip1271Nonce; // Boolean indicating whether the contract is initialized. bool private initialized; - // Params we'll need caller to pass in to specify an Across Deposit. The input token will be swapped into first - // before submitting a bridge deposit, which is why we don't include the input token amount as it is not known - // until after the swap. - struct DepositData { - // Token received on destination chain. - address outputToken; - // Amount of output token to be received by recipient. - uint256 outputAmount; - // The account credited with deposit who can submit speedups to the Across deposit. - address depositor; - // The account that will receive the output token on the destination chain. If the output token is - // wrapped native token, then if this is an EOA then they will receive native token on the destination - // chain and if this is a contract then they will receive an ERC20. - address recipient; - // The destination chain identifier. - uint256 destinationChainid; - // The account that can exclusively fill the deposit before the exclusivity parameter. - address exclusiveRelayer; - // Timestamp of the deposit used by system to charge fees. Must be within short window of time into the past - // relative to this chain's current time or deposit will revert. - uint32 quoteTimestamp; - // The timestamp on the destination chain after which this deposit can no longer be filled. - uint32 fillDeadline; - // The timestamp or offset on the destination chain after which anyone can fill the deposit. A detailed description on - // how the parameter is interpreted by the V3 spoke pool can be found at https://github.com/across-protocol/contracts/blob/fa67f5e97eabade68c67127f2261c2d44d9b007e/contracts/SpokePool.sol#L476 - uint32 exclusivityParameter; - // Data that is forwarded to the recipient if the recipient is a contract. - bytes message; - } + // Slot for checking whether this contract is expecting a callback from permit2. Used to confirm whether it should return a valid signature response. + // When solidity 0.8.24 becomes more widely available, this should be replaced with a TSTORE caching method. + bool private expectingPermit2Callback; + + // EIP 1271 magic bytes indicating a valid signature. + bytes4 private constant EIP1271_VALID_SIGNATURE = 0x1626ba7e; + + // EIP 1271 bytes indicating an invalid signature. + bytes4 private constant EIP1271_INVALID_SIGNATURE = 0xffffffff; event SwapBeforeBridge( address exchange, + bytes exchangeCalldata, address indexed swapToken, address indexed acrossInputToken, uint256 swapTokenAmount, @@ -80,70 +149,56 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller { /**************************************** * ERRORS * ****************************************/ + error InvalidPermit2(); + error ContractInitialized(); + error InvalidSignatureLength(); error MinimumExpectedInputAmount(); error LeftoverSrcTokens(); - error InvalidFunctionSelector(); - error ContractInitialized(); error InvalidMsgValue(); error InvalidSpokePool(); + error InvalidProxy(); error InvalidSwapToken(); + error NotProxy(); + error InvalidSignature(); /** - * @notice Construct a new SwapAndBridgeBase contract. - * @param _allowedSelectors Function selectors that are allowed to be called on the exchange. + * @notice Construct a new Proxy contract. + * @dev Is empty and all of the state variables are initialized in the initialize function + * to allow for deployment at a deterministic address via create2, which requires that the bytecode + * across different networks is the same. Constructor parameters affect the bytecode so we can only + * add parameters here that are consistent across networks. */ - constructor(bytes4[] memory _allowedSelectors) { - for (uint256 i = 0; i < _allowedSelectors.length; i++) { - allowedSelectors[_allowedSelectors[i]] = true; - } - } + constructor() EIP712("ACROSS-V3-PERIPHERY", "1.0.0") {} /** * @notice Initializes the SwapAndBridgeBase contract. * @param _spokePool Address of the SpokePool contract that we'll submit deposits to. * @param _wrappedNativeToken Address of the wrapped native token for the network this contract is deployed to. - * @param _exchange Address of the exchange where tokens will be swapped. + * @param _proxy Address of the proxy contract that users should interact with to call this contract. + * @param _permit2 Address of the deployed network's canonical permit2 contract. * @dev These values are initialized in a function and not in the constructor so that the creation code of this contract - * is the same across networks with different addresses for the wrapped native token, the exchange this contract uses to - * swap and bridge, and this network's corresponding spoke pool contract. This is to allow this contract to be deterministically - * deployed with CREATE2. - * @dev This function can be front-run by anybody, so it is critical to check that the `spokePool`, `wrappedNativeToken`, and `exchange` - * values used in the single call to this function were passed in correctly before enabling the usage of this contract. + * is the same across networks with different addresses for the wrapped native token and this network's + * corresponding spoke pool contract. This is to allow this contract to be deterministically deployed with CREATE2. */ function initialize( V3SpokePoolInterface _spokePool, WETH9Interface _wrappedNativeToken, - address _exchange - ) external { + address _proxy, + IPermit2 _permit2 + ) external nonReentrant { if (initialized) revert ContractInitialized(); initialized = true; + if (!address(_spokePool).isContract()) revert InvalidSpokePool(); spokePool = _spokePool; wrappedNativeToken = _wrappedNativeToken; - exchange = _exchange; + if (!_proxy.isContract()) revert InvalidProxy(); + proxy = _proxy; + if (!address(_permit2).isContract()) revert InvalidPermit2(); + permit2 = _permit2; } - /** - * @notice Passthrough function to `depositV3()` on the SpokePool contract. - * @dev Protects the caller from losing their ETH (or other native token) by reverting if the SpokePool address - * they intended to call does not exist on this chain. Because this contract can be deployed at the same address - * everywhere callers should be protected even if the transaction is submitted to an unintended network. - * This contract should only be used for native token deposits, as this problem only exists for native tokens. - * @param recipient Address to receive funds at on destination chain. - * @param inputToken Token to lock into this contract to initiate deposit. - * @param inputAmount Amount of tokens to deposit. - * @param outputAmount Amount of tokens to receive on destination chain. - * @param destinationChainId Denotes network where user will receive funds from SpokePool by a relayer. - * @param quoteTimestamp Timestamp used by relayers to compute this deposit's realizedLPFeePct which is paid - * to LP pool on HubPool. - * @param message Arbitrary data that can be used to pass additional information to the recipient along with the tokens. - * Note: this is intended to be used to pass along instructions for how a contract should use or allocate the tokens. - * @param exclusiveRelayer Address of the relayer who has exclusive rights to fill this deposit. Can be set to - * 0x0 if no period is desired. If so, then must set exclusivityParameter to 0. - * @param exclusivityParameter Timestamp or offset, after which any relayer can fill this deposit. Must set - * to 0 if exclusiveRelayer is set to 0x0, and vice versa. - * @param fillDeadline Timestamp after which this deposit can no longer be filled. - */ + // @inheritdoc SpokePoolV3PeripheryInterface function deposit( address recipient, address inputToken, @@ -155,7 +210,7 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller { uint32 fillDeadline, uint32 exclusivityParameter, bytes memory message - ) external payable nonReentrant { + ) external payable override nonReentrant { if (msg.value != inputAmount) revert InvalidMsgValue(); if (!address(spokePool).isContract()) revert InvalidSpokePool(); // Set msg.sender as the depositor so that msg.sender can speed up the deposit. @@ -177,133 +232,107 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller { ); } - /** - * @notice Swaps tokens on this chain via specified router before submitting Across deposit atomically. - * Caller can specify their slippage tolerance for the swap and Across deposit params. - * @dev If swapToken or acrossInputToken are the native token for this chain then this function might fail. - * the assumption is that this function will handle only ERC20 tokens. - * @param swapToken Address of the token that will be swapped for acrossInputToken. - * @param acrossInputToken Address of the token that will be bridged via Across as the inputToken. - * @param routerCalldata ABI encoded function data to call on router. Should form a swap of swapToken for - * enough of acrossInputToken, otherwise this function will revert. - * @param swapTokenAmount Amount of swapToken to swap for a minimum amount of depositData.inputToken. - * @param minExpectedInputTokenAmount Minimum amount of received depositData.inputToken that we'll submit bridge - * deposit with. - * @param depositData Specifies the Across deposit params we'll send after the swap. - */ - function swapAndBridge( - IERC20 swapToken, - IERC20 acrossInputToken, - bytes calldata routerCalldata, - uint256 swapTokenAmount, - uint256 minExpectedInputTokenAmount, - DepositData calldata depositData - ) external payable nonReentrant { + // @inheritdoc SpokePoolV3PeripheryInterface + function swapAndBridge(SwapAndDepositData calldata swapAndDepositData) external payable override nonReentrant { // If a user performs a swapAndBridge with the swap token as the native token, wrap the value and treat the rest of transaction // as though the user deposited a wrapped native token. if (msg.value != 0) { - if (msg.value != swapTokenAmount) revert InvalidMsgValue(); - if (address(swapToken) != address(wrappedNativeToken)) revert InvalidSwapToken(); + if (msg.value != swapAndDepositData.swapTokenAmount) revert InvalidMsgValue(); + if (address(swapAndDepositData.swapToken) != address(wrappedNativeToken)) revert InvalidSwapToken(); wrappedNativeToken.deposit{ value: msg.value }(); } else { - swapToken.safeTransferFrom(msg.sender, address(this), swapTokenAmount); + // If swap requires an approval to this contract, then force user to go through proxy + // to prevent their approval from being abused. + _calledByProxy(); + IERC20(swapAndDepositData.swapToken).safeTransferFrom( + msg.sender, + address(this), + swapAndDepositData.swapTokenAmount + ); } - _swapAndBridge( - routerCalldata, - swapTokenAmount, - minExpectedInputTokenAmount, - depositData, - swapToken, - acrossInputToken - ); + _swapAndBridge(swapAndDepositData); } - /** - * @notice Swaps an EIP-2612 token on this chain via specified router before submitting Across deposit atomically. - * Caller can specify their slippage tolerance for the swap and Across deposit params. - * @dev If swapToken does not implement `permit` to the specifications of EIP-2612, this function will fail. - * @param swapToken Address of the token that will be swapped for acrossInputToken. - * @param acrossInputToken Address of the token that will be bridged via Across as the inputToken. - * @param routerCalldata ABI encoded function data to call on router. Should form a swap of swapToken for - * enough of acrossInputToken, otherwise this function will revert. - * @param swapTokenAmount Amount of swapToken to swap for a minimum amount of depositData.inputToken. - * @param minExpectedInputTokenAmount Minimum amount of received depositData.inputToken that we'll submit bridge - * deposit with. - * @param depositData Specifies the Across deposit params we'll send after the swap. - * @param deadline Deadline before which the permit signature is valid. - * @param v v of the permit signature. - * @param r r of the permit signature. - * @param s s of the permit signature. - */ + // @inheritdoc SpokePoolV3PeripheryInterface function swapAndBridgeWithPermit( - IERC20Permit swapToken, - IERC20 acrossInputToken, - bytes calldata routerCalldata, - uint256 swapTokenAmount, - uint256 minExpectedInputTokenAmount, - DepositData calldata depositData, + address signatureOwner, + SwapAndDepositData calldata swapAndDepositData, uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external nonReentrant { - IERC20 _swapToken = IERC20(address(swapToken)); // Cast IERC20Permit to IERC20. + bytes calldata permitSignature, + bytes calldata swapAndDepositDataSignature + ) external override nonReentrant { + (bytes32 r, bytes32 s, uint8 v) = PeripherySigningLib.deserializeSignature(permitSignature); + // Load variables used in this function onto the stack. + address _swapToken = swapAndDepositData.swapToken; + uint256 _swapTokenAmount = swapAndDepositData.swapTokenAmount; + uint256 _submissionFeeAmount = swapAndDepositData.submissionFees.amount; + address _submissionFeeRecipient = swapAndDepositData.submissionFees.recipient; + uint256 _pullAmount = _submissionFeeAmount + _swapTokenAmount; + // For permit transactions, we wrap the call in a try/catch block so that the transaction will continue even if the call to // permit fails. For example, this may be useful if the permit signature, which can be redeemed by anyone, is executed by somebody // other than this contract. - try swapToken.permit(msg.sender, address(this), swapTokenAmount, deadline, v, r, s) {} catch {} - - _swapToken.safeTransferFrom(msg.sender, address(this), swapTokenAmount); - _swapAndBridge( - routerCalldata, - swapTokenAmount, - minExpectedInputTokenAmount, - depositData, - _swapToken, - acrossInputToken + try IERC20Permit(_swapToken).permit(signatureOwner, address(this), _pullAmount, deadline, v, r, s) {} catch {} + IERC20(_swapToken).safeTransferFrom(signatureOwner, address(this), _pullAmount); + _paySubmissionFees(_swapToken, _submissionFeeRecipient, _submissionFeeAmount); + // Verify that the signatureOwner signed the input swapAndDepositData. + _validateSignature( + signatureOwner, + PeripherySigningLib.hashSwapAndDepositData(swapAndDepositData), + swapAndDepositDataSignature ); + _swapAndBridge(swapAndDepositData); } - /** - * @notice Swaps an EIP-3009 token on this chain via specified router before submitting Across deposit atomically. - * Caller can specify their slippage tolerance for the swap and Across deposit params. - * @dev If swapToken does not implement `receiveWithAuthorization` to the specifications of EIP-3009, this call will revert. - * @param swapToken Address of the token that will be swapped for acrossInputToken. - * @param acrossInputToken Address of the token that will be bridged via Across as the inputToken. - * @param routerCalldata ABI encoded function data to call on router. Should form a swap of swapToken for - * enough of acrossInputToken, otherwise this function will revert. - * @param swapTokenAmount Amount of swapToken to swap for a minimum amount of depositData.inputToken. - * @param minExpectedInputTokenAmount Minimum amount of received depositData.inputToken that we'll submit bridge - * deposit with. - * @param depositData Specifies the Across deposit params we'll send after the swap. - * @param validAfter The unix time after which the `receiveWithAuthorization` signature is valid. - * @param validBefore The unix time before which the `receiveWithAuthorization` signature is valid. - * @param nonce Unique nonce used in the `receiveWithAuthorization` signature. - * @param v v of the EIP-3009 signature. - * @param r r of the EIP-3009 signature. - * @param s s of the EIP-3009 signature. - */ + // @inheritdoc SpokePoolV3PeripheryInterface + function swapAndBridgeWithPermit2( + address signatureOwner, + SwapAndDepositData calldata swapAndDepositData, + IPermit2.PermitTransferFrom calldata permit, + bytes calldata signature + ) external override nonReentrant { + bytes32 witness = PeripherySigningLib.hashSwapAndDepositData(swapAndDepositData); + uint256 _submissionFeeAmount = swapAndDepositData.submissionFees.amount; + IPermit2.SignatureTransferDetails memory transferDetails = IPermit2.SignatureTransferDetails({ + to: address(this), + requestedAmount: swapAndDepositData.swapTokenAmount + _submissionFeeAmount + }); + + permit2.permitWitnessTransferFrom( + permit, + transferDetails, + signatureOwner, + witness, + PeripherySigningLib.EIP712_SWAP_AND_DEPOSIT_TYPE_STRING, + signature + ); + _paySubmissionFees( + swapAndDepositData.swapToken, + swapAndDepositData.submissionFees.recipient, + _submissionFeeAmount + ); + _swapAndBridge(swapAndDepositData); + } + + // @inheritdoc SpokePoolV3PeripheryInterface function swapAndBridgeWithAuthorization( - IERC20Auth swapToken, - IERC20 acrossInputToken, - bytes calldata routerCalldata, - uint256 swapTokenAmount, - uint256 minExpectedInputTokenAmount, - DepositData calldata depositData, + address signatureOwner, + SwapAndDepositData calldata swapAndDepositData, uint256 validAfter, uint256 validBefore, bytes32 nonce, - uint8 v, - bytes32 r, - bytes32 s - ) external nonReentrant { + bytes calldata receiveWithAuthSignature, + bytes calldata swapAndDepositDataSignature + ) external override nonReentrant { + (bytes32 r, bytes32 s, uint8 v) = PeripherySigningLib.deserializeSignature(receiveWithAuthSignature); + uint256 _submissionFeeAmount = swapAndDepositData.submissionFees.amount; // While any contract can vacuously implement `transferWithAuthorization` (or just have a fallback), - // if tokens were not sent to this contract, by this call to the swapToken, the call to `transferFrom` - // in _swapAndBridge will revert. - swapToken.receiveWithAuthorization( - msg.sender, + // if tokens were not sent to this contract, by this call to swapData.swapToken, this function will revert + // when attempting to swap tokens it does not own. + IERC20Auth(address(swapAndDepositData.swapToken)).receiveWithAuthorization( + signatureOwner, address(this), - swapTokenAmount, + swapAndDepositData.swapTokenAmount + _submissionFeeAmount, validAfter, validBefore, nonce, @@ -311,76 +340,126 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller { r, s ); - IERC20 _swapToken = IERC20(address(swapToken)); // Cast IERC20Auth to IERC20. - - _swapAndBridge( - routerCalldata, - swapTokenAmount, - minExpectedInputTokenAmount, - depositData, - _swapToken, - acrossInputToken + _paySubmissionFees( + swapAndDepositData.swapToken, + swapAndDepositData.submissionFees.recipient, + _submissionFeeAmount + ); + + // Verify that the signatureOwner signed the input swapAndDepositData. + _validateSignature( + signatureOwner, + PeripherySigningLib.hashSwapAndDepositData(swapAndDepositData), + swapAndDepositDataSignature ); + _swapAndBridge(swapAndDepositData); } - /** - * @notice Deposits an EIP-2612 token Across input token into the Spoke Pool contract. - * @dev If `acrossInputToken` does not implement `permit` to the specifications of EIP-2612, this function will fail. - * @param acrossInputToken EIP-2612 compliant token to deposit. - * @param acrossInputAmount Amount of the input token to deposit. - * @param depositData Specifies the Across deposit params to send. - * @param deadline Deadline before which the permit signature is valid. - * @param v v of the permit signature. - * @param r r of the permit signature. - * @param s s of the permit signature. - */ + // @inheritdoc SpokePoolV3PeripheryInterface function depositWithPermit( - IERC20Permit acrossInputToken, - uint256 acrossInputAmount, + address signatureOwner, DepositData calldata depositData, uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external nonReentrant { - IERC20 _acrossInputToken = IERC20(address(acrossInputToken)); // Cast IERC20Permit to an IERC20 type. + bytes calldata permitSignature, + bytes calldata depositDataSignature + ) external override nonReentrant { + (bytes32 r, bytes32 s, uint8 v) = PeripherySigningLib.deserializeSignature(permitSignature); + // Load variables used in this function onto the stack. + address _inputToken = depositData.baseDepositData.inputToken; + uint256 _inputAmount = depositData.inputAmount; + uint256 _submissionFeeAmount = depositData.submissionFees.amount; + address _submissionFeeRecipient = depositData.submissionFees.recipient; + uint256 _pullAmount = _submissionFeeAmount + _inputAmount; + // For permit transactions, we wrap the call in a try/catch block so that the transaction will continue even if the call to // permit fails. For example, this may be useful if the permit signature, which can be redeemed by anyone, is executed by somebody // other than this contract. - try acrossInputToken.permit(msg.sender, address(this), acrossInputAmount, deadline, v, r, s) {} catch {} + try IERC20Permit(_inputToken).permit(signatureOwner, address(this), _pullAmount, deadline, v, r, s) {} catch {} + IERC20(_inputToken).safeTransferFrom(signatureOwner, address(this), _pullAmount); + _paySubmissionFees(_inputToken, _submissionFeeRecipient, _submissionFeeAmount); - _acrossInputToken.safeTransferFrom(msg.sender, address(this), acrossInputAmount); - _depositV3(_acrossInputToken, acrossInputAmount, depositData); + // Verify that the signatureOwner signed the input depositData. + _validateSignature(signatureOwner, PeripherySigningLib.hashDepositData(depositData), depositDataSignature); + _depositV3( + depositData.baseDepositData.depositor, + depositData.baseDepositData.recipient, + _inputToken, + depositData.baseDepositData.outputToken, + _inputAmount, + depositData.baseDepositData.outputAmount, + depositData.baseDepositData.destinationChainId, + depositData.baseDepositData.exclusiveRelayer, + depositData.baseDepositData.quoteTimestamp, + depositData.baseDepositData.fillDeadline, + depositData.baseDepositData.exclusivityParameter, + depositData.baseDepositData.message + ); } - /** - * @notice Deposits an EIP-3009 compliant Across input token into the Spoke Pool contract. - * @dev If `acrossInputToken` does not implement `receiveWithAuthorization` to the specifications of EIP-3009, this call will revert. - * @param acrossInputToken EIP-3009 compliant token to deposit. - * @param acrossInputAmount Amount of the input token to deposit. - * @param depositData Specifies the Across deposit params to send. - * @param validAfter The unix time after which the `receiveWithAuthorization` signature is valid. - * @param validBefore The unix time before which the `receiveWithAuthorization` signature is valid. - * @param nonce Unique nonce used in the `receiveWithAuthorization` signature. - * @param v v of the EIP-3009 signature. - * @param r r of the EIP-3009 signature. - * @param s s of the EIP-3009 signature. - */ + // @inheritdoc SpokePoolV3PeripheryInterface + function depositWithPermit2( + address signatureOwner, + DepositData calldata depositData, + IPermit2.PermitTransferFrom calldata permit, + bytes calldata signature + ) external override nonReentrant { + bytes32 witness = PeripherySigningLib.hashDepositData(depositData); + uint256 _submissionFeeAmount = depositData.submissionFees.amount; + IPermit2.SignatureTransferDetails memory transferDetails = IPermit2.SignatureTransferDetails({ + to: address(this), + requestedAmount: depositData.inputAmount + _submissionFeeAmount + }); + + permit2.permitWitnessTransferFrom( + permit, + transferDetails, + signatureOwner, + witness, + PeripherySigningLib.EIP712_DEPOSIT_TYPE_STRING, + signature + ); + _paySubmissionFees( + depositData.baseDepositData.inputToken, + depositData.submissionFees.recipient, + _submissionFeeAmount + ); + + _depositV3( + depositData.baseDepositData.depositor, + depositData.baseDepositData.recipient, + depositData.baseDepositData.inputToken, + depositData.baseDepositData.outputToken, + depositData.inputAmount, + depositData.baseDepositData.outputAmount, + depositData.baseDepositData.destinationChainId, + depositData.baseDepositData.exclusiveRelayer, + depositData.baseDepositData.quoteTimestamp, + depositData.baseDepositData.fillDeadline, + depositData.baseDepositData.exclusivityParameter, + depositData.baseDepositData.message + ); + } + + // @inheritdoc SpokePoolV3PeripheryInterface function depositWithAuthorization( - IERC20Auth acrossInputToken, - uint256 acrossInputAmount, + address signatureOwner, DepositData calldata depositData, uint256 validAfter, uint256 validBefore, bytes32 nonce, - uint8 v, - bytes32 r, - bytes32 s - ) external nonReentrant { - acrossInputToken.receiveWithAuthorization( - msg.sender, + bytes calldata receiveWithAuthSignature, + bytes calldata depositDataSignature + ) external override nonReentrant { + // Load variables used multiple times onto the stack. + uint256 _inputAmount = depositData.inputAmount; + uint256 _submissionFeeAmount = depositData.submissionFees.amount; + + // Redeem the receiveWithAuthSignature. + (bytes32 r, bytes32 s, uint8 v) = PeripherySigningLib.deserializeSignature(receiveWithAuthSignature); + IERC20Auth(depositData.baseDepositData.inputToken).receiveWithAuthorization( + signatureOwner, address(this), - acrossInputAmount, + _inputAmount + _submissionFeeAmount, validAfter, validBefore, nonce, @@ -388,106 +467,205 @@ contract SpokePoolV3Periphery is Lockable, MultiCaller { r, s ); - IERC20 _acrossInputToken = IERC20(address(acrossInputToken)); // Cast the input token to an IERC20. - _depositV3(_acrossInputToken, acrossInputAmount, depositData); + _paySubmissionFees( + depositData.baseDepositData.inputToken, + depositData.submissionFees.recipient, + _submissionFeeAmount + ); + + // Verify that the signatureOwner signed the input depositData. + _validateSignature(signatureOwner, PeripherySigningLib.hashDepositData(depositData), depositDataSignature); + _depositV3( + depositData.baseDepositData.depositor, + depositData.baseDepositData.recipient, + depositData.baseDepositData.inputToken, + depositData.baseDepositData.outputToken, + _inputAmount, + depositData.baseDepositData.outputAmount, + depositData.baseDepositData.destinationChainId, + depositData.baseDepositData.exclusiveRelayer, + depositData.baseDepositData.quoteTimestamp, + depositData.baseDepositData.fillDeadline, + depositData.baseDepositData.exclusivityParameter, + depositData.baseDepositData.message + ); + } + + /** + * @notice Verifies that the signer is the owner of the signing contract. + * @dev The _hash and _signature fields are intentionally ignored since this contract will accept + * any signature which originated from permit2 after the call to the exchange. + * @dev This is safe since this contract should never hold funds nor approvals, other than when it is depositing or swapping. + */ + function isValidSignature(bytes32, bytes calldata) external view returns (bytes4 magicBytes) { + magicBytes = (msg.sender == address(permit2) && expectingPermit2Callback) + ? EIP1271_VALID_SIGNATURE + : EIP1271_INVALID_SIGNATURE; + } + + /** + * @notice Returns the contract's EIP712 domain separator, used to sign hashed depositData/swapAndDepositData types. + */ + function domainSeparator() external view returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @notice Validates that the typed data hash corresponds to the input signature owner and corresponding signature. + * @param signatureOwner The alledged signer of the input hash. + * @param typedDataHash The EIP712 data hash to check the signature against. + * @param signature The signature to validate. + */ + function _validateSignature( + address signatureOwner, + bytes32 typedDataHash, + bytes calldata signature + ) private view { + if (!SignatureChecker.isValidSignatureNow(signatureOwner, _hashTypedDataV4(typedDataHash), signature)) + revert InvalidSignature(); } /** * @notice Approves the spoke pool and calls `depositV3` function with the specified input parameters. - * @param _acrossInputToken Token to deposit into the spoke pool. - * @param _acrossInputAmount Amount of the input token to deposit into the spoke pool. - * @param depositData Specifies the Across deposit params to use. + * @param depositor The address on the origin chain which should be treated as the depositor by Across, and will therefore receive refunds if this deposit + * is unfilled. + * @param recipient The address on the destination chain which should receive outputAmount of outputToken. + * @param inputToken The token to deposit on the origin chain. + * @param outputToken The token to receive on the destination chain. + * @param inputAmount The amount of the input token to deposit. + * @param outputAmount The amount of the output token to receive. + * @param destinationChainId The network ID for the destination chain. + * @param exclusiveRelayer The optional address for an Across relayer which may fill the deposit exclusively. + * @param quoteTimestamp The timestamp at which the relay and LP fee was calculated. + * @param fillDeadline The timestamp at which the deposit must be filled before it will be refunded by Across. + * @param exclusivityParameter The deadline or offset during which the exclusive relayer has rights to fill the deposit without contention. + * @param message The message to execute on the destination chain. */ function _depositV3( - IERC20 _acrossInputToken, - uint256 _acrossInputAmount, - DepositData calldata depositData + address depositor, + address recipient, + address inputToken, + address outputToken, + uint256 inputAmount, + uint256 outputAmount, + uint256 destinationChainId, + address exclusiveRelayer, + uint32 quoteTimestamp, + uint32 fillDeadline, + uint32 exclusivityParameter, + bytes calldata message ) private { - _acrossInputToken.safeIncreaseAllowance(address(spokePool), _acrossInputAmount); + IERC20(inputToken).forceApprove(address(spokePool), inputAmount); spokePool.depositV3( - depositData.depositor, - depositData.recipient, - address(_acrossInputToken), // input token - depositData.outputToken, // output token - _acrossInputAmount, // input amount. - depositData.outputAmount, // output amount - depositData.destinationChainid, - depositData.exclusiveRelayer, - depositData.quoteTimestamp, - depositData.fillDeadline, - depositData.exclusivityParameter, - depositData.message + depositor, + recipient, + inputToken, // input token + outputToken, // output token + inputAmount, // input amount. + outputAmount, // output amount + destinationChainId, + exclusiveRelayer, + quoteTimestamp, + fillDeadline, + exclusivityParameter, + message ); } - // This contract supports two variants of swap and bridge, one that allows one token and another that allows the caller to pass them in. - function _swapAndBridge( - bytes calldata routerCalldata, - uint256 swapTokenAmount, - uint256 minExpectedInputTokenAmount, - DepositData calldata depositData, - IERC20 _swapToken, - IERC20 _acrossInputToken - ) private { - // Note: this check should never be impactful, but is here out of an abundance of caution. - // For example, if the exchange address in the contract is also an ERC20 token that is approved by some - // user on this contract, a malicious actor could call transferFrom to steal the user's tokens. - if (!allowedSelectors[bytes4(routerCalldata)]) revert InvalidFunctionSelector(); + /** + * @notice Swaps a token on the origin chain before depositing into the Across spoke pool atomically. + * @param swapAndDepositData The parameters to use when calling both the swap on an exchange and bridging via an Across spoke pool. + */ + function _swapAndBridge(SwapAndDepositData calldata swapAndDepositData) private { + // Load variables we use multiple times onto the stack. + IERC20 _swapToken = IERC20(swapAndDepositData.swapToken); + IERC20 _acrossInputToken = IERC20(swapAndDepositData.depositData.inputToken); + TransferType _transferType = swapAndDepositData.transferType; + address _exchange = swapAndDepositData.exchange; + uint256 _swapTokenAmount = swapAndDepositData.swapTokenAmount; // Swap and run safety checks. uint256 srcBalanceBefore = _swapToken.balanceOf(address(this)); uint256 dstBalanceBefore = _acrossInputToken.balanceOf(address(this)); - _swapToken.safeIncreaseAllowance(exchange, swapTokenAmount); + // The exchange will either receive funds from this contract via a direct transfer, an approval to spend funds on this contract, or via an + // EIP1271 permit2 signature. + if (_transferType == TransferType.Approval) _swapToken.forceApprove(_exchange, _swapTokenAmount); + else if (_transferType == TransferType.Transfer) _swapToken.transfer(_exchange, _swapTokenAmount); + else { + _swapToken.forceApprove(address(permit2), _swapTokenAmount); + expectingPermit2Callback = true; + permit2.permit( + address(this), // owner + IPermit2.PermitSingle({ + details: IPermit2.PermitDetails({ + token: address(_swapToken), + amount: uint160(_swapTokenAmount), + expiration: uint48(block.timestamp), + nonce: eip1271Nonce++ + }), + spender: _exchange, + sigDeadline: block.timestamp + }), // permitSingle + "0x" // signature is unused. The only verification for a valid signature is if we are at this code block. + ); + expectingPermit2Callback = false; + } // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory result) = exchange.call(routerCalldata); + (bool success, bytes memory result) = _exchange.call(swapAndDepositData.routerCalldata); require(success, string(result)); - _checkSwapOutputAndDeposit( - swapTokenAmount, - srcBalanceBefore, - dstBalanceBefore, - minExpectedInputTokenAmount, - depositData, - _swapToken, - _acrossInputToken - ); - } - - /** - * @notice Check that the swap returned enough tokens to submit an Across deposit with and then submit the deposit. - * @param swapTokenAmount Amount of swapToken to swap for a minimum amount of acrossInputToken. - * @param swapTokenBalanceBefore Balance of swapToken before swap. - * @param inputTokenBalanceBefore Amount of Across input token we held before swap - * @param minExpectedInputTokenAmount Minimum amount of received acrossInputToken that we'll bridge - **/ - function _checkSwapOutputAndDeposit( - uint256 swapTokenAmount, - uint256 swapTokenBalanceBefore, - uint256 inputTokenBalanceBefore, - uint256 minExpectedInputTokenAmount, - DepositData calldata depositData, - IERC20 _swapToken, - IERC20 _acrossInputToken - ) private { // Sanity check that we received as many tokens as we require: - uint256 returnAmount = _acrossInputToken.balanceOf(address(this)) - inputTokenBalanceBefore; + uint256 returnAmount = _acrossInputToken.balanceOf(address(this)) - dstBalanceBefore; + // Sanity check that received amount from swap is enough to submit Across deposit with. - if (returnAmount < minExpectedInputTokenAmount) revert MinimumExpectedInputAmount(); + if (returnAmount < swapAndDepositData.minExpectedInputTokenAmount) revert MinimumExpectedInputAmount(); // Sanity check that we don't have any leftover swap tokens that would be locked in this contract (i.e. check // that we weren't partial filled). - if (swapTokenBalanceBefore - _swapToken.balanceOf(address(this)) != swapTokenAmount) revert LeftoverSrcTokens(); + if (srcBalanceBefore - _swapToken.balanceOf(address(this)) != _swapTokenAmount) revert LeftoverSrcTokens(); emit SwapBeforeBridge( - exchange, + _exchange, + swapAndDepositData.routerCalldata, address(_swapToken), address(_acrossInputToken), - swapTokenAmount, + _swapTokenAmount, returnAmount, - depositData.outputToken, - depositData.outputAmount + swapAndDepositData.depositData.outputToken, + swapAndDepositData.depositData.outputAmount ); + // Deposit the swapped tokens into Across and bridge them using remainder of input params. - _depositV3(_acrossInputToken, returnAmount, depositData); + _depositV3( + swapAndDepositData.depositData.depositor, + swapAndDepositData.depositData.recipient, + address(_acrossInputToken), + swapAndDepositData.depositData.outputToken, + returnAmount, + swapAndDepositData.depositData.outputAmount, + swapAndDepositData.depositData.destinationChainId, + swapAndDepositData.depositData.exclusiveRelayer, + swapAndDepositData.depositData.quoteTimestamp, + swapAndDepositData.depositData.fillDeadline, + swapAndDepositData.depositData.exclusivityParameter, + swapAndDepositData.depositData.message + ); + } + + function _paySubmissionFees( + address feeToken, + address recipient, + uint256 amount + ) private { + if (amount > 0) { + IERC20(feeToken).safeTransfer(recipient, amount); + } + } + + /** + * @notice Function to check that the msg.sender is the initialized proxy contract. + */ + function _calledByProxy() internal view { + if (msg.sender != proxy) revert NotProxy(); } } diff --git a/contracts/SwapAndBridge.sol b/contracts/SwapAndBridge.sol deleted file mode 100644 index 4c6cf84d7..000000000 --- a/contracts/SwapAndBridge.sol +++ /dev/null @@ -1,505 +0,0 @@ -//SPDX-License-Identifier: Unlicense -pragma solidity ^0.8.0; - -import "./interfaces/V3SpokePoolInterface.sol"; -import "./external/interfaces/IERC20Auth.sol"; -import "./external/interfaces/WETH9Interface.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; -import "./Lockable.sol"; -import "@uma/core/contracts/common/implementation/MultiCaller.sol"; - -/** - * @title SwapAndBridgeBase - * @notice Base contract for both variants of SwapAndBridge. - * @dev Variables which may be immutable are not marked as immutable so that this contract may be deployed deterministically. - * @custom:security-contact bugs@across.to - */ -abstract contract SwapAndBridgeBase is Lockable, MultiCaller { - using SafeERC20 for IERC20; - - // This contract performs a low level call with arbirary data to an external contract. This is a large attack - // surface and we should whitelist which function selectors are allowed to be called on the exchange. - mapping(bytes4 => bool) public allowedSelectors; - - // Across SpokePool we'll submit deposits to with acrossInputToken as the input token. - V3SpokePoolInterface public spokePool; - - // Exchange address or router where the swapping will happen. - address public exchange; - - // Wrapped native token contract address. - WETH9Interface internal wrappedNativeToken; - - // Params we'll need caller to pass in to specify an Across Deposit. The input token will be swapped into first - // before submitting a bridge deposit, which is why we don't include the input token amount as it is not known - // until after the swap. - struct DepositData { - // Token received on destination chain. - address outputToken; - // Amount of output token to be received by recipient. - uint256 outputAmount; - // The account credited with deposit who can submit speedups to the Across deposit. - address depositor; - // The account that will receive the output token on the destination chain. If the output token is - // wrapped native token, then if this is an EOA then they will receive native token on the destination - // chain and if this is a contract then they will receive an ERC20. - address recipient; - // The destination chain identifier. - uint256 destinationChainid; - // The account that can exclusively fill the deposit before the exclusivity deadline. - address exclusiveRelayer; - // Timestamp of the deposit used by system to charge fees. Must be within short window of time into the past - // relative to this chain's current time or deposit will revert. - uint32 quoteTimestamp; - // The timestamp on the destination chain after which this deposit can no longer be filled. - uint32 fillDeadline; - // The timestamp on the destination chain after which anyone can fill the deposit. - uint32 exclusivityDeadline; - // Data that is forwarded to the recipient if the recipient is a contract. - bytes message; - } - - event SwapBeforeBridge( - address exchange, - address indexed swapToken, - address indexed acrossInputToken, - uint256 swapTokenAmount, - uint256 acrossInputAmount, - address indexed acrossOutputToken, - uint256 acrossOutputAmount - ); - - /**************************************** - * ERRORS * - ****************************************/ - error MinimumExpectedInputAmount(); - error LeftoverSrcTokens(); - error InvalidFunctionSelector(); - - /** - * @notice Construct a new SwapAndBridgeBase contract. - * @param _spokePool Address of the SpokePool contract that we'll submit deposits to. - * @param _exchange Address of the exchange where tokens will be swapped. - * @param _allowedSelectors Function selectors that are allowed to be called on the exchange. - */ - constructor( - V3SpokePoolInterface _spokePool, - WETH9Interface _wrappedNativeToken, - address _exchange, - bytes4[] memory _allowedSelectors - ) { - spokePool = _spokePool; - exchange = _exchange; - wrappedNativeToken = _wrappedNativeToken; - for (uint256 i = 0; i < _allowedSelectors.length; i++) { - allowedSelectors[_allowedSelectors[i]] = true; - } - } - - // This contract supports two variants of swap and bridge, one that allows one token and another that allows the caller to pass them in. - function _swapAndBridge( - bytes calldata routerCalldata, - uint256 swapTokenAmount, - uint256 minExpectedInputTokenAmount, - DepositData calldata depositData, - IERC20 _swapToken, - IERC20 _acrossInputToken - ) internal { - // Note: this check should never be impactful, but is here out of an abundance of caution. - // For example, if the exchange address in the contract is also an ERC20 token that is approved by some - // user on this contract, a malicious actor could call transferFrom to steal the user's tokens. - if (!allowedSelectors[bytes4(routerCalldata)]) revert InvalidFunctionSelector(); - - // Swap and run safety checks. - uint256 srcBalanceBefore = _swapToken.balanceOf(address(this)); - uint256 dstBalanceBefore = _acrossInputToken.balanceOf(address(this)); - - _swapToken.safeIncreaseAllowance(exchange, swapTokenAmount); - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory result) = exchange.call(routerCalldata); - require(success, string(result)); - - _checkSwapOutputAndDeposit( - swapTokenAmount, - srcBalanceBefore, - dstBalanceBefore, - minExpectedInputTokenAmount, - depositData, - _swapToken, - _acrossInputToken - ); - } - - /** - * @notice Check that the swap returned enough tokens to submit an Across deposit with and then submit the deposit. - * @param swapTokenAmount Amount of swapToken to swap for a minimum amount of acrossInputToken. - * @param swapTokenBalanceBefore Balance of swapToken before swap. - * @param inputTokenBalanceBefore Amount of Across input token we held before swap - * @param minExpectedInputTokenAmount Minimum amount of received acrossInputToken that we'll bridge - **/ - function _checkSwapOutputAndDeposit( - uint256 swapTokenAmount, - uint256 swapTokenBalanceBefore, - uint256 inputTokenBalanceBefore, - uint256 minExpectedInputTokenAmount, - DepositData calldata depositData, - IERC20 _swapToken, - IERC20 _acrossInputToken - ) internal { - // Sanity check that we received as many tokens as we require: - uint256 returnAmount = _acrossInputToken.balanceOf(address(this)) - inputTokenBalanceBefore; - // Sanity check that received amount from swap is enough to submit Across deposit with. - if (returnAmount < minExpectedInputTokenAmount) revert MinimumExpectedInputAmount(); - // Sanity check that we don't have any leftover swap tokens that would be locked in this contract (i.e. check - // that we weren't partial filled). - if (swapTokenBalanceBefore - _swapToken.balanceOf(address(this)) != swapTokenAmount) revert LeftoverSrcTokens(); - - emit SwapBeforeBridge( - exchange, - address(_swapToken), - address(_acrossInputToken), - swapTokenAmount, - returnAmount, - depositData.outputToken, - depositData.outputAmount - ); - // Deposit the swapped tokens into Across and bridge them using remainder of input params. - _depositV3(_acrossInputToken, returnAmount, depositData); - } - - /** - * @notice Approves the spoke pool and calls `depositV3` function with the specified input parameters. - * @param _acrossInputToken Token to deposit into the spoke pool. - * @param _acrossInputAmount Amount of the input token to deposit into the spoke pool. - * @param depositData Specifies the Across deposit params to use. - */ - function _depositV3( - IERC20 _acrossInputToken, - uint256 _acrossInputAmount, - DepositData calldata depositData - ) internal { - _acrossInputToken.safeIncreaseAllowance(address(spokePool), _acrossInputAmount); - spokePool.depositV3( - depositData.depositor, - depositData.recipient, - address(_acrossInputToken), // input token - depositData.outputToken, // output token - _acrossInputAmount, // input amount. - depositData.outputAmount, // output amount - depositData.destinationChainid, - depositData.exclusiveRelayer, - depositData.quoteTimestamp, - depositData.fillDeadline, - depositData.exclusivityDeadline, - depositData.message - ); - } -} - -/** - * @title SwapAndBridge - * @notice Allows caller to swap between two pre-specified tokens on a chain before bridging the received token - * via Across atomically. Provides safety checks post-swap and before-deposit. - * @dev This variant primarily exists - */ -contract SwapAndBridge is SwapAndBridgeBase { - using SafeERC20 for IERC20; - - // This contract simply enables the caller to swap a token on this chain for another specified one - // and bridge it as the input token via Across. This simplification is made to make the code - // easier to reason about and solve a specific use case for Across. - IERC20 public immutable SWAP_TOKEN; - - // The token that will be bridged via Across as the inputToken. - IERC20 public immutable ACROSS_INPUT_TOKEN; - - /** - * @notice Construct a new SwapAndBridge contract. - * @param _spokePool Address of the SpokePool contract that we'll submit deposits to. - * @param _exchange Address of the exchange where tokens will be swapped. - * @param _allowedSelectors Function selectors that are allowed to be called on the exchange. - * @param _swapToken Address of the token that will be swapped for acrossInputToken. Cannot be 0x0 - * @param _acrossInputToken Address of the token that will be bridged via Across as the inputToken. - */ - constructor( - V3SpokePoolInterface _spokePool, - WETH9Interface _wrappedNativeToken, - address _exchange, - bytes4[] memory _allowedSelectors, - IERC20 _swapToken, - IERC20 _acrossInputToken - ) SwapAndBridgeBase(_spokePool, _wrappedNativeToken, _exchange, _allowedSelectors) { - SWAP_TOKEN = _swapToken; - ACROSS_INPUT_TOKEN = _acrossInputToken; - } - - /** - * @notice Swaps tokens on this chain via specified router before submitting Across deposit atomically. - * Caller can specify their slippage tolerance for the swap and Across deposit params. - * @dev If swapToken or acrossInputToken are the native token for this chain then this function might fail. - * the assumption is that this function will handle only ERC20 tokens. - * @param routerCalldata ABI encoded function data to call on router. Should form a swap of swapToken for - * enough of acrossInputToken, otherwise this function will revert. - * @param swapTokenAmount Amount of swapToken to swap for a minimum amount of depositData.inputToken. - * @param minExpectedInputTokenAmount Minimum amount of received depositData.inputToken that we'll submit bridge - * deposit with. - * @param depositData Specifies the Across deposit params we'll send after the swap. - */ - function swapAndBridge( - bytes calldata routerCalldata, - uint256 swapTokenAmount, - uint256 minExpectedInputTokenAmount, - DepositData calldata depositData - ) external nonReentrant { - _swapAndBridge( - routerCalldata, - swapTokenAmount, - minExpectedInputTokenAmount, - depositData, - SWAP_TOKEN, - ACROSS_INPUT_TOKEN - ); - } -} - -/** - * @title UniversalSwapAndBridge - * @notice Allows caller to swap between any two tokens specified at runtime on a chain before - * bridging the received token via Across atomically. Provides safety checks post-swap and before-deposit. - */ -contract UniversalSwapAndBridge is SwapAndBridgeBase { - using SafeERC20 for IERC20; - - error InsufficientSwapValue(); - error InvalidSwapToken(); - - /** - * @notice Construct a new SwapAndBridgeBase contract. - * @param _spokePool Address of the SpokePool contract that we'll submit deposits to. - * @param _exchange Address of the exchange where tokens will be swapped. - * @param _allowedSelectors Function selectors that are allowed to be called on the exchange. - */ - constructor( - V3SpokePoolInterface _spokePool, - WETH9Interface _wrappedNativeToken, - address _exchange, - bytes4[] memory _allowedSelectors - ) SwapAndBridgeBase(_spokePool, _wrappedNativeToken, _exchange, _allowedSelectors) {} - - /** - * @notice Swaps tokens on this chain via specified router before submitting Across deposit atomically. - * Caller can specify their slippage tolerance for the swap and Across deposit params. - * @dev If swapToken or acrossInputToken are the native token for this chain then this function might fail. - * the assumption is that this function will handle only ERC20 tokens. - * @param swapToken Address of the token that will be swapped for acrossInputToken. - * @param acrossInputToken Address of the token that will be bridged via Across as the inputToken. - * @param routerCalldata ABI encoded function data to call on router. Should form a swap of swapToken for - * enough of acrossInputToken, otherwise this function will revert. - * @param swapTokenAmount Amount of swapToken to swap for a minimum amount of depositData.inputToken. - * @param minExpectedInputTokenAmount Minimum amount of received depositData.inputToken that we'll submit bridge - * deposit with. - * @param depositData Specifies the Across deposit params we'll send after the swap. - */ - function swapAndBridge( - IERC20 swapToken, - IERC20 acrossInputToken, - bytes calldata routerCalldata, - uint256 swapTokenAmount, - uint256 minExpectedInputTokenAmount, - DepositData calldata depositData - ) external payable nonReentrant { - // If a user performs a swapAndBridge with the swap token as the native token, wrap the value and treat the rest of transaction - // as though the user deposited a wrapped native token. - if (msg.value != 0) { - if (msg.value != swapTokenAmount) revert InsufficientSwapValue(); - if (address(swapToken) != address(wrappedNativeToken)) revert InvalidSwapToken(); - wrappedNativeToken.deposit{ value: msg.value }(); - } else { - swapToken.safeTransferFrom(msg.sender, address(this), swapTokenAmount); - } - _swapAndBridge( - routerCalldata, - swapTokenAmount, - minExpectedInputTokenAmount, - depositData, - swapToken, - acrossInputToken - ); - } - - /** - * @notice Swaps an EIP-2612 token on this chain via specified router before submitting Across deposit atomically. - * Caller can specify their slippage tolerance for the swap and Across deposit params. - * @dev If swapToken does not implement `permit` to the specifications of EIP-2612, this function will fail. - * @param swapToken Address of the token that will be swapped for acrossInputToken. - * @param acrossInputToken Address of the token that will be bridged via Across as the inputToken. - * @param routerCalldata ABI encoded function data to call on router. Should form a swap of swapToken for - * enough of acrossInputToken, otherwise this function will revert. - * @param swapTokenAmount Amount of swapToken to swap for a minimum amount of depositData.inputToken. - * @param minExpectedInputTokenAmount Minimum amount of received depositData.inputToken that we'll submit bridge - * deposit with. - * @param depositData Specifies the Across deposit params we'll send after the swap. - * @param deadline Deadline before which the permit signature is valid. - * @param v v of the permit signature. - * @param r r of the permit signature. - * @param s s of the permit signature. - */ - function swapAndBridgeWithPermit( - IERC20Permit swapToken, - IERC20 acrossInputToken, - bytes calldata routerCalldata, - uint256 swapTokenAmount, - uint256 minExpectedInputTokenAmount, - DepositData calldata depositData, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external nonReentrant { - IERC20 _swapToken = IERC20(address(swapToken)); // Cast IERC20Permit to IERC20. - // For permit transactions, we wrap the call in a try/catch block so that the transaction will continue even if the call to - // permit fails. For example, this may be useful if the permit signature, which can be redeemed by anyone, is executed by somebody - // other than this contract. - try swapToken.permit(msg.sender, address(this), swapTokenAmount, deadline, v, r, s) {} catch {} - - _swapToken.safeTransferFrom(msg.sender, address(this), swapTokenAmount); - _swapAndBridge( - routerCalldata, - swapTokenAmount, - minExpectedInputTokenAmount, - depositData, - _swapToken, - acrossInputToken - ); - } - - /** - * @notice Swaps an EIP-3009 token on this chain via specified router before submitting Across deposit atomically. - * Caller can specify their slippage tolerance for the swap and Across deposit params. - * @dev If swapToken does not implement `receiveWithAuthorization` to the specifications of EIP-3009, this call will revert. - * @param swapToken Address of the token that will be swapped for acrossInputToken. - * @param acrossInputToken Address of the token that will be bridged via Across as the inputToken. - * @param routerCalldata ABI encoded function data to call on router. Should form a swap of swapToken for - * enough of acrossInputToken, otherwise this function will revert. - * @param swapTokenAmount Amount of swapToken to swap for a minimum amount of depositData.inputToken. - * @param minExpectedInputTokenAmount Minimum amount of received depositData.inputToken that we'll submit bridge - * deposit with. - * @param depositData Specifies the Across deposit params we'll send after the swap. - * @param validAfter The unix time after which the `receiveWithAuthorization` signature is valid. - * @param validBefore The unix time before which the `receiveWithAuthorization` signature is valid. - * @param nonce Unique nonce used in the `receiveWithAuthorization` signature. - * @param v v of the EIP-3009 signature. - * @param r r of the EIP-3009 signature. - * @param s s of the EIP-3009 signature. - */ - function swapAndBridgeWithAuthorization( - IERC20Auth swapToken, - IERC20 acrossInputToken, - bytes calldata routerCalldata, - uint256 swapTokenAmount, - uint256 minExpectedInputTokenAmount, - DepositData calldata depositData, - uint256 validAfter, - uint256 validBefore, - bytes32 nonce, - uint8 v, - bytes32 r, - bytes32 s - ) external nonReentrant { - // While any contract can vacuously implement `transferWithAuthorization` (or just have a fallback), - // if tokens were not sent to this contract, by this call to the swapToken, the call to `transferFrom` - // in _swapAndBridge will revert. - swapToken.receiveWithAuthorization( - msg.sender, - address(this), - swapTokenAmount, - validAfter, - validBefore, - nonce, - v, - r, - s - ); - IERC20 _swapToken = IERC20(address(swapToken)); // Cast IERC20Auth to IERC20. - - _swapAndBridge( - routerCalldata, - swapTokenAmount, - minExpectedInputTokenAmount, - depositData, - _swapToken, - acrossInputToken - ); - } - - /** - * @notice Deposits an EIP-2612 token Across input token into the Spoke Pool contract. - * @dev If `acrossInputToken` does not implement `permit` to the specifications of EIP-2612, this function will fail. - * @param acrossInputToken EIP-2612 compliant token to deposit. - * @param acrossInputAmount Amount of the input token to deposit. - * @param depositData Specifies the Across deposit params to send. - * @param deadline Deadline before which the permit signature is valid. - * @param v v of the permit signature. - * @param r r of the permit signature. - * @param s s of the permit signature. - */ - function depositWithPermit( - IERC20Permit acrossInputToken, - uint256 acrossInputAmount, - DepositData calldata depositData, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external nonReentrant { - IERC20 _acrossInputToken = IERC20(address(acrossInputToken)); // Cast IERC20Permit to an IERC20 type. - // For permit transactions, we wrap the call in a try/catch block so that the transaction will continue even if the call to - // permit fails. For example, this may be useful if the permit signature, which can be redeemed by anyone, is executed by somebody - // other than this contract. - try acrossInputToken.permit(msg.sender, address(this), acrossInputAmount, deadline, v, r, s) {} catch {} - - _acrossInputToken.safeTransferFrom(msg.sender, address(this), acrossInputAmount); - _depositV3(_acrossInputToken, acrossInputAmount, depositData); - } - - /** - * @notice Deposits an EIP-3009 compliant Across input token into the Spoke Pool contract. - * @dev If `acrossInputToken` does not implement `receiveWithAuthorization` to the specifications of EIP-3009, this call will revert. - * @param acrossInputToken EIP-3009 compliant token to deposit. - * @param acrossInputAmount Amount of the input token to deposit. - * @param depositData Specifies the Across deposit params to send. - * @param validAfter The unix time after which the `receiveWithAuthorization` signature is valid. - * @param validBefore The unix time before which the `receiveWithAuthorization` signature is valid. - * @param nonce Unique nonce used in the `receiveWithAuthorization` signature. - * @param v v of the EIP-3009 signature. - * @param r r of the EIP-3009 signature. - * @param s s of the EIP-3009 signature. - */ - function depositWithAuthorization( - IERC20Auth acrossInputToken, - uint256 acrossInputAmount, - DepositData calldata depositData, - uint256 validAfter, - uint256 validBefore, - bytes32 nonce, - uint8 v, - bytes32 r, - bytes32 s - ) external nonReentrant { - acrossInputToken.receiveWithAuthorization( - msg.sender, - address(this), - acrossInputAmount, - validAfter, - validBefore, - nonce, - v, - r, - s - ); - IERC20 _acrossInputToken = IERC20(address(acrossInputToken)); // Cast the input token to an IERC20. - _depositV3(_acrossInputToken, acrossInputAmount, depositData); - } -} diff --git a/contracts/external/interfaces/IPermit2.sol b/contracts/external/interfaces/IPermit2.sol index c091bf8a6..131d2218d 100644 --- a/contracts/external/interfaces/IPermit2.sol +++ b/contracts/external/interfaces/IPermit2.sol @@ -2,6 +2,19 @@ pragma solidity ^0.8.0; interface IPermit2 { + struct PermitDetails { + address token; + uint160 amount; + uint48 expiration; + uint48 nonce; + } + + struct PermitSingle { + PermitDetails details; + address spender; + uint256 sigDeadline; + } + struct TokenPermissions { address token; uint256 amount; @@ -18,6 +31,12 @@ interface IPermit2 { uint256 requestedAmount; } + function permit( + address owner, + PermitSingle memory permitSingle, + bytes calldata signature + ) external; + function permitWitnessTransferFrom( PermitTransferFrom memory permit, SignatureTransferDetails calldata transferDetails, diff --git a/contracts/interfaces/SpokePoolV3PeripheryInterface.sol b/contracts/interfaces/SpokePoolV3PeripheryInterface.sol new file mode 100644 index 000000000..ec5e031d7 --- /dev/null +++ b/contracts/interfaces/SpokePoolV3PeripheryInterface.sol @@ -0,0 +1,260 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import { IERC20Auth } from "../external/interfaces/IERC20Auth.sol"; +import { SpokePoolV3Periphery } from "../SpokePoolV3Periphery.sol"; +import { PeripherySigningLib } from "../libraries/PeripherySigningLib.sol"; +import { IPermit2 } from "../external/interfaces/IPermit2.sol"; + +interface SpokePoolV3PeripheryProxyInterface { + function swapAndBridge(SpokePoolV3PeripheryInterface.SwapAndDepositData calldata swapAndDepositData) external; +} + +/** + * @title SpokePoolV3Periphery + * @notice Contract for performing more complex interactions with an Across spoke pool deployment. + * @dev Variables which may be immutable are not marked as immutable, nor defined in the constructor, so that this + * contract may be deployed deterministically at the same address across different networks. + * @custom:security-contact bugs@across.to + */ +interface SpokePoolV3PeripheryInterface { + // Enum describing the method of transferring tokens to an exchange. + enum TransferType { + // Approve the exchange so that it may transfer tokens from this contract. + Approval, + // Transfer tokens to the exchange before calling it in this contract. + Transfer, + // Approve the exchange by authorizing a transfer with Permit2. + Permit2Approval + } + + // Submission fees can be set by user to pay whoever submits the transaction in a gasless flow. + // These are assumed to be in the same currency that is input into the contract. + struct Fees { + // Amount of fees to pay recipient for submitting transaction. + uint256 amount; + // Recipient of fees amount. + address recipient; + } + + // Params we'll need caller to pass in to specify an Across Deposit. The input token will be swapped into first + // before submitting a bridge deposit, which is why we don't include the input token amount as it is not known + // until after the swap. + struct BaseDepositData { + // Token deposited on origin chain. + address inputToken; + // Token received on destination chain. + address outputToken; + // Amount of output token to be received by recipient. + uint256 outputAmount; + // The account credited with deposit who can submit speedups to the Across deposit. + address depositor; + // The account that will receive the output token on the destination chain. If the output token is + // wrapped native token, then if this is an EOA then they will receive native token on the destination + // chain and if this is a contract then they will receive an ERC20. + address recipient; + // The destination chain identifier. + uint256 destinationChainId; + // The account that can exclusively fill the deposit before the exclusivity parameter. + address exclusiveRelayer; + // Timestamp of the deposit used by system to charge fees. Must be within short window of time into the past + // relative to this chain's current time or deposit will revert. + uint32 quoteTimestamp; + // The timestamp on the destination chain after which this deposit can no longer be filled. + uint32 fillDeadline; + // The timestamp or offset on the destination chain after which anyone can fill the deposit. A detailed description on + // how the parameter is interpreted by the V3 spoke pool can be found at https://github.com/across-protocol/contracts/blob/fa67f5e97eabade68c67127f2261c2d44d9b007e/contracts/SpokePool.sol#L476 + uint32 exclusivityParameter; + // Data that is forwarded to the recipient if the recipient is a contract. + bytes message; + } + + // Minimum amount of parameters needed to perform a swap on an exchange specified. We include information beyond just the router calldata + // and exchange address so that we may ensure that the swap was performed properly. + struct SwapAndDepositData { + // Amount of fees to pay for submitting transaction. Unused in gasful flows. + Fees submissionFees; + // Deposit data to use when interacting with the Across spoke pool. + BaseDepositData depositData; + // Token to swap. + address swapToken; + // Address of the exchange to use in the swap. + address exchange; + // Method of transferring tokens to the exchange. + TransferType transferType; + // Amount of the token to swap on the exchange. + uint256 swapTokenAmount; + // Minimum output amount of the exchange, and, by extension, the minimum required amount to deposit into an Across spoke pool. + uint256 minExpectedInputTokenAmount; + // The calldata to use when calling the exchange. + bytes routerCalldata; + } + + // Extended deposit data to be used specifically for signing off on periphery deposits. + struct DepositData { + // Amount of fees to pay for submitting transaction. Unused in gasful flows. + Fees submissionFees; + // Deposit data describing the parameters for the V3 Across deposit. + BaseDepositData baseDepositData; + // The precise input amount to deposit into the spoke pool. + uint256 inputAmount; + } + + /** + * @notice Passthrough function to `depositV3()` on the SpokePool contract. + * @dev Protects the caller from losing their ETH (or other native token) by reverting if the SpokePool address + * they intended to call does not exist on this chain. Because this contract can be deployed at the same address + * everywhere callers should be protected even if the transaction is submitted to an unintended network. + * This contract should only be used for native token deposits, as this problem only exists for native tokens. + * @param recipient Address to receive funds at on destination chain. + * @param inputToken Token to lock into this contract to initiate deposit. + * @param inputAmount Amount of tokens to deposit. + * @param outputAmount Amount of tokens to receive on destination chain. + * @param destinationChainId Denotes network where user will receive funds from SpokePool by a relayer. + * @param quoteTimestamp Timestamp used by relayers to compute this deposit's realizedLPFeePct which is paid + * to LP pool on HubPool. + * @param message Arbitrary data that can be used to pass additional information to the recipient along with the tokens. + * Note: this is intended to be used to pass along instructions for how a contract should use or allocate the tokens. + * @param exclusiveRelayer Address of the relayer who has exclusive rights to fill this deposit. Can be set to + * 0x0 if no period is desired. If so, then must set exclusivityParameter to 0. + * @param exclusivityParameter Timestamp or offset, after which any relayer can fill this deposit. Must set + * to 0 if exclusiveRelayer is set to 0x0, and vice versa. + * @param fillDeadline Timestamp after which this deposit can no longer be filled. + */ + function deposit( + address recipient, + address inputToken, + uint256 inputAmount, + uint256 outputAmount, + uint256 destinationChainId, + address exclusiveRelayer, + uint32 quoteTimestamp, + uint32 fillDeadline, + uint32 exclusivityParameter, + bytes memory message + ) external payable; + + /** + * @notice Swaps tokens on this chain via specified router before submitting Across deposit atomically. + * Caller can specify their slippage tolerance for the swap and Across deposit params. + * @dev If msg.value is 0, then this function is only callable by the proxy contract, to protect against + * approval abuse attacks where a user has set an approval on this contract to spend any ERC20 token. + * @dev If swapToken or acrossInputToken are the native token for this chain then this function might fail. + * the assumption is that this function will handle only ERC20 tokens. + * @param swapAndDepositData Specifies the data needed to perform a swap on a generic exchange. + */ + function swapAndBridge(SwapAndDepositData calldata swapAndDepositData) external payable; + + /** + * @notice Swaps an EIP-2612 token on this chain via specified router before submitting Across deposit atomically. + * Caller can specify their slippage tolerance for the swap and Across deposit params. + * @dev If the swapToken in swapData does not implement `permit` to the specifications of EIP-2612, this function will fail. + * @param signatureOwner The owner of the permit signature and swapAndDepositData signature. Assumed to be the depositor for the Across spoke pool. + * @param swapAndDepositData Specifies the params we need to perform a swap on a generic exchange. + * @param deadline Deadline before which the permit signature is valid. + * @param permitSignature Permit signature encoded as (bytes32 r, bytes32 s, uint8 v). + * @param swapAndDepositDataSignature The signature against the input swapAndDepositData encoded as (bytes32 r, bytes32 s, uint8 v). + */ + function swapAndBridgeWithPermit( + address signatureOwner, + SwapAndDepositData calldata swapAndDepositData, + uint256 deadline, + bytes calldata permitSignature, + bytes calldata swapAndDepositDataSignature + ) external; + + /** + * @notice Uses permit2 to transfer tokens from a user before swapping a token on this chain via specified router and submitting an Across deposit atomically. + * Caller can specify their slippage tolerance for the swap and Across deposit params. + * @dev This function assumes the caller has properly set an allowance for the permit2 contract on this network. + * @dev This function assumes that the amount of token to be swapped is equal to the amount of the token to be received from permit2. + * @param signatureOwner The owner of the permit2 signature and depositor for the Across spoke pool. + * @param swapAndDepositData Specifies the params we need to perform a swap on a generic exchange. + * @param permit The permit data signed over by the owner. + * @param signature The permit2 signature to verify against the deposit data. + */ + function swapAndBridgeWithPermit2( + address signatureOwner, + SwapAndDepositData calldata swapAndDepositData, + IPermit2.PermitTransferFrom calldata permit, + bytes calldata signature + ) external; + + /** + * @notice Swaps an EIP-3009 token on this chain via specified router before submitting Across deposit atomically. + * Caller can specify their slippage tolerance for the swap and Across deposit params. + * @dev If swapToken does not implement `receiveWithAuthorization` to the specifications of EIP-3009, this call will revert. + * @param signatureOwner The owner of the EIP3009 signature and swapAndDepositData signature. Assumed to be the depositor for the Across spoke pool. + * @param swapAndDepositData Specifies the params we need to perform a swap on a generic exchange. + * @param validAfter The unix time after which the `receiveWithAuthorization` signature is valid. + * @param validBefore The unix time before which the `receiveWithAuthorization` signature is valid. + * @param nonce Unique nonce used in the `receiveWithAuthorization` signature. + * @param receiveWithAuthSignature EIP3009 signature encoded as (bytes32 r, bytes32 s, uint8 v). + * @param swapAndDepositDataSignature The signature against the input swapAndDepositData encoded as (bytes32 r, bytes32 s, uint8 v). + */ + function swapAndBridgeWithAuthorization( + address signatureOwner, + SwapAndDepositData calldata swapAndDepositData, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + bytes calldata receiveWithAuthSignature, + bytes calldata swapAndDepositDataSignature + ) external; + + /** + * @notice Deposits an EIP-2612 token Across input token into the Spoke Pool contract. + * @dev If `acrossInputToken` does not implement `permit` to the specifications of EIP-2612, this function will fail. + * @param signatureOwner The owner of the permit signature and depositData signature. Assumed to be the depositor for the Across spoke pool. + * @param depositData Specifies the Across deposit params to send. + * @param deadline Deadline before which the permit signature is valid. + * @param permitSignature Permit signature encoded as (bytes32 r, bytes32 s, uint8 v). + * @param depositDataSignature The signature against the input depositData encoded as (bytes32 r, bytes32 s, uint8 v). + */ + function depositWithPermit( + address signatureOwner, + DepositData calldata depositData, + uint256 deadline, + bytes calldata permitSignature, + bytes calldata depositDataSignature + ) external; + + /** + * @notice Uses permit2 to transfer and submit an Across deposit to the Spoke Pool contract. + * @dev This function assumes the caller has properly set an allowance for the permit2 contract on this network. + * @dev This function assumes that the amount of token to be swapped is equal to the amount of the token to be received from permit2. + * @param signatureOwner The owner of the permit2 signature and depositor for the Across spoke pool. + * @param depositData Specifies the Across deposit params we'll send after the swap. + * @param permit The permit data signed over by the owner. + * @param signature The permit2 signature to verify against the deposit data. + */ + function depositWithPermit2( + address signatureOwner, + DepositData calldata depositData, + IPermit2.PermitTransferFrom calldata permit, + bytes calldata signature + ) external; + + /** + * @notice Deposits an EIP-3009 compliant Across input token into the Spoke Pool contract. + * @dev If `acrossInputToken` does not implement `receiveWithAuthorization` to the specifications of EIP-3009, this call will revert. + * @param signatureOwner The owner of the EIP3009 signature and depositData signature. Assumed to be the depositor for the Across spoke pool. + * @param depositData Specifies the Across deposit params to send. + * @param validAfter The unix time after which the `receiveWithAuthorization` signature is valid. + * @param validBefore The unix time before which the `receiveWithAuthorization` signature is valid. + * @param nonce Unique nonce used in the `receiveWithAuthorization` signature. + * @param receiveWithAuthSignature EIP3009 signature encoded as (bytes32 r, bytes32 s, uint8 v). + * @param depositDataSignature The signature against the input depositData encoded as (bytes32 r, bytes32 s, uint8 v). + */ + function depositWithAuthorization( + address signatureOwner, + DepositData calldata depositData, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + bytes calldata receiveWithAuthSignature, + bytes calldata depositDataSignature + ) external; +} diff --git a/contracts/libraries/PeripherySigningLib.sol b/contracts/libraries/PeripherySigningLib.sol new file mode 100644 index 000000000..5956a6de8 --- /dev/null +++ b/contracts/libraries/PeripherySigningLib.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { SpokePoolV3PeripheryInterface } from "../interfaces/SpokePoolV3PeripheryInterface.sol"; + +library PeripherySigningLib { + string internal constant EIP712_FEES_TYPE = "Fees(uint256 amount,address recipient)"; + string internal constant EIP712_BASE_DEPOSIT_DATA_TYPE = + "BaseDepositData(address inputToken,address outputToken,uint256 outputAmount,address depositor,address recipient,uint256 destinationChainId,address exclusiveRelayer,uint32 quoteTimestamp,uint32 fillDeadline,uint32 exclusivityParameter,bytes message)"; + string internal constant EIP712_DEPOSIT_DATA_TYPE = + "DepositData(Fees submissionFees,BaseDepositData baseDepositData,uint256 inputAmount)"; + string internal constant EIP712_SWAP_AND_DEPOSIT_DATA_TYPE = + "SwapAndDepositData(Fees submissionFees,BaseDepositData depositData,address swapToken,address exchange,TransferType transferType,uint256 swapTokenAmount,uint256 minExpectedInputTokenAmount,bytes routerCalldata)"; + + // EIP712 Type hashes. + bytes32 internal constant EIP712_FEES_TYPEHASH = keccak256(abi.encodePacked(EIP712_FEES_TYPE)); + bytes32 internal constant EIP712_BASE_DEPOSIT_DATA_TYPEHASH = + keccak256(abi.encodePacked(EIP712_BASE_DEPOSIT_DATA_TYPE)); + bytes32 internal constant EIP712_DEPOSIT_DATA_TYPEHASH = + keccak256(abi.encodePacked(EIP712_DEPOSIT_DATA_TYPE, EIP712_BASE_DEPOSIT_DATA_TYPE, EIP712_FEES_TYPE)); + bytes32 internal constant EIP712_SWAP_AND_DEPOSIT_DATA_TYPEHASH = + keccak256(abi.encodePacked(EIP712_SWAP_AND_DEPOSIT_DATA_TYPE, EIP712_BASE_DEPOSIT_DATA_TYPE, EIP712_FEES_TYPE)); + + // EIP712 Type strings. + string internal constant TOKEN_PERMISSIONS_TYPE = "TokenPermissions(address token,uint256 amount)"; + string internal constant EIP712_SWAP_AND_DEPOSIT_TYPE_STRING = + string( + abi.encodePacked( + "SwapAndDepositData witness)", + EIP712_BASE_DEPOSIT_DATA_TYPE, + EIP712_FEES_TYPE, + EIP712_SWAP_AND_DEPOSIT_DATA_TYPE, + TOKEN_PERMISSIONS_TYPE + ) + ); + string internal constant EIP712_DEPOSIT_TYPE_STRING = + string( + abi.encodePacked( + "DepositData witness)", + EIP712_BASE_DEPOSIT_DATA_TYPE, + EIP712_DEPOSIT_DATA_TYPE, + EIP712_FEES_TYPE, + TOKEN_PERMISSIONS_TYPE + ) + ); + + error InvalidSignature(); + + /** + * @notice Creates the EIP712 compliant hashed data corresponding to the BaseDepositData struct. + * @param baseDepositData Input struct whose values are hashed. + * @dev BaseDepositData is only used as a nested struct for both DepositData and SwapAndDepositData. + */ + function hashBaseDepositData(SpokePoolV3PeripheryInterface.BaseDepositData calldata baseDepositData) + internal + pure + returns (bytes32) + { + return + keccak256( + abi.encode( + EIP712_BASE_DEPOSIT_DATA_TYPEHASH, + baseDepositData.inputToken, + baseDepositData.outputToken, + baseDepositData.outputAmount, + baseDepositData.depositor, + baseDepositData.recipient, + baseDepositData.destinationChainId, + baseDepositData.exclusiveRelayer, + baseDepositData.quoteTimestamp, + baseDepositData.fillDeadline, + baseDepositData.exclusivityParameter, + keccak256(baseDepositData.message) + ) + ); + } + + /** + * @notice Creates the EIP712 compliant hashed data corresponding to the Fees struct. + * @param fees Input struct whose values are hashed. + * @dev Fees is only used as a nested struct for both DepositData and SwapAndDepositData. + */ + function hashFees(SpokePoolV3PeripheryInterface.Fees calldata fees) internal pure returns (bytes32) { + return keccak256(abi.encode(EIP712_FEES_TYPEHASH, fees.amount, fees.recipient)); + } + + /** + * @notice Creates the EIP712 compliant hashed data corresponding to the DepositData struct. + * @param depositData Input struct whose values are hashed. + */ + function hashDepositData(SpokePoolV3PeripheryInterface.DepositData calldata depositData) + internal + pure + returns (bytes32) + { + return + keccak256( + abi.encode( + EIP712_DEPOSIT_DATA_TYPEHASH, + hashFees(depositData.submissionFees), + hashBaseDepositData(depositData.baseDepositData), + depositData.inputAmount + ) + ); + } + + /** + * @notice Creates the EIP712 compliant hashed data corresponding to the SwapAndDepositData struct. + * @param swapAndDepositData Input struct whose values are hashed. + */ + function hashSwapAndDepositData(SpokePoolV3PeripheryInterface.SwapAndDepositData calldata swapAndDepositData) + internal + pure + returns (bytes32) + { + return + keccak256( + abi.encode( + EIP712_SWAP_AND_DEPOSIT_DATA_TYPEHASH, + hashFees(swapAndDepositData.submissionFees), + hashBaseDepositData(swapAndDepositData.depositData), + swapAndDepositData.swapToken, + swapAndDepositData.exchange, + swapAndDepositData.transferType, + swapAndDepositData.swapTokenAmount, + swapAndDepositData.minExpectedInputTokenAmount, + keccak256(swapAndDepositData.routerCalldata) + ) + ); + } + + /** + * @notice Reads an input bytes, and, assuming it is a signature for a 32-byte hash, returns the v, r, and s values. + * @param _signature The input signature to deserialize. + */ + function deserializeSignature(bytes calldata _signature) + internal + pure + returns ( + bytes32 r, + bytes32 s, + uint8 v + ) + { + if (_signature.length != 65) revert InvalidSignature(); + v = uint8(_signature[64]); + r = bytes32(_signature[0:32]); + s = bytes32(_signature[32:64]); + } +} diff --git a/contracts/test/MockERC20.sol b/contracts/test/MockERC20.sol new file mode 100644 index 000000000..84150eee4 --- /dev/null +++ b/contracts/test/MockERC20.sol @@ -0,0 +1,51 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +import { IERC20Auth } from "../external/interfaces/IERC20Auth.sol"; +import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; + +/** + * @title MockERC20 + * @notice Implements mocked ERC20 contract with various features. + */ +contract MockERC20 is IERC20Auth, ERC20Permit { + bytes32 public constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH = + keccak256( + "ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)" + ); + // Expose the typehash in ERC20Permit. + bytes32 public constant PERMIT_TYPEHASH_EXTERNAL = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + constructor() ERC20Permit("MockERC20") ERC20("MockERC20", "ERC20") {} + + // This does no nonce checking. + function receiveWithAuthorization( + address from, + address to, + uint256 value, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) external { + require(validAfter <= block.timestamp && validBefore >= block.timestamp, "Invalid time bounds"); + require(msg.sender == to, "Receiver not caller"); + bytes memory signature = bytes.concat(r, s, bytes1(v)); + + bytes32 structHash = keccak256( + abi.encode(RECEIVE_WITH_AUTHORIZATION_TYPEHASH, from, to, value, validAfter, validBefore, nonce) + ); + bytes32 sigHash = _hashTypedDataV4(structHash); + require(SignatureChecker.isValidSignatureNow(from, sigHash, signature), "Invalid signature"); + _transfer(from, to, value); + } + + function hashTypedData(bytes32 typedData) external view returns (bytes32) { + return _hashTypedDataV4(typedData); + } +} diff --git a/contracts/test/MockPermit2.sol b/contracts/test/MockPermit2.sol new file mode 100644 index 000000000..aa4ad1b5f --- /dev/null +++ b/contracts/test/MockPermit2.sol @@ -0,0 +1,206 @@ +pragma solidity ^0.8.0; + +import { IPermit2 } from "../external/interfaces/IPermit2.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; + +// Taken from https://github.com/Uniswap/permit2/blob/main/src/EIP712.sol +contract Permit2EIP712 { + bytes32 private immutable _CACHED_DOMAIN_SEPARATOR; + uint256 private immutable _CACHED_CHAIN_ID; + + bytes32 private constant _HASHED_NAME = keccak256("Permit2"); + bytes32 private constant _TYPE_HASH = + keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); + + constructor() { + _CACHED_CHAIN_ID = block.chainid; + _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME); + } + + function DOMAIN_SEPARATOR() public view returns (bytes32) { + return + block.chainid == _CACHED_CHAIN_ID + ? _CACHED_DOMAIN_SEPARATOR + : _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME); + } + + function _buildDomainSeparator(bytes32 typeHash, bytes32 nameHash) private view returns (bytes32) { + return keccak256(abi.encode(typeHash, nameHash, block.chainid, address(this))); + } + + function _hashTypedData(bytes32 dataHash) internal view returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), dataHash)); + } +} + +contract MockPermit2 is IPermit2, Permit2EIP712 { + using SafeERC20 for IERC20; + + mapping(address => mapping(uint256 => uint256)) public nonceBitmap; + mapping(address => mapping(address => mapping(address => uint256))) public allowance; + + bytes32 public constant _TOKEN_PERMISSIONS_TYPEHASH = keccak256("TokenPermissions(address token,uint256 amount)"); + string public constant _PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB = + "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,"; + + error SignatureExpired(); + error InvalidAmount(); + error InvalidNonce(); + error AllowanceExpired(); + error InsufficientAllowance(); + + function permitWitnessTransferFrom( + PermitTransferFrom memory _permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes32 witness, + string calldata witnessTypeString, + bytes calldata signature + ) external override { + _permitTransferFrom( + _permit, + transferDetails, + owner, + hashWithWitness(_permit, witness, witnessTypeString), + signature + ); + } + + function transferFrom( + address from, + address to, + uint160 amount, + address token + ) external { + _transfer(from, to, amount, token); + } + + // This is not a copy of permit2's permit. + function permit( + address owner, + PermitSingle memory permitSingle, + bytes calldata signature + ) external { + if (block.timestamp > permitSingle.sigDeadline) revert SignatureExpired(); + + // Verify the signer address from the signature. + SignatureVerification.verify(signature, _hashTypedData(keccak256(abi.encode(permitSingle))), owner); + + allowance[owner][permitSingle.details.token][permitSingle.spender] = permitSingle.details.amount; + } + + // This is not a copy of permit2's permit. + function _transfer( + address from, + address to, + uint160 amount, + address token + ) private { + uint256 allowed = allowance[from][token][msg.sender]; + + if (allowed != type(uint160).max) { + if (amount > allowed) { + revert InsufficientAllowance(); + } else { + unchecked { + allowance[from][token][msg.sender] = uint160(allowed) - amount; + } + } + } + + // Transfer the tokens from the from address to the recipient. + IERC20(token).safeTransferFrom(from, to, amount); + } + + function _permitTransferFrom( + PermitTransferFrom memory _permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes32 dataHash, + bytes calldata signature + ) private { + uint256 requestedAmount = transferDetails.requestedAmount; + + if (block.timestamp > _permit.deadline) revert SignatureExpired(); + if (requestedAmount > _permit.permitted.amount) revert InvalidAmount(); + + _useUnorderedNonce(owner, _permit.nonce); + + SignatureVerification.verify(signature, _hashTypedData(dataHash), owner); + + IERC20(_permit.permitted.token).safeTransferFrom(owner, transferDetails.to, requestedAmount); + } + + function bitmapPositions(uint256 nonce) private pure returns (uint256 wordPos, uint256 bitPos) { + wordPos = uint248(nonce >> 8); + bitPos = uint8(nonce); + } + + function _useUnorderedNonce(address from, uint256 nonce) internal { + (uint256 wordPos, uint256 bitPos) = bitmapPositions(nonce); + uint256 bit = 1 << bitPos; + uint256 flipped = nonceBitmap[from][wordPos] ^= bit; + + if (flipped & bit == 0) revert InvalidNonce(); + } + + function hashWithWitness( + PermitTransferFrom memory _permit, + bytes32 witness, + string calldata witnessTypeString + ) internal view returns (bytes32) { + bytes32 typeHash = keccak256(abi.encodePacked(_PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB, witnessTypeString)); + + bytes32 tokenPermissionsHash = _hashTokenPermissions(_permit.permitted); + return + keccak256(abi.encode(typeHash, tokenPermissionsHash, msg.sender, _permit.nonce, _permit.deadline, witness)); + } + + function _hashTokenPermissions(TokenPermissions memory permitted) private pure returns (bytes32) { + return keccak256(abi.encode(_TOKEN_PERMISSIONS_TYPEHASH, permitted)); + } +} + +// Taken from https://github.com/Uniswap/permit2/blob/main/src/libraries/SignatureVerification.sol +library SignatureVerification { + error InvalidSignatureLength(); + error InvalidSignature(); + error InvalidSigner(); + error InvalidContractSignature(); + + bytes32 constant UPPER_BIT_MASK = (0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + + function verify( + bytes calldata signature, + bytes32 hash, + address claimedSigner + ) internal view { + bytes32 r; + bytes32 s; + uint8 v; + + if (claimedSigner.code.length == 0) { + if (signature.length == 65) { + (r, s) = abi.decode(signature, (bytes32, bytes32)); + v = uint8(signature[64]); + } else if (signature.length == 64) { + // EIP-2098 + bytes32 vs; + (r, vs) = abi.decode(signature, (bytes32, bytes32)); + s = vs & UPPER_BIT_MASK; + v = uint8(uint256(vs >> 255)) + 27; + } else { + revert InvalidSignatureLength(); + } + address signer = ecrecover(hash, v, r, s); + if (signer == address(0)) revert InvalidSignature(); + if (signer != claimedSigner) revert InvalidSigner(); + } else { + bytes4 magicValue = IERC1271(claimedSigner).isValidSignature(hash, signature); + if (magicValue != IERC1271.isValidSignature.selector) revert InvalidContractSignature(); + } + } +} diff --git a/test/evm/foundry/local/Create2Factory.t.sol b/test/evm/foundry/local/Create2Factory.t.sol new file mode 100644 index 000000000..70250e38d --- /dev/null +++ b/test/evm/foundry/local/Create2Factory.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { Test } from "forge-std/Test.sol"; + +import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol"; +import { Create2Factory } from "../../../../contracts/Create2Factory.sol"; + +contract InitializedContract { + bool public initialized; + + function initialize(bool _initialized) external { + initialized = _initialized; + } +} + +contract Create2FactoryTest is Test { + Create2Factory create2Factory; + + function setUp() public { + create2Factory = new Create2Factory(); + } + + function testDeterministicDeployNoValue() public { + bytes32 salt = "12345"; + bytes memory creationCode = abi.encodePacked(type(InitializedContract).creationCode); + + address computedAddress = Create2.computeAddress(salt, keccak256(creationCode), address(create2Factory)); + bytes memory initializationData = abi.encodeWithSelector(InitializedContract.initialize.selector, true); + address deployedAddress = create2Factory.deploy(0, salt, creationCode, initializationData); + + assertEq(computedAddress, deployedAddress); + assertTrue(InitializedContract(deployedAddress).initialized()); + } +} diff --git a/test/evm/foundry/local/SpokePoolPeriphery.t.sol b/test/evm/foundry/local/SpokePoolPeriphery.t.sol new file mode 100644 index 000000000..da9af4d74 --- /dev/null +++ b/test/evm/foundry/local/SpokePoolPeriphery.t.sol @@ -0,0 +1,1124 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { Test } from "forge-std/Test.sol"; + +import { SpokePoolVerifier } from "../../../../contracts/SpokePoolVerifier.sol"; +import { SpokePoolV3Periphery, SpokePoolPeripheryProxy } from "../../../../contracts/SpokePoolV3Periphery.sol"; +import { Ethereum_SpokePool } from "../../../../contracts/Ethereum_SpokePool.sol"; +import { V3SpokePoolInterface } from "../../../../contracts/interfaces/V3SpokePoolInterface.sol"; +import { SpokePoolV3PeripheryInterface } from "../../../../contracts/interfaces/SpokePoolV3PeripheryInterface.sol"; +import { WETH9 } from "../../../../contracts/external/WETH9.sol"; +import { WETH9Interface } from "../../../../contracts/external/interfaces/WETH9Interface.sol"; +import { IPermit2 } from "../../../../contracts/external/interfaces/IPermit2.sol"; +import { MockPermit2, Permit2EIP712, SignatureVerification } from "../../../../contracts/test/MockPermit2.sol"; +import { PeripherySigningLib } from "../../../../contracts/libraries/PeripherySigningLib.sol"; +import { MockERC20 } from "../../../../contracts/test/MockERC20.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; + +contract Exchange { + IPermit2 permit2; + + constructor(IPermit2 _permit2) { + permit2 = _permit2; + } + + function swap( + IERC20 tokenIn, + IERC20 tokenOut, + uint256 amountIn, + uint256 amountOutMin, + bool usePermit2 + ) external { + if (tokenIn.balanceOf(address(this)) >= amountIn) { + tokenIn.transfer(address(1), amountIn); + require(tokenOut.transfer(msg.sender, amountOutMin)); + return; + } + // The periphery contract should call the exchange, which should call permit2. Permit2 should call the periphery contract, and + // should allow the exchange to take tokens away from the periphery. + if (usePermit2) { + permit2.transferFrom(msg.sender, address(this), uint160(amountIn), address(tokenIn)); + tokenOut.transfer(msg.sender, amountOutMin); + return; + } + require(tokenIn.transferFrom(msg.sender, address(this), amountIn)); + require(tokenOut.transfer(msg.sender, amountOutMin)); + } +} + +// Utility contract which lets us perform external calls to an internal library. +contract HashUtils { + function hashDepositData(SpokePoolV3PeripheryInterface.DepositData calldata depositData) + external + pure + returns (bytes32) + { + return PeripherySigningLib.hashDepositData(depositData); + } + + function hashSwapAndDepositData(SpokePoolV3Periphery.SwapAndDepositData calldata swapAndDepositData) + external + pure + returns (bytes32) + { + return PeripherySigningLib.hashSwapAndDepositData(swapAndDepositData); + } +} + +contract SpokePoolPeripheryTest is Test { + Ethereum_SpokePool ethereumSpokePool; + HashUtils hashUtils; + SpokePoolV3Periphery spokePoolPeriphery; + SpokePoolPeripheryProxy proxy; + Exchange dex; + Exchange cex; + IPermit2 permit2; + + WETH9Interface mockWETH; + MockERC20 mockERC20; + + address depositor; + address owner; + address recipient; + address relayer; + + uint256 destinationChainId = 10; + uint256 mintAmount = 10**22; + uint256 submissionFeeAmount = 1; + uint256 depositAmount = 5 * (10**18); + uint256 depositAmountWithSubmissionFee = depositAmount + submissionFeeAmount; + uint256 mintAmountWithSubmissionFee = mintAmount + submissionFeeAmount; + uint32 fillDeadlineBuffer = 7200; + uint256 privateKey = 0x12345678910; + + bytes32 domainSeparator; + bytes32 private constant TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + string private constant PERMIT_TRANSFER_TYPE_STUB = + "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,"; + + bytes32 private constant TOKEN_PERMISSIONS_TYPEHASH = + keccak256(abi.encodePacked(PeripherySigningLib.TOKEN_PERMISSIONS_TYPE)); + + function setUp() public { + hashUtils = new HashUtils(); + + mockWETH = WETH9Interface(address(new WETH9())); + mockERC20 = new MockERC20(); + + depositor = vm.addr(privateKey); + owner = vm.addr(2); + recipient = vm.addr(3); + relayer = vm.addr(4); + permit2 = IPermit2(new MockPermit2()); + dex = new Exchange(permit2); + cex = new Exchange(permit2); + + vm.startPrank(owner); + spokePoolPeriphery = new SpokePoolV3Periphery(); + domainSeparator = Permit2EIP712(address(permit2)).DOMAIN_SEPARATOR(); + proxy = new SpokePoolPeripheryProxy(); + proxy.initialize(spokePoolPeriphery); + Ethereum_SpokePool implementation = new Ethereum_SpokePool( + address(mockWETH), + fillDeadlineBuffer, + fillDeadlineBuffer + ); + address spokePoolProxy = address( + new ERC1967Proxy(address(implementation), abi.encodeCall(Ethereum_SpokePool.initialize, (0, owner))) + ); + ethereumSpokePool = Ethereum_SpokePool(payable(spokePoolProxy)); + ethereumSpokePool.setEnableRoute(address(mockWETH), destinationChainId, true); + ethereumSpokePool.setEnableRoute(address(mockERC20), destinationChainId, true); + spokePoolPeriphery.initialize(V3SpokePoolInterface(ethereumSpokePool), mockWETH, address(proxy), permit2); + vm.stopPrank(); + + deal(depositor, mintAmountWithSubmissionFee); + deal(address(mockERC20), depositor, mintAmountWithSubmissionFee, true); + deal(address(mockERC20), address(dex), depositAmount, true); + vm.startPrank(depositor); + mockWETH.deposit{ value: mintAmountWithSubmissionFee }(); + mockERC20.approve(address(proxy), mintAmountWithSubmissionFee); + IERC20(address(mockWETH)).approve(address(proxy), mintAmountWithSubmissionFee); + + // Approve permit2 + IERC20(address(mockWETH)).approve(address(permit2), mintAmountWithSubmissionFee * 10); + vm.stopPrank(); + } + + function testInitializePeriphery() public { + SpokePoolV3Periphery _spokePoolPeriphery = new SpokePoolV3Periphery(); + _spokePoolPeriphery.initialize(V3SpokePoolInterface(ethereumSpokePool), mockWETH, address(proxy), permit2); + assertEq(address(_spokePoolPeriphery.spokePool()), address(ethereumSpokePool)); + assertEq(address(_spokePoolPeriphery.wrappedNativeToken()), address(mockWETH)); + assertEq(address(_spokePoolPeriphery.proxy()), address(proxy)); + assertEq(address(_spokePoolPeriphery.permit2()), address(permit2)); + vm.expectRevert(SpokePoolV3Periphery.ContractInitialized.selector); + _spokePoolPeriphery.initialize(V3SpokePoolInterface(ethereumSpokePool), mockWETH, address(proxy), permit2); + } + + function testInitializeProxy() public { + SpokePoolPeripheryProxy _proxy = new SpokePoolPeripheryProxy(); + _proxy.initialize(spokePoolPeriphery); + assertEq(address(_proxy.spokePoolPeriphery()), address(spokePoolPeriphery)); + vm.expectRevert(SpokePoolPeripheryProxy.ContractInitialized.selector); + _proxy.initialize(spokePoolPeriphery); + } + + /** + * Approval based flows + */ + function testSwapAndBridge() public { + // Should emit expected deposit event + vm.startPrank(depositor); + vm.expectEmit(address(ethereumSpokePool)); + emit V3SpokePoolInterface.V3FundsDeposited( + address(mockERC20), + address(0), + depositAmount, + depositAmount, + destinationChainId, + 0, // depositId + uint32(block.timestamp), + uint32(block.timestamp) + fillDeadlineBuffer, + 0, // exclusivityDeadline + depositor, + depositor, + address(0), // exclusiveRelayer + new bytes(0) + ); + proxy.swapAndBridge( + _defaultSwapAndDepositData( + address(mockWETH), + mintAmount, + 0, + address(0), + dex, + SpokePoolV3PeripheryInterface.TransferType.Approval, + address(mockERC20), + depositAmount, + depositor + ) + ); + vm.stopPrank(); + } + + function testSwapAndBridgePermitTransferType() public { + // Should emit expected deposit event + vm.startPrank(depositor); + vm.expectEmit(address(ethereumSpokePool)); + emit V3SpokePoolInterface.V3FundsDeposited( + address(mockERC20), + address(0), + depositAmount, + depositAmount, + destinationChainId, + 0, // depositId + uint32(block.timestamp), + uint32(block.timestamp) + fillDeadlineBuffer, + 0, // exclusivityDeadline + depositor, + depositor, + address(0), // exclusiveRelayer + new bytes(0) + ); + proxy.swapAndBridge( + _defaultSwapAndDepositData( + address(mockWETH), + mintAmount, + 0, + address(0), + dex, + SpokePoolV3PeripheryInterface.TransferType.Permit2Approval, + address(mockERC20), + depositAmount, + depositor + ) + ); + vm.stopPrank(); + } + + function testSwapAndBridgeTransferTransferType() public { + // Should emit expected deposit event + vm.startPrank(depositor); + vm.expectEmit(address(ethereumSpokePool)); + emit V3SpokePoolInterface.V3FundsDeposited( + address(mockERC20), + address(0), + depositAmount, + depositAmount, + destinationChainId, + 0, // depositId + uint32(block.timestamp), + uint32(block.timestamp) + fillDeadlineBuffer, + 0, // exclusivityDeadline + depositor, + depositor, + address(0), // exclusiveRelayer + new bytes(0) + ); + proxy.swapAndBridge( + _defaultSwapAndDepositData( + address(mockWETH), + mintAmount, + 0, + address(0), + dex, + SpokePoolV3PeripheryInterface.TransferType.Transfer, + address(mockERC20), + depositAmount, + depositor + ) + ); + vm.stopPrank(); + } + + /** + * Value based flows + */ + function testSwapAndBridgeNoValueNoProxy() public { + // Cannot call swapAndBridge with no value directly. + vm.startPrank(depositor); + vm.expectRevert(SpokePoolV3Periphery.NotProxy.selector); + spokePoolPeriphery.swapAndBridge( + _defaultSwapAndDepositData( + address(mockWETH), + mintAmount, + 0, + address(0), + dex, + SpokePoolV3PeripheryInterface.TransferType.Approval, + address(mockERC20), + depositAmount, + depositor + ) + ); + + vm.stopPrank(); + } + + function testSwapAndBridgeWithValue() public { + // Unlike previous test, this one calls the spokePoolPeriphery directly rather than through the proxy + // because there is no approval required to be set on the periphery. + deal(depositor, mintAmount); + + // Should emit expected deposit event + vm.startPrank(depositor); + + vm.expectEmit(address(ethereumSpokePool)); + emit V3SpokePoolInterface.V3FundsDeposited( + address(mockERC20), + address(0), + depositAmount, + depositAmount, + destinationChainId, + 0, // depositId + uint32(block.timestamp), + uint32(block.timestamp) + fillDeadlineBuffer, + 0, // exclusivityDeadline + depositor, + depositor, + address(0), // exclusiveRelayer + new bytes(0) + ); + spokePoolPeriphery.swapAndBridge{ value: mintAmount }( + _defaultSwapAndDepositData( + address(mockWETH), + mintAmount, + 0, + address(0), + dex, + SpokePoolV3PeripheryInterface.TransferType.Approval, + address(mockERC20), + depositAmount, + depositor + ) + ); + vm.stopPrank(); + } + + function testDepositWithValue() public { + // Unlike previous test, this one calls the spokePoolPeriphery directly rather than through the proxy + // because there is no approval required to be set on the periphery. + deal(depositor, mintAmount); + + // Should emit expected deposit event + vm.startPrank(depositor); + vm.expectEmit(address(ethereumSpokePool)); + emit V3SpokePoolInterface.V3FundsDeposited( + address(mockWETH), + address(0), + mintAmount, + mintAmount, + destinationChainId, + 0, // depositId + uint32(block.timestamp), + uint32(block.timestamp) + fillDeadlineBuffer, + 0, // exclusivityDeadline + depositor, + depositor, + address(0), // exclusiveRelayer + new bytes(0) + ); + spokePoolPeriphery.deposit{ value: mintAmount }( + depositor, // recipient + address(mockWETH), // inputToken + mintAmount, + mintAmount, + destinationChainId, + address(0), // exclusiveRelayer + uint32(block.timestamp), + uint32(block.timestamp) + fillDeadlineBuffer, + 0, + new bytes(0) + ); + vm.stopPrank(); + } + + function testDepositNoValueNoProxy() public { + // Cannot call deposit with no value directly. + vm.startPrank(depositor); + vm.expectRevert(SpokePoolV3Periphery.InvalidMsgValue.selector); + spokePoolPeriphery.deposit( + depositor, // recipient + address(mockWETH), // inputToken + mintAmount, + mintAmount, + destinationChainId, + address(0), // exclusiveRelayer + uint32(block.timestamp), + uint32(block.timestamp) + fillDeadlineBuffer, + 0, + new bytes(0) + ); + + vm.stopPrank(); + } + + /** + * Permit (2612) based flows + */ + function testPermitDepositValidWitness() public { + SpokePoolV3PeripheryInterface.DepositData memory depositData = _defaultDepositData( + address(mockERC20), + mintAmount, + submissionFeeAmount, + relayer, + depositor + ); + + bytes32 nonce = 0; + + // Get the permit signature. + bytes32 structHash = keccak256( + abi.encode( + mockERC20.PERMIT_TYPEHASH_EXTERNAL(), + depositor, + address(spokePoolPeriphery), + mintAmountWithSubmissionFee, + nonce, + block.timestamp + ) + ); + bytes32 msgHash = mockERC20.hashTypedData(structHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, msgHash); + bytes memory signature = bytes.concat(r, s, bytes1(v)); + + // Get the deposit data signature. + bytes32 depositMsgHash = keccak256( + abi.encodePacked("\x19\x01", spokePoolPeriphery.domainSeparator(), hashUtils.hashDepositData(depositData)) + ); + (uint8 _v, bytes32 _r, bytes32 _s) = vm.sign(privateKey, depositMsgHash); + bytes memory depositDataSignature = bytes.concat(_r, _s, bytes1(_v)); + + // Should emit expected deposit event + vm.expectEmit(address(ethereumSpokePool)); + emit V3SpokePoolInterface.V3FundsDeposited( + address(mockERC20), + address(0), + mintAmount, + mintAmount, + destinationChainId, + 0, // depositId + uint32(block.timestamp), + uint32(block.timestamp) + fillDeadlineBuffer, + 0, // exclusivityDeadline + depositor, + depositor, + address(0), // exclusiveRelayer + new bytes(0) + ); + spokePoolPeriphery.depositWithPermit( + depositor, // signatureOwner + depositData, + block.timestamp, // deadline + signature, // permitSignature + depositDataSignature + ); + + // Check that fee recipient receives expected amount + assertEq(mockERC20.balanceOf(relayer), submissionFeeAmount); + } + + function testPermitSwapAndBridgeValidWitness() public { + // We need to deal the exchange some WETH in this test since we swap a permit ERC20 to WETH. + mockWETH.deposit{ value: depositAmount }(); + mockWETH.transfer(address(dex), depositAmount); + + SpokePoolV3PeripheryInterface.SwapAndDepositData memory swapAndDepositData = _defaultSwapAndDepositData( + address(mockERC20), + mintAmount, + submissionFeeAmount, + relayer, + dex, + SpokePoolV3PeripheryInterface.TransferType.Permit2Approval, + address(mockWETH), + depositAmount, + depositor + ); + + bytes32 nonce = 0; + + // Get the permit signature. + bytes32 structHash = keccak256( + abi.encode( + mockERC20.PERMIT_TYPEHASH_EXTERNAL(), + depositor, + address(spokePoolPeriphery), + mintAmountWithSubmissionFee, + nonce, + block.timestamp + ) + ); + bytes32 msgHash = mockERC20.hashTypedData(structHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, msgHash); + bytes memory signature = bytes.concat(r, s, bytes1(v)); + + // Get the swap and deposit data signature. + bytes32 swapAndDepositMsgHash = keccak256( + abi.encodePacked( + "\x19\x01", + spokePoolPeriphery.domainSeparator(), + hashUtils.hashSwapAndDepositData(swapAndDepositData) + ) + ); + (uint8 _v, bytes32 _r, bytes32 _s) = vm.sign(privateKey, swapAndDepositMsgHash); + bytes memory swapAndDepositDataSignature = bytes.concat(_r, _s, bytes1(_v)); + + // Should emit expected deposit event + vm.expectEmit(address(ethereumSpokePool)); + emit V3SpokePoolInterface.V3FundsDeposited( + address(mockWETH), + address(0), + depositAmount, + depositAmount, + destinationChainId, + 0, // depositId + uint32(block.timestamp), + uint32(block.timestamp) + fillDeadlineBuffer, + 0, // exclusivityDeadline + depositor, + depositor, + address(0), // exclusiveRelayer + new bytes(0) + ); + spokePoolPeriphery.swapAndBridgeWithPermit( + depositor, // signatureOwner + swapAndDepositData, + block.timestamp, // deadline + signature, // permitSignature + swapAndDepositDataSignature + ); + + // Check that fee recipient receives expected amount + assertEq(mockERC20.balanceOf(relayer), submissionFeeAmount); + } + + function testPermitSwapAndBridgeInvalidWitness(address rando) public { + vm.assume(rando != depositor); + // We need to deal the exchange some WETH in this test since we swap a permit ERC20 to WETH. + mockWETH.deposit{ value: depositAmount }(); + mockWETH.transfer(address(dex), depositAmount); + + SpokePoolV3PeripheryInterface.SwapAndDepositData memory swapAndDepositData = _defaultSwapAndDepositData( + address(mockERC20), + mintAmount, + submissionFeeAmount, + relayer, + dex, + SpokePoolV3PeripheryInterface.TransferType.Permit2Approval, + address(mockWETH), + depositAmount, + depositor + ); + + bytes32 nonce = 0; + + // Get the permit signature. + bytes32 structHash = keccak256( + abi.encode( + mockERC20.PERMIT_TYPEHASH_EXTERNAL(), + depositor, + address(spokePoolPeriphery), + mintAmountWithSubmissionFee, + nonce, + block.timestamp + ) + ); + bytes32 msgHash = mockERC20.hashTypedData(structHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, msgHash); + bytes memory signature = bytes.concat(r, s, bytes1(v)); + + // Get the swap and deposit data signature. + bytes32 swapAndDepositMsgHash = keccak256( + abi.encodePacked( + "\x19\x01", + spokePoolPeriphery.domainSeparator(), + hashUtils.hashSwapAndDepositData(swapAndDepositData) + ) + ); + (uint8 _v, bytes32 _r, bytes32 _s) = vm.sign(privateKey, swapAndDepositMsgHash); + bytes memory swapAndDepositDataSignature = bytes.concat(_r, _s, bytes1(_v)); + + // Make a swapAndDepositStruct which is different from the one the depositor signed off on. For example, make one where we set somebody else as the recipient/depositor. + SpokePoolV3PeripheryInterface.SwapAndDepositData memory invalidSwapAndDepositData = _defaultSwapAndDepositData( + address(mockERC20), + mintAmount, + submissionFeeAmount, + relayer, + dex, + SpokePoolV3PeripheryInterface.TransferType.Permit2Approval, + address(mockWETH), + depositAmount, + rando + ); + + // Should emit expected deposit event + vm.expectRevert(SpokePoolV3Periphery.InvalidSignature.selector); + spokePoolPeriphery.swapAndBridgeWithPermit( + depositor, // signatureOwner + invalidSwapAndDepositData, + block.timestamp, // deadline + signature, // permitSignature + swapAndDepositDataSignature + ); + } + + /** + * Transfer with authorization based flows + */ + function testTransferWithAuthDepositValidWitness() public { + SpokePoolV3PeripheryInterface.DepositData memory depositData = _defaultDepositData( + address(mockERC20), + mintAmount, + submissionFeeAmount, + relayer, + depositor + ); + + bytes32 nonce = bytes32(block.prevrandao); + + // Get the transfer with auth signature. + bytes32 structHash = keccak256( + abi.encode( + mockERC20.RECEIVE_WITH_AUTHORIZATION_TYPEHASH(), + depositor, + address(spokePoolPeriphery), + mintAmountWithSubmissionFee, + block.timestamp, + block.timestamp, + nonce + ) + ); + bytes32 msgHash = mockERC20.hashTypedData(structHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, msgHash); + bytes memory signature = bytes.concat(r, s, bytes1(v)); + + // Get the deposit data signature. + bytes32 depositMsgHash = keccak256( + abi.encodePacked("\x19\x01", spokePoolPeriphery.domainSeparator(), hashUtils.hashDepositData(depositData)) + ); + (uint8 _v, bytes32 _r, bytes32 _s) = vm.sign(privateKey, depositMsgHash); + bytes memory depositDataSignature = bytes.concat(_r, _s, bytes1(_v)); + + // Should emit expected deposit event + vm.expectEmit(address(ethereumSpokePool)); + emit V3SpokePoolInterface.V3FundsDeposited( + address(mockERC20), + address(0), + mintAmount, + mintAmount, + destinationChainId, + 0, // depositId + uint32(block.timestamp), + uint32(block.timestamp) + fillDeadlineBuffer, + 0, // exclusivityDeadline + depositor, + depositor, + address(0), // exclusiveRelayer + new bytes(0) + ); + spokePoolPeriphery.depositWithAuthorization( + depositor, // signatureOwner + depositData, + block.timestamp, // valid before + block.timestamp, // valid after + nonce, // nonce + signature, // receiveWithAuthSignature + depositDataSignature + ); + + // Check that fee recipient receives expected amount + assertEq(mockERC20.balanceOf(relayer), submissionFeeAmount); + } + + function testTransferWithAuthSwapAndBridgeValidWitness() public { + // We need to deal the exchange some WETH in this test since we swap a eip3009 ERC20 to WETH. + mockWETH.deposit{ value: depositAmount }(); + mockWETH.transfer(address(dex), depositAmount); + + SpokePoolV3PeripheryInterface.SwapAndDepositData memory swapAndDepositData = _defaultSwapAndDepositData( + address(mockERC20), + mintAmount, + submissionFeeAmount, + relayer, + dex, + SpokePoolV3PeripheryInterface.TransferType.Permit2Approval, + address(mockWETH), + depositAmount, + depositor + ); + + bytes32 nonce = bytes32(block.prevrandao); + + // Get the transfer with auth signature. + bytes32 structHash = keccak256( + abi.encode( + mockERC20.RECEIVE_WITH_AUTHORIZATION_TYPEHASH(), + depositor, + address(spokePoolPeriphery), + mintAmountWithSubmissionFee, + block.timestamp, + block.timestamp, + nonce + ) + ); + bytes32 msgHash = mockERC20.hashTypedData(structHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, msgHash); + bytes memory signature = bytes.concat(r, s, bytes1(v)); + + // Get the swap and deposit data signature. + bytes32 swapAndDepositMsgHash = keccak256( + abi.encodePacked( + "\x19\x01", + spokePoolPeriphery.domainSeparator(), + hashUtils.hashSwapAndDepositData(swapAndDepositData) + ) + ); + (uint8 _v, bytes32 _r, bytes32 _s) = vm.sign(privateKey, swapAndDepositMsgHash); + bytes memory swapAndDepositDataSignature = bytes.concat(_r, _s, bytes1(_v)); + + // Should emit expected deposit event + vm.expectEmit(address(ethereumSpokePool)); + emit V3SpokePoolInterface.V3FundsDeposited( + address(mockWETH), + address(0), + depositAmount, + depositAmount, + destinationChainId, + 0, // depositId + uint32(block.timestamp), + uint32(block.timestamp) + fillDeadlineBuffer, + 0, // exclusivityDeadline + depositor, + depositor, + address(0), // exclusiveRelayer + new bytes(0) + ); + spokePoolPeriphery.swapAndBridgeWithAuthorization( + depositor, // signatureOwner + swapAndDepositData, + block.timestamp, // validAfter + block.timestamp, // validBefore + nonce, // nonce + signature, // receiveWithAuthSignature + swapAndDepositDataSignature + ); + + // Check that fee recipient receives expected amount + assertEq(mockERC20.balanceOf(relayer), submissionFeeAmount); + } + + function testTransferWithAuthSwapAndBridgeInvalidWitness(address rando) public { + vm.assume(rando != depositor); + // We need to deal the exchange some WETH in this test since we swap a eip3009 ERC20 to WETH. + mockWETH.deposit{ value: depositAmount }(); + mockWETH.transfer(address(dex), depositAmount); + + SpokePoolV3PeripheryInterface.SwapAndDepositData memory swapAndDepositData = _defaultSwapAndDepositData( + address(mockERC20), + mintAmount, + submissionFeeAmount, + relayer, + dex, + SpokePoolV3PeripheryInterface.TransferType.Permit2Approval, + address(mockWETH), + depositAmount, + depositor + ); + + bytes32 nonce = bytes32(block.prevrandao); + + // Get the transfer with auth signature. + bytes32 structHash = keccak256( + abi.encode( + mockERC20.RECEIVE_WITH_AUTHORIZATION_TYPEHASH(), + depositor, + address(spokePoolPeriphery), + mintAmountWithSubmissionFee, + block.timestamp, + block.timestamp, + nonce + ) + ); + bytes32 msgHash = mockERC20.hashTypedData(structHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, msgHash); + bytes memory signature = bytes.concat(r, s, bytes1(v)); + + // Get the swap and deposit data signature. + bytes32 swapAndDepositMsgHash = keccak256( + abi.encodePacked( + "\x19\x01", + spokePoolPeriphery.domainSeparator(), + hashUtils.hashSwapAndDepositData(swapAndDepositData) + ) + ); + (uint8 _v, bytes32 _r, bytes32 _s) = vm.sign(privateKey, swapAndDepositMsgHash); + bytes memory swapAndDepositDataSignature = bytes.concat(_r, _s, bytes1(_v)); + + // Make a swapAndDepositStruct which is different from the one the depositor signed off on. For example, make one where we set somebody else as the recipient/depositor. + SpokePoolV3PeripheryInterface.SwapAndDepositData memory invalidSwapAndDepositData = _defaultSwapAndDepositData( + address(mockERC20), + mintAmount, + submissionFeeAmount, + relayer, + dex, + SpokePoolV3PeripheryInterface.TransferType.Permit2Approval, + address(mockWETH), + depositAmount, + rando + ); + + // Should emit expected deposit event + vm.expectRevert(SpokePoolV3Periphery.InvalidSignature.selector); + spokePoolPeriphery.swapAndBridgeWithAuthorization( + depositor, // signatureOwner + invalidSwapAndDepositData, + block.timestamp, // validAfter + block.timestamp, // validBefore + nonce, // nonce + signature, // receiveWithAuthSignature + swapAndDepositDataSignature + ); + } + + /** + * Permit2 based flows + */ + function testPermit2DepositValidWitness() public { + SpokePoolV3PeripheryInterface.DepositData memory depositData = _defaultDepositData( + address(mockWETH), + mintAmount, + submissionFeeAmount, + relayer, + depositor + ); + // Signature transfer details + IPermit2.PermitTransferFrom memory permit = IPermit2.PermitTransferFrom({ + permitted: IPermit2.TokenPermissions({ token: address(mockWETH), amount: mintAmountWithSubmissionFee }), + nonce: 1, + deadline: block.timestamp + 100 + }); + + bytes32 typehash = keccak256( + abi.encodePacked(PERMIT_TRANSFER_TYPE_STUB, PeripherySigningLib.EIP712_DEPOSIT_TYPE_STRING) + ); + bytes32 tokenPermissions = keccak256(abi.encode(TOKEN_PERMISSIONS_TYPEHASH, permit.permitted)); + + // Get the permit2 signature. + bytes32 msgHash = keccak256( + abi.encodePacked( + "\x19\x01", + domainSeparator, + keccak256( + abi.encode( + typehash, + tokenPermissions, + address(spokePoolPeriphery), + permit.nonce, + permit.deadline, + hashUtils.hashDepositData(depositData) + ) + ) + ) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, msgHash); + bytes memory signature = bytes.concat(r, s, bytes1(v)); + + // Should emit expected deposit event + vm.expectEmit(address(ethereumSpokePool)); + emit V3SpokePoolInterface.V3FundsDeposited( + address(mockWETH), + address(0), + mintAmount, + mintAmount, + destinationChainId, + 0, // depositId + uint32(block.timestamp), + uint32(block.timestamp) + fillDeadlineBuffer, + 0, // exclusivityDeadline + depositor, + depositor, + address(0), // exclusiveRelayer + new bytes(0) + ); + spokePoolPeriphery.depositWithPermit2( + depositor, // signatureOwner + depositData, + permit, // permit + signature // permit2 signature + ); + + // Check that fee recipient receives expected amount + assertEq(mockWETH.balanceOf(relayer), submissionFeeAmount); + } + + function testPermit2SwapAndBridgeValidWitness() public { + SpokePoolV3PeripheryInterface.SwapAndDepositData memory swapAndDepositData = _defaultSwapAndDepositData( + address(mockWETH), + mintAmount, + submissionFeeAmount, + relayer, + dex, + SpokePoolV3PeripheryInterface.TransferType.Transfer, + address(mockERC20), + depositAmount, + depositor + ); + + // Signature transfer details + IPermit2.PermitTransferFrom memory permit = IPermit2.PermitTransferFrom({ + permitted: IPermit2.TokenPermissions({ token: address(mockWETH), amount: mintAmountWithSubmissionFee }), + nonce: 1, + deadline: block.timestamp + 100 + }); + + bytes32 typehash = keccak256( + abi.encodePacked(PERMIT_TRANSFER_TYPE_STUB, PeripherySigningLib.EIP712_SWAP_AND_DEPOSIT_TYPE_STRING) + ); + bytes32 tokenPermissions = keccak256(abi.encode(TOKEN_PERMISSIONS_TYPEHASH, permit.permitted)); + + // Get the permit2 signature. + bytes32 msgHash = keccak256( + abi.encodePacked( + "\x19\x01", + domainSeparator, + keccak256( + abi.encode( + typehash, + tokenPermissions, + address(spokePoolPeriphery), + permit.nonce, + permit.deadline, + hashUtils.hashSwapAndDepositData(swapAndDepositData) + ) + ) + ) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, msgHash); + bytes memory signature = bytes.concat(r, s, bytes1(v)); + + // Should emit expected deposit event + vm.expectEmit(address(ethereumSpokePool)); + emit V3SpokePoolInterface.V3FundsDeposited( + address(mockERC20), + address(0), + depositAmount, + depositAmount, + destinationChainId, + 0, // depositId + uint32(block.timestamp), + uint32(block.timestamp) + fillDeadlineBuffer, + 0, // exclusivityDeadline + depositor, + depositor, + address(0), // exclusiveRelayer + new bytes(0) + ); + spokePoolPeriphery.swapAndBridgeWithPermit2( + depositor, // signatureOwner + swapAndDepositData, + permit, + signature + ); + + // Check that fee recipient receives expected amount + assertEq(mockWETH.balanceOf(relayer), submissionFeeAmount); + } + + function testPermit2SwapAndBridgeInvalidWitness(address rando) public { + vm.assume(rando != depositor); + SpokePoolV3PeripheryInterface.SwapAndDepositData memory swapAndDepositData = _defaultSwapAndDepositData( + address(mockWETH), + mintAmount, + submissionFeeAmount, + relayer, + dex, + SpokePoolV3PeripheryInterface.TransferType.Transfer, + address(mockERC20), + depositAmount, + depositor + ); + + // Signature transfer details + IPermit2.PermitTransferFrom memory permit = IPermit2.PermitTransferFrom({ + permitted: IPermit2.TokenPermissions({ token: address(mockWETH), amount: mintAmountWithSubmissionFee }), + nonce: 1, + deadline: block.timestamp + 100 + }); + + bytes32 typehash = keccak256( + abi.encodePacked(PERMIT_TRANSFER_TYPE_STUB, PeripherySigningLib.EIP712_SWAP_AND_DEPOSIT_TYPE_STRING) + ); + bytes32 tokenPermissions = keccak256(abi.encode(TOKEN_PERMISSIONS_TYPEHASH, permit.permitted)); + + // Get the permit2 signature. + bytes32 msgHash = keccak256( + abi.encodePacked( + "\x19\x01", + domainSeparator, + keccak256( + abi.encode( + typehash, + tokenPermissions, + address(spokePoolPeriphery), + permit.nonce, + permit.deadline, + hashUtils.hashSwapAndDepositData(swapAndDepositData) + ) + ) + ) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, msgHash); + bytes memory signature = bytes.concat(r, s, bytes1(v)); + + // Make a swapAndDepositStruct which is different from the one the depositor signed off on. For example, make one where we set somebody else as the recipient/depositor. + SpokePoolV3PeripheryInterface.SwapAndDepositData memory invalidSwapAndDepositData = _defaultSwapAndDepositData( + address(mockERC20), + mintAmount, + submissionFeeAmount, + relayer, + dex, + SpokePoolV3PeripheryInterface.TransferType.Permit2Approval, + address(mockWETH), + depositAmount, + rando + ); + + // Should emit expected deposit event + vm.expectRevert(SignatureVerification.InvalidSigner.selector); + spokePoolPeriphery.swapAndBridgeWithPermit2( + depositor, // signatureOwner + invalidSwapAndDepositData, + permit, + signature + ); + } + + /** + * Helper functions + */ + function _defaultDepositData( + address _token, + uint256 _amount, + uint256 _feeAmount, + address _feeRecipient, + address _depositor + ) internal view returns (SpokePoolV3Periphery.DepositData memory) { + return + SpokePoolV3PeripheryInterface.DepositData({ + submissionFees: SpokePoolV3PeripheryInterface.Fees({ amount: _feeAmount, recipient: _feeRecipient }), + baseDepositData: SpokePoolV3PeripheryInterface.BaseDepositData({ + inputToken: _token, + outputToken: address(0), + outputAmount: _amount, + depositor: _depositor, + recipient: _depositor, + destinationChainId: destinationChainId, + exclusiveRelayer: address(0), + quoteTimestamp: uint32(block.timestamp), + fillDeadline: uint32(block.timestamp) + fillDeadlineBuffer, + exclusivityParameter: 0, + message: new bytes(0) + }), + inputAmount: _amount + }); + } + + function _defaultSwapAndDepositData( + address _swapToken, + uint256 _swapAmount, + uint256 _feeAmount, + address _feeRecipient, + Exchange _exchange, + SpokePoolV3PeripheryInterface.TransferType _transferType, + address _inputToken, + uint256 _amount, + address _depositor + ) internal view returns (SpokePoolV3Periphery.SwapAndDepositData memory) { + bool usePermit2 = _transferType == SpokePoolV3PeripheryInterface.TransferType.Permit2Approval; + return + SpokePoolV3PeripheryInterface.SwapAndDepositData({ + submissionFees: SpokePoolV3PeripheryInterface.Fees({ amount: _feeAmount, recipient: _feeRecipient }), + depositData: SpokePoolV3PeripheryInterface.BaseDepositData({ + inputToken: _inputToken, + outputToken: address(0), + outputAmount: _amount, + depositor: _depositor, + recipient: _depositor, + destinationChainId: destinationChainId, + exclusiveRelayer: address(0), + quoteTimestamp: uint32(block.timestamp), + fillDeadline: uint32(block.timestamp) + fillDeadlineBuffer, + exclusivityParameter: 0, + message: new bytes(0) + }), + swapToken: _swapToken, + exchange: address(_exchange), + transferType: _transferType, + swapTokenAmount: _swapAmount, // swapTokenAmount + minExpectedInputTokenAmount: _amount, + routerCalldata: abi.encodeWithSelector( + _exchange.swap.selector, + IERC20(_swapToken), + IERC20(_inputToken), + _swapAmount, + _amount, + usePermit2 + ) + }); + } +}