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 e69de29b..01a102c2 100644 --- a/test/integration/concrete/invoice-module/create-invoice/createInvoice.t.sol +++ b/test/integration/concrete/invoice-module/create-invoice/createInvoice.t.sol @@ -0,0 +1,507 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { InvoiceModule_Integration_Shared_Test } from "../../../shared/InvoiceModule.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 InvoiceModule_Integration_Shared_Test { + function setUp() public virtual override { + InvoiceModule_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 {ContainerZeroCodeSize} error + vm.expectRevert(Errors.ContainerZeroCodeSize.selector); + + // Create an one-off transfer invoice + createInvoiceWithOneOffTransfer(); + + // Run the test + invoiceModule.createInvoice(invoice); + } + + function test_RevertWhen_NonCompliantContainer() external whenCallerContract { + // Make Eve the caller in this test suite as she's the owner of the {Container} contract + vm.startPrank({ msgSender: users.eve }); + + // Create an one-off transfer invoice + createInvoiceWithOneOffTransfer(); + + // Create the calldata for the Invoice Module execution + bytes memory data = abi.encodeWithSignature( + "createInvoice((address,uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice + ); + + // Expect the call to revert with the {ContainerUnsupportedInterface} error + vm.expectRevert(Errors.ContainerUnsupportedInterface.selector); + + // Run the test + mockNonCompliantContainer.execute({ module: address(invoiceModule), value: 0, data: data }); + } + + function test_RevertWhen_ZeroPaymentAmount() external whenCallerContract whenCompliantContainer { + // Make Eve the caller in this test suite as she's the owner of the {Container} contract + vm.startPrank({ msgSender: users.eve }); + + // Create an one-off transfer invoice + createInvoiceWithOneOffTransfer(); + + // 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((address,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 + container.execute({ module: address(invoiceModule), value: 0, data: data }); + } + + function test_RevertWhen_StartTimeGreaterThanEndTime() + external + whenCallerContract + whenCompliantContainer + whenNonZeroPaymentAmount + { + // Make Eve the caller in this test suite as she's the owner of the {Container} contract + vm.startPrank({ msgSender: users.eve }); + + // Create an one-off transfer invoice + createInvoiceWithOneOffTransfer(); + + // 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((address,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 + container.execute({ module: address(invoiceModule), value: 0, data: data }); + } + + function test_RevertWhen_EndTimeInThePast() + external + whenCallerContract + whenCompliantContainer + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + { + // Make Eve the caller in this test suite as she's the owner of the {Container} contract + vm.startPrank({ msgSender: users.eve }); + + // Create an one-off transfer invoice + createInvoiceWithOneOffTransfer(); + + // Set the block.timestamp to 1641070800 + vm.warp(1641070800); + + // 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((address,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 + container.execute({ module: address(invoiceModule), value: 0, data: data }); + } + + function test_CreateInvoice_PaymentMethodOneOffTransfer() + external + whenCallerContract + whenCompliantContainer + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + givenPaymentMethodOneOffTransfer + { + // Make Eve the caller in this test suite as she's the owner of the {Container} 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 + createInvoiceWithOneOffTransfer(); + + // Create the calldata for the Invoice Module execution + bytes memory data = abi.encodeWithSignature( + "createInvoice((address,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: 0, + recipient: users.eve, + status: Types.Status.Pending, + startTime: invoice.startTime, + endTime: invoice.endTime, + payment: invoice.payment + }); + + // Expect the {Container} contract to emit a {ModuleExecutionSucceded} event + vm.expectEmit(); + emit Events.ModuleExecutionSucceded({ module: address(invoiceModule), value: 0, data: data }); + + // Run the test + container.execute({ module: address(invoiceModule), value: 0, data: data }); + + // Assert the actual and expected invoice state + Types.Invoice memory actualInvoice = invoiceModule.getInvoice({ id: 0 }); + assertEq(actualInvoice.recipient, users.eve); + 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 + whenCompliantContainer + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + givenPaymentMethodRecurringTransfer + { + // Make Eve the caller in this test suite as she's the owner of the {Container} 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 + 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((address,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 + container.execute({ module: address(invoiceModule), value: 0, data: data }); + } + + function test_CreateInvoice_RecurringTransfer() + external + whenCallerContract + whenCompliantContainer + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + givenPaymentMethodRecurringTransfer + whenPaymentIntervalLongEnough + { + // Make Eve the caller in this test suite as she's the owner of the {Container} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a recurring transfer invoice that must be paid on weekly basis + createInvoiceWithRecurringTransfer({ recurrence: Types.Recurrence.Weekly }); + + // Create the calldata for the Invoice Module execution + bytes memory data = abi.encodeWithSignature( + "createInvoice((address,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: 0, + recipient: users.eve, + status: Types.Status.Pending, + startTime: invoice.startTime, + endTime: invoice.endTime, + payment: invoice.payment + }); + + // Expect the {Container} contract to emit a {ModuleExecutionSucceded} event + vm.expectEmit(); + emit Events.ModuleExecutionSucceded({ module: address(invoiceModule), value: 0, data: data }); + + // Run the test + container.execute({ module: address(invoiceModule), value: 0, data: data }); + + // Assert the actual and expected invoice state + Types.Invoice memory actualInvoice = invoiceModule.getInvoice({ id: 0 }); + assertEq(actualInvoice.recipient, users.eve); + 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 + whenCompliantContainer + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + givenPaymentMethodTranchedStream + { + // Make Eve the caller in this test suite as she's the owner of the {Container} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a new invoice with a tranched stream payment + 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((address,uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice + ); + + // Run the test + container.execute({ module: address(invoiceModule), value: 0, data: data }); + } + + function test_RevertWhen_PaymentMethodTranchedStream_PaymentIntervalTooShortForSelectedRecurrence() + external + whenCallerContract + whenCompliantContainer + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + givenPaymentMethodTranchedStream + whenTranchedStreamWithGoodRecurring + { + // Make Eve the caller in this test suite as she's the owner of the {Container} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a new invoice with a tranched stream payment + 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((address,uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice + ); + + // Run the test + container.execute({ module: address(invoiceModule), value: 0, data: data }); + } + + function test_RevertWhen_PaymentMethodTranchedStream_PaymentAssetNativeToken() + external + whenCallerContract + whenCompliantContainer + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + givenPaymentMethodTranchedStream + whenTranchedStreamWithGoodRecurring + whenPaymentIntervalLongEnough + { + // Make Eve the caller in this test suite as she's the owner of the {Container} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a new invoice with a linear stream payment + 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((address,uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice + ); + + // Run the test + container.execute({ module: address(invoiceModule), value: 0, data: data }); + } + + function test_CreateInvoice_Tranched() + external + whenCallerContract + whenCompliantContainer + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + givenPaymentMethodTranchedStream + whenPaymentAssetNotNativeToken + { + // Make Eve the caller in this test suite as she's the owner of the {Container} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a new invoice with a tranched stream payment + createInvoiceWithTranchedStream({ recurrence: Types.Recurrence.Weekly }); + + // Create the calldata for the Invoice Module execution + bytes memory data = abi.encodeWithSignature( + "createInvoice((address,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: 0, + recipient: users.eve, + status: Types.Status.Pending, + startTime: invoice.startTime, + endTime: invoice.endTime, + payment: invoice.payment + }); + + // Expect the {Container} contract to emit a {ModuleExecutionSucceded} event + vm.expectEmit(); + emit Events.ModuleExecutionSucceded({ module: address(invoiceModule), value: 0, data: data }); + + // Run the test + container.execute({ module: address(invoiceModule), value: 0, data: data }); + + // Assert the actual and expected invoice state + Types.Invoice memory actualInvoice = invoiceModule.getInvoice({ id: 0 }); + assertEq(actualInvoice.recipient, users.eve); + 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, 4); + 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 + whenCompliantContainer + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + givenPaymentMethodLinearStream + { + // Make Eve the caller in this test suite as she's the owner of the {Container} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a new invoice with a linear stream payment + 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((address,uint8,uint40,uint40,(uint8,uint8,uint40,address,uint128,uint256)))", + invoice + ); + + // Run the test + container.execute({ module: address(invoiceModule), value: 0, data: data }); + } + + function test_CreateInvoice_LinearStream() + external + whenCallerContract + whenCompliantContainer + whenNonZeroPaymentAmount + whenStartTimeLowerThanEndTime + whenEndTimeInTheFuture + givenPaymentMethodLinearStream + whenPaymentAssetNotNativeToken + { + // Make Eve the caller in this test suite as she's the owner of the {Container} contract + vm.startPrank({ msgSender: users.eve }); + + // Create a new invoice with a linear stream payment + createInvoiceWithLinearStream(); + + // Create the calldata for the Invoice Module execution + bytes memory data = abi.encodeWithSignature( + "createInvoice((address,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: 0, + recipient: users.eve, + status: Types.Status.Pending, + startTime: invoice.startTime, + endTime: invoice.endTime, + payment: invoice.payment + }); + + // Expect the {Container} contract to emit a {ModuleExecutionSucceded} event + vm.expectEmit(); + emit Events.ModuleExecutionSucceded({ module: address(invoiceModule), value: 0, data: data }); + + // Run the test + container.execute({ module: address(invoiceModule), value: 0, data: data }); + + // Assert the actual and expected invoice state + Types.Invoice memory actualInvoice = invoiceModule.getInvoice({ id: 0 }); + assertEq(actualInvoice.recipient, users.eve); + 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/create-invoice/createInvoice.tree b/test/integration/concrete/invoice-module/create-invoice/createInvoice.tree index 685c04c8..e38cdb84 100644 --- a/test/integration/concrete/invoice-module/create-invoice/createInvoice.tree +++ b/test/integration/concrete/invoice-module/create-invoice/createInvoice.tree @@ -5,3 +5,39 @@ createInvoice.t.sol ├── when the caller contract DOES NOT implement the ERC-165 {IContainer} interface │ └── it should revert with the {ContainerUnsupportedInterface} error └── when the caller contract DOES implement the ERC-165 {IContainer} interface + ├── when the payment amount IS zero + │ └── it should revert with the {ZeroPaymentAmount} error + └── when the payment amount IS greater than zero + ├── when the start time IS greater than the end time + │ └── it should revert with the {StartTimeGreaterThanEndTime} error + └── when the start time IS NOT greater than the end time + ├── when the end time IS in the past + │ └── 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 + ├── 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 + ├── given the payment method is a tranched stream + │ ├── when the recurrence IS set to one-off + │ │ └── it should revert with the {TranchedStreamInvalidOneOffRecurence} error + │ └── when the recurrence IS NOT set to one-off + │ ├── 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 + │ ├── 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 + └── 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