diff --git a/LICENSE b/LICENSE index 8e9122e4d..1aa90cc3d 100644 --- a/LICENSE +++ b/LICENSE @@ -148,7 +148,7 @@ of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. - Each version is given a distinguishing version number. If the + Each version is distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and diff --git a/src/modules/paymentProcessor/PP_Connext_Crosschain_v1.sol b/src/modules/paymentProcessor/PP_Connext_Crosschain_v1.sol index 7aead5e0e..d546e659e 100644 --- a/src/modules/paymentProcessor/PP_Connext_Crosschain_v1.sol +++ b/src/modules/paymentProcessor/PP_Connext_Crosschain_v1.sol @@ -1,12 +1,19 @@ +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.20; -import {CrosschainBase_v1} from - "src/modules/paymentProcessor/abstracts/CrosschainBase_v1.sol"; +// External Imports +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {Module_v1} from "src/modules/base/Module_v1.sol"; + +// Internal Imports +import {CrossChainBase_v1} from + "src/modules/paymentProcessor/abstracts/CrossChainBase_v1.sol"; import {ICrossChainBase_v1} from "src/modules/paymentProcessor/interfaces/ICrosschainBase_v1.sol"; +import {IPP_Connext_Crosschain_v1} from + "src/modules/paymentProcessor/interfaces/IPP_Connext_Crosschain_v1.sol"; import {IERC20PaymentClientBase_v1} from "@lm/interfaces/IERC20PaymentClientBase_v1.sol"; -import {IERC20} from "@oz/token/ERC20/IERC20.sol"; import {PP_Crosschain_v1} from "src/modules/paymentProcessor/abstracts/PP_Crosschain_v1.sol"; import {IWETH} from "src/modules/paymentProcessor/interfaces/IWETH.sol"; @@ -15,103 +22,307 @@ import {IEverclearSpoke} from import {IOrchestrator_v1} from "src/orchestrator/interfaces/IOrchestrator_v1.sol"; +/** + * @title Connext Cross-chain Payment Processor + * + * @notice Specialized payment processor implementation for handling cross-chain payments via Connext protocol. + * + * @dev This contract extends PP_Crosschain_v1 and provides: + * - Integration with Connext's EverClear protocol for secure cross-chain transfers + * - Native token handling through WETH wrapper + * - Robust payment order processing and validation + * - Failed transfer handling with retry and cancellation mechanisms + * - Bridge-specific transfer logic implementation + * - Support for Base network (chainId: 8453) + * - Comprehensive transfer state tracking + * + * @custom:security-contact security@inverter.network + * In case of any concerns or findings, please refer to our Security Policy + * at security.inverter.network or email us directly! + * + * @author Inverter Network + * @custom:version 1.0.0 + * @custom:standard-version 1.0.0 + */ contract PP_Connext_Crosschain_v1 is PP_Crosschain_v1 { + // Storage Variables IEverclearSpoke public everClearSpoke; IWETH public weth; + /// @dev Tracks all details for all payment orders of a paymentReceiver for a specific paymentClient. + /// paymentClient => paymentReceiver => intentId. + mapping( + address paymentClient => mapping(address recipient => bytes32 intentId) + ) public processedIntentId; + + /// @dev Tracks failed transfers that can be retried + /// paymentClient => recipient => intentId => amount + mapping( + address paymentClient + => mapping( + address recipient => mapping(bytes intentId => uint amount) + ) + ) public failedTransfers; + + // Errors + error FailedTransfer(); + + constructor() { + _disableInitializers(); + } + + /** + * @notice Initializes the payment processor module + * @param orchestrator_ The orchestrator contract address + * @param metadata Module metadata + * @param configData ABI encoded configuration data (everClearSpoke and WETH addresses) + */ function init( IOrchestrator_v1 orchestrator_, Metadata memory metadata, bytes memory configData - ) external override initializer { + ) external override(Module_v1) initializer { __Module_init(orchestrator_, metadata); - ( - uint chainId_, - address connextBridgeLogic_, - address everClearSpoke_, - address weth_ - ) = abi.decode(configData, (uint, address, address, address)); + (address everClearSpoke_, address weth_) = + abi.decode(configData, (address, address)); everClearSpoke = IEverclearSpoke(everClearSpoke_); weth = IWETH(weth_); } - /// @notice Execute the cross-chain bridge transfer - /// @dev Override this function to implement specific bridge logic - /// @param order The payment order containing all necessary transfer details - /// @return bridgeData Arbitrary data returned by the bridge implementation - function _executeBridgeTransfer( - IERC20PaymentClientBase_v1.PaymentOrder memory order, + // External Mutating Functions + /** + * @notice Processes multiple payment orders through the bridge + * @param client The payment client contract interface + * @param executionData Additional data needed for execution (encoded maxFee and TTL) + */ + function processPayments( + IERC20PaymentClientBase_v1 client, bytes memory executionData - ) internal override returns (bytes memory) { - //@notice call the connextBridgeLogic to execute the bridge transfer - bytes32 intentId = xcall(order, executionData); - return abi.encode(intentId); - } - - function processPayments(IERC20PaymentClientBase_v1 client) - external - override - { - // Collect orders from the client + ) external { IERC20PaymentClientBase_v1.PaymentOrder[] memory orders; (orders,,) = client.collectPaymentOrders(); - + address clientAddress = address(client); for (uint i = 0; i < orders.length; i++) { bytes memory bridgeData = _executeBridgeTransfer(orders[i], executionData); - emit PaymentOrderProcessed( - address(client), - orders[i].recipient, - address(orders[i].paymentToken), - orders[i].amount, - orders[i].start, - orders[i].cliff, - orders[i].end - ); - _paymentId++; - - // Inform the client about the processed amount + if (bytes32(bridgeData) != bytes32(0)) { + // Handle successful transfer + _bridgeData[i] = bridgeData; + emit PaymentOrderProcessed( + address(client), + orders[i].recipient, + orders[i].paymentToken, + orders[i].amount, + orders[i].start, + orders[i].cliff, + orders[i].end + ); + _paymentId++; + processedIntentId[address(client)][orders[i].recipient] = + bytes32(bridgeData); + } else { + // Handle failed transfer + failedTransfers[clientAddress][orders[i].recipient][executionData] + = orders[i].amount; + emit TransferFailed( + clientAddress, + orders[i].recipient, + executionData, + orders[i].amount + ); + } client.amountPaid(orders[i].paymentToken, orders[i].amount); } } - function xcall( + /** + * @notice Cancels a pending transfer and returns funds to the client + * @param client The payment client address + * @param recipient The recipient address + * @param executionData The execution data to cancel + * @param order The payment order details + */ + function cancelTransfer( + address client, + address recipient, + bytes memory executionData, + IERC20PaymentClientBase_v1.PaymentOrder memory order + ) external { + _validateTransferRequest(client, recipient, executionData); + _cleanupFailedTransfer(client, recipient, executionData); + + if (!IERC20(order.paymentToken).transfer(recipient, order.amount)) { + revert FailedTransfer(); + } + + emit TransferCancelled(client, recipient, executionData, order.amount); + } + + /** + * @notice Retries a previously failed transfer + * @param client The payment client address + * @param recipient The recipient address + * @param order The payment order details + * @param executionData Old execution data that failed + * @param newExecutionData New execution data for retry + */ + function retryFailedTransfer( + address client, + address recipient, + bytes memory executionData, + bytes memory newExecutionData, + IERC20PaymentClientBase_v1.PaymentOrder memory order + ) external { + _validateTransferRequest(client, recipient, executionData); + + bytes32 newIntentId = + _createCrossChainIntent(order, newExecutionData, false); + if (newIntentId == bytes32(0)) { + revert Module__PP_Crosschain__MessageDeliveryFailed( + 8453, 8453, executionData + ); + } + + _cleanupFailedTransfer(client, recipient, executionData); + processedIntentId[client][recipient] = newIntentId; + } + + // View Functions + /** + * @notice Retrieves the bridge data for a specific payment ID + * @param paymentId The unique identifier of the payment + * @return The bridge data associated with the payment + */ + function getBridgeData(uint paymentId) + public + view + override + returns (bytes memory) + { + return _bridgeData[paymentId]; + } + + // Overridden Internal Functions + /** + * @dev Execute the cross-chain bridge transfer + * @param order The payment order containing transfer details + * @param executionData Additional execution parameters + * @return bridgeData Data returned by the bridge implementation + */ + function _executeBridgeTransfer( IERC20PaymentClientBase_v1.PaymentOrder memory order, bytes memory executionData - ) internal returns (bytes32 intentId) { - // Decode any additional parameters from executionData - (uint maxFee, uint ttl) = abi.decode(executionData, (uint, uint)); + ) internal override returns (bytes memory) { + bytes32 _intentId = _createCrossChainIntent(order, executionData, true); + return abi.encode(_intentId); + } - // Wrap ETH into WETH to send with the xcall - IERC20(order.paymentToken).transferFrom( - msg.sender, address(this), order.amount - ); + // Internal Helper Functions + /** + * @dev Creates a new cross-chain intent for payment transfer + * @param order The payment order details + * @param executionData Additional execution parameters + * @param transferFromRecipient Whether to transfer from the recipient + * @return The ID of the created intent + */ + function _createCrossChainIntent( + IERC20PaymentClientBase_v1.PaymentOrder memory order, + bytes memory executionData, + bool transferFromRecipient + ) internal returns (bytes32) { + _validateOrder(order); - // This contract approves transfer to EverClearSpoke - IERC20(order.paymentToken).approve( - address(everClearSpoke), order.amount - ); + if (executionData.length == 0) { + revert + ICrossChainBase_v1 + .Module__CrossChainBase_InvalidExecutionData(); + } + (uint maxFee, uint ttl) = abi.decode(executionData, (uint, uint)); + if (ttl == 0) { + revert + IPP_Connext_Crosschain_v1 + .Module__PP_Connext_Crosschain__InvalidTTL(); + } + if (transferFromRecipient) { + IERC20(order.paymentToken).transferFrom( + order.recipient, address(this), order.amount + ); + IERC20(order.paymentToken).approve( + address(everClearSpoke), order.amount + ); + } - // Create destinations array with the target chain uint32[] memory destinations = new uint32[](1); - destinations[0] = 8453; // @note -> hardcode for now -> order.destinationChainId; + destinations[0] = 8453; - // Call newIntent on the EverClearSpoke contract - (intentId,) = everClearSpoke.newIntent( + return everClearSpoke.newIntent( destinations, - order.recipient, // to - order.paymentToken, // inputAsset - address(weth), // outputAsset (assuming same asset on destination) - order.amount, // amount - uint24(maxFee), // maxFee (cast to uint24) - uint48(ttl), // ttl (cast to uint48) - "" // empty data field, modify if needed + order.recipient, + order.paymentToken, + address(weth), + order.amount, + uint24(maxFee), + uint48(ttl), + "" ); + } + + /** + * @dev Validates a transfer request + * @param client The payment client address + * @param recipient The recipient address + * @param executionData The execution data + */ + function _validateTransferRequest( + address client, + address recipient, + bytes memory executionData + ) internal view returns (uint) { + //msg.sender should be the client + if (msg.sender != client) { + revert Module__InvalidAddress(); + } + uint failedAmount = failedTransfers[client][recipient][executionData]; + if (failedAmount == 0) { + revert Module__CrossChainBase__InvalidAmount(); + } + //intentId should be 0 if the transfer has not been processed yet + if (processedIntentId[client][recipient] != bytes32(0)) { + revert Module__PP_Crosschain__InvalidIntentId(); + } + + return failedAmount; + } - return intentId; + /** + * @dev Cleans up storage after handling a failed transfer + * @param client The payment client address + * @param recipient The recipient address + * @param executionData The execution data + */ + function _cleanupFailedTransfer( + address client, + address recipient, + bytes memory executionData + ) internal { + delete processedIntentId[client][recipient]; + delete failedTransfers[client][recipient][executionData]; } - ///cancelling payments on connext?????? + /** + * @dev Validates a payment order + * @param order The payment order to validate + */ + function _validateOrder( + IERC20PaymentClientBase_v1.PaymentOrder memory order + ) internal pure { + if (order.amount == 0) { + revert ICrossChainBase_v1.Module__CrossChainBase__InvalidAmount(); + } + if (order.recipient == address(0)) { + revert ICrossChainBase_v1.Module__CrossChainBase__InvalidRecipient(); + } + } } diff --git a/src/modules/paymentProcessor/abstracts/CrossChainBase_v1.sol b/src/modules/paymentProcessor/abstracts/CrossChainBase_v1.sol new file mode 100644 index 000000000..b2929d62f --- /dev/null +++ b/src/modules/paymentProcessor/abstracts/CrossChainBase_v1.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.23; + +import {IOrchestrator_v1} from + "src/orchestrator/interfaces/IOrchestrator_v1.sol"; +import {IPaymentProcessor_v1} from "@pp/IPaymentProcessor_v1.sol"; +import {IERC20PaymentClientBase_v1} from + "@lm/interfaces/IERC20PaymentClientBase_v1.sol"; +import {Module_v1} from "src/modules/base/Module_v1.sol"; +import {ERC165Upgradeable, Module_v1} from "src/modules/base/Module_v1.sol"; +import {ICrossChainBase_v1} from "../interfaces/ICrosschainBase_v1.sol"; + +/** + * @title Cross-chain Base Contract + * @notice Abstract base contract providing core cross-chain functionality for payment + * processors. + * @dev This contract exposes fundamental cross-chain operations and provides: + * - Bridge data storage and retrieval functionality + * - Abstract interface for bridge transfer execution + * - Integration with the Module_v1 base contract + * - Implementation of ICrossChainBase_v1 interface + * @custom:security-contact security@inverter.network + * In case of any concerns or findings, please refer to our + * Security Policy at security.inverter.network or email us + * directly! + * @custom:version 1.0.0 + * @custom:standard-version 1.0.0 + * @author 33Audits + */ +abstract contract CrossChainBase_v1 is ICrossChainBase_v1, Module_v1 { + // Storage Variables + mapping(uint => bytes) internal _bridgeData; + + // External Functions + /// @notice Process payments for a given payment client + /// @param client The payment client to process payments for + function processPayments(IERC20PaymentClientBase_v1 client) + external + virtual; + + // Public Functions + /// @inheritdoc ERC165Upgradeable + function supportsInterface(bytes4 interfaceId_) + public + view + virtual + override(Module_v1) + returns (bool) + { + return interfaceId_ == type(ICrossChainBase_v1).interfaceId + || super.supportsInterface(interfaceId_); + } + + /// @notice Get the bridge data for a given payment ID + /// @param paymentId The ID of the payment to get the bridge data for + /// @return The bridge data for the given payment ID + function getBridgeData(uint paymentId) + public + view + virtual + returns (bytes memory); + + // Internal Functions + /// @notice Execute the cross-chain bridge transfer + /// @dev Override this function to implement specific bridge logic + /// @param order The payment order containing all necessary transfer details + /// @return bridgeData Arbitrary data returned by the bridge implementation + function _executeBridgeTransfer( + IERC20PaymentClientBase_v1.PaymentOrder memory order, + bytes memory executionData + ) internal virtual returns (bytes memory); +} diff --git a/src/modules/paymentProcessor/abstracts/CrosschainBase_v1.sol b/src/modules/paymentProcessor/abstracts/CrosschainBase_v1.sol deleted file mode 100644 index c67dfc512..000000000 --- a/src/modules/paymentProcessor/abstracts/CrosschainBase_v1.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity 0.8.23; - -import {IOrchestrator_v1} from - "src/orchestrator/interfaces/IOrchestrator_v1.sol"; -import {IPaymentProcessor_v1} from "@pp/IPaymentProcessor_v1.sol"; -import {IERC20PaymentClientBase_v1} from - "@lm/interfaces/IERC20PaymentClientBase_v1.sol"; -import {Module_v1} from "src/modules/base/Module_v1.sol"; -//import {IPP_CrossChain_v1} from "./IPP_Template_v1.sol"; -import {ERC165Upgradeable, Module_v1} from "src/modules/base/Module_v1.sol"; -import {ICrossChainBase_v1} from "../interfaces/ICrosschainBase_v1.sol"; -/** - * @title Inverter Template Payment Processor - * - * @notice Basic template payment processor used as base for developing new payment processors. - * - * @dev This contract is used to showcase a basic setup for a payment processor. The contract showcases the - * following: - * - Inherit from the Module_v1 contract to enable interaction with the Inverter workflow. - * - Use of the IPaymentProcessor_v1 interface to facilitate interaction with a payment client. - * - Implement custom interface which has all the public facing functions, errors, events and structs. - * - Pre-defined layout for all contract functions, modifiers, state variables etc. - * - Use of the ERC165Upgradeable contract to check for interface support. - * - * @custom:security-contact security@inverter.network - * In case of any concerns or findings, please refer to our Security Policy - * at security.inverter.network or email us directly! - * - * @author Inverter Network - */ - -abstract contract CrosschainBase_v1 is ICrossChainBase_v1, Module_v1 { - /// @inheritdoc ERC165Upgradeable - function supportsInterface(bytes4 interfaceId_) - public - view - virtual - override(Module_v1) - returns (bool) - { - return interfaceId_ == type(ICrossChainBase_v1).interfaceId - || super.supportsInterface(interfaceId_); - } - //-------------------------------------------------------------------------- - // State - - /// @dev The number of payment orders. - uint internal _paymentId; - - /// @notice Execute the cross-chain bridge transfer - /// @dev Override this function to implement specific bridge logic - /// @param order The payment order containing all necessary transfer details - /// @return bridgeData Arbitrary data returned by the bridge implementation - function _executeBridgeTransfer( - IERC20PaymentClientBase_v1.PaymentOrder memory order, - bytes memory executionData - ) internal virtual returns (bytes memory); -} diff --git a/src/modules/paymentProcessor/abstracts/PP_Crosschain_v1.sol b/src/modules/paymentProcessor/abstracts/PP_Crosschain_v1.sol index fa1dbc4c1..92c60f172 100644 --- a/src/modules/paymentProcessor/abstracts/PP_Crosschain_v1.sol +++ b/src/modules/paymentProcessor/abstracts/PP_Crosschain_v1.sol @@ -1,58 +1,64 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity 0.8.23; +// Internal Imports import {IOrchestrator_v1} from "src/orchestrator/interfaces/IOrchestrator_v1.sol"; import {IPaymentProcessor_v1} from "@pp/IPaymentProcessor_v1.sol"; import {IERC20PaymentClientBase_v1} from "@lm/interfaces/IERC20PaymentClientBase_v1.sol"; import {ERC165Upgradeable, Module_v1} from "src/modules/base/Module_v1.sol"; - -import {CrosschainBase_v1} from "./CrosschainBase_v1.sol"; - -// External Interfaces -import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {CrossChainBase_v1} from "./CrossChainBase_v1.sol"; +import {IPP_Crosschain_v1} from "../interfaces/IPP_Crosschain_v1.sol"; // External Dependencies +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; import {ERC20} from "@oz/token/ERC20/ERC20.sol"; - -// External Libraries import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; -// Internal Interfaces -import {IPP_Crosschain_v1} from "../interfaces/IPP_Crosschain_v1.sol"; - /** - * @title Inverter Template Payment Processor + * @title Cross-chain Payment Processor Base Contract * - * @notice Basic template payment processor used as base for developing new payment processors. + * @notice Abstract base contract for implementing cross-chain payment processing functionality. * - * @dev This contract is used to showcase a basic setup for a payment processor. The contract showcases the - * following: - * - Inherit from the Module_v1 contract to enable interaction with the Inverter workflow. - * - Use of the IPaymentProcessor_v1 interface to facilitate interaction with a payment client. - * - Implement custom interface which has all the public facing functions, errors, events and structs. - * - Pre-defined layout for all contract functions, modifiers, state variables etc. - * - Use of the ERC165Upgradeable contract to check for interface support. + * @dev This contract serves as the base for cross-chain payment processors and provides: + * - Extension of CrossChainBase_v1 for cross-chain functionality + * - Implementation of IPP_Crosschain_v1 interface + * - Core payment validation logic + * - Basic security checks for payment processing + * - Abstract functions for bridge-specific implementations * * @custom:security-contact security@inverter.network * In case of any concerns or findings, please refer to our Security Policy * at security.inverter.network or email us directly! * * @author Inverter Network + * @custom:version 1.0.0 + * @custom:standard-version 1.0.0 */ -abstract contract PP_Crosschain_v1 is CrosschainBase_v1, IPP_Crosschain_v1 { - /// @inheritdoc ERC165Upgradeable - function supportsInterface(bytes4 interfaceId_) - public - view - virtual - override(CrosschainBase_v1) - returns (bool) - { - return interfaceId_ == type(IPP_Crosschain_v1).interfaceId - || super.supportsInterface(interfaceId_); - } +abstract contract PP_Crosschain_v1 is CrossChainBase_v1, IPP_Crosschain_v1 { + //-------------------------------------------------------------------------- + // Events + event PaymentProcessed(uint indexed paymentId, address indexed client); + event PaymentCancelled(uint indexed paymentId, address indexed client); + event UnclaimablePaymentClaimed( + address indexed client, + address indexed token, + address indexed receiver, + uint amount + ); + + //-------------------------------------------------------------------------- + // Constants + uint public constant VERSION = 1; + + //-------------------------------------------------------------------------- + // Storage Variables + bytes public executionData; + uint public _paymentId; + + /// @dev Gap for possible future upgrades. + uint[50] private __gap; //-------------------------------------------------------------------------- // Modifiers @@ -74,22 +80,26 @@ abstract contract PP_Crosschain_v1 is CrosschainBase_v1, IPP_Crosschain_v1 { } //-------------------------------------------------------------------------- - // Storage - - bytes public executionData; + // External/Public Functions - /// @dev Gap for possible future upgrades. - uint[50] private __gap; - - //-------------------------------------------------------------------------- - // Virtual Functions + /// @inheritdoc ERC165Upgradeable + function supportsInterface(bytes4 interfaceId_) + public + view + virtual + override(CrossChainBase_v1) + returns (bool) + { + return interfaceId_ == type(IPP_Crosschain_v1).interfaceId + || super.supportsInterface(interfaceId_); + } /// @notice Process payments for a given payment client /// @param client The payment client to process payments for function processPayments(IERC20PaymentClientBase_v1 client) external virtual - override(IPaymentProcessor_v1) + override(IPaymentProcessor_v1, CrossChainBase_v1) {} /// @inheritdoc IPaymentProcessor_v1 @@ -132,6 +142,9 @@ abstract contract PP_Crosschain_v1 is CrosschainBase_v1, IPP_Crosschain_v1 { && _validPaymentToken(order.paymentToken); } + //-------------------------------------------------------------------------- + // Internal Functions + /// @dev Validate address input. /// @param addr Address to validate. /// @return True if address is valid. @@ -160,8 +173,6 @@ abstract contract PP_Crosschain_v1 is CrosschainBase_v1, IPP_Crosschain_v1 { pure returns (bool) { - // _start + _cliff should be less or equal to _end - // this already implies that _start is not greater than _end return _start + _cliff <= _end; } @@ -169,9 +180,6 @@ abstract contract PP_Crosschain_v1 is CrosschainBase_v1, IPP_Crosschain_v1 { /// @param _token Address of the token to validate. /// @return True if address is valid. function _validPaymentToken(address _token) internal returns (bool) { - // Only a basic sanity check that the address supports the balanceOf() function. The corresponding - // module should ensure it's sending an ERC20. - (bool success, bytes memory data) = _token.call( abi.encodeWithSelector( IERC20(_token).balanceOf.selector, address(this) diff --git a/src/modules/paymentProcessor/interfaces/ICrosschainBase_v1.sol b/src/modules/paymentProcessor/interfaces/ICrosschainBase_v1.sol index 9346ad2a4..ae19d5a5c 100644 --- a/src/modules/paymentProcessor/interfaces/ICrosschainBase_v1.sol +++ b/src/modules/paymentProcessor/interfaces/ICrosschainBase_v1.sol @@ -7,11 +7,15 @@ pragma solidity ^0.8.0; import {IERC20PaymentClientBase_v1} from "@lm/interfaces/IERC20PaymentClientBase_v1.sol"; +/// @title ICrossChainBase_v1 +/// @notice Base interface for cross-chain payment processing functionality interface ICrossChainBase_v1 { - //-------------------------------------------------------------------------- - // Structs - /// @notice Struct to hold cross-chain message data + /// @param messageId Unique identifier for the cross-chain message + /// @param sourceChain Address or identifier of the source chain + /// @param targetChain Address or identifier of the target chain + /// @param payload The encoded data being sent across chains + /// @param executed Boolean flag indicating if the message has been processed struct CrossChainMessage { uint messageId; address sourceChain; @@ -24,23 +28,27 @@ interface ICrossChainBase_v1 { // Events /// @notice Emitted when a bridge transfer is executed + /// @param bridgeData The encoded data of the bridge transfer event BridgeTransferExecuted(bytes indexed bridgeData); //-------------------------------------------------------------------------- // Errors - /// @notice Amount can not be zero. + /// @notice Thrown when attempting to process a transfer with zero amount error Module__CrossChainBase__InvalidAmount(); - /// @notice Client is not valid. + /// @notice Thrown when an unauthorized client attempts to interact with the contract error Module__CrossChainBase__NotValidClient(); - /// @notice Message has already been executed + /// @notice Thrown when attempting to execute a message that has already been processed error Module__CrossChainBase_MessageAlreadyExecuted(); - /// @notice Invalid chain ID provided - error Module__CrossChainBase_InvalidChainId(); - - /// @notice Message verification failed + /// @notice Thrown when the cross-chain message fails verification error Module__CrossChainBase_MessageVerificationFailed(); + + /// @notice Thrown when the provided execution data is malformed or invalid + error Module__CrossChainBase_InvalidExecutionData(); + + /// @notice Thrown when the recipient address is invalid or not allowed + error Module__CrossChainBase__InvalidRecipient(); } diff --git a/src/modules/paymentProcessor/interfaces/IEverclear.sol b/src/modules/paymentProcessor/interfaces/IEverclear.sol index 293afc3ec..2123210c3 100644 --- a/src/modules/paymentProcessor/interfaces/IEverclear.sol +++ b/src/modules/paymentProcessor/interfaces/IEverclear.sol @@ -10,5 +10,5 @@ interface IEverclearSpoke { uint24 maxFee, uint48 ttl, bytes memory data - ) external returns (bytes32 intentId, uint amountOut); + ) external returns (bytes32 intentId); } diff --git a/src/modules/paymentProcessor/interfaces/IPP_Connext_Crosschain.sol b/src/modules/paymentProcessor/interfaces/IPP_Connext_Crosschain.sol deleted file mode 100644 index dd034d85a..000000000 --- a/src/modules/paymentProcessor/interfaces/IPP_Connext_Crosschain.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity ^0.8.0; - -// Internal Interfaces -import {IERC20PaymentClientBase_v1} from - "@lm/interfaces/IERC20PaymentClientBase_v1.sol"; -import {IPaymentProcessor_v1} from - "src/modules/paymentProcessor/IPaymentProcessor_v1.sol"; - -// External Interfaces -import {IERC20} from "@oz/token/ERC20/IERC20.sol"; - -interface IPP_Connext_Crosschain is IPaymentProcessor_v1 {} diff --git a/src/modules/paymentProcessor/interfaces/IPP_Connext_Crosschain_v1.sol b/src/modules/paymentProcessor/interfaces/IPP_Connext_Crosschain_v1.sol new file mode 100644 index 000000000..c296de066 --- /dev/null +++ b/src/modules/paymentProcessor/interfaces/IPP_Connext_Crosschain_v1.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.20; + +// Internal Dependencies +import {IPaymentProcessor_v1} from + "src/modules/paymentProcessor/IPaymentProcessor_v1.sol"; +import {IEverclearSpoke} from + "src/modules/paymentProcessor/interfaces/IEverclear.sol"; + +// External Dependencies +import {IERC20PaymentClientBase_v1} from + "@lm/interfaces/IERC20PaymentClientBase_v1.sol"; +import {IWETH} from "src/modules/paymentProcessor/interfaces/IWETH.sol"; + +/// @notice Interface for cross-chain payment processing using Connext protocol +interface IPP_Connext_Crosschain_v1 is IPaymentProcessor_v1 { + //-------------------------------------------------------------------------- + // Errors + + /// @notice Thrown when the provided Time-To-Live (TTL) parameter is invalid + error Module__PP_Connext_Crosschain__InvalidTTL(); + + //-------------------------------------------------------------------------- + // View Functions + + /// @notice Returns the Everclear spoke contract instance + /// @return The IEverclearSpoke contract interface + function everClearSpoke() external view returns (IEverclearSpoke); + + /// @notice Returns the WETH contract instance + /// @return The IWETH contract interface used for wrapping/unwrapping ETH + function weth() external view returns (IWETH); + + //-------------------------------------------------------------------------- + // External Functions + + /// @notice Process payments for a given client with execution data + /// @dev This function handles the cross-chain payment processing using Connext + /// @param client The payment client contract initiating the payment + /// @param executionData The encoded execution parameters (maxFee, ttl) + function processPayments( + IERC20PaymentClientBase_v1 client, + bytes memory executionData + ) external; + + /// @notice Get bridge data for a specific payment ID + /// @dev Used to retrieve information about a cross-chain payment + /// @param paymentId The ID of the payment to query + /// @return The bridge data associated with the payment (encoded bytes) + function getBridgeData(uint paymentId) + external + view + returns (bytes memory); +} diff --git a/src/modules/paymentProcessor/interfaces/IPP_Crosschain_v1.sol b/src/modules/paymentProcessor/interfaces/IPP_Crosschain_v1.sol index 6f9b5e207..fc3668ec8 100644 --- a/src/modules/paymentProcessor/interfaces/IPP_Crosschain_v1.sol +++ b/src/modules/paymentProcessor/interfaces/IPP_Crosschain_v1.sol @@ -1,34 +1,79 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.0; -// Internal Interfaces -import {IERC20PaymentClientBase_v1} from - "@lm/interfaces/IERC20PaymentClientBase_v1.sol"; +// Internal Dependencies import {IPaymentProcessor_v1} from "src/modules/paymentProcessor/IPaymentProcessor_v1.sol"; -// External Interfaces -import {IERC20} from "@oz/token/ERC20/IERC20.sol"; - +/// @notice Interface for cross-chain payment processing functionality interface IPP_Crosschain_v1 is IPaymentProcessor_v1 { + // Events + //-------------------------------------------------------------------------- + + /// @notice Emitted when a cross-chain transfer fails to complete + /// @param client The address initiating the transfer + /// @param recipient The intended recipient of the transfer + /// @param executionData The unique identifier for this transfer attempt + /// @param amount The amount that failed to transfer + event TransferFailed( + address indexed client, + address indexed recipient, + bytes indexed executionData, + uint amount + ); + + /// @notice Emitted when a previously failed transfer is attempted again + /// @param client The address initiating the retry + /// @param recipient The intended recipient of the transfer + /// @param oldIntentId The intent ID of the failed transfer + /// @param newIntentId The new intent ID for this retry attempt + event FailedTransferRetried( + address indexed client, + address indexed recipient, + bytes32 indexed oldIntentId, + bytes32 newIntentId + ); + + /// @notice Emitted when a transfer is cancelled by the client or system + /// @param client The address that initiated the original transfer + /// @param recipient The intended recipient of the cancelled transfer + /// @param intentId The unique identifier of the cancelled transfer + /// @param amount The amount that was intended to be transferred + event TransferCancelled( + address indexed client, + address indexed recipient, + bytes indexed intentId, + uint amount + ); + + // Errors + //-------------------------------------------------------------------------- + + /// @notice Thrown when the provided intent ID is invalid or does not exist + error Module__PP_Crosschain__InvalidIntentId(); + + /// @notice Thrown when the payment amount is invalid (e.g., zero or exceeds + /// limits) + error Module__PP_Crosschain__InvalidAmount(); + + /// @notice Thrown when the cross-chain bridge fees exceed the maximum allowed + error Module__PP_Crosschain__InvalidBridgeFee(); + /// @notice Thrown when the cross-chain message fails to be delivered /// @param sourceChain The chain ID where the message originated - /// @param destinationChain The chain ID where the message was meant to be delivered - /// @param messageId The unique identifier of the failed message + /// @param destinationChain The chain ID where the message was meant to be + /// delivered + /// @param executionData The encoded execution parameters (maxFee, ttl) error Module__PP_Crosschain__MessageDeliveryFailed( - uint sourceChain, uint destinationChain, bytes32 messageId + uint sourceChain, uint destinationChain, bytes executionData ); - /// @notice Thrown when attempting to process a cross-chain payment with invalid parameters + /// @notice Thrown when attempting to process a cross-chain payment with invalid + /// parameters /// @param paymentClient The address of the payment client /// @param paymentId The ID of the payment being processed /// @param destinationChain The target chain ID for the payment error Module__PP_Crosschain__InvalidPaymentParameters( address paymentClient, uint paymentId, uint destinationChain ); - - /// @notice Thrown when the cross-chain bridge fees exceed the maximum allowed - /// @param actualFee The actual fee required - /// @param maxFee The maximum fee allowed - error Module__PP_Crosschain__BridgeFeeTooHigh(uint actualFee, uint maxFee); } diff --git a/src/modules/paymentProcessor/interfaces/IWETH.sol b/src/modules/paymentProcessor/interfaces/IWETH.sol index 79b2b6256..87b293284 100644 --- a/src/modules/paymentProcessor/interfaces/IWETH.sol +++ b/src/modules/paymentProcessor/interfaces/IWETH.sol @@ -3,4 +3,5 @@ pragma solidity ^0.8.0; interface IWETH { function deposit() external payable; function withdraw(uint amount) external; + function approve(address spender, uint amount) external returns (bool); } diff --git a/src/templates/tests/unit/FM_Template_v1.t.sol b/src/templates/tests/unit/FM_Template_v1.t.sol index 3806448c3..30fc599db 100644 --- a/src/templates/tests/unit/FM_Template_v1.t.sol +++ b/src/templates/tests/unit/FM_Template_v1.t.sol @@ -64,7 +64,7 @@ contract FM_Template_v1_Test is ModuleTest { // Deploy the SuT address impl = address(new FM_Template_v1_Exposed()); fundingManager = FM_Template_v1_Exposed(Clones.clone(impl)); - + orchestratorToken = new ERC20Mock("Orchestrator Token", "OTK"); // Setup the module to test diff --git a/test/modules/paymentProcessor/PP_Connext_Bridge.t.sol b/test/modules/paymentProcessor/PP_Connext_Bridge.t.sol deleted file mode 100644 index 64b560ee4..000000000 --- a/test/modules/paymentProcessor/PP_Connext_Bridge.t.sol +++ /dev/null @@ -1,73 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -pragma solidity ^0.8.0; - -//Internal Dependencies -import { - ModuleTest, - IModule_v1, - IOrchestrator_v1 -} from "test/modules/ModuleTest.sol"; -import {OZErrors} from "test/utils/errors/OZErrors.sol"; -import {ICrossChainBase_v1} from - "src/modules/paymentProcessor/interfaces/ICrosschainBase_v1.sol"; -import {CrosschainBase_v1} from - "src/modules/paymentProcessor/abstracts/CrosschainBase_v1.sol"; -//External Dependencies -import {Clones} from "@oz/proxy/Clones.sol"; - -//Tests and Mocks -// import cr -import { - IERC20PaymentClientBase_v1, - ERC20PaymentClientBaseV1Mock, - ERC20Mock -} from "test/utils/mocks/modules/paymentClient/ERC20PaymentClientBaseV1Mock.sol"; - -import {ICrossChainBase_v1} from - "src/modules/paymentProcessor/interfaces/ICrosschainBase_v1.sol"; -/** - * @title Inverter Template Payment Processor - * - * @notice Basic template payment processor used to showcase the unit testing setup - * - * @dev Not all functions are tested in this template. Placeholders of the functions that are not tested are added - * into the contract. This test showcases the following: - * - Inherit from the ModuleTest contract to enable interaction with the Inverter workflow. - * - Showcases the setup of the workflow, uses in test unit tests. - * - Pre-defined layout for all setup and functions to be tested. - * - Shows the use of Gherkin for documenting the testing. VS Code extension used for formatting is recommended. - * - Shows the use of the modifierInPlace pattern to test the modifier placement. - * - * @custom:security-contact security@inverter.network - * In case of any concerns or findings, please refer to our Security Policy - * at security.inverter.network or email us directly! - * - * @author Inverter Network - */ - -contract PP_Connext_Bridge_Test is ModuleTest { - //-------------------------------------------------------------------------- - //Constants - - //-------------------------------------------------------------------------- - //State - - //-------------------------------------------------------------------------- - //Mocks - - //-------------------------------------------------------------------------- - //Setup - function setUp() public {} - - //-------------------------------------------------------------------------- - //Test: Initialization - - //Test if the orchestrator is correctly set - function testInit() public override(ModuleTest) {} - - //Test the interface support - function testSupportsInterface() public {} - - //Test the reinit function - function testReinitFails() public override(ModuleTest) {} -} diff --git a/test/modules/paymentProcessor/PP_Connext_Crosschain_v1.t.sol b/test/modules/paymentProcessor/PP_Connext_Crosschain_v1.t.sol new file mode 100644 index 000000000..ba0ee42f4 --- /dev/null +++ b/test/modules/paymentProcessor/PP_Connext_Crosschain_v1.t.sol @@ -0,0 +1,1282 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.20; + +//-------------------------------------------------------------------------- +// Imports +// External Dependencies + +import {Test} from "forge-std/Test.sol"; +import {Clones} from "@oz/proxy/Clones.sol"; +import {IERC20Errors} from "@oz/interfaces/draft-IERC6093.sol"; +import {IWETH} from "src/modules/paymentProcessor/interfaces/IWETH.sol"; +import "forge-std/console2.sol"; + +// Internal Dependencies +import {PP_Connext_Crosschain_v1} from + "src/modules/paymentProcessor/PP_Connext_Crosschain_v1.sol"; +import {CrossChainBase_v1} from + "src/modules/paymentProcessor/abstracts/CrossChainBase_v1.sol"; +import {ICrossChainBase_v1} from + "src/modules/paymentProcessor/interfaces/ICrosschainBase_v1.sol"; +import {IPaymentProcessor_v1} from + "src/modules/paymentProcessor/IPaymentProcessor_v1.sol"; +import {IPP_Crosschain_v1} from + "src/modules/paymentProcessor/interfaces/IPP_Crosschain_v1.sol"; +import {IModule_v1, IOrchestrator_v1} from "src/modules/base/IModule_v1.sol"; +import {IERC20PaymentClientBase_v1} from + "@lm/interfaces/IERC20PaymentClientBase_v1.sol"; + +// Tests and Mocks +import {CrossChainBase_v1_Exposed} from + "test/utils/mocks/modules/paymentProcessor/CrossChainBase_v1_Exposed.sol"; +import {PP_Connext_Crosschain_v1_Exposed} from + "test/utils/mocks/modules/paymentProcessor/PP_Connext_Crosschain_v1_Exposed.sol"; +import {Mock_EverclearPayment} from + "test/utils/mocks/external/Mock_EverclearPayment.sol"; +import { + IERC20PaymentClientBase_v1, + ERC20PaymentClientBaseV1Mock +} from "test/utils/mocks/modules/paymentClient/ERC20PaymentClientBaseV1Mock.sol"; +import {ModuleTest} from "test/modules/ModuleTest.sol"; +import {OZErrors} from "test/utils/errors/OZErrors.sol"; + +contract PP_Connext_Crosschain_v1_Test is ModuleTest { + //-------------------------------------------------------------------------- + // Constants + uint constant MINTED_SUPPLY = 1000 ether; + uint constant ZERO_AMOUNT = 0; + //-------------------------------------------------------------------------- + // Test Storage + PP_Connext_Crosschain_v1_Exposed public paymentProcessor; + Mock_EverclearPayment public everclearPaymentMock; + ERC20PaymentClientBaseV1Mock paymentClient; + IPP_Crosschain_v1 public crossChainBase; + IWETH public weth; + + // Bridge-related storage + address public mockConnextBridge; + address public mockEverClearSpoke; + address public mockWeth; + + // Execution data storage + uint maxFee = 0; + uint ttl = 1; + bytes executionData; + bytes invalidExecutionData; + + //-------------------------------------------------------------------------- + // Setup Function + + function setUp() public { + // Prepare execution data for bridge operations + executionData = abi.encode(maxFee, ttl); + invalidExecutionData = abi.encode(address(0)); + + // Deploy mock contracts and set addresses + everclearPaymentMock = new Mock_EverclearPayment(); + mockEverClearSpoke = address(everclearPaymentMock); + mockWeth = address(weth); + + // Deploy payment processor via clone + address impl = address(new PP_Connext_Crosschain_v1_Exposed()); + paymentProcessor = PP_Connext_Crosschain_v1_Exposed(Clones.clone(impl)); + + _setUpOrchestrator(paymentProcessor); + _authorizer.setIsAuthorized(address(this), true); + + // Initialize payment processor with config + bytes memory configData = abi.encode(mockEverClearSpoke, mockWeth); + paymentProcessor.init(_orchestrator, _METADATA, configData); + + // Deploy and add payment client through timelock process + impl = address(new ERC20PaymentClientBaseV1Mock()); + paymentClient = ERC20PaymentClientBaseV1Mock(Clones.clone(impl)); + _orchestrator.initiateAddModuleWithTimelock(address(paymentClient)); + vm.warp(block.timestamp + _orchestrator.MODULE_UPDATE_TIMELOCK()); + _orchestrator.executeAddModule(address(paymentClient)); + + // Configure payment client + paymentClient.init(_orchestrator, _METADATA, bytes("")); + paymentClient.setIsAuthorized(address(paymentProcessor), true); + paymentClient.setToken(_token); + + _setupInitialBalances(); + } + + //-------------------------------------------------------------------------- + // Initialization Tests + + /* Test initialization + */ + function testInit() public override(ModuleTest) { + assertEq( + address(paymentProcessor.orchestrator()), address(_orchestrator) + ); + } + + /* Test interface support + */ + function testSupportsInterface() public { + // Test for IModule_v1 interface + assertTrue( + paymentProcessor.supportsInterface(type(IModule_v1).interfaceId) + ); + + // Test for ICrossChainBase_v1 interface + assertTrue( + paymentProcessor.supportsInterface( + type(ICrossChainBase_v1).interfaceId + ) + ); + + // Test for a non-supported interface (using a random interface ID) + assertFalse(paymentProcessor.supportsInterface(0xffffffff)); + } + + /* Test reinitialization + */ + function testReinitFails() public override(ModuleTest) { + vm.expectRevert(OZErrors.Initializable__InvalidInitialization); + paymentProcessor.init(_orchestrator, _METADATA, abi.encode(1)); + } + + //-------------------------------------------------------------------------- + // Payment Processing Tests + + /* Test single payment processing + └── Given single valid payment order + └── When processing cross-chain payments + └── Then it should emit PaymentProcessed events for payment + └── And it should create cross-chain intent + */ + function testFuzz_PublicProcessPayments_succeedsGivenSingleValidPaymentOrder( + address testRecipient, + uint testAmount + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + _setupSinglePayment(testRecipient, testAmount); + // Get the client interface + IERC20PaymentClientBase_v1 client = + IERC20PaymentClientBase_v1(address(paymentClient)); + // Expect the event + vm.expectEmit(true, true, true, true); + emit IPaymentProcessor_v1.PaymentOrderProcessed( + address(paymentClient), + testRecipient, + address(_token), + testAmount, + block.timestamp, + 0, + block.timestamp + 1 days + ); + + // Process payments + uint balanceBefore = _token.balanceOf(address(paymentProcessor)); + paymentProcessor.processPayments(client, executionData); + assertEq(_token.balanceOf(address(testRecipient)), 0); + console2.log(_token.balanceOf(address(testRecipient))); + uint balanceAfter = _token.balanceOf(address(paymentProcessor)); + assertEq(balanceAfter, balanceBefore + testAmount); + + bytes32 intentId = paymentProcessor.processedIntentId( + address(paymentClient), testRecipient + ); + assertEq( + uint(everclearPaymentMock.status(intentId)), + uint(Mock_EverclearPayment.IntentStatus.ADDED) + ); + } + + /* Test single payment outstanding token amounts + └── Given a single valid payment order + └── When processing cross-chain payments + └── Then it should verify the outstanding token amounts + */ + function testFuzz_PublicProcessPayments_verifyOutstandingTokenAmounts( + address testRecipient, + uint testAmount + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + _setupSinglePayment(testRecipient, testAmount); + // Get the client interface + IERC20PaymentClientBase_v1 client = + IERC20PaymentClientBase_v1(address(paymentClient)); + + assertEq(client.outstandingTokenAmount(address(_token)), testAmount); + paymentProcessor.processPayments(client, executionData); + assertEq(client.outstandingTokenAmount(address(_token)), 0); + } + + /* Test multiple payment processing + └── Given multiple valid payment orders + └── When processing cross-chain payments + └── Then it should emit PaymentProcessed events for each payment + └── And it should create multiple cross-chain intents + */ + function testFuzz_PublicProcessPayments_succeedsGivenMultipleValidPaymentOrders( + uint8 numRecipients, + uint testAmount + ) public { + vm.assume(numRecipients > 0 && numRecipients <= 10); + vm.assume(testAmount > 0 && testAmount < MINTED_SUPPLY); + + // Setup mock payment orders + address[] memory setupRecipients = new address[](numRecipients); + uint[] memory setupAmounts = new uint[](numRecipients); + + for (uint i = 0; i < numRecipients; i++) { + setupRecipients[i] = address( + uint160(uint(keccak256(abi.encodePacked(i, block.timestamp)))) + ); + setupAmounts[i] = + 1 + (uint64(uint(keccak256(abi.encode(i, testAmount))))); + } + + IERC20PaymentClientBase_v1.PaymentOrder[] memory orders = + _createPaymentOrders(numRecipients, setupRecipients, setupAmounts); + + // Get the client interface + IERC20PaymentClientBase_v1 client = + IERC20PaymentClientBase_v1(address(paymentClient)); + + // Expect events for each payment + for (uint i = 0; i < numRecipients; i++) { + vm.expectEmit(true, true, true, true); + emit IPaymentProcessor_v1.PaymentOrderProcessed( + address(paymentClient), + setupRecipients[i], + address(_token), + setupAmounts[i], + block.timestamp, + 0, + block.timestamp + 1 days + ); + } + + // Process payments + paymentProcessor.processPayments(client, executionData); + + //should be checking in the mock for valid bridge data + for (uint i = 0; i < numRecipients; i++) { + bytes32 intentId = paymentProcessor.processedIntentId( + address(paymentClient), setupRecipients[i] + ); + assertEq( + uint(everclearPaymentMock.status(intentId)), + uint(Mock_EverclearPayment.IntentStatus.ADDED) + ); + } + } + + /* Test multiple payment outstanding token amounts + └── Given multiple valid payment orders + └── When processing cross-chain payments + └── Then it should verify the outstanding token amounts for each payment + */ + function testFuzz_PublicProcessPaymentsMultiple_verifyOutstandingTokenAmounts( + uint8 numRecipients, + uint testAmount + ) public { + vm.assume(numRecipients > 0 && numRecipients <= 10); + vm.assume(testAmount > 0 && testAmount < MINTED_SUPPLY); + + // Setup mock payment orders + address[] memory setupRecipients = new address[](numRecipients); + uint[] memory setupAmounts = new uint[](numRecipients); + + uint OutstandingAmount = 0; + + for (uint i = 0; i < numRecipients; i++) { + setupRecipients[i] = address( + uint160(uint(keccak256(abi.encodePacked(i, block.timestamp)))) + ); + setupAmounts[i] = + 1 + (uint64(uint(keccak256(abi.encode(i, testAmount))))); + OutstandingAmount += setupAmounts[i]; + } + + IERC20PaymentClientBase_v1.PaymentOrder[] memory orders = + _createPaymentOrders(numRecipients, setupRecipients, setupAmounts); + + // Get the client interface + IERC20PaymentClientBase_v1 client = + IERC20PaymentClientBase_v1(address(paymentClient)); + + assertEq( + client.outstandingTokenAmount(address(_token)), OutstandingAmount + ); + // Process payments + paymentProcessor.processPayments(client, executionData); + assertEq(client.outstandingTokenAmount(address(_token)), 0); + } + + /* Test empty payment processing + └── Given no payment orders + └── When processing payments + └── Then it should complete successfully + └── And bridge data should remain empty + */ + + function testFuzz_PublicProcessPayments_succeedsGivenNoPaymentOrders() + public + { + // Process payments and verify _bridgeData mapping is not updated + paymentProcessor.processPayments( + IERC20PaymentClientBase_v1(address(paymentClient)), executionData + ); + assertTrue( + keccak256(paymentProcessor.getBridgeData(0)) == keccak256(bytes("")), + "Bridge data should be empty" + ); + assertEq( + paymentProcessor.processedIntentId( + address(paymentClient), address(0) + ), + bytes32(0) + ); + } + + //-------------------------------------------------------------------------- + // Error Case Tests + + function testFuzz_PublicProcessPayments_revertsGivenInvalidExecutionData( + address testRecipient, + uint testAmount + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + _setupSinglePayment(testRecipient, testAmount); + + // Get the client interface + IERC20PaymentClientBase_v1 client = + IERC20PaymentClientBase_v1(address(paymentClient)); + + // Process payments + vm.expectRevert(); + paymentProcessor.processPayments(client, invalidExecutionData); + } + + /* Test empty execution data + ├── Given empty execution data bytes + │ └── When attempting to process payment + │ └── Then it should revert with InvalidExecutionData + */ + function testFuzz_PublicProcessPayments_revertsGivenEmptyExecutionData( + address testRecipient, + uint testAmount + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + _setupSinglePayment(testRecipient, testAmount); + + // Get the client interface + IERC20PaymentClientBase_v1 client = + IERC20PaymentClientBase_v1(address(paymentClient)); + + // Process payments + vm.expectRevert( + ICrossChainBase_v1 + .Module__CrossChainBase_InvalidExecutionData + .selector + ); + paymentProcessor.processPayments(client, bytes("")); + } + + /* Test invalid recipient + ├── Given a payment order with address(0) recipient + │ └── When attempting to process payment + │ └── Then it should revert with InvalidRecipient + */ + function testFuzz_PublicProcessPayments_revertsGivenInvalidRecipient( + uint testAmount + ) public { + vm.assume(testAmount > 0 && testAmount < MINTED_SUPPLY); // Keeping within our minted balance + + _setupSinglePayment(address(0), testAmount); + // Get the client interface + IERC20PaymentClientBase_v1 client = + IERC20PaymentClientBase_v1(address(paymentClient)); + + // Process payments + vm.expectRevert( + ICrossChainBase_v1.Module__CrossChainBase__InvalidRecipient.selector + ); + paymentProcessor.processPayments(client, executionData); + } + + /* Test invalid amount + ├── Given a payment order with zero amount + │ └── When attempting to process payment + │ └── Then it should revert with InvalidAmount + */ + function testFuzz_PublicProcessPayments_revertsGivenInvalidAmount( + address testRecipient + ) public { + vm.assume(testRecipient != address(0)); + + _setupSinglePayment(testRecipient, 0); + // Get the client interface + IERC20PaymentClientBase_v1 client = + IERC20PaymentClientBase_v1(address(paymentClient)); + + // Process payments + vm.expectRevert( + ICrossChainBase_v1.Module__CrossChainBase__InvalidAmount.selector + ); + paymentProcessor.processPayments(client, executionData); + } + + /* Test bridge data storage + ├── Given a valid payment order + │ └── When processing payment + │ └── Then bridge data should not be empty + │ └── And intent ID should be stored correctly + └── And intent status should be ADDED in Everclear spoke + */ + function testFuzz_PublicProcessPayments_worksGivenCorrectBridgeData( + address testRecipient, + uint testAmount + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + _setupSinglePayment(testRecipient, testAmount); + // Get the client interface + IERC20PaymentClientBase_v1 client = + IERC20PaymentClientBase_v1(address(paymentClient)); + // Process payments and verify _bridgeData mapping is updated + paymentProcessor.processPayments(client, executionData); + assertTrue( + keccak256(paymentProcessor.getBridgeData(0)) != keccak256(bytes("")), + "Bridge data should not be empty" + ); + + bytes32 intentId = bytes32(paymentProcessor.getBridgeData(0)); + assertEq( + uint(everclearPaymentMock.status(intentId)), + uint(Mock_EverclearPayment.IntentStatus.ADDED) + ); + } + + /* Test empty bridge data + ── When checking bridge data with no added payments + └── Then it should return empty bytes + */ + function testFuzz_PublicProcessPayments_succeedsGivenEmptyBridgeData() + public + { + IERC20PaymentClientBase_v1 client = + IERC20PaymentClientBase_v1(address(paymentClient)); + // Process payments and verify _bridgeData mapping is updated + paymentProcessor.processPayments(client, executionData); + assertTrue( + keccak256(paymentProcessor.getBridgeData(0)) == keccak256(bytes("")), + "Bridge data should be empty" + ); + } + + /* Test insufficient balance + └── Given payment amount exceeds available balance + └── When attempting to process payment + └── Then it should revert with ERC20InsufficientBalance + */ + function testFuzz_PublicProcessPayments_revertsGivenInsufficientBalance( + address testRecipient, + uint testAmount + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + _setupSinglePayment(testRecipient, testAmount); + + vm.prank(testRecipient); + _token.transfer(address(0xDEAD), testAmount); + assertEq(_token.balanceOf(address(testRecipient)), 0); + + IERC20PaymentClientBase_v1 client = + IERC20PaymentClientBase_v1(address(paymentClient)); + console2.log(address(paymentProcessor)); + + vm.expectRevert( + abi.encodeWithSelector( + IERC20Errors.ERC20InsufficientBalance.selector, + testRecipient, + _token.balanceOf(testRecipient), + testAmount + ) + ); + paymentProcessor.processPayments(client, executionData); + } + + /* Test edge case amounts + └── Given payment processor has exactly required amount + └── When processing payment + └── Then it should process successfully + └── And should emit PaymentProcessed event + └── And should handle exact balance correctly + */ + function testFuzz_PublicProcessPayments_worksGivenEdgeCaseAmounts( + address testRecipient, + uint96 testAmount + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + + // Setup - Clear existing balance + uint currentBalance = _token.balanceOf(address(paymentProcessor)); + if (currentBalance > 0) { + vm.prank(address(paymentProcessor)); + _token.transfer(address(0xDEAD), currentBalance); + } + + // Setup - Mint exact amount needed + _token.mint(address(paymentProcessor), testAmount); + + // Setup - Configure payment + _setupSinglePayment(testRecipient, testAmount); + + // Expectations + vm.expectEmit(true, true, true, true); + emit IPaymentProcessor_v1.PaymentOrderProcessed( + address(paymentClient), + testRecipient, + address(_token), + testAmount, + block.timestamp, + 0, + block.timestamp + 1 days + ); + + // Action - Process payments + paymentProcessor.processPayments( + IERC20PaymentClientBase_v1(address(paymentClient)), executionData + ); + } + + /* Test retry failed transfer + └── Given a failed transfer + └── When retrying with valid execution data + └── Then it should create a new intent + └── And clear the failed transfer record + └── And emit FailedTransferRetried event + */ + function testFuzz_PublicRetryFailedTransfer_succeedsGivenValidFailedTransfer( + address testRecipient, + uint testAmount + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + + // Setup initial payment + IERC20PaymentClientBase_v1.PaymentOrder[] memory orders = + _setupSinglePayment(testRecipient, testAmount); + + // Store the initial execution data that will fail + bytes memory failingExecutionData = abi.encode(333, 1); // maxFee of 333 will cause failure + // First attempt with high maxFee to force failure + paymentProcessor.processPayments( + IERC20PaymentClientBase_v1(address(paymentClient)), + failingExecutionData + ); + + // Verify failed transfer was recorded with the failing execution data + assertEq( + paymentProcessor.failedTransfers( + address(paymentClient), + testRecipient, + failingExecutionData // Use the same execution data that was used in processPayments + ), + testAmount + ); + + // Now retry with proper execution data + vm.prank(address(paymentClient)); + paymentProcessor.retryFailedTransfer( + address(paymentClient), + testRecipient, + failingExecutionData, // Old execution data that failed + executionData, // New execution data for retry + orders[0] + ); + + // Verify: + // 1. Failed transfer record was cleared + assertEq( + paymentProcessor.failedTransfers( + address(paymentClient), + testRecipient, + failingExecutionData // Check using the original failing execution data + ), + 0 + ); + + // 2. New intent was created (should be non-zero) + bytes32 newIntentId = paymentProcessor.processedIntentId( + address(paymentClient), testRecipient + ); + assertTrue(newIntentId != bytes32(0)); + } + + /* Test cancel transfer + └── Given a pending transfer + └── When cancelled by the recipient + └── Then it should clear the intent + └── And return funds to recipient + └── And emit TransferCancelled event + */ + function testFuzz_PublicCancelTransfer_succeedsGivenValidPendingTransfer( + address testRecipient, + uint testAmount + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + + // Setup the payment and process it + IERC20PaymentClientBase_v1.PaymentOrder[] memory orders = + _setupSinglePayment(testRecipient, testAmount); + + uint balanceBefore = _token.balanceOf(address(paymentProcessor)); + bytes memory executionData = abi.encode(333, 1); + //call processPayments with maxFee = 333 + paymentProcessor.processPayments( + IERC20PaymentClientBase_v1(address(paymentClient)), executionData + ); + uint balanceAfter = _token.balanceOf(address(paymentProcessor)); + assertEq(balanceAfter, balanceBefore + testAmount); + + // see if failed failedTransfers updates + assertEq( + paymentProcessor.failedTransfers( + address(paymentClient), testRecipient, executionData + ), + orders[0].amount + ); + + uint failedAmount = paymentProcessor.failedTransfers( + address(paymentClient), testRecipient, executionData + ); + assertEq(failedAmount, orders[0].amount); + + uint balanceBeforeCancel = _token.balanceOf(address(paymentProcessor)); + // Cancel as recipient + vm.prank(address(paymentClient)); + paymentProcessor.cancelTransfer( + address(paymentClient), testRecipient, executionData, orders[0] + ); + uint balanceAfterCancel = _token.balanceOf(address(paymentProcessor)); + + assertEq(balanceAfterCancel, balanceBeforeCancel - testAmount); + + // Verify intentId was cleared + assertEq( + paymentProcessor.processedIntentId( + address(paymentClient), testRecipient + ), + bytes32(0) + ); + } + + /* Test cancel transfer by non-recipient + └── Given a pending transfer + └── When cancelled by someone other than recipient + └── Then it should revert with InvalidAddress + */ + function testFuzz_PublicCancelTransfer_revertsGivenNonRecipientCaller( + address testRecipient, + address nonRecipient, + uint testAmount + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + _setupSinglePayment(testRecipient, testAmount); + + // Process payment to create intent + paymentProcessor.processPayments( + IERC20PaymentClientBase_v1(address(paymentClient)), executionData + ); + + bytes32 pendingIntentId = paymentProcessor.processedIntentId( + address(paymentClient), testRecipient + ); + + // Create payment order for cancellation + IERC20PaymentClientBase_v1.PaymentOrder memory order = + IERC20PaymentClientBase_v1.PaymentOrder({ + recipient: testRecipient, + paymentToken: address(_token), + amount: testAmount, + start: block.timestamp, + cliff: 0, + end: block.timestamp + 1 days + }); + + // Prank as non-recipient + console2.log(address(paymentClient)); + vm.prank(nonRecipient); + vm.expectRevert(IModule_v1.Module__InvalidAddress.selector); + paymentProcessor.cancelTransfer( + address(paymentClient), testRecipient, executionData, order + ); + } + + /* Test cancel transfer after processing + └── Given a successfully processed payment + └── When attempting to cancel the transfer + └── Then it should revert with InvalidAmount + └── And the intent ID should remain unchanged + └── And the payment order should remain processed + */ + function testFuzz_PublicCancelTransfer_revertsGivenProcessedTransfer( + address testRecipient, + uint testAmount + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + + // Setup initial payment and process it + IERC20PaymentClientBase_v1.PaymentOrder[] memory orders = + _setupSinglePayment(testRecipient, testAmount); + + paymentProcessor.processPayments( + IERC20PaymentClientBase_v1(address(paymentClient)), executionData + ); + + // Cancel the transfer + vm.prank(address(paymentClient)); + vm.expectRevert( + ICrossChainBase_v1.Module__CrossChainBase__InvalidAmount.selector + ); + paymentProcessor.cancelTransfer( + address(paymentClient), testRecipient, executionData, orders[0] + ); + } + + /* Test TTL validation + └── Given execution data with zero TTL + └── When processing payments + └── Then it should revert with InvalidTTL + */ + function testFuzz_ProcessPayments_revertsWithZeroTTL( + address testRecipient, + uint testAmount + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + + _setupSinglePayment(testRecipient, testAmount); + + bytes memory zeroTTLData = abi.encode(maxFee, 0); + vm.expectRevert(); + paymentProcessor.processPayments( + IERC20PaymentClientBase_v1(address(paymentClient)), zeroTTLData + ); + } + + /* Test retry with invalid client + └── Given a retry request from non-client address + └── When retrying failed transfer + └── Then it should revert with InvalidAddress + */ + function testFuzz_PublicRetryFailedTransfer_revertsGivenInvalidCaller( + address testRecipient, + uint testAmount, + address invalidCaller + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + vm.assume(invalidCaller != address(paymentClient)); + + // Setup failed transfer + IERC20PaymentClientBase_v1.PaymentOrder[] memory orders = + _setupSinglePayment(testRecipient, testAmount); + + bytes memory failingExecutionData = abi.encode(333, 1); + paymentProcessor.processPayments( + IERC20PaymentClientBase_v1(address(paymentClient)), + failingExecutionData + ); + + // Attempt retry from invalid caller + vm.prank(invalidCaller); + vm.expectRevert(IModule_v1.Module__InvalidAddress.selector); + paymentProcessor.retryFailedTransfer( + address(paymentClient), + testRecipient, + failingExecutionData, + executionData, + orders[0] + ); + } + + /* Test retry with no failed transfer record + └── Given a retry request for non-existent failed transfer + └── When retrying transfer + └── Then it should revert with InvalidAmount + */ + function testFuzz_PublicRetryFailedTransfer_revertsGivenNoFailedTransfer( + address testRecipient, + uint testAmount + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + + IERC20PaymentClientBase_v1.PaymentOrder[] memory orders = + _setupSinglePayment(testRecipient, testAmount); + + vm.prank(address(paymentClient)); + vm.expectRevert( + ICrossChainBase_v1.Module__CrossChainBase__InvalidAmount.selector + ); + paymentProcessor.retryFailedTransfer( + address(paymentClient), + testRecipient, + executionData, + executionData, + orders[0] + ); + } + + /* Test retry with existing intent + └── Given a retry request when intent already exists + └── When retrying transfer + └── Then it should revert with InvalidIntentId + */ + function testFuzz_PublicRetryFailedTransfer_revertsGivenExistingIntent( + address testRecipient, + uint testAmount + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + + // Setup initial payment and process it + IERC20PaymentClientBase_v1.PaymentOrder[] memory orders = + _setupSinglePayment(testRecipient, testAmount); + + // Create a successful intent first + paymentProcessor.processPayments( + IERC20PaymentClientBase_v1(address(paymentClient)), executionData + ); + + // Now try to retry (should fail because intent exists) + vm.prank(address(paymentClient)); + vm.expectRevert(); + paymentProcessor.retryFailedTransfer( + address(paymentClient), + testRecipient, + executionData, + executionData, + orders[0] + ); + } + + /* Test retry with invalid execution data + └── Given a retry request with invalid execution data + └── When retrying failed transfer + └── Then it should revert with InvalidExecutionData + */ + function testFuzz_PublicRetryFailedTransfer_revertsGivenInvalidExecutionData( + address testRecipient, + uint testAmount + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + + // Setup failed transfer + IERC20PaymentClientBase_v1.PaymentOrder[] memory orders = + _setupSinglePayment(testRecipient, testAmount); + + bytes memory failingExecutionData = abi.encode(333, 1); + paymentProcessor.processPayments( + IERC20PaymentClientBase_v1(address(paymentClient)), + failingExecutionData + ); + + vm.prank(address(paymentClient)); + vm.expectRevert( + abi.encodeWithSelector( + IPP_Crosschain_v1 + .Module__PP_Crosschain__MessageDeliveryFailed + .selector, + 8453, + 8453, + failingExecutionData + ) + ); + paymentProcessor.retryFailedTransfer( + address(paymentClient), + testRecipient, + failingExecutionData, + failingExecutionData, // use failedExecutionData as newExecutionData + orders[0] + ); + } + + /* Test process payments without token approval + └── Given a payment order with zero approval + └── When attempting to process payment + └── Then it should revert with InvalidTokenApproval + */ + function testFuzz_PublicProcessPayments_revertsGivenNoTokenApproval( + address testRecipient, + uint testAmount + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + _setupSinglePayment(testRecipient, testAmount); + + // Reset approval + vm.prank(testRecipient); + _token.approve(address(paymentProcessor), 0); + + // Expect revert for insufficient allowance + console2.log(_token.balanceOf(address(testRecipient))); + vm.expectRevert( + abi.encodeWithSelector( + IERC20Errors.ERC20InsufficientAllowance.selector, + address(paymentProcessor), + 0, + testAmount + ) + ); + paymentProcessor.processPayments( + IERC20PaymentClientBase_v1(address(paymentClient)), executionData + ); + } + + /* Test payment processing with unsupported token + └── Given a payment order with an unsupported token + └── When attempting to process payment + └── Then it should revert with UnsupportedToken + */ + function testFuzz_PublicProcessPayments_revertsGivenUnsupportedToken( + address testRecipient, + uint testAmount + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + + // Setup payment with unsupported token + IERC20PaymentClientBase_v1.PaymentOrder memory order = + IERC20PaymentClientBase_v1.PaymentOrder({ + recipient: testRecipient, + paymentToken: address(0xDEADBEEF), // Unsupported token address + amount: testAmount, + start: block.timestamp, + cliff: 0, + end: block.timestamp + 1 days + }); + + paymentClient.addPaymentOrder(order); + + // Expect revert due to unsupported token + vm.expectRevert(); + paymentProcessor.processPayments( + IERC20PaymentClientBase_v1(address(paymentClient)), executionData + ); + } + + /* Test payment processing with zero token balance + └── Given payment processor has zero token balance + └── When processing payments + └── Then it should revert with ERC20InsufficientBalance + */ + function testFuzz_PublicProcessPayments_revertsGivenZeroBalance( + address testRecipient + ) public { + vm.assume(testRecipient != address(0)); + + _setupSinglePayment(testRecipient, ZERO_AMOUNT); + assertEq(_token.balanceOf(address(testRecipient)), ZERO_AMOUNT); + console2.log(_token.balanceOf(address(testRecipient))); + vm.expectRevert( + ICrossChainBase_v1.Module__CrossChainBase__InvalidAmount.selector + ); + paymentProcessor.processPayments( + IERC20PaymentClientBase_v1(address(paymentClient)), executionData + ); + } + + /* Test payment processing with duplicate recipients + └── Given payment orders with duplicate recipients + └── When processing payments + └── Then it should handle duplicates correctly + └── And update intent IDs properly + └── And track total amounts correctly + */ + function testFuzz_PublicProcessPayments_succeedsGivenDuplicateRecipients( + address testRecipient, + uint testAmount + ) public { + vm.assume(testRecipient != address(0)); + + // Create multiple orders for same recipient + address[] memory recipients = new address[](3); + uint[] memory amounts = new uint[](3); + + for (uint i = 0; i < 3; i++) { + recipients[i] = testRecipient; + vm.assume(testAmount > 0 && testAmount <= MINTED_SUPPLY); + amounts[i] = testAmount; + } + + IERC20PaymentClientBase_v1.PaymentOrder[] memory orders = + _createPaymentOrders(3, recipients, amounts); + + vm.prank(testRecipient); + _token.approve(address(paymentProcessor), type(uint).max); + + // Process payments + paymentProcessor.processPayments( + IERC20PaymentClientBase_v1(address(paymentClient)), executionData + ); + + // Verify final intent ID exists + bytes32 finalIntentId = paymentProcessor.processedIntentId( + address(paymentClient), testRecipient + ); + assertTrue(finalIntentId != bytes32(0)); + } + + /* Test payment processing with varying start/end times + └── Given payment orders with different time configurations + └── When processing payments + └── Then it should handle valid time ranges + └── And revert for invalid ones + */ + function testFuzz_PublicProcessPayments_succeedsGivenVariableTimeRanges( + address testRecipient, + uint testAmount, + uint32 timeOffset + ) public { + _assumeValidRecipientAndAmount(testRecipient, testAmount); + vm.assume(timeOffset > 0 && timeOffset < 30 days); + + // Create payment order with fuzzed time range + IERC20PaymentClientBase_v1.PaymentOrder memory order = + IERC20PaymentClientBase_v1.PaymentOrder({ + recipient: testRecipient, + paymentToken: address(_token), + amount: testAmount, + start: block.timestamp, + cliff: 0, + end: block.timestamp + timeOffset + }); + + _token.mint(testRecipient, testAmount); + vm.prank(testRecipient); + _token.approve(address(paymentProcessor), testAmount); + paymentClient.addPaymentOrder(order); + + paymentProcessor.processPayments( + IERC20PaymentClientBase_v1(address(paymentClient)), executionData + ); + + bytes32 intentId = paymentProcessor.processedIntentId( + address(paymentClient), testRecipient + ); + assertTrue(intentId != bytes32(0)); + } + + function testFuzz_PublicProcessPayments_succeedsGivenExpiredEndDate( + address testRecipient, + uint testAmount + ) public { + //@audit-issue -> discuss with 33 about this test + _assumeValidRecipientAndAmount(testRecipient, testAmount); + + // Setup payment with expired end date + IERC20PaymentClientBase_v1.PaymentOrder memory order = + IERC20PaymentClientBase_v1.PaymentOrder({ + recipient: testRecipient, + paymentToken: address(_token), + amount: testAmount, + start: block.timestamp - 2 days, + cliff: 0, //@note 33audits -> shouldnt this revert since start and end time are in the past? + end: block.timestamp - 1 days // End date in the past + }); + + // Mint tokens to recipient + _token.mint(testRecipient, testAmount); + vm.prank(testRecipient); + _token.approve(address(paymentProcessor), testAmount); + + paymentClient.addPaymentOrder(order); + + // Process payments + uint balanceBefore = _token.balanceOf(address(paymentProcessor)); + paymentProcessor.processPayments( + IERC20PaymentClientBase_v1(address(paymentClient)), executionData + ); + assertEq(_token.balanceOf(testRecipient), 0); + assertEq( + _token.balanceOf(address(paymentProcessor)), + balanceBefore + testAmount + ); + } + + //-------------------------------------------------------------------------- + // Payment Order Validation Tests + + /* Test invalid recipient + └── Given a payment order with address(0) recipient + └── When validating the payment order + └── Then it should return false + */ + function testPublicValidPaymentOrder_revertsGivenInvalidRecipient() + public + { + IERC20PaymentClientBase_v1.PaymentOrder memory order = + IERC20PaymentClientBase_v1.PaymentOrder({ + recipient: address(0), + paymentToken: address(_token), + amount: 1, + start: block.timestamp, + cliff: 0, + end: block.timestamp + 1 days + }); + assertEq(paymentProcessor.validPaymentOrder(order), false); + } + + /* Test invalid token + └── Given a payment order with address(0) token + └── When validating the payment order + └── Then it should return false + */ + function testFuzz_validPaymentOrder_InvatestPublicValidPaymentOrder_revertsGivenInvalidTokenlidToken( + ) public { + IERC20PaymentClientBase_v1.PaymentOrder memory order = + IERC20PaymentClientBase_v1.PaymentOrder({ + recipient: address(0xBEEF), + paymentToken: address(0), + amount: 1, + start: block.timestamp, + cliff: 0, + end: block.timestamp + 1 days + }); + assertEq(paymentProcessor.validPaymentOrder(order), false); + } + + /* Test invalid amount + └── Given a payment order with zero amount + └── When validating the payment order + └── Then it should return false + */ + function testPublicValidPaymentOrder_revertsGivenInvalidAmount() public { + IERC20PaymentClientBase_v1.PaymentOrder memory order = + IERC20PaymentClientBase_v1.PaymentOrder({ + recipient: address(0xBEEF), + paymentToken: address(_token), + amount: 0, + start: block.timestamp, + cliff: 0, + end: block.timestamp + 1 days + }); + assertEq(paymentProcessor.validPaymentOrder(order), false); + } + + /* Test invalid start + └── Given a payment order with start time after end time + └── When validating the payment order + └── Then it should return false + */ + function testPublicValidPaymentOrder_revertsGivenInvalidStart() public { + IERC20PaymentClientBase_v1.PaymentOrder memory order = + IERC20PaymentClientBase_v1.PaymentOrder({ + recipient: address(0xBEEF), + paymentToken: address(_token), + amount: 1, + start: block.timestamp + 1 days, + cliff: 0, + end: block.timestamp + }); + assertEq(paymentProcessor.validPaymentOrder(order), false); + } + + /* Test invalid cliff + └── Given a payment order with cliff time after end time + └── When validating the payment order + └── Then it should return false + */ + function testPublicValidPaymentOrder_revertsGivenInvalidCliff() public { + IERC20PaymentClientBase_v1.PaymentOrder memory order = + IERC20PaymentClientBase_v1.PaymentOrder({ + recipient: address(0xBEEF), + paymentToken: address(_token), + amount: 1, + start: block.timestamp, + cliff: 1 days, + end: block.timestamp - 1 days + }); + assertEq(paymentProcessor.validPaymentOrder(order), false); + } + + /* Test valid payment order + └── Given a valid payment order + └── When validating the payment order + └── Then it should return true + */ + function testPublicValidPaymentOrder_succeeds() public { + IERC20PaymentClientBase_v1.PaymentOrder memory order = + IERC20PaymentClientBase_v1.PaymentOrder({ + recipient: address(0xBEEF), + paymentToken: address(_token), + amount: 1, + start: block.timestamp, + cliff: 0, + end: block.timestamp + 1 days + }); + assertEq(paymentProcessor.validPaymentOrder(order), true); + } + + //-------------------------------------------------------------------------- + // Helper Functions + + function _setupSinglePayment(address _recipient, uint _amount) + internal + returns (IERC20PaymentClientBase_v1.PaymentOrder[] memory) + { + address[] memory setupRecipients = new address[](1); + setupRecipients[0] = _recipient; + uint[] memory setupAmounts = new uint[](1); + setupAmounts[0] = _amount; + + IERC20PaymentClientBase_v1.PaymentOrder[] memory orders = + _createPaymentOrders(1, setupRecipients, setupAmounts); + return orders; + } + + function _createPaymentOrders( + uint orderCount, + address[] memory recipients, + uint[] memory amounts + ) internal returns (IERC20PaymentClientBase_v1.PaymentOrder[] memory) { + // Sanity checks for array lengths + require( + recipients.length == orderCount && amounts.length == orderCount, + "Array lengths must match orderCount" + ); + IERC20PaymentClientBase_v1.PaymentOrder[] memory orders = + new IERC20PaymentClientBase_v1.PaymentOrder[](orderCount); + + for (uint i = 0; i < orderCount; i++) { + if (recipients[i] != address(0)) { + //mint tokens to recipient & approve payment processor + _token.mint(recipients[i], amounts[i]); + vm.prank(recipients[i]); + _token.approve(address(paymentProcessor), amounts[i]); + } + orders[i] = IERC20PaymentClientBase_v1.PaymentOrder({ + recipient: recipients[i], + paymentToken: address(_token), + amount: amounts[i], + start: block.timestamp, + cliff: 0, + end: block.timestamp + 1 days + }); + //add payment order to client + paymentClient.addPaymentOrder(orders[i]); + } + return orders; + } + + function _setupInitialBalances() internal { + // Setup token approvals and initial balances + _token.mint(address(this), MINTED_SUPPLY); + _token.approve(address(paymentProcessor), type(uint).max); + + _token.mint(address(paymentProcessor), MINTED_SUPPLY); // Mint _tokens to processor + vm.prank(address(paymentProcessor)); + _token.approve(address(paymentProcessor), type(uint).max); // Processor approves bridge logic + } + + function _assumeValidRecipientAndAmount( + address testRecipient, + uint testAmount + ) internal pure { + vm.assume(testRecipient != address(0)); + vm.assume(testAmount > 0 && testAmount < MINTED_SUPPLY); + } +} diff --git a/test/modules/paymentProcessor/abstracts/CrossChainBase_v1.t.sol b/test/modules/paymentProcessor/abstracts/CrossChainBase_v1.t.sol new file mode 100644 index 000000000..1ec488022 --- /dev/null +++ b/test/modules/paymentProcessor/abstracts/CrossChainBase_v1.t.sol @@ -0,0 +1,174 @@ +//SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +// Internal +import { + ModuleTest, + IModule_v1, + IOrchestrator_v1 +} from "test/modules/ModuleTest.sol"; +import {OZErrors} from "test/utils/errors/OZErrors.sol"; +import {ICrossChainBase_v1} from + "src/modules/paymentProcessor/interfaces/ICrosschainBase_v1.sol"; +import {CrossChainBase_v1} from + "src/modules/paymentProcessor/abstracts/CrossChainBase_v1.sol"; +import { + IERC20PaymentClientBase_v1, + ERC20PaymentClientBaseV1Mock, + ERC20Mock +} from "test/utils/mocks/modules/paymentClient/ERC20PaymentClientBaseV1Mock.sol"; +import {CrossChainBase_v1_Exposed} from + "test/utils/mocks/modules/paymentProcessor/CrossChainBase_v1_Exposed.sol"; + +import {IPaymentProcessor_v1} from + "src/orchestrator/interfaces/IOrchestrator_v1.sol"; +import {ICrossChainBase_v1} from + "src/modules/paymentProcessor/interfaces/ICrosschainBase_v1.sol"; +//External Dependencies +import {OZErrors} from "test/utils/errors/OZErrors.sol"; +import {Clones} from "@oz/proxy/Clones.sol"; + +contract CrossChainBase_v1_Test is ModuleTest { + //-------------------------------------------------------------------------- + //Constants + //-------------------------------------------------------------------------- + //Mocks + ERC20PaymentClientBaseV1Mock paymentClient; + CrossChainBase_v1_Exposed public crossChainBase; + + //-------------------------------------------------------------------------- + //Setup + function setUp() public { + //This function is used to setup the unit test + //Deploy the SuT + address impl = address(new CrossChainBase_v1_Exposed()); + crossChainBase = CrossChainBase_v1_Exposed(Clones.clone(impl)); + + //Setup the module to test + _setUpOrchestrator(crossChainBase); + + //General setup for other contracts in the workflow + _authorizer.setIsAuthorized(address(this), true); + + //Initiate the PP with the medata and config data + crossChainBase.init(_orchestrator, _METADATA, abi.encode(1)); + + //Setup other modules needed in the unit tests. + //In this case a payment client is needed to test the PP_Template_v1. + impl = address(new ERC20PaymentClientBaseV1Mock()); + paymentClient = ERC20PaymentClientBaseV1Mock(Clones.clone(impl)); + //Adding the payment client is done through a timelock mechanism + _orchestrator.initiateAddModuleWithTimelock(address(paymentClient)); + vm.warp(block.timestamp + _orchestrator.MODULE_UPDATE_TIMELOCK()); + _orchestrator.executeAddModule(address(paymentClient)); + //Init payment client + paymentClient.init(_orchestrator, _METADATA, bytes("")); + paymentClient.setIsAuthorized(address(crossChainBase), true); + paymentClient.setToken(_token); + } + //-------------------------------------------------------------------------- + //Test: Initialization + /* + └── Given the contract is not initialized + └── When initializing the contract + └── Then it should set the correct orchestrator address */ + + function testInit() public override(ModuleTest) { + assertEq(address(crossChainBase.orchestrator()), address(_orchestrator)); + } + + //-------------------------------------------------------------------------- + //Test: Interface Support + /* + └── Given the contract is initialized + └── When checking for ICrossChainBase_v1 interface support + └── Then it should return true + └── When checking for an unknown interface + └── Then it should return false */ + function testSupportsInterface() public { + // Test for ICrossChainBase_v1 interface support + bytes4 interfaceId = type(ICrossChainBase_v1).interfaceId; + assertTrue(crossChainBase.supportsInterface(interfaceId)); + } + + /* + └── Given the contract is already initialized + └── When trying to reinitialize + └── Then it should revert with Initializable__InvalidInitialization */ + function testReinitFails() public override(ModuleTest) { + vm.expectRevert(OZErrors.Initializable__InvalidInitialization); + crossChainBase.init(_orchestrator, _METADATA, abi.encode(1)); + } + + /** + * @dev Test interface support failure case + * └── Given the contract is initialized + * └── When checking for an unknown interface + * └── Then it should return false + */ + function testSupportsInterface_failsGivenUnknownInterface() public { + bytes4 randomInterfaceId = bytes4(keccak256("random()")); + assertFalse(crossChainBase.supportsInterface(randomInterfaceId)); + } + + //-------------------------------------------------------------------------- + //Test: executeBridgeTransfer + + /** + * @dev Test bridge transfer with empty payment order + * └── Given an empty payment order is created + * └── When executeBridgeTransfer is called + * └── Then it should return empty bytes + */ + function testExecuteBridgeTransfer_succeedsGivenEmptyPaymentOrder() + public + { + address[] memory setupRecipients = new address[](1); + setupRecipients[0] = address(1); + uint[] memory setupAmounts = new uint[](1); + setupAmounts[0] = 100 ether; + + IERC20PaymentClientBase_v1.PaymentOrder[] memory orders = + _createPaymentOrders(1, setupRecipients, setupAmounts); + paymentClient.addPaymentOrders(orders); + + bytes memory executionData = abi.encode(0, 0); //maxFee and ttl setup + + bytes memory result = crossChainBase.exposed_executeBridgeTransfer( + orders[0], executionData + ); + assertEq(result, bytes("")); + } + + //-------------------------------------------------------------------------- + //Helper Functions + + function _createPaymentOrders( + uint orderCount, + address[] memory recipients, + uint[] memory amounts + ) + internal + view + returns (IERC20PaymentClientBase_v1.PaymentOrder[] memory) + { + // Sanity checks for array lengths + require( + recipients.length == orderCount && amounts.length == orderCount, + "Array lengths must match orderCount" + ); + IERC20PaymentClientBase_v1.PaymentOrder[] memory orders = + new IERC20PaymentClientBase_v1.PaymentOrder[](orderCount); + for (uint i = 0; i < orderCount; i++) { + orders[i] = IERC20PaymentClientBase_v1.PaymentOrder({ + recipient: recipients[i], + paymentToken: address(0xabcd), + amount: amounts[i], + start: block.timestamp, + cliff: 0, + end: block.timestamp + 1 days + }); + } + return orders; + } +} diff --git a/test/modules/paymentProcessor/abstracts/CrosschainBase_v1.t.sol b/test/modules/paymentProcessor/abstracts/CrosschainBase_v1.t.sol index 550e0119f..a4cd37699 100644 --- a/test/modules/paymentProcessor/abstracts/CrosschainBase_v1.t.sol +++ b/test/modules/paymentProcessor/abstracts/CrosschainBase_v1.t.sol @@ -1,7 +1,7 @@ //SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.0; -//Internal Dependencies +// Internal import { ModuleTest, IModule_v1, @@ -10,77 +10,153 @@ import { import {OZErrors} from "test/utils/errors/OZErrors.sol"; import {ICrossChainBase_v1} from "src/modules/paymentProcessor/interfaces/ICrosschainBase_v1.sol"; -import {CrosschainBase_v1} from - "src/modules/paymentProcessor/abstracts/CrosschainBase_v1.sol"; -//External Dependencies -import {Clones} from "@oz/proxy/Clones.sol"; - -//Tests and Mocks -// import cr +import {CrossChainBase_v1} from + "src/modules/paymentProcessor/abstracts/CrossChainBase_v1.sol"; import { IERC20PaymentClientBase_v1, ERC20PaymentClientBaseV1Mock, ERC20Mock } from "test/utils/mocks/modules/paymentClient/ERC20PaymentClientBaseV1Mock.sol"; -//import exposed -import {CrosschainBase_v1_Exposed} from "./CrosschainBase_v1_Exposed.sol"; - -//System under test (SuT) -// import { -// IPP_CrossChain_v1, -// PP_CrossChain_v1, -// IPaymentProcessor_v1 -// } from "src/templates/modules/PP_Template_v1.sol"; +import {CrossChainBase_v1_Exposed} from + "test/utils/mocks/modules/paymentProcessor/CrossChainBase_v1_Exposed.sol"; + import {IPaymentProcessor_v1} from "src/orchestrator/interfaces/IOrchestrator_v1.sol"; import {ICrossChainBase_v1} from "src/modules/paymentProcessor/interfaces/ICrosschainBase_v1.sol"; +//External Dependencies +import {OZErrors} from "test/utils/errors/OZErrors.sol"; +import {Clones} from "@oz/proxy/Clones.sol"; -/** - * @title Inverter Template Payment Processor - * - * @notice Basic template payment processor used to showcase the unit testing setup - * - * @dev Not all functions are tested in this template. Placeholders of the functions that are not tested are added - * into the contract. This test showcases the following: - * - Inherit from the ModuleTest contract to enable interaction with the Inverter workflow. - * - Showcases the setup of the workflow, uses in test unit tests. - * - Pre-defined layout for all setup and functions to be tested. - * - Shows the use of Gherkin for documenting the testing. VS Code extension used for formatting is recommended. - * - Shows the use of the modifierInPlace pattern to test the modifier placement. - * - * @custom:security-contact security@inverter.network - * In case of any concerns or findings, please refer to our Security Policy - * at security.inverter.network or email us directly! - * - * @author Inverter Network - */ -contract CrosschainBase_v1_Test is ModuleTest { +contract CrossChainBase_v1_Test is ModuleTest { //-------------------------------------------------------------------------- //Constants //-------------------------------------------------------------------------- - //State - //Mocks ERC20PaymentClientBaseV1Mock paymentClient; - - //System under test (SuT) - CrosschainBase_v1 public paymentProcessor; - //PP_CrossChain_v1_Exposed public paymentProcessor; + CrossChainBase_v1_Exposed public crossChainBase; //-------------------------------------------------------------------------- //Setup - function setUp() public {} + function setUp() public { + //This function is used to setup the unit test + //Deploy the SuT + address impl = address(new CrossChainBase_v1_Exposed()); + crossChainBase = CrossChainBase_v1_Exposed(Clones.clone(impl)); + + //Setup the module to test + _setUpOrchestrator(crossChainBase); + + //General setup for other contracts in the workflow + _authorizer.setIsAuthorized(address(this), true); + //Initiate the PP with the medata and config data + crossChainBase.init(_orchestrator, _METADATA, abi.encode(1)); + + //Setup other modules needed in the unit tests. + //In this case a payment client is needed to test the PP_Template_v1. + impl = address(new ERC20PaymentClientBaseV1Mock()); + paymentClient = ERC20PaymentClientBaseV1Mock(Clones.clone(impl)); + //Adding the payment client is done through a timelock mechanism + _orchestrator.initiateAddModuleWithTimelock(address(paymentClient)); + vm.warp(block.timestamp + _orchestrator.MODULE_UPDATE_TIMELOCK()); + _orchestrator.executeAddModule(address(paymentClient)); + //Init payment client + paymentClient.init(_orchestrator, _METADATA, bytes("")); + paymentClient.setIsAuthorized(address(crossChainBase), true); + paymentClient.setToken(_token); + } //-------------------------------------------------------------------------- //Test: Initialization + /* + └── Given the contract is not initialized + └── When initializing the contract + └── Then it should set the correct orchestrator address */ + + function testInit() public override(ModuleTest) { + assertEq(address(crossChainBase.orchestrator()), address(_orchestrator)); + } + + //-------------------------------------------------------------------------- + //Test: Interface Support + /* + └── Given the contract is initialized + └── When checking for ICrossChainBase_v1 interface support + └── Then it should return true + └── When checking for an unknown interface + └── Then it should return false */ + function testSupportsInterface() public { + // Test for ICrossChainBase_v1 interface support + bytes4 interfaceId = type(ICrossChainBase_v1).interfaceId; + assertTrue(crossChainBase.supportsInterface(interfaceId)); + } + + function testSupportsInterface_revertsGivenUnknownInterface() public { + bytes4 randomInterfaceId = bytes4(keccak256("random()")); + assertFalse(crossChainBase.supportsInterface(randomInterfaceId)); + } - //Test if the orchestrator is correctly set - function testInit() public override(ModuleTest) {} + /* + └── Given the contract is already initialized + └── When trying to reinitialize + └── Then it should revert with Initializable__InvalidInitialization */ + function testReinitFails() public override(ModuleTest) { + vm.expectRevert(OZErrors.Initializable__InvalidInitialization); + crossChainBase.init(_orchestrator, _METADATA, abi.encode(1)); + } + //-------------------------------------------------------------------------- + //Test: executeBridgeTransfer + + /* + └── Given an empty payment order is created + └── When executeBridgeTransfer is called + └── Then it should return empty bytes */ + function testExecuteBridgeTransfer_worksGivenEmptyPaymentOrder() public { + address[] memory setupRecipients = new address[](1); + setupRecipients[0] = address(1); + uint[] memory setupAmounts = new uint[](1); + setupAmounts[0] = 100 ether; + + IERC20PaymentClientBase_v1.PaymentOrder[] memory orders = + _createPaymentOrders(1, setupRecipients, setupAmounts); + paymentClient.addPaymentOrders(orders); - //Test the interface support - function testSupportsInterface() public {} + bytes memory executionData = abi.encode(0, 0); //maxFee and ttl setup + + bytes memory result = crossChainBase.exposed_executeBridgeTransfer( + orders[0], executionData + ); + assertEq(result, bytes("")); + } + //-------------------------------------------------------------------------- + //Helper Functions - //Test the reinit function - function testReinitFails() public override(ModuleTest) {} + function _createPaymentOrders( + uint orderCount, + address[] memory recipients, + uint[] memory amounts + ) + internal + view + returns (IERC20PaymentClientBase_v1.PaymentOrder[] memory) + { + // Sanity checks for array lengths + require( + recipients.length == orderCount && amounts.length == orderCount, + "Array lengths must match orderCount" + ); + IERC20PaymentClientBase_v1.PaymentOrder[] memory orders = + new IERC20PaymentClientBase_v1.PaymentOrder[](orderCount); + for (uint i = 0; i < orderCount; i++) { + orders[i] = IERC20PaymentClientBase_v1.PaymentOrder({ + recipient: recipients[i], + paymentToken: address(0xabcd), + amount: amounts[i], + start: block.timestamp, + cliff: 0, + end: block.timestamp + 1 days + }); + } + return orders; + } } diff --git a/test/modules/paymentProcessor/interfaces/IEverClearSpoke.sol b/test/modules/paymentProcessor/interfaces/IEverClearSpoke.sol deleted file mode 100644 index 716829a8c..000000000 --- a/test/modules/paymentProcessor/interfaces/IEverClearSpoke.sol +++ /dev/null @@ -1,116 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -/** - * @title IEverclearSpoke - * @notice Interface for the Everclear spoke contract - */ -interface IEverclearSpoke { - /*/////////////////////////////////////////////////////////////// - STRUCTS - //////////////////////////////////////////////////////////////*/ - struct Intent { - bytes32 initiator; - bytes32 receiver; - bytes32 inputAsset; - bytes32 outputAsset; - uint amount; - uint24 maxFee; - uint32 origin; - uint32[] destinations; - uint nonce; - uint48 timestamp; - uint48 ttl; - bytes data; - } - - struct FillMessage { - bytes32 intentId; - bytes32 initiator; - bytes32 solver; - uint48 executionTimestamp; - uint24 fee; - } - - struct Permit2Params { - uint nonce; - uint deadline; - bytes signature; - } - - struct SpokeInitializationParams { - address gateway; - address messageReceiver; - address lighthouse; - address watchtower; - address callExecutor; - uint32 hubDomain; - address owner; - } - - /*/////////////////////////////////////////////////////////////// - FUNCTIONS - //////////////////////////////////////////////////////////////*/ - function initialize(SpokeInitializationParams calldata init) external; - function pause() external; - function unpause() external; - function updateSecurityModule(address newSecurityModule) external; - function newIntent( - uint32[] memory destinations, - address to, - address inputAsset, - address outputAsset, - uint amount, - uint24 maxFee, - uint48 ttl, - bytes calldata data - ) external returns (bytes32 intentId, Intent memory intent); - function newIntent( - uint32[] memory destinations, - address to, - address inputAsset, - address outputAsset, - uint amount, - uint24 maxFee, - uint48 ttl, - bytes calldata data, - Permit2Params calldata permit2Params - ) external returns (bytes32 intentId, Intent memory intent); - function fillIntent(Intent calldata intent, uint24 fee) - external - returns (FillMessage memory fillMessage); - function fillIntentForSolver( - address solver, - Intent calldata intent, - uint nonce, - uint24 fee, - bytes calldata signature - ) external returns (FillMessage memory fillMessage); - function processIntentQueue(Intent[] calldata intents) external payable; - function processFillQueue(uint32 amount) external payable; - function processIntentQueueViaRelayer( - uint32 domain, - Intent[] calldata intents, - address relayer, - uint ttl, - uint nonce, - uint bufferBPS, - bytes calldata signature - ) external; - function processFillQueueViaRelayer( - uint32 domain, - uint32 amount, - address relayer, - uint ttl, - uint nonce, - uint bufferBPS, - bytes calldata signature - ) external; - function deposit(address asset, uint amount) external; - function withdraw(address asset, uint amount) external; - function updateGateway(address newGateway) external; - function updateMessageReceiver(address newMessageReceiver) external; - function authorizeGasReceiver(address receiver, bool authorized) external; - function updateMessageGasLimit(uint newGasLimit) external; - function executeIntentCalldata(Intent calldata intent) external; -} diff --git a/test/utils/mocks/external/Mock_EverclearPayment.sol b/test/utils/mocks/external/Mock_EverclearPayment.sol index fbf4aa0ff..598d32f44 100644 --- a/test/utils/mocks/external/Mock_EverclearPayment.sol +++ b/test/utils/mocks/external/Mock_EverclearPayment.sol @@ -6,12 +6,14 @@ contract Mock_EverclearPayment { uint public nonce; uint32 public DOMAIN; + IntentStatus public nextIntentStatus; mapping(bytes32 => IntentStatus) public status; enum IntentStatus { NONE, ADDED, SETTLED, + FAILED, SETTLED_AND_MANUALLY_EXECUTED } @@ -39,11 +41,14 @@ contract Mock_EverclearPayment { uint24 _maxFee, uint48 _ttl, bytes calldata _data - ) external returns (bytes32 _intentId, Intent memory _intent) { + ) external returns (bytes32 _intentId) { // Increment nonce for each new intent nonce++; - - _intent = Intent({ + if (_maxFee == 333) { + return bytes32(0); + } + //if data is the word "fail" intentional return bytes32(0) + Intent memory _intent = Intent({ initiator: msg.sender, receiver: _to, inputAsset: _inputAsset, @@ -61,10 +66,14 @@ contract Mock_EverclearPayment { // Generate a unique intent ID _intentId = keccak256(abi.encode(_intent)); - // Set intent status to ADDED and emit the event + // // Set intent status to ADDED and emit the event status[_intentId] = IntentStatus.ADDED; emit IntentAdded(_intentId, nonce, _intent); - return (_intentId, _intent); + return (_intentId); + } + + function setNextIntentStatus(IntentStatus _status) external { + nextIntentStatus = _status; } } diff --git a/test/modules/paymentProcessor/abstracts/CrosschainBase_v1_Exposed.sol b/test/utils/mocks/modules/paymentProcessor/CrossChainBase_v1_Exposed.sol similarity index 53% rename from test/modules/paymentProcessor/abstracts/CrosschainBase_v1_Exposed.sol rename to test/utils/mocks/modules/paymentProcessor/CrossChainBase_v1_Exposed.sol index 1a3e49f5f..8eac2dbfb 100644 --- a/test/modules/paymentProcessor/abstracts/CrosschainBase_v1_Exposed.sol +++ b/test/utils/mocks/modules/paymentProcessor/CrossChainBase_v1_Exposed.sol @@ -1,27 +1,37 @@ // SPDX-License-Identifier: LGPL-3.0-only -pragma solidity 0.8.23; // Internal Dependencies -//import {PP_CrossChain_v1} from "src/templates/modules/PP_Template_v1.sol"; -import {CrosschainBase_v1} from - "src/modules/paymentProcessor/abstracts/CrosschainBase_v1.sol"; +import {CrossChainBase_v1} from + "src/modules/paymentProcessor/abstracts/CrossChainBase_v1.sol"; import {IERC20PaymentClientBase_v1} from "@lm/interfaces/IERC20PaymentClientBase_v1.sol"; -contract CrosschainBase_v1_Exposed is CrosschainBase_v1 { - /// @notice Implementation of the bridge transfer logic using EverClear - ///// @inheritdoc CrosschainBase_v1 - function exposed_executeBridgeTransfer( +contract CrossChainBase_v1_Exposed is CrossChainBase_v1 { + function _executeBridgeTransfer( IERC20PaymentClientBase_v1.PaymentOrder memory order, bytes memory executionData - ) external payable returns (bytes memory) { - return _executeBridgeTransfer(order, executionData); + ) internal pure override returns (bytes memory) { + return ""; } - function _executeBridgeTransfer( + function getBridgeData(uint paymentId) + public + pure + override + returns (bytes memory) + { + return ""; + } + + function processPayments(IERC20PaymentClientBase_v1 client) + external + override + {} + + function exposed_executeBridgeTransfer( IERC20PaymentClientBase_v1.PaymentOrder memory order, bytes memory executionData - ) internal override(CrosschainBase_v1) returns (bytes memory) { - // Add default return value here for testing + ) external payable returns (bytes memory) { + return _executeBridgeTransfer(order, executionData); } } diff --git a/test/utils/mocks/modules/paymentProcessor/PP_Connext_Crosschain_v1_Exposed.sol b/test/utils/mocks/modules/paymentProcessor/PP_Connext_Crosschain_v1_Exposed.sol new file mode 100644 index 000000000..bac9b70d4 --- /dev/null +++ b/test/utils/mocks/modules/paymentProcessor/PP_Connext_Crosschain_v1_Exposed.sol @@ -0,0 +1,34 @@ +pragma solidity ^0.8.20; + +import {PP_Connext_Crosschain_v1} from + "src/modules/paymentProcessor/PP_Connext_Crosschain_v1.sol"; +import {IERC20PaymentClientBase_v1} from + "@lm/interfaces/IERC20PaymentClientBase_v1.sol"; +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; + +contract PP_Connext_Crosschain_v1_Exposed is PP_Connext_Crosschain_v1 { + // Expose internal _executeBridgeTransfer function + function exposed_executeBridgeTransfer( + IERC20PaymentClientBase_v1.PaymentOrder memory order, + bytes memory executionData + ) external returns (bytes memory) { + return _executeBridgeTransfer(order, executionData); + } + + // Expose internal xcall function + function exposed_createCrossChainIntent( + IERC20PaymentClientBase_v1.PaymentOrder memory order, + bytes memory executionData + ) external returns (bytes32) { + return _createCrossChainIntent(order, executionData, false); + } + + function exposed_setFailedTransfer( + address client, + address recipient, + bytes memory intentId, + uint amount + ) external { + failedTransfers[client][recipient][intentId] = amount; + } +}