From 8b1dfa4e6bc997766841e58da583f62fdc71c6dc Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Thu, 7 Nov 2024 13:11:33 +0200 Subject: [PATCH 01/13] refactor: extract payment logic from 'InvoiceModule' to 'PaymentModule' --- src/modules/invoice-module/InvoiceModule.sol | 383 +++--------------- .../interfaces/IInvoiceModule.sol | 89 +--- .../invoice-module/libraries/Errors.sol | 65 +-- src/modules/payment-module/PaymentModule.sol | 354 ++++++++++++++++ .../interfaces/IPaymentModule.sol | 88 ++++ .../payment-module/libraries/Errors.sol | 60 +++ .../libraries/Helpers.sol | 0 .../libraries/Types.sol | 35 +- .../sablier-v2/StreamManager.sol | 6 +- .../sablier-v2/interfaces/IStreamManager.sol | 5 +- 10 files changed, 578 insertions(+), 507 deletions(-) create mode 100644 src/modules/payment-module/PaymentModule.sol create mode 100644 src/modules/payment-module/interfaces/IPaymentModule.sol create mode 100644 src/modules/payment-module/libraries/Errors.sol rename src/modules/{invoice-module => payment-module}/libraries/Helpers.sol (100%) rename src/modules/{invoice-module => payment-module}/libraries/Types.sol (69%) rename src/modules/{invoice-module => payment-module}/sablier-v2/StreamManager.sol (98%) rename src/modules/{invoice-module => payment-module}/sablier-v2/interfaces/IStreamManager.sol (97%) diff --git a/src/modules/invoice-module/InvoiceModule.sol b/src/modules/invoice-module/InvoiceModule.sol index 174db91..c60526f 100644 --- a/src/modules/invoice-module/InvoiceModule.sol +++ b/src/modules/invoice-module/InvoiceModule.sol @@ -1,32 +1,33 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.26; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; -import { Types } from "./libraries/Types.sol"; -import { Errors } from "./libraries/Errors.sol"; +import { PaymentModule } from "./../payment-module/PaymentModule.sol"; import { IInvoiceModule } from "./interfaces/IInvoiceModule.sol"; -import { ISpace } from "./../../interfaces/ISpace.sol"; -import { StreamManager } from "./sablier-v2/StreamManager.sol"; -import { Helpers } from "./libraries/Helpers.sol"; +import { Errors } from "./libraries/Errors.sol"; /// @title InvoiceModule /// @notice See the documentation in {IInvoiceModule} -contract InvoiceModule is IInvoiceModule, StreamManager, ERC721 { - using SafeERC20 for IERC20; +contract InvoiceModule is IInvoiceModule, PaymentModule, ERC721 { using Strings for uint256; + /*////////////////////////////////////////////////////////////////////////// + PUBLIC STORAGE + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev The address of the off-chain Relayer responsible to mint on-chain invoices + address public relayer; + /*////////////////////////////////////////////////////////////////////////// PRIVATE STORAGE //////////////////////////////////////////////////////////////////////////*/ - /// @dev Invoice details mapped by the `id` invoice ID - mapping(uint256 id => Types.Invoice) private _invoices; + /// @dev Invoice ID mapped to the payment request ID + mapping(uint256 invoiceId => uint256 paymentRequestId) private _invoiceIdToPaymentRequest; /// @dev Counter to keep track of the next ID used to create a new invoice uint256 private _nextInvoiceId; @@ -38,15 +39,15 @@ contract InvoiceModule is IInvoiceModule, StreamManager, ERC721 { CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ - /// @dev Initializes the {StreamManager} contract and first invoice ID + /// @dev Initializes the {PaymentModule} and {ERC721} contracts constructor( ISablierV2LockupLinear _sablierLockupLinear, ISablierV2LockupTranched _sablierLockupTranched, address _brokerAdmin, string memory _URI ) - StreamManager(_sablierLockupLinear, _sablierLockupTranched, _brokerAdmin) - ERC721("Metastation Invoice NFT", "MD-INVOICES") + PaymentModule(_sablierLockupLinear, _sablierLockupTranched, _brokerAdmin) + ERC721("Werk Invoice NFTs", "WK-INVOICES") { // Start the invoice IDs from 1 _nextInvoiceId = 1; @@ -56,231 +57,9 @@ contract InvoiceModule is IInvoiceModule, StreamManager, ERC721 { } /*////////////////////////////////////////////////////////////////////////// - MODIFIERS - //////////////////////////////////////////////////////////////////////////*/ - - /// @dev Allow only calls from contracts implementing the {ISpace} interface - modifier onlySpace() { - // Checks: the sender is a valid non-zero code size contract - if (msg.sender.code.length == 0) { - revert Errors.SpaceZeroCodeSize(); - } - - // Checks: the sender implements the ERC-165 interface required by {ISpace} - bytes4 interfaceId = type(ISpace).interfaceId; - if (!ISpace(msg.sender).supportsInterface(interfaceId)) revert Errors.SpaceUnsupportedInterface(); - _; - } - - /*////////////////////////////////////////////////////////////////////////// - CONSTANT FUNCTIONS + CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @inheritdoc IInvoiceModule - function getInvoice(uint256 id) external view returns (Types.Invoice memory invoice) { - return _invoices[id]; - } - - /*////////////////////////////////////////////////////////////////////////// - NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc IInvoiceModule - function createInvoice(Types.Invoice calldata invoice) external onlySpace returns (uint256 invoiceId) { - // Checks: the amount is non-zero - if (invoice.payment.amount == 0) { - revert Errors.ZeroPaymentAmount(); - } - - // Checks: the start time is stricly lower than the end time - if (invoice.startTime > invoice.endTime) { - revert Errors.StartTimeGreaterThanEndTime(); - } - - // Checks: end time is not in the past - uint40 currentTime = uint40(block.timestamp); - if (currentTime >= invoice.endTime) { - revert Errors.EndTimeInThePast(); - } - - // Checks: the recurrence type is not equal to one-off if dealing with a tranched stream-based invoice - if (invoice.payment.method == Types.Method.TranchedStream) { - // The recurrence cannot be set to one-off - if (invoice.payment.recurrence == Types.Recurrence.OneOff) { - revert Errors.TranchedStreamInvalidOneOffRecurence(); - } - } - - // Validates the invoice interval (endTime - startTime) and returns the number of payments of the invoice - // based on the payment method, interval and recurrence type - // - // Notes: - // - The number of payments is taken into account only for transfer-based invoices - // - There should be only one payment when dealing with a one-off transfer-based invoice - // - When dealing with a recurring transfer, the number of payments must be calculated based - // on the payment interval (endTime - startTime) and recurrence type - uint40 numberOfPayments; - if (invoice.payment.method == Types.Method.Transfer && invoice.payment.recurrence == Types.Recurrence.OneOff) { - numberOfPayments = 1; - } else if (invoice.payment.method != Types.Method.LinearStream) { - numberOfPayments = _checkIntervalPayments({ - recurrence: invoice.payment.recurrence, - startTime: invoice.startTime, - endTime: invoice.endTime - }); - - // Set the number of payments to zero if dealing with a tranched-based invoice - // The `_checkIntervalPayment` method is still called for a tranched-based invoice just - // to validate the interval and ensure it can support multiple payments based on the chosen recurrence - if (invoice.payment.method == Types.Method.TranchedStream) numberOfPayments = 0; - } - - // Checks: the asset is different than the native token if dealing with either a linear or tranched stream-based invoice - if (invoice.payment.method != Types.Method.Transfer) { - if (invoice.payment.asset == address(0)) { - revert Errors.OnlyERC20StreamsAllowed(); - } - } - - // Get the next invoice ID - invoiceId = _nextInvoiceId; - - // Effects: create the invoice - _invoices[invoiceId] = Types.Invoice({ - status: Types.Status.Pending, - startTime: invoice.startTime, - endTime: invoice.endTime, - payment: Types.Payment({ - recurrence: invoice.payment.recurrence, - method: invoice.payment.method, - paymentsLeft: numberOfPayments, - amount: invoice.payment.amount, - asset: invoice.payment.asset, - streamId: 0 - }) - }); - - // Effects: increment the next invoice id - // Use unchecked because the invoice id cannot realistically overflow - unchecked { - ++_nextInvoiceId; - } - - // Effects: mint the invoice NFT to the recipient space - _mint({ to: msg.sender, tokenId: invoiceId }); - - // Log the invoice creation - emit InvoiceCreated({ - id: invoiceId, - recipient: msg.sender, - status: Types.Status.Pending, - startTime: invoice.startTime, - endTime: invoice.endTime, - payment: invoice.payment - }); - } - - /// @inheritdoc IInvoiceModule - function payInvoice(uint256 id) external payable { - // Load the invoice from storage - Types.Invoice memory invoice = _invoices[id]; - - // Retrieve the recipient of the invoice - // This will also check if the invoice is minted or not burned - address recipient = ownerOf(id); - - // Checks: the invoice is not already paid or canceled - if (invoice.status == Types.Status.Paid) { - revert Errors.InvoiceAlreadyPaid(); - } else if (invoice.status == Types.Status.Canceled) { - revert Errors.InvoiceCanceled(); - } - - // Handle the payment workflow depending on the payment method type - if (invoice.payment.method == Types.Method.Transfer) { - // Effects: pay the invoice and update its status to `Paid` or `Ongoing` depending on the payment type - _payByTransfer(id, invoice, recipient); - } else { - uint256 streamId; - // Check to see whether the invoice must be paid through a linear or tranched stream - if (invoice.payment.method == Types.Method.LinearStream) { - streamId = _payByLinearStream(invoice, recipient); - } else { - streamId = _payByTranchedStream(invoice, recipient); - } - - // Effects: update the status of the invoice to `Ongoing` and the stream ID - // if dealing with a linear or tranched-based invoice - _invoices[id].status = Types.Status.Ongoing; - _invoices[id].payment.streamId = streamId; - } - - // Log the payment transaction - emit InvoicePaid({ id: id, payer: msg.sender, status: _invoices[id].status, payment: _invoices[id].payment }); - } - - /// @inheritdoc IInvoiceModule - function cancelInvoice(uint256 id) external { - // Load the invoice from storage - Types.Invoice memory invoice = _invoices[id]; - - // Checks: the invoice is paid or already canceled - if (invoice.status == Types.Status.Paid) { - revert Errors.CannotCancelPaidInvoice(); - } else if (invoice.status == Types.Status.Canceled) { - revert Errors.InvoiceAlreadyCanceled(); - } - - // Checks: `msg.sender` is the recipient if invoice status is pending - // - // Notes: - // - Once a linear or tranched stream is created, the `msg.sender` is checked in the - // {SablierV2Lockup} `cancel` method - if (invoice.status == Types.Status.Pending) { - // Retrieve the recipient of the invoice - address recipient = ownerOf(id); - - if (recipient != msg.sender) { - revert Errors.OnlyInvoiceRecipient(); - } - } - // Checks, Effects, Interactions: cancel the stream if status is ongoing - // - // Notes: - // - A transfer-based invoice can be canceled directly - // - A linear or tranched stream MUST be canceled by calling the `cancel` method on the according - // {ISablierV2Lockup} contract - else if (invoice.status == Types.Status.Ongoing) { - _cancelStream({ streamType: invoice.payment.method, streamId: invoice.payment.streamId }); - } - - // Effects: mark the invoice as canceled - _invoices[id].status = Types.Status.Canceled; - - // Log the invoice cancelation - emit InvoiceCanceled(id); - } - - /// @inheritdoc IInvoiceModule - function withdrawInvoiceStream(uint256 id) public returns (uint128 withdrawnAmount) { - // Load the invoice from storage - Types.Invoice memory invoice = _invoices[id]; - - // Retrieve the recipient of the invoice - address recipient = ownerOf(id); - - // Effects: update the invoice status to `Paid` once the full payment amount has been successfully streamed - uint128 streamedAmount = - streamedAmountOf({ streamType: invoice.payment.method, streamId: invoice.payment.streamId }); - if (streamedAmount == invoice.payment.amount) { - _invoices[id].status = Types.Status.Paid; - } - - // Check, Effects, Interactions: withdraw from the stream - return - _withdrawStream({ streamType: invoice.payment.method, streamId: invoice.payment.streamId, to: recipient }); - } - /// @inheritdoc ERC721 function tokenURI(uint256 tokenId) public view override returns (string memory) { // Checks: the `tokenId` was minted or is not burned @@ -291,120 +70,50 @@ contract InvoiceModule is IInvoiceModule, StreamManager, ERC721 { return string.concat(baseURI, tokenId.toString(), ".json"); } - /// @inheritdoc ERC721 - function transferFrom(address from, address to, uint256 tokenId) public override { - // Retrieve the invoice details - Types.Invoice memory invoice = _invoices[tokenId]; - - // Checks: the payment request has been accepted and a stream has already been - // created if dealing with a stream-based payment - if (invoice.payment.streamId != 0) { - // Checks and Effects: withdraw the maximum withdrawable amount to the current stream recipient - // and transfer the stream NFT to the new recipient - _withdrawMaxAndTransferStream({ - streamType: invoice.payment.method, - streamId: invoice.payment.streamId, - newRecipient: to - }); - } - - // Checks, Effects and Interactions: transfer the invoice NFT - super.transferFrom(from, to, tokenId); - } - /*////////////////////////////////////////////////////////////////////////// - INTERNAL-METHODS + NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Pays the `id` invoice by transfer - function _payByTransfer(uint256 id, Types.Invoice memory invoice, address recipient) internal { - // Effects: update the invoice status to `Paid` if the required number of payments has been made - // Using unchecked because the number of payments left cannot underflow as the invoice status - // will be updated to `Paid` once `paymentLeft` is zero - unchecked { - uint40 paymentsLeft = invoice.payment.paymentsLeft - 1; - _invoices[id].payment.paymentsLeft = paymentsLeft; - if (paymentsLeft == 0) { - _invoices[id].status = Types.Status.Paid; - } else if (invoice.status == Types.Status.Pending) { - _invoices[id].status = Types.Status.Ongoing; - } - } - - // Check if the payment must be done in native token (ETH) or an ERC-20 token - if (invoice.payment.asset == address(0)) { - // Checks: the payment amount matches the invoice value - if (msg.value < invoice.payment.amount) { - revert Errors.PaymentAmountLessThanInvoiceValue({ amount: invoice.payment.amount }); - } - - // Interactions: pay the recipient with native token (ETH) - (bool success,) = payable(recipient).call{ value: invoice.payment.amount }(""); - if (!success) revert Errors.NativeTokenPaymentFailed(); - } else { - // Interactions: pay the recipient with the ERC-20 token - IERC20(invoice.payment.asset).safeTransferFrom({ - from: msg.sender, - to: recipient, - value: invoice.payment.amount - }); + /// @inheritdoc IInvoiceModule + function mint(address to, uint256 paymentRequestId) public { + // Checks: `msg.sender` is the authorized Relayer to mint tokens + if (msg.sender != relayer) { + revert Errors.Unathorized(); } - } - - /// @dev Create the linear stream payment - function _payByLinearStream(Types.Invoice memory invoice, address recipient) internal returns (uint256 streamId) { - streamId = StreamManager.createLinearStream({ - asset: IERC20(invoice.payment.asset), - totalAmount: invoice.payment.amount, - startTime: invoice.startTime, - endTime: invoice.endTime, - recipient: recipient - }); - } - - /// @dev Create the tranched stream payment - function _payByTranchedStream( - Types.Invoice memory invoice, - address recipient - ) internal returns (uint256 streamId) { - uint40 numberOfTranches = - Helpers.computeNumberOfPayments(invoice.payment.recurrence, invoice.endTime - invoice.startTime); - streamId = StreamManager.createTranchedStream({ - asset: IERC20(invoice.payment.asset), - totalAmount: invoice.payment.amount, - startTime: invoice.startTime, - recipient: recipient, - numberOfTranches: numberOfTranches, - recurrence: invoice.payment.recurrence - }); - } + // Get the next token ID + uint256 tokenId = _nextInvoiceId; - /// @notice Calculates the number of payments to be made for a recurring transfer and tranched stream-based invoice - /// @dev Reverts if the number of payments is zero, indicating that either the interval or recurrence type was set incorrectly - function _checkIntervalPayments( - Types.Recurrence recurrence, - uint40 startTime, - uint40 endTime - ) internal pure returns (uint40 numberOfPayments) { - // Checks: the invoice payment interval matches the recurrence type - // This cannot underflow as the start time is stricly lower than the end time when this call executes - uint40 interval; + // Effects: increment the next payment request ID + // Use unchecked because the request id cannot realistically overflow unchecked { - interval = endTime - startTime; + ++_nextInvoiceId; } - // Check and calculate the expected number of payments based on the invoice recurrence and payment interval - numberOfPayments = Helpers.computeNumberOfPayments(recurrence, interval); + // Effects: set the `paymentRequestId` that belongs to the `tokenId` invoice + _invoiceIdToPaymentRequest[tokenId] = paymentRequestId; - // Revert if there are zero payments to be made since the payment method due to invalid interval and recurrence type - if (numberOfPayments == 0) { - revert Errors.PaymentIntervalTooShortForSelectedRecurrence(); - } + // Effects: mint the request NFT to the recipient space + _mint(to, tokenId); } + /*////////////////////////////////////////////////////////////////////////// + INTERNAL-METHODS + //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc ERC721 function _baseURI() internal view override returns (string memory) { return _collectionURI; } + + /// @inheritdoc ERC721 + /// @dev Guard tokens from being transferred making them Soulbound Tokens (SBT) + function _update(address to, uint256 tokenId, address auth) internal override(ERC721) returns (address) { + address from = _ownerOf(tokenId); + if (from != address(0) && to != address(0)) { + revert("Soulbound: Transfer failed"); + } + + return super._update(to, tokenId, auth); + } } diff --git a/src/modules/invoice-module/interfaces/IInvoiceModule.sol b/src/modules/invoice-module/interfaces/IInvoiceModule.sol index ca83343..14c74fc 100644 --- a/src/modules/invoice-module/interfaces/IInvoiceModule.sol +++ b/src/modules/invoice-module/interfaces/IInvoiceModule.sol @@ -1,96 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.26; -import { Types } from "./../libraries/Types.sol"; - /// @title IInvoiceModule /// @notice Contract module that provides functionalities to issue and pay an on-chain invoice interface IInvoiceModule { - /*////////////////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Emitted when an invoice is created - /// @param id The ID of the invoice - /// @param recipient The address receiving the payment - /// @param status The status of the invoice - /// @param startTime The timestamp when the invoice takes effect - /// @param endTime The timestamp by which the invoice must be paid - /// @param payment Struct representing the payment details associated with the invoice - event InvoiceCreated( - uint256 id, - address indexed recipient, - Types.Status status, - uint40 startTime, - uint40 endTime, - Types.Payment payment - ); - - /// @notice Emitted when an invoice is paid - /// @param id The ID of the invoice - /// @param payer The address of the payer - /// @param status The status of the invoice - /// @param payment Struct representing the payment details associated with the invoice - event InvoicePaid(uint256 indexed id, address indexed payer, Types.Status status, Types.Payment payment); - - /// @notice Emitted when an invoice is canceled - /// @param id The ID of the invoice - event InvoiceCanceled(uint256 indexed id); - - /*////////////////////////////////////////////////////////////////////////// - CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Retrieves the details of the `id` invoice - /// @param id The ID of the invoice for which to get the details - function getInvoice(uint256 id) external view returns (Types.Invoice memory invoice); - /*////////////////////////////////////////////////////////////////////////// NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @notice Creates a new invoice - /// - /// Requirements: - /// - `msg.sender` must be a contract implementing the {ISpace} interface - /// - /// Notes: - /// - `recipient` is not checked because the call is enforced to be made through a {Space} contract - /// - /// @param invoice The details of the invoice following the {Invoice} struct format - /// @return id The on-chain ID of the invoice - function createInvoice(Types.Invoice calldata invoice) external returns (uint256 id); - - /// @notice Pays a transfer-based invoice - /// - /// Notes: - /// - `msg.sender` is enforced to be a specific payer address - /// - /// @param id The ID of the invoice to pay - function payInvoice(uint256 id) external payable; - - /// @notice Cancels the `id` invoice - /// - /// Notes: - /// - A transfer-based invoice can be canceled only by its creator (recipient) - /// - A linear/tranched stream-based invoice can be canceled by its creator only if its - /// status is `Pending`; otherwise only the stream sender can cancel it - /// - if the invoice has a linear or tranched stream payment method, the streaming flow will be - /// stopped and the remaining funds will be refunded to the stream payer - /// - /// Important: - /// - if the invoice has a linear or tranched stream payment method, the portion that has already - /// been streamed is NOT automatically transferred - /// - /// @param id The ID of the invoice - function cancelInvoice(uint256 id) external; - - /// @notice Withdraws the maximum withdrawable amount from the stream associated with the `id` invoice - /// - /// Notes: - /// - reverts if `msg.sender` is not the stream recipient - /// - reverts if the payment method of the `id` invoice is not linear or tranched stream based - /// - /// @param id The ID of the invoice - function withdrawInvoiceStream(uint256 id) external returns (uint128 withdrawnAmount); + /// @notice Creates an on-chain representation of an off-chain invoice by minting an ERC-721 token + /// @param to The address to which the NFT will be minted + /// @param paymentRequestId The ID of the payment request to which this invoice belongs + function mint(address to, uint256 paymentRequestId) external; } diff --git a/src/modules/invoice-module/libraries/Errors.sol b/src/modules/invoice-module/libraries/Errors.sol index e9f0c88..50ed6dd 100644 --- a/src/modules/invoice-module/libraries/Errors.sol +++ b/src/modules/invoice-module/libraries/Errors.sol @@ -2,71 +2,12 @@ pragma solidity ^0.8.26; /// @title Errors -/// @notice Library containing all custom errors the {InvoiceModule} and {StreamManager} may revert with +/// @notice Library containing all custom errors the {InvoiceModule} may revert with library Errors { /*////////////////////////////////////////////////////////////////////////// INVOICE-MODULE //////////////////////////////////////////////////////////////////////////*/ - /// @notice Thrown when the caller is an invalid zero code contract or EOA - error SpaceZeroCodeSize(); - - /// @notice Thrown when the caller is a contract that does not implement the {ISpace} interface - error SpaceUnsupportedInterface(); - - /// @notice Thrown when the end time of an invoice is in the past - error EndTimeInThePast(); - - /// @notice Thrown when the start time is later than the end time - error StartTimeGreaterThanEndTime(); - - /// @notice Thrown when the payment amount set for a new invoice is zero - error ZeroPaymentAmount(); - - /// @notice Thrown when the payment amount is less than the invoice value - error PaymentAmountLessThanInvoiceValue(uint256 amount); - - /// @notice Thrown when a payment in the native token (ETH) fails - error NativeTokenPaymentFailed(); - - /// @notice Thrown when a linear or tranched stream is created with the native token as the payment asset - error OnlyERC20StreamsAllowed(); - - /// @notice Thrown when a payer attempts to pay an invoice that has already been paid - error InvoiceAlreadyPaid(); - - /// @notice Thrown when a payer attempts to pay a canceled invoice - error InvoiceCanceled(); - - /// @notice Thrown when the invoice ID references a null invoice - error InvoiceNull(); - - /// @notice Thrown when `msg.sender` attempts to withdraw from an invoice that is not stream-based - error InvoiceNotStreamBased(); - - /// @notice Thrown when `msg.sender` is not the invoice recipient - error OnlyInvoiceRecipient(); - - /// @notice Thrown when the payment interval (endTime - startTime) is too short for the selected recurrence - /// i.e. recurrence is set to weekly but interval is shorter than 1 week - error PaymentIntervalTooShortForSelectedRecurrence(); - - /// @notice Thrown when a tranched stream has a one-off recurrence type - error TranchedStreamInvalidOneOffRecurence(); - - /// @notice Thrown when an attempt is made to cancel an already paid invoice - error CannotCancelPaidInvoice(); - - /// @notice Thrown when an attempt is made to cancel an already canceled invoice - error InvoiceAlreadyCanceled(); - - /// @notice Thrown when the caller is not the initial stream sender - error OnlyInitialStreamSender(address initialSender); - - /*////////////////////////////////////////////////////////////////////////// - STREAM-MANAGER - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Thrown when the caller is not the broker admin - error OnlyBrokerAdmin(); + /// @notice Thrown when the caller is unathorized to execute a call + error Unathorized(); } diff --git a/src/modules/payment-module/PaymentModule.sol b/src/modules/payment-module/PaymentModule.sol new file mode 100644 index 0000000..d253a0d --- /dev/null +++ b/src/modules/payment-module/PaymentModule.sol @@ -0,0 +1,354 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; + +import { Types } from "./libraries/Types.sol"; +import { Errors } from "./libraries/Errors.sol"; +import { IPaymentModule } from "./interfaces/IPaymentModule.sol"; +import { ISpace } from "./../../interfaces/ISpace.sol"; +import { StreamManager } from "./sablier-v2/StreamManager.sol"; +import { Helpers } from "./libraries/Helpers.sol"; + +/// @title PaymentModule +/// @notice See the documentation in {IPaymentModule} +contract PaymentModule is IPaymentModule, StreamManager { + using SafeERC20 for IERC20; + using Strings for uint256; + + /*////////////////////////////////////////////////////////////////////////// + PRIVATE STORAGE + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Payment requests details mapped by the `id` payment request ID + mapping(uint256 id => Types.PaymentRequest) private _requests; + + /// @dev Counter to keep track of the next ID used to create a new payment request + uint256 private _nextRequestId; + + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Initializes the {StreamManager} contract and first request ID + constructor( + ISablierV2LockupLinear _sablierLockupLinear, + ISablierV2LockupTranched _sablierLockupTranched, + address _brokerAdmin + ) StreamManager(_sablierLockupLinear, _sablierLockupTranched, _brokerAdmin) { + // Start the first payment request ID from 1 + _nextRequestId = 1; + } + + /*////////////////////////////////////////////////////////////////////////// + MODIFIERS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Allow only calls from contracts implementing the {ISpace} interface + modifier onlySpace() { + // Checks: the sender is a valid non-zero code size contract + if (msg.sender.code.length == 0) { + revert Errors.SpaceZeroCodeSize(); + } + + // Checks: the sender implements the ERC-165 interface required by {ISpace} + bytes4 interfaceId = type(ISpace).interfaceId; + if (!ISpace(msg.sender).supportsInterface(interfaceId)) revert Errors.SpaceUnsupportedInterface(); + _; + } + + /*////////////////////////////////////////////////////////////////////////// + CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IPaymentModule + function getRequest(uint256 id) external view returns (Types.PaymentRequest memory request) { + return _requests[id]; + } + + /*////////////////////////////////////////////////////////////////////////// + NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IPaymentModule + function createRequest(Types.PaymentRequest calldata request) external onlySpace returns (uint256 id) { + // Checks: the amount is non-zero + if (request.config.amount == 0) { + revert Errors.ZeroPaymentAmount(); + } + + // Checks: the start time is stricly lower than the end time + if (request.startTime > request.endTime) { + revert Errors.StartTimeGreaterThanEndTime(); + } + + // Checks: end time is not in the past + uint40 currentTime = uint40(block.timestamp); + if (currentTime >= request.endTime) { + revert Errors.EndTimeInThePast(); + } + + // Checks: the recurrence type is not equal to one-off if dealing with a tranched stream-based request + if (request.config.method == Types.Method.TranchedStream) { + // The recurrence cannot be set to one-off + if (request.config.recurrence == Types.Recurrence.OneOff) { + revert Errors.TranchedStreamInvalidOneOffRecurence(); + } + } + + // Validates the payment request interval (endTime - startTime) and returns the number of payments + // based on the payment method, interval and recurrence type + // + // Notes: + // - The number of payments is taken into account only for transfer-based invoices + // - There should be only one payment when dealing with a one-off transfer-based request + // - When dealing with a recurring transfer, the number of payments must be calculated based + // on the payment interval (endTime - startTime) and recurrence type + uint40 numberOfPayments; + if (request.config.method == Types.Method.Transfer && request.config.recurrence == Types.Recurrence.OneOff) { + numberOfPayments = 1; + } else if (request.config.method != Types.Method.LinearStream) { + numberOfPayments = _checkIntervalPayments({ + recurrence: request.config.recurrence, + startTime: request.startTime, + endTime: request.endTime + }); + + // Set the number of payments to zero if dealing with a tranched-based request + // The `_checkIntervalPayment` method is still called for a tranched-based request just + // to validate the interval and ensure it can support multiple payments based on the chosen recurrence + if (request.config.method == Types.Method.TranchedStream) numberOfPayments = 0; + } + + // Checks: the asset is different than the native token if dealing with either a linear or tranched stream-based payment + if (request.config.method != Types.Method.Transfer) { + if (request.config.asset == address(0)) { + revert Errors.OnlyERC20StreamsAllowed(); + } + } + + // Get the next payment request ID + id = _nextRequestId; + + // Effects: create the payment request + _requests[id] = Types.PaymentRequest({ + status: Types.Status.Pending, + startTime: request.startTime, + endTime: request.endTime, + recipient: request.recipient, + config: Types.Config({ + recurrence: request.config.recurrence, + method: request.config.method, + paymentsLeft: numberOfPayments, + amount: request.config.amount, + asset: request.config.asset, + streamId: 0 + }) + }); + + // Effects: increment the next payment request ID + // Use unchecked because the request id cannot realistically overflow + unchecked { + ++_nextRequestId; + } + + // Log the payment request creation + emit RequestCreated({ + id: id, + recipient: msg.sender, + startTime: request.startTime, + endTime: request.endTime, + config: request.config + }); + } + + /// @inheritdoc IPaymentModule + function payRequest(uint256 id) external payable { + // Load the payment request state from storage + Types.PaymentRequest memory request = _requests[id]; + + // Checks: the payment request is not already completed or canceled + if (request.status == Types.Status.Completed) { + revert Errors.RequestCompleted(); + } else if (request.status == Types.Status.Canceled) { + revert Errors.RequestCanceled(); + } + + // Handle the payment workflow depending on the payment method type + if (request.config.method == Types.Method.Transfer) { + // Effects: pay the request and update its status to `Completed` or `Accepted` depending on the payment type + _payByTransfer(id, request); + } else { + uint256 streamId; + // Check to see whether the request must be paid through a linear or tranched stream + if (request.config.method == Types.Method.LinearStream) { + streamId = _payByLinearStream(request); + } else { + streamId = _payByTranchedStream(request); + } + + // Effects: update the status of the request to `Accepted` and the stream ID + // if dealing with a linear or tranched-based request + _requests[id].status = Types.Status.Accepted; + _requests[id].config.streamId = streamId; + } + + // Log the payment transaction + emit RequestPaid({ id: id, payer: msg.sender, status: _requests[id].status, config: _requests[id].config }); + } + + /// @inheritdoc IPaymentModule + function cancelRequest(uint256 id) external { + // Load the payment request state from storage + Types.PaymentRequest memory request = _requests[id]; + + // Checks: the payment request is already completed + if (request.status == Types.Status.Completed) { + revert Errors.RequestCompleted(); + } else if (request.status == Types.Status.Canceled) { + revert Errors.RequestCanceled(); + } + + // Checks: `msg.sender` is the recipient if the payment request status is pending + // + // Notes: + // - Once a linear or tranched stream is created, the `msg.sender` is checked in the + // {SablierV2Lockup} `cancel` method + if (request.status == Types.Status.Pending) { + if (request.recipient != msg.sender) { + revert Errors.OnlyRequestRecipient(); + } + } + // Checks, Effects, Interactions: cancel the stream if payment request has already been accepted + // and the payment method is either linear or tranched stream + // + // Notes: + // - A transfer-based payment request can be canceled directly + // - A linear or tranched stream MUST be canceled by calling the `cancel` method on the according + // {ISablierV2Lockup} contract + else if (request.status == Types.Status.Accepted && request.config.method != Types.Method.Transfer) { + _cancelStream({ streamType: request.config.method, streamId: request.config.streamId }); + } + + // Effects: mark the payment request as canceled + _requests[id].status = Types.Status.Canceled; + + // Log the payment request cancelation + emit RequestCanceled(id); + } + + /// @inheritdoc IPaymentModule + function withdrawRequestStream(uint256 id) public returns (uint128 withdrawnAmount) { + // Load the payment request state from storage + Types.PaymentRequest memory request = _requests[id]; + + // Retrieve the recipient of the request + address recipient = request.recipient; + + // Effects: update the request status to `Completed` once the full payment amount has been successfully streamed + uint128 streamedAmount = streamedAmountOf({ + streamType: request.config.method, + streamId: request.config.streamId + }); + if (streamedAmount == request.config.amount) { + _requests[id].status = Types.Status.Completed; + } + + // Check, Effects, Interactions: withdraw from the stream + return _withdrawStream({ streamType: request.config.method, streamId: request.config.streamId, to: recipient }); + } + + /*////////////////////////////////////////////////////////////////////////// + INTERNAL-METHODS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Pays the `id` request by transfer + function _payByTransfer(uint256 id, Types.PaymentRequest memory request) internal { + // Effects: update the request status to `Completed` if the required number of payments has been made + // Using unchecked because the number of payments left cannot underflow as the request status + // will be updated to `Completed` once `paymentLeft` is zero + unchecked { + uint40 paymentsLeft = request.config.paymentsLeft - 1; + _requests[id].config.paymentsLeft = paymentsLeft; + if (paymentsLeft == 0) { + _requests[id].status = Types.Status.Completed; + } else if (request.status == Types.Status.Pending) { + _requests[id].status = Types.Status.Accepted; + } + } + + // Check if the payment must be done in native token (ETH) or an ERC-20 token + if (request.config.asset == address(0)) { + // Checks: the payment amount matches the request value + if (msg.value < request.config.amount) { + revert Errors.PaymentAmountLessThanInvoiceValue({ amount: request.config.amount }); + } + + // Interactions: pay the recipient with native token (ETH) + (bool success, ) = payable(request.recipient).call{ value: request.config.amount }(""); + if (!success) revert Errors.NativeTokenPaymentFailed(); + } else { + // Interactions: pay the recipient with the ERC-20 token + IERC20(request.config.asset).safeTransferFrom({ + from: msg.sender, + to: request.recipient, + value: request.config.amount + }); + } + } + + /// @dev Create the linear stream payment + function _payByLinearStream(Types.PaymentRequest memory request) internal returns (uint256 streamId) { + streamId = StreamManager.createLinearStream({ + asset: IERC20(request.config.asset), + totalAmount: request.config.amount, + startTime: request.startTime, + endTime: request.endTime, + recipient: request.recipient + }); + } + + /// @dev Create the tranched stream payment + function _payByTranchedStream(Types.PaymentRequest memory request) internal returns (uint256 streamId) { + uint40 numberOfTranches = Helpers.computeNumberOfPayments( + request.config.recurrence, + request.endTime - request.startTime + ); + + streamId = StreamManager.createTranchedStream({ + asset: IERC20(request.config.asset), + totalAmount: request.config.amount, + startTime: request.startTime, + recipient: request.recipient, + numberOfTranches: numberOfTranches, + recurrence: request.config.recurrence + }); + } + + /// @notice Calculates the number of payments to be made for a recurring transfer and tranched stream-based request + /// @dev Reverts if the number of payments is zero, indicating that either the interval or recurrence type was set incorrectly + function _checkIntervalPayments( + Types.Recurrence recurrence, + uint40 startTime, + uint40 endTime + ) internal pure returns (uint40 numberOfPayments) { + // Checks: the request payment interval matches the recurrence type + // This cannot underflow as the start time is stricly lower than the end time when this call executes + uint40 interval; + unchecked { + interval = endTime - startTime; + } + + // Check and calculate the expected number of payments based on the request recurrence and payment interval + numberOfPayments = Helpers.computeNumberOfPayments(recurrence, interval); + + // Revert if there are zero payments to be made since the payment method due to invalid interval and recurrence type + if (numberOfPayments == 0) { + revert Errors.PaymentIntervalTooShortForSelectedRecurrence(); + } + } +} diff --git a/src/modules/payment-module/interfaces/IPaymentModule.sol b/src/modules/payment-module/interfaces/IPaymentModule.sol new file mode 100644 index 0000000..4ee3432 --- /dev/null +++ b/src/modules/payment-module/interfaces/IPaymentModule.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +import { Types } from "./../libraries/Types.sol"; + +/// @title IPaymentModule +/// @notice Contract module that provides functionalities to issue on-chain payment requests +interface IPaymentModule { + /*////////////////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when a payment request is created + /// @param id The ID of the payment request + /// @param recipient The address receiving the payment + /// @param startTime The timestamp when the payment request takes effect + /// @param endTime The timestamp by which the payment request must be paid + /// @param config Struct representing the payment details associated with the payment request + event RequestCreated(uint256 id, address indexed recipient, uint40 startTime, uint40 endTime, Types.Config config); + + /// @notice Emitted when a payment is made for a payment request + /// @param id The ID of the payment request + /// @param payer The address of the payer + /// @param status The status of the payment request + /// @param config Struct representing the payment details + event RequestPaid(uint256 indexed id, address indexed payer, Types.Status status, Types.Config config); + + /// @notice Emitted when a payment request is canceled + /// @param id The ID of the payment request + event RequestCanceled(uint256 indexed id); + + /*////////////////////////////////////////////////////////////////////////// + CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Retrieves the details of the `id` payment request + /// @param id The ID of the payment request for which to get the details + function getRequest(uint256 id) external view returns (Types.PaymentRequest memory request); + + /*////////////////////////////////////////////////////////////////////////// + NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Creates a new payment request + /// + /// Requirements: + /// - `msg.sender` must be a contract implementing the {ISpace} interface + /// + /// Notes: + /// - `recipient` is not checked because the call is enforced to be made through a {Space} contract + /// + /// @param request request The details of the payment request following the {Invoice} struct format + /// @return id The on-chain ID of the payment request + function createRequest(Types.PaymentRequest calldata request) external returns (uint256 id); + + /// @notice Pays a transfer-based payment request + /// + /// Notes: + /// - `msg.sender` is enforced to be a specific payer address + /// + /// @param id The ID of the payment request to pay + function payRequest(uint256 id) external payable; + + /// @notice Cancels the `id` payment request + /// + /// Notes: + /// - A transfer-based payment request can be canceled only by its creator (recipient) + /// - A linear/tranched stream-based payment request can be canceled by its creator only if its + /// status is `Pending`; otherwise only the stream sender can cancel it + /// - if the payment request has a linear or tranched stream payment method, the streaming flow will be + /// stopped and the remaining funds will be refunded to the stream payer + /// + /// Important: + /// - if the payment request has a linear or tranched stream payment method, the portion that has already + /// been streamed is NOT automatically transferred + /// + /// @param id The ID of the payment request + function cancelRequest(uint256 id) external; + + /// @notice Withdraws the maximum withdrawable amount from the stream associated with the `id` payment request + /// + /// Notes: + /// - reverts if `msg.sender` is not the stream recipient + /// - reverts if the payment method of the `id` payment request is not linear or tranched stream + /// + /// @param id The ID of the payment request + function withdrawRequestStream(uint256 id) external returns (uint128 withdrawnAmount); +} diff --git a/src/modules/payment-module/libraries/Errors.sol b/src/modules/payment-module/libraries/Errors.sol new file mode 100644 index 0000000..6cbc4bc --- /dev/null +++ b/src/modules/payment-module/libraries/Errors.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +/// @title Errors +/// @notice Library containing all custom errors the {PaymentModule} and {StreamManager} may revert with +library Errors { + /*////////////////////////////////////////////////////////////////////////// + PAYMENT-MODULE + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Thrown when the caller is an invalid zero code contract or EOA + error SpaceZeroCodeSize(); + + /// @notice Thrown when the caller is a contract that does not implement the {ISpace} interface + error SpaceUnsupportedInterface(); + + /// @notice Thrown when the end time of an invoice is in the past + error EndTimeInThePast(); + + /// @notice Thrown when the start time is later than the end time + error StartTimeGreaterThanEndTime(); + + /// @notice Thrown when the payment amount set for a new invoice is zero + error ZeroPaymentAmount(); + + /// @notice Thrown when the payment amount is less than the invoice value + error PaymentAmountLessThanInvoiceValue(uint256 amount); + + /// @notice Thrown when a payment in the native token (ETH) fails + error NativeTokenPaymentFailed(); + + /// @notice Thrown when a linear or tranched stream is created with the native token as the payment asset + error OnlyERC20StreamsAllowed(); + + /// @notice Thrown when a payer attempts to pay a canceled payment request + error RequestCanceled(); + + /// @notice Thrown when a payer attempts to pay a completed payment request + error RequestCompleted(); + + /// @notice Thrown when `msg.sender` is not the payment request recipient + error OnlyRequestRecipient(); + + /// @notice Thrown when the payment interval (endTime - startTime) is too short for the selected recurrence + /// i.e. recurrence is set to weekly but interval is shorter than 1 week + error PaymentIntervalTooShortForSelectedRecurrence(); + + /// @notice Thrown when a tranched stream has a one-off recurrence type + error TranchedStreamInvalidOneOffRecurence(); + + /// @notice Thrown when the caller is not the initial stream sender + error OnlyInitialStreamSender(address initialSender); + + /*////////////////////////////////////////////////////////////////////////// + STREAM-MANAGER + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Thrown when the caller is not the broker admin + error OnlyBrokerAdmin(); +} diff --git a/src/modules/invoice-module/libraries/Helpers.sol b/src/modules/payment-module/libraries/Helpers.sol similarity index 100% rename from src/modules/invoice-module/libraries/Helpers.sol rename to src/modules/payment-module/libraries/Helpers.sol diff --git a/src/modules/invoice-module/libraries/Types.sol b/src/modules/payment-module/libraries/Types.sol similarity index 69% rename from src/modules/invoice-module/libraries/Types.sol rename to src/modules/payment-module/libraries/Types.sol index 5bc14c3..9977c36 100644 --- a/src/modules/invoice-module/libraries/Types.sol +++ b/src/modules/payment-module/libraries/Types.sol @@ -15,7 +15,7 @@ library Types { Yearly } - /// @notice Enum representing the different payment methods an invoice can have + /// @notice Enum representing the different payment methods /// @custom:value Transfer Payment method must be made through a transfer /// @custom:value LinearStream Payment method must be made through a linear stream /// @custom:value TranchedStream Payment method must be made through a tranched stream @@ -25,14 +25,14 @@ library Types { TranchedStream } - /// @notice Struct encapsulating the different values describing a payment + /// @notice Struct encapsulating the different values describing a payment config /// @param method The payment method /// @param recurrence The payment recurrence /// @param paymentsLeft The number of payments required to fully settle the invoice (only for transfer or tranched stream based invoices) /// @param asset The address of the payment asset /// @param amount The amount that must be paid /// @param streamId The ID of the linear or tranched stream if payment method is either `LinearStream` or `TranchedStream`, otherwise 0 - struct Payment { + struct Config { // slot 0 Method method; Recurrence recurrence; @@ -44,30 +44,31 @@ library Types { uint256 streamId; } - /// @notice Enum representing the different statuses an invoice can have - /// @custom:value Pending Invoice waiting to be paid - /// @custom:value Ongoing Invoice is being paid; if the payment method is a One-Off Transfer, the invoice status will - /// automatically be set to `Paid`. Otherwise, it will remain `Ongoing` until the invoice is fully paid. - /// @custom:value Canceled Invoice cancelled by the recipient (if Transfer-based) or stream sender + /// @notice Enum representing the different statuses a payment request can have + /// @custom:value Pending Payment request waiting to be accepted by the payer + /// @custom:value Accepted Payment request has been accepted and is being paid; if the payment method is a One-Off Transfer, + /// the payment request status will automatically be set to `Completed`. Otherwise, it will remain `Accepted` until it is fully paid + /// @custom:value Canceled Payment request canceled by declined by the recipient (if Transfer-based) or stream sender enum Status { Pending, - Ongoing, - Paid, + Accepted, + Completed, Canceled } - /// @notice Struct encapsulating the different values describing an invoice - /// @param recipient The address of the payee + /// @notice Struct encapsulating the different values describing a payment request /// @param status The status of the invoice - /// @param startTime The unix timestamp indicating when the invoice payment starts - /// @param endTime The unix timestamp indicating when the invoice payment ends - /// @param payment The payment struct describing the invoice payment - struct Invoice { + /// @param startTime The unix timestamp indicating when the payment starts + /// @param endTime The unix timestamp indicating when the payment ends + /// @param recipient The address to which the payment is made + /// @param payment The payment configurations + struct PaymentRequest { // slot 0 Status status; uint40 startTime; uint40 endTime; + address recipient; // slot 1, 2 and 3 - Payment payment; + Config config; } } diff --git a/src/modules/invoice-module/sablier-v2/StreamManager.sol b/src/modules/payment-module/sablier-v2/StreamManager.sol similarity index 98% rename from src/modules/invoice-module/sablier-v2/StreamManager.sol rename to src/modules/payment-module/sablier-v2/StreamManager.sol index bf27b56..e6ecce5 100644 --- a/src/modules/invoice-module/sablier-v2/StreamManager.sol +++ b/src/modules/payment-module/sablier-v2/StreamManager.sol @@ -208,8 +208,10 @@ abstract contract StreamManager is IStreamManager { // Create the tranches array params.tranches = new LockupTranched.Tranche[](numberOfTranches); for (uint256 i; i < numberOfTranches; ++i) { - params.tranches[i] = - LockupTranched.Tranche({ amount: amountPerTranche, timestamp: startTime + durationPerTranche }); + params.tranches[i] = LockupTranched.Tranche({ + amount: amountPerTranche, + timestamp: startTime + durationPerTranche + }); // Jump to the next tranche by adding the duration per tranche timestamp to the start time startTime += durationPerTranche; diff --git a/src/modules/invoice-module/sablier-v2/interfaces/IStreamManager.sol b/src/modules/payment-module/sablier-v2/interfaces/IStreamManager.sol similarity index 97% rename from src/modules/invoice-module/sablier-v2/interfaces/IStreamManager.sol rename to src/modules/payment-module/sablier-v2/interfaces/IStreamManager.sol index 6ec2a41..ccd9c1f 100644 --- a/src/modules/invoice-module/sablier-v2/interfaces/IStreamManager.sol +++ b/src/modules/payment-module/sablier-v2/interfaces/IStreamManager.sol @@ -62,10 +62,7 @@ interface IStreamManager { /// @notice See the documentation in {ISablierV2Lockup-streamedAmountOf} /// Notes: /// - `streamType` parameter has been added to retrieve from the according {ISablierV2Lockup} contract - function streamedAmountOf( - Types.Method streamType, - uint256 streamId - ) external view returns (uint128 streamedAmount); + function streamedAmountOf(Types.Method streamType, uint256 streamId) external view returns (uint128 streamedAmount); /*////////////////////////////////////////////////////////////////////////// NON-CONSTANT FUNCTIONS From a256e78bf96d45a07e30c68690163b45883bf34a Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Thu, 7 Nov 2024 13:16:40 +0200 Subject: [PATCH 02/13] build: install & configure prettier and solhint --- .solhint.json | 19 +++++++++++++++++++ .vscode/settings.json | 2 -- bun.lockb | Bin 5411 -> 42895 bytes foundry.toml | 2 +- package.json | 12 +++++++++++- 5 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 .solhint.json diff --git a/.solhint.json b/.solhint.json new file mode 100644 index 0000000..3b098b5 --- /dev/null +++ b/.solhint.json @@ -0,0 +1,19 @@ +{ + "extends": "solhint:recommended", + "rules": { + "avoid-low-level-calls": "off", + "code-complexity": ["error", 9], + "compiler-version": ["error", ">=0.8.22"], + "contract-name-camelcase": "off", + "const-name-snakecase": "off", + "func-name-mixedcase": "off", + "func-visibility": ["error", { "ignoreConstructors": true }], + "gas-custom-errors": "off", + "max-line-length": ["error", 124], + "named-parameters-mapping": "warn", + "no-empty-blocks": "off", + "not-rely-on-time": "off", + "one-contract-per-file": "off", + "var-name-mixedcase": "off" + } +} diff --git a/.vscode/settings.json b/.vscode/settings.json index b538903..ce12ac2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,4 @@ { - "solidity.packageDefaultDependenciesContractsDirectory": "src", - "solidity.packageDefaultDependenciesDirectory": "lib", "editor.formatOnSave": true, "[solidity]": { "editor.defaultFormatter": "NomicFoundation.hardhat-solidity" diff --git a/bun.lockb b/bun.lockb index 5446f411d75bb4ba0147974e69dc8fc6be9128be..2a82ff7fbaa77db375c208b95b1e029157221144 100755 GIT binary patch literal 42895 zcmeHw2{@Ep__t*UEs9D=XrVCnEeVOVsFW>xgTYv4rkSyoic+amgi6{M6J=g(m3 z(V0@YHZtKv)nXoK6j5aUy6uYLvc=n23l-8?Yx z%{8YCxU#!4C%J`MvhY_nWCF+K2O?7c2;&8DO^)EAEkFnb5s`QaQ4x`05Wax8Dum+& z_isTc4{A`$@cz7Y3>P@0tQpK-_^!eH`5*c>{K$Dni4DIy{=ke^5M52pim zg&=-Zf`308!d{SWFUgn30>YjU_ZOs__28%LK#1<2g|Ih-eW9v|*BSE3LP!gZ0(Q_C z+(&XhfUrM=K{PIp8p&Z?g*eK`rAI{3K^qAm2dYOnE0D$w0S!XD55(o55F}41gox)V zgy_B<+(h>oOa@U-DnwBEdJqnPa0Up4gmZxh)k71&C{%(F)%O^LD4(04{D2Tzco@h-AHtVAgvO-?h0}tk2;`zM zdEp#m5fR+)Qep5w(QW+>x*Ro>DKX%E zi)%F3w%nR{UT*%UlS{_muYMFk$#s^T=%g6km|3NAi?=m<*1?{q_0yD`e{D5gHtzfT zl$$z+Dc&OY)dyY0`TiMSO;z*DfABM*##evlVeJ#ET&o^y%c)M8yO}k?<%UJXT9u)X zhm}YVJ>e<3sd~bft?ZxSqKDW+?H=T@4336325fm7=;>N(Sz7fn7CGWgeN_nTGCuBvV&#S3KSEY-}sBvN;#Y{6Rg5vj*f zy@xH6>Gk;ouaCivXD2Qg`Vc- zD?LqaIrSsApSAI>;MsD^MjTi>H*rh<-kd(mmKH2_k{%kfB|7()@tkQBbKmsE`5GxA zd6yd94;X!U<9$$cZ;y*g?0cWA6zxX7f7rxTcvsl^YH0Hp-hj-AQA7N5(*36=WYw+z zp!M*3u!YsM+uR=$9d$I0?9@;*+LP%&k`Kdo6$(J7&d>8bkH3XKP=-NUG7z*zlRT zZc%@k>|v9S{i=KDUFXVOKC$)a?%{E-_O6hnAT|oU1--$G0WFhS&E}Q|!5DgyG#jqnUC8rcA1MnxOAE(EGmq`GA0O&5xY4nvj#d zXH{QOp0fGlsah$IQ&5Rvh{bljpzQN-n_TW8SiRlSIvvY+!l(A33y&Bew$lBpCPN5W;-eWry)2L6F&i zhiL@O7b37scFAFU65wY69@PikgJrWz4&&}@%3}nk_UJU?9e^~GMep^9*xWM^B0FTQ@ZP-n zycOV)?z-bJ{vF_v{*e6LaTq@y_Wd@1$MLRm;QWgLkK{+?cSi>pe;x3)r1HDleoCM? zRDXm+zN>n`<+}lX0^ku2d3+x(g*u#X6X5j#kJ=9&5eONC0tJZ}FA9qen!j-zhn;cY zeA5B%1o*D%4fA6BR=}hA1GgOxalSu=7=I7&NPh^2Wautl3mzt5yB`nTfLMFGi}wY* z1>lkXyW_C@8vu{yU)1)f{ot{zOAh0&1Kv&`f0uxQOc*}|9 z-9zoz6^HQ>Ah4sLe1yaIJLAClyaA8)KX~qd?$9NN@mYYMDJUQ5u&WN^8vt(ucvuDq z*HsRTr}hyM(Gir7?sZrH(SS$&5A$`m{EL7`^AE!Pm-2rC9`#>Td8`9;-I>Gkj)8{; zG=D=kBJkanzaJd(H~=1xzwY8g06!7%Sf1|U4*?$OzdLnDy20i3f=Lg}U)>q|WV{pL zCky09vUhjnl2hmp2`$K$TMgp%eMqP>OZLeA$)i1zf(|tcVs8aUki9N|D*X2%Y@Q9b2#rTCI0>a)gRsK zsvH=<1MvP(K8mCI<938gp$_9I@czmi@L2!&zEChE;(QK($NNuoue;?h0lXcQkJcZk zN*ga+3U#>rR>0%uA5?aC%eNTJ?|)s@I~EF;&jWmrKz`^7#2GryBP_)D2Y|N$JZiiD z(*KR%{UXBuSNXYsw*@@b8xDoZj?%FFMSw@+w>y20jBf$_7{KG_#_pDHG=zWtit3NB zST0nCFo*MT0gwA1z9$Ta({SEGz@zc=U-jPvcq_nT-tN|)3h#$a1m(k{aJTF44|w$a zhvrTE42WeB7UKHn0^S?&DBe}s5iiE~P~$&;pnJFt(RF7IlecQ7yL@VqxM65 z-RZ-){PTdH0C<3lz@t!?9mY$mx4%F8FY?a;Jdz*JA6=b8`U)i72Y48J@k1dFN15?a z2D+nlVMA~P?!(j+FRVgn%|#xqeW>pt*HI`6_wjQr3Q^s0`NBeYv_jty5sn<9@gA_)Wnh9+(@6a$t@l z`WK9KqW(4zB4sPYzKzA%goT zMEMzlI0_LTQxHcX(j!L@Cx-~f72HQ5>Nin>ct;^BH%5?-LWGYM#K|GbH&2irCrIxo zL~_Lo(#av>O%U8Che*Ce!F?Q}C>oQJQ}ciTVY3Jfd_I z|2r;!AD>81zA}cE<&DiI0Gw06GT^R>xN4KA9Njx4+ zzb;er%&Ded@UhV6+xzR2c#)2Q75=8(Xw4k2A_n za4fxGaylVvLtIX6MpF4%H7&yoiCF_bc+DF~A@NFLQV1!ZBy2BVp?#Mg^G>E%S$Vvu z)R?G`A+F(3k<`qyKO=p;B}SSqn4Ma%?x>FTExV!j=NrGwI;Zb0y4-xlJ+}E{^Dq)G z`8=0WFzw0*!}q1zN>9vPy|28AnScD?$00rMr|GVqI9prm@Z%dr`u?-b3c~gd;WU^| z{dVHe^74eaV(Gf}{RT~n-KkYW;zjEUc9fs}_t(0bq%jJ24oQDw-}CmBX9esY2d_RZ zyVLl%u3@i}#*tYXwXvnyODB%qaco`05nj5+$idQe_9Z&+`W=3{JA}jwd(SpUxpJ%K z^^;QeoR^^~il6jO^It-Ff-?7$t z#zKp&Z{P1}e(1P2>-ARWeaGrx3h*MUbNT3jHlZNo>2Bcg%e%i5Kq!5I5!6t+;)n2Td)_rK0YCjeJ(Q zLCkldQ@E z-ain$W=;K9lqmFKSX#PP)Ul0RRf8kGW@yIU*OA!axI(LE*4Gu6_be^Weeq)T$Hw{^ zdDY`Lb>miwG$=6KBscX8mfJ((mBmUx@}3VFQ+-EINAr)shvPSI>+(FJ@AZHt%N~i5EX_5b_2r957bB zCTCojOlt7>>c&a2IVbDCZs)Gp6`b~Zd+x!xW{MRy8Ez+sE8Lz~w};hlX`o;6%@tB_ zb1F{Q9XaOU@{Gg_ukqR(CFhBguggs*|Ia!LXU>>w>E3hDlPgMF2911mWMk8qIF5=M&Mi~r{#-Z7(0O3-f;Z+be(t_WHy9@~fW%9FrlgeQrP#{^XikyZvp;da zhL6(aYa_yx_H68DI?`cj^-QTM^R=EAPKWpw>B!|PXQ zaMAjM*9-YJX+gggqq1Mt7ah`;^HTe{_wJTeKIaFv*p4cj9HnzVBtv(wteM^M>PFrH zrDz|iIR|elT8&zLR$n^Y1g@JgQ6fd9G92 zH{HTufZEU5!RDuDFUgv$_2z;JiFaTdE9fEkl;e5t{Zr?~uHHG={MTyRd+B$)R%zcg zeo(*s=q{7Vt!Gy8WVHHtFF*Wl&_?}_I#t#$7LcAM~r8MgpQn1KKu0Sg6X|y4ECs5%9foo*R;mVCtYj{Z^!Ku1}*WgmfU(i zaC1SmgpE#;Z(zEd9Eo>O8!O0b-%rfy^7j=@?^zpVXjdgvv>5%X-mYnl8trmpRFRSO zMom8n`$r~0nHIg-FH3vYrS@5W<>$1x#~;-ml{!doA2Eu=i_R{wqwwAq)%09fyZ?|$ zfy2 z8u&B5xh9Wm9rav7d*+L{xfeLppRXoQkn6QHH*bBxXEC%+>8@4VS@{;4XLeV42 z>4vStToUhaGVh%HpK4;#gYW-55p(Rz4%d9eh=#LQLZ==0*%+TvT%D@0JNV>ty~t0q z^TLBA3+o5%Gq;V;uMP@fZWP(r>&}m;Yb4$gWZs=WpQKnu_-q^9teAOXV_Bdf>)kDJ z#gVOVtkyH?f>dRP1k5_N@6m!+3s+JEvB;bb3 z3*+*fs`?$*9S*pb^?iAGf!Cx|)$L6yUyrW5E`8D`!=A)DlFU28@54!ns8c2L$5+$(B?Tc6Q&`{v+k` z=aG2vIVPf~6x}TTvUFGJg=C()9|Vdovh%OFe32o#I&#PRMPpp%ODQJi6_1IQ{F$r~pPjEm;?*YeHV@u@ z_(D|Nr*M-EElpR_XB^(T;fz&b%6RvYH<#9A1|DD0kWwO+Z@*v#XS8{<(KNPv*d!}$ zktf4nZjO!eU)*F);>G8-gdUdG?0#>uXJpZpA9hQ^JO@_U#AZ0XKkhqz*YyTT-DRiz z%k^T{r6iZ@Fjqab5OexTm)beUOYh5yCXFfQ^`<3kt|#$Q$?|@^(l8}`*Z`#^GKm_e zgPCWSBud7N{gm}$;QIx0Yy%Y&BK4g7oJ&pyZFO#FWm)zz)U~ISW?g9!cMYPYKYg%+ zJfG>3d9Oy5F50oPR>bE*!PT|PckfWtJ0z>dlS;RBNzh=^7A#D#9%LSIy&(ERjYLH3 z+1&FBdgROW7|faI9rWW+nWXF(lDzo5n$W`&k+{wK$IO2@z-@feYi09!N52_Ad$uxF zL+|MAsAq3qpDOHEb+fFj!DWl~yfU>w*+Z9Z>c*_vaL?@#vlsRHV`K6les9Cx$wFO^ylzb?ujC0VxlWr-la&oXp*2A2=pH5Y*b+LMy8rX8> zdk>k6oPDA37ta`x=Sh5ij<_j0kKe1kSVOzt=QQlU$GT zvf|U>eeLH&yn6aZ%{4EJe!=j_M7cYu^(zP6&XLo6)f%`_j!^m;Dh<5Sld zlB-?*b<-5lWU1i?s&`$R?Z5N<=j8({sc*7U?YEw=yyv3Lc`SN1N6jI&&fHUM!S(D3 ztqrjx{Th*ZTfV0FjejD#_*$&nkm?0jhfC_2mkzu;Hdl3BzcIrLS@CHuS=o;g9!>ag zP~5_3Fh%ml=u0MdR0FEc#X6>nRs15)JMikH%~76IHynH8uI%bLJM_l#6QkwDeC|Hf zje31^w6h9Tt>+tmmAg7IkFLm;e0CHmFu(1zWA*Dkx6e>Z?n&m?+-$f}*M}r8eh-1j zDU67(WoZ@?#Vy-1JvN{3asSZXpquVf2ir~YZIX`(nV_Zb%owP$WAZ?|gg)#WU(3#D zHaRVcO_d+2T0A*m<-MKKBwo`tX@R%Gaow;+i9RRa#p)?;d`MZ7TC2e@ytj2_RpYuv z^IMY+_<5Orn;!kx*t4MJQkwl4i^td39bCNd+)wdx%2P9gazzra8JYL8sa*Um?atmiJ7@}j99lTxMiAKEomiJbI5_|-M@SEJ9w;IorIk4d=wq2gt| z>S0sk;i@EFc-PhDDA#RnJ)bS{r2pFO6Ac&iiRhoqeb1Bm&UE`}cxQ*9v;H>Ei}hD2 z%2P^ZwjJ-8VWTSR%KSNC)}=YqmugnKhMUgMB=L@JV+B3jjZju|^=^3LcIC^NDR#jp z1`W2|zR>5PQ~m@S`ZI}xE@rJ;CwV6JmD`tbwXVYP!3004+3NK++Y{nOXqr9Te}=sO zMel#Gqs%>X=9%Fdw~KKPtOC!^*jZR7F8=BMkkXy&%Qp?WEm`$;uho>gnFCrk85+*@ zeyko;ozMN!Jg#QgzSv_cO$V&l;6svk3?_w;GH(k zne_6Kue4RRxViYiJ)6|j**k)b)6-1v4WiG_ZrQNj{T}V=v+_Ox?lnIGa`(KWgt#ik z$W@*!-dnz}VEHc+?^rVLQ^#c&k9evcU%scXa$BN9mQ-9H$9!}q+-ADJ-i`mxf zU-IFdamqmbTNTM4u8k{q-@4p7<=g!eTYGQWd*|9F1=Wz_teDE{KJq1}rOggSjosiu zlGmEdTX22d%7&c}jfdsvHlI%!VODZ+i`}7~Pqni88S!j-_6S{1UlQ&2`IEF+on`)V z<4f^g3e)FLml)%bS93=-guTX}#A`$5-8sM^g}1Qp`p;5#UV8PP;lY+`BfwuUczR`+*VJeEFI^T<`F1BsuN6MA~tS`DMW7wKE( zsaM~aef9C`ZHL!bDBscvC(jS|WZoP5?VtF5`I+i5dY|;>-WQ8(qrOqn#)bbn=%4uP z<@f-*_f||=9YZUXzSU^;t+(@(H`RNOm?81_jjg-xfrj2!zLDgeK<4#)8&4f{*V15r z;SVDXjm=|H6at#mgUi+3)}J&Qd2mrdYCq=JJM(Wc`?HjzSB`wq`};+^q8&90<5adh zG!ITZK9R)hK<3@(I@^I;dTMk?Wm#I(w86f+@2;na>vC5)N?9Cc7Wj^rmK#3DY|hGB z%}1##-<(@sos#06xB7NbtH-jMokjC^IFooMl6lW<@;GuUv+!5Y4QHYkH=2dkdTBUVoReL9L5ISSK1Q{>5_L2+L3rCk$FEh zx$S#VtzhBa{A_{8fz{P_^M)o3vAb5IR4HZhz;SR~{9+%~TkF;iaPneLUb^t(z4J4M ziEhoQ{(M2Q_xYkXS87STlgYeGOPoz6r#M;;_q?Yvd)1Cx+18tF><--!AHMf))X1J^ zUKxI{X&g>DeL|73)}iknbp^c%3$yhkgjt!&z)?1n-5 zY1Rqb4)jhAXWSig=ZCge%-8qHvLroBCG)1|4fy3&zl)hIG0x#wQTfa;8;Ztfk99#= z;k}dUMkh-CaIIWhr&4?L;^8w_1}wj%FUmS+ZWrKvig)cN*Jpgx8xrp{GOsUN>Wu6V zqt{}uclW%Mr)#S-;BegAmG`2(2H&aibaR|-((GGs*HML8GPwWlx>4h*Jg*gHuHK>N z)nodA#Qr+_dy#mjlX)K|ZgFamH}E}`SZkAc?5%e4#(mQoR;xc}^&cPj#-VcenHJ6U zkB{0g=8J4q<`t?WA4q3-QIxF3MyRVxFIh6$n#Aiw=1u!v%2;6S?^ft#nJm*>IXY^H z+PpXBxxsrCnB@kqucs?s>p6VnWsmy@6E_W7YgAJ@d67iS6?g4{$0L_zs>^ISOX5Y} zU0_E^t4;cO#xgfqZ{Q^bsZ(zjoIQ8I;$qXt_4z%M+&N5e{*BghrVNy4UU~1_xSiIn`UnZp4geU6>~_u&Sc*7yUXL( z6pYX4XQMcnZM$r$_!>sEOKFscoQphtRj&ayx*?Od+@8MbN{dRx=cLQ`PX^yI8nd2t zd!gpbiqgI(qra1QUC6xd@)dOtvSylpGuYOL@oCG<`I~(FYh2Pen%c{}0ti&(n=%7jI4_QW%l%pIr+D_Zt^_3`Liv~A{wUd5Z}s~&EvBflSV zBlE@^X1&jL>|OWkSVmrRYf-N4d?RU<^?8o&7oKhk__8-+(yi=TLuqBnnYZ69^Rqao zqcAqX%d^~d?7ibRMtn3XDJIG5PUa0$TT#S%ov5B~F`?hYVU`cn-fiduTj*gzu^6GWQ_js-42UF)+=Dgw7HZ}aXc_l1Taudb5tYpSK%UsK*oPkdh4>$$& zNiKZPPCxh1<8!3psNCp2Ci?dFvL3dluh2WH(;>+fYl*2d0X# z=J#HIGgc8zcN%XiJ*(!@7ehsb6aHU3)T>Fn_`4!RPieT}U>dq*fI77*C0yqC-n@M; zYd7y(^Ro8yq@>p}1JAGd)vI_1+s2P;KW1;rj>9>(4()n+ep$H!b)VO*oXM3xo{)I) zcSrlrY*5*a^WqScB61Zc#DJCa*NOn z(gm6|+q9iO6&hHq6D>&joHEQ}(&@T7jRX0t3i3Gt{{DmD?f3AQ^X@V(JH!4f#V%uU z#j;5ydrl>t$kr&muGGYy?Ad#wSA>d>)Q3LlVv^r;I49QYdJQ^K`(#Pg`o5b*DyNb6 z)w9WZIP+rA63HT27g6oeO-shTXI*+w>iKSFTzHJWve=fQ?(+}~SuWUW@r{XX); zz9z-z?w^x)cDwC#e;<$03g&%{NP3`=d3P6I^QXT(Ds7nGZ_bAetA=?^=zm+PfA6N0 zsinCIy5jEkZ$w}Js;>*m6&ti}U$I{ATG`_}^VvnYYetk+H4HQ)_cMPouVU{+4<#q} zidQYYEd6u+=xL8k24wnAc*2_=_~InJwQph3jY|^mv+pl^@@99&*DK;mlW&hoF_7oH zD$E?5uSg^B9|Fj{)+GUA7n|sM8EPg{_hvari|REkm9H=8zjE!Q{&$l0o+*4$60~Z8 zRj#Vz$*U0 z)-64mm}>oQXTKW#v~kmvT$OY8O+J~tm~P~;Hr#=i#dw+IGk;Cl%)%)R(Z9WCpp$vG z?W<1oPF0`la^mfeBzcX?6OL~_-lF!*@T7wEdA-1Z;=3+i^wpYN#SfgeYU-EL*jmZ_ zHu31Rc{*cd(v9n99VdTRfxqKJ^pu>G_%n+04_B`q@@2*01(!CTn^DxtySqb)omZLO z)T-CZ`%8mXwb+N^_(w&LjDHxte6Vq~3vmljW5kB|W{_M9cUt z=K&)=UTyxcqepi$=@TEvU-{HHIqhV?h*v$DYHZ~)o{!m=VYHon(NXkPFWuf23B|z! z%;dg0jwR_ggv{$bFHc+U$RmqEX|p!HpY-kN#8)c$j!RDNnWv?=V9}nwVSZI_&&*W4 z9JRi0w)0|M)3d&Jlhwn-Z*qd3zE!9lKV>k97k#IQ9mRHpSJ2?YPx~s~J0>glxR7SF z|NXkXG0!4p-izeq>-19Yvv+ZL#OiGh>Si z)792AVeL}IqBH|@y5la%#cJen5k}@+r?9ZZZ)Mt}9zluc3S7fN=J_qlxWB@1Sg31F z+9{=()f+dfebBiUJf#2nzJ{TyeZOzK5GOlj(Z+p&=kzl^D!(J2?}U?iFBY6rU2jX9 z=KQXZvQ&BdTTa}&$VRWZdBdWNuf4Dz_1RA4id#~owdUYw^{jCRO0!x6hN|@R{~|8? z{rYITFNJO-Jw%Xs8xN;nrG6Rjz3Iypr`!4(x-|;-rrS3ds(QK}qdl>^-)1PuldNOOi9^fq$ydB4PHiw2ZE9Xo&H+p5r%MY0$(Y6v7f+ki*Oz%tHm&_sat_&M*+`G@S71zcT z&CB>!J1%9@1EccZ9u4l3ioYkTCz;)47;e?S->;!pvvH556<=@c_0@RUx=7Eo;^|la zA+=HRBt78ob4m37{StWpw>k9v9{$^z&*7sx_#P@A-NSz;a0`4e2jAMn|8LXF|Eu~E z?IHMEp(@ZAz5nW-XbPkk{5MT?^#Z~fy6CT5{&oM`G=Ta6{#&rh@NpJ=Ul{+tO$UGL z`&R?q(Excpr>XP*?s9kVf6Dv6aJ%#;Rs6mGR|9`F@K*zWHSkvhe>Lz|1AjH}R|9`F z@K*zWHSkvhe>Lz|1AjH}R|9`F@K*!>pKCyrEqI^)N)XCeaybFI3?`RH3lG=D)*^LX zICQ#|nxVcLmob;l3er(CR-=JgJtiv%URk!e)?a*wZlLc$(0MWbdnPE3&ZUvZe~Sct zgMsqEYiJQQ@W`R>C(u234Nd&5H^;OnQPxf5RgV{Cw~Wz%K+J3w|zm^qmjV4f@UnePcHp zJPo`*_yF+edp`7AFZ4Up;owJr*8m?6UJv{{@KNBS!6V%vT_N3|?=R4|NrB)^!K2?v zq2Dwaf=9neqk`83uLWKod<=M`N~Fd}@TfnaZ@eaeN537I2p)ZtKNKM58N3^K^zAG9P8EGqioT1s1wS6V9e8{2=(kIj;KzbT-*MW2 zN5A9g16~$9>SM#e8-bq*UJ3~aZV;+8IALv$>Vb5Dbb{IewFlxuZ7m8OwL5Bm3V3nw zVgira4z(fDXDE2oPDr0fCrGER;9bBYT_Bwx{U8~Kf=BJI2EH$NBtNPLyw@lC3hFZh zz{`P0>8PKf5cM(C*HHgL{R~~>eunzh5b&svp}sa8ydiis@TeU}fJc2o2fP+|P4J_@ zj|8s`9<{T9Ag&Lg9(Zf;s9llHQ9B@gqc%ZpgZhdYcoXnQ=cvD+K4b$P;SdJ37iu@u z-l$(8{5bHazo0fneZ?6(YHug-sIQKABy_=5KV?+=~^J^(z@SrGVO@DbpVzQVzWfoFnO1CQDf^@EAg@E^ECiTqH;(#z1) zC(;~dH`BH`!=Qb3L+uHDAs*Z5lafL>xZ7q~6I=3=67@CGg@1gVL)*n;TYpkw{Ic4= zyuns70b`=8k1$C0P>*SDjC2j4DD>?QVP_ZH^%N7;2LTMs^+j$0MjqI(fd5DaM)Tc5?&E)j;`a@f8twx=n;fUdCxTx@X@QcM5?y2jr}wAtot z>mk_MFt%E*d8&qe+ntktITu_4Y0*xY#~)30qRwP@073&Re}vk=u14zM+BYy}lkU=BmFVSCxwKCeIm=sVaFHnzMA zDbPO<2HUL0HgXZ0A(7N!Pc+gsgY^Lxcv$D-vHe}Zpq>b9*wQw(>>LrE~U1=hZ?bzuTy zgjxaH`^NT##YCY~>l#3P@YiwJ!myYqnnKXpjctBo8^ge+uM1k}s2pf*Vvfi5!m)i~ zLIPqmVvFL~f-y*q`Wr}%ZGB_g!hpe}9=1KG6|mJ{ND=hwctHxbPu!k@ErMeU#E^no z0ji2^i(^~H?HFuL99u7j6f`G7O|iXkY~L7C1T!DDY>q7{Lki5-NORcsIkvS7DM$k7 zqu5G1w#p1Cf?kE~sbl-ikRs?+*upxt=uEH?dc!ukfz=sFjjg6*E6tE%tZRbC z+@E#*XN_VT?bs$XunFd!4kSPeDoB9sykoo4gaptEuw$0vTY|@yr->9|dH!?zVC(PL z+O(J`>IOh2g6+Ly`_jOM_B)V-Ey809)Q}=r8?h~TY! zZ<EaF5%iEu zNP#D)_^l)2b+nhz{rihrjYV5K6A?uv;&~<>+u#O_V9e1u92SR4kHOZuAqCAlP$@0I zz;jJJw&x8gumU5g9RzIH;x~aYfbE-zr84mx7msazLkb$ez*Yt+Xm5_KeM1UtX-srS zi_}93+NEK8;E*DyPcx*T`e2LUL^;G*Py)f=xh)>s2yai(gcLNtVJqV8Y{wu4m4oex zLkjBhP@fK0bYj%g;o%OR5#zC~aln}I=YtR$ml_mK3&z&R+sldMFsSH@Xl!SkNHIin z)gRVv&`t-dGmHeJw^Z1~gSz9f6?0%iy%(frM>1mrSb=nG$DBwZr0#GHHbEon&wKKJ z?(F(P-{Cj+H^K854u3z0;+LgsAH$<_n6z+b9*4mUwo)@_e`GUndt@8Agg;ciWb;Ef z9e}4hgN`2VAVV;N7ZT}j5yXmQ26AGl9IjpvixW(za(RKn__hz);#hbpJ3KO&!KA{Y z6eEzqi?v~~>CCxwHk%&KVCn_1m^=`F3mrjLFed28BzmS9J&oJ zguw}nru$O^=~3LNH8ju-;N!L00H%UoyQPIRmC)Y7$%+Zb}aXAvUWwj4)hHi9o&UBLUV!e+VUUJfq;KOg;9te&pSc=vyTB@ z(Ax-AbPC6P6>zA_Lc74JL}z7oL4Ro7H0+G$--W{j(;28sg$sB>!gvo>hXX$g{7Kg#6(wJNZj~>IL5)aP)FwNj; zN4WGj7l@B9spz5Eyc0dPmj%f72y{EN00{4@gop@+rWmRzltnZ(|7by%$BASHpsI#5 z0_a4&Pz{;E)M!Q^FGN^Fv?&6$MOy?4fRTViLOPmn+5K!13Qef^tGlB31A&CS` zq!^Xh_jiJBZzxpfwpJtN0-|I>i(Fo8IGqc99eQo&jX=rTjT(JT13VSS_vX}$JLZA1tGQ!SjFbfV>dj1D09 z#~r8|>SMxN8V54(CKU{eNH4TJe13VJxV|bJ%UDs#zNB#odYQh z4xJjrVMTx@(Qp(}%?@%yp&i^M)YqBF9mxZ99o+^+cHTGeDHL$vOJb08wru@FO&A{7 zrZ}{f;tz3{e)R-fPvJ`kaWD!@{LEA{*wb_-M%%m(u(sLOKUI8>#{ch597FOX9sOHIftFc1??f4F-=DMi)lk zL2f9egS*6pPV_w+Htd-7z@yn9NQmgaX9QY|fqsnI9)=!23xDC(r8A>orelZG0_XxL zofpmGgy{y*s6mWyIzJl~UJ<~V!k>5Xy$Vf5h&FFZu zOH%#2d4TKRHwoo-#@N=gpr-;0IoiJQ5Yi%?0E7x4bV}^G`11)ourdSDM9U_|6ZFi8 z2(&>I!qh=-pzh#q`=TX;4WH`*1-@*TQ3#GCK0rw^*cqaYnDFHZPVNB(|5MQmH~x?c zrt{E}L#IWssR1-*06iRS8##0?o5ke9qjMOs;u=zk6D~R@oD~h*XBwAIg+o!U4VMAW z=X5TM6B5aVJyHM-7B*tr8O~ybMY7xDXnBIYG(5FOPy=Z^8kNpOJ2Fln4AThM8gscY zmicAcaOXzQI018MVR}dtR30OO&Jm)M4)a4j+rjO-%RqV%oddHRnv0n90AYjqpIHfg z|BmibQ+H59g6d!OCOS0Hgb@sM4uJI`r0TzC1lr$k5KCrfo#J0j0yO`+2Q}?{5B0Cs z2Bv@AqY|h32E+u%gx)|rWTAzKXf7f<71nEb8O#Xa+VEo8bS}Ro|B)m_D?(5pQAg## z^oEYygpZz%fPdx_EVV+!{JpJ!>GvCiOr15}ZyE%O-{Qo!rL%`B+{@5N2Lf`mOAj}{ zPYA&9`;9Jfw9i3+Z;y0ohDYya0EGh37#wt>RwQ<>{*gg2T!l0MDHkx1VuaA0DXzU| z^XEFC_~)(m-J1|8{yB#N4?Jv|bYVJ2q-_&3zaM zJC-5k0xD7r+N$%XHDdFD3IlB0HPKeY@vlGHHNp!bHhRVv`rPr4oIv-FJMBvJr-h?^ zMj(}_rI3;Fdqzb08P2^8AdsW|^dAfWgc zU6K*Ka0CPjAU|4)gm)YMqcR*>0;ug8ke#1mlg>Q>ONv4FGve2c5y570cyJU!=Y|mH zsl*N$bwVmyjEMy~05;CTwoYxip^&!gF0E(nB@$3L8{u!ziPxVCTvl@}!V*FDg3=MQH> zMFLi#>Cig`54%D}27Yl1WcZS3e_}Mi>OtdhXt4~guz7=&3s^`o;*7boIsRYG037XY z0_&ckDS!48WDKE)v!PX@V7CXme>fRr1j6$cBM9EV!Q@PwI&j%=B2OIQ32UYHOi*Hb z1Zv;;^3qlkK-;dn)Ga%&1yBm9BT@*w?(SqnRR%SJ6&Oi8rV%ST zIye;4UGNf)vH>wU+NJ9MRwfYq76;V}UH}BZj}y>q5@GGvmIr9suAz3FFU)N(Zx9l{ z$U#pNg3^W5pZH1?!O+*D#4dmsDBMUkn(Me|a)2k;KOYhOo)L(Czkyn}(@7hDGXQ9O z*(KeRUS9!}6yv{w?4(Qazo|t)CNG?0jOyO`wxsH7~aE)fm!A`=8g-%58PaoY<;IHx9m_<(3QunNN5`p<77V5|yig?7<6mu`U2V|1L2 h%!{TY;#{=-3yt25%B+|`#-!(<$?eJ delta 848 zcmeA_&$L);f}W-A963zvM0o{k1=O_RHBDtS5^wfC2Bs zaQUoAUI+(4Mgy7R3=9o#fwVG^J_n@bfwVSMd^eC52J(yZa}rZ385q2Qd;uWe2}pAy zsYB;a7GTMkyn;P|v1sy7c6&yF$(9`Uj0KY`IqX?aFfuTBOt$5)28#X!ipEX0<`u86KKn4SY z7i=)lTVMdA;>^^Eik7#N}_|1UR|hNYdGvX@KdFN;_YlsDEh2FYtr_N|aKgQc}r z=Nq^0=d!GZ>IX(8EH(bP>~UDA_UkU7j0rH3jX^S#=Tt~YYXhAFO18ml9s!c&?`ASF zn(7(o839F|CZDg6_CLskNJV I^2KHQ0Id Date: Thu, 7 Nov 2024 13:17:47 +0200 Subject: [PATCH 03/13] style: run prettier write --- script/Base.s.sol | 2 +- script/DeployDeterministicInvoiceModule.s.sol | 8 ++- src/ModuleKeeper.sol | 2 +- src/Space.sol | 54 ++++++++++--------- src/StationRegistry.sol | 8 +-- src/utils/BaseAccountFactory.sol | 16 ++---- test/Base.t.sol | 31 ++++++----- test/integration/Integration.t.sol | 6 ++- .../create-invoice/createInvoice.t.sol | 39 +++++++++----- .../pay-invoice/payInvoice.t.sol | 3 +- .../transfer-from/transferFrom.t.sol | 6 ++- .../withdrawStream.t.sol | 12 +++-- test/integration/fuzz/createInvoice.t.sol | 11 ++-- test/integration/fuzz/payInvoice.t.sol | 11 ++-- test/integration/shared/createInvoice.t.sol | 19 +++---- .../shared/withdrawLinearStream.t.sol | 5 +- test/mocks/MockBadSpace.sol | 32 +++++++---- test/mocks/MockERC20NoReturn.sol | 5 +- test/mocks/MockOwnable.sol | 2 +- test/mocks/MockStreamManager.sol | 2 +- .../helpers/computeNumberOfPayments.t.sol | 18 ++++--- .../concrete/space/fallback/fallback.t.sol | 2 +- .../unit/concrete/space/receive/receive.t.sol | 2 +- .../withdraw-native/withdrawNative.t.sol | 2 +- .../create-account/createAccount.t.sol | 21 +++++--- .../updateModuleKeeper.t.sol | 4 +- test/utils/Constants.sol | 2 +- test/utils/Errors.sol | 3 -- test/utils/Events.sol | 40 ++++++-------- test/utils/Helpers.sol | 27 +++++----- 30 files changed, 220 insertions(+), 175 deletions(-) diff --git a/script/Base.s.sol b/script/Base.s.sol index 8c941cc..7278470 100644 --- a/script/Base.s.sol +++ b/script/Base.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import { Script } from "forge-std/Script.sol"; diff --git a/script/DeployDeterministicInvoiceModule.s.sol b/script/DeployDeterministicInvoiceModule.s.sol index a8d3081..6ff7639 100644 --- a/script/DeployDeterministicInvoiceModule.s.sol +++ b/script/DeployDeterministicInvoiceModule.s.sol @@ -21,7 +21,11 @@ contract DeployDeterministicInvoiceModule is BaseScript { bytes32 salt = bytes32(abi.encodePacked(create2Salt)); // Deterministically deploy the {InvoiceModule} contracts - invoiceModule = - new InvoiceModule{ salt: salt }(sablierLockupLinear, sablierLockupTranched, brokerAdmin, baseURI); + invoiceModule = new InvoiceModule{ salt: salt }( + sablierLockupLinear, + sablierLockupTranched, + brokerAdmin, + baseURI + ); } } diff --git a/src/ModuleKeeper.sol b/src/ModuleKeeper.sol index a794e6d..fda2c70 100644 --- a/src/ModuleKeeper.sol +++ b/src/ModuleKeeper.sol @@ -20,7 +20,7 @@ contract ModuleKeeper is IModuleKeeper, Ownable { //////////////////////////////////////////////////////////////////////////*/ /// @dev Initializes the initial owner of the {ModuleKeeper} - constructor(address _initialOwner) Ownable(_initialOwner) { } + constructor(address _initialOwner) Ownable(_initialOwner) {} /*////////////////////////////////////////////////////////////////////////// NON-CONSTANT FUNCTIONS diff --git a/src/Space.sol b/src/Space.sol index a97833e..16aaa56 100644 --- a/src/Space.sol +++ b/src/Space.sol @@ -37,11 +37,11 @@ contract Space is ISpace, AccountCore, ERC1271, ModuleManager { //////////////////////////////////////////////////////////////////////////*/ /// @dev Initializes the address of the EIP 4337 factory and EntryPoint contract - constructor(IEntryPoint _entrypoint, address _factory) AccountCore(_entrypoint, _factory) { } + constructor(IEntryPoint _entrypoint, address _factory) AccountCore(_entrypoint, _factory) {} /// @notice Initializes the {ModuleKeeper}, enables initial modules and configures the {Space} smart account function initialize(address _defaultAdmin, bytes calldata _data) public override { - (,, address[] memory initialModules) = abi.decode(_data, (uint256, uint256, address[])); + (, , address[] memory initialModules) = abi.decode(_data, (uint256, uint256, address[])); // Enable the initial module(s) ModuleKeeper moduleKeeper = StationRegistry(factory).moduleKeeper(); @@ -153,9 +153,21 @@ contract Space is ISpace, AccountCore, ERC1271, ModuleManager { // therefore the `onERC1155Received` hook must be implemented // - depending on the length of the `ids` array, we're using `safeBatchTransferFrom` or `safeTransferFrom` if (ids.length > 1) { - collection.safeBatchTransferFrom({ from: address(this), to: msg.sender, ids: ids, values: amounts, data: "" }); + collection.safeBatchTransferFrom({ + from: address(this), + to: msg.sender, + ids: ids, + values: amounts, + data: "" + }); } else { - collection.safeTransferFrom({ from: address(this), to: msg.sender, id: ids[0], value: amounts[0], data: "" }); + collection.safeTransferFrom({ + from: address(this), + to: msg.sender, + id: ids[0], + value: amounts[0], + data: "" + }); } // Log the successful ERC-1155 token withdrawal @@ -163,14 +175,12 @@ contract Space is ISpace, AccountCore, ERC1271, ModuleManager { } /// @inheritdoc ISpace - function withdrawNative( - uint256 amount - ) public onlyAdminOrEntrypoint { + function withdrawNative(uint256 amount) public onlyAdminOrEntrypoint { // Checks: the native balance of the space minus the amount locked for operations is greater than the requested amount if (amount > address(this).balance) revert Errors.InsufficientNativeToWithdraw(); // Interactions: withdraw by transferring the amount to the sender - (bool success,) = msg.sender.call{ value: amount }(""); + (bool success, ) = msg.sender.call{ value: amount }(""); // Revert if the call failed if (!success) revert Errors.NativeWithdrawFailed(); @@ -179,9 +189,7 @@ contract Space is ISpace, AccountCore, ERC1271, ModuleManager { } /// @inheritdoc IModuleManager - function enableModule( - address module - ) public override onlyAdminOrEntrypoint { + function enableModule(address module) public override onlyAdminOrEntrypoint { // Retrieve the address of the {ModuleKeeper} ModuleKeeper moduleKeeper = StationRegistry(factory).moduleKeeper(); @@ -190,9 +198,7 @@ contract Space is ISpace, AccountCore, ERC1271, ModuleManager { } /// @inheritdoc IModuleManager - function disableModule( - address module - ) public override onlyAdminOrEntrypoint { + function disableModule(address module) public override onlyAdminOrEntrypoint { // Effects: disable the module _disableModule(module); } @@ -202,10 +208,7 @@ contract Space is ISpace, AccountCore, ERC1271, ModuleManager { //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc ERC1271 - function isValidSignature( - bytes32 _hash, - bytes memory _signature - ) public view override returns (bytes4 magicValue) { + function isValidSignature(bytes32 _hash, bytes memory _signature) public view override returns (bytes4 magicValue) { // Compute the hash of message the should be signed bytes32 targetDigest = getMessageHash(_hash); @@ -230,20 +233,19 @@ contract Space is ISpace, AccountCore, ERC1271, ModuleManager { } /// @inheritdoc ISpace - function getMessageHash( - bytes32 _hash - ) public view returns (bytes32) { + function getMessageHash(bytes32 _hash) public view returns (bytes32) { bytes32 messageHash = keccak256(abi.encode(_hash)); bytes32 typedDataHash = keccak256(abi.encode(MSG_TYPEHASH, messageHash)); return keccak256(abi.encodePacked("\x19\x01", _domainSeparatorV4(), typedDataHash)); } /// @inheritdoc IERC165 - function supportsInterface( - bytes4 interfaceId - ) public pure returns (bool) { - return interfaceId == type(ISpace).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId - || interfaceId == type(IERC721Receiver).interfaceId || interfaceId == type(IERC165).interfaceId; + function supportsInterface(bytes4 interfaceId) public pure returns (bool) { + return + interfaceId == type(ISpace).interfaceId || + interfaceId == type(IERC1155Receiver).interfaceId || + interfaceId == type(IERC721Receiver).interfaceId || + interfaceId == type(IERC165).interfaceId; } /// @inheritdoc IERC721Receiver diff --git a/src/StationRegistry.sol b/src/StationRegistry.sol index c19b638..cbb0c30 100644 --- a/src/StationRegistry.sol +++ b/src/StationRegistry.sol @@ -111,9 +111,7 @@ contract StationRegistry is IStationRegistry, BaseAccountFactory, PermissionsEnu } /// @inheritdoc IStationRegistry - function updateModuleKeeper( - ModuleKeeper newModuleKeeper - ) external onlyRole(DEFAULT_ADMIN_ROLE) { + function updateModuleKeeper(ModuleKeeper newModuleKeeper) external onlyRole(DEFAULT_ADMIN_ROLE) { // Effects: update the {ModuleKeeper} address moduleKeeper = newModuleKeeper; @@ -126,9 +124,7 @@ contract StationRegistry is IStationRegistry, BaseAccountFactory, PermissionsEnu //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc IStationRegistry - function totalAccountsOfSigner( - address signer - ) public view returns (uint256) { + function totalAccountsOfSigner(address signer) public view returns (uint256) { return accountsOfSigner[signer].length(); } diff --git a/src/utils/BaseAccountFactory.sol b/src/utils/BaseAccountFactory.sol index 6892c94..f728f9f 100644 --- a/src/utils/BaseAccountFactory.sol +++ b/src/utils/BaseAccountFactory.sol @@ -75,9 +75,7 @@ abstract contract BaseAccountFactory is IAccountFactory, Multicall { //////////////////////////////////////////////////////////////*/ /// @notice Callback function for an Account to register itself on the factory. - function onRegister( - bytes32 _salt - ) external { + function onRegister(bytes32 _salt) external { address account = msg.sender; require(_isAccountOfFactory(account, _salt), "AccountFactory: not an account."); @@ -112,9 +110,7 @@ abstract contract BaseAccountFactory is IAccountFactory, Multicall { //////////////////////////////////////////////////////////////*/ /// @notice Returns whether an account is registered on this factory. - function isRegistered( - address _account - ) external view returns (bool) { + function isRegistered(address _account) external view returns (bool) { return allAccounts.contains(_account); } @@ -147,9 +143,7 @@ abstract contract BaseAccountFactory is IAccountFactory, Multicall { } /// @notice Returns all accounts that the given address is a signer of. - function getAccountsOfSigner( - address signer - ) external view returns (address[] memory accounts) { + function getAccountsOfSigner(address signer) external view returns (address[] memory accounts) { return accountsOfSigner[signer].values(); } @@ -163,9 +157,7 @@ abstract contract BaseAccountFactory is IAccountFactory, Multicall { return _account == predicted; } - function _getImplementation( - address cloneAddress - ) internal view returns (address) { + function _getImplementation(address cloneAddress) internal view returns (address) { bytes memory code = cloneAddress.code; return BytesLib.toAddress(code, 10); } diff --git a/test/Base.t.sol b/test/Base.t.sol index bafd1df..a5b37e2 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import { Events } from "./utils/Events.sol"; import { Users } from "./utils/Types.sol"; @@ -98,8 +98,11 @@ abstract contract Base_Test is Test, Events { } vm.stopPrank(); - bytes memory data = - computeCreateAccountCalldata({ deployer: _owner, stationId: _spaceId, initialModules: _initialModules }); + bytes memory data = computeCreateAccountCalldata({ + deployer: _owner, + stationId: _spaceId, + initialModules: _initialModules + }); vm.prank({ msgSender: _owner }); _container = Space(payable(stationRegistry.createAccount({ _admin: _owner, _data: data }))); @@ -118,17 +121,18 @@ abstract contract Base_Test is Test, Events { } vm.stopPrank(); - bytes memory data = - computeCreateAccountCalldata({ deployer: _owner, stationId: _spaceId, initialModules: _initialModules }); + bytes memory data = computeCreateAccountCalldata({ + deployer: _owner, + stationId: _spaceId, + initialModules: _initialModules + }); vm.prank({ msgSender: _owner }); _badSpace = MockBadSpace(payable(stationRegistry.createAccount({ _admin: _owner, _data: data }))); vm.stopPrank(); } - function allowlistModule( - address _module - ) internal { + function allowlistModule(address _module) internal { moduleKeeper.addToAllowlist({ module: _module }); } @@ -137,9 +141,7 @@ abstract contract Base_Test is Test, Events { //////////////////////////////////////////////////////////////////////////*/ /// @dev Generates a user, labels its address, and funds it with test assets - function createUser( - string memory name - ) internal returns (address payable) { + function createUser(string memory name) internal returns (address payable) { address payable user = payable(makeAddr(name)); vm.deal({ account: user, newBalance: 100 ether }); deal({ token: address(usdt), to: user, give: 10_000_000e18 }); @@ -160,8 +162,11 @@ abstract contract Base_Test is Test, Events { bytes32 salt = keccak256(abi.encode(deployer, data)); // Use {Clones} library to predict the smart account address based on the smart account implementation, salt and account factory - expectedAddress = - Clones.predictDeterministicAddress(stationRegistry.accountImplementation(), salt, address(stationRegistry)); + expectedAddress = Clones.predictDeterministicAddress( + stationRegistry.accountImplementation(), + salt, + address(stationRegistry) + ); } /// @dev Constructs the calldata passed to the {StationRegistry}.createAccount method diff --git a/test/integration/Integration.t.sol b/test/integration/Integration.t.sol index 181d3d5..621d3e7 100644 --- a/test/integration/Integration.t.sol +++ b/test/integration/Integration.t.sol @@ -61,8 +61,10 @@ abstract contract Integration_Test is Base_Test { /// @dev Deploys the {InvoiceModule} module by initializing the Sablier v2-required contracts first function deployInvoiceModule() internal { mockNFTDescriptor = new MockNFTDescriptor(); - sablierV2LockupLinear = - new SablierV2LockupLinear({ initialAdmin: users.admin, initialNFTDescriptor: mockNFTDescriptor }); + sablierV2LockupLinear = new SablierV2LockupLinear({ + initialAdmin: users.admin, + initialNFTDescriptor: mockNFTDescriptor + }); sablierV2LockupTranched = new SablierV2LockupTranched({ initialAdmin: users.admin, initialNFTDescriptor: mockNFTDescriptor, diff --git a/test/integration/concrete/invoice-module/create-invoice/createInvoice.t.sol b/test/integration/concrete/invoice-module/create-invoice/createInvoice.t.sol index 6dd4c51..161384f 100644 --- a/test/integration/concrete/invoice-module/create-invoice/createInvoice.t.sol +++ b/test/integration/concrete/invoice-module/create-invoice/createInvoice.t.sol @@ -36,7 +36,8 @@ contract CreateInvoice_Integration_Concret_Test is CreateInvoice_Integration_Sha // Create the calldata for the Invoice Module execution bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", invoice + "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice ); // Expect the call to revert with the {SpaceUnsupportedInterface} error @@ -58,7 +59,8 @@ contract CreateInvoice_Integration_Concret_Test is CreateInvoice_Integration_Sha // Create the calldata for the Invoice Module execution bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", invoice + "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice ); // Expect the call to revert with the {ZeroPaymentAmount} error @@ -86,7 +88,8 @@ contract CreateInvoice_Integration_Concret_Test is CreateInvoice_Integration_Sha // Create the calldata for the Invoice Module execution bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", invoice + "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice ); // Expect the call to revert with the {StartTimeGreaterThanEndTime} error @@ -119,7 +122,8 @@ contract CreateInvoice_Integration_Concret_Test is CreateInvoice_Integration_Sha // Create the calldata for the Invoice Module execution bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", invoice + "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice ); // Expect the call to revert with the {EndTimeInThePast} error @@ -147,7 +151,8 @@ contract CreateInvoice_Integration_Concret_Test is CreateInvoice_Integration_Sha // Create the calldata for the Invoice Module execution bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", invoice + "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice ); // Expect the module call to emit an {InvoiceCreated} event @@ -205,7 +210,8 @@ contract CreateInvoice_Integration_Concret_Test is CreateInvoice_Integration_Sha // Create the calldata for the Invoice Module execution bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", invoice + "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice ); // Expect the call to revert with the {PaymentIntervalTooShortForSelectedRecurrence} error @@ -233,7 +239,8 @@ contract CreateInvoice_Integration_Concret_Test is CreateInvoice_Integration_Sha // Create the calldata for the Invoice Module execution bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", invoice + "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice ); // Expect the module call to emit an {InvoiceCreated} event @@ -293,7 +300,8 @@ contract CreateInvoice_Integration_Concret_Test is CreateInvoice_Integration_Sha // Create the calldata for the Invoice Module execution bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", invoice + "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice ); // Run the test @@ -324,7 +332,8 @@ contract CreateInvoice_Integration_Concret_Test is CreateInvoice_Integration_Sha // Create the calldata for the Invoice Module execution bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", invoice + "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice ); // Run the test @@ -356,7 +365,8 @@ contract CreateInvoice_Integration_Concret_Test is CreateInvoice_Integration_Sha // Create the calldata for the Invoice Module execution bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", invoice + "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice ); // Run the test @@ -381,7 +391,8 @@ contract CreateInvoice_Integration_Concret_Test is CreateInvoice_Integration_Sha // Create the calldata for the Invoice Module execution bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", invoice + "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice ); // Expect the module call to emit an {InvoiceCreated} event @@ -441,7 +452,8 @@ contract CreateInvoice_Integration_Concret_Test is CreateInvoice_Integration_Sha // Create the calldata for the Invoice Module execution bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", invoice + "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice ); // Run the test @@ -466,7 +478,8 @@ contract CreateInvoice_Integration_Concret_Test is CreateInvoice_Integration_Sha // Create the calldata for the Invoice Module execution bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", invoice + "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice ); // Expect the module call to emit an {InvoiceCreated} event diff --git a/test/integration/concrete/invoice-module/pay-invoice/payInvoice.t.sol b/test/integration/concrete/invoice-module/pay-invoice/payInvoice.t.sol index 5fb2473..c7d2f50 100644 --- a/test/integration/concrete/invoice-module/pay-invoice/payInvoice.t.sol +++ b/test/integration/concrete/invoice-module/pay-invoice/payInvoice.t.sol @@ -78,7 +78,8 @@ contract PayInvoice_Integration_Concret_Test is PayInvoice_Integration_Shared_Te // Expect the call to be reverted with the {PaymentAmountLessThanInvoiceValue} error vm.expectRevert( abi.encodeWithSelector( - Errors.PaymentAmountLessThanInvoiceValue.selector, invoices[invoiceId].payment.amount + Errors.PaymentAmountLessThanInvoiceValue.selector, + invoices[invoiceId].payment.amount ) ); diff --git a/test/integration/concrete/invoice-module/transfer-from/transferFrom.t.sol b/test/integration/concrete/invoice-module/transfer-from/transferFrom.t.sol index 8486d27..51acb07 100644 --- a/test/integration/concrete/invoice-module/transfer-from/transferFrom.t.sol +++ b/test/integration/concrete/invoice-module/transfer-from/transferFrom.t.sol @@ -42,8 +42,10 @@ contract TransferFrom_Integration_Concret_Test is TransferFrom_Integration_Share uint256 balanceOfBefore = usdt.balanceOf(address(space)); // Get the maximum withdrawable amount from the stream before transferring the stream NFT - uint128 maxWithdrawableAmount = - invoiceModule.withdrawableAmountOf({ streamType: Types.Method.LinearStream, streamId: streamId }); + uint128 maxWithdrawableAmount = invoiceModule.withdrawableAmountOf({ + streamType: Types.Method.LinearStream, + streamId: streamId + }); // Make Eve's space the caller which is the recipient of the invoice vm.startPrank({ msgSender: address(space) }); diff --git a/test/integration/concrete/invoice-module/withdraw-invoice-stream/withdrawStream.t.sol b/test/integration/concrete/invoice-module/withdraw-invoice-stream/withdrawStream.t.sol index 5002a08..e5d0e44 100644 --- a/test/integration/concrete/invoice-module/withdraw-invoice-stream/withdrawStream.t.sol +++ b/test/integration/concrete/invoice-module/withdraw-invoice-stream/withdrawStream.t.sol @@ -31,8 +31,10 @@ contract WithdrawLinearStream_Integration_Concret_Test is WithdrawLinearStream_I uint256 balanceOfBefore = usdt.balanceOf(address(space)); // Get the maximum withdrawable amount from the stream - uint128 maxWithdrawableAmount = - invoiceModule.withdrawableAmountOf({ streamType: Types.Method.LinearStream, streamId: streamId }); + uint128 maxWithdrawableAmount = invoiceModule.withdrawableAmountOf({ + streamType: Types.Method.LinearStream, + streamId: streamId + }); // Make Eve's space the caller in this test suite as his space is the recipient of the invoice vm.startPrank({ msgSender: address(space) }); @@ -66,8 +68,10 @@ contract WithdrawLinearStream_Integration_Concret_Test is WithdrawLinearStream_I uint256 balanceOfBefore = usdt.balanceOf(address(space)); // Get the maximum withdrawable amount from the stream - uint128 maxWithdrawableAmount = - invoiceModule.withdrawableAmountOf({ streamType: Types.Method.TranchedStream, streamId: streamId }); + uint128 maxWithdrawableAmount = invoiceModule.withdrawableAmountOf({ + streamType: Types.Method.TranchedStream, + streamId: streamId + }); // Make Eve's space the caller in this test suite as her space is the owner of the invoice vm.startPrank({ msgSender: address(space) }); diff --git a/test/integration/fuzz/createInvoice.t.sol b/test/integration/fuzz/createInvoice.t.sol index d569232..499d416 100644 --- a/test/integration/fuzz/createInvoice.t.sol +++ b/test/integration/fuzz/createInvoice.t.sol @@ -40,8 +40,12 @@ contract CreateInvoice_Integration_Fuzz_Test is CreateInvoice_Integration_Shared vm.assume(amount > 0); // Calculate the number of payments if this is a transfer-based invoice - (bool valid, uint40 numberOfPayments) = - Helpers.checkFuzzedPaymentMethod(paymentMethod, recurrence, startTime, endTime); + (bool valid, uint40 numberOfPayments) = Helpers.checkFuzzedPaymentMethod( + paymentMethod, + recurrence, + startTime, + endTime + ); if (!valid) return; // Create a new invoice with a transfer-based payment @@ -61,7 +65,8 @@ contract CreateInvoice_Integration_Fuzz_Test is CreateInvoice_Integration_Shared // Create the calldata for the {InvoiceModule} execution bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", invoice + "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice ); // Expect the module call to emit an {InvoiceCreated} event diff --git a/test/integration/fuzz/payInvoice.t.sol b/test/integration/fuzz/payInvoice.t.sol index 2b23124..44564c2 100644 --- a/test/integration/fuzz/payInvoice.t.sol +++ b/test/integration/fuzz/payInvoice.t.sol @@ -38,8 +38,12 @@ contract PayInvoice_Integration_Fuzz_Test is PayInvoice_Integration_Shared_Test vm.assume(amount > 0); // Calculate the number of payments if this is a transfer-based invoice - (bool valid, uint40 numberOfPayments) = - Helpers.checkFuzzedPaymentMethod(paymentMethod, recurrence, startTime, endTime); + (bool valid, uint40 numberOfPayments) = Helpers.checkFuzzedPaymentMethod( + paymentMethod, + recurrence, + startTime, + endTime + ); if (!valid) return; // Create a new invoice with the fuzzed payment method @@ -59,7 +63,8 @@ contract PayInvoice_Integration_Fuzz_Test is PayInvoice_Integration_Shared_Test // Create the calldata for the {InvoiceModule} execution bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", invoice + "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice ); uint256 invoiceId = _nextInvoiceId; diff --git a/test/integration/shared/createInvoice.t.sol b/test/integration/shared/createInvoice.t.sol index db17468..c32bf03 100644 --- a/test/integration/shared/createInvoice.t.sol +++ b/test/integration/shared/createInvoice.t.sol @@ -106,11 +106,9 @@ abstract contract CreateInvoice_Integration_Shared_Test is Integration_Test { } /// @dev Creates an invoice with a recurring transfer payment - function createInvoiceWithRecurringTransfer(Types.Recurrence recurrence) - internal - view - returns (Types.Invoice memory invoice) - { + function createInvoiceWithRecurringTransfer( + Types.Recurrence recurrence + ) internal view returns (Types.Invoice memory invoice) { invoice = _createInvoice(uint40(block.timestamp), uint40(block.timestamp) + 4 weeks); invoice.payment = Types.Payment({ @@ -138,11 +136,9 @@ abstract contract CreateInvoice_Integration_Shared_Test is Integration_Test { } /// @dev Creates an invoice with a tranched stream-based payment - function createInvoiceWithTranchedStream(Types.Recurrence recurrence) - internal - view - returns (Types.Invoice memory invoice) - { + function createInvoiceWithTranchedStream( + Types.Recurrence recurrence + ) internal view returns (Types.Invoice memory invoice) { invoice = _createInvoice(uint40(block.timestamp), uint40(block.timestamp) + 4 weeks); invoice.payment = Types.Payment({ @@ -181,7 +177,8 @@ abstract contract CreateInvoice_Integration_Shared_Test is Integration_Test { // Create the invoice bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", invoice + "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice ); // Select the according {Space} of the user diff --git a/test/integration/shared/withdrawLinearStream.t.sol b/test/integration/shared/withdrawLinearStream.t.sol index f78fa6d..acbc493 100644 --- a/test/integration/shared/withdrawLinearStream.t.sol +++ b/test/integration/shared/withdrawLinearStream.t.sol @@ -4,10 +4,7 @@ pragma solidity ^0.8.26; import { Integration_Test } from "../Integration.t.sol"; import { PayInvoice_Integration_Shared_Test } from "./payInvoice.t.sol"; -abstract contract WithdrawLinearStream_Integration_Shared_Test is - Integration_Test, - PayInvoice_Integration_Shared_Test -{ +abstract contract WithdrawLinearStream_Integration_Shared_Test is Integration_Test, PayInvoice_Integration_Shared_Test { function setUp() public virtual override(Integration_Test, PayInvoice_Integration_Shared_Test) { PayInvoice_Integration_Shared_Test.setUp(); createMockInvoices(); diff --git a/test/mocks/MockBadSpace.sol b/test/mocks/MockBadSpace.sol index 34c1c70..c0d3973 100644 --- a/test/mocks/MockBadSpace.sol +++ b/test/mocks/MockBadSpace.sol @@ -37,7 +37,7 @@ contract MockBadSpace is ISpace, AccountCore, ERC1271, ModuleManager { //////////////////////////////////////////////////////////////////////////*/ /// @dev Initializes the address of the {Space} owner, {ModuleKeeper} and enables the initial module(s) - constructor(IEntryPoint _entrypoint, address _factory) AccountCore(_entrypoint, _factory) { } + constructor(IEntryPoint _entrypoint, address _factory) AccountCore(_entrypoint, _factory) {} /*////////////////////////////////////////////////////////////////////////// RECEIVE & FALLBACK @@ -145,9 +145,21 @@ contract MockBadSpace is ISpace, AccountCore, ERC1271, ModuleManager { // therefore the `onERC1155Received` hook must be implemented // - depending on the length of the `ids` array, we're using `safeBatchTransferFrom` or `safeTransferFrom` if (ids.length > 1) { - collection.safeBatchTransferFrom({ from: address(this), to: msg.sender, ids: ids, values: amounts, data: "" }); + collection.safeBatchTransferFrom({ + from: address(this), + to: msg.sender, + ids: ids, + values: amounts, + data: "" + }); } else { - collection.safeTransferFrom({ from: address(this), to: msg.sender, id: ids[0], value: amounts[0], data: "" }); + collection.safeTransferFrom({ + from: address(this), + to: msg.sender, + id: ids[0], + value: amounts[0], + data: "" + }); } // Log the successful ERC-1155 token withdrawal @@ -160,7 +172,7 @@ contract MockBadSpace is ISpace, AccountCore, ERC1271, ModuleManager { if (amount > address(this).balance) revert Errors.InsufficientNativeToWithdraw(); // Interactions: withdraw by transferring the amount to the sender - (bool success,) = msg.sender.call{ value: amount }(""); + (bool success, ) = msg.sender.call{ value: amount }(""); // Revert if the call failed if (!success) revert Errors.NativeWithdrawFailed(); @@ -185,10 +197,7 @@ contract MockBadSpace is ISpace, AccountCore, ERC1271, ModuleManager { //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc ERC1271 - function isValidSignature( - bytes32 _hash, - bytes memory _signature - ) public view override returns (bytes4 magicValue) { + function isValidSignature(bytes32 _hash, bytes memory _signature) public view override returns (bytes4 magicValue) { // Compute the hash of message the should be signed bytes32 targetDigest = getMessageHash(_hash); @@ -221,8 +230,11 @@ contract MockBadSpace is ISpace, AccountCore, ERC1271, ModuleManager { /// @inheritdoc IERC165 function supportsInterface(bytes4 interfaceId) public pure returns (bool) { - return interfaceId == type(ISpace).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId - || interfaceId == type(IERC721Receiver).interfaceId || interfaceId == type(IERC165).interfaceId; + return + interfaceId == type(ISpace).interfaceId || + interfaceId == type(IERC1155Receiver).interfaceId || + interfaceId == type(IERC721Receiver).interfaceId || + interfaceId == type(IERC165).interfaceId; } /// @inheritdoc IERC721Receiver diff --git a/test/mocks/MockERC20NoReturn.sol b/test/mocks/MockERC20NoReturn.sol index 373f0f1..1216491 100644 --- a/test/mocks/MockERC20NoReturn.sol +++ b/test/mocks/MockERC20NoReturn.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; +pragma solidity ^0.8.22; /// @notice An implementation of ERC-20 standard forked from the OpenZeppelin v4 library that do not return a boolean upon calling {transferFrom} or {transfer} /// @dev Reference https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.0/contracts/token/ERC20/ERC20.sol @@ -158,7 +158,8 @@ contract MockERC20NoReturn { emit Transfer(sender, recipient, amount); } - /** @dev Creates `amount` tokens and assigns them to `account`, increasing + /** + * @dev Creates `amount` tokens and assigns them to `account`, increasing * the total supply. * * Emits a {Transfer} event with `from` set to the zero address. diff --git a/test/mocks/MockOwnable.sol b/test/mocks/MockOwnable.sol index e9db589..53e8dc6 100644 --- a/test/mocks/MockOwnable.sol +++ b/test/mocks/MockOwnable.sol @@ -6,5 +6,5 @@ import { Ownable } from "./../../src/abstracts/Ownable.sol"; /// @title MockOwnable /// @notice A mock implementation that uses the `onlyOwner` auth mechanism contract MockOwnable is Ownable { - constructor(address _owner) Ownable(_owner) { } + constructor(address _owner) Ownable(_owner) {} } diff --git a/test/mocks/MockStreamManager.sol b/test/mocks/MockStreamManager.sol index 3acbb46..2334112 100644 --- a/test/mocks/MockStreamManager.sol +++ b/test/mocks/MockStreamManager.sol @@ -12,5 +12,5 @@ contract MockStreamManager is StreamManager { ISablierV2LockupLinear _sablierLockupLinear, ISablierV2LockupTranched _sablierLockupTranched, address _brokerAdmin - ) StreamManager(_sablierLockupLinear, _sablierLockupTranched, _brokerAdmin) { } + ) StreamManager(_sablierLockupLinear, _sablierLockupTranched, _brokerAdmin) {} } diff --git a/test/unit/concrete/helpers/computeNumberOfPayments.t.sol b/test/unit/concrete/helpers/computeNumberOfPayments.t.sol index 61f437b..df5dcc2 100644 --- a/test/unit/concrete/helpers/computeNumberOfPayments.t.sol +++ b/test/unit/concrete/helpers/computeNumberOfPayments.t.sol @@ -16,8 +16,10 @@ contract ComputeNumberOfPayments_Helpers_Test is Base_Test { uint40 endTime = uint40(block.timestamp + 11 weeks); // Run the test - uint40 numberOfPayments = - Helpers.computeNumberOfPayments({ recurrence: Types.Recurrence.Weekly, interval: endTime - startTime }); + uint40 numberOfPayments = Helpers.computeNumberOfPayments({ + recurrence: Types.Recurrence.Weekly, + interval: endTime - startTime + }); // Assert the actual and expected number of payments assertEq(numberOfPayments, 11); @@ -29,8 +31,10 @@ contract ComputeNumberOfPayments_Helpers_Test is Base_Test { uint40 endTime = uint40(block.timestamp + 2 * 4 weeks); // Run the test - uint40 numberOfPayments = - Helpers.computeNumberOfPayments({ recurrence: Types.Recurrence.Monthly, interval: endTime - startTime }); + uint40 numberOfPayments = Helpers.computeNumberOfPayments({ + recurrence: Types.Recurrence.Monthly, + interval: endTime - startTime + }); // Assert the actual and expected number of payments assertEq(numberOfPayments, 2); @@ -42,8 +46,10 @@ contract ComputeNumberOfPayments_Helpers_Test is Base_Test { uint40 endTime = uint40(block.timestamp + 3 * 48 weeks); // Run the test - uint40 numberOfPayments = - Helpers.computeNumberOfPayments({ recurrence: Types.Recurrence.Yearly, interval: endTime - startTime }); + uint40 numberOfPayments = Helpers.computeNumberOfPayments({ + recurrence: Types.Recurrence.Yearly, + interval: endTime - startTime + }); // Assert the actual and expected number of payments assertEq(numberOfPayments, 3); diff --git a/test/unit/concrete/space/fallback/fallback.t.sol b/test/unit/concrete/space/fallback/fallback.t.sol index 49d321f..2b676c8 100644 --- a/test/unit/concrete/space/fallback/fallback.t.sol +++ b/test/unit/concrete/space/fallback/fallback.t.sol @@ -18,7 +18,7 @@ contract Fallback_Unit_Concrete_Test is Space_Unit_Concrete_Test { emit Events.NativeReceived({ from: users.bob, amount: 1 ether }); // Run the test - (bool success,) = address(space).call{ value: 1 ether }("test"); + (bool success, ) = address(space).call{ value: 1 ether }("test"); if (!success) revert(); // Assert the {Space} contract balance diff --git a/test/unit/concrete/space/receive/receive.t.sol b/test/unit/concrete/space/receive/receive.t.sol index 9d74f39..7a73912 100644 --- a/test/unit/concrete/space/receive/receive.t.sol +++ b/test/unit/concrete/space/receive/receive.t.sol @@ -18,7 +18,7 @@ contract Receive_Unit_Concrete_Test is Space_Unit_Concrete_Test { emit Events.NativeReceived({ from: users.bob, amount: 1 ether }); // Run the test - (bool success,) = address(space).call{ value: 1 ether }(""); + (bool success, ) = address(space).call{ value: 1 ether }(""); if (!success) revert(); // Assert the {Space} contract balance diff --git a/test/unit/concrete/space/withdraw-native/withdrawNative.t.sol b/test/unit/concrete/space/withdraw-native/withdrawNative.t.sol index d92f006..4549b22 100644 --- a/test/unit/concrete/space/withdraw-native/withdrawNative.t.sol +++ b/test/unit/concrete/space/withdraw-native/withdrawNative.t.sol @@ -51,7 +51,7 @@ contract WithdrawNative_Unit_Concrete_Test is Space_Unit_Concrete_Test { modifier whenSufficientNativeToWithdraw(Space space) { // Deposit sufficient native tokens (ETH) into the space to enable the withdrawal - (bool success,) = payable(space).call{ value: 2 ether }(""); + (bool success, ) = payable(space).call{ value: 2 ether }(""); if (!success) revert(); _; } diff --git a/test/unit/concrete/station-registry/create-account/createAccount.t.sol b/test/unit/concrete/station-registry/create-account/createAccount.t.sol index e3a0dc9..d0b5242 100644 --- a/test/unit/concrete/station-registry/create-account/createAccount.t.sol +++ b/test/unit/concrete/station-registry/create-account/createAccount.t.sol @@ -19,8 +19,11 @@ contract CreateAccount_Unit_Concrete_Test is StationRegistry_Unit_Concrete_Test // The {StationRegistry} contract deploys each new {Space} contract. // Therefore, we need to calculate the current nonce of the {StationRegistry} // to pre-compute the address of the new {Space} before deployment. - (address expectedSpace, bytes memory data) = - computeDeploymentAddressAndCalldata({ deployer: users.bob, stationId: 0, initialModules: mockModules }); + (address expectedSpace, bytes memory data) = computeDeploymentAddressAndCalldata({ + deployer: users.bob, + stationId: 0, + initialModules: mockModules + }); // Allowlist the mock modules on the {ModuleKeeper} contract from the admin account vm.startPrank({ msgSender: users.admin }); @@ -61,8 +64,11 @@ contract CreateAccount_Unit_Concrete_Test is StationRegistry_Unit_Concrete_Test function test_RevertWhen_CallerNotStationOwner() external whenStationIdNonZero { // Construct the calldata to be used to initialize the {Space} smart account - bytes memory data = - computeCreateAccountCalldata({ deployer: users.eve, stationId: 1, initialModules: mockModules }); + bytes memory data = computeCreateAccountCalldata({ + deployer: users.eve, + stationId: 1, + initialModules: mockModules + }); // Make Eve the caller in this test suite vm.prank({ msgSender: users.eve }); @@ -82,8 +88,11 @@ contract CreateAccount_Unit_Concrete_Test is StationRegistry_Unit_Concrete_Test // The {StationRegistry} contract deploys each new {Space} contract. // Therefore, we need to calculate the current nonce of the {StationRegistry} // to pre-compute the address of the new {Space} before deployment. - (address expectedSpace, bytes memory data) = - computeDeploymentAddressAndCalldata({ deployer: users.bob, stationId: 1, initialModules: mockModules }); + (address expectedSpace, bytes memory data) = computeDeploymentAddressAndCalldata({ + deployer: users.bob, + stationId: 1, + initialModules: mockModules + }); // Allowlist the mock modules on the {ModuleKeeper} contract from the admin account vm.startPrank({ msgSender: users.admin }); diff --git a/test/unit/concrete/station-registry/update-module-keeper/updateModuleKeeper.t.sol b/test/unit/concrete/station-registry/update-module-keeper/updateModuleKeeper.t.sol index 75ab7f5..8bcd281 100644 --- a/test/unit/concrete/station-registry/update-module-keeper/updateModuleKeeper.t.sol +++ b/test/unit/concrete/station-registry/update-module-keeper/updateModuleKeeper.t.sol @@ -19,7 +19,9 @@ contract UpdateModuleKeeper_Unit_Concrete_Test is StationRegistry_Unit_Concrete_ // Expect the next call to revert with the {PermissionsUnauthorizedAccount} error vm.expectRevert( abi.encodeWithSelector( - Errors.PermissionsUnauthorizedAccount.selector, users.bob, Constants.DEFAULT_ADMIN_ROLE + Errors.PermissionsUnauthorizedAccount.selector, + users.bob, + Constants.DEFAULT_ADMIN_ROLE ) ); diff --git a/test/utils/Constants.sol b/test/utils/Constants.sol index 5d67554..c7b456b 100644 --- a/test/utils/Constants.sol +++ b/test/utils/Constants.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; library Constants { /// @dev Role identifier for addresses with the default admin role diff --git a/test/utils/Errors.sol b/test/utils/Errors.sol index 3d9865f..aa90988 100644 --- a/test/utils/Errors.sol +++ b/test/utils/Errors.sol @@ -93,9 +93,6 @@ library Errors { /// @notice Thrown when a payer attempts to pay a canceled invoice error InvoiceCanceled(); - /// @notice Thrown when the invoice ID references a null invoice - error InvoiceNull(); - /// @notice Thrown when `msg.sender` is not the creator (recipient) of the invoice error OnlyInvoiceRecipient(); diff --git a/test/utils/Events.sol b/test/utils/Events.sol index 4626337..8071cc5 100644 --- a/test/utils/Events.sol +++ b/test/utils/Events.sol @@ -92,35 +92,27 @@ abstract contract Events { event ModuleDisabled(address indexed module, address indexed owner); /*////////////////////////////////////////////////////////////////////////// - INVOICE + PAYMENT-MODULE //////////////////////////////////////////////////////////////////////////*/ - /// @notice Emitted when a regular or recurring invoice is created - /// @param id The ID of the invoice + /// @notice Emitted when a payment request is created + /// @param id The ID of the payment request /// @param recipient The address receiving the payment - /// @param status The status of the invoice - /// @param startTime The timestamp when the invoice takes effect - /// @param endTime The timestamp by which the invoice must be paid - /// @param payment Struct representing the payment details associated with the invoice - event InvoiceCreated( - uint256 id, - address indexed recipient, - Types.Status status, - uint40 startTime, - uint40 endTime, - Types.Payment payment - ); - - /// @notice Emitted when an invoice is paid - /// @param id The ID of the invoice + /// @param startTime The timestamp when the payment request takes effect + /// @param endTime The timestamp by which the payment request must be paid + /// @param config Struct representing the payment details associated with the payment request + event RequestCreated(uint256 id, address indexed recipient, uint40 startTime, uint40 endTime, Types.Config config); + + /// @notice Emitted when a payment is made for a payment request + /// @param id The ID of the payment request /// @param payer The address of the payer - /// @param status The status of the invoice - /// @param payment Struct representing the payment details associated with the invoice - event InvoicePaid(uint256 indexed id, address indexed payer, Types.Status status, Types.Payment payment); + /// @param status The status of the payment request + /// @param config Struct representing the payment details + event RequestPaid(uint256 indexed id, address indexed payer, Types.Status status, Types.Config config); - /// @notice Emitted when an invoice is canceled - /// @param id The ID of the invoice - event InvoiceCanceled(uint256 indexed id); + /// @notice Emitted when a payment request is canceled + /// @param id The ID of the payment request + event RequestCanceled(uint256 indexed id); /// @notice Emitted when the broker fee is updated /// @param oldFee The old broker fee diff --git a/test/utils/Helpers.sol b/test/utils/Helpers.sol index 4fd0042..853e317 100644 --- a/test/utils/Helpers.sol +++ b/test/utils/Helpers.sol @@ -6,19 +6,20 @@ import { Helpers as InvoiceHelpers } from "./../../src/modules/invoice-module/li library Helpers { function createInvoiceDataType() public view returns (Types.Invoice memory) { - return Types.Invoice({ - status: Types.Status.Pending, - startTime: 0, - endTime: uint40(block.timestamp) + 1 weeks, - payment: Types.Payment({ - method: Types.Method.Transfer, - recurrence: Types.Recurrence.OneOff, - paymentsLeft: 1, - asset: address(0), - amount: uint128(1 ether), - streamId: 0 - }) - }); + return + Types.Invoice({ + status: Types.Status.Pending, + startTime: 0, + endTime: uint40(block.timestamp) + 1 weeks, + payment: Types.Payment({ + method: Types.Method.Transfer, + recurrence: Types.Recurrence.OneOff, + paymentsLeft: 1, + asset: address(0), + amount: uint128(1 ether), + streamId: 0 + }) + }); } /// @dev Calculates the number of payments that must be done based on a Recurring invoice From 8f2c0a3e8e670346b2b3e0036f8f7eed797a6573 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Fri, 15 Nov 2024 15:52:22 +0200 Subject: [PATCH 04/13] feat(payment-module): refactor status of payment request retrieval --- src/modules/payment-module/PaymentModule.sol | 223 +++++++++++------- .../interfaces/IPaymentModule.sol | 41 ++-- .../payment-module/libraries/Errors.sol | 14 +- .../payment-module/libraries/Helpers.sol | 8 +- .../payment-module/libraries/Types.sol | 12 +- .../sablier-v2/StreamManager.sol | 49 +++- .../sablier-v2/interfaces/IStreamManager.sol | 27 ++- 7 files changed, 247 insertions(+), 127 deletions(-) diff --git a/src/modules/payment-module/PaymentModule.sol b/src/modules/payment-module/PaymentModule.sol index d253a0d..c3b74fa 100644 --- a/src/modules/payment-module/PaymentModule.sol +++ b/src/modules/payment-module/PaymentModule.sol @@ -6,6 +6,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; +import { Lockup } from "@sablier/v2-core/src/types/DataTypes.sol"; import { Types } from "./libraries/Types.sol"; import { Errors } from "./libraries/Errors.sol"; @@ -39,7 +40,9 @@ contract PaymentModule is IPaymentModule, StreamManager { ISablierV2LockupLinear _sablierLockupLinear, ISablierV2LockupTranched _sablierLockupTranched, address _brokerAdmin - ) StreamManager(_sablierLockupLinear, _sablierLockupTranched, _brokerAdmin) { + ) + StreamManager(_sablierLockupLinear, _sablierLockupTranched, _brokerAdmin) + { // Start the first payment request ID from 1 _nextRequestId = 1; } @@ -66,8 +69,13 @@ contract PaymentModule is IPaymentModule, StreamManager { //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc IPaymentModule - function getRequest(uint256 id) external view returns (Types.PaymentRequest memory request) { - return _requests[id]; + function getRequest(uint256 requestId) external view returns (Types.PaymentRequest memory request) { + return _requests[requestId]; + } + + /// @inheritdoc IPaymentModule + function statusOf(uint256 requestId) public view returns (Types.Status status) { + status = _statusOf(requestId); } /*////////////////////////////////////////////////////////////////////////// @@ -75,7 +83,12 @@ contract PaymentModule is IPaymentModule, StreamManager { //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc IPaymentModule - function createRequest(Types.PaymentRequest calldata request) external onlySpace returns (uint256 id) { + function createRequest(Types.PaymentRequest calldata request) external onlySpace returns (uint256 requestId) { + // Checks: the recipient address is not the zero address + if (request.recipient == address(0)) { + revert Errors.InvalidZeroAddressRecipient(); + } + // Checks: the amount is non-zero if (request.config.amount == 0) { revert Errors.ZeroPaymentAmount(); @@ -104,26 +117,25 @@ contract PaymentModule is IPaymentModule, StreamManager { // based on the payment method, interval and recurrence type // // Notes: - // - The number of payments is taken into account only for transfer-based invoices + // - The number of payments is validated only for requests with payment method set on Tranched Stream or Recurring Transfer // - There should be only one payment when dealing with a one-off transfer-based request // - When dealing with a recurring transfer, the number of payments must be calculated based // on the payment interval (endTime - startTime) and recurrence type - uint40 numberOfPayments; - if (request.config.method == Types.Method.Transfer && request.config.recurrence == Types.Recurrence.OneOff) { - numberOfPayments = 1; - } else if (request.config.method != Types.Method.LinearStream) { + uint40 numberOfPayments = 1; + if (request.config.method != Types.Method.LinearStream && request.config.recurrence != Types.Recurrence.OneOff) + { numberOfPayments = _checkIntervalPayments({ recurrence: request.config.recurrence, startTime: request.startTime, endTime: request.endTime }); - - // Set the number of payments to zero if dealing with a tranched-based request - // The `_checkIntervalPayment` method is still called for a tranched-based request just - // to validate the interval and ensure it can support multiple payments based on the chosen recurrence - if (request.config.method == Types.Method.TranchedStream) numberOfPayments = 0; } + // Set the number of payments back to one if dealing with a tranched-based request + // The `_checkIntervalPayment` method is still called for a tranched-based request just + // to validate the interval and ensure it can support multiple payments based on the chosen recurrence + if (request.config.method == Types.Method.TranchedStream) numberOfPayments = 1; + // Checks: the asset is different than the native token if dealing with either a linear or tranched stream-based payment if (request.config.method != Types.Method.Transfer) { if (request.config.asset == address(0)) { @@ -132,11 +144,12 @@ contract PaymentModule is IPaymentModule, StreamManager { } // Get the next payment request ID - id = _nextRequestId; + requestId = _nextRequestId; // Effects: create the payment request - _requests[id] = Types.PaymentRequest({ - status: Types.Status.Pending, + _requests[requestId] = Types.PaymentRequest({ + wasCanceled: false, + wasAccepted: false, startTime: request.startTime, endTime: request.endTime, recipient: request.recipient, @@ -151,15 +164,15 @@ contract PaymentModule is IPaymentModule, StreamManager { }); // Effects: increment the next payment request ID - // Use unchecked because the request id cannot realistically overflow + // Use unchecked because the request id cannot realistically overflow unchecked { ++_nextRequestId; } // Log the payment request creation emit RequestCreated({ - id: id, - recipient: msg.sender, + requestId: requestId, + recipient: request.recipient, startTime: request.startTime, endTime: request.endTime, config: request.config @@ -167,58 +180,82 @@ contract PaymentModule is IPaymentModule, StreamManager { } /// @inheritdoc IPaymentModule - function payRequest(uint256 id) external payable { + function payRequest(uint256 requestId) external payable { // Load the payment request state from storage - Types.PaymentRequest memory request = _requests[id]; + Types.PaymentRequest memory request = _requests[requestId]; + + // Checks: the payment request is not null + if (request.recipient == address(0)) { + revert Errors.NullRequest(); + } - // Checks: the payment request is not already completed or canceled - if (request.status == Types.Status.Completed) { - revert Errors.RequestCompleted(); - } else if (request.status == Types.Status.Canceled) { + // Retrieve the request status + Types.Status requestStatus = _statusOf(requestId); + + // Checks: the payment request is not already paid or canceled + // Note: for stream-based requests the `status` changes to `Paid` only after the funds are fully streamed + if (requestStatus == Types.Status.Paid || request.config.paymentsLeft == 0) { + revert Errors.RequestPaid(); + } else if (requestStatus == Types.Status.Canceled) { revert Errors.RequestCanceled(); } // Handle the payment workflow depending on the payment method type if (request.config.method == Types.Method.Transfer) { - // Effects: pay the request and update its status to `Completed` or `Accepted` depending on the payment type - _payByTransfer(id, request); + // Effects: pay the request and update its status to `Paid` or `Accepted` depending on the payment type + _payByTransfer(request); } else { uint256 streamId; - // Check to see whether the request must be paid through a linear or tranched stream + + // Check to see whether the request must be paid through a linear or tranched stream if (request.config.method == Types.Method.LinearStream) { streamId = _payByLinearStream(request); } else { streamId = _payByTranchedStream(request); } - // Effects: update the status of the request to `Accepted` and the stream ID - // if dealing with a linear or tranched-based request - _requests[id].status = Types.Status.Accepted; - _requests[id].config.streamId = streamId; + // Effects: set the stream ID of the payment request + _requests[requestId].config.streamId = streamId; + } + + // Effects: decrease the number of payments left + // Using unchecked because the number of payments left cannot underflow: + // - For transfer-based requests, the status will be updated to `Paid` when `paymentsLeft` reaches zero; + // - For stream-based requests, `paymentsLeft` is validated before decrementing; + uint40 paymentsLeft; + unchecked { + paymentsLeft = request.config.paymentsLeft - 1; + _requests[requestId].config.paymentsLeft = paymentsLeft; } + // Effects: mark the payment request as accepted + _requests[requestId].wasAccepted = true; + // Log the payment transaction - emit RequestPaid({ id: id, payer: msg.sender, status: _requests[id].status, config: _requests[id].config }); + emit RequestPaid({ requestId: requestId, payer: msg.sender, config: _requests[requestId].config }); } /// @inheritdoc IPaymentModule - function cancelRequest(uint256 id) external { + function cancelRequest(uint256 requestId) external { // Load the payment request state from storage - Types.PaymentRequest memory request = _requests[id]; + Types.PaymentRequest memory request = _requests[requestId]; + + // Retrieve the request status + Types.Status requestStatus = _statusOf(requestId); - // Checks: the payment request is already completed - if (request.status == Types.Status.Completed) { - revert Errors.RequestCompleted(); - } else if (request.status == Types.Status.Canceled) { + // Checks: the payment request is already paid or canceled + if (requestStatus == Types.Status.Paid) { + revert Errors.RequestPaid(); + } else if (requestStatus == Types.Status.Canceled) { revert Errors.RequestCanceled(); } - // Checks: `msg.sender` is the recipient if the payment request status is pending + // Checks: `msg.sender` is the recipient if the payment request status is `Pending` // // Notes: // - Once a linear or tranched stream is created, the `msg.sender` is checked in the // {SablierV2Lockup} `cancel` method - if (request.status == Types.Status.Pending) { + if (requestStatus == Types.Status.Pending) { if (request.recipient != msg.sender) { revert Errors.OnlyRequestRecipient(); } @@ -230,36 +267,28 @@ contract PaymentModule is IPaymentModule, StreamManager { // - A transfer-based payment request can be canceled directly // - A linear or tranched stream MUST be canceled by calling the `cancel` method on the according // {ISablierV2Lockup} contract - else if (request.status == Types.Status.Accepted && request.config.method != Types.Method.Transfer) { + else if (request.config.method != Types.Method.Transfer) { _cancelStream({ streamType: request.config.method, streamId: request.config.streamId }); } // Effects: mark the payment request as canceled - _requests[id].status = Types.Status.Canceled; + _requests[requestId].wasCanceled = true; // Log the payment request cancelation - emit RequestCanceled(id); + emit RequestCanceled(requestId); } /// @inheritdoc IPaymentModule - function withdrawRequestStream(uint256 id) public returns (uint128 withdrawnAmount) { + function withdrawRequestStream(uint256 requestId) public returns (uint128 withdrawnAmount) { // Load the payment request state from storage - Types.PaymentRequest memory request = _requests[id]; - - // Retrieve the recipient of the request - address recipient = request.recipient; + Types.PaymentRequest memory request = _requests[requestId]; - // Effects: update the request status to `Completed` once the full payment amount has been successfully streamed - uint128 streamedAmount = streamedAmountOf({ + // Check, Effects, Interactions: withdraw from the stream + return _withdrawStream({ streamType: request.config.method, - streamId: request.config.streamId + streamId: request.config.streamId, + to: request.recipient }); - if (streamedAmount == request.config.amount) { - _requests[id].status = Types.Status.Completed; - } - - // Check, Effects, Interactions: withdraw from the stream - return _withdrawStream({ streamType: request.config.method, streamId: request.config.streamId, to: recipient }); } /*////////////////////////////////////////////////////////////////////////// @@ -267,29 +296,16 @@ contract PaymentModule is IPaymentModule, StreamManager { //////////////////////////////////////////////////////////////////////////*/ /// @dev Pays the `id` request by transfer - function _payByTransfer(uint256 id, Types.PaymentRequest memory request) internal { - // Effects: update the request status to `Completed` if the required number of payments has been made - // Using unchecked because the number of payments left cannot underflow as the request status - // will be updated to `Completed` once `paymentLeft` is zero - unchecked { - uint40 paymentsLeft = request.config.paymentsLeft - 1; - _requests[id].config.paymentsLeft = paymentsLeft; - if (paymentsLeft == 0) { - _requests[id].status = Types.Status.Completed; - } else if (request.status == Types.Status.Pending) { - _requests[id].status = Types.Status.Accepted; - } - } - + function _payByTransfer(Types.PaymentRequest memory request) internal { // Check if the payment must be done in native token (ETH) or an ERC-20 token if (request.config.asset == address(0)) { - // Checks: the payment amount matches the request value + // Checks: the payment amount matches the request value if (msg.value < request.config.amount) { revert Errors.PaymentAmountLessThanInvoiceValue({ amount: request.config.amount }); } // Interactions: pay the recipient with native token (ETH) - (bool success, ) = payable(request.recipient).call{ value: request.config.amount }(""); + (bool success,) = payable(request.recipient).call{ value: request.config.amount }(""); if (!success) revert Errors.NativeTokenPaymentFailed(); } else { // Interactions: pay the recipient with the ERC-20 token @@ -314,10 +330,8 @@ contract PaymentModule is IPaymentModule, StreamManager { /// @dev Create the tranched stream payment function _payByTranchedStream(Types.PaymentRequest memory request) internal returns (uint256 streamId) { - uint40 numberOfTranches = Helpers.computeNumberOfPayments( - request.config.recurrence, - request.endTime - request.startTime - ); + uint40 numberOfTranches = + Helpers.computeNumberOfPayments(request.config.recurrence, request.endTime - request.startTime); streamId = StreamManager.createTranchedStream({ asset: IERC20(request.config.asset), @@ -335,15 +349,19 @@ contract PaymentModule is IPaymentModule, StreamManager { Types.Recurrence recurrence, uint40 startTime, uint40 endTime - ) internal pure returns (uint40 numberOfPayments) { - // Checks: the request payment interval matches the recurrence type + ) + internal + pure + returns (uint40 numberOfPayments) + { + // Checks: the request payment interval matches the recurrence type // This cannot underflow as the start time is stricly lower than the end time when this call executes uint40 interval; unchecked { interval = endTime - startTime; } - // Check and calculate the expected number of payments based on the request recurrence and payment interval + // Check and calculate the expected number of payments based on the recurrence and payment interval numberOfPayments = Helpers.computeNumberOfPayments(recurrence, interval); // Revert if there are zero payments to be made since the payment method due to invalid interval and recurrence type @@ -351,4 +369,45 @@ contract PaymentModule is IPaymentModule, StreamManager { revert Errors.PaymentIntervalTooShortForSelectedRecurrence(); } } + + /// @notice Retrieves the status of the `requestId` payment request + /// Note: + /// - The status of a payment request is determined by the `wasCanceled` and `wasAccepted` flags and: + /// - For a stream-based payment request, by the status of the underlying stream; + /// - For a transfer-based payment request, by the number of payments left; + function _statusOf(uint256 requestId) internal view returns (Types.Status status) { + // Load the payment request state from storage + Types.PaymentRequest memory request = _requests[requestId]; + + if (!request.wasAccepted && !request.wasCanceled) { + return Types.Status.Pending; + } + + // Check if dealing with a stream-based payment request + if (request.config.streamId != 0) { + Lockup.Status statusOfStream = StreamManager.statusOfStream(request.config.method, request.config.streamId); + + if (statusOfStream == Lockup.Status.SETTLED) { + return Types.Status.Paid; + } else if (statusOfStream == Lockup.Status.DEPLETED) { + // Retrieve the total streamed amount until now + uint128 streamedAmount = + streamedAmountOf({ streamType: request.config.method, streamId: request.config.streamId }); + + // Check if the payment request is canceled or paid + streamedAmount < request.config.amount ? Types.Status.Canceled : Types.Status.Paid; + } else { + return Types.Status.Accepted; + } + } + + // Otherwise, the payment request is a transfer-based one + if (request.wasCanceled) { + return Types.Status.Canceled; + } else if (request.config.paymentsLeft == 0) { + return Types.Status.Paid; + } + + return Types.Status.Accepted; + } } diff --git a/src/modules/payment-module/interfaces/IPaymentModule.sol b/src/modules/payment-module/interfaces/IPaymentModule.sol index 4ee3432..aec84c1 100644 --- a/src/modules/payment-module/interfaces/IPaymentModule.sol +++ b/src/modules/payment-module/interfaces/IPaymentModule.sol @@ -11,31 +11,36 @@ interface IPaymentModule { //////////////////////////////////////////////////////////////////////////*/ /// @notice Emitted when a payment request is created - /// @param id The ID of the payment request + /// @param requestId The ID of the payment request /// @param recipient The address receiving the payment /// @param startTime The timestamp when the payment request takes effect /// @param endTime The timestamp by which the payment request must be paid /// @param config Struct representing the payment details associated with the payment request - event RequestCreated(uint256 id, address indexed recipient, uint40 startTime, uint40 endTime, Types.Config config); + event RequestCreated( + uint256 requestId, address indexed recipient, uint40 startTime, uint40 endTime, Types.Config config + ); /// @notice Emitted when a payment is made for a payment request - /// @param id The ID of the payment request + /// @param requestId The ID of the payment request /// @param payer The address of the payer - /// @param status The status of the payment request /// @param config Struct representing the payment details - event RequestPaid(uint256 indexed id, address indexed payer, Types.Status status, Types.Config config); + event RequestPaid(uint256 indexed requestId, address indexed payer, Types.Config config); /// @notice Emitted when a payment request is canceled - /// @param id The ID of the payment request - event RequestCanceled(uint256 indexed id); + /// @param requestId The ID of the payment request + event RequestCanceled(uint256 indexed requestId); /*////////////////////////////////////////////////////////////////////////// CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ /// @notice Retrieves the details of the `id` payment request - /// @param id The ID of the payment request for which to get the details - function getRequest(uint256 id) external view returns (Types.PaymentRequest memory request); + /// @param requestId The ID of the payment request for which to get the details + function getRequest(uint256 requestId) external view returns (Types.PaymentRequest memory request); + + /// @notice Retrieves the status of the `requestId` payment request + /// @param requestId The ID of the payment request for which to retrieve the status + function statusOf(uint256 requestId) external view returns (Types.Status status); /*////////////////////////////////////////////////////////////////////////// NON-CONSTANT FUNCTIONS @@ -49,17 +54,17 @@ interface IPaymentModule { /// Notes: /// - `recipient` is not checked because the call is enforced to be made through a {Space} contract /// - /// @param request request The details of the payment request following the {Invoice} struct format - /// @return id The on-chain ID of the payment request - function createRequest(Types.PaymentRequest calldata request) external returns (uint256 id); + /// @param request request The details of the payment request following the {PaymentRequest} struct format + /// @return requestId The on-chain ID of the payment request + function createRequest(Types.PaymentRequest calldata request) external returns (uint256 requestId); /// @notice Pays a transfer-based payment request /// /// Notes: /// - `msg.sender` is enforced to be a specific payer address /// - /// @param id The ID of the payment request to pay - function payRequest(uint256 id) external payable; + /// @param requestId The ID of the payment request to pay + function payRequest(uint256 requestId) external payable; /// @notice Cancels the `id` payment request /// @@ -74,8 +79,8 @@ interface IPaymentModule { /// - if the payment request has a linear or tranched stream payment method, the portion that has already /// been streamed is NOT automatically transferred /// - /// @param id The ID of the payment request - function cancelRequest(uint256 id) external; + /// @param requestId The ID of the payment request + function cancelRequest(uint256 requestId) external; /// @notice Withdraws the maximum withdrawable amount from the stream associated with the `id` payment request /// @@ -83,6 +88,6 @@ interface IPaymentModule { /// - reverts if `msg.sender` is not the stream recipient /// - reverts if the payment method of the `id` payment request is not linear or tranched stream /// - /// @param id The ID of the payment request - function withdrawRequestStream(uint256 id) external returns (uint128 withdrawnAmount); + /// @param requestId The ID of the payment request + function withdrawRequestStream(uint256 requestId) external returns (uint128 withdrawnAmount); } diff --git a/src/modules/payment-module/libraries/Errors.sol b/src/modules/payment-module/libraries/Errors.sol index 6cbc4bc..63aa088 100644 --- a/src/modules/payment-module/libraries/Errors.sol +++ b/src/modules/payment-module/libraries/Errors.sol @@ -14,16 +14,16 @@ library Errors { /// @notice Thrown when the caller is a contract that does not implement the {ISpace} interface error SpaceUnsupportedInterface(); - /// @notice Thrown when the end time of an invoice is in the past + /// @notice Thrown when the end time of a payment request is in the past error EndTimeInThePast(); /// @notice Thrown when the start time is later than the end time error StartTimeGreaterThanEndTime(); - /// @notice Thrown when the payment amount set for a new invoice is zero + /// @notice Thrown when the payment amount set for a new paymentRequest is zero error ZeroPaymentAmount(); - /// @notice Thrown when the payment amount is less than the invoice value + /// @notice Thrown when the payment amount is less than the payment request value error PaymentAmountLessThanInvoiceValue(uint256 amount); /// @notice Thrown when a payment in the native token (ETH) fails @@ -36,11 +36,14 @@ library Errors { error RequestCanceled(); /// @notice Thrown when a payer attempts to pay a completed payment request - error RequestCompleted(); + error RequestPaid(); /// @notice Thrown when `msg.sender` is not the payment request recipient error OnlyRequestRecipient(); + /// @notice Thrown when the recipient address is the zero address + error InvalidZeroAddressRecipient(); + /// @notice Thrown when the payment interval (endTime - startTime) is too short for the selected recurrence /// i.e. recurrence is set to weekly but interval is shorter than 1 week error PaymentIntervalTooShortForSelectedRecurrence(); @@ -51,6 +54,9 @@ library Errors { /// @notice Thrown when the caller is not the initial stream sender error OnlyInitialStreamSender(address initialSender); + /// @notice Thrown when the payment request is null + error NullRequest(); + /*////////////////////////////////////////////////////////////////////////// STREAM-MANAGER //////////////////////////////////////////////////////////////////////////*/ diff --git a/src/modules/payment-module/libraries/Helpers.sol b/src/modules/payment-module/libraries/Helpers.sol index bb09d62..d32d36b 100644 --- a/src/modules/payment-module/libraries/Helpers.sol +++ b/src/modules/payment-module/libraries/Helpers.sol @@ -6,14 +6,18 @@ import { Types } from "./Types.sol"; /// @title Helpers /// @notice Library with helpers used across the {InvoiceModule} contract library Helpers { - /// @dev Calculates the number of payments that must be done for a recurring transfer or tranched stream invoice + /// @dev Calculates the number of payments that must be done for a recurring transfer or tranched stream paymentRequest /// Notes: /// - Known issue: due to leap seconds, not every year equals 365 days and not every day has 24 hours /// - See https://docs.soliditylang.org/en/v0.8.26/units-and-global-variables.html#time-units function computeNumberOfPayments( Types.Recurrence recurrence, uint40 interval - ) internal pure returns (uint40 numberOfPayments) { + ) + internal + pure + returns (uint40 numberOfPayments) + { // Calculate the number of payments based on the recurrence type if (recurrence == Types.Recurrence.Weekly) { numberOfPayments = interval / 1 weeks; diff --git a/src/modules/payment-module/libraries/Types.sol b/src/modules/payment-module/libraries/Types.sol index 9977c36..2f41757 100644 --- a/src/modules/payment-module/libraries/Types.sol +++ b/src/modules/payment-module/libraries/Types.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.26; -/// @notice Namespace for the structs used across the Invoice Module contracts +/// @notice Namespace for the structs used across the {PaymentModule} related contracts library Types { /// @notice Enum representing the different recurrences a payment can have /// @custom:value OneOff One single payment that must be made either as a single transfer or through a linear stream @@ -28,7 +28,7 @@ library Types { /// @notice Struct encapsulating the different values describing a payment config /// @param method The payment method /// @param recurrence The payment recurrence - /// @param paymentsLeft The number of payments required to fully settle the invoice (only for transfer or tranched stream based invoices) + /// @param paymentsLeft The number of payments required to fully settle the payment request (only for transfer or tranched stream based paymentRequests) /// @param asset The address of the payment asset /// @param amount The amount that must be paid /// @param streamId The ID of the linear or tranched stream if payment method is either `LinearStream` or `TranchedStream`, otherwise 0 @@ -48,23 +48,25 @@ library Types { /// @custom:value Pending Payment request waiting to be accepted by the payer /// @custom:value Accepted Payment request has been accepted and is being paid; if the payment method is a One-Off Transfer, /// the payment request status will automatically be set to `Completed`. Otherwise, it will remain `Accepted` until it is fully paid + /// @custom:value Paid Payment request has been fully paid /// @custom:value Canceled Payment request canceled by declined by the recipient (if Transfer-based) or stream sender enum Status { Pending, Accepted, - Completed, + Paid, Canceled } /// @notice Struct encapsulating the different values describing a payment request - /// @param status The status of the invoice + /// @param status The status of the payment request /// @param startTime The unix timestamp indicating when the payment starts /// @param endTime The unix timestamp indicating when the payment ends /// @param recipient The address to which the payment is made /// @param payment The payment configurations struct PaymentRequest { // slot 0 - Status status; + bool wasCanceled; + bool wasAccepted; uint40 startTime; uint40 endTime; address recipient; diff --git a/src/modules/payment-module/sablier-v2/StreamManager.sol b/src/modules/payment-module/sablier-v2/StreamManager.sol index e6ecce5..209af16 100644 --- a/src/modules/payment-module/sablier-v2/StreamManager.sol +++ b/src/modules/payment-module/sablier-v2/StreamManager.sol @@ -5,7 +5,7 @@ import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablier import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol"; import { LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; -import { Broker, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { Broker, LockupLinear, Lockup } from "@sablier/v2-core/src/types/DataTypes.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { ud60x18, UD60x18, ud, intoUint128 } from "@prb/math/src/UD60x18.sol"; @@ -78,7 +78,11 @@ abstract contract StreamManager is IStreamManager { function withdrawableAmountOf( Types.Method streamType, uint256 streamId - ) public view returns (uint128 withdrawableAmount) { + ) + public + view + returns (uint128 withdrawableAmount) + { withdrawableAmount = _getISablierV2Lockup(streamType).withdrawableAmountOf(streamId); } @@ -87,6 +91,11 @@ abstract contract StreamManager is IStreamManager { streamedAmount = _getISablierV2Lockup(streamType).streamedAmountOf(streamId); } + /// @inheritdoc IStreamManager + function statusOfStream(Types.Method streamType, uint256 streamId) public view returns (Lockup.Status status) { + status = _getISablierV2Lockup(streamType).statusOf(streamId); + } + /*////////////////////////////////////////////////////////////////////////// NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ @@ -98,7 +107,10 @@ abstract contract StreamManager is IStreamManager { uint40 startTime, uint40 endTime, address recipient - ) public returns (uint256 streamId) { + ) + public + returns (uint256 streamId) + { // Transfer the provided amount of ERC-20 tokens to this contract and approve the Sablier contract to spend it _transferFromAndApprove({ asset: asset, amount: totalAmount, spender: address(LOCKUP_LINEAR) }); @@ -117,7 +129,10 @@ abstract contract StreamManager is IStreamManager { address recipient, uint128 numberOfTranches, Types.Recurrence recurrence - ) public returns (uint256 streamId) { + ) + public + returns (uint256 streamId) + { // Transfer the provided amount of ERC-20 tokens to this contract and approve the Sablier contract to spend it _transferFromAndApprove({ asset: asset, amount: totalAmount, spender: address(LOCKUP_TRANCHED) }); @@ -152,7 +167,10 @@ abstract contract StreamManager is IStreamManager { uint40 startTime, uint40 endTime, address recipient - ) internal returns (uint256 streamId) { + ) + internal + returns (uint256 streamId) + { // Declare the params struct LockupLinear.CreateWithTimestamps memory params; @@ -179,7 +197,10 @@ abstract contract StreamManager is IStreamManager { address recipient, uint128 numberOfTranches, Types.Recurrence recurrence - ) internal returns (uint256 streamId) { + ) + internal + returns (uint256 streamId) + { // Declare the params struct LockupTranched.CreateWithTimestamps memory params; @@ -208,10 +229,8 @@ abstract contract StreamManager is IStreamManager { // Create the tranches array params.tranches = new LockupTranched.Tranche[](numberOfTranches); for (uint256 i; i < numberOfTranches; ++i) { - params.tranches[i] = LockupTranched.Tranche({ - amount: amountPerTranche, - timestamp: startTime + durationPerTranche - }); + params.tranches[i] = + LockupTranched.Tranche({ amount: amountPerTranche, timestamp: startTime + durationPerTranche }); // Jump to the next tranche by adding the duration per tranche timestamp to the start time startTime += durationPerTranche; @@ -237,7 +256,10 @@ abstract contract StreamManager is IStreamManager { Types.Method streamType, uint256 streamId, address to - ) internal returns (uint128 withdrawnAmount) { + ) + internal + returns (uint128 withdrawnAmount) + { // Set the according {ISablierV2Lockup} based on the stream type ISablierV2Lockup sablier = _getISablierV2Lockup(streamType); @@ -252,7 +274,10 @@ abstract contract StreamManager is IStreamManager { Types.Method streamType, uint256 streamId, address newRecipient - ) internal returns (uint128 withdrawnAmount) { + ) + internal + returns (uint128 withdrawnAmount) + { // Set the according {ISablierV2Lockup} based on the stream type ISablierV2Lockup sablier = _getISablierV2Lockup(streamType); diff --git a/src/modules/payment-module/sablier-v2/interfaces/IStreamManager.sol b/src/modules/payment-module/sablier-v2/interfaces/IStreamManager.sol index ccd9c1f..36a739f 100644 --- a/src/modules/payment-module/sablier-v2/interfaces/IStreamManager.sol +++ b/src/modules/payment-module/sablier-v2/interfaces/IStreamManager.sol @@ -6,6 +6,7 @@ import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISabli import { LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Lockup } from "@sablier/v2-core/src/types/DataTypes.sol"; import { UD60x18 } from "@prb/math/src/UD60x18.sol"; import { Types } from "./../../libraries/Types.sol"; @@ -57,12 +58,26 @@ interface IStreamManager { function withdrawableAmountOf( Types.Method streamType, uint256 streamId - ) external view returns (uint128 withdrawableAmount); + ) + external + view + returns (uint128 withdrawableAmount); /// @notice See the documentation in {ISablierV2Lockup-streamedAmountOf} /// Notes: /// - `streamType` parameter has been added to retrieve from the according {ISablierV2Lockup} contract - function streamedAmountOf(Types.Method streamType, uint256 streamId) external view returns (uint128 streamedAmount); + function streamedAmountOf( + Types.Method streamType, + uint256 streamId + ) + external + view + returns (uint128 streamedAmount); + + /// @notice See the documentation in {ISablierV2Lockup-statusOf} + /// Notes: + /// - `streamType` parameter has been added to retrieve from the according {ISablierV2Lockup} contract + function statusOfStream(Types.Method streamType, uint256 streamId) external view returns (Lockup.Status status); /*////////////////////////////////////////////////////////////////////////// NON-CONSTANT FUNCTIONS @@ -80,7 +95,9 @@ interface IStreamManager { uint40 startTime, uint40 endTime, address recipient - ) external returns (uint256 streamId); + ) + external + returns (uint256 streamId); /// @notice Creates a Lockup Tranched stream; See https://docs.sablier.com/concepts/protocol/stream-types#lockup-tranched /// @param asset The address of the ERC-20 token to be streamed @@ -96,7 +113,9 @@ interface IStreamManager { address recipient, uint128 numberOfTranches, Types.Recurrence recurrence - ) external returns (uint256 streamId); + ) + external + returns (uint256 streamId); /// @notice Updates the fee charged by the broker /// From 20d6f0c664f3c2329fe8451e91f8523e186bac83 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Fri, 15 Nov 2024 15:57:01 +0200 Subject: [PATCH 05/13] test: fix tests due to invoice module refactoring to payment module --- test/Base.t.sol | 49 +- test/integration/Integration.t.sol | 35 +- .../cancel-invoice/cancelInvoice.t.sol | 312 ----------- .../cancel-invoice/cancelInvoice.tree | 40 -- .../create-invoice/createInvoice.t.sol | 517 ----------------- .../pay-invoice/payInvoice.t.sol | 328 ----------- .../transfer-from/transferFrom.t.sol | 49 +- .../transfer-from/transferFrom.tree | 4 +- .../withdrawStream.tree | 7 - .../cancel-request/cancelRequest.t.sol | 325 +++++++++++ .../cancel-request/cancelRequest.tree | 40 ++ .../create-request/createRequest.t.sol | 519 ++++++++++++++++++ .../create-request/createRequest.tree} | 18 +- .../pay-request/payRequest.t.sol | 344 ++++++++++++ .../pay-request/payRequest.tree} | 48 +- .../withdrawStream.t.sol | 58 +- .../withdrawStream.tree | 7 + .../updateStreamBrokerFee.t.sol | 4 +- test/integration/fuzz/createInvoice.t.sol | 105 ---- test/integration/fuzz/createRequest.t.sol | 104 ++++ test/integration/fuzz/payInvoice.t.sol | 130 ----- test/integration/fuzz/payRequest.t.sol | 128 +++++ ...ancelInvoice.t.sol => cancelRequest.t.sol} | 11 +- test/integration/shared/createInvoice.t.sol | 201 ------- test/integration/shared/createRequest.t.sol | 216 ++++++++ .../{payInvoice.t.sol => payRequest.t.sol} | 16 +- test/integration/shared/transferFrom.t.sol | 8 +- .../shared/withdrawLinearStream.t.sol | 15 +- .../shared/withdrawTranchedStream.t.sol | 12 +- test/mocks/MockModule.sol | 2 +- test/mocks/MockStreamManager.sol | 6 +- .../helpers/computeNumberOfPayments.t.sol | 22 +- test/unit/concrete/space/Space.t.sol | 2 +- .../withdraw-native/withdrawNative.t.sol | 4 +- .../create-account/createAccount.t.sol | 23 +- .../transferDockOwnership.t.sol | 2 +- test/utils/Errors.sol | 32 +- test/utils/Events.sol | 17 +- test/utils/Helpers.sol | 58 +- 39 files changed, 1922 insertions(+), 1896 deletions(-) delete mode 100644 test/integration/concrete/invoice-module/cancel-invoice/cancelInvoice.t.sol delete mode 100644 test/integration/concrete/invoice-module/cancel-invoice/cancelInvoice.tree delete mode 100644 test/integration/concrete/invoice-module/create-invoice/createInvoice.t.sol delete mode 100644 test/integration/concrete/invoice-module/pay-invoice/payInvoice.t.sol delete mode 100644 test/integration/concrete/invoice-module/withdraw-invoice-stream/withdrawStream.tree create mode 100644 test/integration/concrete/payment-module/cancel-request/cancelRequest.t.sol create mode 100644 test/integration/concrete/payment-module/cancel-request/cancelRequest.tree create mode 100644 test/integration/concrete/payment-module/create-request/createRequest.t.sol rename test/integration/concrete/{invoice-module/create-invoice/createInvoice.tree => payment-module/create-request/createRequest.tree} (85%) create mode 100644 test/integration/concrete/payment-module/pay-request/payRequest.t.sol rename test/integration/concrete/{invoice-module/pay-invoice/payInvoice.tree => payment-module/pay-request/payRequest.tree} (55%) rename test/integration/concrete/{invoice-module => payment-module}/withdraw-invoice-stream/withdrawStream.t.sol (52%) create mode 100644 test/integration/concrete/payment-module/withdraw-invoice-stream/withdrawStream.tree delete mode 100644 test/integration/fuzz/createInvoice.t.sol create mode 100644 test/integration/fuzz/createRequest.t.sol delete mode 100644 test/integration/fuzz/payInvoice.t.sol create mode 100644 test/integration/fuzz/payRequest.t.sol rename test/integration/shared/{cancelInvoice.t.sol => cancelRequest.t.sol} (61%) delete mode 100644 test/integration/shared/createInvoice.t.sol create mode 100644 test/integration/shared/createRequest.t.sol rename test/integration/shared/{payInvoice.t.sol => payRequest.t.sol} (56%) diff --git a/test/Base.t.sol b/test/Base.t.sol index a5b37e2..01a4098 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -89,43 +89,43 @@ abstract contract Base_Test is Test, Events { /// @dev Deploys a new {Space} smart account based on the provided `owner`, `moduleKeeper` and `initialModules` input params function deploySpace( address _owner, - uint256 _spaceId, + uint256 _stationId, address[] memory _initialModules - ) internal returns (Space _container) { + ) + internal + returns (Space _space) + { vm.startPrank({ msgSender: users.admin }); for (uint256 i; i < _initialModules.length; ++i) { allowlistModule(_initialModules[i]); } vm.stopPrank(); - bytes memory data = computeCreateAccountCalldata({ - deployer: _owner, - stationId: _spaceId, - initialModules: _initialModules - }); + bytes memory data = + computeCreateAccountCalldata({ deployer: _owner, stationId: _stationId, initialModules: _initialModules }); vm.prank({ msgSender: _owner }); - _container = Space(payable(stationRegistry.createAccount({ _admin: _owner, _data: data }))); + _space = Space(payable(stationRegistry.createAccount({ _admin: _owner, _data: data }))); vm.stopPrank(); } /// @dev Deploys a new {MockBadSpace} smart account based on the provided `owner`, `moduleKeeper` and `initialModules` input params function deployBadSpace( address _owner, - uint256 _spaceId, + uint256 _stationId, address[] memory _initialModules - ) internal returns (MockBadSpace _badSpace) { + ) + internal + returns (MockBadSpace _badSpace) + { vm.startPrank({ msgSender: users.admin }); for (uint256 i; i < _initialModules.length; ++i) { allowlistModule(_initialModules[i]); } vm.stopPrank(); - bytes memory data = computeCreateAccountCalldata({ - deployer: _owner, - stationId: _spaceId, - initialModules: _initialModules - }); + bytes memory data = + computeCreateAccountCalldata({ deployer: _owner, stationId: _stationId, initialModules: _initialModules }); vm.prank({ msgSender: _owner }); _badSpace = MockBadSpace(payable(stationRegistry.createAccount({ _admin: _owner, _data: data }))); @@ -155,18 +155,19 @@ abstract contract Base_Test is Test, Events { address deployer, uint256 stationId, address[] memory initialModules - ) internal view returns (address expectedAddress, bytes memory data) { + ) + internal + view + returns (address expectedAddress, bytes memory data) + { data = computeCreateAccountCalldata(deployer, stationId, initialModules); // Compute the final salt made by the deployer address and initialization data bytes32 salt = keccak256(abi.encode(deployer, data)); // Use {Clones} library to predict the smart account address based on the smart account implementation, salt and account factory - expectedAddress = Clones.predictDeterministicAddress( - stationRegistry.accountImplementation(), - salt, - address(stationRegistry) - ); + expectedAddress = + Clones.predictDeterministicAddress(stationRegistry.accountImplementation(), salt, address(stationRegistry)); } /// @dev Constructs the calldata passed to the {StationRegistry}.createAccount method @@ -174,7 +175,11 @@ abstract contract Base_Test is Test, Events { address deployer, uint256 stationId, address[] memory initialModules - ) internal view returns (bytes memory data) { + ) + internal + view + returns (bytes memory data) + { // Get the total account deployed by `deployer` and use it as a unique salt field // because a signer must be able to deploy multiple smart accounts within one // station with the same initial modules diff --git a/test/integration/Integration.t.sol b/test/integration/Integration.t.sol index 621d3e7..35d4a59 100644 --- a/test/integration/Integration.t.sol +++ b/test/integration/Integration.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.26; import { Base_Test } from "../Base.t.sol"; -import { InvoiceModule } from "./../../src/modules/invoice-module/InvoiceModule.sol"; +import { PaymentModule } from "./../../src/modules/payment-module/PaymentModule.sol"; import { SablierV2LockupLinear } from "@sablier/v2-core/src/SablierV2LockupLinear.sol"; import { SablierV2LockupTranched } from "@sablier/v2-core/src/SablierV2LockupTranched.sol"; import { MockNFTDescriptor } from "../mocks/MockNFTDescriptor.sol"; @@ -15,7 +15,7 @@ abstract contract Integration_Test is Base_Test { TEST CONTRACTS //////////////////////////////////////////////////////////////////////////*/ - InvoiceModule internal invoiceModule; + PaymentModule internal paymentModule; // Sablier V2 related test contracts MockNFTDescriptor internal mockNFTDescriptor; SablierV2LockupLinear internal sablierV2LockupLinear; @@ -30,24 +30,24 @@ abstract contract Integration_Test is Base_Test { function setUp() public virtual override { Base_Test.setUp(); - // Deploy the {InvoiceModule} modul - deployInvoiceModule(); + // Deploy the {PaymentModule} modul + deployPaymentModule(); - // Setup the initial {InvoiceModule} module to be initialized on the {Space} + // Setup the initial {PaymentModule} module to be initialized on the {Space} address[] memory modules = new address[](1); - modules[0] = address(invoiceModule); + modules[0] = address(paymentModule); - // Deploy the {Space} contract with the {InvoiceModule} enabled by default - space = deploySpace({ _owner: users.eve, _spaceId: 0, _initialModules: modules }); + // Deploy the {Space} contract with the {PaymentModule} enabled by default + space = deploySpace({ _owner: users.eve, _stationId: 0, _initialModules: modules }); // Deploy a "bad" {Space} with the `mockBadReceiver` as the owner - badSpace = deployBadSpace({ _owner: address(mockBadReceiver), _spaceId: 0, _initialModules: modules }); + badSpace = deployBadSpace({ _owner: address(mockBadReceiver), _stationId: 0, _initialModules: modules }); // Deploy the mock {StreamManager} mockStreamManager = new MockStreamManager(sablierV2LockupLinear, sablierV2LockupTranched, users.admin); // Label the test contracts so we can easily track them - vm.label({ account: address(invoiceModule), newLabel: "InvoiceModule" }); + vm.label({ account: address(paymentModule), newLabel: "PaymentModule" }); vm.label({ account: address(sablierV2LockupLinear), newLabel: "SablierV2LockupLinear" }); vm.label({ account: address(sablierV2LockupTranched), newLabel: "SablierV2LockupTranched" }); vm.label({ account: address(space), newLabel: "Eve's Space" }); @@ -58,23 +58,20 @@ abstract contract Integration_Test is Base_Test { DEPLOYMENT-RELATED FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Deploys the {InvoiceModule} module by initializing the Sablier v2-required contracts first - function deployInvoiceModule() internal { + /// @dev Deploys the {PaymentModule} module by initializing the Sablier v2-required contracts first + function deployPaymentModule() internal { mockNFTDescriptor = new MockNFTDescriptor(); - sablierV2LockupLinear = new SablierV2LockupLinear({ - initialAdmin: users.admin, - initialNFTDescriptor: mockNFTDescriptor - }); + sablierV2LockupLinear = + new SablierV2LockupLinear({ initialAdmin: users.admin, initialNFTDescriptor: mockNFTDescriptor }); sablierV2LockupTranched = new SablierV2LockupTranched({ initialAdmin: users.admin, initialNFTDescriptor: mockNFTDescriptor, maxTrancheCount: 1000 }); - invoiceModule = new InvoiceModule({ + paymentModule = new PaymentModule({ _sablierLockupLinear: sablierV2LockupLinear, _sablierLockupTranched: sablierV2LockupTranched, - _brokerAdmin: users.admin, - _URI: "ipfs://CID/" + _brokerAdmin: users.admin }); } } diff --git a/test/integration/concrete/invoice-module/cancel-invoice/cancelInvoice.t.sol b/test/integration/concrete/invoice-module/cancel-invoice/cancelInvoice.t.sol deleted file mode 100644 index 460e3ea..0000000 --- a/test/integration/concrete/invoice-module/cancel-invoice/cancelInvoice.t.sol +++ /dev/null @@ -1,312 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; - -import { CancelInvoice_Integration_Shared_Test } from "../../../shared/cancelInvoice.t.sol"; -import { Types } from "./../../../../../src/modules/invoice-module/libraries/Types.sol"; -import { Events } from "../../../../utils/Events.sol"; -import { Errors } from "../../../../utils/Errors.sol"; - -contract CancelInvoice_Integration_Concret_Test is CancelInvoice_Integration_Shared_Test { - function setUp() public virtual override { - CancelInvoice_Integration_Shared_Test.setUp(); - } - - function test_RevertWhen_InvoiceIsPaid() external { - // Set the one-off ETH transfer invoice as current one - uint256 invoiceId = 2; - - // Make Bob the payer for the default invoice - vm.startPrank({ msgSender: users.bob }); - - // Pay the invoice first - invoiceModule.payInvoice{ value: invoices[invoiceId].payment.amount }({ id: invoiceId }); - - // Make Eve the caller who is the recipient of the invoice - vm.startPrank({ msgSender: users.eve }); - - // Expect the call to revert with the {CannotCancelPaidInvoice} error - vm.expectRevert(Errors.CannotCancelPaidInvoice.selector); - - // Run the test - invoiceModule.cancelInvoice({ id: invoiceId }); - } - - function test_RevertWhen_InvoiceIsCanceled() external whenInvoiceNotAlreadyPaid { - // Set the one-off ETH transfer invoice as current one - uint256 invoiceId = 2; - - // Make Eve's space the caller which is the recipient of the invoice - vm.startPrank({ msgSender: address(space) }); - - // Cancel the invoice first - invoiceModule.cancelInvoice({ id: invoiceId }); - - // Expect the call to revert with the {InvoiceAlreadyCanceled} error - vm.expectRevert(Errors.InvoiceAlreadyCanceled.selector); - - // Run the test - invoiceModule.cancelInvoice({ id: invoiceId }); - } - - function test_RevertWhen_PaymentMethodTransfer_SenderNotInvoiceRecipient() - external - whenInvoiceNotAlreadyPaid - whenInvoiceNotCanceled - givenPaymentMethodTransfer - { - // Set the one-off ETH transfer invoice as current one - uint256 invoiceId = 2; - - // Make Bob the caller who IS NOT the recipient of the invoice - vm.startPrank({ msgSender: users.bob }); - - // Expect the call to revert with the {OnlyInvoiceRecipient} error - vm.expectRevert(Errors.OnlyInvoiceRecipient.selector); - - // Run the test - invoiceModule.cancelInvoice({ id: invoiceId }); - } - - function test_CancelInvoice_PaymentMethodTransfer() - external - whenInvoiceNotAlreadyPaid - whenInvoiceNotCanceled - givenPaymentMethodTransfer - whenSenderInvoiceRecipient - { - // Set the one-off ETH transfer invoice as current one - uint256 invoiceId = 2; - - // Make Eve's space the caller which is the recipient of the invoice - vm.startPrank({ msgSender: address(space) }); - - // Expect the {InvoiceCanceled} event to be emitted - vm.expectEmit(); - emit Events.InvoiceCanceled({ id: invoiceId }); - - // Run the test - invoiceModule.cancelInvoice({ id: invoiceId }); - - // Assert the actual and expected invoice status - Types.Invoice memory invoice = invoiceModule.getInvoice({ id: invoiceId }); - assertEq(uint8(invoice.status), uint8(Types.Status.Canceled)); - } - - function test_RevertWhen_PaymentMethodLinearStream_StatusPending_SenderNotInvoiceRecipient() - external - whenInvoiceNotAlreadyPaid - whenInvoiceNotCanceled - givenPaymentMethodLinearStream - givenInvoiceStatusPending - { - // Set current invoice as a linear stream-based one - uint256 invoiceId = 5; - - // Make Bob the caller who IS NOT the recipient of the invoice - vm.startPrank({ msgSender: users.bob }); - - // Expect the call to revert with the {OnlyInvoiceRecipient} error - vm.expectRevert(Errors.OnlyInvoiceRecipient.selector); - - // Run the test - invoiceModule.cancelInvoice({ id: invoiceId }); - } - - function test_CancelInvoice_PaymentMethodLinearStream_StatusPending() - external - whenInvoiceNotAlreadyPaid - whenInvoiceNotCanceled - givenPaymentMethodLinearStream - givenInvoiceStatusPending - whenSenderInvoiceRecipient - { - // Set current invoice as a linear stream-based one - uint256 invoiceId = 5; - - // Make Eve's space the caller which is the recipient of the invoice - vm.startPrank({ msgSender: address(space) }); - - // Expect the {InvoiceCanceled} event to be emitted - vm.expectEmit(); - emit Events.InvoiceCanceled({ id: invoiceId }); - - // Run the test - invoiceModule.cancelInvoice({ id: invoiceId }); - - // Assert the actual and expected invoice status - Types.Invoice memory invoice = invoiceModule.getInvoice({ id: invoiceId }); - assertEq(uint8(invoice.status), uint8(Types.Status.Canceled)); - } - - function test_RevertWhen_PaymentMethodLinearStream_StatusOngoing_SenderNoInitialtStreamSender() - external - whenInvoiceNotAlreadyPaid - whenInvoiceNotCanceled - givenPaymentMethodLinearStream - givenInvoiceStatusOngoing - { - // Set current invoice as a linear stream-based one - uint256 invoiceId = 5; - - // The invoice must be paid for its status to be updated to `Ongoing` - // Make Bob the payer of the invoice (also Bob will be the stream sender) - vm.startPrank({ msgSender: users.bob }); - - // Approve the {InvoiceModule} to transfer the USDT tokens on Bob's behalf - usdt.approve({ spender: address(invoiceModule), amount: invoices[invoiceId].payment.amount }); - - // Pay the invoice first (status will be updated to `Ongoing`) - invoiceModule.payInvoice{ value: invoices[invoiceId].payment.amount }({ id: invoiceId }); - - // Make Eve the caller who IS NOT the initial stream sender but rather the recipient - vm.startPrank({ msgSender: users.eve }); - - // Expect the call to revert with the {OnlyInitialStreamSender} error - vm.expectRevert(abi.encodeWithSelector(Errors.OnlyInitialStreamSender.selector, users.bob)); - - // Run the test - invoiceModule.cancelInvoice({ id: invoiceId }); - } - - function test_CancelInvoice_PaymentMethodLinearStream_StatusOngoing() - external - whenInvoiceNotAlreadyPaid - whenInvoiceNotCanceled - givenPaymentMethodLinearStream - givenInvoiceStatusOngoing - whenSenderInitialStreamSender - { - // Set current invoice as a linear stream-based one - uint256 invoiceId = 5; - - // The invoice must be paid for its status to be updated to `Ongoing` - // Make Bob the payer of the invoice (also Bob will be the initial stream sender) - vm.startPrank({ msgSender: users.bob }); - - // Approve the {InvoiceModule} to transfer the USDT tokens on Bob's behalf - usdt.approve({ spender: address(invoiceModule), amount: invoices[invoiceId].payment.amount }); - - // Pay the invoice first (status will be updated to `Ongoing`) - invoiceModule.payInvoice{ value: invoices[invoiceId].payment.amount }({ id: invoiceId }); - - // Expect the {InvoiceCanceled} event to be emitted - vm.expectEmit(); - emit Events.InvoiceCanceled({ id: invoiceId }); - - // Run the test - invoiceModule.cancelInvoice({ id: invoiceId }); - - // Assert the actual and expected invoice status - Types.Invoice memory invoice = invoiceModule.getInvoice({ id: invoiceId }); - assertEq(uint8(invoice.status), uint8(Types.Status.Canceled)); - } - - function test_RevertWhen_PaymentMethodTranchedStream_StatusPending_SenderNotInvoiceRecipient() - external - whenInvoiceNotAlreadyPaid - whenInvoiceNotCanceled - givenPaymentMethodTranchedStream - givenInvoiceStatusPending - { - // Set current invoice as a tranched stream-based one - uint256 invoiceId = 5; - - // Make Bob the caller who IS NOT the recipient of the invoice - vm.startPrank({ msgSender: users.bob }); - - // Expect the call to revert with the {OnlyInvoiceRecipient} error - vm.expectRevert(Errors.OnlyInvoiceRecipient.selector); - - // Run the test - invoiceModule.cancelInvoice({ id: invoiceId }); - } - - function test_CancelInvoice_PaymentMethodTranchedStream_StatusPending() - external - whenInvoiceNotAlreadyPaid - whenInvoiceNotCanceled - givenPaymentMethodTranchedStream - givenInvoiceStatusPending - whenSenderInvoiceRecipient - { - // Set current invoice as a tranched stream-based one - uint256 invoiceId = 5; - - // Make Eve's space the caller which is the recipient of the invoice - vm.startPrank({ msgSender: address(space) }); - - // Expect the {InvoiceCanceled} event to be emitted - vm.expectEmit(); - emit Events.InvoiceCanceled({ id: invoiceId }); - - // Run the test - invoiceModule.cancelInvoice({ id: invoiceId }); - - // Assert the actual and expected invoice status - Types.Invoice memory invoice = invoiceModule.getInvoice({ id: invoiceId }); - assertEq(uint8(invoice.status), uint8(Types.Status.Canceled)); - } - - function test_RevertWhen_PaymentMethodTranchedStream_StatusOngoing_SenderNoInitialtStreamSender() - external - whenInvoiceNotAlreadyPaid - whenInvoiceNotCanceled - givenPaymentMethodTranchedStream - givenInvoiceStatusOngoing - { - // Set current invoice as a tranched stream-based one - uint256 invoiceId = 5; - - // The invoice must be paid for its status to be updated to `Ongoing` - // Make Bob the payer of the invoice (also Bob will be the stream sender) - vm.startPrank({ msgSender: users.bob }); - - // Approve the {InvoiceModule} to transfer the USDT tokens on Bob's behalf - usdt.approve({ spender: address(invoiceModule), amount: invoices[invoiceId].payment.amount }); - - // Pay the invoice first (status will be updated to `Ongoing`) - invoiceModule.payInvoice{ value: invoices[invoiceId].payment.amount }({ id: invoiceId }); - - // Make Eve the caller who IS NOT the initial stream sender but rather the recipient - vm.startPrank({ msgSender: users.eve }); - - // Expect the call to revert with the {OnlyInitialStreamSender} error - vm.expectRevert(abi.encodeWithSelector(Errors.OnlyInitialStreamSender.selector, users.bob)); - - // Run the test - invoiceModule.cancelInvoice({ id: invoiceId }); - } - - function test_CancelInvoice_PaymentMethodTranchedStream_StatusOngoing() - external - whenInvoiceNotAlreadyPaid - whenInvoiceNotCanceled - givenPaymentMethodTranchedStream - givenInvoiceStatusOngoing - whenSenderInitialStreamSender - { - // Set current invoice as a tranched stream-based one - uint256 invoiceId = 5; - - // The invoice must be paid for its status to be updated to `Ongoing` - // Make Bob the payer of the invoice (also Bob will be the initial stream sender) - vm.startPrank({ msgSender: users.bob }); - - // Approve the {InvoiceModule} to transfer the USDT tokens on Bob's behalf - usdt.approve({ spender: address(invoiceModule), amount: invoices[invoiceId].payment.amount }); - - // Pay the invoice first (status will be updated to `Ongoing`) - invoiceModule.payInvoice{ value: invoices[invoiceId].payment.amount }({ id: invoiceId }); - - // Expect the {InvoiceCanceled} event to be emitted - vm.expectEmit(); - emit Events.InvoiceCanceled({ id: invoiceId }); - - // Run the test - invoiceModule.cancelInvoice({ id: invoiceId }); - - // Assert the actual and expected invoice status - Types.Invoice memory invoice = invoiceModule.getInvoice({ id: invoiceId }); - assertEq(uint8(invoice.status), uint8(Types.Status.Canceled)); - } -} diff --git a/test/integration/concrete/invoice-module/cancel-invoice/cancelInvoice.tree b/test/integration/concrete/invoice-module/cancel-invoice/cancelInvoice.tree deleted file mode 100644 index f076dea..0000000 --- a/test/integration/concrete/invoice-module/cancel-invoice/cancelInvoice.tree +++ /dev/null @@ -1,40 +0,0 @@ -cancelInvoice.t.sol -├── when the invoice status IS Paid -│ └── it should revert with the {CannotCancelPaidInvoice} error -└── when the invoice status IS NOT Paid - ├── when the invoice status IS Canceled - │ └── it should revert with the {InvoiceAlreadyCanceled} error - └── when the invoice status IS NOT Canceled - ├── given the payment method is transfer - │ ├── when the sender IS NOT the invoice recipient - │ │ └── it should revert with the {OnlyInvoiceRecipient} - │ └── when the sender IS the invoice recipient - │ ├── it should mark the invoice as Canceled - │ └── it should emit an {InvoiceCanceled} event - ├── given the payment method is linear stream-based - │ ├── given the invoice status is Pending - │ │ ├── when the sender IS NOT the invoice recipient - │ │ │ └── it should revert with the {OnlyInvoiceRecipient} - │ │ └── when the sender IS the invoice recipient - │ │ ├── it should mark the invoice as Canceled - │ │ └── it should emit an {InvoiceCanceled} event - │ └── given the invoice status is Ongoing - │ ├── when the sender IS NOT the initial stream sender - │ │ └── it should revert with the {OnlyInitialStreamSender} error - │ └── when the sender IS the initial stream sender - │ ├── it should mark the invoice as Canceled - │ └── it should emit an {InvoiceCanceled} event - └── given the payment method is tranched stream-based - ├── given the invoice status is Pending - │ ├── when the sender IS NOT the invoice recipient - │ │ └── it should revert with the {OnlyInvoiceRecipient} - │ └── when the sender IS the invoice recipient - │ ├── it should mark the invoice as Canceled - │ └── it should emit an {InvoiceCanceled} event - └── given the invoice status is Ongoing - ├── when the sender IS NOT the initial stream sender - │ └──it should revert with the {OnlyInitialStreamSender} error - └── when the sender IS the initial stream sender - ├── it should mark the invoice as Canceled - └── it should emit an {InvoiceCanceled} event - diff --git a/test/integration/concrete/invoice-module/create-invoice/createInvoice.t.sol b/test/integration/concrete/invoice-module/create-invoice/createInvoice.t.sol deleted file mode 100644 index 161384f..0000000 --- a/test/integration/concrete/invoice-module/create-invoice/createInvoice.t.sol +++ /dev/null @@ -1,517 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; - -import { CreateInvoice_Integration_Shared_Test } from "../../../shared/createInvoice.t.sol"; -import { Types } from "./../../../../../src/modules/invoice-module/libraries/Types.sol"; -import { Errors } from "../../../../utils/Errors.sol"; -import { Events } from "../../../../utils/Events.sol"; - -contract CreateInvoice_Integration_Concret_Test is CreateInvoice_Integration_Shared_Test { - Types.Invoice invoice; - - function setUp() public virtual override { - CreateInvoice_Integration_Shared_Test.setUp(); - } - - function test_RevertWhen_CallerNotContract() external { - // Make Bob the caller in this test suite which is an EOA - vm.startPrank({ msgSender: users.bob }); - - // Expect the call to revert with the {SpaceZeroCodeSize} error - vm.expectRevert(Errors.SpaceZeroCodeSize.selector); - - // Create an one-off transfer invoice - invoice = createInvoiceWithOneOffTransfer({ asset: address(usdt) }); - - // Run the test - invoiceModule.createInvoice(invoice); - } - - function test_RevertWhen_NonCompliantSpace() external whenCallerContract { - // Make Eve the caller in this test suite as she's the owner of the {Space} contract - vm.startPrank({ msgSender: users.eve }); - - // Create an one-off transfer invoice - invoice = createInvoiceWithOneOffTransfer({ asset: address(usdt) }); - - // Create the calldata for the Invoice Module execution - bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", - invoice - ); - - // Expect the call to revert with the {SpaceUnsupportedInterface} error - vm.expectRevert(Errors.SpaceUnsupportedInterface.selector); - - // Run the test - mockNonCompliantSpace.execute({ module: address(invoiceModule), value: 0, data: data }); - } - - function test_RevertWhen_ZeroPaymentAmount() external whenCallerContract whenCompliantSpace { - // Make Eve the caller in this test suite as she's the owner of the {Space} contract - vm.startPrank({ msgSender: users.eve }); - - // Create an one-off transfer invoice - invoice = createInvoiceWithOneOffTransfer({ asset: address(usdt) }); - - // Set the payment amount to zero to simulate the error - invoice.payment.amount = 0; - - // Create the calldata for the Invoice Module execution - bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", - invoice - ); - - // Expect the call to revert with the {ZeroPaymentAmount} error - vm.expectRevert(Errors.ZeroPaymentAmount.selector); - - // Run the test - space.execute({ module: address(invoiceModule), value: 0, data: data }); - } - - function test_RevertWhen_StartTimeGreaterThanEndTime() - external - whenCallerContract - whenCompliantSpace - whenNonZeroPaymentAmount - { - // Make Eve the caller in this test suite as she's the owner of the {Space} contract - vm.startPrank({ msgSender: users.eve }); - - // Create an one-off transfer invoice - invoice = createInvoiceWithOneOffTransfer({ asset: address(usdt) }); - - // Set the start time to be the current timestamp and the end time one second earlier - invoice.startTime = uint40(block.timestamp); - invoice.endTime = uint40(block.timestamp) - 1; - - // Create the calldata for the Invoice Module execution - bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", - invoice - ); - - // Expect the call to revert with the {StartTimeGreaterThanEndTime} error - vm.expectRevert(Errors.StartTimeGreaterThanEndTime.selector); - - // Run the test - space.execute({ module: address(invoiceModule), value: 0, data: data }); - } - - function test_RevertWhen_EndTimeInThePast() - external - whenCallerContract - whenCompliantSpace - whenNonZeroPaymentAmount - whenStartTimeLowerThanEndTime - { - // Make Eve the caller in this test suite as she's the owner of the {Space} contract - vm.startPrank({ msgSender: users.eve }); - - // Create an one-off transfer invoice - invoice = createInvoiceWithOneOffTransfer({ asset: address(usdt) }); - - // Set the block.timestamp to 1641070800 - vm.warp(1_641_070_800); - - // Set the start time to be the lower than the end time so the 'start time lower than end time' passes - // but set the end time in the past to get the {EndTimeInThePast} revert - invoice.startTime = uint40(block.timestamp) - 2 days; - invoice.endTime = uint40(block.timestamp) - 1 days; - - // Create the calldata for the Invoice Module execution - bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", - invoice - ); - - // Expect the call to revert with the {EndTimeInThePast} error - vm.expectRevert(Errors.EndTimeInThePast.selector); - - // Run the test - space.execute({ module: address(invoiceModule), value: 0, data: data }); - } - - function test_CreateInvoice_PaymentMethodOneOffTransfer() - external - whenCallerContract - whenCompliantSpace - whenNonZeroPaymentAmount - whenStartTimeLowerThanEndTime - whenEndTimeInTheFuture - givenPaymentMethodOneOffTransfer - { - // Make Eve the caller in this test suite as she's the owner of the {Space} contract - vm.startPrank({ msgSender: users.eve }); - - // Create a recurring transfer invoice that must be paid on a monthly basis - // Hence, the interval between the start and end time must be at least 1 month - invoice = createInvoiceWithOneOffTransfer({ asset: address(usdt) }); - - // Create the calldata for the Invoice Module execution - bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", - invoice - ); - - // Expect the module call to emit an {InvoiceCreated} event - vm.expectEmit(); - emit Events.InvoiceCreated({ - id: 1, - recipient: address(space), - status: Types.Status.Pending, - startTime: invoice.startTime, - endTime: invoice.endTime, - payment: invoice.payment - }); - - // Expect the {Space} contract to emit a {ModuleExecutionSucceded} event - vm.expectEmit(); - emit Events.ModuleExecutionSucceded({ module: address(invoiceModule), value: 0, data: data }); - - // Run the test - space.execute({ module: address(invoiceModule), value: 0, data: data }); - - // Assert the actual and expected invoice state - Types.Invoice memory actualInvoice = invoiceModule.getInvoice({ id: 1 }); - address expectedRecipient = invoiceModule.ownerOf(1); - - assertEq(expectedRecipient, address(space)); - assertEq(uint8(actualInvoice.status), uint8(Types.Status.Pending)); - assertEq(actualInvoice.startTime, invoice.startTime); - assertEq(actualInvoice.endTime, invoice.endTime); - assertEq(uint8(actualInvoice.payment.method), uint8(Types.Method.Transfer)); - assertEq(uint8(actualInvoice.payment.recurrence), uint8(Types.Recurrence.OneOff)); - assertEq(actualInvoice.payment.paymentsLeft, 1); - assertEq(actualInvoice.payment.asset, invoice.payment.asset); - assertEq(actualInvoice.payment.amount, invoice.payment.amount); - assertEq(actualInvoice.payment.streamId, 0); - } - - function test_RevertWhen_PaymentMethodRecurringTransfer_PaymentIntervalTooShortForSelectedRecurrence() - external - whenCallerContract - whenCompliantSpace - whenNonZeroPaymentAmount - whenStartTimeLowerThanEndTime - whenEndTimeInTheFuture - givenPaymentMethodRecurringTransfer - { - // Make Eve the caller in this test suite as she's the owner of the {Space} contract - vm.startPrank({ msgSender: users.eve }); - - // Create a recurring transfer invoice that must be paid on a monthly basis - // Hence, the interval between the start and end time must be at least 1 month - invoice = createInvoiceWithRecurringTransfer({ recurrence: Types.Recurrence.Monthly }); - - // Alter the end time to be 3 weeks from now - invoice.endTime = uint40(block.timestamp) + 3 weeks; - - // Create the calldata for the Invoice Module execution - bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", - invoice - ); - - // Expect the call to revert with the {PaymentIntervalTooShortForSelectedRecurrence} error - vm.expectRevert(Errors.PaymentIntervalTooShortForSelectedRecurrence.selector); - - // Run the test - space.execute({ module: address(invoiceModule), value: 0, data: data }); - } - - function test_CreateInvoice_RecurringTransfer() - external - whenCallerContract - whenCompliantSpace - whenNonZeroPaymentAmount - whenStartTimeLowerThanEndTime - whenEndTimeInTheFuture - givenPaymentMethodRecurringTransfer - whenPaymentIntervalLongEnough - { - // Make Eve the caller in this test suite as she's the owner of the {Space} contract - vm.startPrank({ msgSender: users.eve }); - - // Create a recurring transfer invoice that must be paid on weekly basis - invoice = createInvoiceWithRecurringTransfer({ recurrence: Types.Recurrence.Weekly }); - - // Create the calldata for the Invoice Module execution - bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", - invoice - ); - - // Expect the module call to emit an {InvoiceCreated} event - vm.expectEmit(); - emit Events.InvoiceCreated({ - id: 1, - recipient: address(space), - status: Types.Status.Pending, - startTime: invoice.startTime, - endTime: invoice.endTime, - payment: invoice.payment - }); - - // Expect the {Space} contract to emit a {ModuleExecutionSucceded} event - vm.expectEmit(); - emit Events.ModuleExecutionSucceded({ module: address(invoiceModule), value: 0, data: data }); - - // Run the test - space.execute({ module: address(invoiceModule), value: 0, data: data }); - - // Assert the actual and expected invoice state - Types.Invoice memory actualInvoice = invoiceModule.getInvoice({ id: 1 }); - address expectedRecipient = invoiceModule.ownerOf(1); - - assertEq(expectedRecipient, address(space)); - assertEq(uint8(actualInvoice.status), uint8(Types.Status.Pending)); - assertEq(actualInvoice.startTime, invoice.startTime); - assertEq(actualInvoice.endTime, invoice.endTime); - assertEq(uint8(actualInvoice.payment.method), uint8(Types.Method.Transfer)); - assertEq(uint8(actualInvoice.payment.recurrence), uint8(Types.Recurrence.Weekly)); - assertEq(actualInvoice.payment.paymentsLeft, 4); - assertEq(actualInvoice.payment.asset, invoice.payment.asset); - assertEq(actualInvoice.payment.amount, invoice.payment.amount); - assertEq(actualInvoice.payment.streamId, 0); - } - - function test_RevertWhen_PaymentMethodTranchedStream_RecurrenceSetToOneOff() - external - whenCallerContract - whenCompliantSpace - whenNonZeroPaymentAmount - whenStartTimeLowerThanEndTime - whenEndTimeInTheFuture - givenPaymentMethodTranchedStream - { - // Make Eve the caller in this test suite as she's the owner of the {Space} contract - vm.startPrank({ msgSender: users.eve }); - - // Create a new invoice with a tranched stream payment - invoice = createInvoiceWithTranchedStream({ recurrence: Types.Recurrence.Weekly }); - - // Alter the payment recurrence by setting it to one-off - invoice.payment.recurrence = Types.Recurrence.OneOff; - - // Expect the call to revert with the {TranchedStreamInvalidOneOffRecurence} error - vm.expectRevert(Errors.TranchedStreamInvalidOneOffRecurence.selector); - - // Create the calldata for the Invoice Module execution - bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", - invoice - ); - - // Run the test - space.execute({ module: address(invoiceModule), value: 0, data: data }); - } - - function test_RevertWhen_PaymentMethodTranchedStream_PaymentIntervalTooShortForSelectedRecurrence() - external - whenCallerContract - whenCompliantSpace - whenNonZeroPaymentAmount - whenStartTimeLowerThanEndTime - whenEndTimeInTheFuture - givenPaymentMethodTranchedStream - whenTranchedStreamWithGoodRecurring - { - // Make Eve the caller in this test suite as she's the owner of the {Space} contract - vm.startPrank({ msgSender: users.eve }); - - // Create a new invoice with a tranched stream payment - invoice = createInvoiceWithTranchedStream({ recurrence: Types.Recurrence.Monthly }); - - // Alter the end time to be 3 weeks from now - invoice.endTime = uint40(block.timestamp) + 3 weeks; - - // Expect the call to revert with the {PaymentIntervalTooShortForSelectedRecurrence} error - vm.expectRevert(Errors.PaymentIntervalTooShortForSelectedRecurrence.selector); - - // Create the calldata for the Invoice Module execution - bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", - invoice - ); - - // Run the test - space.execute({ module: address(invoiceModule), value: 0, data: data }); - } - - function test_RevertWhen_PaymentMethodTranchedStream_PaymentAssetNativeToken() - external - whenCallerContract - whenCompliantSpace - whenNonZeroPaymentAmount - whenStartTimeLowerThanEndTime - whenEndTimeInTheFuture - givenPaymentMethodTranchedStream - whenTranchedStreamWithGoodRecurring - whenPaymentIntervalLongEnough - { - // Make Eve the caller in this test suite as she's the owner of the {Space} contract - vm.startPrank({ msgSender: users.eve }); - - // Create a new invoice with a linear stream payment - invoice = createInvoiceWithTranchedStream({ recurrence: Types.Recurrence.Weekly }); - - // Alter the payment asset by setting it to - invoice.payment.asset = address(0); - - // Expect the call to revert with the {OnlyERC20StreamsAllowed} error - vm.expectRevert(Errors.OnlyERC20StreamsAllowed.selector); - - // Create the calldata for the Invoice Module execution - bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", - invoice - ); - - // Run the test - space.execute({ module: address(invoiceModule), value: 0, data: data }); - } - - function test_CreateInvoice_Tranched() - external - whenCallerContract - whenCompliantSpace - whenNonZeroPaymentAmount - whenStartTimeLowerThanEndTime - whenEndTimeInTheFuture - givenPaymentMethodTranchedStream - whenPaymentAssetNotNativeToken - { - // Make Eve the caller in this test suite as she's the owner of the {Space} contract - vm.startPrank({ msgSender: users.eve }); - - // Create a new invoice with a tranched stream payment - invoice = createInvoiceWithTranchedStream({ recurrence: Types.Recurrence.Weekly }); - - // Create the calldata for the Invoice Module execution - bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", - invoice - ); - - // Expect the module call to emit an {InvoiceCreated} event - vm.expectEmit(); - emit Events.InvoiceCreated({ - id: 1, - recipient: address(space), - status: Types.Status.Pending, - startTime: invoice.startTime, - endTime: invoice.endTime, - payment: invoice.payment - }); - - // Expect the {Space} contract to emit a {ModuleExecutionSucceded} event - vm.expectEmit(); - emit Events.ModuleExecutionSucceded({ module: address(invoiceModule), value: 0, data: data }); - - // Run the test - space.execute({ module: address(invoiceModule), value: 0, data: data }); - - // Assert the actual and expected invoice state - Types.Invoice memory actualInvoice = invoiceModule.getInvoice({ id: 1 }); - address expectedRecipient = invoiceModule.ownerOf(1); - - assertEq(expectedRecipient, address(space)); - assertEq(uint8(actualInvoice.status), uint8(Types.Status.Pending)); - assertEq(actualInvoice.startTime, invoice.startTime); - assertEq(actualInvoice.endTime, invoice.endTime); - assertEq(uint8(actualInvoice.payment.method), uint8(Types.Method.TranchedStream)); - assertEq(uint8(actualInvoice.payment.recurrence), uint8(Types.Recurrence.Weekly)); - assertEq(actualInvoice.payment.paymentsLeft, 0); - assertEq(actualInvoice.payment.asset, invoice.payment.asset); - assertEq(actualInvoice.payment.amount, invoice.payment.amount); - assertEq(actualInvoice.payment.streamId, 0); - } - - function test_RevertWhen_PaymentMethodLinearStream_PaymentAssetNativeToken() - external - whenCallerContract - whenCompliantSpace - whenNonZeroPaymentAmount - whenStartTimeLowerThanEndTime - whenEndTimeInTheFuture - givenPaymentMethodLinearStream - { - // Make Eve the caller in this test suite as she's the owner of the {Space} contract - vm.startPrank({ msgSender: users.eve }); - - // Create a new invoice with a linear stream payment - invoice = createInvoiceWithLinearStream(); - - // Alter the payment asset by setting it to - invoice.payment.asset = address(0); - - // Expect the call to revert with the {OnlyERC20StreamsAllowed} error - vm.expectRevert(Errors.OnlyERC20StreamsAllowed.selector); - - // Create the calldata for the Invoice Module execution - bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", - invoice - ); - - // Run the test - space.execute({ module: address(invoiceModule), value: 0, data: data }); - } - - function test_CreateInvoice_LinearStream() - external - whenCallerContract - whenCompliantSpace - whenNonZeroPaymentAmount - whenStartTimeLowerThanEndTime - whenEndTimeInTheFuture - givenPaymentMethodLinearStream - whenPaymentAssetNotNativeToken - { - // Make Eve the caller in this test suite as she's the owner of the {Space} contract - vm.startPrank({ msgSender: users.eve }); - - // Create a new invoice with a linear stream payment - invoice = createInvoiceWithLinearStream(); - - // Create the calldata for the Invoice Module execution - bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", - invoice - ); - - // Expect the module call to emit an {InvoiceCreated} event - vm.expectEmit(); - emit Events.InvoiceCreated({ - id: 1, - recipient: address(space), - status: Types.Status.Pending, - startTime: invoice.startTime, - endTime: invoice.endTime, - payment: invoice.payment - }); - - // Expect the {Space} contract to emit a {ModuleExecutionSucceded} event - vm.expectEmit(); - emit Events.ModuleExecutionSucceded({ module: address(invoiceModule), value: 0, data: data }); - - // Run the test - space.execute({ module: address(invoiceModule), value: 0, data: data }); - - // Assert the actual and expected invoice state - Types.Invoice memory actualInvoice = invoiceModule.getInvoice({ id: 1 }); - address expectedRecipient = invoiceModule.ownerOf(1); - - assertEq(expectedRecipient, address(space)); - assertEq(uint8(actualInvoice.status), uint8(Types.Status.Pending)); - assertEq(actualInvoice.startTime, invoice.startTime); - assertEq(actualInvoice.endTime, invoice.endTime); - assertEq(uint8(actualInvoice.payment.method), uint8(Types.Method.LinearStream)); - assertEq(uint8(actualInvoice.payment.recurrence), uint8(Types.Recurrence.Weekly)); - assertEq(actualInvoice.payment.asset, invoice.payment.asset); - assertEq(actualInvoice.payment.amount, invoice.payment.amount); - assertEq(actualInvoice.payment.streamId, 0); - } -} diff --git a/test/integration/concrete/invoice-module/pay-invoice/payInvoice.t.sol b/test/integration/concrete/invoice-module/pay-invoice/payInvoice.t.sol deleted file mode 100644 index c7d2f50..0000000 --- a/test/integration/concrete/invoice-module/pay-invoice/payInvoice.t.sol +++ /dev/null @@ -1,328 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; - -import { PayInvoice_Integration_Shared_Test } from "../../../shared/payInvoice.t.sol"; -import { Types } from "./../../../../../src/modules/invoice-module/libraries/Types.sol"; -import { Events } from "../../../../utils/Events.sol"; -import { Errors } from "../../../../utils/Errors.sol"; - -import { LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; - -contract PayInvoice_Integration_Concret_Test is PayInvoice_Integration_Shared_Test { - function setUp() public virtual override { - PayInvoice_Integration_Shared_Test.setUp(); - } - - function test_RevertWhen_InvoiceNull() external { - // Expect the call to revert with the {ERC721NonexistentToken} error - vm.expectRevert(abi.encodeWithSelector(Errors.ERC721NonexistentToken.selector, 99)); - - // Run the test - invoiceModule.payInvoice({ id: 99 }); - } - - function test_RevertWhen_InvoiceAlreadyPaid() external whenInvoiceNotNull { - // Set the one-off USDT transfer invoice as current one - uint256 invoiceId = 1; - - // Make Bob the payer for the default invoice - vm.startPrank({ msgSender: users.bob }); - - // Approve the {InvoiceModule} to transfer the ERC-20 token on Bob's behalf - usdt.approve({ spender: address(invoiceModule), amount: invoices[invoiceId].payment.amount }); - - // Pay first the invoice - invoiceModule.payInvoice({ id: invoiceId }); - - // Expect the call to be reverted with the {InvoiceAlreadyPaid} error - vm.expectRevert(Errors.InvoiceAlreadyPaid.selector); - - // Run the test - invoiceModule.payInvoice({ id: invoiceId }); - } - - function test_RevertWhen_InvoiceCanceled() external whenInvoiceNotNull whenInvoiceNotAlreadyPaid { - // Set the one-off USDT transfer invoice as current one - uint256 invoiceId = 1; - - // Make Eve's space the caller in this test suite as his space is the owner of the invoice - vm.startPrank({ msgSender: address(space) }); - - // Cancel the invoice first - invoiceModule.cancelInvoice({ id: invoiceId }); - - // Make Bob the payer of this invoice - vm.startPrank({ msgSender: users.bob }); - - // Expect the call to be reverted with the {InvoiceCanceled} error - vm.expectRevert(Errors.InvoiceCanceled.selector); - - // Run the test - invoiceModule.payInvoice({ id: invoiceId }); - } - - function test_RevertWhen_PaymentMethodTransfer_PaymentAmountLessThanInvoiceValue() - external - whenInvoiceNotNull - whenInvoiceNotAlreadyPaid - whenInvoiceNotCanceled - givenPaymentMethodTransfer - givenPaymentAmountInNativeToken - { - // Set the one-off ETH transfer invoice as current one - uint256 invoiceId = 2; - - // Make Bob the payer for the default invoice - vm.startPrank({ msgSender: users.bob }); - - // Expect the call to be reverted with the {PaymentAmountLessThanInvoiceValue} error - vm.expectRevert( - abi.encodeWithSelector( - Errors.PaymentAmountLessThanInvoiceValue.selector, - invoices[invoiceId].payment.amount - ) - ); - - // Run the test - invoiceModule.payInvoice{ value: invoices[invoiceId].payment.amount - 1 }({ id: invoiceId }); - } - - function test_RevertWhen_PaymentMethodTransfer_NativeTokenTransferFails() - external - whenInvoiceNotNull - whenInvoiceNotAlreadyPaid - whenInvoiceNotCanceled - givenPaymentMethodTransfer - givenPaymentAmountInNativeToken - whenPaymentAmountEqualToInvoiceValue - { - // Create a mock invoice with a one-off ETH transfer from the Eve's space - Types.Invoice memory invoice = createInvoiceWithOneOffTransfer({ asset: address(0) }); - executeCreateInvoice({ invoice: invoice, user: users.eve }); - - uint256 invoiceId = _nextInvoiceId; - - // Make Eve's space the caller for the next call to approve & transfer the invoice NFT to a bad receiver - vm.startPrank({ msgSender: address(space) }); - - // Approve the {InvoiceModule} to transfer the token - invoiceModule.approve({ to: address(invoiceModule), tokenId: invoiceId }); - - // Transfer the invoice to a bad receiver so we can test against `NativeTokenPaymentFailed` - invoiceModule.transferFrom({ from: address(space), to: address(mockBadReceiver), tokenId: invoiceId }); - - // Make Bob the payer for this invoice - vm.startPrank({ msgSender: users.bob }); - - // Expect the call to be reverted with the {NativeTokenPaymentFailed} error - vm.expectRevert(Errors.NativeTokenPaymentFailed.selector); - - // Run the test - invoiceModule.payInvoice{ value: invoice.payment.amount }({ id: invoiceId }); - } - - function test_PayInvoice_PaymentMethodTransfer_NativeToken_OneOff() - external - whenInvoiceNotNull - whenInvoiceNotAlreadyPaid - whenInvoiceNotCanceled - givenPaymentMethodTransfer - givenPaymentAmountInNativeToken - whenPaymentAmountEqualToInvoiceValue - whenNativeTokenPaymentSucceeds - { - // Set the one-off ETH transfer invoice as current one - uint256 invoiceId = 2; - - // Make Bob the payer for the default invoice - vm.startPrank({ msgSender: users.bob }); - - // Store the ETH balances of Bob and recipient before paying the invoice - uint256 balanceOfBobBefore = address(users.bob).balance; - uint256 balanceOfRecipientBefore = address(space).balance; - - // Expect the {InvoicePaid} event to be emitted - vm.expectEmit(); - emit Events.InvoicePaid({ - id: invoiceId, - payer: users.bob, - status: Types.Status.Paid, - payment: Types.Payment({ - method: invoices[invoiceId].payment.method, - recurrence: invoices[invoiceId].payment.recurrence, - paymentsLeft: 0, - asset: invoices[invoiceId].payment.asset, - amount: invoices[invoiceId].payment.amount, - streamId: 0 - }) - }); - - // Run the test - invoiceModule.payInvoice{ value: invoices[invoiceId].payment.amount }({ id: invoiceId }); - - // Assert the actual and the expected state of the invoice - Types.Invoice memory invoice = invoiceModule.getInvoice({ id: invoiceId }); - assertEq(uint8(invoice.status), uint8(Types.Status.Paid)); - assertEq(invoice.payment.paymentsLeft, 0); - - // Assert the balances of payer and recipient - assertEq(address(users.bob).balance, balanceOfBobBefore - invoices[invoiceId].payment.amount); - assertEq(address(space).balance, balanceOfRecipientBefore + invoices[invoiceId].payment.amount); - } - - function test_PayInvoice_PaymentMethodTransfer_ERC20Token_Recurring() - external - whenInvoiceNotNull - whenInvoiceNotAlreadyPaid - whenInvoiceNotCanceled - givenPaymentMethodTransfer - givenPaymentAmountInERC20Tokens - whenPaymentAmountEqualToInvoiceValue - { - // Set the recurring USDT transfer invoice as current one - uint256 invoiceId = 3; - - // Make Bob the payer for the default invoice - vm.startPrank({ msgSender: users.bob }); - - // Store the USDT balances of Bob and recipient before paying the invoice - uint256 balanceOfBobBefore = usdt.balanceOf(users.bob); - uint256 balanceOfRecipientBefore = usdt.balanceOf(address(space)); - - // Approve the {InvoiceModule} to transfer the ERC-20 tokens on Bob's behalf - usdt.approve({ spender: address(invoiceModule), amount: invoices[invoiceId].payment.amount }); - - // Expect the {InvoicePaid} event to be emitted - vm.expectEmit(); - emit Events.InvoicePaid({ - id: invoiceId, - payer: users.bob, - status: Types.Status.Ongoing, - payment: Types.Payment({ - method: invoices[invoiceId].payment.method, - recurrence: invoices[invoiceId].payment.recurrence, - paymentsLeft: 3, - asset: invoices[invoiceId].payment.asset, - amount: invoices[invoiceId].payment.amount, - streamId: 0 - }) - }); - - // Run the test - invoiceModule.payInvoice{ value: invoices[invoiceId].payment.amount }({ id: invoiceId }); - - // Assert the actual and the expected state of the invoice - Types.Invoice memory invoice = invoiceModule.getInvoice({ id: invoiceId }); - assertEq(uint8(invoice.status), uint8(Types.Status.Ongoing)); - assertEq(invoice.payment.paymentsLeft, 3); - - // Assert the balances of payer and recipient - assertEq(usdt.balanceOf(users.bob), balanceOfBobBefore - invoices[invoiceId].payment.amount); - assertEq(usdt.balanceOf(address(space)), balanceOfRecipientBefore + invoices[invoiceId].payment.amount); - } - - function test_PayInvoice_PaymentMethodLinearStream() - external - whenInvoiceNotNull - whenInvoiceNotAlreadyPaid - whenInvoiceNotCanceled - givenPaymentMethodLinearStream - givenPaymentAmountInERC20Tokens - whenPaymentAmountEqualToInvoiceValue - { - // Set the linear USDT stream-based invoice as current one - uint256 invoiceId = 4; - - // Make Bob the payer for the default invoice - vm.startPrank({ msgSender: users.bob }); - - // Approve the {InvoiceModule} to transfer the ERC-20 tokens on Bob's behalf - usdt.approve({ spender: address(invoiceModule), amount: invoices[invoiceId].payment.amount }); - - // Expect the {InvoicePaid} event to be emitted - vm.expectEmit(); - emit Events.InvoicePaid({ - id: invoiceId, - payer: users.bob, - status: Types.Status.Ongoing, - payment: Types.Payment({ - method: invoices[invoiceId].payment.method, - recurrence: invoices[invoiceId].payment.recurrence, - paymentsLeft: 0, - asset: invoices[invoiceId].payment.asset, - amount: invoices[invoiceId].payment.amount, - streamId: 1 - }) - }); - - // Run the test - invoiceModule.payInvoice{ value: invoices[invoiceId].payment.amount }({ id: invoiceId }); - - // Assert the actual and the expected state of the invoice - Types.Invoice memory invoice = invoiceModule.getInvoice({ id: invoiceId }); - assertEq(uint8(invoice.status), uint8(Types.Status.Ongoing)); - assertEq(invoice.payment.streamId, 1); - assertEq(invoice.payment.paymentsLeft, 0); - - // Assert the actual and the expected state of the Sablier v2 linear stream - LockupLinear.StreamLL memory stream = invoiceModule.getLinearStream({ streamId: 1 }); - assertEq(stream.sender, address(invoiceModule)); - assertEq(stream.recipient, address(space)); - assertEq(address(stream.asset), address(usdt)); - assertEq(stream.startTime, invoice.startTime); - assertEq(stream.endTime, invoice.endTime); - } - - function test_PayInvoice_PaymentMethodTranchedStream() - external - whenInvoiceNotNull - whenInvoiceNotAlreadyPaid - whenInvoiceNotCanceled - givenPaymentMethodTranchedStream - givenPaymentAmountInERC20Tokens - whenPaymentAmountEqualToInvoiceValue - { - // Set the tranched USDT stream-based invoice as current one - uint256 invoiceId = 5; - - // Make Bob the payer for the default invoice - vm.startPrank({ msgSender: users.bob }); - - // Approve the {InvoiceModule} to transfer the ERC-20 tokens on Bob's behalf - usdt.approve({ spender: address(invoiceModule), amount: invoices[invoiceId].payment.amount }); - - // Expect the {InvoicePaid} event to be emitted - vm.expectEmit(); - emit Events.InvoicePaid({ - id: invoiceId, - payer: users.bob, - status: Types.Status.Ongoing, - payment: Types.Payment({ - method: invoices[invoiceId].payment.method, - recurrence: invoices[invoiceId].payment.recurrence, - paymentsLeft: 0, - asset: invoices[invoiceId].payment.asset, - amount: invoices[invoiceId].payment.amount, - streamId: 1 - }) - }); - - // Run the test - invoiceModule.payInvoice{ value: invoices[invoiceId].payment.amount }({ id: invoiceId }); - - // Assert the actual and the expected state of the invoice - Types.Invoice memory invoice = invoiceModule.getInvoice({ id: invoiceId }); - assertEq(uint8(invoice.status), uint8(Types.Status.Ongoing)); - assertEq(invoice.payment.streamId, 1); - assertEq(invoice.payment.paymentsLeft, 0); - - // Assert the actual and the expected state of the Sablier v2 tranched stream - LockupTranched.StreamLT memory stream = invoiceModule.getTranchedStream({ streamId: 1 }); - assertEq(stream.sender, address(invoiceModule)); - assertEq(stream.recipient, address(space)); - assertEq(address(stream.asset), address(usdt)); - assertEq(stream.startTime, invoice.startTime); - assertEq(stream.endTime, invoice.endTime); - assertEq(stream.tranches.length, 4); - } -} diff --git a/test/integration/concrete/invoice-module/transfer-from/transferFrom.t.sol b/test/integration/concrete/invoice-module/transfer-from/transferFrom.t.sol index 51acb07..798a126 100644 --- a/test/integration/concrete/invoice-module/transfer-from/transferFrom.t.sol +++ b/test/integration/concrete/invoice-module/transfer-from/transferFrom.t.sol @@ -3,37 +3,36 @@ pragma solidity ^0.8.26; import { TransferFrom_Integration_Shared_Test } from "../../../shared/transferFrom.t.sol"; import { Errors } from "../../../../utils/Errors.sol"; -import { Events } from "../../../../utils/Events.sol"; -import { Types } from "./../../../../../src/modules/invoice-module/libraries/Types.sol"; +import { Types } from "./../../../../../src/modules/payment-module/libraries/Types.sol"; contract TransferFrom_Integration_Concret_Test is TransferFrom_Integration_Shared_Test { function setUp() public virtual override { TransferFrom_Integration_Shared_Test.setUp(); } - function test_RevertWhen_TokenDoesNotExist() external { - // Make Eve's space the caller which is the recipient of the invoice + /* function test_RevertWhen_TokenDoesNotExist() external { + // Make Eve's space the caller which is the recipient of the payment request vm.startPrank({ msgSender: address(space) }); // Expect the call to revert with the {ERC721NonexistentToken} error vm.expectRevert(abi.encodeWithSelector(Errors.ERC721NonexistentToken.selector, 99)); // Run the test - invoiceModule.transferFrom({ from: address(space), to: users.eve, tokenId: 99 }); + invoiceModul.transferFrom({ from: address(space), to: users.eve, tokenId: 99 }); } function test_TransferFrom_PaymentMethodStream() external whenTokenExists { - uint256 invoiceId = 4; + uint256 paymentRequestId = 4; uint256 streamId = 1; - // Make Bob the payer for the invoice + // Make Bob the payer for the payment request vm.startPrank({ msgSender: users.bob }); // Approve the {InvoiceModule} to transfer the USDT tokens on Bob's behalf - usdt.approve({ spender: address(invoiceModule), amount: invoices[invoiceId].payment.amount }); + usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].payment.amount }); - // Pay the invoice - invoiceModule.payInvoice{ value: invoices[invoiceId].payment.amount }({ id: invoiceId }); + // Pay the payment request + paymentModule.payRequest{ value: paymentRequests[paymentRequestId].payment.amount }({ requestId: paymentRequestId }); // Simulate the passage of time so that the maximum withdrawable amount is non-zero vm.warp(block.timestamp + 5 weeks); @@ -42,40 +41,38 @@ contract TransferFrom_Integration_Concret_Test is TransferFrom_Integration_Share uint256 balanceOfBefore = usdt.balanceOf(address(space)); // Get the maximum withdrawable amount from the stream before transferring the stream NFT - uint128 maxWithdrawableAmount = invoiceModule.withdrawableAmountOf({ - streamType: Types.Method.LinearStream, - streamId: streamId - }); + uint128 maxWithdrawableAmount = + paymentModule.withdrawableAmountOf({ streamType: Types.Method.LinearStream, streamId: streamId }); - // Make Eve's space the caller which is the recipient of the invoice + // Make Eve's space the caller which is the recipient of the payment request vm.startPrank({ msgSender: address(space) }); // Approve the {InvoiceModule} to transfer the `streamId` stream on behalf of the Eve's space - sablierV2LockupLinear.approve({ to: address(invoiceModule), tokenId: streamId }); + sablierV2LockupLinear.approve({ to: address(paymentModule), tokenId: streamId }); // Run the test - invoiceModule.transferFrom({ from: address(space), to: users.eve, tokenId: invoiceId }); + paymentModule.transferFrom({ from: address(space), to: users.eve, tokenrequestId: paymentRequestId }); // Assert the current and expected Eve's space USDT balance assertEq(balanceOfBefore + maxWithdrawableAmount, usdt.balanceOf(address(space))); - // Assert the current and expected owner of the invoice NFT - assertEq(invoiceModule.ownerOf({ tokenId: invoiceId }), users.eve); + // Assert the current and expected owner of the payment request NFT + assertEq(paymentModule.ownerOf({ tokenrequestId: paymentRequestId }), users.eve); - // Assert the current and expected owner of the invoice stream NFT + // Assert the current and expected owner of the payment request stream NFT assertEq(sablierV2LockupLinear.ownerOf({ tokenId: streamId }), users.eve); } function test_TransferFrom_PaymentTransfer() external whenTokenExists { - uint256 invoiceId = 1; + uint256 paymentRequestId = 1; - // Make Eve's space the caller which is the recipient of the invoice + // Make Eve's space the caller which is the recipient of the payment request vm.startPrank({ msgSender: address(space) }); // Run the test - invoiceModule.transferFrom({ from: address(space), to: users.eve, tokenId: invoiceId }); + paymentModule.transferFrom({ from: address(space), to: users.eve, tokenrequestId: paymentRequestId }); - // Assert the current and expected owner of the invoice NFT - assertEq(invoiceModule.ownerOf({ tokenId: invoiceId }), users.eve); - } + // Assert the current and expected owner of the payment request NFT + assertEq(paymentModule.ownerOf({ tokenrequestId: paymentRequestId }), users.eve); + } */ } diff --git a/test/integration/concrete/invoice-module/transfer-from/transferFrom.tree b/test/integration/concrete/invoice-module/transfer-from/transferFrom.tree index 93f9779..76d1a46 100644 --- a/test/integration/concrete/invoice-module/transfer-from/transferFrom.tree +++ b/test/integration/concrete/invoice-module/transfer-from/transferFrom.tree @@ -5,6 +5,6 @@ transferFrom.t.sol ├── when the payment method is stream-based │ ├── it should withdraw the maximum withdrawable amount of the Sablier stream │ ├── it should transfer the Sablier stream NFT - │ └── it should transfer the invoice NFT + │ └── it should transfer the payment request NFT └── when the payment is transfer-based - └── it should transfer the invoice NFT \ No newline at end of file + └── it should transfer the payment request NFT \ No newline at end of file diff --git a/test/integration/concrete/invoice-module/withdraw-invoice-stream/withdrawStream.tree b/test/integration/concrete/invoice-module/withdraw-invoice-stream/withdrawStream.tree deleted file mode 100644 index e5dc7ad..0000000 --- a/test/integration/concrete/invoice-module/withdraw-invoice-stream/withdrawStream.tree +++ /dev/null @@ -1,7 +0,0 @@ -withdrawStream.t.sol -├── given the payment method is linear stream -│ └── given the invoice status is Ongoing -│ └── it should allow the invoice recipient to withdraw from the stream -└── given the payment method is tranched stream - └── given the invoice status is Ongoing - └── it should allow the invoice recipient to withdraw from the stream \ No newline at end of file diff --git a/test/integration/concrete/payment-module/cancel-request/cancelRequest.t.sol b/test/integration/concrete/payment-module/cancel-request/cancelRequest.t.sol new file mode 100644 index 0000000..626f830 --- /dev/null +++ b/test/integration/concrete/payment-module/cancel-request/cancelRequest.t.sol @@ -0,0 +1,325 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import { CancelRequest_Integration_Shared_Test } from "../../../shared/cancelRequest.t.sol"; +import { Types } from "./../../../../../src/modules/payment-module/libraries/Types.sol"; +import { Events } from "../../../../utils/Events.sol"; +import { Errors } from "../../../../utils/Errors.sol"; + +contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Shared_Test { + function setUp() public virtual override { + CancelRequest_Integration_Shared_Test.setUp(); + } + + function test_RevertWhen_InvoiceIsPaid() external { + // Set the one-off ETH transfer payment request as current one + uint256 paymentRequestId = 2; + + // Make Bob the payer for the default paymentRequest + vm.startPrank({ msgSender: users.bob }); + + // Pay the payment request first + paymentModule.payRequest{ value: paymentRequests[paymentRequestId].config.amount }({ + requestId: paymentRequestId + }); + + // Make Eve the caller who is the recipient of the payment request + vm.startPrank({ msgSender: users.eve }); + + // Expect the call to revert with the {RequestPaid} error + vm.expectRevert(Errors.RequestPaid.selector); + + // Run the test + paymentModule.cancelRequest({ requestId: paymentRequestId }); + } + + function test_RevertWhen_RequestCanceled() external whenRequestNotAlreadyPaid { + // Set the one-off ETH transfer payment request as current one + uint256 paymentRequestId = 2; + + // Make Eve's space the caller which is the recipient of the payment request + vm.startPrank({ msgSender: address(space) }); + + // Cancel the payment request first + paymentModule.cancelRequest({ requestId: paymentRequestId }); + + // Expect the call to revert with the {RequestCanceled} error + vm.expectRevert(Errors.RequestCanceled.selector); + + // Run the test + paymentModule.cancelRequest({ requestId: paymentRequestId }); + } + + function test_RevertWhen_PaymentMethodTransfer_SenderNotInvoiceRecipient() + external + whenRequestNotAlreadyPaid + whenRequestNotCanceled + givenPaymentMethodTransfer + { + // Set the one-off ETH transfer payment request as current one + uint256 paymentRequestId = 2; + + // Make Bob the caller who IS NOT the recipient of the payment request + vm.startPrank({ msgSender: users.bob }); + + // Expect the call to revert with the {OnlyRequestRecipient} error + vm.expectRevert(Errors.OnlyRequestRecipient.selector); + + // Run the test + paymentModule.cancelRequest({ requestId: paymentRequestId }); + } + + function test_CancelRequest_PaymentMethodTransfer() + external + whenRequestNotAlreadyPaid + whenRequestNotCanceled + givenPaymentMethodTransfer + whenSenderInvoiceRecipient + { + // Set the one-off ETH transfer payment request as current one + uint256 paymentRequestId = 2; + + // Make Eve's space the caller which is the recipient of the payment request + vm.startPrank({ msgSender: address(space) }); + + // Expect the {RequestCanceled} event to be emitted + vm.expectEmit(); + emit Events.RequestCanceled({ requestId: paymentRequestId }); + + // Run the test + paymentModule.cancelRequest({ requestId: paymentRequestId }); + + // Assert the actual and expected paymentRequest status + Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: paymentRequestId }); + assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Canceled)); + } + + function test_RevertWhen_PaymentMethodLinearStream_StatusPending_SenderNotInvoiceRecipient() + external + whenRequestNotAlreadyPaid + whenRequestNotCanceled + givenPaymentMethodLinearStream + givenInvoiceStatusPending + { + // Set current paymentRequest as a linear stream-based one + uint256 paymentRequestId = 5; + + // Make Bob the caller who IS NOT the recipient of the payment request + vm.startPrank({ msgSender: users.bob }); + + // Expect the call to revert with the {OnlyRequestRecipient} error + vm.expectRevert(Errors.OnlyRequestRecipient.selector); + + // Run the test + paymentModule.cancelRequest({ requestId: paymentRequestId }); + } + + function test_CancelRequest_PaymentMethodLinearStream_StatusCanceled() + external + whenRequestNotAlreadyPaid + whenRequestNotCanceled + givenPaymentMethodLinearStream + givenInvoiceStatusPending + whenSenderInvoiceRecipient + { + // Set current paymentRequest as a linear stream-based one + uint256 paymentRequestId = 5; + + // Make Eve's space the caller which is the recipient of the payment request + vm.startPrank({ msgSender: address(space) }); + + // Expect the {RequestCanceled} event to be emitted + vm.expectEmit(); + emit Events.RequestCanceled({ requestId: paymentRequestId }); + + // Run the test + paymentModule.cancelRequest({ requestId: paymentRequestId }); + + // Assert the actual and expected paymentRequest status + Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: paymentRequestId }); + assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Canceled)); + } + + function test_RevertWhen_PaymentMethodLinearStream_StatusPending_SenderNoInitialtStreamSender() + external + whenRequestNotAlreadyPaid + whenRequestNotCanceled + givenPaymentMethodLinearStream + givenRequestStatusPending + { + // Set current paymentRequest as a linear stream-based one + uint256 paymentRequestId = 5; + + // The payment request must be paid for its status to be updated to `Accepted` + // Make Bob the payer of the payment request (also Bob will be the stream sender) + vm.startPrank({ msgSender: users.bob }); + + // Approve the {InvoiceModule} to transfer the USDT tokens on Bob's behalf + usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); + + // Pay the payment request first (status will be updated to `Accepted`) + paymentModule.payRequest{ value: paymentRequests[paymentRequestId].config.amount }({ + requestId: paymentRequestId + }); + + // Make Eve the caller who IS NOT the initial stream sender but rather the recipient + vm.startPrank({ msgSender: users.eve }); + + // Expect the call to revert with the {OnlyInitialStreamSender} error + vm.expectRevert(abi.encodeWithSelector(Errors.OnlyInitialStreamSender.selector, users.bob)); + + // Run the test + paymentModule.cancelRequest({ requestId: paymentRequestId }); + } + + function test_CancelRequest_PaymentMethodLinearStream_StatusPending() + external + whenRequestNotAlreadyPaid + whenRequestNotCanceled + givenPaymentMethodLinearStream + givenRequestStatusPending + whenSenderInitialStreamSender + { + // Set current paymentRequest as a linear stream-based one + uint256 paymentRequestId = 5; + + // The payment request must be paid for its status to be updated to `Accepted` + // Make Bob the payer of the payment request (also Bob will be the initial stream sender) + vm.startPrank({ msgSender: users.bob }); + + // Approve the {InvoiceModule} to transfer the USDT tokens on Bob's behalf + usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); + + // Pay the payment request first (status will be updated to `Accepted`) + paymentModule.payRequest{ value: paymentRequests[paymentRequestId].config.amount }({ + requestId: paymentRequestId + }); + + // Expect the {RequestCanceled} event to be emitted + vm.expectEmit(); + emit Events.RequestCanceled({ requestId: paymentRequestId }); + + // Make Bob the caller who is the sender of the payment request stream + vm.startPrank({ msgSender: users.bob }); + + // Run the test + paymentModule.cancelRequest({ requestId: paymentRequestId }); + + // Assert the actual and expected paymentRequest status + Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: paymentRequestId }); + assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Canceled)); + } + + function test_RevertWhen_PaymentMethodTranchedStream_StatusPending_SenderNotInvoiceRecipient() + external + whenRequestNotAlreadyPaid + whenRequestNotCanceled + givenPaymentMethodTranchedStream + givenInvoiceStatusPending + { + // Set current paymentRequest as a tranched stream-based one + uint256 paymentRequestId = 5; + + // Make Bob the caller who IS NOT the recipient of the payment request + vm.startPrank({ msgSender: users.bob }); + + // Expect the call to revert with the {OnlyRequestRecipient} error + vm.expectRevert(Errors.OnlyRequestRecipient.selector); + + // Run the test + paymentModule.cancelRequest({ requestId: paymentRequestId }); + } + + function test_CancelRequest_PaymentMethodTranchedStream_StatusCanceled() + external + whenRequestNotAlreadyPaid + whenRequestNotCanceled + givenPaymentMethodTranchedStream + givenInvoiceStatusPending + whenSenderInvoiceRecipient + { + // Set current paymentRequest as a tranched stream-based one + uint256 paymentRequestId = 5; + + // Make Eve's space the caller which is the recipient of the payment request + vm.startPrank({ msgSender: address(space) }); + + // Expect the {RequestCanceled} event to be emitted + vm.expectEmit(); + emit Events.RequestCanceled({ requestId: paymentRequestId }); + + // Run the test + paymentModule.cancelRequest({ requestId: paymentRequestId }); + + // Assert the actual and expected paymentRequest status + Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: paymentRequestId }); + assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Canceled)); + } + + function test_RevertWhen_PaymentMethodTranchedStream_StatusPending_SenderNoInitialtStreamSender() + external + whenRequestNotAlreadyPaid + whenRequestNotCanceled + givenPaymentMethodTranchedStream + givenRequestStatusPending + { + // Set current paymentRequest as a tranched stream-based one + uint256 paymentRequestId = 5; + + // The payment request must be paid for its status to be updated to `Accepted` + // Make Bob the payer of the payment request (also Bob will be the stream sender) + vm.startPrank({ msgSender: users.bob }); + + // Approve the {InvoiceModule} to transfer the USDT tokens on Bob's behalf + usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); + + // Pay the payment request first (status will be updated to `Accepted`) + paymentModule.payRequest{ value: paymentRequests[paymentRequestId].config.amount }({ + requestId: paymentRequestId + }); + + // Make Eve the caller who IS NOT the initial stream sender but rather the recipient + vm.startPrank({ msgSender: users.eve }); + + // Expect the call to revert with the {OnlyInitialStreamSender} error + vm.expectRevert(abi.encodeWithSelector(Errors.OnlyInitialStreamSender.selector, users.bob)); + + // Run the test + paymentModule.cancelRequest({ requestId: paymentRequestId }); + } + + function test_CancelRequest_PaymentMethodTranchedStream_StatusPending() + external + whenRequestNotAlreadyPaid + whenRequestNotCanceled + givenPaymentMethodTranchedStream + givenRequestStatusPending + whenSenderInitialStreamSender + { + // Set current paymentRequest as a tranched stream-based one + uint256 paymentRequestId = 5; + + // The payment request must be paid for its status to be updated to `Accepted` + // Make Bob the payer of the payment request (also Bob will be the initial stream sender) + vm.startPrank({ msgSender: users.bob }); + + // Approve the {InvoiceModule} to transfer the USDT tokens on Bob's behalf + usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); + + // Pay the payment request first (status will be updated to `Accepted`) + paymentModule.payRequest{ value: paymentRequests[paymentRequestId].config.amount }({ + requestId: paymentRequestId + }); + + // Expect the {RequestCanceled} event to be emitted + vm.expectEmit(); + emit Events.RequestCanceled({ requestId: paymentRequestId }); + + // Run the test + paymentModule.cancelRequest({ requestId: paymentRequestId }); + + // Assert the actual and expected paymentRequest status + Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: paymentRequestId }); + assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Canceled)); + } +} diff --git a/test/integration/concrete/payment-module/cancel-request/cancelRequest.tree b/test/integration/concrete/payment-module/cancel-request/cancelRequest.tree new file mode 100644 index 0000000..6e53955 --- /dev/null +++ b/test/integration/concrete/payment-module/cancel-request/cancelRequest.tree @@ -0,0 +1,40 @@ +cancelRequest.t.sol +├── when the request status IS Paid +│ └── it should revert with the {RequestPaid} error +└── when the request status IS NOT Paid + ├── when the request status IS Canceled + │ └── it should revert with the {RequestCanceled} error + └── when the request status IS NOT Canceled + ├── given the payment method is transfer + │ ├── when the sender IS NOT the request recipient + │ │ └── it should revert with the {OnlyRequestRecipient} + │ └── when the sender IS the request recipient + │ ├── it should mark the request as Canceled + │ └── it should emit a {RequestCanceled} event + ├── given the payment method is linear stream-based + │ ├── given the request status is Pending + │ │ ├── when the sender IS NOT the request recipient + │ │ │ └── it should revert with the {OnlyRequestRecipient} + │ │ └── when the sender IS the request recipient + │ │ ├── it should mark the request as Canceled + │ │ └── it should emit an {RequestCanceled} event + │ └── given the request status is Accepted + │ ├── when the sender IS NOT the initial stream sender + │ │ └── it should revert with the {OnlyInitialStreamSender} error + │ └── when the sender IS the initial stream sender + │ ├── it should mark the request as Canceled + │ └── it should emit an {RequestCanceled} event + └── given the payment method is tranched stream-based + ├── given the request status is Pending + │ ├── when the sender IS NOT the request recipient + │ │ └── it should revert with the {OnlyRequestRecipient} + │ └── when the sender IS the request recipient + │ ├── it should mark the request as Canceled + │ └── it should emit an {RequestCanceled} event + └── given the request status is Accepted + ├── when the sender IS NOT the initial stream sender + │ └──it should revert with the {OnlyInitialStreamSender} error + └── when the sender IS the initial stream sender + ├── it should mark the request as Canceled + └── it should emit an {RequestCanceled} event + diff --git a/test/integration/concrete/payment-module/create-request/createRequest.t.sol b/test/integration/concrete/payment-module/create-request/createRequest.t.sol new file mode 100644 index 0000000..dacd592 --- /dev/null +++ b/test/integration/concrete/payment-module/create-request/createRequest.t.sol @@ -0,0 +1,519 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import { CreateRequest_Integration_Shared_Test } from "../../../shared/createRequest.t.sol"; +import { Types } from "./../../../../../src/modules/payment-module/libraries/Types.sol"; +import { Errors } from "../../../../utils/Errors.sol"; +import { Events } from "../../../../utils/Events.sol"; + +contract CreateRequest_Integration_Concret_Test is CreateRequest_Integration_Shared_Test { + Types.PaymentRequest paymentRequest; + + function setUp() public virtual override { + CreateRequest_Integration_Shared_Test.setUp(); + } + + function test_RevertWhen_CallerNotContract() external { + // Make Bob the caller in this test suite which is an EOA + vm.startPrank({ msgSender: users.bob }); + + // Expect the call to revert with the {SpaceZeroCodeSize} error + vm.expectRevert(Errors.SpaceZeroCodeSize.selector); + + // Create a one-off transfer payment request + paymentRequest = createPaymentRequestWithOneOffTransfer({ asset: address(usdt), recipient: users.bob }); + + // Run the test + paymentModule.createRequest(paymentRequest); + } + + function test_RevertWhen_NonCompliantSpace() external whenCallerContract { + // Make Eve the caller in this test suite as she's the owner of the {Space} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a one-off transfer payment request + paymentRequest = createPaymentRequestWithOneOffTransfer({ asset: address(usdt), recipient: address(space) }); + + // Create the calldata for the Payment Module execution + bytes memory data = abi.encodeWithSignature( + "createRequest((bool,bool,uint40,uint40,address,(uint8,uint8,uint40,address,uint128,uint256)))", + paymentRequest + ); + + // Expect the call to revert with the {SpaceUnsupportedInterface} error + vm.expectRevert(Errors.SpaceUnsupportedInterface.selector); + + // Run the test + mockNonCompliantSpace.execute({ module: address(paymentModule), value: 0, data: data }); + } + + function test_RevertWhen_ZeroPaymentAmount() external whenCallerContract whenCompliantSpace { + // Make Eve the caller in this test suite as she's the owner of the {Space} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a one-off transfer payment request + paymentRequest = createPaymentRequestWithOneOffTransfer({ asset: address(usdt), recipient: address(space) }); + + // Set the payment amount to zero to simulate the error + paymentRequest.config.amount = 0; + + // Create the calldata for the Payment Module execution + bytes memory data = abi.encodeWithSignature( + "createRequest((bool,bool,uint40,uint40,address,(uint8,uint8,uint40,address,uint128,uint256)))", + paymentRequest + ); + + // Expect the call to revert with the {ZeroPaymentAmount} error + vm.expectRevert(Errors.ZeroPaymentAmount.selector); + + // Run the test + space.execute({ module: address(paymentModule), value: 0, data: data }); + } + + function test_RevertWhen_StartTimeGreaterThanEndTime() + external + whenCallerContract + whenCompliantSpace + whenNonZeroPaymentAmount + { + // Make Eve the caller in this test suite as she's the owner of the {Space} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a one-off transfer payment request + paymentRequest = createPaymentRequestWithOneOffTransfer({ asset: address(usdt), recipient: address(space) }); + + // Set the start time to be the current timestamp and the end time one second earlier + paymentRequest.startTime = uint40(block.timestamp); + paymentRequest.endTime = uint40(block.timestamp) - 1; + + // Create the calldata for the Payment Module execution + bytes memory data = abi.encodeWithSignature( + "createRequest((bool,bool,uint40,uint40,address,(uint8,uint8,uint40,address,uint128,uint256)))", + paymentRequest + ); + + // Expect the call to revert with the {StartTimeGreaterThanEndTime} error + vm.expectRevert(Errors.StartTimeGreaterThanEndTime.selector); + + // Run the test + space.execute({ module: address(paymentModule), value: 0, data: data }); + } + + function test_RevertWhen_EndTimeInThePast() + external + whenCallerContract + whenCompliantSpace + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + { + // Make Eve the caller in this test suite as she's the owner of the {Space} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a one-off transfer payment request + paymentRequest = createPaymentRequestWithOneOffTransfer({ asset: address(usdt), recipient: address(space) }); + + // Set the block.timestamp to 1641070800 + vm.warp(1_641_070_800); + + // Set the start time to be the lower than the end time so the 'start time lower than end time' passes + // but set the end time in the past to get the {EndTimeInThePast} revert + paymentRequest.startTime = uint40(block.timestamp) - 2 days; + paymentRequest.endTime = uint40(block.timestamp) - 1 days; + + // Create the calldata for the Payment Module execution + bytes memory data = abi.encodeWithSignature( + "createRequest((bool,bool,uint40,uint40,address,(uint8,uint8,uint40,address,uint128,uint256)))", + paymentRequest + ); + + // Expect the call to revert with the {EndTimeInThePast} error + vm.expectRevert(Errors.EndTimeInThePast.selector); + + // Run the test + space.execute({ module: address(paymentModule), value: 0, data: data }); + } + + function test_CreateRequest_PaymentMethodOneOffTransfer() + external + whenCallerContract + whenCompliantSpace + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + givenPaymentMethodOneOffTransfer + { + // Make Eve the caller in this test suite as she's the owner of the {Space} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a recurring transfer payment request that must be paid on a monthly basis + // Hence, the interval between the start and end time must be at least 1 month + paymentRequest = createPaymentRequestWithOneOffTransfer({ asset: address(usdt), recipient: address(space) }); + + // Create the calldata for the Payment Module execution + bytes memory data = abi.encodeWithSignature( + "createRequest((bool,bool,uint40,uint40,address,(uint8,uint8,uint40,address,uint128,uint256)))", + paymentRequest + ); + + // Expect the module call to emit an {RequestCreated} event + vm.expectEmit(); + emit Events.RequestCreated({ + requestId: 1, + recipient: address(space), + startTime: paymentRequest.startTime, + endTime: paymentRequest.endTime, + config: paymentRequest.config + }); + + // Expect the {Space} contract to emit a {ModuleExecutionSucceded} event + vm.expectEmit(); + emit Events.ModuleExecutionSucceded({ module: address(paymentModule), value: 0, data: data }); + + // Run the test + space.execute({ module: address(paymentModule), value: 0, data: data }); + + // Assert the actual and expected paymentRequest state + Types.PaymentRequest memory actualRequest = paymentModule.getRequest({ requestId: 1 }); + Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: 1 }); + + assertEq(actualRequest.recipient, address(space)); + assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Pending)); + assertEq(actualRequest.startTime, paymentRequest.startTime); + assertEq(actualRequest.endTime, paymentRequest.endTime); + assertEq(uint8(actualRequest.config.method), uint8(Types.Method.Transfer)); + assertEq(uint8(actualRequest.config.recurrence), uint8(Types.Recurrence.OneOff)); + assertEq(actualRequest.config.paymentsLeft, 1); + assertEq(actualRequest.config.asset, paymentRequest.config.asset); + assertEq(actualRequest.config.amount, paymentRequest.config.amount); + assertEq(actualRequest.config.streamId, 0); + } + + function test_RevertWhen_PaymentMethodRecurringTransfer_PaymentIntervalTooShortForSelectedRecurrence() + external + whenCallerContract + whenCompliantSpace + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + givenPaymentMethodRecurringTransfer + { + // Make Eve the caller in this test suite as she's the owner of the {Space} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a recurring transfer payment request that must be paid on a monthly basis + // Hence, the interval between the start and end time must be at least 1 month + paymentRequest = + createInvoiceWithRecurringTransfer({ recurrence: Types.Recurrence.Monthly, recipient: address(space) }); + + // Alter the end time to be 3 weeks from now + paymentRequest.endTime = uint40(block.timestamp) + 3 weeks; + + // Create the calldata for the Payment Module execution + bytes memory data = abi.encodeWithSignature( + "createRequest((bool,bool,uint40,uint40,address,(uint8,uint8,uint40,address,uint128,uint256)))", + paymentRequest + ); + + // Expect the call to revert with the {PaymentIntervalTooShortForSelectedRecurrence} error + vm.expectRevert(Errors.PaymentIntervalTooShortForSelectedRecurrence.selector); + + // Run the test + space.execute({ module: address(paymentModule), value: 0, data: data }); + } + + function test_CreateRequest_RecurringTransfer() + external + whenCallerContract + whenCompliantSpace + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + givenPaymentMethodRecurringTransfer + whenPaymentIntervalLongEnough + { + // Make Eve the caller in this test suite as she's the owner of the {Space} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a recurring transfer payment request that must be paid on weekly basis + paymentRequest = + createInvoiceWithRecurringTransfer({ recurrence: Types.Recurrence.Weekly, recipient: address(space) }); + + // Create the calldata for the Payment Module execution + bytes memory data = abi.encodeWithSignature( + "createRequest((bool,bool,uint40,uint40,address,(uint8,uint8,uint40,address,uint128,uint256)))", + paymentRequest + ); + + // Expect the module call to emit an {RequestCreated} event + vm.expectEmit(); + emit Events.RequestCreated({ + requestId: 1, + recipient: address(space), + startTime: paymentRequest.startTime, + endTime: paymentRequest.endTime, + config: paymentRequest.config + }); + + // Expect the {Space} contract to emit a {ModuleExecutionSucceded} event + vm.expectEmit(); + emit Events.ModuleExecutionSucceded({ module: address(paymentModule), value: 0, data: data }); + + // Run the test + space.execute({ module: address(paymentModule), value: 0, data: data }); + + // Assert the actual and expected paymentRequest state + Types.PaymentRequest memory actualRequest = paymentModule.getRequest({ requestId: 1 }); + Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: 1 }); + + assertEq(actualRequest.recipient, address(space)); + assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Pending)); + assertEq(actualRequest.startTime, paymentRequest.startTime); + assertEq(actualRequest.endTime, paymentRequest.endTime); + assertEq(uint8(actualRequest.config.method), uint8(Types.Method.Transfer)); + assertEq(uint8(actualRequest.config.recurrence), uint8(Types.Recurrence.Weekly)); + assertEq(actualRequest.config.paymentsLeft, 4); + assertEq(actualRequest.config.asset, paymentRequest.config.asset); + assertEq(actualRequest.config.amount, paymentRequest.config.amount); + assertEq(actualRequest.config.streamId, 0); + } + + function test_RevertWhen_PaymentMethodTranchedStream_RecurrenceSetToOneOff() + external + whenCallerContract + whenCompliantSpace + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + givenPaymentMethodTranchedStream + { + // Make Eve the caller in this test suite as she's the owner of the {Space} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a new paymentRequest with a tranched stream payment + paymentRequest = + createPaymentRequestWithTranchedStream({ recurrence: Types.Recurrence.Weekly, recipient: address(space) }); + + // Alter the payment recurrence by setting it to one-off + paymentRequest.config.recurrence = Types.Recurrence.OneOff; + + // Expect the call to revert with the {TranchedStreamInvalidOneOffRecurence} error + vm.expectRevert(Errors.TranchedStreamInvalidOneOffRecurence.selector); + + // Create the calldata for the Payment Module execution + bytes memory data = abi.encodeWithSignature( + "createRequest((bool,bool,uint40,uint40,address,(uint8,uint8,uint40,address,uint128,uint256)))", + paymentRequest + ); + + // Run the test + space.execute({ module: address(paymentModule), value: 0, data: data }); + } + + function test_RevertWhen_PaymentMethodTranchedStream_PaymentIntervalTooShortForSelectedRecurrence() + external + whenCallerContract + whenCompliantSpace + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + givenPaymentMethodTranchedStream + whenTranchedStreamWithGoodRecurring + { + // Make Eve the caller in this test suite as she's the owner of the {Space} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a new paymentRequest with a tranched stream payment + paymentRequest = + createPaymentRequestWithTranchedStream({ recurrence: Types.Recurrence.Monthly, recipient: address(space) }); + + // Alter the end time to be 3 weeks from now + paymentRequest.endTime = uint40(block.timestamp) + 3 weeks; + + // Expect the call to revert with the {PaymentIntervalTooShortForSelectedRecurrence} error + vm.expectRevert(Errors.PaymentIntervalTooShortForSelectedRecurrence.selector); + + // Create the calldata for the Payment Module execution + bytes memory data = abi.encodeWithSignature( + "createRequest((bool,bool,uint40,uint40,address,(uint8,uint8,uint40,address,uint128,uint256)))", + paymentRequest + ); + + // Run the test + space.execute({ module: address(paymentModule), value: 0, data: data }); + } + + function test_RevertWhen_PaymentMethodTranchedStream_PaymentAssetNativeToken() + external + whenCallerContract + whenCompliantSpace + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + givenPaymentMethodTranchedStream + whenTranchedStreamWithGoodRecurring + whenPaymentIntervalLongEnough + { + // Make Eve the caller in this test suite as she's the owner of the {Space} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a new paymentRequest with a linear stream payment + paymentRequest = + createPaymentRequestWithTranchedStream({ recurrence: Types.Recurrence.Weekly, recipient: address(space) }); + + // Alter the payment asset by setting it to + paymentRequest.config.asset = address(0); + + // Expect the call to revert with the {OnlyERC20StreamsAllowed} error + vm.expectRevert(Errors.OnlyERC20StreamsAllowed.selector); + + // Create the calldata for the Payment Module execution + bytes memory data = abi.encodeWithSignature( + "createRequest((bool,bool,uint40,uint40,address,(uint8,uint8,uint40,address,uint128,uint256)))", + paymentRequest + ); + + // Run the test + space.execute({ module: address(paymentModule), value: 0, data: data }); + } + + function test_CreateRequest_Tranched() + external + whenCallerContract + whenCompliantSpace + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + givenPaymentMethodTranchedStream + whenPaymentAssetNotNativeToken + { + // Make Eve the caller in this test suite as she's the owner of the {Space} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a new paymentRequest with a tranched stream payment + paymentRequest = + createPaymentRequestWithTranchedStream({ recurrence: Types.Recurrence.Weekly, recipient: address(space) }); + + // Create the calldata for the Payment Module execution + bytes memory data = abi.encodeWithSignature( + "createRequest((bool,bool,uint40,uint40,address,(uint8,uint8,uint40,address,uint128,uint256)))", + paymentRequest + ); + + // Expect the module call to emit an {RequestCreated} event + vm.expectEmit(); + emit Events.RequestCreated({ + requestId: 1, + recipient: address(space), + startTime: paymentRequest.startTime, + endTime: paymentRequest.endTime, + config: paymentRequest.config + }); + + // Expect the {Space} contract to emit a {ModuleExecutionSucceded} event + vm.expectEmit(); + emit Events.ModuleExecutionSucceded({ module: address(paymentModule), value: 0, data: data }); + + // Run the test + space.execute({ module: address(paymentModule), value: 0, data: data }); + + // Assert the actual and expected paymentRequest state + Types.PaymentRequest memory actualRequest = paymentModule.getRequest({ requestId: 1 }); + Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: 1 }); + + assertEq(actualRequest.recipient, address(space)); + assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Pending)); + assertEq(actualRequest.startTime, paymentRequest.startTime); + assertEq(actualRequest.endTime, paymentRequest.endTime); + assertEq(uint8(actualRequest.config.method), uint8(Types.Method.TranchedStream)); + assertEq(uint8(actualRequest.config.recurrence), uint8(Types.Recurrence.Weekly)); + assertEq(actualRequest.config.paymentsLeft, 1); + assertEq(actualRequest.config.asset, paymentRequest.config.asset); + assertEq(actualRequest.config.amount, paymentRequest.config.amount); + assertEq(actualRequest.config.streamId, 0); + } + + function test_RevertWhen_PaymentMethodLinearStream_PaymentAssetNativeToken() + external + whenCallerContract + whenCompliantSpace + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + givenPaymentMethodLinearStream + { + // Make Eve the caller in this test suite as she's the owner of the {Space} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a new paymentRequest with a linear stream payment + paymentRequest = createPaymentRequestWithLinearStream({ recipient: address(space) }); + + // Alter the payment asset by setting it to + paymentRequest.config.asset = address(0); + + // Expect the call to revert with the {OnlyERC20StreamsAllowed} error + vm.expectRevert(Errors.OnlyERC20StreamsAllowed.selector); + + // Create the calldata for the Payment Module execution + bytes memory data = abi.encodeWithSignature( + "createRequest((bool,bool,uint40,uint40,address,(uint8,uint8,uint40,address,uint128,uint256)))", + paymentRequest + ); + + // Run the test + space.execute({ module: address(paymentModule), value: 0, data: data }); + } + + function test_CreateRequest_LinearStream() + external + whenCallerContract + whenCompliantSpace + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + givenPaymentMethodLinearStream + whenPaymentAssetNotNativeToken + { + // Make Eve the caller in this test suite as she's the owner of the {Space} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a new paymentRequest with a linear stream payment + paymentRequest = createPaymentRequestWithLinearStream({ recipient: address(space) }); + + // Create the calldata for the Payment Module execution + bytes memory data = abi.encodeWithSignature( + "createRequest((bool,bool,uint40,uint40,address,(uint8,uint8,uint40,address,uint128,uint256)))", + paymentRequest + ); + + // Expect the module call to emit an {RequestCreated} event + vm.expectEmit(); + emit Events.RequestCreated({ + requestId: 1, + recipient: address(space), + startTime: paymentRequest.startTime, + endTime: paymentRequest.endTime, + config: paymentRequest.config + }); + + // Expect the {Space} contract to emit a {ModuleExecutionSucceded} event + vm.expectEmit(); + emit Events.ModuleExecutionSucceded({ module: address(paymentModule), value: 0, data: data }); + + // Run the test + space.execute({ module: address(paymentModule), value: 0, data: data }); + + // Assert the actual and expected paymentRequest state + Types.PaymentRequest memory actualRequest = paymentModule.getRequest({ requestId: 1 }); + Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: 1 }); + + assertEq(actualRequest.recipient, address(space)); + assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Pending)); + assertEq(actualRequest.startTime, paymentRequest.startTime); + assertEq(actualRequest.endTime, paymentRequest.endTime); + assertEq(uint8(actualRequest.config.method), uint8(Types.Method.LinearStream)); + assertEq(uint8(actualRequest.config.recurrence), uint8(Types.Recurrence.Weekly)); + assertEq(actualRequest.config.asset, paymentRequest.config.asset); + assertEq(actualRequest.config.amount, paymentRequest.config.amount); + assertEq(actualRequest.config.streamId, 0); + } +} diff --git a/test/integration/concrete/invoice-module/create-invoice/createInvoice.tree b/test/integration/concrete/payment-module/create-request/createRequest.tree similarity index 85% rename from test/integration/concrete/invoice-module/create-invoice/createInvoice.tree rename to test/integration/concrete/payment-module/create-request/createRequest.tree index 23a5184..1fe1007 100644 --- a/test/integration/concrete/invoice-module/create-invoice/createInvoice.tree +++ b/test/integration/concrete/payment-module/create-request/createRequest.tree @@ -1,4 +1,4 @@ -createInvoice.t.sol +createRequest.t.sol ├── when the caller IS NOT a contract │ └── it should revert with the {SpaceZeroCodeSize} error └── when the caller IS a contract @@ -15,14 +15,14 @@ createInvoice.t.sol │ └── it should revert with the {EndTimeInThePast} error └── when the end time IS NOT in the past ├── given the payment method is a regular transfer - │ ├── it should create the invoice - │ └── it should emit an {InvoiceCreated} event + │ ├── it should create the payment request + │ └── it should emit an {RequestCreated} event ├── given the payment method is a recurring transfer │ ├── when the payment interval is too short for the selected recurrence │ │ └── it should revert with the {PaymentIntervalTooShortForSelectedRecurrence} error │ └── when the payment interval is long enough for the selected recurrence - │ ├── it should create the invoice - │ └── it should emit an {InvoiceCreated} event + │ ├── it should create the payment request + │ └── it should emit an {RequestCreated} event ├── given the payment method is a tranched stream │ ├── when the recurrence IS set to one-off │ │ └── it should revert with the {TranchedStreamInvalidOneOffRecurence} error @@ -33,11 +33,11 @@ createInvoice.t.sol │ ├── when the payment asset IS the native token │ │ └── it should revert with the {OnlyERC20StreamsAllowed} error │ └── when the payment asset IS NOT the native token - │ ├── it should create the invoice - │ └── it should emit an {InvoiceCreated} event + │ ├── it should create the payment request + │ └── it should emit an {RequestCreated} event └── given the payment method is a linear stream ├── when the payment asset IS the native token │ └── it should revert with the {OnlyERC20StreamsAllowed} error └── when the payment asset IS NOT the native token - ├── it should create the invoice - └── it should emit an {InvoiceCreated} event + ├── it should create the payment request + └── it should emit an {RequestCreated} event diff --git a/test/integration/concrete/payment-module/pay-request/payRequest.t.sol b/test/integration/concrete/payment-module/pay-request/payRequest.t.sol new file mode 100644 index 0000000..834734f --- /dev/null +++ b/test/integration/concrete/payment-module/pay-request/payRequest.t.sol @@ -0,0 +1,344 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import { PayRequest_Integration_Shared_Test } from "../../../shared/payRequest.t.sol"; +import { Types } from "./../../../../../src/modules/payment-module/libraries/Types.sol"; +import { Events } from "../../../../utils/Events.sol"; +import { Errors } from "../../../../utils/Errors.sol"; + +import { LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; + +contract PayInvoice_Integration_Concret_Test is PayRequest_Integration_Shared_Test { + function setUp() public virtual override { + PayRequest_Integration_Shared_Test.setUp(); + } + + function test_RevertWhen_RequestNull() external { + // Expect the call to revert with the {NullRequest} error + vm.expectRevert(Errors.NullRequest.selector); + + // Run the test + paymentModule.payRequest({ requestId: 99 }); + } + + function test_RevertWhen_RequestAlreadyPaid() external whenRequestNotNull { + // Set the one-off USDT transfer payment request as current one + uint256 paymentRequestId = 1; + + // Make Bob the payer for the default paymentRequest + vm.startPrank({ msgSender: users.bob }); + + // Approve the {InvoiceModule} to transfer the ERC-20 token on Bob's behalf + usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); + + // Pay first the payment request + paymentModule.payRequest({ requestId: paymentRequestId }); + + // Expect the call to be reverted with the {RequestPaid} error + vm.expectRevert(Errors.RequestPaid.selector); + + // Run the test + paymentModule.payRequest({ requestId: paymentRequestId }); + } + + function test_RevertWhen_RequestCanceled() external whenRequestNotNull whenRequestNotAlreadyPaid { + // Set the one-off USDT transfer payment request as current one + uint256 paymentRequestId = 1; + + // Make Eve's space the caller in this test suite as his space is the owner of the payment request + vm.startPrank({ msgSender: address(space) }); + + // Cancel the payment request first + paymentModule.cancelRequest({ requestId: paymentRequestId }); + + // Make Bob the payer of this paymentRequest + vm.startPrank({ msgSender: users.bob }); + + // Expect the call to be reverted with the {RequestCanceled} error + vm.expectRevert(Errors.RequestCanceled.selector); + + // Run the test + paymentModule.payRequest({ requestId: paymentRequestId }); + } + + function test_RevertWhen_PaymentMethodTransfer_PaymentAmountLessThanInvoiceValue() + external + whenRequestNotNull + whenRequestNotAlreadyPaid + whenRequestNotCanceled + givenPaymentMethodTransfer + givenPaymentAmountInNativeToken + { + // Set the one-off ETH transfer payment request as current one + uint256 paymentRequestId = 2; + + // Make Bob the payer for the default paymentRequest + vm.startPrank({ msgSender: users.bob }); + + // Expect the call to be reverted with the {PaymentAmountLessThanInvoiceValue} error + vm.expectRevert( + abi.encodeWithSelector( + Errors.PaymentAmountLessThanInvoiceValue.selector, paymentRequests[paymentRequestId].config.amount + ) + ); + + // Run the test + paymentModule.payRequest{ value: paymentRequests[paymentRequestId].config.amount - 1 }({ + requestId: paymentRequestId + }); + } + + function test_RevertWhen_PaymentMethodTransfer_NativeTokenTransferFails() + external + whenRequestNotNull + whenRequestNotAlreadyPaid + whenRequestNotCanceled + givenPaymentMethodTransfer + givenPaymentAmountInNativeToken + whenPaymentAmountEqualToInvoiceValue + { + // Create a mock payment request with a one-off ETH transfer from the Eve's space + Types.PaymentRequest memory paymentRequest = + createPaymentRequestWithOneOffTransfer({ asset: address(0), recipient: address(mockBadReceiver) }); + executeCreatePaymentRequest({ paymentRequest: paymentRequest, user: users.eve }); + + uint256 paymentRequestId = _nextRequestId; + + // Make Eve's space the caller for the next call to approve & transfer the payment request NFT to a bad receiver + //vm.startPrank({ msgSender: address(space) }); + + // Approve the {InvoiceModule} to transfer the token + //paymentModule.approve({ to: address(paymentModule), tokenrequestId: paymentRequestId }); + + // Transfer the payment request to a bad receiver so we can test against `NativeTokenPaymentFailed` + //paymentModule.transferFrom({ from: address(space), to: address(mockBadReceiver), tokenrequestId: paymentRequestId }); + + // Make Bob the payer for this paymentRequest + vm.startPrank({ msgSender: users.bob }); + + // Expect the call to be reverted with the {NativeTokenPaymentFailed} error + vm.expectRevert(Errors.NativeTokenPaymentFailed.selector); + + // Run the test + paymentModule.payRequest{ value: paymentRequest.config.amount }({ requestId: paymentRequestId }); + } + + function test_PayRequest_PaymentMethodTransfer_NativeToken_OneOff() + external + whenRequestNotNull + whenRequestNotAlreadyPaid + whenRequestNotCanceled + givenPaymentMethodTransfer + givenPaymentAmountInNativeToken + whenPaymentAmountEqualToInvoiceValue + whenNativeTokenPaymentSucceeds + { + // Set the one-off ETH transfer payment request as current one + uint256 paymentRequestId = 2; + + // Make Bob the payer for the default paymentRequest + vm.startPrank({ msgSender: users.bob }); + + // Store the ETH balances of Bob and recipient before paying the payment request + uint256 balanceOfBobBefore = address(users.bob).balance; + uint256 balanceOfRecipientBefore = address(space).balance; + + // Expect the {RequestPaid} event to be emitted + vm.expectEmit(); + emit Events.RequestPaid({ + requestId: paymentRequestId, + payer: users.bob, + config: Types.Config({ + method: paymentRequests[paymentRequestId].config.method, + recurrence: paymentRequests[paymentRequestId].config.recurrence, + paymentsLeft: 0, + asset: paymentRequests[paymentRequestId].config.asset, + amount: paymentRequests[paymentRequestId].config.amount, + streamId: 0 + }) + }); + + // Run the test + paymentModule.payRequest{ value: paymentRequests[paymentRequestId].config.amount }({ + requestId: paymentRequestId + }); + + // Assert the actual and the expected state of the payment request + Types.PaymentRequest memory paymentRequest = paymentModule.getRequest({ requestId: paymentRequestId }); + Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: paymentRequestId }); + + assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Paid)); + assertEq(paymentRequest.config.paymentsLeft, 0); + + // Assert the balances of payer and recipient + assertEq(address(users.bob).balance, balanceOfBobBefore - paymentRequests[paymentRequestId].config.amount); + assertEq(address(space).balance, balanceOfRecipientBefore + paymentRequests[paymentRequestId].config.amount); + } + + function test_PayRequest_PaymentMethodTransfer_ERC20Token_Recurring() + external + whenRequestNotNull + whenRequestNotAlreadyPaid + whenRequestNotCanceled + givenPaymentMethodTransfer + givenPaymentAmountInERC20Tokens + whenPaymentAmountEqualToInvoiceValue + { + // Set the recurring USDT transfer payment request as current one + uint256 paymentRequestId = 3; + + // Make Bob the payer for the default paymentRequest + vm.startPrank({ msgSender: users.bob }); + + // Store the USDT balances of Bob and recipient before paying the payment request + uint256 balanceOfBobBefore = usdt.balanceOf(users.bob); + uint256 balanceOfRecipientBefore = usdt.balanceOf(address(space)); + + // Approve the {InvoiceModule} to transfer the ERC-20 tokens on Bob's behalf + usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); + + // Expect the {RequestPaid} event to be emitted + vm.expectEmit(); + emit Events.RequestPaid({ + requestId: paymentRequestId, + payer: users.bob, + config: Types.Config({ + method: paymentRequests[paymentRequestId].config.method, + recurrence: paymentRequests[paymentRequestId].config.recurrence, + paymentsLeft: 3, + asset: paymentRequests[paymentRequestId].config.asset, + amount: paymentRequests[paymentRequestId].config.amount, + streamId: 0 + }) + }); + + // Run the test + paymentModule.payRequest{ value: paymentRequests[paymentRequestId].config.amount }({ + requestId: paymentRequestId + }); + + // Assert the actual and the expected state of the payment request + Types.PaymentRequest memory paymentRequest = paymentModule.getRequest({ requestId: paymentRequestId }); + Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: paymentRequestId }); + + assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Accepted)); + assertEq(paymentRequest.config.paymentsLeft, 3); + + // Assert the balances of payer and recipient + assertEq(usdt.balanceOf(users.bob), balanceOfBobBefore - paymentRequests[paymentRequestId].config.amount); + assertEq( + usdt.balanceOf(address(space)), balanceOfRecipientBefore + paymentRequests[paymentRequestId].config.amount + ); + } + + function test_PayRequest_PaymentMethodLinearStream() + external + whenRequestNotNull + whenRequestNotAlreadyPaid + whenRequestNotCanceled + givenPaymentMethodLinearStream + givenPaymentAmountInERC20Tokens + whenPaymentAmountEqualToInvoiceValue + { + // Set the linear USDT stream-based paymentRequest as current one + uint256 paymentRequestId = 4; + + // Make Bob the payer for the default paymentRequest + vm.startPrank({ msgSender: users.bob }); + + // Approve the {InvoiceModule} to transfer the ERC-20 tokens on Bob's behalf + usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); + + // Expect the {RequestPaid} event to be emitted + vm.expectEmit(); + emit Events.RequestPaid({ + requestId: paymentRequestId, + payer: users.bob, + config: Types.Config({ + method: paymentRequests[paymentRequestId].config.method, + recurrence: paymentRequests[paymentRequestId].config.recurrence, + paymentsLeft: 0, + asset: paymentRequests[paymentRequestId].config.asset, + amount: paymentRequests[paymentRequestId].config.amount, + streamId: 1 + }) + }); + + // Run the test + paymentModule.payRequest{ value: paymentRequests[paymentRequestId].config.amount }({ + requestId: paymentRequestId + }); + + // Assert the actual and the expected state of the payment request + Types.PaymentRequest memory paymentRequest = paymentModule.getRequest({ requestId: paymentRequestId }); + Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: paymentRequestId }); + + assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Accepted)); + assertEq(paymentRequest.config.streamId, 1); + assertEq(paymentRequest.config.paymentsLeft, 0); + + // Assert the actual and the expected state of the Sablier v2 linear stream + LockupLinear.StreamLL memory stream = paymentModule.getLinearStream({ streamId: 1 }); + assertEq(stream.sender, address(paymentModule)); + assertEq(stream.recipient, address(space)); + assertEq(address(stream.asset), address(usdt)); + assertEq(stream.startTime, paymentRequest.startTime); + assertEq(stream.endTime, paymentRequest.endTime); + } + + function test_PayRequest_PaymentMethodTranchedStream() + external + whenRequestNotNull + whenRequestNotAlreadyPaid + whenRequestNotCanceled + givenPaymentMethodTranchedStream + givenPaymentAmountInERC20Tokens + whenPaymentAmountEqualToInvoiceValue + { + // Set the tranched USDT stream-based paymentRequest as current one + uint256 paymentRequestId = 5; + + // Make Bob the payer for the default paymentRequest + vm.startPrank({ msgSender: users.bob }); + + // Approve the {InvoiceModule} to transfer the ERC-20 tokens on Bob's behalf + usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); + + // Expect the {RequestPaid} event to be emitted + vm.expectEmit(); + emit Events.RequestPaid({ + requestId: paymentRequestId, + payer: users.bob, + config: Types.Config({ + method: paymentRequests[paymentRequestId].config.method, + recurrence: paymentRequests[paymentRequestId].config.recurrence, + paymentsLeft: 0, + asset: paymentRequests[paymentRequestId].config.asset, + amount: paymentRequests[paymentRequestId].config.amount, + streamId: 1 + }) + }); + + // Run the test + paymentModule.payRequest{ value: paymentRequests[paymentRequestId].config.amount }({ + requestId: paymentRequestId + }); + + // Assert the actual and the expected state of the payment request + Types.PaymentRequest memory paymentRequest = paymentModule.getRequest({ requestId: paymentRequestId }); + Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: paymentRequestId }); + + assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Accepted)); + assertEq(paymentRequest.config.streamId, 1); + assertEq(paymentRequest.config.paymentsLeft, 0); + + // Assert the actual and the expected state of the Sablier v2 tranched stream + LockupTranched.StreamLT memory stream = paymentModule.getTranchedStream({ streamId: 1 }); + assertEq(stream.sender, address(paymentModule)); + assertEq(stream.recipient, address(space)); + assertEq(address(stream.asset), address(usdt)); + assertEq(stream.startTime, paymentRequest.startTime); + assertEq(stream.endTime, paymentRequest.endTime); + assertEq(stream.tranches.length, 4); + } +} diff --git a/test/integration/concrete/invoice-module/pay-invoice/payInvoice.tree b/test/integration/concrete/payment-module/pay-request/payRequest.tree similarity index 55% rename from test/integration/concrete/invoice-module/pay-invoice/payInvoice.tree rename to test/integration/concrete/payment-module/pay-request/payRequest.tree index 3369cad..37b8e80 100644 --- a/test/integration/concrete/invoice-module/pay-invoice/payInvoice.tree +++ b/test/integration/concrete/payment-module/pay-request/payRequest.tree @@ -1,40 +1,40 @@ -payInvoice.t.sol -├── when the invoice IS null (there is no ERC-721 token minted) -│ └── it should revert with the {ERC721NonexistentToken} error -└── when the invoice IS NOT null - ├── when the invoice IS already paid - │ └── it should revert with the {InvoiceAlreadyPaid} error - └── when the invoice IS NOT already paid - ├── when the invoice IS canceled - │ └── it should revert with the {InvoiceCanceled} error - └── when the invoice IS NOT canceled +payRequest.t.sol +├── when the payment request IS null +│ └── it should revert with the {NullRequest} error +└── when the payment request IS NOT null + ├── when the payment request IS already paid + │ └── it should revert with the {RequestPaid} error + └── when the payment request IS NOT already paid + ├── when the payment request IS canceled + │ └── it should revert with the {RequestCanceled} error + └── when the payment request IS NOT canceled ├── given the payment method is transfer │ ├── given the payment amount is in native token (ETH) - │ │ ├── when the payment amount is less than the invoice value + │ │ ├── when the payment amount is less than the payment request value │ │ │ └── it should revert with the {PaymentAmountLessThanInvoiceValue} error - │ │ └── when the payment amount IS equal to the invoice value + │ │ └── when the payment amount IS equal to the payment request value │ │ ├── when the native token transfer fails │ │ │ └── it should revert with the {NativeTokenPaymentFailed} error │ │ └── when the native token transfer succeeds │ │ ├── given the payment method is a one-off transfer - │ │ │ ├── it should update the invoice status to Paid + │ │ │ ├── it should update the payment request status to Paid │ │ │ └── it should decrease the number of payments to zero │ │ ├── given the payment method is a recurring transfer - │ │ │ ├── it should update the invoice status to Ongoing + │ │ │ ├── it should update the payment request status to Ongoing │ │ │ └── it should decrease the number of payments - │ │ ├── it should transfer the payment amount to the invoice recipient - │ │ └── it should emit an {InvoicePaid} event + │ │ ├── it should transfer the payment amount to the payment request recipient + │ │ └── it should emit an {RequestPaid} event │ └── given the payment amount is in an ERC-20 token - │ ├── it should transfer the payment amount to the invoice recipient - │ └── it should emit an {InvoicePaid} event + │ ├── it should transfer the payment amount to the payment request recipient + │ └── it should emit an {RequestPaid} event ├── given the payment method is linear stream │ ├── it should create a Sablier v2 linear stream - │ ├── it should update the invoice status to Ongoing - │ ├── it should update the invoice stream ID - │ └── it should emit an {InvoicePaid} event + │ ├── it should update the payment request status to Ongoing + │ ├── it should update the payment request stream ID + │ └── it should emit an {RequestPaid} event └── given the payment method is tranched stream ├── it should create a Sablier v2 tranched stream - ├── it should update the invoice status to Ongoing - ├── it should update the invoice stream ID - └── it should emit an {InvoicePaid} event + ├── it should update the payment request status to Ongoing + ├── it should update the payment request stream ID + └── it should emit an {RequestPaid} event diff --git a/test/integration/concrete/invoice-module/withdraw-invoice-stream/withdrawStream.t.sol b/test/integration/concrete/payment-module/withdraw-invoice-stream/withdrawStream.t.sol similarity index 52% rename from test/integration/concrete/invoice-module/withdraw-invoice-stream/withdrawStream.t.sol rename to test/integration/concrete/payment-module/withdraw-invoice-stream/withdrawStream.t.sol index e5d0e44..68b4cf4 100644 --- a/test/integration/concrete/invoice-module/withdraw-invoice-stream/withdrawStream.t.sol +++ b/test/integration/concrete/payment-module/withdraw-invoice-stream/withdrawStream.t.sol @@ -2,27 +2,29 @@ pragma solidity ^0.8.26; import { WithdrawLinearStream_Integration_Shared_Test } from "../../../shared/withdrawLinearStream.t.sol"; -import { Types } from "./../../../../../src/modules/invoice-module/libraries/Types.sol"; +import { Types } from "./../../../../../src/modules/payment-module/libraries/Types.sol"; contract WithdrawLinearStream_Integration_Concret_Test is WithdrawLinearStream_Integration_Shared_Test { function setUp() public virtual override { WithdrawLinearStream_Integration_Shared_Test.setUp(); } - function test_WithdrawStream_LinearStream() external givenPaymentMethodLinearStream givenInvoiceStatusOngoing { - // Set current invoice as a linear stream-based one - uint256 invoiceId = 4; + function test_WithdrawStream_LinearStream() external givenPaymentMethodLinearStream givenRequestStatusPending { + // Set current paymentRequest as a linear stream-based one + uint256 paymentRequestId = 4; uint256 streamId = 1; - // The invoice must be paid in order to update its status to `Ongoing` - // Make Bob the payer of the invoice (also Bob will be the initial stream sender) + // The payment request must be paid in order to update its status to `Accepted` + // Make Bob the payer of the payment request (also Bob will be the initial stream sender) vm.startPrank({ msgSender: users.bob }); // Approve the {InvoiceModule} to transfer the USDT tokens on Bob's behalf - usdt.approve({ spender: address(invoiceModule), amount: invoices[invoiceId].payment.amount }); + usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); - // Pay the invoice first (status will be updated to `Ongoing`) - invoiceModule.payInvoice{ value: invoices[invoiceId].payment.amount }({ id: invoiceId }); + // Pay the payment request first (status will be updated to `Accepted`) + paymentModule.payRequest{ value: paymentRequests[paymentRequestId].config.amount }({ + requestId: paymentRequestId + }); // Advance the timestamp by 5 weeks to simulate the withdrawal vm.warp(block.timestamp + 5 weeks); @@ -31,35 +33,35 @@ contract WithdrawLinearStream_Integration_Concret_Test is WithdrawLinearStream_I uint256 balanceOfBefore = usdt.balanceOf(address(space)); // Get the maximum withdrawable amount from the stream - uint128 maxWithdrawableAmount = invoiceModule.withdrawableAmountOf({ - streamType: Types.Method.LinearStream, - streamId: streamId - }); + uint128 maxWithdrawableAmount = + paymentModule.withdrawableAmountOf({ streamType: Types.Method.LinearStream, streamId: streamId }); - // Make Eve's space the caller in this test suite as his space is the recipient of the invoice + // Make Eve's space the caller in this test suite as his space is the recipient of the payment request vm.startPrank({ msgSender: address(space) }); // Run the test - invoiceModule.withdrawInvoiceStream(invoiceId); + paymentModule.withdrawRequestStream(paymentRequestId); // Assert the current and expected USDT balance of Eve assertEq(balanceOfBefore + maxWithdrawableAmount, usdt.balanceOf(address(space))); } - function test_WithdrawStream_TranchedStream() external givenPaymentMethodTranchedStream givenInvoiceStatusOngoing { - // Set current invoice as a tranched stream-based one - uint256 invoiceId = 5; + function test_WithdrawStream_TranchedStream() external givenPaymentMethodTranchedStream givenRequestStatusPending { + // Set current paymentRequest as a tranched stream-based one + uint256 paymentRequestId = 5; uint256 streamId = 1; - // The invoice must be paid for its status to be updated to `Ongoing` - // Make Bob the payer of the invoice (also Bob will be the initial stream sender) + // The payment request must be paid for its status to be updated to `Accepted` + // Make Bob the payer of the payment request (also Bob will be the initial stream sender) vm.startPrank({ msgSender: users.bob }); // Approve the {InvoiceModule} to transfer the USDT tokens on Bob's behalf - usdt.approve({ spender: address(invoiceModule), amount: invoices[invoiceId].payment.amount }); + usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); - // Pay the invoice first (status will be updated to `Ongoing`) - invoiceModule.payInvoice{ value: invoices[invoiceId].payment.amount }({ id: invoiceId }); + // Pay the payment request first (status will be updated to `Accepted`) + paymentModule.payRequest{ value: paymentRequests[paymentRequestId].config.amount }({ + requestId: paymentRequestId + }); // Advance the timestamp by 5 weeks to simulate the withdrawal vm.warp(block.timestamp + 5 weeks); @@ -68,16 +70,14 @@ contract WithdrawLinearStream_Integration_Concret_Test is WithdrawLinearStream_I uint256 balanceOfBefore = usdt.balanceOf(address(space)); // Get the maximum withdrawable amount from the stream - uint128 maxWithdrawableAmount = invoiceModule.withdrawableAmountOf({ - streamType: Types.Method.TranchedStream, - streamId: streamId - }); + uint128 maxWithdrawableAmount = + paymentModule.withdrawableAmountOf({ streamType: Types.Method.TranchedStream, streamId: streamId }); - // Make Eve's space the caller in this test suite as her space is the owner of the invoice + // Make Eve's space the caller in this test suite as her space is the owner of the payment request vm.startPrank({ msgSender: address(space) }); // Run the test - invoiceModule.withdrawInvoiceStream(invoiceId); + paymentModule.withdrawRequestStream(paymentRequestId); // Assert the current and expected USDT balance of Eve's space assertEq(balanceOfBefore + maxWithdrawableAmount, usdt.balanceOf(address(space))); diff --git a/test/integration/concrete/payment-module/withdraw-invoice-stream/withdrawStream.tree b/test/integration/concrete/payment-module/withdraw-invoice-stream/withdrawStream.tree new file mode 100644 index 0000000..fde0e5d --- /dev/null +++ b/test/integration/concrete/payment-module/withdraw-invoice-stream/withdrawStream.tree @@ -0,0 +1,7 @@ +withdrawStream.t.sol +├── given the payment method is linear stream +│ └── given the payment request status is Ongoing +│ └── it should allow the payment request recipient to withdraw from the stream +└── given the payment method is tranched stream + └── given the payment request status is Ongoing + └── it should allow the payment request recipient to withdraw from the stream \ No newline at end of file diff --git a/test/integration/concrete/stream-manager/update-stream-broker-fee/updateStreamBrokerFee.t.sol b/test/integration/concrete/stream-manager/update-stream-broker-fee/updateStreamBrokerFee.t.sol index 7ef1d67..776087d 100644 --- a/test/integration/concrete/stream-manager/update-stream-broker-fee/updateStreamBrokerFee.t.sol +++ b/test/integration/concrete/stream-manager/update-stream-broker-fee/updateStreamBrokerFee.t.sol @@ -2,13 +2,13 @@ pragma solidity ^0.8.26; import { Integration_Test } from "../../../Integration.t.sol"; -import { Types } from "./../../../../../src/modules/invoice-module/libraries/Types.sol"; +import { Types } from "./../../../../../src/modules/payment-module/libraries/Types.sol"; import { Errors } from "../../../../utils/Errors.sol"; import { Events } from "../../../../utils/Events.sol"; import { ud, UD60x18 } from "@prb/math/src/UD60x18.sol"; contract UpdateStreamBrokerFee_Integration_Concret_Test is Integration_Test { - Types.Invoice invoice; + Types.PaymentRequest paymentRequest; function setUp() public virtual override { Integration_Test.setUp(); diff --git a/test/integration/fuzz/createInvoice.t.sol b/test/integration/fuzz/createInvoice.t.sol deleted file mode 100644 index 499d416..0000000 --- a/test/integration/fuzz/createInvoice.t.sol +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; - -import { CreateInvoice_Integration_Shared_Test } from "../shared/createInvoice.t.sol"; -import { Types } from "./../../../src/modules/invoice-module/libraries/Types.sol"; -import { Helpers } from "../../utils/Helpers.sol"; -import { Events } from "../../utils/Events.sol"; - -contract CreateInvoice_Integration_Fuzz_Test is CreateInvoice_Integration_Shared_Test { - Types.Invoice invoice; - - function setUp() public virtual override { - CreateInvoice_Integration_Shared_Test.setUp(); - - // Make Eve the caller in this test suite as she's the owner of the {Space} contract - vm.startPrank({ msgSender: users.eve }); - } - - function testFuzz_CreateInvoice( - uint8 recurrence, - uint8 paymentMethod, - uint40 startTime, - uint40 endTime, - uint128 amount - ) - external - whenCallerContract - whenCompliantSpace - whenNonZeroPaymentAmount - whenStartTimeLowerThanEndTime - whenEndTimeInTheFuture - whenPaymentAssetNotNativeToken - { - // Discard bad fuzz inputs - // Assume recurrence is within Types.Recurrence enum values (OneOff, Weekly, Monthly, Yearly) (0, 1, 2, 3) - vm.assume(recurrence < 4); - // Assume recurrence is within Types.Method enum values (Transfer, LinearStream, TranchedStream) (0, 1, 2) - vm.assume(paymentMethod < 3); - vm.assume(startTime >= uint40(block.timestamp) && startTime < endTime); - vm.assume(amount > 0); - - // Calculate the number of payments if this is a transfer-based invoice - (bool valid, uint40 numberOfPayments) = Helpers.checkFuzzedPaymentMethod( - paymentMethod, - recurrence, - startTime, - endTime - ); - if (!valid) return; - - // Create a new invoice with a transfer-based payment - invoice = Types.Invoice({ - status: Types.Status.Pending, - startTime: startTime, - endTime: endTime, - payment: Types.Payment({ - recurrence: Types.Recurrence(recurrence), - method: Types.Method(paymentMethod), - paymentsLeft: numberOfPayments, - amount: amount, - asset: address(usdt), - streamId: 0 - }) - }); - - // Create the calldata for the {InvoiceModule} execution - bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", - invoice - ); - - // Expect the module call to emit an {InvoiceCreated} event - vm.expectEmit(); - emit Events.InvoiceCreated({ - id: 1, - recipient: address(space), - status: Types.Status.Pending, - startTime: invoice.startTime, - endTime: invoice.endTime, - payment: invoice.payment - }); - - // Expect the {Space} contract to emit a {ModuleExecutionSucceded} event - vm.expectEmit(); - emit Events.ModuleExecutionSucceded({ module: address(invoiceModule), value: 0, data: data }); - - // Run the test - space.execute({ module: address(invoiceModule), value: 0, data: data }); - - // Assert the actual and expected invoice state - Types.Invoice memory actualInvoice = invoiceModule.getInvoice({ id: 1 }); - address actualRecipient = invoiceModule.ownerOf(1); - - assertEq(actualRecipient, address(space)); - assertEq(uint8(actualInvoice.status), uint8(Types.Status.Pending)); - assertEq(actualInvoice.startTime, invoice.startTime); - assertEq(actualInvoice.endTime, invoice.endTime); - assertEq(uint8(actualInvoice.payment.method), uint8(invoice.payment.method)); - assertEq(uint8(actualInvoice.payment.recurrence), uint8(invoice.payment.recurrence)); - assertEq(actualInvoice.payment.asset, invoice.payment.asset); - assertEq(actualInvoice.payment.amount, invoice.payment.amount); - assertEq(actualInvoice.payment.streamId, 0); - assertEq(actualInvoice.payment.paymentsLeft, invoice.payment.paymentsLeft); - } -} diff --git a/test/integration/fuzz/createRequest.t.sol b/test/integration/fuzz/createRequest.t.sol new file mode 100644 index 0000000..8872b7e --- /dev/null +++ b/test/integration/fuzz/createRequest.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import { CreateRequest_Integration_Shared_Test } from "../shared/createRequest.t.sol"; +import { Types } from "./../../../src/modules/payment-module/libraries/Types.sol"; +import { Helpers } from "../../utils/Helpers.sol"; +import { Events } from "../../utils/Events.sol"; + +contract CreateRequest_Integration_Fuzz_Test is CreateRequest_Integration_Shared_Test { + Types.PaymentRequest paymentRequest; + + function setUp() public virtual override { + CreateRequest_Integration_Shared_Test.setUp(); + + // Make Eve the caller in this test suite as she's the owner of the {Space} contract + vm.startPrank({ msgSender: users.eve }); + } + + function testFuzz_CreateRequest( + uint8 recurrence, + uint8 paymentMethod, + address recipient, + uint40 startTime, + uint40 endTime, + uint128 amount + ) + external + whenCallerContract + whenCompliantSpace + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + whenPaymentAssetNotNativeToken + { + // Discard bad fuzz inputs + // Assume recurrence is within Types.Recurrence enum values (OneOff, Weekly, Monthly, Yearly) (0, 1, 2, 3) + vm.assume(recurrence < 4); + // Assume recurrence is within Types.Method enum values (Transfer, LinearStream, TranchedStream) (0, 1, 2) + vm.assume(paymentMethod < 3); + vm.assume(recipient != address(0) && recipient != address(this)); + vm.assume(startTime >= uint40(block.timestamp) && startTime < endTime); + vm.assume(amount > 0); + + // Calculate the number of payments if this is a transfer-based payment request + (bool valid, uint40 numberOfPayments) = + Helpers.checkFuzzedPaymentMethod(paymentMethod, recurrence, startTime, endTime); + if (!valid) return; + + // Create a new payment request with a transfer-based payment + paymentRequest = Types.PaymentRequest({ + wasCanceled: false, + wasAccepted: false, + startTime: startTime, + endTime: endTime, + recipient: recipient, + config: Types.Config({ + recurrence: Types.Recurrence(recurrence), + method: Types.Method(paymentMethod), + paymentsLeft: numberOfPayments, + amount: amount, + asset: address(usdt), + streamId: 0 + }) + }); + + // Create the calldata for the {PaymentModule} execution + bytes memory data = abi.encodeWithSignature( + "createRequest((bool,bool,uint40,uint40,address,(uint8,uint8,uint40,address,uint128,uint256)))", + paymentRequest + ); + + // Expect the module call to emit an {RequestCreated} event + vm.expectEmit(); + emit Events.RequestCreated({ + requestId: 1, + recipient: paymentRequest.recipient, + startTime: paymentRequest.startTime, + endTime: paymentRequest.endTime, + config: paymentRequest.config + }); + + // Expect the {Space} contract to emit a {ModuleExecutionSucceded} event + vm.expectEmit(); + emit Events.ModuleExecutionSucceded({ module: address(paymentModule), value: 0, data: data }); + + // Run the test + space.execute({ module: address(paymentModule), value: 0, data: data }); + + // Assert the actual and expected paymentRequest state + Types.PaymentRequest memory actualRequest = paymentModule.getRequest({ requestId: 1 }); + Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: 1 }); + + assertEq(actualRequest.recipient, paymentRequest.recipient); + assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Pending)); + assertEq(actualRequest.startTime, paymentRequest.startTime); + assertEq(actualRequest.endTime, paymentRequest.endTime); + assertEq(uint8(actualRequest.config.method), uint8(paymentRequest.config.method)); + assertEq(uint8(actualRequest.config.recurrence), uint8(paymentRequest.config.recurrence)); + assertEq(actualRequest.config.asset, paymentRequest.config.asset); + assertEq(actualRequest.config.amount, paymentRequest.config.amount); + assertEq(actualRequest.config.streamId, 0); + assertEq(actualRequest.config.paymentsLeft, paymentRequest.config.paymentsLeft); + } +} diff --git a/test/integration/fuzz/payInvoice.t.sol b/test/integration/fuzz/payInvoice.t.sol deleted file mode 100644 index 44564c2..0000000 --- a/test/integration/fuzz/payInvoice.t.sol +++ /dev/null @@ -1,130 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; - -import { PayInvoice_Integration_Shared_Test } from "../shared/payInvoice.t.sol"; -import { Types } from "./../../../src/modules/invoice-module/libraries/Types.sol"; -import { Events } from "../../utils/Events.sol"; -import { Helpers } from "../../utils/Helpers.sol"; - -contract PayInvoice_Integration_Fuzz_Test is PayInvoice_Integration_Shared_Test { - Types.Invoice invoice; - - function setUp() public virtual override { - PayInvoice_Integration_Shared_Test.setUp(); - } - - function testFuzz_PayInvoice( - uint8 recurrence, - uint8 paymentMethod, - uint40 startTime, - uint40 endTime, - uint128 amount - ) - external - whenInvoiceNotNull - whenInvoiceNotAlreadyPaid - whenInvoiceNotCanceled - givenPaymentMethodTransfer - givenPaymentAmountInNativeToken - whenPaymentAmountEqualToInvoiceValue - whenNativeTokenPaymentSucceeds - { - // Discard bad fuzz inputs - // Assume recurrence is within Types.Recurrence enum values (OneOff, Weekly, Monthly, Yearly) (0, 1, 2, 3) - vm.assume(recurrence < 4); - // Assume recurrence is within Types.Method enum values (Transfer, LinearStream, TranchedStream) (0, 1, 2) - vm.assume(paymentMethod < 3); - vm.assume(startTime >= uint40(block.timestamp) && startTime < endTime); - vm.assume(amount > 0); - - // Calculate the number of payments if this is a transfer-based invoice - (bool valid, uint40 numberOfPayments) = Helpers.checkFuzzedPaymentMethod( - paymentMethod, - recurrence, - startTime, - endTime - ); - if (!valid) return; - - // Create a new invoice with the fuzzed payment method - invoice = Types.Invoice({ - status: Types.Status.Pending, - startTime: startTime, - endTime: endTime, - payment: Types.Payment({ - recurrence: Types.Recurrence(recurrence), - method: Types.Method(paymentMethod), - paymentsLeft: numberOfPayments, - amount: amount, - asset: address(usdt), - streamId: 0 - }) - }); - - // Create the calldata for the {InvoiceModule} execution - bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", - invoice - ); - - uint256 invoiceId = _nextInvoiceId; - - // Make Eve the caller to create the fuzzed invoice - vm.startPrank({ msgSender: users.eve }); - - // Create the fuzzed invoice - space.execute({ module: address(invoiceModule), value: 0, data: data }); - - // Mint enough USDT to the payer's address to be able to pay the invoice - deal({ token: address(usdt), to: users.bob, give: invoice.payment.amount }); - - // Make payer the caller to pay for the fuzzed invoice - vm.startPrank({ msgSender: users.bob }); - - // Approve the {InvoiceModule} to transfer the USDT tokens on payer's behalf - usdt.approve({ spender: address(invoiceModule), amount: invoice.payment.amount }); - - // Store the USDT balances of the payer and recipient before paying the invoice - uint256 balanceOfPayerBefore = usdt.balanceOf(users.bob); - uint256 balanceOfRecipientBefore = usdt.balanceOf(address(space)); - - uint256 streamId = paymentMethod == 0 ? 0 : 1; - numberOfPayments = numberOfPayments > 0 ? numberOfPayments - 1 : 0; - - Types.Status expectedInvoiceStatus = numberOfPayments == 0 && invoice.payment.method == Types.Method.Transfer - ? Types.Status.Paid - : Types.Status.Ongoing; - - // Expect the {InvoicePaid} event to be emitted - vm.expectEmit(); - emit Events.InvoicePaid({ - id: invoiceId, - payer: users.bob, - status: expectedInvoiceStatus, - payment: Types.Payment({ - method: invoice.payment.method, - recurrence: invoice.payment.recurrence, - paymentsLeft: numberOfPayments, - asset: invoice.payment.asset, - amount: invoice.payment.amount, - streamId: streamId - }) - }); - - // Run the test - invoiceModule.payInvoice({ id: invoiceId }); - - // Assert the actual and the expected state of the invoice - Types.Invoice memory actualInvoice = invoiceModule.getInvoice({ id: invoiceId }); - assertEq(uint8(actualInvoice.status), uint8(expectedInvoiceStatus)); - assertEq(actualInvoice.payment.paymentsLeft, numberOfPayments); - - // Assert the actual and expected balances of the payer and recipient - assertEq(usdt.balanceOf(users.bob), balanceOfPayerBefore - invoice.payment.amount); - if (invoice.payment.method == Types.Method.Transfer) { - assertEq(usdt.balanceOf(address(space)), balanceOfRecipientBefore + invoice.payment.amount); - } else { - assertEq(usdt.balanceOf(address(space)), balanceOfRecipientBefore); - } - } -} diff --git a/test/integration/fuzz/payRequest.t.sol b/test/integration/fuzz/payRequest.t.sol new file mode 100644 index 0000000..7caeb26 --- /dev/null +++ b/test/integration/fuzz/payRequest.t.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import { PayRequest_Integration_Shared_Test } from "../shared/payRequest.t.sol"; +import { Types } from "./../../../src/modules/payment-module/libraries/Types.sol"; +import { Events } from "../../utils/Events.sol"; +import { Helpers } from "../../utils/Helpers.sol"; + +contract PayRequest_Integration_Fuzz_Test is PayRequest_Integration_Shared_Test { + Types.PaymentRequest paymentRequest; + + function setUp() public virtual override { + PayRequest_Integration_Shared_Test.setUp(); + } + + function testFuzz_PayRequest( + uint8 recurrence, + uint8 paymentMethod, + uint40 startTime, + uint40 endTime, + uint128 amount + ) + external + whenRequestNotNull + whenRequestNotAlreadyPaid + whenRequestNotCanceled + givenPaymentMethodTransfer + givenPaymentAmountInNativeToken + whenPaymentAmountEqualToInvoiceValue + whenNativeTokenPaymentSucceeds + { + // Discard bad fuzz inputs + // Assume recurrence is within Types.Recurrence enum values (OneOff, Weekly, Monthly, Yearly) (0, 1, 2, 3) + vm.assume(recurrence < 4); + // Assume recurrence is within Types.Method enum values (Transfer, LinearStream, TranchedStream) (0, 1, 2) + vm.assume(paymentMethod < 3); + vm.assume(startTime >= uint40(block.timestamp) && startTime < endTime); + vm.assume(amount > 0); + + // Calculate the number of payments if this is a transfer-based payment request + (bool valid, uint40 expectedNumberOfPayments) = + Helpers.checkFuzzedPaymentMethod(paymentMethod, recurrence, startTime, endTime); + if (!valid) return; + + // Create a new payment request with the fuzzed payment method + paymentRequest = Types.PaymentRequest({ + wasCanceled: false, + wasAccepted: false, + startTime: startTime, + endTime: endTime, + recipient: address(space), + config: Types.Config({ + recurrence: Types.Recurrence(recurrence), + method: Types.Method(paymentMethod), + paymentsLeft: expectedNumberOfPayments, + amount: amount, + asset: address(usdt), + streamId: 0 + }) + }); + + // Create the calldata for the {InvoiceModule} execution + bytes memory data = abi.encodeWithSignature( + "createRequest((bool,bool,uint40,uint40,address,(uint8,uint8,uint40,address,uint128,uint256)))", + paymentRequest + ); + + uint256 paymentRequestId = _nextRequestId; + + // Make Eve the caller to create the fuzzed paymentRequest + vm.startPrank({ msgSender: users.eve }); + + // Create the fuzzed paymentRequest + space.execute({ module: address(paymentModule), value: 0, data: data }); + + // Mint enough USDT to the payer's address to be able to pay the payment request + deal({ token: address(usdt), to: users.bob, give: paymentRequest.config.amount }); + + // Make payer the caller to pay for the fuzzed paymentRequest + vm.startPrank({ msgSender: users.bob }); + + // Approve the {InvoiceModule} to transfer the USDT tokens on payer's behalf + usdt.approve({ spender: address(paymentModule), amount: paymentRequest.config.amount }); + + // Store the USDT balances of the payer and recipient before paying the payment request + uint256 balanceOfPayerBefore = usdt.balanceOf(users.bob); + uint256 balanceOfRecipientBefore = usdt.balanceOf(address(space)); + + uint256 streamId = paymentMethod == 0 ? 0 : 1; + uint40 expectedNumberOfPaymentsLeft = expectedNumberOfPayments > 0 ? expectedNumberOfPayments - 1 : 0; + + Types.Status expectedRequestStatus = expectedNumberOfPaymentsLeft == 0 + && paymentRequest.config.method == Types.Method.Transfer ? Types.Status.Paid : Types.Status.Accepted; + + // Expect the {RequestPaid} event to be emitted + vm.expectEmit(); + emit Events.RequestPaid({ + requestId: paymentRequestId, + payer: users.bob, + config: Types.Config({ + method: paymentRequest.config.method, + recurrence: paymentRequest.config.recurrence, + paymentsLeft: expectedNumberOfPaymentsLeft, + asset: paymentRequest.config.asset, + amount: paymentRequest.config.amount, + streamId: streamId + }) + }); + + // Run the test + paymentModule.payRequest({ requestId: paymentRequestId }); + + // Assert the actual and the expected state of the payment request + Types.PaymentRequest memory actualRequest = paymentModule.getRequest({ requestId: paymentRequestId }); + Types.Status actualRequestStatus = paymentModule.statusOf({ requestId: paymentRequestId }); + + assertEq(uint8(actualRequestStatus), uint8(expectedRequestStatus)); + assertEq(actualRequest.config.paymentsLeft, expectedNumberOfPaymentsLeft); + + // Assert the actual and expected balances of the payer and recipient + assertEq(usdt.balanceOf(users.bob), balanceOfPayerBefore - paymentRequest.config.amount); + if (paymentRequest.config.method == Types.Method.Transfer) { + assertEq(usdt.balanceOf(address(space)), balanceOfRecipientBefore + paymentRequest.config.amount); + } else { + assertEq(usdt.balanceOf(address(space)), balanceOfRecipientBefore); + } + } +} diff --git a/test/integration/shared/cancelInvoice.t.sol b/test/integration/shared/cancelRequest.t.sol similarity index 61% rename from test/integration/shared/cancelInvoice.t.sol rename to test/integration/shared/cancelRequest.t.sol index 600bda2..bf9aa0d 100644 --- a/test/integration/shared/cancelInvoice.t.sol +++ b/test/integration/shared/cancelRequest.t.sol @@ -2,12 +2,11 @@ pragma solidity ^0.8.26; import { Integration_Test } from "../Integration.t.sol"; -import { PayInvoice_Integration_Shared_Test } from "./payInvoice.t.sol"; +import { PayRequest_Integration_Shared_Test } from "./payRequest.t.sol"; -abstract contract CancelInvoice_Integration_Shared_Test is Integration_Test, PayInvoice_Integration_Shared_Test { - function setUp() public virtual override(Integration_Test, PayInvoice_Integration_Shared_Test) { - PayInvoice_Integration_Shared_Test.setUp(); - createMockInvoices(); +abstract contract CancelRequest_Integration_Shared_Test is Integration_Test, PayRequest_Integration_Shared_Test { + function setUp() public virtual override(Integration_Test, PayRequest_Integration_Shared_Test) { + PayRequest_Integration_Shared_Test.setUp(); } modifier whenInvoiceStatusNotPaid() { @@ -26,7 +25,7 @@ abstract contract CancelInvoice_Integration_Shared_Test is Integration_Test, Pay _; } - modifier givenInvoiceStatusOngoing() { + modifier givenRequestStatusPending() { _; } diff --git a/test/integration/shared/createInvoice.t.sol b/test/integration/shared/createInvoice.t.sol deleted file mode 100644 index c32bf03..0000000 --- a/test/integration/shared/createInvoice.t.sol +++ /dev/null @@ -1,201 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; - -import { Integration_Test } from "../Integration.t.sol"; -import { Types } from "./../../../src/modules/invoice-module/libraries/Types.sol"; -import { Space } from "./../../../src/Space.sol"; -import { MockBadSpace } from "../../mocks/MockBadSpace.sol"; - -abstract contract CreateInvoice_Integration_Shared_Test is Integration_Test { - mapping(uint256 invoiceId => Types.Invoice) invoices; - uint256 public _nextInvoiceId; - - function setUp() public virtual override { - Integration_Test.setUp(); - } - - function createMockInvoices() internal { - // Create a mock invoice with a one-off USDT transfer - Types.Invoice memory invoice = createInvoiceWithOneOffTransfer({ asset: address(usdt) }); - invoices[1] = invoice; - executeCreateInvoice({ invoice: invoice, user: users.eve }); - - // Create a mock invoice with a one-off ETH transfer - invoice = createInvoiceWithOneOffTransfer({ asset: address(0) }); - invoices[2] = invoice; - executeCreateInvoice({ invoice: invoice, user: users.eve }); - - // Create a mock invoice with a recurring USDT transfer - invoice = createInvoiceWithRecurringTransfer({ recurrence: Types.Recurrence.Weekly }); - invoices[3] = invoice; - executeCreateInvoice({ invoice: invoice, user: users.eve }); - - // Create a mock invoice with a linear stream payment - invoice = createInvoiceWithLinearStream(); - invoices[4] = invoice; - executeCreateInvoice({ invoice: invoice, user: users.eve }); - - // Create a mock invoice with a tranched stream payment - invoice = createInvoiceWithTranchedStream({ recurrence: Types.Recurrence.Weekly }); - invoices[5] = invoice; - executeCreateInvoice({ invoice: invoice, user: users.eve }); - - _nextInvoiceId = 6; - } - - modifier whenCallerContract() { - _; - } - - modifier whenCompliantSpace() { - _; - } - - modifier whenNonZeroPaymentAmount() { - _; - } - - modifier whenStartTimeLowerThanEndTime() { - _; - } - - modifier whenEndTimeInTheFuture() { - _; - } - - modifier whenPaymentIntervalLongEnough() { - _; - } - - modifier whenTranchedStreamWithGoodRecurring() { - _; - } - - modifier whenPaymentAssetNotNativeToken() { - _; - } - - modifier givenPaymentMethodOneOffTransfer() { - _; - } - - modifier givenPaymentMethodRecurringTransfer() { - _; - } - - modifier givenPaymentMethodTranchedStream() { - _; - } - - modifier givenPaymentMethodLinearStream() { - _; - } - - /// @dev Creates an invoice with a one-off transfer payment - function createInvoiceWithOneOffTransfer(address asset) internal view returns (Types.Invoice memory invoice) { - invoice = _createInvoice(uint40(block.timestamp), uint40(block.timestamp) + 4 weeks); - - invoice.payment = Types.Payment({ - method: Types.Method.Transfer, - recurrence: Types.Recurrence.OneOff, - paymentsLeft: 1, - asset: asset, - amount: 100e18, - streamId: 0 - }); - } - - /// @dev Creates an invoice with a recurring transfer payment - function createInvoiceWithRecurringTransfer( - Types.Recurrence recurrence - ) internal view returns (Types.Invoice memory invoice) { - invoice = _createInvoice(uint40(block.timestamp), uint40(block.timestamp) + 4 weeks); - - invoice.payment = Types.Payment({ - method: Types.Method.Transfer, - recurrence: recurrence, - paymentsLeft: 0, - asset: address(usdt), - amount: 100e18, - streamId: 0 - }); - } - - /// @dev Creates an invoice with a linear stream-based payment - function createInvoiceWithLinearStream() internal view returns (Types.Invoice memory invoice) { - invoice = _createInvoice(uint40(block.timestamp), uint40(block.timestamp) + 4 weeks); - - invoice.payment = Types.Payment({ - method: Types.Method.LinearStream, - recurrence: Types.Recurrence.Weekly, // doesn't matter - paymentsLeft: 0, - asset: address(usdt), - amount: 100e18, - streamId: 0 - }); - } - - /// @dev Creates an invoice with a tranched stream-based payment - function createInvoiceWithTranchedStream( - Types.Recurrence recurrence - ) internal view returns (Types.Invoice memory invoice) { - invoice = _createInvoice(uint40(block.timestamp), uint40(block.timestamp) + 4 weeks); - - invoice.payment = Types.Payment({ - method: Types.Method.TranchedStream, - recurrence: recurrence, - paymentsLeft: 0, - asset: address(usdt), - amount: 100e18, - streamId: 0 - }); - } - - /// @dev Creates an invoice with fuzzed parameters - function createFuzzedInvoice( - Types.Method method, - Types.Recurrence recurrence, - uint40 startTime, - uint40 endTime, - uint128 amount - ) internal view returns (Types.Invoice memory invoice) { - invoice = _createInvoice(startTime, endTime); - - invoice.payment = Types.Payment({ - method: method, - recurrence: recurrence, - paymentsLeft: 0, - asset: address(usdt), - amount: amount, - streamId: 0 - }); - } - - function executeCreateInvoice(Types.Invoice memory invoice, address user) public { - // Make the `user` account the caller who must be the owner of the {Space} contract - vm.startPrank({ msgSender: user }); - - // Create the invoice - bytes memory data = abi.encodeWithSignature( - "createInvoice((uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", - invoice - ); - - // Select the according {Space} of the user - - if (user == users.eve) { - Space(space).execute({ module: address(invoiceModule), value: 0, data: data }); - } else { - MockBadSpace(badSpace).execute({ module: address(invoiceModule), value: 0, data: data }); - } - - // Stop the active prank - vm.stopPrank(); - } - - function _createInvoice(uint40 startTime, uint40 endTime) internal pure returns (Types.Invoice memory invoice) { - invoice.status = Types.Status.Pending; - invoice.startTime = startTime; - invoice.endTime = endTime; - } -} diff --git a/test/integration/shared/createRequest.t.sol b/test/integration/shared/createRequest.t.sol new file mode 100644 index 0000000..2cfa6cd --- /dev/null +++ b/test/integration/shared/createRequest.t.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import { Integration_Test } from "../Integration.t.sol"; +import { Types } from "./../../../src/modules/payment-module/libraries/Types.sol"; +import { Space } from "./../../../src/Space.sol"; +import { MockBadSpace } from "../../mocks/MockBadSpace.sol"; + +abstract contract CreateRequest_Integration_Shared_Test is Integration_Test { + mapping(uint256 paymentRequestId => Types.PaymentRequest) paymentRequests; + uint256 public _nextRequestId; + + function setUp() public virtual override { + Integration_Test.setUp(); + } + + function createMockPaymentRequests() internal { + // Create a mock payment request with a one-off USDT transfer + Types.PaymentRequest memory paymentRequest = + createPaymentRequestWithOneOffTransfer({ asset: address(usdt), recipient: address(space) }); + paymentRequests[1] = paymentRequest; + executeCreatePaymentRequest({ paymentRequest: paymentRequest, user: users.eve }); + + // Create a mock payment request with a one-off ETH transfer + paymentRequest = createPaymentRequestWithOneOffTransfer({ asset: address(0), recipient: address(space) }); + paymentRequests[2] = paymentRequest; + executeCreatePaymentRequest({ paymentRequest: paymentRequest, user: users.eve }); + + // Create a mock payment request with a recurring USDT transfer + paymentRequest = + createInvoiceWithRecurringTransfer({ recurrence: Types.Recurrence.Weekly, recipient: address(space) }); + paymentRequests[3] = paymentRequest; + executeCreatePaymentRequest({ paymentRequest: paymentRequest, user: users.eve }); + + // Create a mock payment request with a linear stream payment + paymentRequest = createPaymentRequestWithLinearStream({ recipient: address(space) }); + paymentRequests[4] = paymentRequest; + executeCreatePaymentRequest({ paymentRequest: paymentRequest, user: users.eve }); + + // Create a mock payment request with a tranched stream payment + paymentRequest = + createPaymentRequestWithTranchedStream({ recurrence: Types.Recurrence.Weekly, recipient: address(space) }); + paymentRequests[5] = paymentRequest; + executeCreatePaymentRequest({ paymentRequest: paymentRequest, user: users.eve }); + + _nextRequestId = 6; + } + + modifier whenCallerContract() { + _; + } + + modifier whenCompliantSpace() { + _; + } + + modifier whenNonZeroPaymentAmount() { + _; + } + + modifier whenStartTimeLowerThanEndTime() { + _; + } + + modifier whenEndTimeInTheFuture() { + _; + } + + modifier whenPaymentIntervalLongEnough() { + _; + } + + modifier whenTranchedStreamWithGoodRecurring() { + _; + } + + modifier whenPaymentAssetNotNativeToken() { + _; + } + + modifier givenPaymentMethodOneOffTransfer() { + _; + } + + modifier givenPaymentMethodRecurringTransfer() { + _; + } + + modifier givenPaymentMethodTranchedStream() { + _; + } + + modifier givenPaymentMethodLinearStream() { + _; + } + + /// @dev Creates a payment request with a one-off transfer payment + function createPaymentRequestWithOneOffTransfer( + address asset, + address recipient + ) + internal + view + returns (Types.PaymentRequest memory paymentRequest) + { + paymentRequest = + _createBasePaymentRequest(recipient, uint40(block.timestamp), uint40(block.timestamp) + 4 weeks); + + paymentRequest.config = Types.Config({ + method: Types.Method.Transfer, + recurrence: Types.Recurrence.OneOff, + paymentsLeft: 1, + asset: asset, + amount: 100e18, + streamId: 0 + }); + } + + /// @dev Creates a payment request with a recurring transfer payment + function createInvoiceWithRecurringTransfer( + Types.Recurrence recurrence, + address recipient + ) + internal + view + returns (Types.PaymentRequest memory paymentRequest) + { + paymentRequest = + _createBasePaymentRequest(recipient, uint40(block.timestamp), uint40(block.timestamp) + 4 weeks); + + paymentRequest.config = Types.Config({ + method: Types.Method.Transfer, + recurrence: recurrence, + paymentsLeft: 0, + asset: address(usdt), + amount: 100e18, + streamId: 0 + }); + } + + /// @dev Creates a payment request with a linear stream-based payment + function createPaymentRequestWithLinearStream(address recipient) + internal + view + returns (Types.PaymentRequest memory paymentRequest) + { + paymentRequest = + _createBasePaymentRequest(recipient, uint40(block.timestamp), uint40(block.timestamp) + 4 weeks); + + paymentRequest.config = Types.Config({ + method: Types.Method.LinearStream, + recurrence: Types.Recurrence.Weekly, // doesn't matter + paymentsLeft: 0, + asset: address(usdt), + amount: 100e18, + streamId: 0 + }); + } + + /// @dev Creates a payment request with a tranched stream-based payment + function createPaymentRequestWithTranchedStream( + Types.Recurrence recurrence, + address recipient + ) + internal + view + returns (Types.PaymentRequest memory paymentRequest) + { + paymentRequest = + _createBasePaymentRequest(recipient, uint40(block.timestamp), uint40(block.timestamp) + 4 weeks); + + paymentRequest.config = Types.Config({ + method: Types.Method.TranchedStream, + recurrence: recurrence, + paymentsLeft: 0, + asset: address(usdt), + amount: 100e18, + streamId: 0 + }); + } + + function executeCreatePaymentRequest(Types.PaymentRequest memory paymentRequest, address user) public { + // Make the `user` account the caller who must be the owner of the {Space} contract + vm.startPrank({ msgSender: user }); + + // Create the payment request + bytes memory data = abi.encodeWithSignature( + "createRequest((bool,bool,uint40,uint40,address,(uint8,uint8,uint40,address,uint128,uint256)))", + paymentRequest + ); + + // Select the according {Space} of the user + if (user == users.eve) { + Space(space).execute({ module: address(paymentModule), value: 0, data: data }); + } else { + MockBadSpace(badSpace).execute({ module: address(paymentModule), value: 0, data: data }); + } + + // Stop the active prank + vm.stopPrank(); + } + + function _createBasePaymentRequest( + address recipient, + uint40 startTime, + uint40 endTime + ) + internal + pure + returns (Types.PaymentRequest memory paymentRequest) + { + paymentRequest.recipient = recipient; + paymentRequest.startTime = startTime; + paymentRequest.endTime = endTime; + } +} diff --git a/test/integration/shared/payInvoice.t.sol b/test/integration/shared/payRequest.t.sol similarity index 56% rename from test/integration/shared/payInvoice.t.sol rename to test/integration/shared/payRequest.t.sol index f13997d..649ecbd 100644 --- a/test/integration/shared/payInvoice.t.sol +++ b/test/integration/shared/payRequest.t.sol @@ -2,23 +2,23 @@ pragma solidity ^0.8.26; import { Integration_Test } from "../Integration.t.sol"; -import { CreateInvoice_Integration_Shared_Test } from "./createInvoice.t.sol"; +import { CreateRequest_Integration_Shared_Test } from "./createRequest.t.sol"; -abstract contract PayInvoice_Integration_Shared_Test is Integration_Test, CreateInvoice_Integration_Shared_Test { - function setUp() public virtual override(Integration_Test, CreateInvoice_Integration_Shared_Test) { - CreateInvoice_Integration_Shared_Test.setUp(); - createMockInvoices(); +abstract contract PayRequest_Integration_Shared_Test is Integration_Test, CreateRequest_Integration_Shared_Test { + function setUp() public virtual override(Integration_Test, CreateRequest_Integration_Shared_Test) { + CreateRequest_Integration_Shared_Test.setUp(); + createMockPaymentRequests(); } - modifier whenInvoiceNotNull() { + modifier whenRequestNotNull() { _; } - modifier whenInvoiceNotAlreadyPaid() { + modifier whenRequestNotAlreadyPaid() { _; } - modifier whenInvoiceNotCanceled() { + modifier whenRequestNotCanceled() { _; } diff --git a/test/integration/shared/transferFrom.t.sol b/test/integration/shared/transferFrom.t.sol index 16d2a05..8ca9819 100644 --- a/test/integration/shared/transferFrom.t.sol +++ b/test/integration/shared/transferFrom.t.sol @@ -2,11 +2,11 @@ pragma solidity ^0.8.26; import { Integration_Test } from "../Integration.t.sol"; -import { PayInvoice_Integration_Shared_Test } from "./payInvoice.t.sol"; +import { PayRequest_Integration_Shared_Test } from "./payRequest.t.sol"; -abstract contract TransferFrom_Integration_Shared_Test is Integration_Test, PayInvoice_Integration_Shared_Test { - function setUp() public virtual override(Integration_Test, PayInvoice_Integration_Shared_Test) { - PayInvoice_Integration_Shared_Test.setUp(); +abstract contract TransferFrom_Integration_Shared_Test is Integration_Test, PayRequest_Integration_Shared_Test { + function setUp() public virtual override(Integration_Test, PayRequest_Integration_Shared_Test) { + PayRequest_Integration_Shared_Test.setUp(); } modifier whenTokenExists() { diff --git a/test/integration/shared/withdrawLinearStream.t.sol b/test/integration/shared/withdrawLinearStream.t.sol index acbc493..a3b44da 100644 --- a/test/integration/shared/withdrawLinearStream.t.sol +++ b/test/integration/shared/withdrawLinearStream.t.sol @@ -2,15 +2,18 @@ pragma solidity ^0.8.26; import { Integration_Test } from "../Integration.t.sol"; -import { PayInvoice_Integration_Shared_Test } from "./payInvoice.t.sol"; +import { PayRequest_Integration_Shared_Test } from "./payRequest.t.sol"; -abstract contract WithdrawLinearStream_Integration_Shared_Test is Integration_Test, PayInvoice_Integration_Shared_Test { - function setUp() public virtual override(Integration_Test, PayInvoice_Integration_Shared_Test) { - PayInvoice_Integration_Shared_Test.setUp(); - createMockInvoices(); +abstract contract WithdrawLinearStream_Integration_Shared_Test is + Integration_Test, + PayRequest_Integration_Shared_Test +{ + function setUp() public virtual override(Integration_Test, PayRequest_Integration_Shared_Test) { + PayRequest_Integration_Shared_Test.setUp(); + createMockPaymentRequests(); } - modifier givenInvoiceStatusOngoing() { + modifier givenRequestStatusPending() { _; } } diff --git a/test/integration/shared/withdrawTranchedStream.t.sol b/test/integration/shared/withdrawTranchedStream.t.sol index 7204670..b82b707 100644 --- a/test/integration/shared/withdrawTranchedStream.t.sol +++ b/test/integration/shared/withdrawTranchedStream.t.sol @@ -2,18 +2,18 @@ pragma solidity ^0.8.26; import { Integration_Test } from "../Integration.t.sol"; -import { PayInvoice_Integration_Shared_Test } from "./payInvoice.t.sol"; +import { PayRequest_Integration_Shared_Test } from "./payRequest.t.sol"; abstract contract WithdrawTranchedStream_Integration_Shared_Test is Integration_Test, - PayInvoice_Integration_Shared_Test + PayRequest_Integration_Shared_Test { - function setUp() public virtual override(Integration_Test, PayInvoice_Integration_Shared_Test) { - PayInvoice_Integration_Shared_Test.setUp(); - createMockInvoices(); + function setUp() public virtual override(Integration_Test, PayRequest_Integration_Shared_Test) { + PayRequest_Integration_Shared_Test.setUp(); + createMockPaymentRequests(); } - modifier givenInvoiceStatusOngoing() { + modifier givenRequestStatusPending() { _; } } diff --git a/test/mocks/MockModule.sol b/test/mocks/MockModule.sol index 78e22cf..6fdab83 100644 --- a/test/mocks/MockModule.sol +++ b/test/mocks/MockModule.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.26; import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import { ISpace } from "./../../src/interfaces/ISpace.sol"; -import { Errors } from "./../../src/modules/invoice-module/libraries/Errors.sol"; +import { Errors } from "./../../src/modules/payment-module/libraries/Errors.sol"; /// @notice A mock implementation of a boilerplate module that creates multiple items and /// associates them with the corresponding {Space} contract diff --git a/test/mocks/MockStreamManager.sol b/test/mocks/MockStreamManager.sol index 2334112..839eaff 100644 --- a/test/mocks/MockStreamManager.sol +++ b/test/mocks/MockStreamManager.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.26; -import { StreamManager } from "./../../src/modules/invoice-module/sablier-v2/StreamManager.sol"; +import { StreamManager } from "./../../src/modules/payment-module/sablier-v2/StreamManager.sol"; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; @@ -12,5 +12,7 @@ contract MockStreamManager is StreamManager { ISablierV2LockupLinear _sablierLockupLinear, ISablierV2LockupTranched _sablierLockupTranched, address _brokerAdmin - ) StreamManager(_sablierLockupLinear, _sablierLockupTranched, _brokerAdmin) {} + ) + StreamManager(_sablierLockupLinear, _sablierLockupTranched, _brokerAdmin) + { } } diff --git a/test/unit/concrete/helpers/computeNumberOfPayments.t.sol b/test/unit/concrete/helpers/computeNumberOfPayments.t.sol index df5dcc2..fba9221 100644 --- a/test/unit/concrete/helpers/computeNumberOfPayments.t.sol +++ b/test/unit/concrete/helpers/computeNumberOfPayments.t.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.26; import { Base_Test } from "../../../Base.t.sol"; -import { Helpers } from "./../../../../src/modules/invoice-module/libraries/Helpers.sol"; -import { Types } from "./../../../../src/modules/invoice-module/libraries/Types.sol"; +import { Helpers } from "./../../../../src/modules/payment-module/libraries/Helpers.sol"; +import { Types } from "./../../../../src/modules/payment-module/libraries/Types.sol"; contract ComputeNumberOfPayments_Helpers_Test is Base_Test { function setUp() public virtual override { @@ -16,10 +16,8 @@ contract ComputeNumberOfPayments_Helpers_Test is Base_Test { uint40 endTime = uint40(block.timestamp + 11 weeks); // Run the test - uint40 numberOfPayments = Helpers.computeNumberOfPayments({ - recurrence: Types.Recurrence.Weekly, - interval: endTime - startTime - }); + uint40 numberOfPayments = + Helpers.computeNumberOfPayments({ recurrence: Types.Recurrence.Weekly, interval: endTime - startTime }); // Assert the actual and expected number of payments assertEq(numberOfPayments, 11); @@ -31,10 +29,8 @@ contract ComputeNumberOfPayments_Helpers_Test is Base_Test { uint40 endTime = uint40(block.timestamp + 2 * 4 weeks); // Run the test - uint40 numberOfPayments = Helpers.computeNumberOfPayments({ - recurrence: Types.Recurrence.Monthly, - interval: endTime - startTime - }); + uint40 numberOfPayments = + Helpers.computeNumberOfPayments({ recurrence: Types.Recurrence.Monthly, interval: endTime - startTime }); // Assert the actual and expected number of payments assertEq(numberOfPayments, 2); @@ -46,10 +42,8 @@ contract ComputeNumberOfPayments_Helpers_Test is Base_Test { uint40 endTime = uint40(block.timestamp + 3 * 48 weeks); // Run the test - uint40 numberOfPayments = Helpers.computeNumberOfPayments({ - recurrence: Types.Recurrence.Yearly, - interval: endTime - startTime - }); + uint40 numberOfPayments = + Helpers.computeNumberOfPayments({ recurrence: Types.Recurrence.Yearly, interval: endTime - startTime }); // Assert the actual and expected number of payments assertEq(numberOfPayments, 3); diff --git a/test/unit/concrete/space/Space.t.sol b/test/unit/concrete/space/Space.t.sol index 131e5fc..658db2f 100644 --- a/test/unit/concrete/space/Space.t.sol +++ b/test/unit/concrete/space/Space.t.sol @@ -10,6 +10,6 @@ contract Space_Unit_Concrete_Test is Base_Test { address[] memory modules = new address[](1); modules[0] = address(mockModule); - space = deploySpace({ _owner: users.eve, _spaceId: 0, _initialModules: modules }); + space = deploySpace({ _owner: users.eve, _stationId: 0, _initialModules: modules }); } } diff --git a/test/unit/concrete/space/withdraw-native/withdrawNative.t.sol b/test/unit/concrete/space/withdraw-native/withdrawNative.t.sol index 4549b22..d1eff62 100644 --- a/test/unit/concrete/space/withdraw-native/withdrawNative.t.sol +++ b/test/unit/concrete/space/withdraw-native/withdrawNative.t.sol @@ -21,7 +21,7 @@ contract WithdrawNative_Unit_Concrete_Test is Space_Unit_Concrete_Test { // Deploy the `badSpace` space address[] memory modules = new address[](1); modules[0] = address(mockModule); - badSpace = deploySpace({ _owner: address(badReceiver), _spaceId: 0, _initialModules: modules }); + badSpace = deploySpace({ _owner: address(badReceiver), _stationId: 0, _initialModules: modules }); } function test_RevertWhen_CallerNotOwner() external { @@ -51,7 +51,7 @@ contract WithdrawNative_Unit_Concrete_Test is Space_Unit_Concrete_Test { modifier whenSufficientNativeToWithdraw(Space space) { // Deposit sufficient native tokens (ETH) into the space to enable the withdrawal - (bool success, ) = payable(space).call{ value: 2 ether }(""); + (bool success,) = payable(space).call{ value: 2 ether }(""); if (!success) revert(); _; } diff --git a/test/unit/concrete/station-registry/create-account/createAccount.t.sol b/test/unit/concrete/station-registry/create-account/createAccount.t.sol index d0b5242..d77d218 100644 --- a/test/unit/concrete/station-registry/create-account/createAccount.t.sol +++ b/test/unit/concrete/station-registry/create-account/createAccount.t.sol @@ -19,11 +19,8 @@ contract CreateAccount_Unit_Concrete_Test is StationRegistry_Unit_Concrete_Test // The {StationRegistry} contract deploys each new {Space} contract. // Therefore, we need to calculate the current nonce of the {StationRegistry} // to pre-compute the address of the new {Space} before deployment. - (address expectedSpace, bytes memory data) = computeDeploymentAddressAndCalldata({ - deployer: users.bob, - stationId: 0, - initialModules: mockModules - }); + (address expectedSpace, bytes memory data) = + computeDeploymentAddressAndCalldata({ deployer: users.bob, stationId: 0, initialModules: mockModules }); // Allowlist the mock modules on the {ModuleKeeper} contract from the admin account vm.startPrank({ msgSender: users.admin }); @@ -58,17 +55,14 @@ contract CreateAccount_Unit_Concrete_Test is StationRegistry_Unit_Concrete_Test modifier whenStationIdNonZero() { // Create & deploy a new space with Eve as the owner - space = deploySpace({ _owner: users.bob, _spaceId: 0, _initialModules: mockModules }); + space = deploySpace({ _owner: users.bob, _stationId: 0, _initialModules: mockModules }); _; } function test_RevertWhen_CallerNotStationOwner() external whenStationIdNonZero { // Construct the calldata to be used to initialize the {Space} smart account - bytes memory data = computeCreateAccountCalldata({ - deployer: users.eve, - stationId: 1, - initialModules: mockModules - }); + bytes memory data = + computeCreateAccountCalldata({ deployer: users.eve, stationId: 1, initialModules: mockModules }); // Make Eve the caller in this test suite vm.prank({ msgSender: users.eve }); @@ -88,11 +82,8 @@ contract CreateAccount_Unit_Concrete_Test is StationRegistry_Unit_Concrete_Test // The {StationRegistry} contract deploys each new {Space} contract. // Therefore, we need to calculate the current nonce of the {StationRegistry} // to pre-compute the address of the new {Space} before deployment. - (address expectedSpace, bytes memory data) = computeDeploymentAddressAndCalldata({ - deployer: users.bob, - stationId: 1, - initialModules: mockModules - }); + (address expectedSpace, bytes memory data) = + computeDeploymentAddressAndCalldata({ deployer: users.bob, stationId: 1, initialModules: mockModules }); // Allowlist the mock modules on the {ModuleKeeper} contract from the admin account vm.startPrank({ msgSender: users.admin }); diff --git a/test/unit/concrete/station-registry/transfer-station-ownership/transferDockOwnership.t.sol b/test/unit/concrete/station-registry/transfer-station-ownership/transferDockOwnership.t.sol index 6b09da1..e1b7bb4 100644 --- a/test/unit/concrete/station-registry/transfer-station-ownership/transferDockOwnership.t.sol +++ b/test/unit/concrete/station-registry/transfer-station-ownership/transferDockOwnership.t.sol @@ -17,7 +17,7 @@ contract TransferStationOwnership_Unit_Concrete_Test is StationRegistry_Unit_Con address[] memory modules = new address[](1); modules[0] = address(mockModule); - space = deploySpace({ _owner: users.eve, _spaceId: 0, _initialModules: modules }); + space = deploySpace({ _owner: users.eve, _stationId: 0, _initialModules: modules }); _; } diff --git a/test/utils/Errors.sol b/test/utils/Errors.sol index aa90988..fdd8f1d 100644 --- a/test/utils/Errors.sol +++ b/test/utils/Errors.sol @@ -60,7 +60,7 @@ library Errors { error InvalidZeroCodeModule(); /*////////////////////////////////////////////////////////////////////////// - INVOICE-MODULE + PAYMENT-MODULE //////////////////////////////////////////////////////////////////////////*/ /// @notice Thrown when the caller is an invalid zero code contract or EOA @@ -69,16 +69,16 @@ library Errors { /// @notice Thrown when the caller is a contract that does not implement the {ISpace} interface error SpaceUnsupportedInterface(); - /// @notice Thrown when the end time of an invoice is in the past + /// @notice Thrown when the end time of a payment request is in the past error EndTimeInThePast(); /// @notice Thrown when the start time is later than the end time error StartTimeGreaterThanEndTime(); - /// @notice Thrown when the payment amount set for a new invoice is zero + /// @notice Thrown when the payment amount set for a new paymentRequest is zero error ZeroPaymentAmount(); - /// @notice Thrown when the payment amount is less than the invoice value + /// @notice Thrown when the payment amount is less than the payment request value error PaymentAmountLessThanInvoiceValue(uint256 amount); /// @notice Thrown when a payment in the native token (ETH) fails @@ -87,14 +87,14 @@ library Errors { /// @notice Thrown when a linear or tranched stream is created with the native token as the payment asset error OnlyERC20StreamsAllowed(); - /// @notice Thrown when a payer attempts to pay an invoice that has already been paid - error InvoiceAlreadyPaid(); + /// @notice Thrown when a payer attempts to pay a canceled payment request + error RequestCanceled(); - /// @notice Thrown when a payer attempts to pay a canceled invoice - error InvoiceCanceled(); + /// @notice Thrown when a payer attempts to pay a completed payment request + error RequestPaid(); - /// @notice Thrown when `msg.sender` is not the creator (recipient) of the invoice - error OnlyInvoiceRecipient(); + /// @notice Thrown when `msg.sender` is not the payment request recipient + error OnlyRequestRecipient(); /// @notice Thrown when the payment interval (endTime - startTime) is too short for the selected recurrence /// i.e. recurrence is set to weekly but interval is shorter than 1 week @@ -103,15 +103,15 @@ library Errors { /// @notice Thrown when a tranched stream has a one-off recurrence type error TranchedStreamInvalidOneOffRecurence(); - /// @notice Thrown when an attempt is made to cancel an already paid invoice - error CannotCancelPaidInvoice(); - - /// @notice Thrown when an attempt is made to cancel an already canceled invoice - error InvoiceAlreadyCanceled(); - /// @notice Thrown when the caller is not the initial stream sender error OnlyInitialStreamSender(address initialSender); + /// @notice Thrown when the payment request is null + error NullRequest(); + + /// @notice Thrown when the recipient address is the zero address + error InvalidZeroAddressRecipient(); + /*////////////////////////////////////////////////////////////////////////// STREAM-MANAGER //////////////////////////////////////////////////////////////////////////*/ diff --git a/test/utils/Events.sol b/test/utils/Events.sol index 8071cc5..1a6457c 100644 --- a/test/utils/Events.sol +++ b/test/utils/Events.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.26; -import { Types } from "./../../src/modules/invoice-module/libraries/Types.sol"; +import { Types } from "./../../src/modules/payment-module/libraries/Types.sol"; import { Space } from "./../../src/Space.sol"; import { ModuleKeeper } from "./../../src/ModuleKeeper.sol"; import { UD60x18 } from "@prb/math/src/UD60x18.sol"; @@ -96,23 +96,24 @@ abstract contract Events { //////////////////////////////////////////////////////////////////////////*/ /// @notice Emitted when a payment request is created - /// @param id The ID of the payment request + /// @param requestId The ID of the payment request /// @param recipient The address receiving the payment /// @param startTime The timestamp when the payment request takes effect /// @param endTime The timestamp by which the payment request must be paid /// @param config Struct representing the payment details associated with the payment request - event RequestCreated(uint256 id, address indexed recipient, uint40 startTime, uint40 endTime, Types.Config config); + event RequestCreated( + uint256 requestId, address indexed recipient, uint40 startTime, uint40 endTime, Types.Config config + ); /// @notice Emitted when a payment is made for a payment request - /// @param id The ID of the payment request + /// @param requestId The ID of the payment request /// @param payer The address of the payer - /// @param status The status of the payment request /// @param config Struct representing the payment details - event RequestPaid(uint256 indexed id, address indexed payer, Types.Status status, Types.Config config); + event RequestPaid(uint256 indexed requestId, address indexed payer, Types.Config config); /// @notice Emitted when a payment request is canceled - /// @param id The ID of the payment request - event RequestCanceled(uint256 indexed id); + /// @param requestId The ID of the payment request + event RequestCanceled(uint256 indexed requestId); /// @notice Emitted when the broker fee is updated /// @param oldFee The old broker fee diff --git a/test/utils/Helpers.sol b/test/utils/Helpers.sol index 853e317..7e15381 100644 --- a/test/utils/Helpers.sol +++ b/test/utils/Helpers.sol @@ -1,32 +1,19 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.26; -import { Types } from "./../../src/modules/invoice-module/libraries/Types.sol"; -import { Helpers as InvoiceHelpers } from "./../../src/modules/invoice-module/libraries/Helpers.sol"; +import { Types } from "./../../src/modules/payment-module/libraries/Types.sol"; +import { Helpers as InvoiceHelpers } from "./../../src/modules/payment-module/libraries/Helpers.sol"; library Helpers { - function createInvoiceDataType() public view returns (Types.Invoice memory) { - return - Types.Invoice({ - status: Types.Status.Pending, - startTime: 0, - endTime: uint40(block.timestamp) + 1 weeks, - payment: Types.Payment({ - method: Types.Method.Transfer, - recurrence: Types.Recurrence.OneOff, - paymentsLeft: 1, - asset: address(0), - amount: uint128(1 ether), - streamId: 0 - }) - }); - } - - /// @dev Calculates the number of payments that must be done based on a Recurring invoice + /// @dev Calculates the number of payments that must be done based on a Recurring paymentRequest function computeNumberOfRecurringPayments( Types.Recurrence recurrence, uint40 interval - ) internal pure returns (uint40 numberOfPayments) { + ) + internal + pure + returns (uint40 numberOfPayments) + { if (recurrence == Types.Recurrence.Weekly) { numberOfPayments = interval / 1 weeks; } else if (recurrence == Types.Recurrence.Monthly) { @@ -43,10 +30,23 @@ library Helpers { uint8 recurrence, uint40 startTime, uint40 endTime - ) internal pure returns (bool valid, uint40 numberOfPayments) { + ) + internal + pure + returns (bool valid, uint40 numberOfPayments) + { if (paymentMethod == uint8(Types.Method.Transfer) && recurrence == uint8(Types.Recurrence.OneOff)) { numberOfPayments = 1; - } else if (paymentMethod != uint8(Types.Method.LinearStream)) { + } else if ( + paymentMethod == uint8(Types.Method.TranchedStream) + || (paymentMethod == uint8(Types.Method.Transfer) && recurrence != uint8(Types.Recurrence.OneOff)) + ) { + // Break fuzz test if payment method is tranched stream and recurrence set to one-off + // as a tranched stream recurrence must be Weekly, Monthly or Yearly + if (recurrence == uint8(Types.Recurrence.OneOff)) { + return (false, 0); + } + numberOfPayments = InvoiceHelpers.computeNumberOfPayments({ recurrence: Types.Recurrence(recurrence), interval: endTime - startTime @@ -60,16 +60,10 @@ library Helpers { // Check for the maximum number of tranched steps in a Tranched Stream if (numberOfPayments > 500) return (false, 0); - numberOfPayments = 0; - } - } - - // Break fuzz test if payment method is tranched stream and recurrence set to one-off - // as a tranched stream recurrence must be Weekly, Monthly or Yearly - if (paymentMethod == uint8(Types.Method.TranchedStream)) { - if (recurrence == uint8(Types.Recurrence.OneOff)) { - return (false, 0); + numberOfPayments = 1; } + } else if (paymentMethod == uint8(Types.Method.LinearStream)) { + numberOfPayments = 1; } return (true, numberOfPayments); From 10b624dd2bab17e8dee7da72b72d186bb7252e57 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Fri, 15 Nov 2024 15:58:59 +0200 Subject: [PATCH 06/13] chore: prettier write on scripts --- script/DeployDeterministicInvoiceModule.s.sol | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/script/DeployDeterministicInvoiceModule.s.sol b/script/DeployDeterministicInvoiceModule.s.sol index 6ff7639..f095493 100644 --- a/script/DeployDeterministicInvoiceModule.s.sol +++ b/script/DeployDeterministicInvoiceModule.s.sol @@ -6,7 +6,7 @@ import { InvoiceModule } from "../src/modules/invoice-module/InvoiceModule.sol"; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; -/// @notice Deploys and initializes the {InvoiceModule} contracts at deterministic addresses across chains +/// @notice Deploys and initializes the {InvoiceModule} contract at deterministic addresses across chains /// @dev Reverts if any contract has already been deployed contract DeployDeterministicInvoiceModule is BaseScript { /// @dev By using a salt, Forge will deploy the contract via a deterministic CREATE2 factory @@ -17,15 +17,16 @@ contract DeployDeterministicInvoiceModule is BaseScript { ISablierV2LockupTranched sablierLockupTranched, address brokerAdmin, string memory baseURI - ) public virtual broadcast returns (InvoiceModule invoiceModule) { + ) + public + virtual + broadcast + returns (InvoiceModule invoiceModule) + { bytes32 salt = bytes32(abi.encodePacked(create2Salt)); - // Deterministically deploy the {InvoiceModule} contracts - invoiceModule = new InvoiceModule{ salt: salt }( - sablierLockupLinear, - sablierLockupTranched, - brokerAdmin, - baseURI - ); + // Deterministically deploy the {InvoiceModule} contract + invoiceModule = + new InvoiceModule{ salt: salt }(sablierLockupLinear, sablierLockupTranched, brokerAdmin, baseURI); } } From 8fca2354e49f739863b72a88ccaef5587a647f40 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Fri, 15 Nov 2024 16:13:17 +0200 Subject: [PATCH 07/13] chore: rename left 'Invoice*' docs/methods to 'Payment*' --- src/modules/payment-module/PaymentModule.sol | 2 +- .../payment-module/libraries/Errors.sol | 2 +- .../payment-module/libraries/Helpers.sol | 2 +- .../transfer-from/transferFrom.t.sol | 4 +-- .../cancel-request/cancelRequest.t.sol | 30 +++++++++---------- .../create-request/createRequest.t.sol | 4 +-- .../pay-request/payRequest.t.sol | 28 ++++++++--------- .../pay-request/payRequest.tree | 2 +- .../withdrawRequestStream.t.sol} | 6 ++-- .../withdrawRequestStream.tree} | 2 +- test/integration/fuzz/payRequest.t.sol | 6 ++-- test/integration/shared/cancelRequest.t.sol | 14 +-------- test/integration/shared/createRequest.t.sol | 4 +-- test/integration/shared/payRequest.t.sol | 2 +- test/utils/Errors.sol | 2 +- test/utils/Helpers.sol | 6 ++-- 16 files changed, 52 insertions(+), 64 deletions(-) rename test/integration/concrete/payment-module/{withdraw-invoice-stream/withdrawStream.t.sol => withdraw-request-stream/withdrawRequestStream.t.sol} (94%) rename test/integration/concrete/payment-module/{withdraw-invoice-stream/withdrawStream.tree => withdraw-request-stream/withdrawRequestStream.tree} (93%) diff --git a/src/modules/payment-module/PaymentModule.sol b/src/modules/payment-module/PaymentModule.sol index c3b74fa..2670b55 100644 --- a/src/modules/payment-module/PaymentModule.sol +++ b/src/modules/payment-module/PaymentModule.sol @@ -301,7 +301,7 @@ contract PaymentModule is IPaymentModule, StreamManager { if (request.config.asset == address(0)) { // Checks: the payment amount matches the request value if (msg.value < request.config.amount) { - revert Errors.PaymentAmountLessThanInvoiceValue({ amount: request.config.amount }); + revert Errors.PaymentAmountLessThanRequestedAmount({ amount: request.config.amount }); } // Interactions: pay the recipient with native token (ETH) diff --git a/src/modules/payment-module/libraries/Errors.sol b/src/modules/payment-module/libraries/Errors.sol index 63aa088..e4cefdf 100644 --- a/src/modules/payment-module/libraries/Errors.sol +++ b/src/modules/payment-module/libraries/Errors.sol @@ -24,7 +24,7 @@ library Errors { error ZeroPaymentAmount(); /// @notice Thrown when the payment amount is less than the payment request value - error PaymentAmountLessThanInvoiceValue(uint256 amount); + error PaymentAmountLessThanRequestedAmount(uint256 amount); /// @notice Thrown when a payment in the native token (ETH) fails error NativeTokenPaymentFailed(); diff --git a/src/modules/payment-module/libraries/Helpers.sol b/src/modules/payment-module/libraries/Helpers.sol index d32d36b..56d430f 100644 --- a/src/modules/payment-module/libraries/Helpers.sol +++ b/src/modules/payment-module/libraries/Helpers.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.26; import { Types } from "./Types.sol"; /// @title Helpers -/// @notice Library with helpers used across the {InvoiceModule} contract +/// @notice Library with helpers used across the {PaymentModule} contract library Helpers { /// @dev Calculates the number of payments that must be done for a recurring transfer or tranched stream paymentRequest /// Notes: diff --git a/test/integration/concrete/invoice-module/transfer-from/transferFrom.t.sol b/test/integration/concrete/invoice-module/transfer-from/transferFrom.t.sol index 798a126..c055d31 100644 --- a/test/integration/concrete/invoice-module/transfer-from/transferFrom.t.sol +++ b/test/integration/concrete/invoice-module/transfer-from/transferFrom.t.sol @@ -28,7 +28,7 @@ contract TransferFrom_Integration_Concret_Test is TransferFrom_Integration_Share // Make Bob the payer for the payment request vm.startPrank({ msgSender: users.bob }); - // Approve the {InvoiceModule} to transfer the USDT tokens on Bob's behalf + // Approve the {PaymentModule} to transfer the USDT tokens on Bob's behalf usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].payment.amount }); // Pay the payment request @@ -47,7 +47,7 @@ contract TransferFrom_Integration_Concret_Test is TransferFrom_Integration_Share // Make Eve's space the caller which is the recipient of the payment request vm.startPrank({ msgSender: address(space) }); - // Approve the {InvoiceModule} to transfer the `streamId` stream on behalf of the Eve's space + // Approve the {PaymentModule} to transfer the `streamId` stream on behalf of the Eve's space sablierV2LockupLinear.approve({ to: address(paymentModule), tokenId: streamId }); // Run the test diff --git a/test/integration/concrete/payment-module/cancel-request/cancelRequest.t.sol b/test/integration/concrete/payment-module/cancel-request/cancelRequest.t.sol index 626f830..986bc67 100644 --- a/test/integration/concrete/payment-module/cancel-request/cancelRequest.t.sol +++ b/test/integration/concrete/payment-module/cancel-request/cancelRequest.t.sol @@ -11,7 +11,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha CancelRequest_Integration_Shared_Test.setUp(); } - function test_RevertWhen_InvoiceIsPaid() external { + function test_RevertWhen_PaymentIsPaid() external { // Set the one-off ETH transfer payment request as current one uint256 paymentRequestId = 2; @@ -50,7 +50,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha paymentModule.cancelRequest({ requestId: paymentRequestId }); } - function test_RevertWhen_PaymentMethodTransfer_SenderNotInvoiceRecipient() + function test_RevertWhen_PaymentMethodTransfer_SenderNotPaymentRecipient() external whenRequestNotAlreadyPaid whenRequestNotCanceled @@ -74,7 +74,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha whenRequestNotAlreadyPaid whenRequestNotCanceled givenPaymentMethodTransfer - whenSenderInvoiceRecipient + whenRequestSenderRecipient { // Set the one-off ETH transfer payment request as current one uint256 paymentRequestId = 2; @@ -94,12 +94,12 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Canceled)); } - function test_RevertWhen_PaymentMethodLinearStream_StatusPending_SenderNotInvoiceRecipient() + function test_RevertWhen_PaymentMethodLinearStream_StatusPending_SenderNotPaymentRecipient() external whenRequestNotAlreadyPaid whenRequestNotCanceled givenPaymentMethodLinearStream - givenInvoiceStatusPending + givenRequestStatusPending { // Set current paymentRequest as a linear stream-based one uint256 paymentRequestId = 5; @@ -119,8 +119,8 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha whenRequestNotAlreadyPaid whenRequestNotCanceled givenPaymentMethodLinearStream - givenInvoiceStatusPending - whenSenderInvoiceRecipient + givenRequestStatusPending + whenRequestSenderRecipient { // Set current paymentRequest as a linear stream-based one uint256 paymentRequestId = 5; @@ -154,7 +154,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha // Make Bob the payer of the payment request (also Bob will be the stream sender) vm.startPrank({ msgSender: users.bob }); - // Approve the {InvoiceModule} to transfer the USDT tokens on Bob's behalf + // Approve the {PaymentModule} to transfer the USDT tokens on Bob's behalf usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); // Pay the payment request first (status will be updated to `Accepted`) @@ -187,7 +187,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha // Make Bob the payer of the payment request (also Bob will be the initial stream sender) vm.startPrank({ msgSender: users.bob }); - // Approve the {InvoiceModule} to transfer the USDT tokens on Bob's behalf + // Approve the {PaymentModule} to transfer the USDT tokens on Bob's behalf usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); // Pay the payment request first (status will be updated to `Accepted`) @@ -210,12 +210,12 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Canceled)); } - function test_RevertWhen_PaymentMethodTranchedStream_StatusPending_SenderNotInvoiceRecipient() + function test_RevertWhen_PaymentMethodTranchedStream_StatusPending_SenderNotPaymentRecipient() external whenRequestNotAlreadyPaid whenRequestNotCanceled givenPaymentMethodTranchedStream - givenInvoiceStatusPending + givenRequestStatusPending { // Set current paymentRequest as a tranched stream-based one uint256 paymentRequestId = 5; @@ -235,8 +235,8 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha whenRequestNotAlreadyPaid whenRequestNotCanceled givenPaymentMethodTranchedStream - givenInvoiceStatusPending - whenSenderInvoiceRecipient + givenRequestStatusPending + whenRequestSenderRecipient { // Set current paymentRequest as a tranched stream-based one uint256 paymentRequestId = 5; @@ -270,7 +270,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha // Make Bob the payer of the payment request (also Bob will be the stream sender) vm.startPrank({ msgSender: users.bob }); - // Approve the {InvoiceModule} to transfer the USDT tokens on Bob's behalf + // Approve the {PaymentModule} to transfer the USDT tokens on Bob's behalf usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); // Pay the payment request first (status will be updated to `Accepted`) @@ -303,7 +303,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha // Make Bob the payer of the payment request (also Bob will be the initial stream sender) vm.startPrank({ msgSender: users.bob }); - // Approve the {InvoiceModule} to transfer the USDT tokens on Bob's behalf + // Approve the {PaymentModule} to transfer the USDT tokens on Bob's behalf usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); // Pay the payment request first (status will be updated to `Accepted`) diff --git a/test/integration/concrete/payment-module/create-request/createRequest.t.sol b/test/integration/concrete/payment-module/create-request/createRequest.t.sol index dacd592..83a8bbd 100644 --- a/test/integration/concrete/payment-module/create-request/createRequest.t.sol +++ b/test/integration/concrete/payment-module/create-request/createRequest.t.sol @@ -203,7 +203,7 @@ contract CreateRequest_Integration_Concret_Test is CreateRequest_Integration_Sha // Create a recurring transfer payment request that must be paid on a monthly basis // Hence, the interval between the start and end time must be at least 1 month paymentRequest = - createInvoiceWithRecurringTransfer({ recurrence: Types.Recurrence.Monthly, recipient: address(space) }); + createPaymentWithRecurringTransfer({ recurrence: Types.Recurrence.Monthly, recipient: address(space) }); // Alter the end time to be 3 weeks from now paymentRequest.endTime = uint40(block.timestamp) + 3 weeks; @@ -236,7 +236,7 @@ contract CreateRequest_Integration_Concret_Test is CreateRequest_Integration_Sha // Create a recurring transfer payment request that must be paid on weekly basis paymentRequest = - createInvoiceWithRecurringTransfer({ recurrence: Types.Recurrence.Weekly, recipient: address(space) }); + createPaymentWithRecurringTransfer({ recurrence: Types.Recurrence.Weekly, recipient: address(space) }); // Create the calldata for the Payment Module execution bytes memory data = abi.encodeWithSignature( diff --git a/test/integration/concrete/payment-module/pay-request/payRequest.t.sol b/test/integration/concrete/payment-module/pay-request/payRequest.t.sol index 834734f..c10e278 100644 --- a/test/integration/concrete/payment-module/pay-request/payRequest.t.sol +++ b/test/integration/concrete/payment-module/pay-request/payRequest.t.sol @@ -8,7 +8,7 @@ import { Errors } from "../../../../utils/Errors.sol"; import { LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; -contract PayInvoice_Integration_Concret_Test is PayRequest_Integration_Shared_Test { +contract PayPayment_Integration_Concret_Test is PayRequest_Integration_Shared_Test { function setUp() public virtual override { PayRequest_Integration_Shared_Test.setUp(); } @@ -28,7 +28,7 @@ contract PayInvoice_Integration_Concret_Test is PayRequest_Integration_Shared_Te // Make Bob the payer for the default paymentRequest vm.startPrank({ msgSender: users.bob }); - // Approve the {InvoiceModule} to transfer the ERC-20 token on Bob's behalf + // Approve the {PaymentModule} to transfer the ERC-20 token on Bob's behalf usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); // Pay first the payment request @@ -61,7 +61,7 @@ contract PayInvoice_Integration_Concret_Test is PayRequest_Integration_Shared_Te paymentModule.payRequest({ requestId: paymentRequestId }); } - function test_RevertWhen_PaymentMethodTransfer_PaymentAmountLessThanInvoiceValue() + function test_RevertWhen_PaymentMethodTransfer_PaymentAmountLessThanRequestedAmount() external whenRequestNotNull whenRequestNotAlreadyPaid @@ -75,10 +75,10 @@ contract PayInvoice_Integration_Concret_Test is PayRequest_Integration_Shared_Te // Make Bob the payer for the default paymentRequest vm.startPrank({ msgSender: users.bob }); - // Expect the call to be reverted with the {PaymentAmountLessThanInvoiceValue} error + // Expect the call to be reverted with the {PaymentAmountLessThanRequestedAmount} error vm.expectRevert( abi.encodeWithSelector( - Errors.PaymentAmountLessThanInvoiceValue.selector, paymentRequests[paymentRequestId].config.amount + Errors.PaymentAmountLessThanRequestedAmount.selector, paymentRequests[paymentRequestId].config.amount ) ); @@ -95,7 +95,7 @@ contract PayInvoice_Integration_Concret_Test is PayRequest_Integration_Shared_Te whenRequestNotCanceled givenPaymentMethodTransfer givenPaymentAmountInNativeToken - whenPaymentAmountEqualToInvoiceValue + whenPaymentAmountEqualToPaymentValue { // Create a mock payment request with a one-off ETH transfer from the Eve's space Types.PaymentRequest memory paymentRequest = @@ -107,7 +107,7 @@ contract PayInvoice_Integration_Concret_Test is PayRequest_Integration_Shared_Te // Make Eve's space the caller for the next call to approve & transfer the payment request NFT to a bad receiver //vm.startPrank({ msgSender: address(space) }); - // Approve the {InvoiceModule} to transfer the token + // Approve the {PaymentModule} to transfer the token //paymentModule.approve({ to: address(paymentModule), tokenrequestId: paymentRequestId }); // Transfer the payment request to a bad receiver so we can test against `NativeTokenPaymentFailed` @@ -130,7 +130,7 @@ contract PayInvoice_Integration_Concret_Test is PayRequest_Integration_Shared_Te whenRequestNotCanceled givenPaymentMethodTransfer givenPaymentAmountInNativeToken - whenPaymentAmountEqualToInvoiceValue + whenPaymentAmountEqualToPaymentValue whenNativeTokenPaymentSucceeds { // Set the one-off ETH transfer payment request as current one @@ -182,7 +182,7 @@ contract PayInvoice_Integration_Concret_Test is PayRequest_Integration_Shared_Te whenRequestNotCanceled givenPaymentMethodTransfer givenPaymentAmountInERC20Tokens - whenPaymentAmountEqualToInvoiceValue + whenPaymentAmountEqualToPaymentValue { // Set the recurring USDT transfer payment request as current one uint256 paymentRequestId = 3; @@ -194,7 +194,7 @@ contract PayInvoice_Integration_Concret_Test is PayRequest_Integration_Shared_Te uint256 balanceOfBobBefore = usdt.balanceOf(users.bob); uint256 balanceOfRecipientBefore = usdt.balanceOf(address(space)); - // Approve the {InvoiceModule} to transfer the ERC-20 tokens on Bob's behalf + // Approve the {PaymentModule} to transfer the ERC-20 tokens on Bob's behalf usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); // Expect the {RequestPaid} event to be emitted @@ -238,7 +238,7 @@ contract PayInvoice_Integration_Concret_Test is PayRequest_Integration_Shared_Te whenRequestNotCanceled givenPaymentMethodLinearStream givenPaymentAmountInERC20Tokens - whenPaymentAmountEqualToInvoiceValue + whenPaymentAmountEqualToPaymentValue { // Set the linear USDT stream-based paymentRequest as current one uint256 paymentRequestId = 4; @@ -246,7 +246,7 @@ contract PayInvoice_Integration_Concret_Test is PayRequest_Integration_Shared_Te // Make Bob the payer for the default paymentRequest vm.startPrank({ msgSender: users.bob }); - // Approve the {InvoiceModule} to transfer the ERC-20 tokens on Bob's behalf + // Approve the {PaymentModule} to transfer the ERC-20 tokens on Bob's behalf usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); // Expect the {RequestPaid} event to be emitted @@ -293,7 +293,7 @@ contract PayInvoice_Integration_Concret_Test is PayRequest_Integration_Shared_Te whenRequestNotCanceled givenPaymentMethodTranchedStream givenPaymentAmountInERC20Tokens - whenPaymentAmountEqualToInvoiceValue + whenPaymentAmountEqualToPaymentValue { // Set the tranched USDT stream-based paymentRequest as current one uint256 paymentRequestId = 5; @@ -301,7 +301,7 @@ contract PayInvoice_Integration_Concret_Test is PayRequest_Integration_Shared_Te // Make Bob the payer for the default paymentRequest vm.startPrank({ msgSender: users.bob }); - // Approve the {InvoiceModule} to transfer the ERC-20 tokens on Bob's behalf + // Approve the {PaymentModule} to transfer the ERC-20 tokens on Bob's behalf usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); // Expect the {RequestPaid} event to be emitted diff --git a/test/integration/concrete/payment-module/pay-request/payRequest.tree b/test/integration/concrete/payment-module/pay-request/payRequest.tree index 37b8e80..40c7ccc 100644 --- a/test/integration/concrete/payment-module/pay-request/payRequest.tree +++ b/test/integration/concrete/payment-module/pay-request/payRequest.tree @@ -11,7 +11,7 @@ payRequest.t.sol ├── given the payment method is transfer │ ├── given the payment amount is in native token (ETH) │ │ ├── when the payment amount is less than the payment request value - │ │ │ └── it should revert with the {PaymentAmountLessThanInvoiceValue} error + │ │ │ └── it should revert with the {PaymentAmountLessThanRequestedAmount} error │ │ └── when the payment amount IS equal to the payment request value │ │ ├── when the native token transfer fails │ │ │ └── it should revert with the {NativeTokenPaymentFailed} error diff --git a/test/integration/concrete/payment-module/withdraw-invoice-stream/withdrawStream.t.sol b/test/integration/concrete/payment-module/withdraw-request-stream/withdrawRequestStream.t.sol similarity index 94% rename from test/integration/concrete/payment-module/withdraw-invoice-stream/withdrawStream.t.sol rename to test/integration/concrete/payment-module/withdraw-request-stream/withdrawRequestStream.t.sol index 68b4cf4..bcdb443 100644 --- a/test/integration/concrete/payment-module/withdraw-invoice-stream/withdrawStream.t.sol +++ b/test/integration/concrete/payment-module/withdraw-request-stream/withdrawRequestStream.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.26; import { WithdrawLinearStream_Integration_Shared_Test } from "../../../shared/withdrawLinearStream.t.sol"; import { Types } from "./../../../../../src/modules/payment-module/libraries/Types.sol"; -contract WithdrawLinearStream_Integration_Concret_Test is WithdrawLinearStream_Integration_Shared_Test { +contract WithdrawRequestStream_Integration_Concret_Test is WithdrawLinearStream_Integration_Shared_Test { function setUp() public virtual override { WithdrawLinearStream_Integration_Shared_Test.setUp(); } @@ -18,7 +18,7 @@ contract WithdrawLinearStream_Integration_Concret_Test is WithdrawLinearStream_I // Make Bob the payer of the payment request (also Bob will be the initial stream sender) vm.startPrank({ msgSender: users.bob }); - // Approve the {InvoiceModule} to transfer the USDT tokens on Bob's behalf + // Approve the {PaymentModule} to transfer the USDT tokens on Bob's behalf usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); // Pay the payment request first (status will be updated to `Accepted`) @@ -55,7 +55,7 @@ contract WithdrawLinearStream_Integration_Concret_Test is WithdrawLinearStream_I // Make Bob the payer of the payment request (also Bob will be the initial stream sender) vm.startPrank({ msgSender: users.bob }); - // Approve the {InvoiceModule} to transfer the USDT tokens on Bob's behalf + // Approve the {PaymentModule} to transfer the USDT tokens on Bob's behalf usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].config.amount }); // Pay the payment request first (status will be updated to `Accepted`) diff --git a/test/integration/concrete/payment-module/withdraw-invoice-stream/withdrawStream.tree b/test/integration/concrete/payment-module/withdraw-request-stream/withdrawRequestStream.tree similarity index 93% rename from test/integration/concrete/payment-module/withdraw-invoice-stream/withdrawStream.tree rename to test/integration/concrete/payment-module/withdraw-request-stream/withdrawRequestStream.tree index fde0e5d..d80b2f7 100644 --- a/test/integration/concrete/payment-module/withdraw-invoice-stream/withdrawStream.tree +++ b/test/integration/concrete/payment-module/withdraw-request-stream/withdrawRequestStream.tree @@ -1,4 +1,4 @@ -withdrawStream.t.sol +withdrawRequestStream.t.sol ├── given the payment method is linear stream │ └── given the payment request status is Ongoing │ └── it should allow the payment request recipient to withdraw from the stream diff --git a/test/integration/fuzz/payRequest.t.sol b/test/integration/fuzz/payRequest.t.sol index 7caeb26..2e1652f 100644 --- a/test/integration/fuzz/payRequest.t.sol +++ b/test/integration/fuzz/payRequest.t.sol @@ -26,7 +26,7 @@ contract PayRequest_Integration_Fuzz_Test is PayRequest_Integration_Shared_Test whenRequestNotCanceled givenPaymentMethodTransfer givenPaymentAmountInNativeToken - whenPaymentAmountEqualToInvoiceValue + whenPaymentAmountEqualToPaymentValue whenNativeTokenPaymentSucceeds { // Discard bad fuzz inputs @@ -59,7 +59,7 @@ contract PayRequest_Integration_Fuzz_Test is PayRequest_Integration_Shared_Test }) }); - // Create the calldata for the {InvoiceModule} execution + // Create the calldata for the {PaymentModule} execution bytes memory data = abi.encodeWithSignature( "createRequest((bool,bool,uint40,uint40,address,(uint8,uint8,uint40,address,uint128,uint256)))", paymentRequest @@ -79,7 +79,7 @@ contract PayRequest_Integration_Fuzz_Test is PayRequest_Integration_Shared_Test // Make payer the caller to pay for the fuzzed paymentRequest vm.startPrank({ msgSender: users.bob }); - // Approve the {InvoiceModule} to transfer the USDT tokens on payer's behalf + // Approve the {PaymentModule} to transfer the USDT tokens on payer's behalf usdt.approve({ spender: address(paymentModule), amount: paymentRequest.config.amount }); // Store the USDT balances of the payer and recipient before paying the payment request diff --git a/test/integration/shared/cancelRequest.t.sol b/test/integration/shared/cancelRequest.t.sol index bf9aa0d..458ef8e 100644 --- a/test/integration/shared/cancelRequest.t.sol +++ b/test/integration/shared/cancelRequest.t.sol @@ -9,19 +9,7 @@ abstract contract CancelRequest_Integration_Shared_Test is Integration_Test, Pay PayRequest_Integration_Shared_Test.setUp(); } - modifier whenInvoiceStatusNotPaid() { - _; - } - - modifier whenInvoiceStatusNotCanceled() { - _; - } - - modifier whenSenderInvoiceRecipient() { - _; - } - - modifier givenInvoiceStatusPending() { + modifier whenRequestSenderRecipient() { _; } diff --git a/test/integration/shared/createRequest.t.sol b/test/integration/shared/createRequest.t.sol index 2cfa6cd..d3492aa 100644 --- a/test/integration/shared/createRequest.t.sol +++ b/test/integration/shared/createRequest.t.sol @@ -28,7 +28,7 @@ abstract contract CreateRequest_Integration_Shared_Test is Integration_Test { // Create a mock payment request with a recurring USDT transfer paymentRequest = - createInvoiceWithRecurringTransfer({ recurrence: Types.Recurrence.Weekly, recipient: address(space) }); + createPaymentWithRecurringTransfer({ recurrence: Types.Recurrence.Weekly, recipient: address(space) }); paymentRequests[3] = paymentRequest; executeCreatePaymentRequest({ paymentRequest: paymentRequest, user: users.eve }); @@ -117,7 +117,7 @@ abstract contract CreateRequest_Integration_Shared_Test is Integration_Test { } /// @dev Creates a payment request with a recurring transfer payment - function createInvoiceWithRecurringTransfer( + function createPaymentWithRecurringTransfer( Types.Recurrence recurrence, address recipient ) diff --git a/test/integration/shared/payRequest.t.sol b/test/integration/shared/payRequest.t.sol index 649ecbd..1eab082 100644 --- a/test/integration/shared/payRequest.t.sol +++ b/test/integration/shared/payRequest.t.sol @@ -34,7 +34,7 @@ abstract contract PayRequest_Integration_Shared_Test is Integration_Test, Create _; } - modifier whenPaymentAmountEqualToInvoiceValue() { + modifier whenPaymentAmountEqualToPaymentValue() { _; } diff --git a/test/utils/Errors.sol b/test/utils/Errors.sol index fdd8f1d..bef3eec 100644 --- a/test/utils/Errors.sol +++ b/test/utils/Errors.sol @@ -79,7 +79,7 @@ library Errors { error ZeroPaymentAmount(); /// @notice Thrown when the payment amount is less than the payment request value - error PaymentAmountLessThanInvoiceValue(uint256 amount); + error PaymentAmountLessThanRequestedAmount(uint256 amount); /// @notice Thrown when a payment in the native token (ETH) fails error NativeTokenPaymentFailed(); diff --git a/test/utils/Helpers.sol b/test/utils/Helpers.sol index 7e15381..1216968 100644 --- a/test/utils/Helpers.sol +++ b/test/utils/Helpers.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.26; import { Types } from "./../../src/modules/payment-module/libraries/Types.sol"; -import { Helpers as InvoiceHelpers } from "./../../src/modules/payment-module/libraries/Helpers.sol"; +import { Helpers as PaymentHelpers } from "./../../src/modules/payment-module/libraries/Helpers.sol"; library Helpers { /// @dev Calculates the number of payments that must be done based on a Recurring paymentRequest @@ -24,7 +24,7 @@ library Helpers { } /// @dev Checks if the fuzzed recurrence and payment method are valid; - /// Check {IInvoiceModule-createInvoice} for reference + /// Check {IPaymentModule-createRequest} for reference function checkFuzzedPaymentMethod( uint8 paymentMethod, uint8 recurrence, @@ -47,7 +47,7 @@ library Helpers { return (false, 0); } - numberOfPayments = InvoiceHelpers.computeNumberOfPayments({ + numberOfPayments = PaymentHelpers.computeNumberOfPayments({ recurrence: Types.Recurrence(recurrence), interval: endTime - startTime }); From 8d937c122655230d4664f5bc06029c8b3671f122 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 18 Nov 2024 14:51:15 +0200 Subject: [PATCH 08/13] refactor: migrate 'InvoiceModule' to 'InvoiceCollection' peripheral --- src/modules/invoice-module/InvoiceModule.sol | 119 ------------------ .../interfaces/IInvoiceModule.sol | 15 --- .../invoice-collection/InvoiceCollection.sol | 99 +++++++++++++++ .../interfaces/IInvoiceCollection.sol | 33 +++++ .../invoice-collection}/libraries/Errors.sol | 4 +- 5 files changed, 134 insertions(+), 136 deletions(-) delete mode 100644 src/modules/invoice-module/InvoiceModule.sol delete mode 100644 src/modules/invoice-module/interfaces/IInvoiceModule.sol create mode 100644 src/peripherals/invoice-collection/InvoiceCollection.sol create mode 100644 src/peripherals/invoice-collection/interfaces/IInvoiceCollection.sol rename src/{modules/invoice-module => peripherals/invoice-collection}/libraries/Errors.sol (77%) diff --git a/src/modules/invoice-module/InvoiceModule.sol b/src/modules/invoice-module/InvoiceModule.sol deleted file mode 100644 index c60526f..0000000 --- a/src/modules/invoice-module/InvoiceModule.sol +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.26; - -import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; -import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; -import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; - -import { PaymentModule } from "./../payment-module/PaymentModule.sol"; -import { IInvoiceModule } from "./interfaces/IInvoiceModule.sol"; -import { Errors } from "./libraries/Errors.sol"; - -/// @title InvoiceModule -/// @notice See the documentation in {IInvoiceModule} -contract InvoiceModule is IInvoiceModule, PaymentModule, ERC721 { - using Strings for uint256; - - /*////////////////////////////////////////////////////////////////////////// - PUBLIC STORAGE - //////////////////////////////////////////////////////////////////////////*/ - - /// @dev The address of the off-chain Relayer responsible to mint on-chain invoices - address public relayer; - - /*////////////////////////////////////////////////////////////////////////// - PRIVATE STORAGE - //////////////////////////////////////////////////////////////////////////*/ - - /// @dev Invoice ID mapped to the payment request ID - mapping(uint256 invoiceId => uint256 paymentRequestId) private _invoiceIdToPaymentRequest; - - /// @dev Counter to keep track of the next ID used to create a new invoice - uint256 private _nextInvoiceId; - - /// @dev Base URI used to get the ERC-721 `tokenURI` metadata JSON schema - string private _collectionURI; - - /*////////////////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////////////////*/ - - /// @dev Initializes the {PaymentModule} and {ERC721} contracts - constructor( - ISablierV2LockupLinear _sablierLockupLinear, - ISablierV2LockupTranched _sablierLockupTranched, - address _brokerAdmin, - string memory _URI - ) - PaymentModule(_sablierLockupLinear, _sablierLockupTranched, _brokerAdmin) - ERC721("Werk Invoice NFTs", "WK-INVOICES") - { - // Start the invoice IDs from 1 - _nextInvoiceId = 1; - - // Set the ERC721 baseURI - _collectionURI = _URI; - } - - /*////////////////////////////////////////////////////////////////////////// - CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc ERC721 - function tokenURI(uint256 tokenId) public view override returns (string memory) { - // Checks: the `tokenId` was minted or is not burned - _requireOwned(tokenId); - - // Create the `tokenURI` by concatenating the `baseURI`, `tokenId` and metadata extension (.json) - string memory baseURI = _baseURI(); - return string.concat(baseURI, tokenId.toString(), ".json"); - } - - /*////////////////////////////////////////////////////////////////////////// - NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc IInvoiceModule - function mint(address to, uint256 paymentRequestId) public { - // Checks: `msg.sender` is the authorized Relayer to mint tokens - if (msg.sender != relayer) { - revert Errors.Unathorized(); - } - - // Get the next token ID - uint256 tokenId = _nextInvoiceId; - - // Effects: increment the next payment request ID - // Use unchecked because the request id cannot realistically overflow - unchecked { - ++_nextInvoiceId; - } - - // Effects: set the `paymentRequestId` that belongs to the `tokenId` invoice - _invoiceIdToPaymentRequest[tokenId] = paymentRequestId; - - // Effects: mint the request NFT to the recipient space - _mint(to, tokenId); - } - - /*////////////////////////////////////////////////////////////////////////// - INTERNAL-METHODS - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc ERC721 - function _baseURI() internal view override returns (string memory) { - return _collectionURI; - } - - /// @inheritdoc ERC721 - /// @dev Guard tokens from being transferred making them Soulbound Tokens (SBT) - function _update(address to, uint256 tokenId, address auth) internal override(ERC721) returns (address) { - address from = _ownerOf(tokenId); - if (from != address(0) && to != address(0)) { - revert("Soulbound: Transfer failed"); - } - - return super._update(to, tokenId, auth); - } -} diff --git a/src/modules/invoice-module/interfaces/IInvoiceModule.sol b/src/modules/invoice-module/interfaces/IInvoiceModule.sol deleted file mode 100644 index 14c74fc..0000000 --- a/src/modules/invoice-module/interfaces/IInvoiceModule.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.26; - -/// @title IInvoiceModule -/// @notice Contract module that provides functionalities to issue and pay an on-chain invoice -interface IInvoiceModule { - /*////////////////////////////////////////////////////////////////////////// - NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Creates an on-chain representation of an off-chain invoice by minting an ERC-721 token - /// @param to The address to which the NFT will be minted - /// @param paymentRequestId The ID of the payment request to which this invoice belongs - function mint(address to, uint256 paymentRequestId) external; -} diff --git a/src/peripherals/invoice-collection/InvoiceCollection.sol b/src/peripherals/invoice-collection/InvoiceCollection.sol new file mode 100644 index 0000000..e828c3a --- /dev/null +++ b/src/peripherals/invoice-collection/InvoiceCollection.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import { ERC721URIStorage } from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { Errors } from "./libraries/Errors.sol"; +import { IInvoiceCollection } from "./interfaces/IInvoiceCollection.sol"; + +/// @title InvoiceCollection +/// @notice See the documentation in {IInvoiceCollection} +contract InvoiceCollection is IInvoiceCollection, ERC721URIStorage { + using Strings for uint256; + + /*////////////////////////////////////////////////////////////////////////// + PUBLIC STORAGE + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev The address of the off-chain Relayer responsible to mint on-chain invoices + address public relayer; + + /// @dev Token ID of the invoicemapped to the payment request ID + mapping(uint256 tokenId => string paymentRequestId) public tokenIdToPaymentRequestId; + + /*////////////////////////////////////////////////////////////////////////// + PRIVATE STORAGE + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Counter to keep track of the next ID used to mint a new token per invoice + uint256 private _nextTokenId; + + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Initializes the {InvoiceCollection} contract + constructor(address _relayer, string memory _name, string memory _symbol) ERC721(_name, _symbol) { + // Set the authorized Relayer + relayer = _relayer; + + // Start the invoice token IDs from 1 + _nextTokenId = 1; + } + + /*////////////////////////////////////////////////////////////////////////// + NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IInvoiceCollection + function mintInvoice( + string memory invoiceURI, + address paymentRecipient, + string memory paymentRequestId + ) + public + returns (uint256 tokenId) + { + // Checks: `msg.sender` is the authorized Relayer to mint tokens + if (msg.sender != relayer) { + revert Errors.Unauthorized(); + } + + // Get the next token ID + tokenId = _nextTokenId; + + // Effects: increment the next token ID + // Use unchecked because the token ID cannot realistically overflow + unchecked { + ++_nextTokenId; + } + + // Effects: set the `paymentRequestId` that belongs to the `tokenId` invoice + tokenIdToPaymentRequestId[tokenId] = paymentRequestId; + + // Effects: mint the invoice NFT to the payment recipient + _mint({ to: paymentRecipient, tokenId: tokenId }); + + // Effects: set the `invoiceURI` for the `tokenId` invoice + _setTokenURI(tokenId, invoiceURI); + + // Log the invoice minting + emit InvoiceMinted({ to: paymentRecipient, tokenId: tokenId, paymentRequestId: paymentRequestId }); + } + + /*////////////////////////////////////////////////////////////////////////// + INTERNAL-METHODS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ERC721 + /// @dev Guard tokens from being transferred making them Soulbound Tokens (SBT) + function _update(address to, uint256 tokenId, address auth) internal override(ERC721) returns (address) { + address from = _ownerOf(tokenId); + if (from != address(0) && to != address(0)) { + revert("Soulbound token!"); + } + + return super._update(to, tokenId, auth); + } +} diff --git a/src/peripherals/invoice-collection/interfaces/IInvoiceCollection.sol b/src/peripherals/invoice-collection/interfaces/IInvoiceCollection.sol new file mode 100644 index 0000000..003024c --- /dev/null +++ b/src/peripherals/invoice-collection/interfaces/IInvoiceCollection.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +/// @title IInvoiceCollection +/// @notice Peripheral contract that provides functionalities to mint ERC-721 tokens representing off-chain invoices +interface IInvoiceCollection { + /*////////////////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when an invoice is created + /// @param to The address of the payment recipient of the invoice + /// @param tokenId The ID of the NFT representing the invoice + /// @param paymentRequestId The ID of the payment request associated with the invoice + event InvoiceMinted(address to, uint256 tokenId, string paymentRequestId); + + /*////////////////////////////////////////////////////////////////////////// + NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Creates an on-chain representation of an off-chain invoice by creating a payment request and minting an ERC-721 token + /// @param invoiceURI The metadata URI of the invoice + /// @param paymentRecipient The address of the payment recipient of the invoice + /// @param paymentRequestId The ID of the payment request associated with the invoice + /// @return tokenId The ID of the NFT representing the invoice + function mintInvoice( + string memory invoiceURI, + address paymentRecipient, + string memory paymentRequestId + ) + external + returns (uint256 tokenId); +} diff --git a/src/modules/invoice-module/libraries/Errors.sol b/src/peripherals/invoice-collection/libraries/Errors.sol similarity index 77% rename from src/modules/invoice-module/libraries/Errors.sol rename to src/peripherals/invoice-collection/libraries/Errors.sol index 50ed6dd..aa25fde 100644 --- a/src/modules/invoice-module/libraries/Errors.sol +++ b/src/peripherals/invoice-collection/libraries/Errors.sol @@ -2,12 +2,12 @@ pragma solidity ^0.8.26; /// @title Errors -/// @notice Library containing all custom errors the {InvoiceModule} may revert with +/// @notice Library containing all custom errors the {InvoiceCollection} may revert with library Errors { /*////////////////////////////////////////////////////////////////////////// INVOICE-MODULE //////////////////////////////////////////////////////////////////////////*/ /// @notice Thrown when the caller is unathorized to execute a call - error Unathorized(); + error Unauthorized(); } From b402594b9f04fc91f79910d3d488bba58248f538 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 18 Nov 2024 14:52:13 +0200 Subject: [PATCH 09/13] test: update tests according to new 'InvoiceCollection' peripheral --- test/integration/Integration.t.sol | 31 ++++++-- .../mint-invoice/mint-invoice.sol | 58 ++++++++++++++ .../mint-invoice/mint-invoice.tree | 8 ++ .../transfer-from/transferFrom.t.sol | 30 +++++++ .../transfer-from/transferFrom.tree | 3 + .../transfer-from/transferFrom.t.sol | 78 ------------------- .../transfer-from/transferFrom.tree | 10 --- test/integration/shared/createInvoice.t.sol | 18 +++++ test/utils/Events.sol | 10 +++ 9 files changed, 151 insertions(+), 95 deletions(-) create mode 100644 test/integration/concrete/invoice-collection/mint-invoice/mint-invoice.sol create mode 100644 test/integration/concrete/invoice-collection/mint-invoice/mint-invoice.tree create mode 100644 test/integration/concrete/invoice-collection/transfer-from/transferFrom.t.sol create mode 100644 test/integration/concrete/invoice-collection/transfer-from/transferFrom.tree delete mode 100644 test/integration/concrete/invoice-module/transfer-from/transferFrom.t.sol delete mode 100644 test/integration/concrete/invoice-module/transfer-from/transferFrom.tree create mode 100644 test/integration/shared/createInvoice.t.sol diff --git a/test/integration/Integration.t.sol b/test/integration/Integration.t.sol index 35d4a59..1e08bc8 100644 --- a/test/integration/Integration.t.sol +++ b/test/integration/Integration.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.26; import { Base_Test } from "../Base.t.sol"; import { PaymentModule } from "./../../src/modules/payment-module/PaymentModule.sol"; +import { InvoiceCollection } from "./../../src/peripherals/invoice-collection/InvoiceCollection.sol"; import { SablierV2LockupLinear } from "@sablier/v2-core/src/SablierV2LockupLinear.sol"; import { SablierV2LockupTranched } from "@sablier/v2-core/src/SablierV2LockupTranched.sol"; import { MockNFTDescriptor } from "../mocks/MockNFTDescriptor.sol"; @@ -16,6 +17,7 @@ abstract contract Integration_Test is Base_Test { //////////////////////////////////////////////////////////////////////////*/ PaymentModule internal paymentModule; + InvoiceCollection internal invoiceCollection; // Sablier V2 related test contracts MockNFTDescriptor internal mockNFTDescriptor; SablierV2LockupLinear internal sablierV2LockupLinear; @@ -30,10 +32,13 @@ abstract contract Integration_Test is Base_Test { function setUp() public virtual override { Base_Test.setUp(); - // Deploy the {PaymentModule} modul + // Deploy the {PaymentModule} module deployPaymentModule(); - // Setup the initial {PaymentModule} module to be initialized on the {Space} + // Deploy the {InvoiceCollection} module + deployInvoiceCollection(); + + // Enable the {PaymentModule} module on the {Space} contract address[] memory modules = new address[](1); modules[0] = address(paymentModule); @@ -60,6 +65,23 @@ abstract contract Integration_Test is Base_Test { /// @dev Deploys the {PaymentModule} module by initializing the Sablier v2-required contracts first function deployPaymentModule() internal { + deploySablierContracts(); + + paymentModule = new PaymentModule({ + _sablierLockupLinear: sablierV2LockupLinear, + _sablierLockupTranched: sablierV2LockupTranched, + _brokerAdmin: users.admin + }); + } + + /// @dev Deploys the {InvoiceCollection} peripheral + function deployInvoiceCollection() internal { + invoiceCollection = + new InvoiceCollection({ _relayer: users.admin, _name: "Werk Invoice NFTs", _symbol: "WERK-INVOICES" }); + } + + /// @dev Deploys the Sablier v2-required contracts + function deploySablierContracts() internal { mockNFTDescriptor = new MockNFTDescriptor(); sablierV2LockupLinear = new SablierV2LockupLinear({ initialAdmin: users.admin, initialNFTDescriptor: mockNFTDescriptor }); @@ -68,10 +90,5 @@ abstract contract Integration_Test is Base_Test { initialNFTDescriptor: mockNFTDescriptor, maxTrancheCount: 1000 }); - paymentModule = new PaymentModule({ - _sablierLockupLinear: sablierV2LockupLinear, - _sablierLockupTranched: sablierV2LockupTranched, - _brokerAdmin: users.admin - }); } } diff --git a/test/integration/concrete/invoice-collection/mint-invoice/mint-invoice.sol b/test/integration/concrete/invoice-collection/mint-invoice/mint-invoice.sol new file mode 100644 index 0000000..edc8a3a --- /dev/null +++ b/test/integration/concrete/invoice-collection/mint-invoice/mint-invoice.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import { Integration_Test } from "../../../Integration.t.sol"; +import { Errors } from "../../../../utils/Errors.sol"; +import { Events } from "../../../../utils/Events.sol"; + +contract MintInvoice_Integration_Concret_Test is Integration_Test { + function setUp() public virtual override { + Integration_Test.setUp(); + } + + function test_RevertWhen_CallerNotRelayer() external { + // Make Bob the caller in this test suite which is the authorized Relayer + vm.startPrank({ msgSender: users.bob }); + + // Expect the call to revert with the {Unauthorized} error + vm.expectRevert(Errors.Unauthorized.selector); + + // Run the test + invoiceCollection.mintInvoice({ + invoiceURI: "ipfs://QmSomeHash", + paymentRecipient: users.bob, + paymentRequestId: "1" + }); + } + + modifier whenCallerRelayer() { + // Make Admin the caller for the next test suite as they're the authorized Relayer + vm.startPrank({ msgSender: users.admin }); + + _; + } + + function test_MintInvoice() external whenCallerRelayer { + // Expect the {MintInvoice} event to be emitted + vm.expectEmit(); + emit Events.InvoiceMinted({ to: users.bob, tokenId: 1, paymentRequestId: "1" }); + + // Run the test + invoiceCollection.mintInvoice({ + invoiceURI: "ipfs://QmSomeHash", + paymentRecipient: users.bob, + paymentRequestId: "1" + }); + + // Assert the actual and expected payment request ID associated with the invoice NFT + string memory actualPaymentRequestId = invoiceCollection.tokenIdToPaymentRequestId(1); + assertEq(actualPaymentRequestId, "1"); + + // Assert the actual and expected invoice URI associated with the invoice NFT + string memory actualInvoiceURI = invoiceCollection.tokenURI(1); + assertEq(actualInvoiceURI, "ipfs://QmSomeHash"); + + // Assert the actual and expected owner of the invoice NFT + assertEq(invoiceCollection.ownerOf(1), users.bob); + } +} diff --git a/test/integration/concrete/invoice-collection/mint-invoice/mint-invoice.tree b/test/integration/concrete/invoice-collection/mint-invoice/mint-invoice.tree new file mode 100644 index 0000000..9f40db6 --- /dev/null +++ b/test/integration/concrete/invoice-collection/mint-invoice/mint-invoice.tree @@ -0,0 +1,8 @@ +mintInvoice.t.sol +├── when the caller IS NOT the authorized Relayer +│ └── it should revert with the {Unathorized} error +└── when the caller IS the authorized Relayer + ├── it should emit the {InvoiceMinted} event + ├── it should set the `paymentRequestId` for the `tokenId` + ├── it should mint the invoice NFT to the payment recipient + └── it should set the `tokenURI` for the `tokenId` \ No newline at end of file diff --git a/test/integration/concrete/invoice-collection/transfer-from/transferFrom.t.sol b/test/integration/concrete/invoice-collection/transfer-from/transferFrom.t.sol new file mode 100644 index 0000000..586c4a3 --- /dev/null +++ b/test/integration/concrete/invoice-collection/transfer-from/transferFrom.t.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import { Integration_Test } from "../../../Integration.t.sol"; + +contract TransferFrom_Integration_Concret_Test is Integration_Test { + function setUp() public virtual override { + Integration_Test.setUp(); + + // Mint an invoice NFT to Bob + vm.startPrank({ msgSender: users.admin }); + invoiceCollection.mintInvoice({ + invoiceURI: "ipfs://QmSomeHash", + paymentRecipient: users.bob, + paymentRequestId: "1" + }); + vm.stopPrank(); + } + + function test_TransferFrom() external { + // Expect the transfer to revert with the "Soulbound token!" reason + vm.expectRevert("Soulbound token!"); + + // Make Bob the caller as he's the owner of the invoice NFT + vm.startPrank({ msgSender: users.bob }); + + // Run the test + invoiceCollection.transferFrom(users.bob, users.eve, 1); + } +} diff --git a/test/integration/concrete/invoice-collection/transfer-from/transferFrom.tree b/test/integration/concrete/invoice-collection/transfer-from/transferFrom.tree new file mode 100644 index 0000000..cfb84ec --- /dev/null +++ b/test/integration/concrete/invoice-collection/transfer-from/transferFrom.tree @@ -0,0 +1,3 @@ +transferFrom.t.sol +└── when the token exists + └── it should revert with the "Soulbound token!" reason \ No newline at end of file diff --git a/test/integration/concrete/invoice-module/transfer-from/transferFrom.t.sol b/test/integration/concrete/invoice-module/transfer-from/transferFrom.t.sol deleted file mode 100644 index c055d31..0000000 --- a/test/integration/concrete/invoice-module/transfer-from/transferFrom.t.sol +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; - -import { TransferFrom_Integration_Shared_Test } from "../../../shared/transferFrom.t.sol"; -import { Errors } from "../../../../utils/Errors.sol"; -import { Types } from "./../../../../../src/modules/payment-module/libraries/Types.sol"; - -contract TransferFrom_Integration_Concret_Test is TransferFrom_Integration_Shared_Test { - function setUp() public virtual override { - TransferFrom_Integration_Shared_Test.setUp(); - } - - /* function test_RevertWhen_TokenDoesNotExist() external { - // Make Eve's space the caller which is the recipient of the payment request - vm.startPrank({ msgSender: address(space) }); - - // Expect the call to revert with the {ERC721NonexistentToken} error - vm.expectRevert(abi.encodeWithSelector(Errors.ERC721NonexistentToken.selector, 99)); - - // Run the test - invoiceModul.transferFrom({ from: address(space), to: users.eve, tokenId: 99 }); - } - - function test_TransferFrom_PaymentMethodStream() external whenTokenExists { - uint256 paymentRequestId = 4; - uint256 streamId = 1; - - // Make Bob the payer for the payment request - vm.startPrank({ msgSender: users.bob }); - - // Approve the {PaymentModule} to transfer the USDT tokens on Bob's behalf - usdt.approve({ spender: address(paymentModule), amount: paymentRequests[paymentRequestId].payment.amount }); - - // Pay the payment request - paymentModule.payRequest{ value: paymentRequests[paymentRequestId].payment.amount }({ requestId: paymentRequestId }); - - // Simulate the passage of time so that the maximum withdrawable amount is non-zero - vm.warp(block.timestamp + 5 weeks); - - // Store Eve's space balance before withdrawing the USDT tokens - uint256 balanceOfBefore = usdt.balanceOf(address(space)); - - // Get the maximum withdrawable amount from the stream before transferring the stream NFT - uint128 maxWithdrawableAmount = - paymentModule.withdrawableAmountOf({ streamType: Types.Method.LinearStream, streamId: streamId }); - - // Make Eve's space the caller which is the recipient of the payment request - vm.startPrank({ msgSender: address(space) }); - - // Approve the {PaymentModule} to transfer the `streamId` stream on behalf of the Eve's space - sablierV2LockupLinear.approve({ to: address(paymentModule), tokenId: streamId }); - - // Run the test - paymentModule.transferFrom({ from: address(space), to: users.eve, tokenrequestId: paymentRequestId }); - - // Assert the current and expected Eve's space USDT balance - assertEq(balanceOfBefore + maxWithdrawableAmount, usdt.balanceOf(address(space))); - - // Assert the current and expected owner of the payment request NFT - assertEq(paymentModule.ownerOf({ tokenrequestId: paymentRequestId }), users.eve); - - // Assert the current and expected owner of the payment request stream NFT - assertEq(sablierV2LockupLinear.ownerOf({ tokenId: streamId }), users.eve); - } - - function test_TransferFrom_PaymentTransfer() external whenTokenExists { - uint256 paymentRequestId = 1; - - // Make Eve's space the caller which is the recipient of the payment request - vm.startPrank({ msgSender: address(space) }); - - // Run the test - paymentModule.transferFrom({ from: address(space), to: users.eve, tokenrequestId: paymentRequestId }); - - // Assert the current and expected owner of the payment request NFT - assertEq(paymentModule.ownerOf({ tokenrequestId: paymentRequestId }), users.eve); - } */ -} diff --git a/test/integration/concrete/invoice-module/transfer-from/transferFrom.tree b/test/integration/concrete/invoice-module/transfer-from/transferFrom.tree deleted file mode 100644 index 76d1a46..0000000 --- a/test/integration/concrete/invoice-module/transfer-from/transferFrom.tree +++ /dev/null @@ -1,10 +0,0 @@ -transferFrom.t.sol -├── when the token does not exist -│ └── it should revert with the {ERC721NonexistentToken} error -└── when the token exist - ├── when the payment method is stream-based - │ ├── it should withdraw the maximum withdrawable amount of the Sablier stream - │ ├── it should transfer the Sablier stream NFT - │ └── it should transfer the payment request NFT - └── when the payment is transfer-based - └── it should transfer the payment request NFT \ No newline at end of file diff --git a/test/integration/shared/createInvoice.t.sol b/test/integration/shared/createInvoice.t.sol new file mode 100644 index 0000000..a91bb2b --- /dev/null +++ b/test/integration/shared/createInvoice.t.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import { Integration_Test } from "../Integration.t.sol"; + +abstract contract CreateInvoice_Integration_Shared_Test is Integration_Test { + function setUp() public virtual override { + Integration_Test.setUp(); + } + + modifier whenCallerContract() { + _; + } + + modifier whenCompliantSpace() { + _; + } +} diff --git a/test/utils/Events.sol b/test/utils/Events.sol index 1a6457c..fcea57a 100644 --- a/test/utils/Events.sol +++ b/test/utils/Events.sol @@ -142,4 +142,14 @@ abstract contract Events { /// @param owner The address of the {ModuleKeeper} owner /// @param module The address of the module to be removed event ModuleRemovedFromAllowlist(address indexed owner, address indexed module); + + /*////////////////////////////////////////////////////////////////////////// + INVOICE-COLLECTION + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when an invoice is created + /// @param to The address of the payment recipient of the invoice + /// @param tokenId The ID of the NFT representing the invoice + /// @param paymentRequestId The ID of the payment request associated with the invoice + event InvoiceMinted(address to, uint256 tokenId, string paymentRequestId); } From a88791cbc71598ed980fd2b617cb1d1c2f58eecd Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 18 Nov 2024 14:56:47 +0200 Subject: [PATCH 10/13] chore: update deployment scripts --- script/Base.s.sol | 2 +- script/DeployDeterministicInvoiceModule.s.sol | 32 ------------------- script/DeployDeterministicModuleKeeper.s.sol | 7 +++- .../DeployDeterministicStationRegistry.s.sol | 7 +++- script/DeployInvoiceCollection.s.sol | 25 +++++++++++++++ script/DeploySpace.sol | 7 +++- 6 files changed, 44 insertions(+), 36 deletions(-) delete mode 100644 script/DeployDeterministicInvoiceModule.s.sol create mode 100644 script/DeployInvoiceCollection.s.sol diff --git a/script/Base.s.sol b/script/Base.s.sol index 7278470..7c6a7e8 100644 --- a/script/Base.s.sol +++ b/script/Base.s.sol @@ -19,7 +19,7 @@ contract BaseScript is Script { deployer = from; } else { mnemonic = vm.envOr({ name: "MNEMONIC", defaultValue: TEST_MNEMONIC }); - (deployer, ) = deriveRememberKey(mnemonic, 0); + (deployer,) = deriveRememberKey(mnemonic, 0); } } diff --git a/script/DeployDeterministicInvoiceModule.s.sol b/script/DeployDeterministicInvoiceModule.s.sol deleted file mode 100644 index f095493..0000000 --- a/script/DeployDeterministicInvoiceModule.s.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.26; - -import { BaseScript } from "./Base.s.sol"; -import { InvoiceModule } from "../src/modules/invoice-module/InvoiceModule.sol"; -import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; -import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; - -/// @notice Deploys and initializes the {InvoiceModule} contract at deterministic addresses across chains -/// @dev Reverts if any contract has already been deployed -contract DeployDeterministicInvoiceModule is BaseScript { - /// @dev By using a salt, Forge will deploy the contract via a deterministic CREATE2 factory - /// https://book.getfoundry.sh/tutorials/create2-tutorial?highlight=deter#deterministic-deployment-using-create2 - function run( - string memory create2Salt, - ISablierV2LockupLinear sablierLockupLinear, - ISablierV2LockupTranched sablierLockupTranched, - address brokerAdmin, - string memory baseURI - ) - public - virtual - broadcast - returns (InvoiceModule invoiceModule) - { - bytes32 salt = bytes32(abi.encodePacked(create2Salt)); - - // Deterministically deploy the {InvoiceModule} contract - invoiceModule = - new InvoiceModule{ salt: salt }(sablierLockupLinear, sablierLockupTranched, brokerAdmin, baseURI); - } -} diff --git a/script/DeployDeterministicModuleKeeper.s.sol b/script/DeployDeterministicModuleKeeper.s.sol index 55a3e1c..bc176e9 100644 --- a/script/DeployDeterministicModuleKeeper.s.sol +++ b/script/DeployDeterministicModuleKeeper.s.sol @@ -12,7 +12,12 @@ contract DeployDeterministicModuleKeeper is BaseScript { function run( string memory create2Salt, address initialOwner - ) public virtual broadcast returns (ModuleKeeper moduleKeeper) { + ) + public + virtual + broadcast + returns (ModuleKeeper moduleKeeper) + { bytes32 salt = bytes32(abi.encodePacked(create2Salt)); // Deterministically deploy the {ModuleKeeper} contract diff --git a/script/DeployDeterministicStationRegistry.s.sol b/script/DeployDeterministicStationRegistry.s.sol index 07f1b59..ad23539 100644 --- a/script/DeployDeterministicStationRegistry.s.sol +++ b/script/DeployDeterministicStationRegistry.s.sol @@ -16,7 +16,12 @@ contract DeployDeterministicStationRegistry is BaseScript { address initialAdmin, IEntryPoint entrypoint, ModuleKeeper moduleKeeper - ) public virtual broadcast returns (StationRegistry stationRegistry) { + ) + public + virtual + broadcast + returns (StationRegistry stationRegistry) + { bytes32 salt = bytes32(abi.encodePacked(create2Salt)); // Deterministically deploy the {StationRegistry} smart account factory diff --git a/script/DeployInvoiceCollection.s.sol b/script/DeployInvoiceCollection.s.sol new file mode 100644 index 0000000..b001fdb --- /dev/null +++ b/script/DeployInvoiceCollection.s.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +import { BaseScript } from "./Base.s.sol"; +import { InvoiceCollection } from "../src/peripherals/invoice-collection/InvoiceCollection.sol"; + +/// @notice Deploys and initializes the {InvoiceCollection} contract at deterministic addresses across chains +/// @dev Reverts if any contract has already been deployed +contract DeployInvoiceCollection is BaseScript { + /// @dev By using a salt, Forge will deploy the contract via a deterministic CREATE2 factory + /// https://book.getfoundry.sh/tutorials/create2-tutorial?highlight=deter#deterministic-deployment-using-create2 + function run( + address relayer, + string memory name, + string memory symbol + ) + public + virtual + broadcast + returns (InvoiceCollection invoiceCollection) + { + // Deploy the {InvoiceCollection} contract + invoiceCollection = new InvoiceCollection(relayer, name, symbol); + } +} diff --git a/script/DeploySpace.sol b/script/DeploySpace.sol index 80a3cc8..ac33114 100644 --- a/script/DeploySpace.sol +++ b/script/DeploySpace.sol @@ -12,7 +12,12 @@ contract DeploySpace is BaseScript { StationRegistry stationRegistry, uint256 stationId, address[] memory initialModules - ) public virtual broadcast returns (Space space) { + ) + public + virtual + broadcast + returns (Space space) + { // Get the number of total accounts created by the `initialAdmin` deployer uint256 totalAccountsOfAdmin = stationRegistry.totalAccountsOfSigner(initialAdmin); From 24e47e75f3a5e866b38735e8215586e7de534171 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 18 Nov 2024 14:57:23 +0200 Subject: [PATCH 11/13] chore(payment-module): change 'createRequest' visibility to public --- src/modules/payment-module/PaymentModule.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/payment-module/PaymentModule.sol b/src/modules/payment-module/PaymentModule.sol index 2670b55..db0f5b0 100644 --- a/src/modules/payment-module/PaymentModule.sol +++ b/src/modules/payment-module/PaymentModule.sol @@ -83,7 +83,7 @@ contract PaymentModule is IPaymentModule, StreamManager { //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc IPaymentModule - function createRequest(Types.PaymentRequest calldata request) external onlySpace returns (uint256 requestId) { + function createRequest(Types.PaymentRequest calldata request) public onlySpace returns (uint256 requestId) { // Checks: the recipient address is not the zero address if (request.recipient == address(0)) { revert Errors.InvalidZeroAddressRecipient(); From 963c6241cbc5f44c0caca3f55a7079a3d443beb5 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 18 Nov 2024 14:58:14 +0200 Subject: [PATCH 12/13] chore: update Makefile --- Makefile | 52 ++++++++++++---------------------------------------- 1 file changed, 12 insertions(+), 40 deletions(-) diff --git a/Makefile b/Makefile index dd7c8ae..05d321c 100644 --- a/Makefile +++ b/Makefile @@ -13,48 +13,20 @@ clean :; forge clean # See https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/solidity/coverage.sh tests-coverage :; ./script/coverage.sh -# Deploy the {InvoiceModule} contract deterministically -# See Sablier V2 deployments: https://docs.sablier.com/contracts/v2/deployments +# Deploys the {InvoiceCollection} peripheral # # Update the following configs before running the script: -# - {SABLIER_LOCKUP_LINEAR} with the according {SablierV2LockupLinear} deployment address -# - {SABLIER_LOCKUP_TRANCHED} with the according {SablierV2LockupTranched} deployment address -# - {BROKER_ADMIN} with the address of the account managing the Sablier V2 integration fee +# - {RELAYER} with the address of the Relayer responsible to mint the invoice NFTs +# - {NAME} with the name of the ERC-721 {InvoiceCollection} contract +# - {SYMBOL} with symbol of the ERC-721 {InvoiceCollection} contract # - {RPC_URL} with the network RPC used for deployment -deploy-deterministic-invoice-module: - forge script script/DeployDeterministicInvoiceModule.s.sol:DeployDeterministicInvoiceModule \ - $(CREATE2SALT) {SABLIER_LOCKUP_LINEAR} {SABLIER_LOCKUP_TRANCHED} {BROKER_ADMIN} \ - --sig "run(string,address,address,address)" --rpc-url {RPC_URL} --private-key $(PRIVATE_KEY) --etherscan-api-key $(ETHERSCAN_API_KEY) +deploy-invoice-collection: + forge script script/DeployInvoiceCollection.s.sol:DeployInvoiceCollection \ + $(CREATE2SALT) {RELAYER} {NAME} {SYMBOL} \ + --sig "run(address,string,string)" --rpc-url {RPC_URL} --private-key $(PRIVATE_KEY) --etherscan-api-key $(ETHERSCAN_API_KEY) --broadcast --verify - -# Deploy a {Container} contract deterministically -# Update the following configs before running the script: -# - {INITIAL_OWNER} with the address of the initial owner -# - {MODULE_KEEPER_ADDRESS} with the address of the {ModuleKeeper} deployment -# - {RPC_URL} with the network RPC used for deployment -deploy-deterministic-container: - forge script script/DeployDeterministicContainer.s.sol:DeployDeterministicContainer \ - $(CREATE2SALT) {INITIAL_OWNER} {MODULE_KEEPER_ADDRESS} [] \ - --sig "run(string,address,address,address[])" --rpc-url {RPC_URL} \ - --private-key $(PRIVATE_KEY) --etherscan-api-key $(ETHERSCAN_API_KEY) \ - --broadcast --verify - -# Deploy a {Container} contract -# Update the following configs before running the script: -# - {INITIAL_OWNER} with the address of the initial owner -# - {DOCK_REGISTRY} with the address of the {DockRegistr} factory -# - {DOCK_ID} with the ID of the dock to which the new {Container} will be deployed -# - {INITIAL_MODULES} with the addresses of the enabled initial modules (array) -# - {RPC_URL} with the network RPC used for deployment -deploy-container: - forge script script/DeployContainer.s.sol:DeployContainer \ - {INITIAL_OWNER} {DOCK_REGISTRY} {DOCK_ID} {INITIAL_MODULES} \ - --sig "run(address,address,uint256,address[])" --rpc-url {RPC_URL} \ - --private-key $(PRIVATE_KEY) --etherscan-api-key $(ETHERSCAN_API_KEY) \ - --broadcast --verify - -# Deploy the {ModuleKeeper} contract deterministically +# Deploys the {ModuleKeeper} contract deterministically # Update the following configs before running the script: # - {INITIAL_OWNER} with the address of the initial owner # - {RPC_URL} with the network RPC used for deployment @@ -65,14 +37,14 @@ deploy-deterministic-module-keeper: --private-key $(PRIVATE_KEY) --etherscan-api-key $(ETHERSCAN_API_KEY) \ --broadcast --verify -# Deploy the {DockRegistry} contract deterministically +# Deploys the {StationRegistry} contract deterministically # Update the following configs before running the script: # - {INITIAL_OWNER} with the address of the initial owner -# - {MODULE_KEEPER} with the address of the {ModuleKeeper} deployment # - {ENTRYPOINT} with the address of the {Entrypoiny} contract (currently v6) +# - {MODULE_KEEPER} with the address of the {ModuleKeeper} deployment # - {RPC_URL} with the network RPC used for deployment deploy-deterministic-dock-registry: - forge script script/DeployDeterministicDockRegistry.s.sol:DeployDeterministicDockRegistry \ + forge script script/DeployDeterministicStationRegistry.s.sol:DeployDeterministicStationRegistry \ $(CREATE2SALT) {INITIAL_OWNER} {ENTRYPOINT} {MODULE_KEEPER} \ --sig "run(string,address,address)" --rpc-url {RPC_URL} \ --private-key $(PRIVATE_KEY) --etherscan-api-key $(ETHERSCAN_API_KEY) \ From cd078de2214d03b9763b009e3e4ebb52ab458b80 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 18 Nov 2024 14:58:48 +0200 Subject: [PATCH 13/13] chore: update gas snapshot --- .gas-snapshot | 147 +++++++++++++++++++++++++------------------------- 1 file changed, 72 insertions(+), 75 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index ef05e89..c3c02cd 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,98 +1,95 @@ -AddToAllowlist_Unit_Concrete_Test:test_AddToAllowlist() (gas: 294199) -AddToAllowlist_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 12955) -AddToAllowlist_Unit_Concrete_Test:test_RevertWhen_InvalidZeroCodeModule() (gas: 13123) -CancelInvoice_Integration_Concret_Test:test_CancelInvoice_PaymentMethodLinearStream_StatusOngoing() (gas: 448598) -CancelInvoice_Integration_Concret_Test:test_CancelInvoice_PaymentMethodLinearStream_StatusPending() (gas: 32437) -CancelInvoice_Integration_Concret_Test:test_CancelInvoice_PaymentMethodTranchedStream_StatusOngoing() (gas: 448577) -CancelInvoice_Integration_Concret_Test:test_CancelInvoice_PaymentMethodTranchedStream_StatusPending() (gas: 32484) -CancelInvoice_Integration_Concret_Test:test_CancelInvoice_PaymentMethodTransfer() (gas: 32470) -CancelInvoice_Integration_Concret_Test:test_RevertWhen_InvoiceIsCanceled() (gas: 29284) -CancelInvoice_Integration_Concret_Test:test_RevertWhen_InvoiceIsPaid() (gas: 55920) -CancelInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodLinearStream_StatusOngoing_SenderNoInitialtStreamSender() (gas: 406709) -CancelInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodLinearStream_StatusPending_SenderNotInvoiceRecipient() (gas: 22770) -CancelInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTranchedStream_StatusOngoing_SenderNoInitialtStreamSender() (gas: 406752) -CancelInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTranchedStream_StatusPending_SenderNotInvoiceRecipient() (gas: 22815) -CancelInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTransfer_SenderNotInvoiceRecipient() (gas: 22803) +AddToAllowlist_Unit_Concrete_Test:test_AddToAllowlist() (gas: 294177) +AddToAllowlist_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 13021) +AddToAllowlist_Unit_Concrete_Test:test_RevertWhen_InvalidZeroCodeModule() (gas: 13101) +CancelRequest_Integration_Concret_Test:test_CancelRequest_PaymentMethodLinearStream_StatusCanceled() (gas: 30130) +CancelRequest_Integration_Concret_Test:test_CancelRequest_PaymentMethodLinearStream_StatusPending() (gas: 456362) +CancelRequest_Integration_Concret_Test:test_CancelRequest_PaymentMethodTranchedStream_StatusCanceled() (gas: 30108) +CancelRequest_Integration_Concret_Test:test_CancelRequest_PaymentMethodTranchedStream_StatusPending() (gas: 455894) +CancelRequest_Integration_Concret_Test:test_CancelRequest_PaymentMethodTransfer() (gas: 30162) +CancelRequest_Integration_Concret_Test:test_RevertWhen_PaymentIsPaid() (gas: 55905) +CancelRequest_Integration_Concret_Test:test_RevertWhen_PaymentMethodLinearStream_StatusPending_SenderNoInitialtStreamSender() (gas: 412484) +CancelRequest_Integration_Concret_Test:test_RevertWhen_PaymentMethodLinearStream_StatusPending_SenderNotPaymentRecipient() (gas: 21892) +CancelRequest_Integration_Concret_Test:test_RevertWhen_PaymentMethodTranchedStream_StatusPending_SenderNoInitialtStreamSender() (gas: 412508) +CancelRequest_Integration_Concret_Test:test_RevertWhen_PaymentMethodTranchedStream_StatusPending_SenderNotPaymentRecipient() (gas: 21849) +CancelRequest_Integration_Concret_Test:test_RevertWhen_PaymentMethodTransfer_SenderNotPaymentRecipient() (gas: 21859) +CancelRequest_Integration_Concret_Test:test_RevertWhen_RequestCanceled() (gas: 29836) ComputeNumberOfPayments_Helpers_Test:test_ComputeNumberOfPayments_Monthly() (gas: 3530) ComputeNumberOfPayments_Helpers_Test:test_ComputeNumberOfPayments_Weekly() (gas: 3483) -ComputeNumberOfPayments_Helpers_Test:test_ComputeNumberOfPayments_Yearly() (gas: 3613) -Constructor_DockRegistry_Test:test_Constructor() (gas: 6152415) +ComputeNumberOfPayments_Helpers_Test:test_ComputeNumberOfPayments_Yearly() (gas: 3591) Constructor_ModuleKeeper_Test:test_Constructor() (gas: 241794) -Constructor_StreamManager_Integration_Concret_Test:test_Constructor() (gas: 1506380) -CreateAccount_Unit_Concrete_Test:test_CreateAccount_DockIdNonZero() (gas: 884365) -CreateAccount_Unit_Concrete_Test:test_CreateAccount_DockIdZero() (gas: 495512) -CreateAccount_Unit_Concrete_Test:test_RevertWhen_CallerNotDockOwner() (gas: 516778) -CreateInvoice_Integration_Concret_Test:test_CreateInvoice_LinearStream() (gas: 257050) -CreateInvoice_Integration_Concret_Test:test_CreateInvoice_PaymentMethodOneOffTransfer() (gas: 257350) -CreateInvoice_Integration_Concret_Test:test_CreateInvoice_RecurringTransfer() (gas: 258474) -CreateInvoice_Integration_Concret_Test:test_CreateInvoice_Tranched() (gas: 258702) -CreateInvoice_Integration_Concret_Test:test_RevertWhen_CallerNotContract() (gas: 87317) -CreateInvoice_Integration_Concret_Test:test_RevertWhen_EndTimeInThePast() (gas: 104070) -CreateInvoice_Integration_Concret_Test:test_RevertWhen_NonCompliantContainer() (gas: 92474) -CreateInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodLinearStream_PaymentAssetNativeToken() (gas: 104125) -CreateInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodRecurringTransfer_PaymentIntervalTooShortForSelectedRecurrence() (gas: 104863) -CreateInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTranchedStream_PaymentAssetNativeToken() (gas: 105365) -CreateInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTranchedStream_PaymentIntervalTooShortForSelectedRecurrence() (gas: 104907) -CreateInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTranchedStream_RecurrenceSetToOneOff() (gas: 103551) -CreateInvoice_Integration_Concret_Test:test_RevertWhen_StartTimeGreaterThanEndTime() (gas: 103394) -CreateInvoice_Integration_Concret_Test:test_RevertWhen_ZeroPaymentAmount() (gas: 82787) -CreateInvoice_Integration_Fuzz_Test:testFuzz_CreateInvoice(uint8,uint8,uint40,uint40,uint128) (runs: 10003, μ: 200780, ~: 257141) -DisableModule_Unit_Concrete_Test:test_DisableModule() (gas: 297575) +Constructor_StationRegistry_Test:test_Constructor() (gas: 6000889) +Constructor_StreamManager_Integration_Concret_Test:test_Constructor() (gas: 1483582) +CreateAccount_Unit_Concrete_Test:test_CreateAccount_StationIdNonZero() (gas: 838312) +CreateAccount_Unit_Concrete_Test:test_CreateAccount_StationIdZero() (gas: 471586) +CreateAccount_Unit_Concrete_Test:test_RevertWhen_CallerNotStationOwner() (gas: 494552) +CreateRequest_Integration_Concret_Test:test_CreateRequest_LinearStream() (gas: 213120) +CreateRequest_Integration_Concret_Test:test_CreateRequest_PaymentMethodOneOffTransfer() (gas: 213582) +CreateRequest_Integration_Concret_Test:test_CreateRequest_RecurringTransfer() (gas: 214358) +CreateRequest_Integration_Concret_Test:test_CreateRequest_Tranched() (gas: 214732) +CreateRequest_Integration_Concret_Test:test_RevertWhen_CallerNotContract() (gas: 87433) +CreateRequest_Integration_Concret_Test:test_RevertWhen_EndTimeInThePast() (gas: 104518) +CreateRequest_Integration_Concret_Test:test_RevertWhen_NonCompliantSpace() (gas: 94507) +CreateRequest_Integration_Concret_Test:test_RevertWhen_PaymentMethodLinearStream_PaymentAssetNativeToken() (gas: 104640) +CreateRequest_Integration_Concret_Test:test_RevertWhen_PaymentMethodRecurringTransfer_PaymentIntervalTooShortForSelectedRecurrence() (gas: 105215) +CreateRequest_Integration_Concret_Test:test_RevertWhen_PaymentMethodTranchedStream_PaymentAssetNativeToken() (gas: 105883) +CreateRequest_Integration_Concret_Test:test_RevertWhen_PaymentMethodTranchedStream_PaymentIntervalTooShortForSelectedRecurrence() (gas: 105401) +CreateRequest_Integration_Concret_Test:test_RevertWhen_PaymentMethodTranchedStream_RecurrenceSetToOneOff() (gas: 104096) +CreateRequest_Integration_Concret_Test:test_RevertWhen_StartTimeGreaterThanEndTime() (gas: 103921) +CreateRequest_Integration_Concret_Test:test_RevertWhen_ZeroPaymentAmount() (gas: 83212) +CreateRequest_Integration_Fuzz_Test:testFuzz_CreateRequest(uint8,uint8,address,uint40,uint40,uint128) (runs: 10002, μ: 167414, ~: 213538) +DisableModule_Unit_Concrete_Test:test_DisableModule() (gas: 297619) DisableModule_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 18242) -EnableModule_Unit_Concrete_Test:test_EnableModule() (gas: 38285) -EnableModule_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 18232) -EnableModule_Unit_Concrete_Test:test_RevertWhen_ModuleNotAllowlisted() (gas: 28781) +EnableModule_Unit_Concrete_Test:test_EnableModule() (gas: 38307) +EnableModule_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 18210) +EnableModule_Unit_Concrete_Test:test_RevertWhen_ModuleNotAllowlisted() (gas: 28825) ExecuteBatch_Unit_Concrete_Test:test_ExecuteBatch() (gas: 310024) ExecuteBatch_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 32567) ExecuteBatch_Unit_Concrete_Test:test_RevertWhen_ModuleNotEnabled() (gas: 38738) -ExecuteBatch_Unit_Concrete_Test:test_RevertWhen_WrongArrayLengths() (gas: 57866) +ExecuteBatch_Unit_Concrete_Test:test_RevertWhen_WrongArrayLengths() (gas: 57822) Execute_Unit_Concrete_Test:test_Execute() (gas: 83058) -Execute_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 20690) -Execute_Unit_Concrete_Test:test_RevertWhen_ModuleNotEnabled() (gas: 21172) +Execute_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 20659) +Execute_Unit_Concrete_Test:test_RevertWhen_ModuleNotEnabled() (gas: 21166) Fallback_Unit_Concrete_Test:test_Fallback() (gas: 23595) -PayInvoice_Integration_Concret_Test:test_PayInvoice_PaymentMethodLinearStream() (gas: 311650) -PayInvoice_Integration_Concret_Test:test_PayInvoice_PaymentMethodTranchedStream() (gas: 438747) -PayInvoice_Integration_Concret_Test:test_PayInvoice_PaymentMethodTransfer_ERC20Token_Recurring() (gas: 106027) -PayInvoice_Integration_Concret_Test:test_PayInvoice_PaymentMethodTransfer_NativeToken_OneOff() (gas: 69528) -PayInvoice_Integration_Concret_Test:test_RevertWhen_InvoiceAlreadyPaid() (gas: 81929) -PayInvoice_Integration_Concret_Test:test_RevertWhen_InvoiceCanceled() (gas: 32071) -PayInvoice_Integration_Concret_Test:test_RevertWhen_InvoiceNull() (gas: 20514) -PayInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTransfer_NativeTokenTransferFails() (gas: 185912) -PayInvoice_Integration_Concret_Test:test_RevertWhen_PaymentMethodTransfer_PaymentAmountLessThanInvoiceValue() (gas: 34136) -PayInvoice_Integration_Fuzz_Test:testFuzz_PayInvoice(uint8,uint8,uint40,uint40,uint128) (runs: 10003, μ: 365189, ~: 342220) +MintInvoice_Integration_Concret_Test:test_MintInvoice() (gas: 127976) +MintInvoice_Integration_Concret_Test:test_RevertWhen_CallerNotRelayer() (gas: 14112) +PayPayment_Integration_Concret_Test:test_PayRequest_PaymentMethodLinearStream() (gas: 317023) +PayPayment_Integration_Concret_Test:test_PayRequest_PaymentMethodTranchedStream() (gas: 445445) +PayPayment_Integration_Concret_Test:test_PayRequest_PaymentMethodTransfer_ERC20Token_Recurring() (gas: 106179) +PayPayment_Integration_Concret_Test:test_PayRequest_PaymentMethodTransfer_NativeToken_OneOff() (gas: 70456) +PayPayment_Integration_Concret_Test:test_RevertWhen_PaymentMethodTransfer_NativeTokenTransferFails() (gas: 146017) +PayPayment_Integration_Concret_Test:test_RevertWhen_PaymentMethodTransfer_PaymentAmountLessThanRequestedAmount() (gas: 31822) +PayPayment_Integration_Concret_Test:test_RevertWhen_RequestAlreadyPaid() (gas: 81025) +PayPayment_Integration_Concret_Test:test_RevertWhen_RequestCanceled() (gas: 32409) +PayPayment_Integration_Concret_Test:test_RevertWhen_RequestNull() (gas: 17722) +PayRequest_Integration_Fuzz_Test:testFuzz_PayRequest(uint8,uint8,uint40,uint40,uint128) (runs: 10001, μ: 348425, ~: 320864) Receive_Unit_Concrete_Test:test_Receive() (gas: 23390) RemoveFromAllowlist_Unit_Concrete_Test:test_AddToAllowlist() (gas: 22211) RemoveFromAllowlist_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 12982) -TransferContainerOwnership_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 509061) -TransferContainerOwnership_Unit_Concrete_Test:test_RevertWhen_InvalidOwnerZeroAddress() (gas: 507020) -TransferContainerOwnership_Unit_Concrete_Test:test_TransferContainerOwnership() (gas: 515004) -TransferDockOwnership_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 508825) -TransferDockOwnership_Unit_Concrete_Test:test_TransferDockOwnership() (gas: 514407) -TransferFrom_Integration_Concret_Test:test_RevertWhen_TokenDoesNotExist() (gas: 30340) -TransferFrom_Integration_Concret_Test:test_TransferFrom_PaymentMethodStream() (gas: 393146) -TransferFrom_Integration_Concret_Test:test_TransferFrom_PaymentTransfer() (gas: 61592) +TransferFrom_Integration_Concret_Test:test_TransferFrom() (gas: 15639) TransferOwnership_Unit_Concrete_Test:test_RevertWhen_CallerNotCurrentOwner() (gas: 15031) -TransferOwnership_Unit_Concrete_Test:test_RevertWhen_NewOwnerZeroAddress() (gas: 12972) +TransferOwnership_Unit_Concrete_Test:test_RevertWhen_NewOwnerZeroAddress() (gas: 12950) TransferOwnership_Unit_Concrete_Test:test_TransferOwnership() (gas: 22589) -TransferOwnership_Unit_Fuzz_Test:testFuzz_RevertWhen_CallerNotCurrentOwner(address) (runs: 10003, μ: 13560, ~: 13560) -TransferOwnership_Unit_Fuzz_Test:testFuzz_TransferOwnership(address) (runs: 10003, μ: 20802, ~: 20803) +TransferOwnership_Unit_Fuzz_Test:testFuzz_RevertWhen_CallerNotCurrentOwner(address) (runs: 10002, μ: 13560, ~: 13560) +TransferOwnership_Unit_Fuzz_Test:testFuzz_TransferOwnership(address) (runs: 10002, μ: 20803, ~: 20803) +TransferStationOwnership_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 486702) +TransferStationOwnership_Unit_Concrete_Test:test_TransferStationOwnership() (gas: 492259) UpdateModuleKeeper_Unit_Concrete_Test:test_RevertWhen_CallerNotRegistryOwner() (gas: 13994) -UpdateModuleKeeper_Unit_Concrete_Test:test_UpdateModuleKeeper() (gas: 21767) +UpdateModuleKeeper_Unit_Concrete_Test:test_UpdateModuleKeeper() (gas: 21811) UpdateStreamBrokerFee_Integration_Concret_Test:test_RevertWhen_CallerNotOwner() (gas: 12865) -UpdateStreamBrokerFee_Integration_Concret_Test:test_UpdateStreamBrokerFee() (gas: 38892) +UpdateStreamBrokerFee_Integration_Concret_Test:test_UpdateStreamBrokerFee() (gas: 38936) WithdrawERC1155_Unit_Concrete_Test:test_RevertWhen_CallerNotAdminOrEntryPoint() (gas: 32720) WithdrawERC1155_Unit_Concrete_Test:test_RevertWhen_InsufficientERC1155Balance() (gas: 44674) WithdrawERC1155_Unit_Concrete_Test:test_WithdrawERC1155() (gas: 117528) WithdrawERC1155_Unit_Concrete_Test:test_WithdrawERC1155_Batch() (gas: 136425) -WithdrawERC20_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 18242) -WithdrawERC20_Unit_Concrete_Test:test_RevertWhen_InsufficientERC20ToWithdraw() (gas: 25726) -WithdrawERC20_Unit_Concrete_Test:test_WithdrawERC20() (gas: 92210) -WithdrawERC721_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 18247) -WithdrawERC721_Unit_Concrete_Test:test_RevertWhen_NonexistentERC721Token() (gas: 31261) -WithdrawERC721_Unit_Concrete_Test:test_WithdrawERC721() (gas: 96612) -WithdrawLinearStream_Integration_Concret_Test:test_WithdrawStream_LinearStream() (gas: 319541) -WithdrawLinearStream_Integration_Concret_Test:test_WithdrawStream_TranchedStream() (gas: 443734) +WithdrawERC20_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 18302) +WithdrawERC20_Unit_Concrete_Test:test_RevertWhen_InsufficientERC20ToWithdraw() (gas: 25695) +WithdrawERC20_Unit_Concrete_Test:test_WithdrawERC20() (gas: 91444) +WithdrawERC721_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 18307) +WithdrawERC721_Unit_Concrete_Test:test_RevertWhen_NonexistentERC721Token() (gas: 31230) +WithdrawERC721_Unit_Concrete_Test:test_WithdrawERC721() (gas: 96680) WithdrawNative_Unit_Concrete_Test:test_RevertWhen_CallerNotOwner() (gas: 18144) WithdrawNative_Unit_Concrete_Test:test_RevertWhen_InsufficientNativeToWithdraw() (gas: 17974) WithdrawNative_Unit_Concrete_Test:test_RevertWhen_NativeWithdrawFailed() (gas: 33626) -WithdrawNative_Unit_Concrete_Test:test_WithdrawNative() (gas: 39195) \ No newline at end of file +WithdrawNative_Unit_Concrete_Test:test_WithdrawNative() (gas: 39195) +WithdrawRequestStream_Integration_Concret_Test:test_WithdrawStream_LinearStream() (gas: 315583) +WithdrawRequestStream_Integration_Concret_Test:test_WithdrawStream_TranchedStream() (gas: 438362) \ No newline at end of file