From 199a14457f4e9efbabd7ceac662b156b0d430c83 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 8 Jul 2024 13:32:45 +0300 Subject: [PATCH 01/40] feat(invoice-module): check for non-zero 'Container' code size and update 'Errors' --- src/modules/invoice-module/InvoiceModule.sol | 8 +++++++- src/modules/invoice-module/libraries/Errors.sol | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/modules/invoice-module/InvoiceModule.sol b/src/modules/invoice-module/InvoiceModule.sol index 769d27d9..fb6b31c7 100644 --- a/src/modules/invoice-module/InvoiceModule.sol +++ b/src/modules/invoice-module/InvoiceModule.sol @@ -34,8 +34,14 @@ contract InvoiceModule is IInvoiceModule { /// @dev Allow only calls from contracts implementing the {IContainer} interface modifier onlyContainer() { + // Checks: the sender is a valid non-zero code size contract + if (msg.sender.code.length == 0) { + revert Errors.ContainerZeroCodeSize(); + } + + // Checks: the sender implements the ERC-165 interface required by {IContainer} bytes4 interfaceId = type(IContainer).interfaceId; - if (!IERC165(msg.sender).supportsInterface(interfaceId)) revert Errors.NotContainer(); + if (!IERC165(msg.sender).supportsInterface(interfaceId)) revert Errors.ContainerUnsupportedInterface(); _; } diff --git a/src/modules/invoice-module/libraries/Errors.sol b/src/modules/invoice-module/libraries/Errors.sol index 290b1c9a..8a547111 100644 --- a/src/modules/invoice-module/libraries/Errors.sol +++ b/src/modules/invoice-module/libraries/Errors.sol @@ -6,7 +6,8 @@ import { Types } from "./Types.sol"; /// @title Errors /// @notice Library containing all custom errors the {InvoiceModule} may revert with library Errors { - error NotContainer(); + error ContainerZeroCodeSize(); + error ContainerUnsupportedInterface(); error InvalidPayer(); error InvalidOrExpiredInvoice(); error EndTimeLowerThanCurrentTime(); From 5fcf4dec1a35b85e1b4cbffb3a76d315baae9e3c Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 8 Jul 2024 13:33:38 +0300 Subject: [PATCH 02/40] test(invoice-module): add basic integration tests structure --- test/integration/Integration.t.sol | 27 +++++++++++++++++++ .../create-invoice/createInvoice.t.sol | 0 .../create-invoice/createInvoice.tree | 7 +++++ 3 files changed, 34 insertions(+) create mode 100644 test/integration/Integration.t.sol create mode 100644 test/integration/concrete/invoice-module/create-invoice/createInvoice.t.sol create mode 100644 test/integration/concrete/invoice-module/create-invoice/createInvoice.tree diff --git a/test/integration/Integration.t.sol b/test/integration/Integration.t.sol new file mode 100644 index 00000000..d77a22fb --- /dev/null +++ b/test/integration/Integration.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import { Base_Test } from "../Base.t.sol"; + +abstract contract Integration_Test is Base_Test { + /*////////////////////////////////////////////////////////////////////////// + SET-UP FUNCTION + //////////////////////////////////////////////////////////////////////////*/ + + function setUp() public virtual override { + Base_Test.setUp(); + + // Make Eve the default caller to deploy a new {Container} contract + vm.startPrank({ msgSender: users.eve }); + + // Setup the initial {InvoiceModule} module + address[] memory modules = new address[](1); + modules[0] = address(invoiceModule); + + // Deploy the {Container} contract with the {InvoiceModule} enabled by default + container = deployContainer({ owner: users.eve, initialModules: modules }); + + // Stop the prank to be able to start a different one in the test suite + vm.stopPrank(); + } +} diff --git a/test/integration/concrete/invoice-module/create-invoice/createInvoice.t.sol b/test/integration/concrete/invoice-module/create-invoice/createInvoice.t.sol new file mode 100644 index 00000000..e69de29b diff --git a/test/integration/concrete/invoice-module/create-invoice/createInvoice.tree b/test/integration/concrete/invoice-module/create-invoice/createInvoice.tree new file mode 100644 index 00000000..685c04c8 --- /dev/null +++ b/test/integration/concrete/invoice-module/create-invoice/createInvoice.tree @@ -0,0 +1,7 @@ +createInvoice.t.sol +├── when the caller IS NOT a contract +│ └── it should revert with the {ContainerZeroCodeSize} error +└── when the caller IS a contract + ├── 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 From 7d8d28bbf098a8bf025ca91b6ca1cfe593b2734c Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 8 Jul 2024 13:32:45 +0300 Subject: [PATCH 03/40] feat(invoice-module): check for non-zero 'Container' code size and update 'Errors' --- src/modules/invoice-module/InvoiceModule.sol | 8 +++++++- src/modules/invoice-module/libraries/Errors.sol | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/modules/invoice-module/InvoiceModule.sol b/src/modules/invoice-module/InvoiceModule.sol index 769d27d9..fb6b31c7 100644 --- a/src/modules/invoice-module/InvoiceModule.sol +++ b/src/modules/invoice-module/InvoiceModule.sol @@ -34,8 +34,14 @@ contract InvoiceModule is IInvoiceModule { /// @dev Allow only calls from contracts implementing the {IContainer} interface modifier onlyContainer() { + // Checks: the sender is a valid non-zero code size contract + if (msg.sender.code.length == 0) { + revert Errors.ContainerZeroCodeSize(); + } + + // Checks: the sender implements the ERC-165 interface required by {IContainer} bytes4 interfaceId = type(IContainer).interfaceId; - if (!IERC165(msg.sender).supportsInterface(interfaceId)) revert Errors.NotContainer(); + if (!IERC165(msg.sender).supportsInterface(interfaceId)) revert Errors.ContainerUnsupportedInterface(); _; } diff --git a/src/modules/invoice-module/libraries/Errors.sol b/src/modules/invoice-module/libraries/Errors.sol index 290b1c9a..8a547111 100644 --- a/src/modules/invoice-module/libraries/Errors.sol +++ b/src/modules/invoice-module/libraries/Errors.sol @@ -6,7 +6,8 @@ import { Types } from "./Types.sol"; /// @title Errors /// @notice Library containing all custom errors the {InvoiceModule} may revert with library Errors { - error NotContainer(); + error ContainerZeroCodeSize(); + error ContainerUnsupportedInterface(); error InvalidPayer(); error InvalidOrExpiredInvoice(); error EndTimeLowerThanCurrentTime(); From fd38bd88ebdb6b01a796247e2395a6a6ac5f032b Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 8 Jul 2024 13:57:58 +0300 Subject: [PATCH 04/40] test: update 'MockModule.sol' to match the new checks --- test/mocks/MockModule.sol | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/mocks/MockModule.sol b/test/mocks/MockModule.sol index e0330aed..4d6ce138 100644 --- a/test/mocks/MockModule.sol +++ b/test/mocks/MockModule.sol @@ -16,8 +16,14 @@ contract MockModule { /// @dev Allow only calls from contracts implementing the {IContainer} interface modifier onlyContainer() { + // Checks: the sender is a valid non-zero code size contract + if (msg.sender.code.length == 0) { + revert Errors.ContainerZeroCodeSize(); + } + + // Checks: the sender implements the ERC-165 interface required by {IContainer} bytes4 interfaceId = type(IContainer).interfaceId; - if (!IERC165(msg.sender).supportsInterface(interfaceId)) revert Errors.NotContainer(); + if (!IERC165(msg.sender).supportsInterface(interfaceId)) revert Errors.ContainerUnsupportedInterface(); _; } From 98b061836e7eb4e156d2b002e1536228c78bfa18 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 8 Jul 2024 13:59:23 +0300 Subject: [PATCH 05/40] forge install: v2-core v1.2.0 --- .gitmodules | 3 +++ lib/v2-core | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/v2-core diff --git a/.gitmodules b/.gitmodules index 690924b6..c9e54db2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/v2-core"] + path = lib/v2-core + url = https://github.com/sablier-labs/v2-core diff --git a/lib/v2-core b/lib/v2-core new file mode 160000 index 00000000..73356945 --- /dev/null +++ b/lib/v2-core @@ -0,0 +1 @@ +Subproject commit 73356945b53e8dd4112f34f3e2c63c278c4a5239 From 08b70e63e10bfe34e5bc5b073342065d3496022e Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 8 Jul 2024 14:07:04 +0300 Subject: [PATCH 06/40] build: install @sablier/v2-core and @prb/math --- .gitmodules | 3 +++ lib/prb-math | 1 + remappings.txt | 2 ++ 3 files changed, 6 insertions(+) create mode 160000 lib/prb-math diff --git a/.gitmodules b/.gitmodules index c9e54db2..d518ac8a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "lib/v2-core"] path = lib/v2-core url = https://github.com/sablier-labs/v2-core +[submodule "lib/prb-math"] + path = lib/prb-math + url = https://github.com/PaulRBerg/prb-math diff --git a/lib/prb-math b/lib/prb-math new file mode 160000 index 00000000..39eec818 --- /dev/null +++ b/lib/prb-math @@ -0,0 +1 @@ +Subproject commit 39eec818282a29df7406b8280b29c084c9a3f3b5 diff --git a/remappings.txt b/remappings.txt index 8a2bfd03..ea163543 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,5 @@ @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +@sablier/v2-core/=lib/v2-core/ +@prb/math/=lib/prb-math/ ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ From 5c422a19222befeef391bb618a5335e1d11b820e Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Tue, 9 Jul 2024 16:36:44 +0300 Subject: [PATCH 07/40] fix(container): return only 'IContainer' interfaceId for ERC-165 --- src/Container.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Container.sol b/src/Container.sol index e2506b06..d62f0963 100644 --- a/src/Container.sol +++ b/src/Container.sol @@ -140,9 +140,6 @@ contract Container is IContainer, ModuleManager { /// @inheritdoc IERC165 function supportsInterface(bytes4 interfaceId) public pure override returns (bool) { - return - interfaceId == type(IContainer).interfaceId || - interfaceId == type(IModuleManager).interfaceId || - interfaceId == type(IERC165).interfaceId; + return interfaceId == type(IContainer).interfaceId || interfaceId == type(IERC165).interfaceId; } } From 95c6f55b82e34f5937e5d37138ea0218f3910844 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Tue, 9 Jul 2024 16:37:48 +0300 Subject: [PATCH 08/40] feat(invoice-module): add Sablier V2 Lockup Linear stream management --- .../invoice-module/LockupStreamCreator.sol | 109 ++++++++++++++++++ .../interfaces/ILockupStreamCreator.sol | 35 ++++++ 2 files changed, 144 insertions(+) create mode 100644 src/modules/invoice-module/LockupStreamCreator.sol create mode 100644 src/modules/invoice-module/interfaces/ILockupStreamCreator.sol diff --git a/src/modules/invoice-module/LockupStreamCreator.sol b/src/modules/invoice-module/LockupStreamCreator.sol new file mode 100644 index 00000000..7fb48ad3 --- /dev/null +++ b/src/modules/invoice-module/LockupStreamCreator.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ud60x18, UD60x18 } from "@prb/math/src/UD60x18.sol"; +import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { ILockupStreamCreator } from "./interfaces/ILockupStreamCreator.sol"; +import { Broker, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { Errors } from "./libraries/Errors.sol"; + +/// @title LockupStreamCreator +/// @dev See the documentation in {ILockupStreamCreator} +contract LockupStreamCreator is ILockupStreamCreator { + /*////////////////////////////////////////////////////////////////////////// + PUBLIC STORAGE + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ILockupStreamCreator + ISablierV2LockupLinear public immutable override LOCKUP_LINEAR; + + /// @inheritdoc ILockupStreamCreator + address public override brokerAdmin; + + /// @inheritdoc ILockupStreamCreator + UD60x18 public brokerFee; + + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Initializes the address of the {SablierV2LockupLinear} contract and the address of the broker admin account or contract + constructor(address _sablierLockupDeployment, address _brokerAdmin) { + LOCKUP_LINEAR = ISablierV2LockupLinear(_sablierLockupDeployment); + brokerAdmin = _brokerAdmin; + } + + /*////////////////////////////////////////////////////////////////////////// + MODIFIERS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Reverts if the `msg.sender` is not the broker admin account or contract + modifier onlyBrokerAdmin() { + if (msg.sender != brokerAdmin) revert Errors.OnlyBrokerAdmin(); + _; + } + + /*////////////////////////////////////////////////////////////////////////// + NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Creates either a Lockup Linear or Dynamic stream + function createStream( + IERC20 asset, + uint128 totalAmount, + LockupLinear.Durations memory durations, + address recipient + ) public returns (uint256 streamId) { + // Transfer the provided amount of ERC-20 tokens to this contract + asset.transferFrom(msg.sender, address(this), totalAmount); + + // Approve the Sablier contract to spend the ERC-20 tokens + asset.approve(address(LOCKUP_LINEAR), totalAmount); + + // Create the Lockup Linear stream + streamId = _createLinearStream(asset, totalAmount, durations, recipient); + } + + /// @dev Updates the fee charged by the broker + function updateBrokerFee(UD60x18 newBrokerFee) public onlyBrokerAdmin { + // Log the broker fee update + emit BrokerFeeUpdated({ oldFee: brokerFee, newFee: newBrokerFee }); + + // Update the fee charged by the broker + brokerFee = newBrokerFee; + } + + /*////////////////////////////////////////////////////////////////////////// + INTERNAL-METHODS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Creates a Lockup Linear stream + /// See https://docs.sablier.com/contracts/v2/guides/create-stream/lockup-linear + function _createLinearStream( + IERC20 asset, + uint128 totalAmount, + LockupLinear.Durations memory durations, + address recipient + ) internal returns (uint256 streamId) { + // Declare the params struct + LockupLinear.CreateWithDurations memory params; + + // Declare the function parameters + params.sender = msg.sender; // The sender will be able to cancel the stream + params.recipient = recipient; // The recipient of the streamed assets + params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees + params.asset = asset; // The streaming asset + params.cancelable = true; // Whether the stream will be cancelable or not + params.transferable = true; // Whether the stream will be transferable or not + params.durations = LockupLinear.Durations({ + cliff: durations.cliff, // Assets will be unlocked only after x period of time + total: durations.total // Setting a total duration of x period of time + }); + params.broker = Broker({ account: brokerAdmin, fee: brokerFee }); // Optional parameter for charging a fee + + // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` + streamId = LOCKUP_LINEAR.createWithDurations(params); + } +} diff --git a/src/modules/invoice-module/interfaces/ILockupStreamCreator.sol b/src/modules/invoice-module/interfaces/ILockupStreamCreator.sol new file mode 100644 index 00000000..9031ea20 --- /dev/null +++ b/src/modules/invoice-module/interfaces/ILockupStreamCreator.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { UD60x18 } from "@prb/math/src/UD60x18.sol"; + +/// @title ILockupStreamCreator +/// @notice Contract used to create Sablier V2 compatible streams +/// @dev This code is referenced in the docs: https://docs.sablier.com/contracts/v2/guides/create-stream/lockup-linear +interface ILockupStreamCreator { + /*////////////////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when the broker fee is updated + /// @param oldFee The old broker fee + /// @param newFee The new broker fee + event BrokerFeeUpdated(UD60x18 oldFee, UD60x18 newFee); + + /*////////////////////////////////////////////////////////////////////////// + CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice The address of the {SablierV2LockupLinear} contract used to create linear streams + /// @dev This is initialized at construction time and it might be different depending on the deployment chain + /// See https://docs.sablier.com/contracts/v2/deployments + function LOCKUP_LINEAR() external view returns (ISablierV2LockupLinear); + + /// @notice The address of the broker admin account or contract managing the broker fee + function brokerAdmin() external view returns (address); + + /// @notice The broker fee charged to create Sablier V2 stream + /// @dev See the `UD60x18` type definition in the `@prb/math/src/ud60x18/ValueType.sol file` + function brokerFee() external view returns (UD60x18); +} From 80589cd948df5d940d25a6595ad3a0adf2b41531 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Tue, 9 Jul 2024 16:39:41 +0300 Subject: [PATCH 09/40] chore(invoice-module): add missing 'OnlyBrokerAdmin' error --- src/modules/invoice-module/libraries/Errors.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/invoice-module/libraries/Errors.sol b/src/modules/invoice-module/libraries/Errors.sol index 8a547111..8b16ff1e 100644 --- a/src/modules/invoice-module/libraries/Errors.sol +++ b/src/modules/invoice-module/libraries/Errors.sol @@ -18,4 +18,5 @@ library Errors { error PaymentFailed(); error InvalidInvoiceStatus(Types.Status currentStatus); error InvalidNumberOfPayments(uint40 expectedNumber); + error OnlyBrokerAdmin(); } From d4a2e5d3d517d30616c0e132d3ebddbdb7707713 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Wed, 10 Jul 2024 11:08:06 +0300 Subject: [PATCH 10/40] feat: update invoice module types and errors --- src/modules/invoice-module/libraries/Errors.sol | 7 +++---- src/modules/invoice-module/libraries/Types.sol | 8 +++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/modules/invoice-module/libraries/Errors.sol b/src/modules/invoice-module/libraries/Errors.sol index 8b16ff1e..6c926c22 100644 --- a/src/modules/invoice-module/libraries/Errors.sol +++ b/src/modules/invoice-module/libraries/Errors.sol @@ -1,22 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import { Types } from "./Types.sol"; - /// @title Errors /// @notice Library containing all custom errors the {InvoiceModule} may revert with library Errors { error ContainerZeroCodeSize(); error ContainerUnsupportedInterface(); error InvalidPayer(); - error InvalidOrExpiredInvoice(); error EndTimeLowerThanCurrentTime(); error StartTimeGreaterThanEndTime(); error InvalidPaymentType(); error PaymentAmountZero(); error InvalidPaymentAmount(uint256 amount); error PaymentFailed(); - error InvalidInvoiceStatus(Types.Status currentStatus); error InvalidNumberOfPayments(uint40 expectedNumber); error OnlyBrokerAdmin(); + error OnlyERC20StreamsAllowed(); + error InvoiceAlreadyPaid(); + error InvoiceCanceled(); } diff --git a/src/modules/invoice-module/libraries/Types.sol b/src/modules/invoice-module/libraries/Types.sol index cd2bddd5..1cd5d7c6 100644 --- a/src/modules/invoice-module/libraries/Types.sol +++ b/src/modules/invoice-module/libraries/Types.sol @@ -2,9 +2,6 @@ pragma solidity ^0.8.26; library Types { - // frequency: recurring between 1 January - 1 March (2 months) - // recurrence: weekly - // method: transfer enum Recurrence { OneTime, Weekly, @@ -24,7 +21,7 @@ library Types { uint24 paymentsLeft; address asset; // slot 1 - uint256 amount; + uint128 amount; } enum Frequency { @@ -33,7 +30,8 @@ library Types { } enum Status { - Active, + Pending, + Ongoing, Paid, Canceled } From ec845e72d7bfb775a2e886278f1d5add69080d6b Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Wed, 10 Jul 2024 11:08:57 +0300 Subject: [PATCH 11/40] feat(invoice-module): integrate Sablier v2 'LockupStreamCreator' and split payment process --- src/modules/invoice-module/InvoiceModule.sol | 77 +++++++++++++++----- 1 file changed, 57 insertions(+), 20 deletions(-) diff --git a/src/modules/invoice-module/InvoiceModule.sol b/src/modules/invoice-module/InvoiceModule.sol index fb6b31c7..fb0c3d9f 100644 --- a/src/modules/invoice-module/InvoiceModule.sol +++ b/src/modules/invoice-module/InvoiceModule.sol @@ -9,10 +9,12 @@ import { Types } from "./libraries/Types.sol"; import { Errors } from "./libraries/Errors.sol"; import { IInvoiceModule } from "./interfaces/IInvoiceModule.sol"; import { IContainer } from "./../../interfaces/IContainer.sol"; +import { LockupStreamCreator } from "./LockupStreamCreator.sol"; +import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; /// @title InvoiceModule /// @notice See the documentation in {IInvoiceModule} -contract InvoiceModule is IInvoiceModule { +contract InvoiceModule is IInvoiceModule, LockupStreamCreator { using SafeERC20 for IERC20; /*////////////////////////////////////////////////////////////////////////// @@ -28,6 +30,16 @@ contract InvoiceModule is IInvoiceModule { /// @dev Counter to keep track of the next ID used to create a new invoice uint256 private _nextInvoiceId; + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Initializes the {LockupStreamCreator} contract + constructor( + address _sablierLockupDeployment, + address _brokerAdmin + ) LockupStreamCreator(_sablierLockupDeployment, _brokerAdmin) {} + /*////////////////////////////////////////////////////////////////////////// MODIFIERS //////////////////////////////////////////////////////////////////////////*/ @@ -121,32 +133,46 @@ contract InvoiceModule is IInvoiceModule { Types.Invoice memory invoice = _invoices[id]; // Checks: the invoice is not already paid or canceled - if (invoice.status != Types.Status.Active) { - revert Errors.InvalidInvoiceStatus({ currentStatus: invoice.status }); + if (invoice.status == Types.Status.Paid) { + revert Errors.InvoiceAlreadyPaid(); + } else if (invoice.status == Types.Status.Canceled) { + revert Errors.InvoiceCanceled(); } - // Checks: the payment type is different than transfer - if (invoice.payment.method != Types.Method.Transfer) revert Errors.InvalidPaymentType(); - - // Effects: update the invoice status to `Paid` if this is a one-off invoice or - // if the invoice is recurring and the required number of payments has been made - if (invoice.frequency == Types.Frequency.Regular) { - _invoices[id].status = Types.Status.Paid; + // Handle the payment workflow depending on the payment method type + if (invoice.payment.method == Types.Method.Transfer) { + _payByTransfer(id, invoice); } else { - // Using unchecked because the number of payments left cannot underflow as the invoice status - // will be updated to `Paid` once `paymentLeft` is zero and this branch will not be - unchecked { - uint24 paymentsLeft = invoice.payment.paymentsLeft - 1; - _invoices[id].payment.paymentsLeft = paymentsLeft; - if (paymentsLeft == 0) { - _invoices[id].status = Types.Status.Paid; - } + // Allow only ERC-20 based streams + if (invoice.payment.asset == address(0)) { + revert Errors.OnlyERC20StreamsAllowed(); + } + + // + _payByStream(invoice); + } + + emit InvoicePaid({ id: id, payer: msg.sender }); + } + + /// @dev Pays the `id` invoice by transfer + function _payByTransfer(uint256 id, Types.Invoice memory invoice) 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 { + uint24 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 paid amount matches the invoice value + // Checks: the payment amount matches the invoice value if (msg.value < invoice.payment.amount) { revert Errors.InvalidPaymentAmount({ amount: invoice.payment.amount }); } @@ -161,8 +187,19 @@ contract InvoiceModule is IInvoiceModule { value: invoice.payment.amount }); } + } - emit InvoicePaid({ id: id, payer: msg.sender }); + function _payByStream(Types.Invoice memory invoice) internal { + // Create the `Durations` struct used to set up the cliff period and end time of the stream + LockupLinear.Durations memory durations = LockupLinear.Durations({ cliff: 0, total: invoice.endTime }); + + // Create the payment stream + LockupStreamCreator.createStream({ + asset: IERC20(invoice.payment.asset), + totalAmount: invoice.payment.amount, + durations: durations, + recipient: invoice.recipient + }); } /*////////////////////////////////////////////////////////////////////////// From d87ce845cf026f58f6aaa00377518101b04176f9 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Wed, 10 Jul 2024 12:40:22 +0300 Subject: [PATCH 12/40] test: fix base 'setUp' method to account Sablier v2 streams and small improvements --- test/Base.t.sol | 28 +++++++++++++++---- test/unit/concrete/container/Container.t.sol | 6 ++-- .../enable-module/enableModule.t.sol | 4 +-- .../concrete/container/execute/execute.t.sol | 23 ++++++--------- test/utils/Helpers.sol | 10 +++---- test/utils/Types.sol | 2 ++ 6 files changed, 42 insertions(+), 31 deletions(-) diff --git a/test/Base.t.sol b/test/Base.t.sol index e4ceafa0..24817092 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -8,6 +8,8 @@ import { MockERC20NoReturn } from "./mocks/MockERC20NoReturn.sol"; import { MockModule } from "./mocks/MockModule.sol"; import { Container } from "./../src/Container.sol"; import { InvoiceModule } from "./../src/modules/invoice-module/InvoiceModule.sol"; +import { SablierV2LockupLinear } from "@sablier/v2-core/src/SablierV2LockupLinear.sol"; +import { NFTDescriptorMock } from "@sablier/v2-core/test/mocks/NFTDescriptorMock.sol"; abstract contract Base_Test is Test, Events { /*////////////////////////////////////////////////////////////////////////// @@ -25,22 +27,36 @@ abstract contract Base_Test is Test, Events { MockERC20NoReturn internal usdt; MockModule internal mockModule; + // Sablier V2 related test contracts + NFTDescriptorMock internal mockNFTDescriptor; + SablierV2LockupLinear internal sablierV2LockupLinear; + /*////////////////////////////////////////////////////////////////////////// SET-UP FUNCTION //////////////////////////////////////////////////////////////////////////*/ function setUp() public virtual { - // Deploy test contracts + // Deploy the mock USDT contract to deal it to the users usdt = new MockERC20NoReturn("Tether USD", "USDT", 6); - invoiceModule = new InvoiceModule(); + + // Create test users + users = Users({ admin: createUser("admin"), eve: createUser("eve"), bob: createUser("bob") }); + + // Deploy test contracts + mockNFTDescriptor = new NFTDescriptorMock(); + sablierV2LockupLinear = new SablierV2LockupLinear({ + initialAdmin: users.admin, + initialNFTDescriptor: mockNFTDescriptor + }); + invoiceModule = new InvoiceModule({ + _brokerAdmin: users.admin, + _sablierLockupDeployment: sablierV2LockupLinear + }); mockModule = new MockModule(); // Label the test contracts so we can easily track them vm.label({ account: address(usdt), newLabel: "USDT" }); vm.label({ account: address(invoiceModule), newLabel: "InvoiceModule" }); - - // Create test users - users = Users({ eve: createUser("eve"), bob: createUser("bob") }); } /*////////////////////////////////////////////////////////////////////////// @@ -60,7 +76,7 @@ abstract contract Base_Test is Test, Events { 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: 1000000e16 }); + deal({ token: address(usdt), to: user, give: 1000000e6 }); return user; } diff --git a/test/unit/concrete/container/Container.t.sol b/test/unit/concrete/container/Container.t.sol index 73a5453d..25a4b09c 100644 --- a/test/unit/concrete/container/Container.t.sol +++ b/test/unit/concrete/container/Container.t.sol @@ -2,14 +2,14 @@ pragma solidity ^0.8.26; import { Base_Test } from "../../../Base.t.sol"; -import { InvoiceModule } from "./../../../../src/modules/invoice-module/InvoiceModule.sol"; contract Container_Unit_Concrete_Test is Base_Test { function setUp() public virtual override { Base_Test.setUp(); - address[] memory modules = new address[](1); - modules[0] = address(invoiceModule); + address[] memory modules = new address[](2); + modules[0] = address(mockModule); + modules[1] = address(invoiceModule); container = deployContainer({ owner: users.eve, initialModules: modules }); } diff --git a/test/unit/concrete/container/enable-module/enableModule.t.sol b/test/unit/concrete/container/enable-module/enableModule.t.sol index 1121980d..716d7765 100644 --- a/test/unit/concrete/container/enable-module/enableModule.t.sol +++ b/test/unit/concrete/container/enable-module/enableModule.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.26; import { Container_Unit_Concrete_Test } from "../Container.t.sol"; -import { InvoiceModule } from "./../../../../../src/modules/invoice-module/InvoiceModule.sol"; +import { MockModule } from "../../../../mocks/MockModule.sol"; import { Events } from "../../../../utils/Events.sol"; import { Errors } from "../../../../utils/Errors.sol"; @@ -42,7 +42,7 @@ contract EnableModule_Unit_Concrete_Test is Container_Unit_Concrete_Test { function test_EnableModule() external whenCallerOwner whenNonZeroCodeModule { // Create a new mock module - InvoiceModule mockModule = new InvoiceModule(); + MockModule mockModule = new MockModule(); // Expect the {ModuleEnabled} to be emitted vm.expectEmit(); diff --git a/test/unit/concrete/container/execute/execute.t.sol b/test/unit/concrete/container/execute/execute.t.sol index 7aa73d35..86c262f9 100644 --- a/test/unit/concrete/container/execute/execute.t.sol +++ b/test/unit/concrete/container/execute/execute.t.sol @@ -42,31 +42,24 @@ contract Execute_Unit_Concrete_Test is Container_Unit_Concrete_Test { } function test_Execute() external whenCallerOwner whenModuleEnabled { - // Create the mock invoice and calldata for the module execution - InvoiceModuleTypes.Invoice memory invoice = Helpers.createInvoiceDataType({ recipient: address(container) }); - bytes memory data = abi.encodeWithSignature( - "createInvoice((address,uint8,uint8,uint40,uint40,(uint8,uint8,uint24,address,uint256)))", - invoice - ); + // Create the calldata for the mock module execution + bytes memory data = abi.encodeWithSignature("createModuleItem()", ""); // Expect the {ModuleExecutionSucceded} event to be emitted vm.expectEmit(); - emit Events.ModuleExecutionSucceded({ module: address(invoiceModule), value: 0, data: data }); + emit Events.ModuleExecutionSucceded({ module: address(mockModule), value: 0, data: data }); // Run the test - container.execute({ module: address(invoiceModule), value: 0, data: data }); + container.execute({ module: address(mockModule), value: 0, data: data }); - // Alter the `createInvoice` method signature by removing the `payment.amount` field - bytes memory wrongData = abi.encodeWithSignature( - "createInvoice((address,uint8,uint8,uint40,uint40,(uint8,uint8,uint24,address)))", - invoice - ); + // Alter the `createModuleItem` method signature by adding an invalid `uint256` field + bytes memory wrongData = abi.encodeWithSignature("createModuleItem(uint256)", 1); // Expect the {ModuleExecutionFailed} event to be emitted vm.expectEmit(); - emit Events.ModuleExecutionFailed({ module: address(invoiceModule), value: 0, data: wrongData }); + emit Events.ModuleExecutionFailed({ module: address(mockModule), value: 0, data: wrongData }); // Run the test - container.execute({ module: address(invoiceModule), value: 0, data: wrongData }); + container.execute({ module: address(mockModule), value: 0, data: wrongData }); } } diff --git a/test/utils/Helpers.sol b/test/utils/Helpers.sol index 67016d52..a4cedd53 100644 --- a/test/utils/Helpers.sol +++ b/test/utils/Helpers.sol @@ -8,16 +8,16 @@ library Helpers { return InvoiceModulesTypes.Invoice({ recipient: recipient, - status: InvoiceModulesTypes.Status.Active, + status: InvoiceModulesTypes.Status.Pending, frequency: InvoiceModulesTypes.Frequency.Regular, startTime: 0, - endTime: uint40(block.timestamp) + 150, + endTime: uint40(block.timestamp) + 1 weeks, payment: InvoiceModulesTypes.Payment({ - recurrence: InvoiceModulesTypes.Recurrence.OneTime, method: InvoiceModulesTypes.Method.Transfer, - amount: 1 ether, + recurrence: InvoiceModulesTypes.Recurrence.OneTime, + paymentsLeft: 1, asset: address(0), - paymentsLeft: 1 + amount: uint128(1 ether) }) }); } diff --git a/test/utils/Types.sol b/test/utils/Types.sol index 7069b09e..fcff7518 100644 --- a/test/utils/Types.sol +++ b/test/utils/Types.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.26; struct Users { + // Account with administrative permissions + address admin; // Account to interact with the protocol address eve; // Account to interact with the protocol From 9187956ff3667a30554a8591bf35c6f7461a9a0e Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Wed, 10 Jul 2024 12:43:53 +0300 Subject: [PATCH 13/40] chore(invoice-module): use proper types for Sablier v2 integration --- src/modules/invoice-module/InvoiceModule.sol | 5 +++-- src/modules/invoice-module/LockupStreamCreator.sol | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/modules/invoice-module/InvoiceModule.sol b/src/modules/invoice-module/InvoiceModule.sol index fb0c3d9f..fdd33bb4 100644 --- a/src/modules/invoice-module/InvoiceModule.sol +++ b/src/modules/invoice-module/InvoiceModule.sol @@ -11,6 +11,7 @@ import { IInvoiceModule } from "./interfaces/IInvoiceModule.sol"; import { IContainer } from "./../../interfaces/IContainer.sol"; import { LockupStreamCreator } from "./LockupStreamCreator.sol"; import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; /// @title InvoiceModule /// @notice See the documentation in {IInvoiceModule} @@ -36,7 +37,7 @@ contract InvoiceModule is IInvoiceModule, LockupStreamCreator { /// @dev Initializes the {LockupStreamCreator} contract constructor( - address _sablierLockupDeployment, + ISablierV2LockupLinear _sablierLockupDeployment, address _brokerAdmin ) LockupStreamCreator(_sablierLockupDeployment, _brokerAdmin) {} @@ -102,7 +103,7 @@ contract InvoiceModule is IInvoiceModule, LockupStreamCreator { // Effects: create the invoice _invoices[id] = Types.Invoice({ recipient: msg.sender, - status: invoice.status, + status: Types.Status.Pending, frequency: invoice.frequency, startTime: invoice.startTime, endTime: invoice.endTime, diff --git a/src/modules/invoice-module/LockupStreamCreator.sol b/src/modules/invoice-module/LockupStreamCreator.sol index 7fb48ad3..b8e1eb39 100644 --- a/src/modules/invoice-module/LockupStreamCreator.sol +++ b/src/modules/invoice-module/LockupStreamCreator.sol @@ -23,15 +23,15 @@ contract LockupStreamCreator is ILockupStreamCreator { address public override brokerAdmin; /// @inheritdoc ILockupStreamCreator - UD60x18 public brokerFee; + UD60x18 public override brokerFee; /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ /// @dev Initializes the address of the {SablierV2LockupLinear} contract and the address of the broker admin account or contract - constructor(address _sablierLockupDeployment, address _brokerAdmin) { - LOCKUP_LINEAR = ISablierV2LockupLinear(_sablierLockupDeployment); + constructor(ISablierV2LockupLinear _sablierLockupDeployment, address _brokerAdmin) { + LOCKUP_LINEAR = _sablierLockupDeployment; brokerAdmin = _brokerAdmin; } From b5f247a739227a8cbf894dcb305049b757a2c129 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Wed, 10 Jul 2024 13:26:11 +0300 Subject: [PATCH 14/40] ci: update 'test.yml' workflow --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7ee1e1ce..15405687 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,7 @@ jobs: - name: Run Forge build run: | forge --version - forge build --sizes + forge build id: build - name: Run Forge tests From f3b938104f2a6b1a19c0dabaac84d528c75ccbd6 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Thu, 11 Jul 2024 12:09:37 +0300 Subject: [PATCH 15/40] feat(invoice-module): integrate Sablier v2 Tranched streams --- .../invoice-module/LockupStreamCreator.sol | 116 +++++++++++++++--- .../interfaces/ILockupStreamCreator.sol | 6 + .../invoice-module/libraries/Helpers.sol | 25 ++++ 3 files changed, 129 insertions(+), 18 deletions(-) create mode 100644 src/modules/invoice-module/libraries/Helpers.sol diff --git a/src/modules/invoice-module/LockupStreamCreator.sol b/src/modules/invoice-module/LockupStreamCreator.sol index b8e1eb39..db9d3560 100644 --- a/src/modules/invoice-module/LockupStreamCreator.sol +++ b/src/modules/invoice-module/LockupStreamCreator.sol @@ -4,10 +4,13 @@ pragma solidity >=0.8.22; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ud60x18, UD60x18 } from "@prb/math/src/UD60x18.sol"; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; import { ILockupStreamCreator } from "./interfaces/ILockupStreamCreator.sol"; import { Broker, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; -import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; import { Errors } from "./libraries/Errors.sol"; +import { Helpers } from "./libraries/Helpers.sol"; +import { Types } from "./libraries/Types.sol"; /// @title LockupStreamCreator /// @dev See the documentation in {ILockupStreamCreator} @@ -19,6 +22,9 @@ contract LockupStreamCreator is ILockupStreamCreator { /// @inheritdoc ILockupStreamCreator ISablierV2LockupLinear public immutable override LOCKUP_LINEAR; + /// @inheritdoc ILockupStreamCreator + ISablierV2LockupTranched public immutable override LOCKUP_TRANCHED; + /// @inheritdoc ILockupStreamCreator address public override brokerAdmin; @@ -49,21 +55,35 @@ contract LockupStreamCreator is ILockupStreamCreator { NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Creates either a Lockup Linear or Dynamic stream - function createStream( + /// @dev Creates a Lockup Linear stream + function createLinearStream( IERC20 asset, uint128 totalAmount, - LockupLinear.Durations memory durations, + uint40 startTime, + uint40 endTime, address recipient ) public returns (uint256 streamId) { - // Transfer the provided amount of ERC-20 tokens to this contract - asset.transferFrom(msg.sender, address(this), totalAmount); + // Transfer the provided amount of ERC-20 tokens to this contract and approve the Sablier contract to spend it + _transferFromAndApprove({ asset: asset, spender: address(LOCKUP_LINEAR), amount: totalAmount }); - // Approve the Sablier contract to spend the ERC-20 tokens - asset.approve(address(LOCKUP_LINEAR), totalAmount); + // Create the Lockup Linear stream + streamId = _createLinearStream(asset, totalAmount, startTime, endTime, recipient); + } + + /// @dev Creates a Lockup Tranched stream + function createTranchedStream( + IERC20 asset, + uint128 totalAmount, + uint40 startTime, + uint40 endTime, + address recipient, + Types.Recurrence recurrence + ) 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, spender: address(LOCKUP_TRANCHED), amount: totalAmount }); // Create the Lockup Linear stream - streamId = _createLinearStream(asset, totalAmount, durations, recipient); + streamId = _createTranchedStream(asset, totalAmount, startTime, endTime, recipient, recurrence); } /// @dev Updates the fee charged by the broker @@ -79,16 +99,18 @@ contract LockupStreamCreator is ILockupStreamCreator { INTERNAL-METHODS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Creates a Lockup Linear stream - /// See https://docs.sablier.com/contracts/v2/guides/create-stream/lockup-linear + /// @notice Creates a Lockup Linear stream + /// See https://docs.sablier.com/concepts/protocol/stream-types#lockup-linear + /// @dev See https://docs.sablier.com/contracts/v2/guides/create-stream/lockup-linear function _createLinearStream( IERC20 asset, uint128 totalAmount, - LockupLinear.Durations memory durations, + uint40 startTime, + uint40 endTime, address recipient ) internal returns (uint256 streamId) { // Declare the params struct - LockupLinear.CreateWithDurations memory params; + LockupLinear.CreateWithTimestamps memory params; // Declare the function parameters params.sender = msg.sender; // The sender will be able to cancel the stream @@ -97,13 +119,71 @@ contract LockupStreamCreator is ILockupStreamCreator { params.asset = asset; // The streaming asset params.cancelable = true; // Whether the stream will be cancelable or not params.transferable = true; // Whether the stream will be transferable or not - params.durations = LockupLinear.Durations({ - cliff: durations.cliff, // Assets will be unlocked only after x period of time - total: durations.total // Setting a total duration of x period of time - }); + params.timestamps = LockupLinear.Timestamps({ start: startTime, cliff: 0, end: endTime }); params.broker = Broker({ account: brokerAdmin, fee: brokerFee }); // Optional parameter for charging a fee // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` - streamId = LOCKUP_LINEAR.createWithDurations(params); + streamId = LOCKUP_LINEAR.createWithTimestamps(params); + } + + /// @notice Creates a Lockup Tranched stream + /// See https://docs.sablier.com/concepts/protocol/stream-types#unlock-monthly + /// @dev See https://docs.sablier.com/contracts/v2/guides/create-stream/lockup-linear + function _createTranchedStream( + IERC20 asset, + uint128 totalAmount, + uint40 startTime, + uint40 endTime, + address recipient, + Types.Recurrence recurrence + ) internal returns (uint256 streamId) { + // Declare the params struct + LockupTranched.CreateWithDurations memory params; + + // Declare the function parameters + params.sender = msg.sender; // The sender will be able to cancel the stream + params.recipient = recipient; // The recipient of the streamed assets + params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees + params.asset = asset; // The streaming asset + params.cancelable = true; // Whether the stream will be cancelable or not + params.transferable = true; // Whether the stream will be transferable or not + + // Calculate the number of tranches based on the payment interval and the type of recurrence + uint128 numberOfTranches = Helpers.computeNumberOfRecurringPayments(recurrence, startTime, endTime); + + // Calculate the duration of each tranche based on the payment recurrence + uint40 durationPerTranche = _computeDurationPerTrache(recurrence); + + // Calculate the amount that must be unlocked with each tranche + uint128 amountPerTranche = totalAmount / numberOfTranches; + + // Create the tranches array + params.tranches = new LockupTranched.TrancheWithDuration[](numberOfTranches); + for (uint256 i; i < numberOfTranches; ++i) { + params.tranches[i] = LockupTranched.TrancheWithDuration({ + amount: amountPerTranche, + duration: durationPerTranche + }); + } + + // Optional parameter for charging a fee + params.broker = Broker({ account: brokerAdmin, fee: brokerFee }); + + // Create the LockupTranched stream + streamId = LOCKUP_TRANCHED.createWithDurations(params); + } + + function _transferFromAndApprove(IERC20 asset, uint128 amount, address spender) internal { + // Transfer the provided amount of ERC-20 tokens to this contract + asset.transferFrom(msg.sender, address(this), amount); + + // Approve the Sablier contract to spend the ERC-20 tokens + asset.approve(spender, amount); + } + + function _computeDurationPerTrache(Types.Recurrence recurrence) internal pure returns (uint40 duration) { + if (recurrence == Types.Recurrence.Weekly) duration = 1 weeks; + else if (recurrence == Types.Recurrence.Monthly) duration = 4 weeks; + else if (recurrence == Types.Recurrence.Yearly) duration = 48 weeks; } } diff --git a/src/modules/invoice-module/interfaces/ILockupStreamCreator.sol b/src/modules/invoice-module/interfaces/ILockupStreamCreator.sol index 9031ea20..4eb83fc4 100644 --- a/src/modules/invoice-module/interfaces/ILockupStreamCreator.sol +++ b/src/modules/invoice-module/interfaces/ILockupStreamCreator.sol @@ -2,6 +2,7 @@ pragma solidity >=0.8.22; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; import { UD60x18 } from "@prb/math/src/UD60x18.sol"; /// @title ILockupStreamCreator @@ -26,6 +27,11 @@ interface ILockupStreamCreator { /// See https://docs.sablier.com/contracts/v2/deployments function LOCKUP_LINEAR() external view returns (ISablierV2LockupLinear); + /// @notice The address of the {SablierV2LockupTranched} contract used to create tranched streams + /// @dev This is initialized at construction time and it might be different depending on the deployment chain + /// See https://docs.sablier.com/contracts/v2/deployments + function LOCKUP_TRANCHED() external view returns (ISablierV2LockupTranched); + /// @notice The address of the broker admin account or contract managing the broker fee function brokerAdmin() external view returns (address); diff --git a/src/modules/invoice-module/libraries/Helpers.sol b/src/modules/invoice-module/libraries/Helpers.sol new file mode 100644 index 00000000..89283047 --- /dev/null +++ b/src/modules/invoice-module/libraries/Helpers.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import { Types } from "./Types.sol"; + +/// @title Helpers +/// @notice Library with helpers used across the Invoice Module contracts +library Helpers { + /// @dev Calculates the number of payments that must be done based on a Recurring invoice + function computeNumberOfRecurringPayments( + Types.Recurrence recurrence, + uint40 startTime, + uint40 endTime + ) internal pure returns (uint40 numberOfPayments) { + uint40 interval = endTime - startTime; + + if (recurrence == Types.Recurrence.Weekly) { + numberOfPayments = interval / 1 weeks; + } else if (recurrence == Types.Recurrence.Monthly) { + numberOfPayments = interval / 4 weeks; + } else if (recurrence == Types.Recurrence.Yearly) { + numberOfPayments = interval / 48 weeks; + } + } +} From 29724e0c3641fd2ab663b499544b6a73a99a0ace Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Thu, 11 Jul 2024 12:10:48 +0300 Subject: [PATCH 16/40] feat(invoice-module): handle stream creationg based on its type --- src/modules/invoice-module/InvoiceModule.sol | 69 +++++++++---------- .../invoice-module/libraries/Types.sol | 4 +- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/src/modules/invoice-module/InvoiceModule.sol b/src/modules/invoice-module/InvoiceModule.sol index fdd33bb4..ad846e9e 100644 --- a/src/modules/invoice-module/InvoiceModule.sol +++ b/src/modules/invoice-module/InvoiceModule.sol @@ -12,6 +12,7 @@ import { IContainer } from "./../../interfaces/IContainer.sol"; import { LockupStreamCreator } from "./LockupStreamCreator.sol"; import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { Helpers } from "./libraries/Helpers.sol"; /// @title InvoiceModule /// @notice See the documentation in {IInvoiceModule} @@ -78,14 +79,14 @@ contract InvoiceModule is IInvoiceModule, LockupStreamCreator { revert Errors.PaymentAmountZero(); } + // Checks: end time is not in the past + uint40 currentTime = uint40(block.timestamp); + if (currentTime >= invoice.endTime) { + revert Errors.EndTimeLowerThanCurrentTime(); + } + // Checks: validate the input parameters if the invoice must be paid in even transfers if (invoice.payment.method == Types.Method.Transfer) { - // Checks: end time is not in the past - uint40 currentTime = uint40(block.timestamp); - if (currentTime >= invoice.endTime) { - revert Errors.EndTimeLowerThanCurrentTime(); - } - // Checks: validate the input parameters if the invoice is recurring if (invoice.frequency == Types.Frequency.Recurring) { _checkRecurringTransferInvoiceParams({ @@ -149,13 +150,20 @@ contract InvoiceModule is IInvoiceModule, LockupStreamCreator { revert Errors.OnlyERC20StreamsAllowed(); } - // - _payByStream(invoice); + uint256 streamId; + // Check to see wether to pay by creating a linear or tranched stream + if (invoice.payment.method == Types.Method.LinearStream) { + streamId = _payByLinearStream(invoice); + } else streamId = _payByTranchedStream(invoice); } emit InvoicePaid({ id: id, payer: msg.sender }); } + /*////////////////////////////////////////////////////////////////////////// + INTERNAL-METHODS + //////////////////////////////////////////////////////////////////////////*/ + /// @dev Pays the `id` invoice by transfer function _payByTransfer(uint256 id, Types.Invoice memory invoice) internal { // Effects: update the invoice status to `Paid` if the required number of payments has been made @@ -190,22 +198,28 @@ contract InvoiceModule is IInvoiceModule, LockupStreamCreator { } } - function _payByStream(Types.Invoice memory invoice) internal { - // Create the `Durations` struct used to set up the cliff period and end time of the stream - LockupLinear.Durations memory durations = LockupLinear.Durations({ cliff: 0, total: invoice.endTime }); - - // Create the payment stream - LockupStreamCreator.createStream({ + /// @dev Create the linear stream payment + function _payByLinearStream(Types.Invoice memory invoice) internal returns (uint256 streamId) { + streamId = LockupStreamCreator.createLinearStream({ asset: IERC20(invoice.payment.asset), totalAmount: invoice.payment.amount, - durations: durations, + startTime: invoice.startTime, + endTime: invoice.endTime, recipient: invoice.recipient }); } - /*////////////////////////////////////////////////////////////////////////// - HELPERS - //////////////////////////////////////////////////////////////////////////*/ + /// @dev Create the tranched stream payment + function _payByTranchedStream(Types.Invoice memory invoice) internal returns (uint256 streamId) { + streamId = LockupStreamCreator.createTranchedStream({ + asset: IERC20(invoice.payment.asset), + totalAmount: invoice.payment.amount, + startTime: invoice.startTime, + endTime: invoice.endTime, + recipient: invoice.recipient, + recurrence: invoice.payment.recurrence + }); + } /// @dev Validates the input parameters if the invoice is recurring and must be paid in even transfers function _checkRecurringTransferInvoiceParams( @@ -220,28 +234,11 @@ contract InvoiceModule is IInvoiceModule, LockupStreamCreator { } // Calculate the expected number of payments based on the invoice recurrence and payment interval - uint40 numberOfPayments = _computeNumberOfRecurringPayments(recurrence, startTime, endTime); + uint40 numberOfPayments = Helpers.computeNumberOfRecurringPayments(recurrence, startTime, endTime); // Checks: the specified number of payments is valid if (paymentsLeft != numberOfPayments) { revert Errors.InvalidNumberOfPayments({ expectedNumber: numberOfPayments }); } } - - /// @dev Calculates the number of payments that must be done for a Recurring invoice that must be paid in transfers - function _computeNumberOfRecurringPayments( - Types.Recurrence recurrence, - uint40 startTime, - uint40 endTime - ) internal pure returns (uint40 numberOfPayments) { - uint40 interval = endTime - startTime; - - if (recurrence == Types.Recurrence.Weekly) { - numberOfPayments = interval / 1 weeks; - } else if (recurrence == Types.Recurrence.Monthly) { - numberOfPayments = interval / 4 weeks; - } else if (recurrence == Types.Recurrence.Yearly) { - numberOfPayments = interval / 48 weeks; - } - } } diff --git a/src/modules/invoice-module/libraries/Types.sol b/src/modules/invoice-module/libraries/Types.sol index 1cd5d7c6..b3e511dd 100644 --- a/src/modules/invoice-module/libraries/Types.sol +++ b/src/modules/invoice-module/libraries/Types.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; +/// @notice Namespace for the structs used across the Invoice Module contracts library Types { enum Recurrence { OneTime, @@ -11,7 +12,8 @@ library Types { enum Method { Transfer, - Stream + LinearStream, + TranchedStream } struct Payment { From d8adafd424f32c041abdca150bf8bf756c13f1fb Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Thu, 11 Jul 2024 12:27:12 +0300 Subject: [PATCH 17/40] refactor(invoice-module): replace 'Frequency' check with the 'paymentsLeft' field --- src/modules/invoice-module/InvoiceModule.sol | 3 +-- src/modules/invoice-module/libraries/Types.sol | 8 +------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/modules/invoice-module/InvoiceModule.sol b/src/modules/invoice-module/InvoiceModule.sol index ad846e9e..1f7d56e5 100644 --- a/src/modules/invoice-module/InvoiceModule.sol +++ b/src/modules/invoice-module/InvoiceModule.sol @@ -88,7 +88,7 @@ contract InvoiceModule is IInvoiceModule, LockupStreamCreator { // Checks: validate the input parameters if the invoice must be paid in even transfers if (invoice.payment.method == Types.Method.Transfer) { // Checks: validate the input parameters if the invoice is recurring - if (invoice.frequency == Types.Frequency.Recurring) { + if (invoice.payment.paymentsLeft > 1) { _checkRecurringTransferInvoiceParams({ recurrence: invoice.payment.recurrence, paymentsLeft: invoice.payment.paymentsLeft, @@ -105,7 +105,6 @@ contract InvoiceModule is IInvoiceModule, LockupStreamCreator { _invoices[id] = Types.Invoice({ recipient: msg.sender, status: Types.Status.Pending, - frequency: invoice.frequency, startTime: invoice.startTime, endTime: invoice.endTime, payment: Types.Payment({ diff --git a/src/modules/invoice-module/libraries/Types.sol b/src/modules/invoice-module/libraries/Types.sol index b3e511dd..fac3c223 100644 --- a/src/modules/invoice-module/libraries/Types.sol +++ b/src/modules/invoice-module/libraries/Types.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.26; /// @notice Namespace for the structs used across the Invoice Module contracts library Types { enum Recurrence { - OneTime, + OneOff, Weekly, Monthly, Yearly @@ -26,11 +26,6 @@ library Types { uint128 amount; } - enum Frequency { - Regular, - Recurring - } - enum Status { Pending, Ongoing, @@ -42,7 +37,6 @@ library Types { // slot 0 address recipient; Status status; - Frequency frequency; uint40 startTime; uint40 endTime; // slot 1 and 2 From f3a6de80bdc9467c65f76c6890ade140bff9bf3b Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Thu, 11 Jul 2024 12:29:26 +0300 Subject: [PATCH 18/40] test: fix create invoice helper --- test/utils/Helpers.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/utils/Helpers.sol b/test/utils/Helpers.sol index a4cedd53..85f03d66 100644 --- a/test/utils/Helpers.sol +++ b/test/utils/Helpers.sol @@ -9,12 +9,11 @@ library Helpers { InvoiceModulesTypes.Invoice({ recipient: recipient, status: InvoiceModulesTypes.Status.Pending, - frequency: InvoiceModulesTypes.Frequency.Regular, startTime: 0, endTime: uint40(block.timestamp) + 1 weeks, payment: InvoiceModulesTypes.Payment({ method: InvoiceModulesTypes.Method.Transfer, - recurrence: InvoiceModulesTypes.Recurrence.OneTime, + recurrence: InvoiceModulesTypes.Recurrence.OneOff, paymentsLeft: 1, asset: address(0), amount: uint128(1 ether) From c44dfa2ef3bf68df9fbd18e3753b1fe0bf5134c7 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Thu, 11 Jul 2024 13:29:18 +0300 Subject: [PATCH 19/40] feat(invoice-module): update checks, emitted event types and 'Payment' struct --- src/modules/invoice-module/InvoiceModule.sol | 34 ++++++++++++------- .../interfaces/IInvoiceModule.sol | 19 +++++++++-- .../invoice-module/libraries/Types.sol | 2 ++ 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/modules/invoice-module/InvoiceModule.sol b/src/modules/invoice-module/InvoiceModule.sol index 1f7d56e5..30184701 100644 --- a/src/modules/invoice-module/InvoiceModule.sol +++ b/src/modules/invoice-module/InvoiceModule.sol @@ -79,6 +79,11 @@ contract InvoiceModule is IInvoiceModule, LockupStreamCreator { revert Errors.PaymentAmountZero(); } + // 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) { @@ -96,6 +101,9 @@ contract InvoiceModule is IInvoiceModule, LockupStreamCreator { endTime: invoice.endTime }); } + // Or by using a linear or tranched stream in which case allow only ERC-20 assets + } else if (invoice.payment.asset == address(0)) { + revert Errors.OnlyERC20StreamsAllowed(); } // Get the next invoice ID @@ -112,7 +120,8 @@ contract InvoiceModule is IInvoiceModule, LockupStreamCreator { method: invoice.payment.method, paymentsLeft: invoice.payment.paymentsLeft, amount: invoice.payment.amount, - asset: invoice.payment.asset + asset: invoice.payment.asset, + streamId: 0 }) }); @@ -125,7 +134,15 @@ contract InvoiceModule is IInvoiceModule, LockupStreamCreator { // Effects: add the invoice on the list of invoices generated by the container _invoicesOf[msg.sender].push(id); - emit InvoiceCreated({ id: id, invoice: invoice }); + // Log the invoice creation + emit InvoiceCreated({ + id: id, + recipient: msg.sender, + status: Types.Status.Pending, + startTime: invoice.startTime, + endTime: invoice.endTime, + payment: invoice.payment + }); } /// @inheritdoc IInvoiceModule @@ -144,11 +161,6 @@ contract InvoiceModule is IInvoiceModule, LockupStreamCreator { if (invoice.payment.method == Types.Method.Transfer) { _payByTransfer(id, invoice); } else { - // Allow only ERC-20 based streams - if (invoice.payment.asset == address(0)) { - revert Errors.OnlyERC20StreamsAllowed(); - } - uint256 streamId; // Check to see wether to pay by creating a linear or tranched stream if (invoice.payment.method == Types.Method.LinearStream) { @@ -156,7 +168,8 @@ contract InvoiceModule is IInvoiceModule, LockupStreamCreator { } else streamId = _payByTranchedStream(invoice); } - emit InvoicePaid({ id: id, payer: msg.sender }); + // Log the payment transaction + emit InvoicePaid({ id: id, payer: msg.sender, status: invoice.status, payment: invoice.payment }); } /*////////////////////////////////////////////////////////////////////////// @@ -227,11 +240,6 @@ contract InvoiceModule is IInvoiceModule, LockupStreamCreator { uint40 startTime, uint40 endTime ) internal pure { - // Checks: the invoice interval if valid - if (startTime < endTime) { - revert Errors.StartTimeGreaterThanEndTime(); - } - // Calculate the expected number of payments based on the invoice recurrence and payment interval uint40 numberOfPayments = Helpers.computeNumberOfRecurringPayments(recurrence, startTime, endTime); diff --git a/src/modules/invoice-module/interfaces/IInvoiceModule.sol b/src/modules/invoice-module/interfaces/IInvoiceModule.sol index ea97de12..da661771 100644 --- a/src/modules/invoice-module/interfaces/IInvoiceModule.sol +++ b/src/modules/invoice-module/interfaces/IInvoiceModule.sol @@ -12,13 +12,26 @@ interface IInvoiceModule { /// @notice Emitted when a regular or recurring invoice is created /// @param id The ID of the invoice - /// @param invoice The details of the invoice following the {Invoice} struct format - event InvoiceCreated(uint256 indexed id, Types.Invoice 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 a regular or recurring invoice is paid /// @param id The ID of the invoice /// @param payer The address of the payer - event InvoicePaid(uint256 indexed id, address indexed 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); /*////////////////////////////////////////////////////////////////////////// CONSTANT FUNCTIONS diff --git a/src/modules/invoice-module/libraries/Types.sol b/src/modules/invoice-module/libraries/Types.sol index fac3c223..6b720ef6 100644 --- a/src/modules/invoice-module/libraries/Types.sol +++ b/src/modules/invoice-module/libraries/Types.sol @@ -24,6 +24,8 @@ library Types { address asset; // slot 1 uint128 amount; + // slot 2 + uint256 streamId; } enum Status { From d428de4b5408257880f7a0363a3ffc4b780e0e88 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Thu, 11 Jul 2024 13:31:05 +0300 Subject: [PATCH 20/40] test: fix create invoice helper --- test/utils/Helpers.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/utils/Helpers.sol b/test/utils/Helpers.sol index 85f03d66..786731dc 100644 --- a/test/utils/Helpers.sol +++ b/test/utils/Helpers.sol @@ -16,7 +16,8 @@ library Helpers { recurrence: InvoiceModulesTypes.Recurrence.OneOff, paymentsLeft: 1, asset: address(0), - amount: uint128(1 ether) + amount: uint128(1 ether), + streamId: 0 }) }); } From 6a7e212b5b70a1cccfb2cae22e99631432c04ad8 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Thu, 11 Jul 2024 15:23:48 +0300 Subject: [PATCH 21/40] feat(invoice-module): add 'StreamManager' contract to handle stream management --- src/modules/invoice-module/StreamManager.sol | 49 +++++++++++++++++++ .../interfaces/IStreamManager.sol | 32 ++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 src/modules/invoice-module/StreamManager.sol create mode 100644 src/modules/invoice-module/interfaces/IStreamManager.sol diff --git a/src/modules/invoice-module/StreamManager.sol b/src/modules/invoice-module/StreamManager.sol new file mode 100644 index 00000000..f468d8b3 --- /dev/null +++ b/src/modules/invoice-module/StreamManager.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { IStreamManager } from "./interfaces/IStreamManager.sol"; +import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol"; + +/// @title StreamManager +/// @notice See the documentation in {IStreamManager} +contract StreamManager is IStreamManager { + /*////////////////////////////////////////////////////////////////////////// + PUBLIC STORAGE + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IStreamManager + ISablierV2Lockup public immutable override sablier; + + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Initializes the {ISablierV2Lockup} contract address + constructor(ISablierV2Lockup _sablier) { + sablier = _sablier; + } + + /*////////////////////////////////////////////////////////////////////////// + NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IStreamManager + function withdraw(uint256 streamId, address to, uint128 amount) external { + sablier.withdraw(streamId, to, amount); + } + + /// @inheritdoc IStreamManager + function withdrawableAmountOf(uint256 streamId) external view returns (uint128 withdrawableAmount) { + withdrawableAmount = sablier.withdrawableAmountOf(streamId); + } + + /// @inheritdoc IStreamManager + function withdrawMax(uint256 streamId, address to) external returns (uint128 withdrawnAmount) { + withdrawnAmount = sablier.withdrawMax(streamId, to); + } + + /// @inheritdoc IStreamManager + function withdrawMultiple(uint256[] calldata streamIds, uint128[] calldata amounts) external { + sablier.withdrawMultiple({ streamIds: streamIds, amounts: amounts }); + } +} diff --git a/src/modules/invoice-module/interfaces/IStreamManager.sol b/src/modules/invoice-module/interfaces/IStreamManager.sol new file mode 100644 index 00000000..dc1a7f51 --- /dev/null +++ b/src/modules/invoice-module/interfaces/IStreamManager.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol"; + +/// @title IStreamManager +/// @notice Contract responsible to handle multiple management actions such as withdraw, cancel or renounce stream and transfer ownership +/// @dev This interface is a subset of the {ISablierV2Lockup} interface +interface IStreamManager { + /*////////////////////////////////////////////////////////////////////////// + CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice The address of the {SablierV2Lockup} contract used to handle streams management + function sablier() external view returns (ISablierV2Lockup); + + /*////////////////////////////////////////////////////////////////////////// + NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice See the documentation in {ISablierV2Lockup} + function withdraw(uint256 streamId, address to, uint128 amount) external; + + /// @notice See the documentation in {ISablierV2Lockup} + function withdrawableAmountOf(uint256 streamId) external view returns (uint128 withdrawableAmount); + + /// @notice See the documentation in {ISablierV2Lockup} + function withdrawMax(uint256 streamId, address to) external returns (uint128 withdrawnAmount); + + /// @notice See the documentation in {ISablierV2Lockup} + function withdrawMultiple(uint256[] calldata streamIds, uint128[] calldata amounts) external; +} From aed6eec6a904851dcc0c754e87c5fc681b2a28a8 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Thu, 11 Jul 2024 15:41:14 +0300 Subject: [PATCH 22/40] feat: add handlers to cancel, renounce and transfer ownership of a stream --- src/modules/invoice-module/StreamManager.sol | 22 ++++++++++++++++++- .../interfaces/IStreamManager.sol | 12 ++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/modules/invoice-module/StreamManager.sol b/src/modules/invoice-module/StreamManager.sol index f468d8b3..f9f98a8d 100644 --- a/src/modules/invoice-module/StreamManager.sol +++ b/src/modules/invoice-module/StreamManager.sol @@ -44,6 +44,26 @@ contract StreamManager is IStreamManager { /// @inheritdoc IStreamManager function withdrawMultiple(uint256[] calldata streamIds, uint128[] calldata amounts) external { - sablier.withdrawMultiple({ streamIds: streamIds, amounts: amounts }); + sablier.withdrawMultiple(streamIds, amounts); + } + + /// @inheritdoc IStreamManager + function withdrawMaxAndTransfer(uint256 streamId, address newRecipient) external returns (uint128 withdrawnAmount) { + withdrawnAmount = sablier.withdrawMaxAndTransfer(streamId, newRecipient); + } + + /// @inheritdoc IStreamManager + function cancel(uint256 streamId) external { + sablier.cancel(streamId); + } + + /// @inheritdoc IStreamManager + function cancelMultiple(uint256[] calldata streamIds) external { + sablier.cancelMultiple(streamIds); + } + + /// @inheritdoc IStreamManager + function renounce(uint256 streamId) external { + sablier.renounce(streamId); } } diff --git a/src/modules/invoice-module/interfaces/IStreamManager.sol b/src/modules/invoice-module/interfaces/IStreamManager.sol index dc1a7f51..1bf75efe 100644 --- a/src/modules/invoice-module/interfaces/IStreamManager.sol +++ b/src/modules/invoice-module/interfaces/IStreamManager.sol @@ -29,4 +29,16 @@ interface IStreamManager { /// @notice See the documentation in {ISablierV2Lockup} function withdrawMultiple(uint256[] calldata streamIds, uint128[] calldata amounts) external; + + /// @notice See the documentation in {ISablierV2Lockup} + function withdrawMaxAndTransfer(uint256 streamId, address newRecipient) external returns (uint128 withdrawnAmount); + + /// @notice See the documentation in {ISablierV2Lockup} + function cancel(uint256 streamId) external; + + /// @notice See the documentation in {ISablierV2Lockup} + function cancelMultiple(uint256[] calldata streamIds) external; + + /// @notice See the documentation in {ISablierV2Lockup} + function renounce(uint256 streamId) external; } From 43aa8aac59b0a4eb0c819d5982fe88c9f96ba52c Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Fri, 12 Jul 2024 08:37:17 +0300 Subject: [PATCH 23/40] feat(invoice-module): manage Sablier v2 streams through the 'StreamCreator' contract and update folder structure --- src/modules/invoice-module/InvoiceModule.sol | 21 +++-- .../interfaces/ILockupStreamCreator.sol | 41 --------- .../StreamCreator.sol} | 47 +++++----- .../{ => sablier-v2}/StreamManager.sol | 0 .../sablier-v2/interfaces/IStreamCreator.sol | 85 +++++++++++++++++++ .../interfaces/IStreamManager.sol | 0 6 files changed, 125 insertions(+), 69 deletions(-) delete mode 100644 src/modules/invoice-module/interfaces/ILockupStreamCreator.sol rename src/modules/invoice-module/{LockupStreamCreator.sol => sablier-v2/StreamCreator.sol} (87%) rename src/modules/invoice-module/{ => sablier-v2}/StreamManager.sol (100%) create mode 100644 src/modules/invoice-module/sablier-v2/interfaces/IStreamCreator.sol rename src/modules/invoice-module/{ => sablier-v2}/interfaces/IStreamManager.sol (100%) diff --git a/src/modules/invoice-module/InvoiceModule.sol b/src/modules/invoice-module/InvoiceModule.sol index 30184701..2c6750e9 100644 --- a/src/modules/invoice-module/InvoiceModule.sol +++ b/src/modules/invoice-module/InvoiceModule.sol @@ -4,19 +4,20 @@ pragma solidity ^0.8.26; import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; +import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol"; import { Types } from "./libraries/Types.sol"; import { Errors } from "./libraries/Errors.sol"; import { IInvoiceModule } from "./interfaces/IInvoiceModule.sol"; import { IContainer } from "./../../interfaces/IContainer.sol"; -import { LockupStreamCreator } from "./LockupStreamCreator.sol"; -import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; -import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { StreamCreator } from "./sablier-v2/StreamCreator.sol"; import { Helpers } from "./libraries/Helpers.sol"; /// @title InvoiceModule /// @notice See the documentation in {IInvoiceModule} -contract InvoiceModule is IInvoiceModule, LockupStreamCreator { +contract InvoiceModule is IInvoiceModule, StreamCreator { using SafeERC20 for IERC20; /*////////////////////////////////////////////////////////////////////////// @@ -36,11 +37,13 @@ contract InvoiceModule is IInvoiceModule, LockupStreamCreator { CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ - /// @dev Initializes the {LockupStreamCreator} contract + /// @dev Initializes the {StreamCreator} contract constructor( - ISablierV2LockupLinear _sablierLockupDeployment, + ISablierV2Lockup _sablier, + ISablierV2LockupLinear _sablierLockupLinearDeployment, + ISablierV2LockupTranched _sablierLockupTranchedDeployment, address _brokerAdmin - ) LockupStreamCreator(_sablierLockupDeployment, _brokerAdmin) {} + ) StreamCreator(_sablier, _sablierLockupLinearDeployment, _sablierLockupTranchedDeployment, _brokerAdmin) {} /*////////////////////////////////////////////////////////////////////////// MODIFIERS @@ -212,7 +215,7 @@ contract InvoiceModule is IInvoiceModule, LockupStreamCreator { /// @dev Create the linear stream payment function _payByLinearStream(Types.Invoice memory invoice) internal returns (uint256 streamId) { - streamId = LockupStreamCreator.createLinearStream({ + streamId = StreamCreator.createLinearStream({ asset: IERC20(invoice.payment.asset), totalAmount: invoice.payment.amount, startTime: invoice.startTime, @@ -223,7 +226,7 @@ contract InvoiceModule is IInvoiceModule, LockupStreamCreator { /// @dev Create the tranched stream payment function _payByTranchedStream(Types.Invoice memory invoice) internal returns (uint256 streamId) { - streamId = LockupStreamCreator.createTranchedStream({ + streamId = StreamCreator.createTranchedStream({ asset: IERC20(invoice.payment.asset), totalAmount: invoice.payment.amount, startTime: invoice.startTime, diff --git a/src/modules/invoice-module/interfaces/ILockupStreamCreator.sol b/src/modules/invoice-module/interfaces/ILockupStreamCreator.sol deleted file mode 100644 index 4eb83fc4..00000000 --- a/src/modules/invoice-module/interfaces/ILockupStreamCreator.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22; - -import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; -import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; -import { UD60x18 } from "@prb/math/src/UD60x18.sol"; - -/// @title ILockupStreamCreator -/// @notice Contract used to create Sablier V2 compatible streams -/// @dev This code is referenced in the docs: https://docs.sablier.com/contracts/v2/guides/create-stream/lockup-linear -interface ILockupStreamCreator { - /*////////////////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Emitted when the broker fee is updated - /// @param oldFee The old broker fee - /// @param newFee The new broker fee - event BrokerFeeUpdated(UD60x18 oldFee, UD60x18 newFee); - - /*////////////////////////////////////////////////////////////////////////// - CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice The address of the {SablierV2LockupLinear} contract used to create linear streams - /// @dev This is initialized at construction time and it might be different depending on the deployment chain - /// See https://docs.sablier.com/contracts/v2/deployments - function LOCKUP_LINEAR() external view returns (ISablierV2LockupLinear); - - /// @notice The address of the {SablierV2LockupTranched} contract used to create tranched streams - /// @dev This is initialized at construction time and it might be different depending on the deployment chain - /// See https://docs.sablier.com/contracts/v2/deployments - function LOCKUP_TRANCHED() external view returns (ISablierV2LockupTranched); - - /// @notice The address of the broker admin account or contract managing the broker fee - function brokerAdmin() external view returns (address); - - /// @notice The broker fee charged to create Sablier V2 stream - /// @dev See the `UD60x18` type definition in the `@prb/math/src/ud60x18/ValueType.sol file` - function brokerFee() external view returns (UD60x18); -} diff --git a/src/modules/invoice-module/LockupStreamCreator.sol b/src/modules/invoice-module/sablier-v2/StreamCreator.sol similarity index 87% rename from src/modules/invoice-module/LockupStreamCreator.sol rename to src/modules/invoice-module/sablier-v2/StreamCreator.sol index db9d3560..33bc5208 100644 --- a/src/modules/invoice-module/LockupStreamCreator.sol +++ b/src/modules/invoice-module/sablier-v2/StreamCreator.sol @@ -1,34 +1,37 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ud60x18, UD60x18 } from "@prb/math/src/UD60x18.sol"; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; -import { ILockupStreamCreator } from "./interfaces/ILockupStreamCreator.sol"; -import { Broker, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol"; import { LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; -import { Errors } from "./libraries/Errors.sol"; -import { Helpers } from "./libraries/Helpers.sol"; -import { Types } from "./libraries/Types.sol"; +import { Broker, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ud60x18, UD60x18 } from "@prb/math/src/UD60x18.sol"; + +import { IStreamCreator } from "./interfaces/IStreamCreator.sol"; +import { StreamManager } from "./StreamManager.sol"; +import { Helpers } from "./../libraries/Helpers.sol"; +import { Errors } from "./../libraries/Errors.sol"; +import { Types } from "./../libraries/Types.sol"; -/// @title LockupStreamCreator -/// @dev See the documentation in {ILockupStreamCreator} -contract LockupStreamCreator is ILockupStreamCreator { +/// @title StreamCreator +/// @dev See the documentation in {IStreamCreator} +contract StreamCreator is IStreamCreator, StreamManager { /*////////////////////////////////////////////////////////////////////////// PUBLIC STORAGE //////////////////////////////////////////////////////////////////////////*/ - /// @inheritdoc ILockupStreamCreator + /// @inheritdoc IStreamCreator ISablierV2LockupLinear public immutable override LOCKUP_LINEAR; - /// @inheritdoc ILockupStreamCreator + /// @inheritdoc IStreamCreator ISablierV2LockupTranched public immutable override LOCKUP_TRANCHED; - /// @inheritdoc ILockupStreamCreator + /// @inheritdoc IStreamCreator address public override brokerAdmin; - /// @inheritdoc ILockupStreamCreator + /// @inheritdoc IStreamCreator UD60x18 public override brokerFee; /*////////////////////////////////////////////////////////////////////////// @@ -36,8 +39,14 @@ contract LockupStreamCreator is ILockupStreamCreator { //////////////////////////////////////////////////////////////////////////*/ /// @dev Initializes the address of the {SablierV2LockupLinear} contract and the address of the broker admin account or contract - constructor(ISablierV2LockupLinear _sablierLockupDeployment, address _brokerAdmin) { - LOCKUP_LINEAR = _sablierLockupDeployment; + constructor( + ISablierV2Lockup _sablier, + ISablierV2LockupLinear _sablierLockupLinearDeployment, + ISablierV2LockupTranched _sablierLockupTranchedDeployment, + address _brokerAdmin + ) StreamManager(_sablier) { + LOCKUP_LINEAR = _sablierLockupLinearDeployment; + LOCKUP_TRANCHED = _sablierLockupTranchedDeployment; brokerAdmin = _brokerAdmin; } @@ -55,7 +64,7 @@ contract LockupStreamCreator is ILockupStreamCreator { NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Creates a Lockup Linear stream + /// @inheritdoc IStreamCreator function createLinearStream( IERC20 asset, uint128 totalAmount, @@ -70,7 +79,7 @@ contract LockupStreamCreator is ILockupStreamCreator { streamId = _createLinearStream(asset, totalAmount, startTime, endTime, recipient); } - /// @dev Creates a Lockup Tranched stream + /// @inheritdoc IStreamCreator function createTranchedStream( IERC20 asset, uint128 totalAmount, @@ -86,7 +95,7 @@ contract LockupStreamCreator is ILockupStreamCreator { streamId = _createTranchedStream(asset, totalAmount, startTime, endTime, recipient, recurrence); } - /// @dev Updates the fee charged by the broker + /// @inheritdoc IStreamCreator function updateBrokerFee(UD60x18 newBrokerFee) public onlyBrokerAdmin { // Log the broker fee update emit BrokerFeeUpdated({ oldFee: brokerFee, newFee: newBrokerFee }); diff --git a/src/modules/invoice-module/StreamManager.sol b/src/modules/invoice-module/sablier-v2/StreamManager.sol similarity index 100% rename from src/modules/invoice-module/StreamManager.sol rename to src/modules/invoice-module/sablier-v2/StreamManager.sol diff --git a/src/modules/invoice-module/sablier-v2/interfaces/IStreamCreator.sol b/src/modules/invoice-module/sablier-v2/interfaces/IStreamCreator.sol new file mode 100644 index 00000000..ebc37071 --- /dev/null +++ b/src/modules/invoice-module/sablier-v2/interfaces/IStreamCreator.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.22; + +import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { UD60x18 } from "@prb/math/src/UD60x18.sol"; +import { Types } from "./../../libraries/Types.sol"; + +/// @title IStreamCreator +/// @notice Contract used to create Sablier V2 compatible streams +/// @dev This code is referenced in the docs: https://docs.sablier.com/concepts/protocol/stream-types +interface IStreamCreator { + /*////////////////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when the broker fee is updated + /// @param oldFee The old broker fee + /// @param newFee The new broker fee + event BrokerFeeUpdated(UD60x18 oldFee, UD60x18 newFee); + + /*////////////////////////////////////////////////////////////////////////// + CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice The address of the {SablierV2LockupLinear} contract used to create linear streams + /// @dev This is initialized at construction time and it might be different depending on the deployment chain + /// See https://docs.sablier.com/contracts/v2/deployments + function LOCKUP_LINEAR() external view returns (ISablierV2LockupLinear); + + /// @notice The address of the {SablierV2LockupTranched} contract used to create tranched streams + /// @dev This is initialized at construction time and it might be different depending on the deployment chain + /// See https://docs.sablier.com/contracts/v2/deployments + function LOCKUP_TRANCHED() external view returns (ISablierV2LockupTranched); + + /// @notice The address of the broker admin account or contract managing the broker fee + function brokerAdmin() external view returns (address); + + /// @notice The broker fee charged to create Sablier V2 stream + /// @dev See the `UD60x18` type definition in the `@prb/math/src/ud60x18/ValueType.sol file` + function brokerFee() external view returns (UD60x18); + + /*////////////////////////////////////////////////////////////////////////// + NON-CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Creates a Lockup Linear stream; See https://docs.sablier.com/concepts/protocol/stream-types#lockup-linear + /// @param asset The address of the ERC-20 token to be streamed + /// @param totalAmount The total amount of ERC-20 tokens to be streamed + /// @param startTime The timestamp when the stream takes effect + /// @param endTime The timestamp by which the stream must be paid + /// @param recipient The address receiving the ERC-20 tokens + function createLinearStream( + IERC20 asset, + uint128 totalAmount, + uint40 startTime, + uint40 endTime, + address recipient + ) 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 + /// @param totalAmount The total amount of ERC-20 tokens to be streamed + /// @param startTime The timestamp when the stream takes effect + /// @param endTime The timestamp by which the stream must be paid + /// @param recipient The address receiving the ERC-20 tokens + /// @param recurrence The recurrence of each tranche + function createTranchedStream( + IERC20 asset, + uint128 totalAmount, + uint40 startTime, + uint40 endTime, + address recipient, + Types.Recurrence recurrence + ) external returns (uint256 streamId); + + /// @notice Updates the fee charged by the broker + /// + /// Notes: + /// - The new fee will be applied only to the new streams hence it can't be retrospectively updated + /// + /// @param newBrokerFee The new broker fee + function updateBrokerFee(UD60x18 newBrokerFee) external; +} diff --git a/src/modules/invoice-module/interfaces/IStreamManager.sol b/src/modules/invoice-module/sablier-v2/interfaces/IStreamManager.sol similarity index 100% rename from src/modules/invoice-module/interfaces/IStreamManager.sol rename to src/modules/invoice-module/sablier-v2/interfaces/IStreamManager.sol From 224969f4af1592e80cc32c8783c6a2bdd82af786 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Fri, 12 Jul 2024 08:38:04 +0300 Subject: [PATCH 24/40] test: update 'Base.t.sol' to support the new structure of the invoice module --- test/Base.t.sol | 15 +++++---------- test/unit/concrete/container/Container.t.sol | 3 +-- .../concrete/module-manager/ModuleManager.t.sol | 5 ++--- .../module-manager/constructor/constructor.t.sol | 10 ++-------- .../enable-module/enableModule.t.sol | 1 - 5 files changed, 10 insertions(+), 24 deletions(-) diff --git a/test/Base.t.sol b/test/Base.t.sol index 24817092..e010f0eb 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -9,6 +9,8 @@ import { MockModule } from "./mocks/MockModule.sol"; import { Container } from "./../src/Container.sol"; import { InvoiceModule } from "./../src/modules/invoice-module/InvoiceModule.sol"; import { SablierV2LockupLinear } from "@sablier/v2-core/src/SablierV2LockupLinear.sol"; +import { SablierV2LockupTranched } from "@sablier/v2-core/src/SablierV2LockupTranched.sol"; +import { SablierV2Lockup } from "@sablier/v2-core/src/abstracts/SablierV2Lockup.sol"; import { NFTDescriptorMock } from "@sablier/v2-core/test/mocks/NFTDescriptorMock.sol"; abstract contract Base_Test is Test, Events { @@ -30,6 +32,8 @@ abstract contract Base_Test is Test, Events { // Sablier V2 related test contracts NFTDescriptorMock internal mockNFTDescriptor; SablierV2LockupLinear internal sablierV2LockupLinear; + SablierV2LockupTranched internal sablierV2LockupTranched; + SablierV2Lockup internal sablier; /*////////////////////////////////////////////////////////////////////////// SET-UP FUNCTION @@ -43,20 +47,11 @@ abstract contract Base_Test is Test, Events { users = Users({ admin: createUser("admin"), eve: createUser("eve"), bob: createUser("bob") }); // Deploy test contracts - mockNFTDescriptor = new NFTDescriptorMock(); - sablierV2LockupLinear = new SablierV2LockupLinear({ - initialAdmin: users.admin, - initialNFTDescriptor: mockNFTDescriptor - }); - invoiceModule = new InvoiceModule({ - _brokerAdmin: users.admin, - _sablierLockupDeployment: sablierV2LockupLinear - }); mockModule = new MockModule(); // Label the test contracts so we can easily track them vm.label({ account: address(usdt), newLabel: "USDT" }); - vm.label({ account: address(invoiceModule), newLabel: "InvoiceModule" }); + vm.label({ account: address(mockModule), newLabel: "MockModule" }); } /*////////////////////////////////////////////////////////////////////////// diff --git a/test/unit/concrete/container/Container.t.sol b/test/unit/concrete/container/Container.t.sol index 25a4b09c..4514beea 100644 --- a/test/unit/concrete/container/Container.t.sol +++ b/test/unit/concrete/container/Container.t.sol @@ -7,9 +7,8 @@ contract Container_Unit_Concrete_Test is Base_Test { function setUp() public virtual override { Base_Test.setUp(); - address[] memory modules = new address[](2); + address[] memory modules = new address[](1); modules[0] = address(mockModule); - modules[1] = address(invoiceModule); container = deployContainer({ owner: users.eve, initialModules: modules }); } diff --git a/test/unit/concrete/module-manager/ModuleManager.t.sol b/test/unit/concrete/module-manager/ModuleManager.t.sol index 0f3a72d6..99b65ff4 100644 --- a/test/unit/concrete/module-manager/ModuleManager.t.sol +++ b/test/unit/concrete/module-manager/ModuleManager.t.sol @@ -11,9 +11,8 @@ contract ModuleManager_Unit_Concrete_Test is Base_Test { Base_Test.setUp(); // Create the initial modules array - address[] memory modules = new address[](2); - modules[0] = address(invoiceModule); - modules[1] = address(mockModule); + address[] memory modules = new address[](1); + modules[0] = address(mockModule); // Deploy the {ModuleManager} with the `modules` initial modules enabled moduleManager = new ModuleManager({ _initialModules: modules }); diff --git a/test/unit/concrete/module-manager/constructor/constructor.t.sol b/test/unit/concrete/module-manager/constructor/constructor.t.sol index 72a1889d..890dc656 100644 --- a/test/unit/concrete/module-manager/constructor/constructor.t.sol +++ b/test/unit/concrete/module-manager/constructor/constructor.t.sol @@ -13,24 +13,18 @@ contract Constructor_Unit_Concrete_Test is Base_Test { } function test_Constructor() external { - // Expect the {ModuleEnabled} event to be emitted - vm.expectEmit(); - emit Events.ModuleEnabled({ module: address(invoiceModule) }); - // Expect the {ModuleEnabled} event to be emitted vm.expectEmit(); emit Events.ModuleEnabled({ module: address(mockModule) }); // Create the initial modules array - address[] memory modules = new address[](2); - modules[0] = address(invoiceModule); - modules[1] = address(mockModule); + address[] memory modules = new address[](1); + modules[0] = address(mockModule); // Deploy the {ModuleManager} with the `modules` initial modules enabled moduleManager = new ModuleManager({ _initialModules: modules }); // Assert the modules enablement state - assertTrue(moduleManager.isModuleEnabled({ module: address(invoiceModule) })); assertTrue(moduleManager.isModuleEnabled({ module: address(mockModule) })); } } diff --git a/test/unit/concrete/module-manager/enable-module/enableModule.t.sol b/test/unit/concrete/module-manager/enable-module/enableModule.t.sol index 80549066..96e8d03a 100644 --- a/test/unit/concrete/module-manager/enable-module/enableModule.t.sol +++ b/test/unit/concrete/module-manager/enable-module/enableModule.t.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.26; import { ModuleManager_Unit_Concrete_Test } from "../ModuleManager.t.sol"; -import { InvoiceModule } from "./../../../../../src/modules/invoice-module/InvoiceModule.sol"; import { Events } from "../../../../utils/Events.sol"; import { Errors } from "../../../../utils/Errors.sol"; import { MockModule } from "../../../../mocks/MockModule.sol"; From 121951c109139fdfe11b2e11e5bbf1bc79ec7b22 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Fri, 12 Jul 2024 16:54:09 +0300 Subject: [PATCH 25/40] refactor: merge 'StreamManager' and 'StreamCreator' contracts --- src/modules/invoice-module/InvoiceModule.sol | 14 +- .../sablier-v2/StreamCreator.sol | 198 --------------- .../sablier-v2/StreamManager.sol | 233 ++++++++++++++++-- .../sablier-v2/interfaces/IStreamCreator.sol | 85 ------- .../sablier-v2/interfaces/IStreamManager.sol | 107 +++++++- 5 files changed, 316 insertions(+), 321 deletions(-) delete mode 100644 src/modules/invoice-module/sablier-v2/StreamCreator.sol delete mode 100644 src/modules/invoice-module/sablier-v2/interfaces/IStreamCreator.sol diff --git a/src/modules/invoice-module/InvoiceModule.sol b/src/modules/invoice-module/InvoiceModule.sol index 2c6750e9..c4115756 100644 --- a/src/modules/invoice-module/InvoiceModule.sol +++ b/src/modules/invoice-module/InvoiceModule.sol @@ -6,18 +6,17 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; -import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol"; import { Types } from "./libraries/Types.sol"; import { Errors } from "./libraries/Errors.sol"; import { IInvoiceModule } from "./interfaces/IInvoiceModule.sol"; import { IContainer } from "./../../interfaces/IContainer.sol"; -import { StreamCreator } from "./sablier-v2/StreamCreator.sol"; +import { StreamManager } from "./sablier-v2/StreamManager.sol"; import { Helpers } from "./libraries/Helpers.sol"; /// @title InvoiceModule /// @notice See the documentation in {IInvoiceModule} -contract InvoiceModule is IInvoiceModule, StreamCreator { +contract InvoiceModule is IInvoiceModule, StreamManager { using SafeERC20 for IERC20; /*////////////////////////////////////////////////////////////////////////// @@ -37,13 +36,12 @@ contract InvoiceModule is IInvoiceModule, StreamCreator { CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ - /// @dev Initializes the {StreamCreator} contract + /// @dev Initializes the {StreamManager} contract constructor( - ISablierV2Lockup _sablier, ISablierV2LockupLinear _sablierLockupLinearDeployment, ISablierV2LockupTranched _sablierLockupTranchedDeployment, address _brokerAdmin - ) StreamCreator(_sablier, _sablierLockupLinearDeployment, _sablierLockupTranchedDeployment, _brokerAdmin) {} + ) StreamManager(_sablierLockupLinearDeployment, _sablierLockupTranchedDeployment, _brokerAdmin) {} /*////////////////////////////////////////////////////////////////////////// MODIFIERS @@ -215,7 +213,7 @@ contract InvoiceModule is IInvoiceModule, StreamCreator { /// @dev Create the linear stream payment function _payByLinearStream(Types.Invoice memory invoice) internal returns (uint256 streamId) { - streamId = StreamCreator.createLinearStream({ + streamId = StreamManager.createLinearStream({ asset: IERC20(invoice.payment.asset), totalAmount: invoice.payment.amount, startTime: invoice.startTime, @@ -226,7 +224,7 @@ contract InvoiceModule is IInvoiceModule, StreamCreator { /// @dev Create the tranched stream payment function _payByTranchedStream(Types.Invoice memory invoice) internal returns (uint256 streamId) { - streamId = StreamCreator.createTranchedStream({ + streamId = StreamManager.createTranchedStream({ asset: IERC20(invoice.payment.asset), totalAmount: invoice.payment.amount, startTime: invoice.startTime, diff --git a/src/modules/invoice-module/sablier-v2/StreamCreator.sol b/src/modules/invoice-module/sablier-v2/StreamCreator.sol deleted file mode 100644 index 33bc5208..00000000 --- a/src/modules/invoice-module/sablier-v2/StreamCreator.sol +++ /dev/null @@ -1,198 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22; - -import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; -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 { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ud60x18, UD60x18 } from "@prb/math/src/UD60x18.sol"; - -import { IStreamCreator } from "./interfaces/IStreamCreator.sol"; -import { StreamManager } from "./StreamManager.sol"; -import { Helpers } from "./../libraries/Helpers.sol"; -import { Errors } from "./../libraries/Errors.sol"; -import { Types } from "./../libraries/Types.sol"; - -/// @title StreamCreator -/// @dev See the documentation in {IStreamCreator} -contract StreamCreator is IStreamCreator, StreamManager { - /*////////////////////////////////////////////////////////////////////////// - PUBLIC STORAGE - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc IStreamCreator - ISablierV2LockupLinear public immutable override LOCKUP_LINEAR; - - /// @inheritdoc IStreamCreator - ISablierV2LockupTranched public immutable override LOCKUP_TRANCHED; - - /// @inheritdoc IStreamCreator - address public override brokerAdmin; - - /// @inheritdoc IStreamCreator - UD60x18 public override brokerFee; - - /*////////////////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////////////////*/ - - /// @dev Initializes the address of the {SablierV2LockupLinear} contract and the address of the broker admin account or contract - constructor( - ISablierV2Lockup _sablier, - ISablierV2LockupLinear _sablierLockupLinearDeployment, - ISablierV2LockupTranched _sablierLockupTranchedDeployment, - address _brokerAdmin - ) StreamManager(_sablier) { - LOCKUP_LINEAR = _sablierLockupLinearDeployment; - LOCKUP_TRANCHED = _sablierLockupTranchedDeployment; - brokerAdmin = _brokerAdmin; - } - - /*////////////////////////////////////////////////////////////////////////// - MODIFIERS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Reverts if the `msg.sender` is not the broker admin account or contract - modifier onlyBrokerAdmin() { - if (msg.sender != brokerAdmin) revert Errors.OnlyBrokerAdmin(); - _; - } - - /*////////////////////////////////////////////////////////////////////////// - NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @inheritdoc IStreamCreator - function createLinearStream( - IERC20 asset, - uint128 totalAmount, - uint40 startTime, - uint40 endTime, - address recipient - ) 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, spender: address(LOCKUP_LINEAR), amount: totalAmount }); - - // Create the Lockup Linear stream - streamId = _createLinearStream(asset, totalAmount, startTime, endTime, recipient); - } - - /// @inheritdoc IStreamCreator - function createTranchedStream( - IERC20 asset, - uint128 totalAmount, - uint40 startTime, - uint40 endTime, - address recipient, - Types.Recurrence recurrence - ) 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, spender: address(LOCKUP_TRANCHED), amount: totalAmount }); - - // Create the Lockup Linear stream - streamId = _createTranchedStream(asset, totalAmount, startTime, endTime, recipient, recurrence); - } - - /// @inheritdoc IStreamCreator - function updateBrokerFee(UD60x18 newBrokerFee) public onlyBrokerAdmin { - // Log the broker fee update - emit BrokerFeeUpdated({ oldFee: brokerFee, newFee: newBrokerFee }); - - // Update the fee charged by the broker - brokerFee = newBrokerFee; - } - - /*////////////////////////////////////////////////////////////////////////// - INTERNAL-METHODS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Creates a Lockup Linear stream - /// See https://docs.sablier.com/concepts/protocol/stream-types#lockup-linear - /// @dev See https://docs.sablier.com/contracts/v2/guides/create-stream/lockup-linear - function _createLinearStream( - IERC20 asset, - uint128 totalAmount, - uint40 startTime, - uint40 endTime, - address recipient - ) internal returns (uint256 streamId) { - // Declare the params struct - LockupLinear.CreateWithTimestamps memory params; - - // Declare the function parameters - params.sender = msg.sender; // The sender will be able to cancel the stream - params.recipient = recipient; // The recipient of the streamed assets - params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees - params.asset = asset; // The streaming asset - params.cancelable = true; // Whether the stream will be cancelable or not - params.transferable = true; // Whether the stream will be transferable or not - params.timestamps = LockupLinear.Timestamps({ start: startTime, cliff: 0, end: endTime }); - params.broker = Broker({ account: brokerAdmin, fee: brokerFee }); // Optional parameter for charging a fee - - // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` - streamId = LOCKUP_LINEAR.createWithTimestamps(params); - } - - /// @notice Creates a Lockup Tranched stream - /// See https://docs.sablier.com/concepts/protocol/stream-types#unlock-monthly - /// @dev See https://docs.sablier.com/contracts/v2/guides/create-stream/lockup-linear - function _createTranchedStream( - IERC20 asset, - uint128 totalAmount, - uint40 startTime, - uint40 endTime, - address recipient, - Types.Recurrence recurrence - ) internal returns (uint256 streamId) { - // Declare the params struct - LockupTranched.CreateWithDurations memory params; - - // Declare the function parameters - params.sender = msg.sender; // The sender will be able to cancel the stream - params.recipient = recipient; // The recipient of the streamed assets - params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees - params.asset = asset; // The streaming asset - params.cancelable = true; // Whether the stream will be cancelable or not - params.transferable = true; // Whether the stream will be transferable or not - - // Calculate the number of tranches based on the payment interval and the type of recurrence - uint128 numberOfTranches = Helpers.computeNumberOfRecurringPayments(recurrence, startTime, endTime); - - // Calculate the duration of each tranche based on the payment recurrence - uint40 durationPerTranche = _computeDurationPerTrache(recurrence); - - // Calculate the amount that must be unlocked with each tranche - uint128 amountPerTranche = totalAmount / numberOfTranches; - - // Create the tranches array - params.tranches = new LockupTranched.TrancheWithDuration[](numberOfTranches); - for (uint256 i; i < numberOfTranches; ++i) { - params.tranches[i] = LockupTranched.TrancheWithDuration({ - amount: amountPerTranche, - duration: durationPerTranche - }); - } - - // Optional parameter for charging a fee - params.broker = Broker({ account: brokerAdmin, fee: brokerFee }); - - // Create the LockupTranched stream - streamId = LOCKUP_TRANCHED.createWithDurations(params); - } - - function _transferFromAndApprove(IERC20 asset, uint128 amount, address spender) internal { - // Transfer the provided amount of ERC-20 tokens to this contract - asset.transferFrom(msg.sender, address(this), amount); - - // Approve the Sablier contract to spend the ERC-20 tokens - asset.approve(spender, amount); - } - - function _computeDurationPerTrache(Types.Recurrence recurrence) internal pure returns (uint40 duration) { - if (recurrence == Types.Recurrence.Weekly) duration = 1 weeks; - else if (recurrence == Types.Recurrence.Monthly) duration = 4 weeks; - else if (recurrence == Types.Recurrence.Yearly) duration = 48 weeks; - } -} diff --git a/src/modules/invoice-module/sablier-v2/StreamManager.sol b/src/modules/invoice-module/sablier-v2/StreamManager.sol index f9f98a8d..466481ef 100644 --- a/src/modules/invoice-module/sablier-v2/StreamManager.sol +++ b/src/modules/invoice-module/sablier-v2/StreamManager.sol @@ -1,26 +1,62 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22; -import { IStreamManager } from "./interfaces/IStreamManager.sol"; +import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +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 { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ud60x18, UD60x18 } from "@prb/math/src/UD60x18.sol"; + +import { IStreamManager } from "./interfaces/IStreamManager.sol"; +import { Helpers } from "./../libraries/Helpers.sol"; +import { Errors } from "./../libraries/Errors.sol"; +import { Types } from "./../libraries/Types.sol"; /// @title StreamManager -/// @notice See the documentation in {IStreamManager} +/// @dev See the documentation in {IStreamManager} contract StreamManager is IStreamManager { /*////////////////////////////////////////////////////////////////////////// PUBLIC STORAGE //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc IStreamManager - ISablierV2Lockup public immutable override sablier; + ISablierV2LockupLinear public immutable override LOCKUP_LINEAR; + + /// @inheritdoc IStreamManager + ISablierV2LockupTranched public immutable override LOCKUP_TRANCHED; + + /// @inheritdoc IStreamManager + address public override brokerAdmin; + + /// @inheritdoc IStreamManager + UD60x18 public override brokerFee; /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ - /// @notice Initializes the {ISablierV2Lockup} contract address - constructor(ISablierV2Lockup _sablier) { - sablier = _sablier; + /// @dev Initializes the address of the {SablierV2LockupLinear} and {SablierV2LockupTranched} contracts + /// and the address of the broker admin account or contract + constructor( + ISablierV2LockupLinear _sablierLockupLinearDeployment, + ISablierV2LockupTranched _sablierLockupTranchedDeployment, + address _brokerAdmin + ) { + LOCKUP_LINEAR = _sablierLockupLinearDeployment; + LOCKUP_TRANCHED = _sablierLockupTranchedDeployment; + brokerAdmin = _brokerAdmin; + } + + /*////////////////////////////////////////////////////////////////////////// + MODIFIERS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Reverts if the `msg.sender` is not the broker admin account or contract + modifier onlyBrokerAdmin() { + if (msg.sender != brokerAdmin) revert Errors.OnlyBrokerAdmin(); + _; } /*////////////////////////////////////////////////////////////////////////// @@ -28,42 +64,205 @@ contract StreamManager is IStreamManager { //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc IStreamManager - function withdraw(uint256 streamId, address to, uint128 amount) external { + function createLinearStream( + IERC20 asset, + uint128 totalAmount, + uint40 startTime, + uint40 endTime, + address recipient + ) 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, spender: address(LOCKUP_LINEAR), amount: totalAmount }); + + // Create the Lockup Linear stream + streamId = _createLinearStream(asset, totalAmount, startTime, endTime, recipient); + } + + /// @inheritdoc IStreamManager + function createTranchedStream( + IERC20 asset, + uint128 totalAmount, + uint40 startTime, + uint40 endTime, + address recipient, + Types.Recurrence recurrence + ) 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, spender: address(LOCKUP_TRANCHED), amount: totalAmount }); + + // Create the Lockup Linear stream + streamId = _createTranchedStream(asset, totalAmount, startTime, endTime, recipient, recurrence); + } + + /// @inheritdoc IStreamManager + function updateBrokerFee(UD60x18 newBrokerFee) public onlyBrokerAdmin { + // Log the broker fee update + emit BrokerFeeUpdated({ oldFee: brokerFee, newFee: newBrokerFee }); + + // Update the fee charged by the broker + brokerFee = newBrokerFee; + } + + /*////////////////////////////////////////////////////////////////////////// + WITHDRAW FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IStreamManager + function withdraw(ISablierV2Lockup sablier, uint256 streamId, address to, uint128 amount) external { sablier.withdraw(streamId, to, amount); } /// @inheritdoc IStreamManager - function withdrawableAmountOf(uint256 streamId) external view returns (uint128 withdrawableAmount) { + function withdrawableAmountOf( + ISablierV2Lockup sablier, + uint256 streamId + ) external view returns (uint128 withdrawableAmount) { withdrawableAmount = sablier.withdrawableAmountOf(streamId); } /// @inheritdoc IStreamManager - function withdrawMax(uint256 streamId, address to) external returns (uint128 withdrawnAmount) { + function withdrawMax( + ISablierV2Lockup sablier, + uint256 streamId, + address to + ) external returns (uint128 withdrawnAmount) { withdrawnAmount = sablier.withdrawMax(streamId, to); } /// @inheritdoc IStreamManager - function withdrawMultiple(uint256[] calldata streamIds, uint128[] calldata amounts) external { + function withdrawMultiple( + ISablierV2Lockup sablier, + uint256[] calldata streamIds, + uint128[] calldata amounts + ) external { sablier.withdrawMultiple(streamIds, amounts); } - /// @inheritdoc IStreamManager - function withdrawMaxAndTransfer(uint256 streamId, address newRecipient) external returns (uint128 withdrawnAmount) { - withdrawnAmount = sablier.withdrawMaxAndTransfer(streamId, newRecipient); - } + /*////////////////////////////////////////////////////////////////////////// + CANCEL FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc IStreamManager - function cancel(uint256 streamId) external { + function cancel(ISablierV2Lockup sablier, uint256 streamId) external { sablier.cancel(streamId); } /// @inheritdoc IStreamManager - function cancelMultiple(uint256[] calldata streamIds) external { + function cancelMultiple(ISablierV2Lockup sablier, uint256[] calldata streamIds) external { sablier.cancelMultiple(streamIds); } + /*////////////////////////////////////////////////////////////////////////// + RENOUNCE FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc IStreamManager - function renounce(uint256 streamId) external { + function renounce(ISablierV2Lockup sablier, uint256 streamId) external { sablier.renounce(streamId); } + + /*////////////////////////////////////////////////////////////////////////// + TRANSFER FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IStreamManager + function withdrawMaxAndTransfer( + ISablierV2Lockup sablier, + uint256 streamId, + address newRecipient + ) external returns (uint128 withdrawnAmount) { + withdrawnAmount = sablier.withdrawMaxAndTransfer(streamId, newRecipient); + } + + /*////////////////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Creates a Lockup Linear stream + /// See https://docs.sablier.com/concepts/protocol/stream-types#lockup-linear + /// @dev See https://docs.sablier.com/contracts/v2/guides/create-stream/lockup-linear + function _createLinearStream( + IERC20 asset, + uint128 totalAmount, + uint40 startTime, + uint40 endTime, + address recipient + ) internal returns (uint256 streamId) { + // Declare the params struct + LockupLinear.CreateWithTimestamps memory params; + + // Declare the function parameters + params.sender = msg.sender; // The sender will be able to cancel the stream + params.recipient = recipient; // The recipient of the streamed assets + params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees + params.asset = asset; // The streaming asset + params.cancelable = true; // Whether the stream will be cancelable or not + params.transferable = true; // Whether the stream will be transferable or not + params.timestamps = LockupLinear.Timestamps({ start: startTime, cliff: 0, end: endTime }); + params.broker = Broker({ account: brokerAdmin, fee: brokerFee }); // Optional parameter for charging a fee + + // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` + streamId = LOCKUP_LINEAR.createWithTimestamps(params); + } + + /// @notice Creates a Lockup Tranched stream + /// See https://docs.sablier.com/concepts/protocol/stream-types#unlock-monthly + /// @dev See https://docs.sablier.com/contracts/v2/guides/create-stream/lockup-linear + function _createTranchedStream( + IERC20 asset, + uint128 totalAmount, + uint40 startTime, + uint40 endTime, + address recipient, + Types.Recurrence recurrence + ) internal returns (uint256 streamId) { + // Declare the params struct + LockupTranched.CreateWithDurations memory params; + + // Declare the function parameters + params.sender = msg.sender; // The sender will be able to cancel the stream + params.recipient = recipient; // The recipient of the streamed assets + params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees + params.asset = asset; // The streaming asset + params.cancelable = true; // Whether the stream will be cancelable or not + params.transferable = true; // Whether the stream will be transferable or not + + // Calculate the number of tranches based on the payment interval and the type of recurrence + uint128 numberOfTranches = Helpers.computeNumberOfRecurringPayments(recurrence, startTime, endTime); + + // Calculate the duration of each tranche based on the payment recurrence + uint40 durationPerTranche = _computeDurationPerTrache(recurrence); + + // Calculate the amount that must be unlocked with each tranche + uint128 amountPerTranche = totalAmount / numberOfTranches; + + // Create the tranches array + params.tranches = new LockupTranched.TrancheWithDuration[](numberOfTranches); + for (uint256 i; i < numberOfTranches; ++i) { + params.tranches[i] = LockupTranched.TrancheWithDuration({ + amount: amountPerTranche, + duration: durationPerTranche + }); + } + + // Optional parameter for charging a fee + params.broker = Broker({ account: brokerAdmin, fee: brokerFee }); + + // Create the LockupTranched stream + streamId = LOCKUP_TRANCHED.createWithDurations(params); + } + + function _transferFromAndApprove(IERC20 asset, uint128 amount, address spender) internal { + // Transfer the provided amount of ERC-20 tokens to this contract + asset.transferFrom(msg.sender, address(this), amount); + + // Approve the Sablier contract to spend the ERC-20 tokens + asset.approve(spender, amount); + } + + function _computeDurationPerTrache(Types.Recurrence recurrence) internal pure returns (uint40 duration) { + if (recurrence == Types.Recurrence.Weekly) duration = 1 weeks; + else if (recurrence == Types.Recurrence.Monthly) duration = 4 weeks; + else if (recurrence == Types.Recurrence.Yearly) duration = 48 weeks; + } } diff --git a/src/modules/invoice-module/sablier-v2/interfaces/IStreamCreator.sol b/src/modules/invoice-module/sablier-v2/interfaces/IStreamCreator.sol deleted file mode 100644 index ebc37071..00000000 --- a/src/modules/invoice-module/sablier-v2/interfaces/IStreamCreator.sol +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.8.22; - -import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; -import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { UD60x18 } from "@prb/math/src/UD60x18.sol"; -import { Types } from "./../../libraries/Types.sol"; - -/// @title IStreamCreator -/// @notice Contract used to create Sablier V2 compatible streams -/// @dev This code is referenced in the docs: https://docs.sablier.com/concepts/protocol/stream-types -interface IStreamCreator { - /*////////////////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Emitted when the broker fee is updated - /// @param oldFee The old broker fee - /// @param newFee The new broker fee - event BrokerFeeUpdated(UD60x18 oldFee, UD60x18 newFee); - - /*////////////////////////////////////////////////////////////////////////// - CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice The address of the {SablierV2LockupLinear} contract used to create linear streams - /// @dev This is initialized at construction time and it might be different depending on the deployment chain - /// See https://docs.sablier.com/contracts/v2/deployments - function LOCKUP_LINEAR() external view returns (ISablierV2LockupLinear); - - /// @notice The address of the {SablierV2LockupTranched} contract used to create tranched streams - /// @dev This is initialized at construction time and it might be different depending on the deployment chain - /// See https://docs.sablier.com/contracts/v2/deployments - function LOCKUP_TRANCHED() external view returns (ISablierV2LockupTranched); - - /// @notice The address of the broker admin account or contract managing the broker fee - function brokerAdmin() external view returns (address); - - /// @notice The broker fee charged to create Sablier V2 stream - /// @dev See the `UD60x18` type definition in the `@prb/math/src/ud60x18/ValueType.sol file` - function brokerFee() external view returns (UD60x18); - - /*////////////////////////////////////////////////////////////////////////// - NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - - /// @notice Creates a Lockup Linear stream; See https://docs.sablier.com/concepts/protocol/stream-types#lockup-linear - /// @param asset The address of the ERC-20 token to be streamed - /// @param totalAmount The total amount of ERC-20 tokens to be streamed - /// @param startTime The timestamp when the stream takes effect - /// @param endTime The timestamp by which the stream must be paid - /// @param recipient The address receiving the ERC-20 tokens - function createLinearStream( - IERC20 asset, - uint128 totalAmount, - uint40 startTime, - uint40 endTime, - address recipient - ) 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 - /// @param totalAmount The total amount of ERC-20 tokens to be streamed - /// @param startTime The timestamp when the stream takes effect - /// @param endTime The timestamp by which the stream must be paid - /// @param recipient The address receiving the ERC-20 tokens - /// @param recurrence The recurrence of each tranche - function createTranchedStream( - IERC20 asset, - uint128 totalAmount, - uint40 startTime, - uint40 endTime, - address recipient, - Types.Recurrence recurrence - ) external returns (uint256 streamId); - - /// @notice Updates the fee charged by the broker - /// - /// Notes: - /// - The new fee will be applied only to the new streams hence it can't be retrospectively updated - /// - /// @param newBrokerFee The new broker fee - function updateBrokerFee(UD60x18 newBrokerFee) external; -} diff --git a/src/modules/invoice-module/sablier-v2/interfaces/IStreamManager.sol b/src/modules/invoice-module/sablier-v2/interfaces/IStreamManager.sol index 1bf75efe..05f76fd1 100644 --- a/src/modules/invoice-module/sablier-v2/interfaces/IStreamManager.sol +++ b/src/modules/invoice-module/sablier-v2/interfaces/IStreamManager.sol @@ -1,44 +1,125 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.8.22; +import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { UD60x18 } from "@prb/math/src/UD60x18.sol"; +import { Types } from "./../../libraries/Types.sol"; /// @title IStreamManager -/// @notice Contract responsible to handle multiple management actions such as withdraw, cancel or renounce stream and transfer ownership -/// @dev This interface is a subset of the {ISablierV2Lockup} interface +/// @notice Contract used to create and manage Sablier V2 compatible streams +/// @dev This code is referenced in the docs: https://docs.sablier.com/concepts/protocol/stream-types interface IStreamManager { /*////////////////////////////////////////////////////////////////////////// - CONSTANT FUNCTIONS + EVENTS //////////////////////////////////////////////////////////////////////////*/ - /// @notice The address of the {SablierV2Lockup} contract used to handle streams management - function sablier() external view returns (ISablierV2Lockup); + /// @notice Emitted when the broker fee is updated + /// @param oldFee The old broker fee + /// @param newFee The new broker fee + event BrokerFeeUpdated(UD60x18 oldFee, UD60x18 newFee); + + /*////////////////////////////////////////////////////////////////////////// + CONSTANT FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice The address of the {SablierV2LockupLinear} contract used to create linear streams + /// @dev This is initialized at construction time and it might be different depending on the deployment chain + /// See https://docs.sablier.com/contracts/v2/deployments + function LOCKUP_LINEAR() external view returns (ISablierV2LockupLinear); + + /// @notice The address of the {SablierV2LockupTranched} contract used to create tranched streams + /// @dev This is initialized at construction time and it might be different depending on the deployment chain + /// See https://docs.sablier.com/contracts/v2/deployments + function LOCKUP_TRANCHED() external view returns (ISablierV2LockupTranched); + + /// @notice The address of the broker admin account or contract managing the broker fee + function brokerAdmin() external view returns (address); + + /// @notice The broker fee charged to create Sablier V2 stream + /// @dev See the `UD60x18` type definition in the `@prb/math/src/ud60x18/ValueType.sol file` + function brokerFee() external view returns (UD60x18); /*////////////////////////////////////////////////////////////////////////// NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ + /// @notice Creates a Lockup Linear stream; See https://docs.sablier.com/concepts/protocol/stream-types#lockup-linear + /// @param asset The address of the ERC-20 token to be streamed + /// @param totalAmount The total amount of ERC-20 tokens to be streamed + /// @param startTime The timestamp when the stream takes effect + /// @param endTime The timestamp by which the stream must be paid + /// @param recipient The address receiving the ERC-20 tokens + function createLinearStream( + IERC20 asset, + uint128 totalAmount, + uint40 startTime, + uint40 endTime, + address recipient + ) 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 + /// @param totalAmount The total amount of ERC-20 tokens to be streamed + /// @param startTime The timestamp when the stream takes effect + /// @param endTime The timestamp by which the stream must be paid + /// @param recipient The address receiving the ERC-20 tokens + /// @param recurrence The recurrence of each tranche + function createTranchedStream( + IERC20 asset, + uint128 totalAmount, + uint40 startTime, + uint40 endTime, + address recipient, + Types.Recurrence recurrence + ) external returns (uint256 streamId); + + /// @notice Updates the fee charged by the broker + /// + /// Notes: + /// - The new fee will be applied only to the new streams hence it can't be retrospectively updated + /// + /// @param newBrokerFee The new broker fee + function updateBrokerFee(UD60x18 newBrokerFee) external; + /// @notice See the documentation in {ISablierV2Lockup} - function withdraw(uint256 streamId, address to, uint128 amount) external; + function withdraw(ISablierV2Lockup sablier, uint256 streamId, address to, uint128 amount) external; /// @notice See the documentation in {ISablierV2Lockup} - function withdrawableAmountOf(uint256 streamId) external view returns (uint128 withdrawableAmount); + function withdrawableAmountOf( + ISablierV2Lockup sablier, + uint256 streamId + ) external view returns (uint128 withdrawableAmount); /// @notice See the documentation in {ISablierV2Lockup} - function withdrawMax(uint256 streamId, address to) external returns (uint128 withdrawnAmount); + function withdrawMax( + ISablierV2Lockup sablier, + uint256 streamId, + address to + ) external returns (uint128 withdrawnAmount); /// @notice See the documentation in {ISablierV2Lockup} - function withdrawMultiple(uint256[] calldata streamIds, uint128[] calldata amounts) external; + function withdrawMultiple( + ISablierV2Lockup sablier, + uint256[] calldata streamIds, + uint128[] calldata amounts + ) external; /// @notice See the documentation in {ISablierV2Lockup} - function withdrawMaxAndTransfer(uint256 streamId, address newRecipient) external returns (uint128 withdrawnAmount); + function withdrawMaxAndTransfer( + ISablierV2Lockup sablier, + uint256 streamId, + address newRecipient + ) external returns (uint128 withdrawnAmount); /// @notice See the documentation in {ISablierV2Lockup} - function cancel(uint256 streamId) external; + function cancel(ISablierV2Lockup sablier, uint256 streamId) external; /// @notice See the documentation in {ISablierV2Lockup} - function cancelMultiple(uint256[] calldata streamIds) external; + function cancelMultiple(ISablierV2Lockup sablier, uint256[] calldata streamIds) external; /// @notice See the documentation in {ISablierV2Lockup} - function renounce(uint256 streamId) external; + function renounce(ISablierV2Lockup sablier, uint256 streamId) external; } From ec0b92896d59ca587a6c07ea02fc2aea95333eb7 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Fri, 12 Jul 2024 17:27:36 +0300 Subject: [PATCH 26/40] chore(invoice-module): rename Sablier Lockup contracts --- src/modules/invoice-module/InvoiceModule.sol | 6 +++--- src/modules/invoice-module/sablier-v2/StreamManager.sol | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/modules/invoice-module/InvoiceModule.sol b/src/modules/invoice-module/InvoiceModule.sol index c4115756..2bf9f279 100644 --- a/src/modules/invoice-module/InvoiceModule.sol +++ b/src/modules/invoice-module/InvoiceModule.sol @@ -38,10 +38,10 @@ contract InvoiceModule is IInvoiceModule, StreamManager { /// @dev Initializes the {StreamManager} contract constructor( - ISablierV2LockupLinear _sablierLockupLinearDeployment, - ISablierV2LockupTranched _sablierLockupTranchedDeployment, + ISablierV2LockupLinear _sablierLockupLinear, + ISablierV2LockupTranched _sablierLockupTranched, address _brokerAdmin - ) StreamManager(_sablierLockupLinearDeployment, _sablierLockupTranchedDeployment, _brokerAdmin) {} + ) StreamManager(_sablierLockupLinear, _sablierLockupTranched, _brokerAdmin) {} /*////////////////////////////////////////////////////////////////////////// MODIFIERS diff --git a/src/modules/invoice-module/sablier-v2/StreamManager.sol b/src/modules/invoice-module/sablier-v2/StreamManager.sol index 466481ef..b54b687b 100644 --- a/src/modules/invoice-module/sablier-v2/StreamManager.sol +++ b/src/modules/invoice-module/sablier-v2/StreamManager.sol @@ -40,12 +40,12 @@ contract StreamManager is IStreamManager { /// @dev Initializes the address of the {SablierV2LockupLinear} and {SablierV2LockupTranched} contracts /// and the address of the broker admin account or contract constructor( - ISablierV2LockupLinear _sablierLockupLinearDeployment, - ISablierV2LockupTranched _sablierLockupTranchedDeployment, + ISablierV2LockupLinear _sablierLockupLinear, + ISablierV2LockupTranched _sablierLockupTranched, address _brokerAdmin ) { - LOCKUP_LINEAR = _sablierLockupLinearDeployment; - LOCKUP_TRANCHED = _sablierLockupTranchedDeployment; + LOCKUP_LINEAR = _sablierLockupLinear; + LOCKUP_TRANCHED = _sablierLockupTranched; brokerAdmin = _brokerAdmin; } From 42b70909d54c752decfff0a1dd45fefe9ed9f443 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 15 Jul 2024 17:24:06 +0300 Subject: [PATCH 27/40] build: install @nomad-xyz/excessively-safe-call lib --- .gitmodules | 3 + .../.github/workflows/test.yml | 26 + .../excessively-safe-call/.gitignore | 2 + .../excessively-safe-call/.gitmodules | 3 + .../excessively-safe-call/CHANGELOG.md | 7 + lib/nomad-xyz/excessively-safe-call/README.md | 110 +++ .../excessively-safe-call/foundry.toml | 5 + .../lib/ds-test/.gitignore | 3 + .../excessively-safe-call/lib/ds-test/LICENSE | 674 ++++++++++++++++++ .../lib/ds-test/Makefile | 14 + .../lib/ds-test/default.nix | 4 + .../lib/ds-test/demo/demo.sol | 222 ++++++ .../lib/ds-test/src/test.sol | 469 ++++++++++++ .../excessively-safe-call/package.json | 23 + .../src/ExcessivelySafeCall.sol | 134 ++++ .../src/test/ExcessivelySafeCall.t.sol | 222 ++++++ remappings.txt | 1 + 17 files changed, 1922 insertions(+) create mode 100644 lib/nomad-xyz/excessively-safe-call/.github/workflows/test.yml create mode 100644 lib/nomad-xyz/excessively-safe-call/.gitignore create mode 100644 lib/nomad-xyz/excessively-safe-call/.gitmodules create mode 100644 lib/nomad-xyz/excessively-safe-call/CHANGELOG.md create mode 100644 lib/nomad-xyz/excessively-safe-call/README.md create mode 100644 lib/nomad-xyz/excessively-safe-call/foundry.toml create mode 100644 lib/nomad-xyz/excessively-safe-call/lib/ds-test/.gitignore create mode 100644 lib/nomad-xyz/excessively-safe-call/lib/ds-test/LICENSE create mode 100644 lib/nomad-xyz/excessively-safe-call/lib/ds-test/Makefile create mode 100644 lib/nomad-xyz/excessively-safe-call/lib/ds-test/default.nix create mode 100644 lib/nomad-xyz/excessively-safe-call/lib/ds-test/demo/demo.sol create mode 100644 lib/nomad-xyz/excessively-safe-call/lib/ds-test/src/test.sol create mode 100644 lib/nomad-xyz/excessively-safe-call/package.json create mode 100644 lib/nomad-xyz/excessively-safe-call/src/ExcessivelySafeCall.sol create mode 100644 lib/nomad-xyz/excessively-safe-call/src/test/ExcessivelySafeCall.t.sol diff --git a/.gitmodules b/.gitmodules index d518ac8a..83348900 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "lib/prb-math"] path = lib/prb-math url = https://github.com/PaulRBerg/prb-math +[submodule "lib/nomad-xyz/excessively-safe-call"] + path = lib/nomad-xyz/excessively-safe-call + url = https://github.com/nomad-xyz/ExcessivelySafeCall diff --git a/lib/nomad-xyz/excessively-safe-call/.github/workflows/test.yml b/lib/nomad-xyz/excessively-safe-call/.github/workflows/test.yml new file mode 100644 index 00000000..45ffbe3a --- /dev/null +++ b/lib/nomad-xyz/excessively-safe-call/.github/workflows/test.yml @@ -0,0 +1,26 @@ +on: [push] + +name: test + +jobs: + check: + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run tests @ 0.8.13 + run: forge test -vvv --use 0.8.13 + + - name: Run tests @ 0.7.6 + run: forge test -vvv --use 0.7.6 + + - name: Run snapshot + run: forge snapshot diff --git a/lib/nomad-xyz/excessively-safe-call/.gitignore b/lib/nomad-xyz/excessively-safe-call/.gitignore new file mode 100644 index 00000000..d8a1d071 --- /dev/null +++ b/lib/nomad-xyz/excessively-safe-call/.gitignore @@ -0,0 +1,2 @@ +cache/ +out/ diff --git a/lib/nomad-xyz/excessively-safe-call/.gitmodules b/lib/nomad-xyz/excessively-safe-call/.gitmodules new file mode 100644 index 00000000..e1247196 --- /dev/null +++ b/lib/nomad-xyz/excessively-safe-call/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/ds-test"] + path = lib/ds-test + url = https://github.com/dapphub/ds-test diff --git a/lib/nomad-xyz/excessively-safe-call/CHANGELOG.md b/lib/nomad-xyz/excessively-safe-call/CHANGELOG.md new file mode 100644 index 00000000..7029c090 --- /dev/null +++ b/lib/nomad-xyz/excessively-safe-call/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +### Unreleased + +### 0.0.1-rc.1 + +- first release diff --git a/lib/nomad-xyz/excessively-safe-call/README.md b/lib/nomad-xyz/excessively-safe-call/README.md new file mode 100644 index 00000000..22638aec --- /dev/null +++ b/lib/nomad-xyz/excessively-safe-call/README.md @@ -0,0 +1,110 @@ +# ExcessivelySafeCall + +This solidity library helps you call untrusted contracts safely. Specifically, +it seeks to prevent _all possible_ ways that the callee can maliciously cause +the caller to revert. Most of these revert cases are covered by the use of a +[low-level call](https://solidity-by-example.org/call/). The main difference +with between `address.call()`call and `address.excessivelySafeCall()` is that +a regular solidity call will **automatically** copy bytes to memory without +consideration of gas. + +This is to say, a low-level solidity call will copy _any amount of bytes_ to +local memory. When bytes are copied from returndata to memory, the +[memory expansion cost +](https://ethereum.stackexchange.com/questions/92546/what-is-expansion-cost) is +paid. This means that when using a standard solidity call, the callee can +**"returnbomb"** the caller, imposing an arbitrary gas cost. Because this gas is +paid _by the caller_ and _in the caller's context_, it can cause the caller to +run out of gas and halt execution. + +To prevent returnbombing, we provide `excessivelySafeCall` and +`excessivelySafeStaticCall`. These behave similarly to solidity's low-level +calls, however, they allow the user to specify a maximum number of bytes to be +copied to local memory. E.g. a user desiring a single return value should +specify a `_maxCopy` of 32 bytes. Refusing to copy large blobs to local memory +effectively prevents the callee from triggering local OOG reversion. We _also_ recommend careful consideration of the gas amount passed to untrusted +callees. + +Consider the following contracts: + +```solidity +contract BadGuy { + function youveActivateMyTrapCard() external pure returns (bytes memory) { + assembly{ + revert(0, 1_000_000) + } + } +} + +contract Mark { + function oops(address badGuy) { + bool success; + bytes memory ret; + + // Mark pays a lot of gas for this copy 😬😬😬 + (success, ret) == badGuy.call( + SOME_GAS, + abi.encodeWithSelector( + BadGuy.youveActivateMyTrapCard.selector + ) + ); + + // Mark may OOG here, preventing local state changes + importantCleanup(); + } +} + +contract ExcessivelySafeSam { + using ExcessivelySafeCall for address; + + // Sam is cool and doesn't get returnbombed + function sunglassesEmoji(address badGuy) { + bool success; + bytes memory ret; + + (success, ret) == badGuy.excessivelySafeCall( + SOME_GAS, + 32, // <-- the magic. Copy no more than 32 bytes to memory + abi.encodeWithSelector( + BadGuy.youveActivateMyTrapCard.selector + ) + ); + + // Sam can afford to clean up after himself. + importantCleanup(); + } +} +``` + +## When would I use this + +`ExcessivelySafeCall` prevents malicious callees from affecting post-execution +cleanup (e.g. state-based replay protection). Given that a dev is unlikely to +hard-code a call to a malicious contract, we expect most danger to come from +dynamic dispatch protocols, where neither the callee nor the code being called +is known to the developer ahead of time. + +Dynamic dispatch in solidity is probably _most_ useful for metatransaction +protocols. This includes gas-abstraction relayers, smart contract wallets, +bridges, etc. + +Nomad uses excessively safe calls for safe processing of cross-domain messages. +This guarantees that a message recipient cannot interfere with safe operation +of the cross-domain communication channel and message processing layer. + +## Interacting with the repo + +**To install in your project**: + +- install [Foundry](https://github.com/gakonst/foundry) +- `forge install nomad-xyz/ExcessivelySafeCall` + +**To run tests**: + +- install [Foundry](https://github.com/gakonst/foundry) +- `forge test` + +## A note on licensing: + +Tests are licensed GPLv3, as they extend the `DSTest` contract. Non-test work +is avialable under user's choice of MIT and Apache2.0. diff --git a/lib/nomad-xyz/excessively-safe-call/foundry.toml b/lib/nomad-xyz/excessively-safe-call/foundry.toml new file mode 100644 index 00000000..247c5c10 --- /dev/null +++ b/lib/nomad-xyz/excessively-safe-call/foundry.toml @@ -0,0 +1,5 @@ +[profile.default] +src = 'src' +out = 'out' +libs = ['lib'] +remappings = ['ds-test/=lib/ds-test/src/'] diff --git a/lib/nomad-xyz/excessively-safe-call/lib/ds-test/.gitignore b/lib/nomad-xyz/excessively-safe-call/lib/ds-test/.gitignore new file mode 100644 index 00000000..63f0b2c6 --- /dev/null +++ b/lib/nomad-xyz/excessively-safe-call/lib/ds-test/.gitignore @@ -0,0 +1,3 @@ +/.dapple +/build +/out diff --git a/lib/nomad-xyz/excessively-safe-call/lib/ds-test/LICENSE b/lib/nomad-xyz/excessively-safe-call/lib/ds-test/LICENSE new file mode 100644 index 00000000..94a9ed02 --- /dev/null +++ b/lib/nomad-xyz/excessively-safe-call/lib/ds-test/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/lib/nomad-xyz/excessively-safe-call/lib/ds-test/Makefile b/lib/nomad-xyz/excessively-safe-call/lib/ds-test/Makefile new file mode 100644 index 00000000..661dac48 --- /dev/null +++ b/lib/nomad-xyz/excessively-safe-call/lib/ds-test/Makefile @@ -0,0 +1,14 @@ +all:; dapp build + +test: + -dapp --use solc:0.4.23 build + -dapp --use solc:0.4.26 build + -dapp --use solc:0.5.17 build + -dapp --use solc:0.6.12 build + -dapp --use solc:0.7.5 build + +demo: + DAPP_SRC=demo dapp --use solc:0.7.5 build + -hevm dapp-test --verbose 3 + +.PHONY: test demo diff --git a/lib/nomad-xyz/excessively-safe-call/lib/ds-test/default.nix b/lib/nomad-xyz/excessively-safe-call/lib/ds-test/default.nix new file mode 100644 index 00000000..cf65419a --- /dev/null +++ b/lib/nomad-xyz/excessively-safe-call/lib/ds-test/default.nix @@ -0,0 +1,4 @@ +{ solidityPackage, dappsys }: solidityPackage { + name = "ds-test"; + src = ./src; +} diff --git a/lib/nomad-xyz/excessively-safe-call/lib/ds-test/demo/demo.sol b/lib/nomad-xyz/excessively-safe-call/lib/ds-test/demo/demo.sol new file mode 100644 index 00000000..f3bb48e7 --- /dev/null +++ b/lib/nomad-xyz/excessively-safe-call/lib/ds-test/demo/demo.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.5.0; + +import "../src/test.sol"; + +contract DemoTest is DSTest { + function test_this() public pure { + require(true); + } + function test_logs() public { + emit log("-- log(string)"); + emit log("a string"); + + emit log("-- log_named_uint(string, uint)"); + emit log_named_uint("uint", 512); + + emit log("-- log_named_int(string, int)"); + emit log_named_int("int", -512); + + emit log("-- log_named_address(string, address)"); + emit log_named_address("address", address(this)); + + emit log("-- log_named_bytes32(string, bytes32)"); + emit log_named_bytes32("bytes32", "a string"); + + emit log("-- log_named_bytes(string, bytes)"); + emit log_named_bytes("bytes", hex"cafefe"); + + emit log("-- log_named_string(string, string)"); + emit log_named_string("string", "a string"); + + emit log("-- log_named_decimal_uint(string, uint, uint)"); + emit log_named_decimal_uint("decimal uint", 1.0e18, 18); + + emit log("-- log_named_decimal_int(string, int, uint)"); + emit log_named_decimal_int("decimal int", -1.0e18, 18); + } + event log_old_named_uint(bytes32,uint); + function test_old_logs() public { + emit log_old_named_uint("key", 500); + emit log_named_bytes32("bkey", "val"); + } + function test_trace() public view { + this.echo("string 1", "string 2"); + } + function test_multiline() public { + emit log("a multiline\\nstring"); + emit log("a multiline string"); + emit log_bytes("a string"); + emit log_bytes("a multiline\nstring"); + emit log_bytes("a multiline\\nstring"); + emit logs(hex"0000"); + emit log_named_bytes("0x0000", hex"0000"); + emit logs(hex"ff"); + } + function echo(string memory s1, string memory s2) public pure + returns (string memory, string memory) + { + return (s1, s2); + } + + function prove_this(uint x) public { + emit log_named_uint("sym x", x); + assertGt(x + 1, 0); + } + + function test_logn() public { + assembly { + log0(0x01, 0x02) + log1(0x01, 0x02, 0x03) + log2(0x01, 0x02, 0x03, 0x04) + log3(0x01, 0x02, 0x03, 0x04, 0x05) + } + } + + event MyEvent(uint, uint indexed, uint, uint indexed); + function test_events() public { + emit MyEvent(1, 2, 3, 4); + } + + function test_asserts() public { + string memory err = "this test has failed!"; + emit log("## assertTrue(bool)\n"); + assertTrue(false); + emit log("\n"); + assertTrue(false, err); + + emit log("\n## assertEq(address,address)\n"); + assertEq(address(this), msg.sender); + emit log("\n"); + assertEq(address(this), msg.sender, err); + + emit log("\n## assertEq32(bytes32,bytes32)\n"); + assertEq32("bytes 1", "bytes 2"); + emit log("\n"); + assertEq32("bytes 1", "bytes 2", err); + + emit log("\n## assertEq(bytes32,bytes32)\n"); + assertEq32("bytes 1", "bytes 2"); + emit log("\n"); + assertEq32("bytes 1", "bytes 2", err); + + emit log("\n## assertEq(uint,uint)\n"); + assertEq(uint(0), 1); + emit log("\n"); + assertEq(uint(0), 1, err); + + emit log("\n## assertEq(int,int)\n"); + assertEq(-1, -2); + emit log("\n"); + assertEq(-1, -2, err); + + emit log("\n## assertEqDecimal(int,int,uint)\n"); + assertEqDecimal(-1.0e18, -1.1e18, 18); + emit log("\n"); + assertEqDecimal(-1.0e18, -1.1e18, 18, err); + + emit log("\n## assertEqDecimal(uint,uint,uint)\n"); + assertEqDecimal(uint(1.0e18), 1.1e18, 18); + emit log("\n"); + assertEqDecimal(uint(1.0e18), 1.1e18, 18, err); + + emit log("\n## assertGt(uint,uint)\n"); + assertGt(uint(0), 0); + emit log("\n"); + assertGt(uint(0), 0, err); + + emit log("\n## assertGt(int,int)\n"); + assertGt(-1, -1); + emit log("\n"); + assertGt(-1, -1, err); + + emit log("\n## assertGtDecimal(int,int,uint)\n"); + assertGtDecimal(-2.0e18, -1.1e18, 18); + emit log("\n"); + assertGtDecimal(-2.0e18, -1.1e18, 18, err); + + emit log("\n## assertGtDecimal(uint,uint,uint)\n"); + assertGtDecimal(uint(1.0e18), 1.1e18, 18); + emit log("\n"); + assertGtDecimal(uint(1.0e18), 1.1e18, 18, err); + + emit log("\n## assertGe(uint,uint)\n"); + assertGe(uint(0), 1); + emit log("\n"); + assertGe(uint(0), 1, err); + + emit log("\n## assertGe(int,int)\n"); + assertGe(-1, 0); + emit log("\n"); + assertGe(-1, 0, err); + + emit log("\n## assertGeDecimal(int,int,uint)\n"); + assertGeDecimal(-2.0e18, -1.1e18, 18); + emit log("\n"); + assertGeDecimal(-2.0e18, -1.1e18, 18, err); + + emit log("\n## assertGeDecimal(uint,uint,uint)\n"); + assertGeDecimal(uint(1.0e18), 1.1e18, 18); + emit log("\n"); + assertGeDecimal(uint(1.0e18), 1.1e18, 18, err); + + emit log("\n## assertLt(uint,uint)\n"); + assertLt(uint(0), 0); + emit log("\n"); + assertLt(uint(0), 0, err); + + emit log("\n## assertLt(int,int)\n"); + assertLt(-1, -1); + emit log("\n"); + assertLt(-1, -1, err); + + emit log("\n## assertLtDecimal(int,int,uint)\n"); + assertLtDecimal(-1.0e18, -1.1e18, 18); + emit log("\n"); + assertLtDecimal(-1.0e18, -1.1e18, 18, err); + + emit log("\n## assertLtDecimal(uint,uint,uint)\n"); + assertLtDecimal(uint(2.0e18), 1.1e18, 18); + emit log("\n"); + assertLtDecimal(uint(2.0e18), 1.1e18, 18, err); + + emit log("\n## assertLe(uint,uint)\n"); + assertLe(uint(1), 0); + emit log("\n"); + assertLe(uint(1), 0, err); + + emit log("\n## assertLe(int,int)\n"); + assertLe(0, -1); + emit log("\n"); + assertLe(0, -1, err); + + emit log("\n## assertLeDecimal(int,int,uint)\n"); + assertLeDecimal(-1.0e18, -1.1e18, 18); + emit log("\n"); + assertLeDecimal(-1.0e18, -1.1e18, 18, err); + + emit log("\n## assertLeDecimal(uint,uint,uint)\n"); + assertLeDecimal(uint(2.0e18), 1.1e18, 18); + emit log("\n"); + assertLeDecimal(uint(2.0e18), 1.1e18, 18, err); + + emit log("\n## assertEq(string,string)\n"); + string memory s1 = "string 1"; + string memory s2 = "string 2"; + assertEq(s1, s2); + emit log("\n"); + assertEq(s1, s2, err); + + emit log("\n## assertEq0(bytes,bytes)\n"); + assertEq0(hex"abcdef01", hex"abcdef02"); + emit log("\n"); + assertEq0(hex"abcdef01", hex"abcdef02", err); + } +} + +contract DemoTestWithSetUp { + function setUp() public { + } + function test_pass() public pure { + } +} diff --git a/lib/nomad-xyz/excessively-safe-call/lib/ds-test/src/test.sol b/lib/nomad-xyz/excessively-safe-call/lib/ds-test/src/test.sol new file mode 100644 index 00000000..515a3bd0 --- /dev/null +++ b/lib/nomad-xyz/excessively-safe-call/lib/ds-test/src/test.sol @@ -0,0 +1,469 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity >=0.5.0; + +contract DSTest { + event log (string); + event logs (bytes); + + event log_address (address); + event log_bytes32 (bytes32); + event log_int (int); + event log_uint (uint); + event log_bytes (bytes); + event log_string (string); + + event log_named_address (string key, address val); + event log_named_bytes32 (string key, bytes32 val); + event log_named_decimal_int (string key, int val, uint decimals); + event log_named_decimal_uint (string key, uint val, uint decimals); + event log_named_int (string key, int val); + event log_named_uint (string key, uint val); + event log_named_bytes (string key, bytes val); + event log_named_string (string key, string val); + + bool public IS_TEST = true; + bool private _failed; + + address constant HEVM_ADDRESS = + address(bytes20(uint160(uint256(keccak256('hevm cheat code'))))); + + modifier mayRevert() { _; } + modifier testopts(string memory) { _; } + + function failed() public returns (bool) { + if (_failed) { + return _failed; + } else { + bool globalFailed = false; + if (hasHEVMContext()) { + (, bytes memory retdata) = HEVM_ADDRESS.call( + abi.encodePacked( + bytes4(keccak256("load(address,bytes32)")), + abi.encode(HEVM_ADDRESS, bytes32("failed")) + ) + ); + globalFailed = abi.decode(retdata, (bool)); + } + return globalFailed; + } + } + + function fail() internal { + if (hasHEVMContext()) { + (bool status, ) = HEVM_ADDRESS.call( + abi.encodePacked( + bytes4(keccak256("store(address,bytes32,bytes32)")), + abi.encode(HEVM_ADDRESS, bytes32("failed"), bytes32(uint256(0x01))) + ) + ); + status; // Silence compiler warnings + } + _failed = true; + } + + function hasHEVMContext() internal view returns (bool) { + uint256 hevmCodeSize = 0; + assembly { + hevmCodeSize := extcodesize(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D) + } + return hevmCodeSize > 0; + } + + modifier logs_gas() { + uint startGas = gasleft(); + _; + uint endGas = gasleft(); + emit log_named_uint("gas", startGas - endGas); + } + + function assertTrue(bool condition) internal { + if (!condition) { + emit log("Error: Assertion Failed"); + fail(); + } + } + + function assertTrue(bool condition, string memory err) internal { + if (!condition) { + emit log_named_string("Error", err); + assertTrue(condition); + } + } + + function assertEq(address a, address b) internal { + if (a != b) { + emit log("Error: a == b not satisfied [address]"); + emit log_named_address(" Expected", b); + emit log_named_address(" Actual", a); + fail(); + } + } + function assertEq(address a, address b, string memory err) internal { + if (a != b) { + emit log_named_string ("Error", err); + assertEq(a, b); + } + } + + function assertEq(bytes32 a, bytes32 b) internal { + if (a != b) { + emit log("Error: a == b not satisfied [bytes32]"); + emit log_named_bytes32(" Expected", b); + emit log_named_bytes32(" Actual", a); + fail(); + } + } + function assertEq(bytes32 a, bytes32 b, string memory err) internal { + if (a != b) { + emit log_named_string ("Error", err); + assertEq(a, b); + } + } + function assertEq32(bytes32 a, bytes32 b) internal { + assertEq(a, b); + } + function assertEq32(bytes32 a, bytes32 b, string memory err) internal { + assertEq(a, b, err); + } + + function assertEq(int a, int b) internal { + if (a != b) { + emit log("Error: a == b not satisfied [int]"); + emit log_named_int(" Expected", b); + emit log_named_int(" Actual", a); + fail(); + } + } + function assertEq(int a, int b, string memory err) internal { + if (a != b) { + emit log_named_string("Error", err); + assertEq(a, b); + } + } + function assertEq(uint a, uint b) internal { + if (a != b) { + emit log("Error: a == b not satisfied [uint]"); + emit log_named_uint(" Expected", b); + emit log_named_uint(" Actual", a); + fail(); + } + } + function assertEq(uint a, uint b, string memory err) internal { + if (a != b) { + emit log_named_string("Error", err); + assertEq(a, b); + } + } + function assertEqDecimal(int a, int b, uint decimals) internal { + if (a != b) { + emit log("Error: a == b not satisfied [decimal int]"); + emit log_named_decimal_int(" Expected", b, decimals); + emit log_named_decimal_int(" Actual", a, decimals); + fail(); + } + } + function assertEqDecimal(int a, int b, uint decimals, string memory err) internal { + if (a != b) { + emit log_named_string("Error", err); + assertEqDecimal(a, b, decimals); + } + } + function assertEqDecimal(uint a, uint b, uint decimals) internal { + if (a != b) { + emit log("Error: a == b not satisfied [decimal uint]"); + emit log_named_decimal_uint(" Expected", b, decimals); + emit log_named_decimal_uint(" Actual", a, decimals); + fail(); + } + } + function assertEqDecimal(uint a, uint b, uint decimals, string memory err) internal { + if (a != b) { + emit log_named_string("Error", err); + assertEqDecimal(a, b, decimals); + } + } + + function assertGt(uint a, uint b) internal { + if (a <= b) { + emit log("Error: a > b not satisfied [uint]"); + emit log_named_uint(" Value a", a); + emit log_named_uint(" Value b", b); + fail(); + } + } + function assertGt(uint a, uint b, string memory err) internal { + if (a <= b) { + emit log_named_string("Error", err); + assertGt(a, b); + } + } + function assertGt(int a, int b) internal { + if (a <= b) { + emit log("Error: a > b not satisfied [int]"); + emit log_named_int(" Value a", a); + emit log_named_int(" Value b", b); + fail(); + } + } + function assertGt(int a, int b, string memory err) internal { + if (a <= b) { + emit log_named_string("Error", err); + assertGt(a, b); + } + } + function assertGtDecimal(int a, int b, uint decimals) internal { + if (a <= b) { + emit log("Error: a > b not satisfied [decimal int]"); + emit log_named_decimal_int(" Value a", a, decimals); + emit log_named_decimal_int(" Value b", b, decimals); + fail(); + } + } + function assertGtDecimal(int a, int b, uint decimals, string memory err) internal { + if (a <= b) { + emit log_named_string("Error", err); + assertGtDecimal(a, b, decimals); + } + } + function assertGtDecimal(uint a, uint b, uint decimals) internal { + if (a <= b) { + emit log("Error: a > b not satisfied [decimal uint]"); + emit log_named_decimal_uint(" Value a", a, decimals); + emit log_named_decimal_uint(" Value b", b, decimals); + fail(); + } + } + function assertGtDecimal(uint a, uint b, uint decimals, string memory err) internal { + if (a <= b) { + emit log_named_string("Error", err); + assertGtDecimal(a, b, decimals); + } + } + + function assertGe(uint a, uint b) internal { + if (a < b) { + emit log("Error: a >= b not satisfied [uint]"); + emit log_named_uint(" Value a", a); + emit log_named_uint(" Value b", b); + fail(); + } + } + function assertGe(uint a, uint b, string memory err) internal { + if (a < b) { + emit log_named_string("Error", err); + assertGe(a, b); + } + } + function assertGe(int a, int b) internal { + if (a < b) { + emit log("Error: a >= b not satisfied [int]"); + emit log_named_int(" Value a", a); + emit log_named_int(" Value b", b); + fail(); + } + } + function assertGe(int a, int b, string memory err) internal { + if (a < b) { + emit log_named_string("Error", err); + assertGe(a, b); + } + } + function assertGeDecimal(int a, int b, uint decimals) internal { + if (a < b) { + emit log("Error: a >= b not satisfied [decimal int]"); + emit log_named_decimal_int(" Value a", a, decimals); + emit log_named_decimal_int(" Value b", b, decimals); + fail(); + } + } + function assertGeDecimal(int a, int b, uint decimals, string memory err) internal { + if (a < b) { + emit log_named_string("Error", err); + assertGeDecimal(a, b, decimals); + } + } + function assertGeDecimal(uint a, uint b, uint decimals) internal { + if (a < b) { + emit log("Error: a >= b not satisfied [decimal uint]"); + emit log_named_decimal_uint(" Value a", a, decimals); + emit log_named_decimal_uint(" Value b", b, decimals); + fail(); + } + } + function assertGeDecimal(uint a, uint b, uint decimals, string memory err) internal { + if (a < b) { + emit log_named_string("Error", err); + assertGeDecimal(a, b, decimals); + } + } + + function assertLt(uint a, uint b) internal { + if (a >= b) { + emit log("Error: a < b not satisfied [uint]"); + emit log_named_uint(" Value a", a); + emit log_named_uint(" Value b", b); + fail(); + } + } + function assertLt(uint a, uint b, string memory err) internal { + if (a >= b) { + emit log_named_string("Error", err); + assertLt(a, b); + } + } + function assertLt(int a, int b) internal { + if (a >= b) { + emit log("Error: a < b not satisfied [int]"); + emit log_named_int(" Value a", a); + emit log_named_int(" Value b", b); + fail(); + } + } + function assertLt(int a, int b, string memory err) internal { + if (a >= b) { + emit log_named_string("Error", err); + assertLt(a, b); + } + } + function assertLtDecimal(int a, int b, uint decimals) internal { + if (a >= b) { + emit log("Error: a < b not satisfied [decimal int]"); + emit log_named_decimal_int(" Value a", a, decimals); + emit log_named_decimal_int(" Value b", b, decimals); + fail(); + } + } + function assertLtDecimal(int a, int b, uint decimals, string memory err) internal { + if (a >= b) { + emit log_named_string("Error", err); + assertLtDecimal(a, b, decimals); + } + } + function assertLtDecimal(uint a, uint b, uint decimals) internal { + if (a >= b) { + emit log("Error: a < b not satisfied [decimal uint]"); + emit log_named_decimal_uint(" Value a", a, decimals); + emit log_named_decimal_uint(" Value b", b, decimals); + fail(); + } + } + function assertLtDecimal(uint a, uint b, uint decimals, string memory err) internal { + if (a >= b) { + emit log_named_string("Error", err); + assertLtDecimal(a, b, decimals); + } + } + + function assertLe(uint a, uint b) internal { + if (a > b) { + emit log("Error: a <= b not satisfied [uint]"); + emit log_named_uint(" Value a", a); + emit log_named_uint(" Value b", b); + fail(); + } + } + function assertLe(uint a, uint b, string memory err) internal { + if (a > b) { + emit log_named_string("Error", err); + assertLe(a, b); + } + } + function assertLe(int a, int b) internal { + if (a > b) { + emit log("Error: a <= b not satisfied [int]"); + emit log_named_int(" Value a", a); + emit log_named_int(" Value b", b); + fail(); + } + } + function assertLe(int a, int b, string memory err) internal { + if (a > b) { + emit log_named_string("Error", err); + assertLe(a, b); + } + } + function assertLeDecimal(int a, int b, uint decimals) internal { + if (a > b) { + emit log("Error: a <= b not satisfied [decimal int]"); + emit log_named_decimal_int(" Value a", a, decimals); + emit log_named_decimal_int(" Value b", b, decimals); + fail(); + } + } + function assertLeDecimal(int a, int b, uint decimals, string memory err) internal { + if (a > b) { + emit log_named_string("Error", err); + assertLeDecimal(a, b, decimals); + } + } + function assertLeDecimal(uint a, uint b, uint decimals) internal { + if (a > b) { + emit log("Error: a <= b not satisfied [decimal uint]"); + emit log_named_decimal_uint(" Value a", a, decimals); + emit log_named_decimal_uint(" Value b", b, decimals); + fail(); + } + } + function assertLeDecimal(uint a, uint b, uint decimals, string memory err) internal { + if (a > b) { + emit log_named_string("Error", err); + assertGeDecimal(a, b, decimals); + } + } + + function assertEq(string memory a, string memory b) internal { + if (keccak256(abi.encodePacked(a)) != keccak256(abi.encodePacked(b))) { + emit log("Error: a == b not satisfied [string]"); + emit log_named_string(" Expected", b); + emit log_named_string(" Actual", a); + fail(); + } + } + function assertEq(string memory a, string memory b, string memory err) internal { + if (keccak256(abi.encodePacked(a)) != keccak256(abi.encodePacked(b))) { + emit log_named_string("Error", err); + assertEq(a, b); + } + } + + function checkEq0(bytes memory a, bytes memory b) internal pure returns (bool ok) { + ok = true; + if (a.length == b.length) { + for (uint i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + ok = false; + } + } + } else { + ok = false; + } + } + function assertEq0(bytes memory a, bytes memory b) internal { + if (!checkEq0(a, b)) { + emit log("Error: a == b not satisfied [bytes]"); + emit log_named_bytes(" Expected", b); + emit log_named_bytes(" Actual", a); + fail(); + } + } + function assertEq0(bytes memory a, bytes memory b, string memory err) internal { + if (!checkEq0(a, b)) { + emit log_named_string("Error", err); + assertEq0(a, b); + } + } +} diff --git a/lib/nomad-xyz/excessively-safe-call/package.json b/lib/nomad-xyz/excessively-safe-call/package.json new file mode 100644 index 00000000..0e5b750c --- /dev/null +++ b/lib/nomad-xyz/excessively-safe-call/package.json @@ -0,0 +1,23 @@ +{ + "name": "@nomad-xyz/excessively-safe-call", + "version": "0.0.1-rc.1", + "description": "Helps you call untrusted contracts safely", + "keywords": [ + "nomad", + "excessively safe call" + ], + "homepage": "https://github.com/nomad-xyz/ExcessivelySafeCall#readme", + "bugs": { + "url": "https://github.com/nomad-xyz/ExcessivelySafeCall/issues" + }, + "repository": { + "type": "git", + "url": "git@github.com:nomad-xyz/ExcessivelySafeCall.git" + }, + "license": "Apache-2.0 OR MIT", + "author": "Illusory Systems Inc.", + "main": "src/ExcessivelySafeCall.sol", + "files": [ + "src/ExcessivelySafeCall.sol" + ] +} \ No newline at end of file diff --git a/lib/nomad-xyz/excessively-safe-call/src/ExcessivelySafeCall.sol b/lib/nomad-xyz/excessively-safe-call/src/ExcessivelySafeCall.sol new file mode 100644 index 00000000..d85bae85 --- /dev/null +++ b/lib/nomad-xyz/excessively-safe-call/src/ExcessivelySafeCall.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.7.6; + +library ExcessivelySafeCall { + uint256 constant LOW_28_MASK = 0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + + /// @notice Use when you _really_ really _really_ don't trust the called + /// contract. This prevents the called contract from causing reversion of + /// the caller in as many ways as we can. + /// @dev The main difference between this and a solidity low-level call is + /// that we limit the number of bytes that the callee can cause to be + /// copied to caller memory. This prevents stupid things like malicious + /// contracts returning 10,000,000 bytes causing a local OOG when copying + /// to memory. + /// @param _target The address to call + /// @param _gas The amount of gas to forward to the remote contract + /// @param _value The value in wei to send to the remote contract + /// @param _maxCopy The maximum number of bytes of returndata to copy + /// to memory. + /// @param _calldata The data to send to the remote contract + /// @return success and returndata, as `.call()`. Returndata is capped to + /// `_maxCopy` bytes. + function excessivelySafeCall( + address _target, + uint256 _gas, + uint256 _value, + uint16 _maxCopy, + bytes memory _calldata + ) internal returns (bool, bytes memory) { + // set up for assembly call + uint256 _toCopy; + bool _success; + bytes memory _returnData = new bytes(_maxCopy); + // dispatch message to recipient + // by assembly calling "handle" function + // we call via assembly to avoid memcopying a very large returndata + // returned by a malicious contract + assembly { + _success := call( + _gas, // gas + _target, // recipient + _value, // ether value + add(_calldata, 0x20), // inloc + mload(_calldata), // inlen + 0, // outloc + 0 // outlen + ) + // limit our copy to 256 bytes + _toCopy := returndatasize() + if gt(_toCopy, _maxCopy) { + _toCopy := _maxCopy + } + // Store the length of the copied bytes + mstore(_returnData, _toCopy) + // copy the bytes from returndata[0:_toCopy] + returndatacopy(add(_returnData, 0x20), 0, _toCopy) + } + return (_success, _returnData); + } + + /// @notice Use when you _really_ really _really_ don't trust the called + /// contract. This prevents the called contract from causing reversion of + /// the caller in as many ways as we can. + /// @dev The main difference between this and a solidity low-level call is + /// that we limit the number of bytes that the callee can cause to be + /// copied to caller memory. This prevents stupid things like malicious + /// contracts returning 10,000,000 bytes causing a local OOG when copying + /// to memory. + /// @param _target The address to call + /// @param _gas The amount of gas to forward to the remote contract + /// @param _maxCopy The maximum number of bytes of returndata to copy + /// to memory. + /// @param _calldata The data to send to the remote contract + /// @return success and returndata, as `.call()`. Returndata is capped to + /// `_maxCopy` bytes. + function excessivelySafeStaticCall( + address _target, + uint256 _gas, + uint16 _maxCopy, + bytes memory _calldata + ) internal view returns (bool, bytes memory) { + // set up for assembly call + uint256 _toCopy; + bool _success; + bytes memory _returnData = new bytes(_maxCopy); + // dispatch message to recipient + // by assembly calling "handle" function + // we call via assembly to avoid memcopying a very large returndata + // returned by a malicious contract + assembly { + _success := staticcall( + _gas, // gas + _target, // recipient + add(_calldata, 0x20), // inloc + mload(_calldata), // inlen + 0, // outloc + 0 // outlen + ) + // limit our copy to 256 bytes + _toCopy := returndatasize() + if gt(_toCopy, _maxCopy) { + _toCopy := _maxCopy + } + // Store the length of the copied bytes + mstore(_returnData, _toCopy) + // copy the bytes from returndata[0:_toCopy] + returndatacopy(add(_returnData, 0x20), 0, _toCopy) + } + return (_success, _returnData); + } + + /** + * @notice Swaps function selectors in encoded contract calls + * @dev Allows reuse of encoded calldata for functions with identical + * argument types but different names. It simply swaps out the first 4 bytes + * for the new selector. This function modifies memory in place, and should + * only be used with caution. + * @param _newSelector The new 4-byte selector + * @param _buf The encoded contract args + */ + function swapSelector(bytes4 _newSelector, bytes memory _buf) internal pure { + require(_buf.length >= 4); + uint256 _mask = LOW_28_MASK; + assembly { + // load the first word of + let _word := mload(add(_buf, 0x20)) + // mask out the top 4 bytes + // /x + _word := and(_word, _mask) + _word := or(_newSelector, _word) + mstore(add(_buf, 0x20), _word) + } + } +} diff --git a/lib/nomad-xyz/excessively-safe-call/src/test/ExcessivelySafeCall.t.sol b/lib/nomad-xyz/excessively-safe-call/src/test/ExcessivelySafeCall.t.sol new file mode 100644 index 00000000..18b01b37 --- /dev/null +++ b/lib/nomad-xyz/excessively-safe-call/src/test/ExcessivelySafeCall.t.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.7.6; + +import "ds-test/test.sol"; +import "src/ExcessivelySafeCall.sol"; + +contract ContractTest is DSTest { + using ExcessivelySafeCall for address; + + address target; + CallTarget t; + + function returnSize() internal pure returns (uint256 _bytes) { + assembly { + _bytes := returndatasize() + } + } + + function setUp() public { + t = new CallTarget(); + target = address(t); + } + + function testCall() public { + bool _success; + bytes memory _ret; + + (_success, _ret) = target.excessivelySafeCall( + 100_000, + 0, + 0, + abi.encodeWithSelector(CallTarget.one.selector) + ); + assertTrue(_success); + assertEq(_ret.length, 0); + assertEq(t.called(), 1); + + (_success, _ret) = target.excessivelySafeCall( + 100_000, + 0, + 0, + abi.encodeWithSelector(CallTarget.two.selector) + ); + assertTrue(_success); + assertEq(_ret.length, 0); + assertEq(t.called(), 2); + + (_success, _ret) = target.excessivelySafeCall( + 100_000, + 0, + 0, + abi.encodeWithSelector(CallTarget.any.selector, 5) + ); + assertTrue(_success); + assertEq(_ret.length, 0); + assertEq(t.called(), 5); + + (_success, _ret) = target.excessivelySafeCall( + 100_000, + 69, + 0, + abi.encodeWithSelector(CallTarget.payme.selector) + ); + assertTrue(_success); + assertEq(_ret.length, 0); + assertEq(t.called(), 69); + } + + function testStaticCall() public { + bool _success; + bytes memory _ret; + + (_success, _ret) = target.excessivelySafeStaticCall( + 100_000, + 0, + abi.encodeWithSelector(CallTarget.two.selector) + ); + assertEq(t.called(), 0, "t modified state"); + assertTrue(!_success, "staticcall should error on state modification"); + } + + function testCopy(uint16 _maxCopy, uint16 _requested) public { + uint16 _toCopy = _maxCopy < _requested ? _maxCopy : _requested; + + bool _success; + bytes memory _ret; + + (_success, _ret) = target.excessivelySafeCall( + 100_000, + 0, + _maxCopy, + abi.encodeWithSelector(CallTarget.retBytes.selector, uint256(_requested)) + ); + assertTrue(_success); + assertEq(_ret.length, _toCopy, "return copied wrong amount"); + + (_success, _ret) = target.excessivelySafeCall( + 100_000, + 0, + _maxCopy, + abi.encodeWithSelector(CallTarget.revBytes.selector, uint256(_requested)) + ); + assertTrue(!_success); + assertEq(_ret.length, _toCopy, "revert copied wrong amount"); + } + + + function testStaticCopy(uint16 _maxCopy, uint16 _requested) public { + uint16 _toCopy = _maxCopy < _requested ? _maxCopy : _requested; + + bool _success; + bytes memory _ret; + + (_success, _ret) = target.excessivelySafeStaticCall( + 100_000, + _maxCopy, + abi.encodeWithSelector(CallTarget.retBytes.selector, uint256(_requested)) + ); + assertTrue(_success); + assertEq(_ret.length, _toCopy, "return copied wrong amount"); + + (_success, _ret) = target.excessivelySafeStaticCall( + 100_000, + _maxCopy, + abi.encodeWithSelector(CallTarget.revBytes.selector, uint256(_requested)) + ); + assertTrue(!_success); + assertEq(_ret.length, _toCopy, "revert copied wrong amount"); + } + + function testBadBehavior() public { + bool _success; + bytes memory _ret; + + (_success, _ret) = target.excessivelySafeCall( + 3_000_000, + 0, + 32, + abi.encodeWithSelector(CallTarget.badRet.selector) + ); + assertTrue(_success); + assertEq(returnSize(), 1_000_000, "didn't return all"); + assertEq(_ret.length, 32, "revert didn't truncate"); + + + (_success, _ret) = target.excessivelySafeCall( + 3_000_000, + 0, + 32, + abi.encodeWithSelector(CallTarget.badRev.selector) + ); + assertTrue(!_success); + assertEq(returnSize(), 1_000_000, "didn't return all"); + assertEq(_ret.length, 32, "revert didn't truncate"); + } + + function testStaticBadBehavior() public { + bool _success; + bytes memory _ret; + + (_success, _ret) = target.excessivelySafeStaticCall( + 2_002_000, + 32, + abi.encodeWithSelector(CallTarget.badRet.selector) + ); + assertTrue(_success); + assertEq(returnSize(), 1_000_000, "didn't return all"); + assertEq(_ret.length, 32, "revert didn't truncate"); + + (_success, _ret) = target.excessivelySafeStaticCall( + 2_002_000, + 32, + abi.encodeWithSelector(CallTarget.badRev.selector) + ); + assertTrue(!_success); + assertEq(returnSize(), 1_000_000, "didn't return all"); + assertEq(_ret.length, 32, "revert didn't truncate"); + } + +} + + +contract CallTarget { + uint256 public called; + constructor () {} + + function one() external { + called = 1; + } + + function two() external { + called = 2; + } + + function any(uint256 _num) external { + called = _num; + } + + function payme() external payable { + called = msg.value; + } + + function retBytes(uint256 _bytes) public pure { + assembly { + return(0, _bytes) + } + } + + function revBytes(uint256 _bytes) public pure { + assembly { + revert(0, _bytes) + } + } + + function badRet() external pure returns (bytes memory) { + retBytes(1_000_000); + } + + function badRev() external pure { + revBytes(1_000_000); + } +} diff --git a/remappings.txt b/remappings.txt index ea163543..4fc0d9b6 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,5 +1,6 @@ @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ @sablier/v2-core/=lib/v2-core/ @prb/math/=lib/prb-math/ +@nomad-xyz/excessively-safe-call/=lib/nomad-xyz/excessively-safe-call/ ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ From b2571b7442e3cdafe926448fd69efa197cb917f0 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 15 Jul 2024 17:25:32 +0300 Subject: [PATCH 28/40] refactor: use 'ExcessivelySafeCall' for module call --- src/Container.sol | 26 ++++++++++++++++---------- src/interfaces/IContainer.sol | 2 +- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Container.sol b/src/Container.sol index d62f0963..8f608265 100644 --- a/src/Container.sol +++ b/src/Container.sol @@ -9,11 +9,13 @@ import { IContainer } from "./interfaces/IContainer.sol"; import { ModuleManager } from "./ModuleManager.sol"; import { IModuleManager } from "./interfaces/IModuleManager.sol"; import { Errors } from "./libraries/Errors.sol"; +import { ExcessivelySafeCall } from "@nomad-xyz/excessively-safe-call/src/ExcessivelySafeCall.sol"; /// @title Container /// @notice See the documentation in {IContainer} contract Container is IContainer, ModuleManager { using SafeERC20 for IERC20; + using ExcessivelySafeCall for address; /*////////////////////////////////////////////////////////////////////////// PRIVATE STORAGE @@ -60,22 +62,26 @@ contract Container is IContainer, ModuleManager { address module, uint256 value, bytes memory data - ) external onlyOwner onlyEnabledModule(module) returns (bool _success) { + ) external onlyOwner onlyEnabledModule(module) returns (bool success) { // Allocate all the gas to the executed module method uint256 txGas = gasleft(); // Execute the call via assembly to avoid returnbomb attacks // See https://github.com/nomad-xyz/ExcessivelySafeCall // - // Do not account for the returned data but only for the `_success` boolean - assembly { - // See https://www.evm.codes/#f1?fork=cancun - _success := call(txGas, module, value, add(data, 0x20), mload(data), 0, 0) - } - - // Log the corresponding event whether the call was successful or not - if (_success) emit ModuleExecutionSucceded(module, value, data); - else emit ModuleExecutionFailed(module, value, data); + // Account for the returned data only if the `_success` boolean is false + // in which case revert with the error message + bytes memory result; + (success, result) = module.excessivelySafeCall({ _gas: txGas, _value: 0, _maxCopy: 4, _calldata: data }); + + if (!success) { + emit ModuleExecutionFailed(module, value, data, result); + + // Revert with the error + assembly { + revert(add(result, 0x20), result) + } + } else emit ModuleExecutionSucceded(module, value, data); } /// @inheritdoc IContainer diff --git a/src/interfaces/IContainer.sol b/src/interfaces/IContainer.sol index e726ad27..d0564805 100644 --- a/src/interfaces/IContainer.sol +++ b/src/interfaces/IContainer.sol @@ -28,7 +28,7 @@ interface IContainer is IERC165 { /// @param module The address of the module that was executed /// @param value The value sent to the module address required for the call /// @param data The ABI-encoded method called on the module - event ModuleExecutionFailed(address indexed module, uint256 value, bytes data); + event ModuleExecutionFailed(address indexed module, uint256 value, bytes data, bytes error); /// @notice Emitted when a module execution is successful /// @param module The address of the module that was executed From 31fe0df03a5c04a1348bb4d468e09e2c3a5c6fcb Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 15 Jul 2024 17:26:52 +0300 Subject: [PATCH 29/40] docs: add documentation for invoice module errors --- .../invoice-module/libraries/Errors.sol | 47 ++++++++++++++++--- test/utils/Errors.sol | 44 +++++++++++++++++ 2 files changed, 84 insertions(+), 7 deletions(-) diff --git a/src/modules/invoice-module/libraries/Errors.sol b/src/modules/invoice-module/libraries/Errors.sol index 6c926c22..e327a236 100644 --- a/src/modules/invoice-module/libraries/Errors.sol +++ b/src/modules/invoice-module/libraries/Errors.sol @@ -2,20 +2,53 @@ 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 {InvoiceModule} and {StreamManager} may revert with library Errors { + /*////////////////////////////////////////////////////////////////////////// + INVOICE-MODULE + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Thrown when the caller is an invalid zero code contract or EOA error ContainerZeroCodeSize(); + + /// @notice Thrown when the caller is a contract that does not implement the {IContainer} interface error ContainerUnsupportedInterface(); - error InvalidPayer(); + + /// @notice Thrown when the end time of an invoice is in the past error EndTimeLowerThanCurrentTime(); + + /// @notice Thrown when the start time is later than the end time error StartTimeGreaterThanEndTime(); - error InvalidPaymentType(); - error PaymentAmountZero(); - error InvalidPaymentAmount(uint256 amount); - error PaymentFailed(); + + /// @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 the number of recurring payments set for a recurring transfer invoice is invalid error InvalidNumberOfPayments(uint40 expectedNumber); - error OnlyBrokerAdmin(); + + /// @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 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(); + + /*////////////////////////////////////////////////////////////////////////// + STREAM-MANAGER + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Thrown when the caller is not the broker admin + error OnlyBrokerAdmin(); } diff --git a/test/utils/Errors.sol b/test/utils/Errors.sol index 6abb1220..29e8e4b3 100644 --- a/test/utils/Errors.sol +++ b/test/utils/Errors.sol @@ -35,4 +35,48 @@ library Errors { /// @notice Thrown when a container tries to execute a method on a non-enabled module error ModuleNotEnabled(); + + /*////////////////////////////////////////////////////////////////////////// + INVOICE-MODULE + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Thrown when the caller is an invalid zero code contract or EOA + error ContainerZeroCodeSize(); + + /// @notice Thrown when the caller is a contract that does not implement the {IContainer} interface + error ContainerUnsupportedInterface(); + + /// @notice Thrown when the end time of an invoice is in the past + error EndTimeLowerThanCurrentTime(); + + /// @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 the number of recurring payments set for a recurring transfer invoice is invalid + error InvalidNumberOfPayments(uint40 expectedNumber); + + /// @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(); + + /*////////////////////////////////////////////////////////////////////////// + STREAM-MANAGER + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Thrown when the caller is not the broker admin + error OnlyBrokerAdmin(); } From 79804632e7cb5ad76c2404ec1abcfe27d05d60df Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 15 Jul 2024 17:28:54 +0300 Subject: [PATCH 30/40] feat(invoice-module): create tranched stream with timestamps, improve checks and types --- src/modules/invoice-module/InvoiceModule.sol | 32 +++++++++++-------- .../invoice-module/libraries/Helpers.sol | 21 ++++++++---- .../invoice-module/libraries/Types.sol | 2 +- .../sablier-v2/StreamManager.sol | 23 +++++++------ .../sablier-v2/interfaces/IStreamManager.sol | 4 +-- 5 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/modules/invoice-module/InvoiceModule.sol b/src/modules/invoice-module/InvoiceModule.sol index 2bf9f279..7b2f2bd4 100644 --- a/src/modules/invoice-module/InvoiceModule.sol +++ b/src/modules/invoice-module/InvoiceModule.sol @@ -56,7 +56,7 @@ contract InvoiceModule is IInvoiceModule, StreamManager { // Checks: the sender implements the ERC-165 interface required by {IContainer} bytes4 interfaceId = type(IContainer).interfaceId; - if (!IERC165(msg.sender).supportsInterface(interfaceId)) revert Errors.ContainerUnsupportedInterface(); + if (!IContainer(msg.sender).supportsInterface(interfaceId)) revert Errors.ContainerUnsupportedInterface(); _; } @@ -77,11 +77,11 @@ contract InvoiceModule is IInvoiceModule, StreamManager { function createInvoice(Types.Invoice calldata invoice) external onlyContainer returns (uint256 id) { // Checks: the amount is non-zero if (invoice.payment.amount == 0) { - revert Errors.PaymentAmountZero(); + revert Errors.ZeroPaymentAmount(); } // Checks: the start time is stricly lower than the end time - if (invoice.startTime >= invoice.endTime) { + if (invoice.startTime > invoice.endTime) { revert Errors.StartTimeGreaterThanEndTime(); } @@ -91,10 +91,9 @@ contract InvoiceModule is IInvoiceModule, StreamManager { revert Errors.EndTimeLowerThanCurrentTime(); } - // Checks: validate the input parameters if the invoice must be paid in even transfers - if (invoice.payment.method == Types.Method.Transfer) { - // Checks: validate the input parameters if the invoice is recurring - if (invoice.payment.paymentsLeft > 1) { + // Checks: validate the input parameters if the invoice must be paid in even recurring transfers or by a tranched stream + if (invoice.payment.method == Types.Method.Transfer || invoice.payment.method == Types.Method.TranchedStream) { + if (invoice.payment.recurrence != Types.Recurrence.OneOff) { _checkRecurringTransferInvoiceParams({ recurrence: invoice.payment.recurrence, paymentsLeft: invoice.payment.paymentsLeft, @@ -183,7 +182,7 @@ contract InvoiceModule is IInvoiceModule, StreamManager { // Using unchecked because the number of payments left cannot underflow as the invoice status // will be updated to `Paid` once `paymentLeft` is zero unchecked { - uint24 paymentsLeft = invoice.payment.paymentsLeft - 1; + uint40 paymentsLeft = invoice.payment.paymentsLeft - 1; _invoices[id].payment.paymentsLeft = paymentsLeft; if (paymentsLeft == 0) { _invoices[id].status = Types.Status.Paid; @@ -196,12 +195,12 @@ contract InvoiceModule is IInvoiceModule, StreamManager { if (invoice.payment.asset == address(0)) { // Checks: the payment amount matches the invoice value if (msg.value < invoice.payment.amount) { - revert Errors.InvalidPaymentAmount({ amount: invoice.payment.amount }); + revert Errors.PaymentAmountLessThanInvoiceValue({ amount: invoice.payment.amount }); } // Interactions: pay the recipient with native token (ETH) (bool success, ) = payable(invoice.recipient).call{ value: invoice.payment.amount }(""); - if (!success) revert Errors.PaymentFailed(); + if (!success) revert Errors.NativeTokenPaymentFailed(); } else { // Interactions: pay the recipient with the ERC-20 token IERC20(invoice.payment.asset).safeTransfer({ @@ -228,8 +227,8 @@ contract InvoiceModule is IInvoiceModule, StreamManager { asset: IERC20(invoice.payment.asset), totalAmount: invoice.payment.amount, startTime: invoice.startTime, - endTime: invoice.endTime, recipient: invoice.recipient, + numberOfTranches: invoice.payment.paymentsLeft, recurrence: invoice.payment.recurrence }); } @@ -241,8 +240,15 @@ contract InvoiceModule is IInvoiceModule, StreamManager { uint40 startTime, uint40 endTime ) internal pure { - // Calculate the expected number of payments based on the invoice recurrence and payment interval - uint40 numberOfPayments = Helpers.computeNumberOfRecurringPayments(recurrence, startTime, endTime); + // 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; + unchecked { + interval = endTime - startTime; + } + + // Check and calculate the expected number of payments based on the invoice recurrence and payment interval + uint40 numberOfPayments = Helpers.checkAndComputeNumberOfRecurringPayments(recurrence, interval); // Checks: the specified number of payments is valid if (paymentsLeft != numberOfPayments) { diff --git a/src/modules/invoice-module/libraries/Helpers.sol b/src/modules/invoice-module/libraries/Helpers.sol index 89283047..d61494c0 100644 --- a/src/modules/invoice-module/libraries/Helpers.sol +++ b/src/modules/invoice-module/libraries/Helpers.sol @@ -2,18 +2,20 @@ pragma solidity ^0.8.26; import { Types } from "./Types.sol"; +import { Errors } from "./Errors.sol"; /// @title Helpers -/// @notice Library with helpers used across the Invoice Module contracts +/// @notice Library with helpers used across the {InvoiceModule} contract library Helpers { - /// @dev Calculates the number of payments that must be done based on a Recurring invoice - function computeNumberOfRecurringPayments( + /// @dev Calculates the number of payments that must be done for a recurring transfer invoice + /// 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 checkAndComputeNumberOfRecurringPayments( Types.Recurrence recurrence, - uint40 startTime, - uint40 endTime + uint40 interval ) internal pure returns (uint40 numberOfPayments) { - uint40 interval = endTime - startTime; - + // Calculate the number of payments based on the recurrence type if (recurrence == Types.Recurrence.Weekly) { numberOfPayments = interval / 1 weeks; } else if (recurrence == Types.Recurrence.Monthly) { @@ -21,5 +23,10 @@ library Helpers { } else if (recurrence == Types.Recurrence.Yearly) { numberOfPayments = interval / 48 weeks; } + + // Revert if there are zero payments to be made since the payment method cannot be a recurring transfer + if (numberOfPayments == 0) { + revert Errors.PaymentIntervalTooShortForSelectedRecurrence(); + } } } diff --git a/src/modules/invoice-module/libraries/Types.sol b/src/modules/invoice-module/libraries/Types.sol index 6b720ef6..aa2f20b5 100644 --- a/src/modules/invoice-module/libraries/Types.sol +++ b/src/modules/invoice-module/libraries/Types.sol @@ -20,7 +20,7 @@ library Types { // slot 0 Method method; Recurrence recurrence; - uint24 paymentsLeft; + uint40 paymentsLeft; address asset; // slot 1 uint128 amount; diff --git a/src/modules/invoice-module/sablier-v2/StreamManager.sol b/src/modules/invoice-module/sablier-v2/StreamManager.sol index b54b687b..af8709d0 100644 --- a/src/modules/invoice-module/sablier-v2/StreamManager.sol +++ b/src/modules/invoice-module/sablier-v2/StreamManager.sol @@ -10,7 +10,6 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ud60x18, UD60x18 } from "@prb/math/src/UD60x18.sol"; import { IStreamManager } from "./interfaces/IStreamManager.sol"; -import { Helpers } from "./../libraries/Helpers.sol"; import { Errors } from "./../libraries/Errors.sol"; import { Types } from "./../libraries/Types.sol"; @@ -83,15 +82,15 @@ contract StreamManager is IStreamManager { IERC20 asset, uint128 totalAmount, uint40 startTime, - uint40 endTime, address recipient, + uint128 numberOfTranches, Types.Recurrence recurrence ) 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, spender: address(LOCKUP_TRANCHED), amount: totalAmount }); // Create the Lockup Linear stream - streamId = _createTranchedStream(asset, totalAmount, startTime, endTime, recipient, recurrence); + streamId = _createTranchedStream(asset, totalAmount, startTime, recipient, numberOfTranches, recurrence); } /// @inheritdoc IStreamManager @@ -212,12 +211,12 @@ contract StreamManager is IStreamManager { IERC20 asset, uint128 totalAmount, uint40 startTime, - uint40 endTime, address recipient, + uint128 numberOfTranches, Types.Recurrence recurrence ) internal returns (uint256 streamId) { // Declare the params struct - LockupTranched.CreateWithDurations memory params; + LockupTranched.CreateWithTimestamps memory params; // Declare the function parameters params.sender = msg.sender; // The sender will be able to cancel the stream @@ -227,9 +226,6 @@ contract StreamManager is IStreamManager { params.cancelable = true; // Whether the stream will be cancelable or not params.transferable = true; // Whether the stream will be transferable or not - // Calculate the number of tranches based on the payment interval and the type of recurrence - uint128 numberOfTranches = Helpers.computeNumberOfRecurringPayments(recurrence, startTime, endTime); - // Calculate the duration of each tranche based on the payment recurrence uint40 durationPerTranche = _computeDurationPerTrache(recurrence); @@ -237,19 +233,22 @@ contract StreamManager is IStreamManager { uint128 amountPerTranche = totalAmount / numberOfTranches; // Create the tranches array - params.tranches = new LockupTranched.TrancheWithDuration[](numberOfTranches); + params.tranches = new LockupTranched.Tranche[](numberOfTranches); for (uint256 i; i < numberOfTranches; ++i) { - params.tranches[i] = LockupTranched.TrancheWithDuration({ + params.tranches[i] = LockupTranched.Tranche({ amount: amountPerTranche, - duration: durationPerTranche + timestamp: startTime + durationPerTranche }); + + // Jump to the next tranche by adding the duration per tranche timestamp to the start time + startTime += durationPerTranche; } // Optional parameter for charging a fee params.broker = Broker({ account: brokerAdmin, fee: brokerFee }); // Create the LockupTranched stream - streamId = LOCKUP_TRANCHED.createWithDurations(params); + streamId = LOCKUP_TRANCHED.createWithTimestamps(params); } function _transferFromAndApprove(IERC20 asset, uint128 amount, address spender) internal { diff --git a/src/modules/invoice-module/sablier-v2/interfaces/IStreamManager.sol b/src/modules/invoice-module/sablier-v2/interfaces/IStreamManager.sol index 05f76fd1..ffa3079d 100644 --- a/src/modules/invoice-module/sablier-v2/interfaces/IStreamManager.sol +++ b/src/modules/invoice-module/sablier-v2/interfaces/IStreamManager.sol @@ -64,15 +64,15 @@ interface IStreamManager { /// @param asset The address of the ERC-20 token to be streamed /// @param totalAmount The total amount of ERC-20 tokens to be streamed /// @param startTime The timestamp when the stream takes effect - /// @param endTime The timestamp by which the stream must be paid /// @param recipient The address receiving the ERC-20 tokens + /// @param numberOfTranches The number of tranches paid by the stream /// @param recurrence The recurrence of each tranche function createTranchedStream( IERC20 asset, uint128 totalAmount, uint40 startTime, - uint40 endTime, address recipient, + uint128 numberOfTranches, Types.Recurrence recurrence ) external returns (uint256 streamId); From da2270b016ce8313ae2c637ecc0b1a23667fd2d0 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 15 Jul 2024 17:29:55 +0300 Subject: [PATCH 31/40] test: move integration-related contracts to dedicated container --- test/Base.t.sol | 16 ++----- test/integration/Integration.t.sol | 47 +++++++++++++++++++- test/integration/shared/InvoiceModule.t.sol | 49 +++++++++++++++++++++ 3 files changed, 99 insertions(+), 13 deletions(-) create mode 100644 test/integration/shared/InvoiceModule.t.sol diff --git a/test/Base.t.sol b/test/Base.t.sol index e010f0eb..a0731a41 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -5,13 +5,9 @@ import { Events } from "./utils/Events.sol"; import { Users } from "./utils/Types.sol"; import { Test } from "forge-std/Test.sol"; import { MockERC20NoReturn } from "./mocks/MockERC20NoReturn.sol"; +import { MockNonCompliantContainer } from "./mocks/MockNonCompliantContainer.sol"; import { MockModule } from "./mocks/MockModule.sol"; import { Container } from "./../src/Container.sol"; -import { InvoiceModule } from "./../src/modules/invoice-module/InvoiceModule.sol"; -import { SablierV2LockupLinear } from "@sablier/v2-core/src/SablierV2LockupLinear.sol"; -import { SablierV2LockupTranched } from "@sablier/v2-core/src/SablierV2LockupTranched.sol"; -import { SablierV2Lockup } from "@sablier/v2-core/src/abstracts/SablierV2Lockup.sol"; -import { NFTDescriptorMock } from "@sablier/v2-core/test/mocks/NFTDescriptorMock.sol"; abstract contract Base_Test is Test, Events { /*////////////////////////////////////////////////////////////////////////// @@ -24,16 +20,10 @@ abstract contract Base_Test is Test, Events { TEST CONTRACTS //////////////////////////////////////////////////////////////////////////*/ - InvoiceModule internal invoiceModule; Container internal container; MockERC20NoReturn internal usdt; MockModule internal mockModule; - - // Sablier V2 related test contracts - NFTDescriptorMock internal mockNFTDescriptor; - SablierV2LockupLinear internal sablierV2LockupLinear; - SablierV2LockupTranched internal sablierV2LockupTranched; - SablierV2Lockup internal sablier; + MockNonCompliantContainer internal mockNonCompliantContainer; /*////////////////////////////////////////////////////////////////////////// SET-UP FUNCTION @@ -48,10 +38,12 @@ abstract contract Base_Test is Test, Events { // Deploy test contracts mockModule = new MockModule(); + mockNonCompliantContainer = new MockNonCompliantContainer({ _owner: users.admin }); // Label the test contracts so we can easily track them vm.label({ account: address(usdt), newLabel: "USDT" }); vm.label({ account: address(mockModule), newLabel: "MockModule" }); + vm.label({ account: address(mockNonCompliantContainer), newLabel: "MockNonCompliantContainer" }); } /*////////////////////////////////////////////////////////////////////////// diff --git a/test/integration/Integration.t.sol b/test/integration/Integration.t.sol index d77a22fb..241f42c6 100644 --- a/test/integration/Integration.t.sol +++ b/test/integration/Integration.t.sol @@ -2,8 +2,22 @@ pragma solidity ^0.8.26; import { Base_Test } from "../Base.t.sol"; +import { InvoiceModule } from "./../../src/modules/invoice-module/InvoiceModule.sol"; +import { SablierV2LockupLinear } from "@sablier/v2-core/src/SablierV2LockupLinear.sol"; +import { SablierV2LockupTranched } from "@sablier/v2-core/src/SablierV2LockupTranched.sol"; +import { NFTDescriptorMock } from "@sablier/v2-core/test/mocks/NFTDescriptorMock.sol"; abstract contract Integration_Test is Base_Test { + /*////////////////////////////////////////////////////////////////////////// + TEST CONTRACTS + //////////////////////////////////////////////////////////////////////////*/ + + InvoiceModule internal invoiceModule; + // Sablier V2 related test contracts + NFTDescriptorMock internal mockNFTDescriptor; + SablierV2LockupLinear internal sablierV2LockupLinear; + SablierV2LockupTranched internal sablierV2LockupTranched; + /*////////////////////////////////////////////////////////////////////////// SET-UP FUNCTION //////////////////////////////////////////////////////////////////////////*/ @@ -11,10 +25,13 @@ abstract contract Integration_Test is Base_Test { function setUp() public virtual override { Base_Test.setUp(); + // Deploy the {InvoiceModule} modul + deployInvoiceModule(); + // Make Eve the default caller to deploy a new {Container} contract vm.startPrank({ msgSender: users.eve }); - // Setup the initial {InvoiceModule} module + // Setup the initial {InvoiceModule} module to be initialized on the {Container} address[] memory modules = new address[](1); modules[0] = address(invoiceModule); @@ -23,5 +40,33 @@ abstract contract Integration_Test is Base_Test { // Stop the prank to be able to start a different one in the test suite vm.stopPrank(); + + // Label the test contracts so we can easily track them + vm.label({ account: address(invoiceModule), newLabel: "InvoiceModule" }); + vm.label({ account: address(sablierV2LockupLinear), newLabel: "SablierV2LockupLinear" }); + vm.label({ account: address(sablierV2LockupTranched), newLabel: "SablierV2LockupTranched" }); + } + + /*////////////////////////////////////////////////////////////////////////// + DEPLOYMENT-RELATED FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Deploys the {InvoiceModule} module by initializing the Sablier v2-required contracts first + function deployInvoiceModule() internal { + mockNFTDescriptor = new NFTDescriptorMock(); + sablierV2LockupLinear = new SablierV2LockupLinear({ + initialAdmin: users.admin, + initialNFTDescriptor: mockNFTDescriptor + }); + sablierV2LockupTranched = new SablierV2LockupTranched({ + initialAdmin: users.admin, + initialNFTDescriptor: mockNFTDescriptor, + maxTrancheCount: 1000 + }); + invoiceModule = new InvoiceModule({ + _sablierLockupLinear: sablierV2LockupLinear, + _sablierLockupTranched: sablierV2LockupTranched, + _brokerAdmin: users.admin + }); } } diff --git a/test/integration/shared/InvoiceModule.t.sol b/test/integration/shared/InvoiceModule.t.sol new file mode 100644 index 00000000..6077e801 --- /dev/null +++ b/test/integration/shared/InvoiceModule.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { Base_Test } from "../../Base.t.sol"; +import { Types } from "./../../../src/modules/invoice-module/libraries/Types.sol"; +import { Helpers } from "../../utils/Helpers.sol"; + +abstract contract InvoiceModule_Integration_Shared_Test is Base_Test { + Types.Invoice _invoice; + + function setUp() public virtual override { + Base_Test.setUp(); + + _invoice.recipient = users.eve; + _invoice.status = Types.Status.Pending; + } + + /// @dev Creates an invoice with a one-off transfer payment + function createInvoiceWithOneOffTransfer() internal { + _invoice.startTime = uint40(block.timestamp); + _invoice.endTime = uint40(block.timestamp) + 4 weeks; + _invoice.payment = Types.Payment({ + method: Types.Method.Transfer, + recurrence: Types.Recurrence.OneOff, + paymentsLeft: 1, + asset: address(usdt), + amount: 100e18, + streamId: 0 + }); + } + + /// @dev Creates an invoice with a recurring transfer payment + function createInvoiceWithRecurringTransfer(Types.Recurrence recurrence) internal { + _invoice.startTime = uint40(block.timestamp); + _invoice.endTime = uint40(block.timestamp) + 4 weeks; + + uint40 interval = _invoice.endTime - _invoice.startTime; + uint40 numberOfPayments = Helpers.computeNumberOfRecurringPayments(recurrence, interval); + + _invoice.payment = Types.Payment({ + method: Types.Method.Transfer, + recurrence: recurrence, + paymentsLeft: numberOfPayments, + asset: address(usdt), + amount: 100e18, + streamId: 0 + }); + } +} From 9c274b6ec5895f2117f1897c88880731a1aaa85a Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 15 Jul 2024 17:30:59 +0300 Subject: [PATCH 32/40] test: fix outdated error in 'execute' unit test and add missing helper and events --- .../concrete/container/execute/execute.t.sol | 6 ++-- test/utils/Events.sol | 2 +- test/utils/Helpers.sol | 28 ++++++++++++++----- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/test/unit/concrete/container/execute/execute.t.sol b/test/unit/concrete/container/execute/execute.t.sol index 86c262f9..9769847f 100644 --- a/test/unit/concrete/container/execute/execute.t.sol +++ b/test/unit/concrete/container/execute/execute.t.sol @@ -2,8 +2,6 @@ pragma solidity ^0.8.26; import { Container_Unit_Concrete_Test } from "../Container.t.sol"; -import { Types as InvoiceModuleTypes } from "./../../../../../src/modules/invoice-module/libraries/Types.sol"; -import { Helpers } from "../../../../utils/Helpers.sol"; import { Errors } from "../../../../utils/Errors.sol"; import { Events } from "../../../../utils/Events.sol"; @@ -20,7 +18,7 @@ contract Execute_Unit_Concrete_Test is Container_Unit_Concrete_Test { vm.expectRevert(Errors.Unauthorized.selector); // Run the test - container.execute({ module: address(invoiceModule), value: 0, data: "" }); + container.execute({ module: address(mockModule), value: 0, data: "" }); } modifier whenCallerOwner() { @@ -57,7 +55,7 @@ contract Execute_Unit_Concrete_Test is Container_Unit_Concrete_Test { // Expect the {ModuleExecutionFailed} event to be emitted vm.expectEmit(); - emit Events.ModuleExecutionFailed({ module: address(mockModule), value: 0, data: wrongData }); + emit Events.ModuleExecutionFailed({ module: address(mockModule), value: 0, data: wrongData, error: "" }); // Run the test container.execute({ module: address(mockModule), value: 0, data: wrongData }); diff --git a/test/utils/Events.sol b/test/utils/Events.sol index d39d0b69..d5080b52 100644 --- a/test/utils/Events.sol +++ b/test/utils/Events.sol @@ -23,7 +23,7 @@ abstract contract Events { /// @param module The address of the module that was executed /// @param value The value sent to the module address required for the call /// @param data The ABI-encoded method called on the module - event ModuleExecutionFailed(address indexed module, uint256 value, bytes data); + event ModuleExecutionFailed(address indexed module, uint256 value, bytes data, bytes4 error); /// @notice Emitted when a module execution is successful /// @param module The address of the module that was executed diff --git a/test/utils/Helpers.sol b/test/utils/Helpers.sol index 786731dc..eb600ce5 100644 --- a/test/utils/Helpers.sol +++ b/test/utils/Helpers.sol @@ -1,19 +1,19 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import { Types as InvoiceModulesTypes } from "./../../src/modules/invoice-module/libraries/Types.sol"; +import { Types as InvoiceModuleTypes } from "./../../src/modules/invoice-module/libraries/Types.sol"; library Helpers { - function createInvoiceDataType(address recipient) public view returns (InvoiceModulesTypes.Invoice memory) { + function createInvoiceDataType(address recipient) public view returns (InvoiceModuleTypes.Invoice memory) { return - InvoiceModulesTypes.Invoice({ + InvoiceModuleTypes.Invoice({ recipient: recipient, - status: InvoiceModulesTypes.Status.Pending, + status: InvoiceModuleTypes.Status.Pending, startTime: 0, endTime: uint40(block.timestamp) + 1 weeks, - payment: InvoiceModulesTypes.Payment({ - method: InvoiceModulesTypes.Method.Transfer, - recurrence: InvoiceModulesTypes.Recurrence.OneOff, + payment: InvoiceModuleTypes.Payment({ + method: InvoiceModuleTypes.Method.Transfer, + recurrence: InvoiceModuleTypes.Recurrence.OneOff, paymentsLeft: 1, asset: address(0), amount: uint128(1 ether), @@ -21,4 +21,18 @@ library Helpers { }) }); } + + /// @dev Calculates the number of payments that must be done based on a Recurring invoice + function computeNumberOfRecurringPayments( + InvoiceModuleTypes.Recurrence recurrence, + uint40 interval + ) internal pure returns (uint40 numberOfPayments) { + if (recurrence == InvoiceModuleTypes.Recurrence.Weekly) { + numberOfPayments = interval / 1 weeks; + } else if (recurrence == InvoiceModuleTypes.Recurrence.Monthly) { + numberOfPayments = interval / 4 weeks; + } else if (recurrence == InvoiceModuleTypes.Recurrence.Yearly) { + numberOfPayments = interval / 48 weeks; + } + } } From 4633d8dc7df96a8ece481e883694d5ac4bf57ec7 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Mon, 15 Jul 2024 17:31:24 +0300 Subject: [PATCH 33/40] test: add 'MockNonCompliantContainer' mock contract --- test/mocks/MockNonCompliantContainer.sol | 51 ++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 test/mocks/MockNonCompliantContainer.sol diff --git a/test/mocks/MockNonCompliantContainer.sol b/test/mocks/MockNonCompliantContainer.sol new file mode 100644 index 00000000..67cd43cc --- /dev/null +++ b/test/mocks/MockNonCompliantContainer.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { ExcessivelySafeCall } from "@nomad-xyz/excessively-safe-call/src/ExcessivelySafeCall.sol"; +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +/// @title Container +/// @notice A mock non-compliant container contract that do not support the {IContainer} interface +contract MockNonCompliantContainer is IERC165 { + using ExcessivelySafeCall for address; + + address public owner; + + event ModuleExecutionSucceded(address module, uint256 value, bytes data); + event ModuleExecutionFailed(address module, uint256 value, bytes data, bytes error); + + constructor(address _owner) { + owner = _owner; + } + + modifier onlyOwner() { + _; + } + + function execute(address module, uint256 value, bytes memory data) external onlyOwner returns (bool success) { + // Allocate all the gas to the executed module method + uint256 txGas = gasleft(); + + // Execute the call via assembly to avoid returnbomb attacks + // See https://github.com/nomad-xyz/ExcessivelySafeCall + // + // Account for the returned data only if the `_success` boolean is false + // in which case revert with the error message + bytes memory result; + (success, result) = module.excessivelySafeCall({ _gas: txGas, _value: 0, _maxCopy: 4, _calldata: data }); + + if (!success) { + emit ModuleExecutionFailed(module, value, data, result); + + // Revert with the error + assembly { + revert(add(result, 0x20), result) + } + } else emit ModuleExecutionSucceded(module, value, data); + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public pure override returns (bool) { + return interfaceId == type(IERC165).interfaceId; + } +} From eb16f289c110c6b86844183cd0ef0cee954ce94f Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Tue, 16 Jul 2024 10:03:46 +0300 Subject: [PATCH 34/40] fix(container): revert with the same error returned by the module contract on 'execute' --- src/Container.sol | 13 +++++-------- src/interfaces/IContainer.sol | 6 ------ test/mocks/MockNonCompliantContainer.sol | 13 +++++-------- test/utils/Events.sol | 6 ------ 4 files changed, 10 insertions(+), 28 deletions(-) diff --git a/src/Container.sol b/src/Container.sol index 8f608265..b3447bf6 100644 --- a/src/Container.sol +++ b/src/Container.sol @@ -66,21 +66,18 @@ contract Container is IContainer, ModuleManager { // Allocate all the gas to the executed module method uint256 txGas = gasleft(); - // Execute the call via assembly to avoid returnbomb attacks + // Execute the call via assembly and get only the first 4 bytes of the returndata + // which will be the selector of the error in case of a revert in the module contract // See https://github.com/nomad-xyz/ExcessivelySafeCall - // - // Account for the returned data only if the `_success` boolean is false - // in which case revert with the error message bytes memory result; (success, result) = module.excessivelySafeCall({ _gas: txGas, _value: 0, _maxCopy: 4, _calldata: data }); + // Revert with the same error returned by the module contract if the call failed if (!success) { - emit ModuleExecutionFailed(module, value, data, result); - - // Revert with the error assembly { - revert(add(result, 0x20), result) + revert(add(result, 0x20), 4) } + // Otherwise log the execution success } else emit ModuleExecutionSucceded(module, value, data); } diff --git a/src/interfaces/IContainer.sol b/src/interfaces/IContainer.sol index d0564805..f604da18 100644 --- a/src/interfaces/IContainer.sol +++ b/src/interfaces/IContainer.sol @@ -24,12 +24,6 @@ interface IContainer is IERC165 { /// @param amount The amount of the withdrawn ERC-20 token event AssetWithdrawn(address indexed sender, address indexed asset, uint256 amount); - /// @notice Emitted when a module execution fails - /// @param module The address of the module that was executed - /// @param value The value sent to the module address required for the call - /// @param data The ABI-encoded method called on the module - event ModuleExecutionFailed(address indexed module, uint256 value, bytes data, bytes error); - /// @notice Emitted when a module execution is successful /// @param module The address of the module that was executed /// @param value The value sent to the module address required for the call diff --git a/test/mocks/MockNonCompliantContainer.sol b/test/mocks/MockNonCompliantContainer.sol index 67cd43cc..cfdcccc6 100644 --- a/test/mocks/MockNonCompliantContainer.sol +++ b/test/mocks/MockNonCompliantContainer.sol @@ -26,21 +26,18 @@ contract MockNonCompliantContainer is IERC165 { // Allocate all the gas to the executed module method uint256 txGas = gasleft(); - // Execute the call via assembly to avoid returnbomb attacks + // Execute the call via assembly and get only the first 4 bytes of the returndata + // which will be the selector of the error in case of a revert in the module contract // See https://github.com/nomad-xyz/ExcessivelySafeCall - // - // Account for the returned data only if the `_success` boolean is false - // in which case revert with the error message bytes memory result; (success, result) = module.excessivelySafeCall({ _gas: txGas, _value: 0, _maxCopy: 4, _calldata: data }); if (!success) { - emit ModuleExecutionFailed(module, value, data, result); - - // Revert with the error + // Revert with the same error returned by the module contract assembly { - revert(add(result, 0x20), result) + revert(add(result, 0x20), 4) } + // Log the execution success } else emit ModuleExecutionSucceded(module, value, data); } diff --git a/test/utils/Events.sol b/test/utils/Events.sol index d5080b52..13f325d8 100644 --- a/test/utils/Events.sol +++ b/test/utils/Events.sol @@ -19,12 +19,6 @@ abstract contract Events { /// @param amount The amount of the withdrawn ERC-20 token event AssetWithdrawn(address indexed sender, address indexed asset, uint256 amount); - /// @notice Emitted when a module execution fails - /// @param module The address of the module that was executed - /// @param value The value sent to the module address required for the call - /// @param data The ABI-encoded method called on the module - event ModuleExecutionFailed(address indexed module, uint256 value, bytes data, bytes4 error); - /// @notice Emitted when a module execution is successful /// @param module The address of the module that was executed /// @param value The value sent to the module address required for the call From 18604356cc3c9a4749c258a04bac02f811f11269 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Tue, 16 Jul 2024 10:04:17 +0300 Subject: [PATCH 35/40] test: fix 'execute' test --- test/unit/concrete/container/execute/execute.t.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/unit/concrete/container/execute/execute.t.sol b/test/unit/concrete/container/execute/execute.t.sol index 9769847f..e4abeb15 100644 --- a/test/unit/concrete/container/execute/execute.t.sol +++ b/test/unit/concrete/container/execute/execute.t.sol @@ -53,9 +53,8 @@ contract Execute_Unit_Concrete_Test is Container_Unit_Concrete_Test { // Alter the `createModuleItem` method signature by adding an invalid `uint256` field bytes memory wrongData = abi.encodeWithSignature("createModuleItem(uint256)", 1); - // Expect the {ModuleExecutionFailed} event to be emitted - vm.expectEmit(); - emit Events.ModuleExecutionFailed({ module: address(mockModule), value: 0, data: wrongData, error: "" }); + // Expect the call to be reverted due to invalid method signature + vm.expectRevert(); // Run the test container.execute({ module: address(mockModule), value: 0, data: wrongData }); From ef946c4d76a033e13e21894a30f89f6938a28f7d Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Tue, 16 Jul 2024 10:44:38 +0300 Subject: [PATCH 36/40] fix(invoice-module): invoice recipient and error renaming --- src/modules/invoice-module/InvoiceModule.sol | 8 ++++---- src/modules/invoice-module/libraries/Errors.sol | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/invoice-module/InvoiceModule.sol b/src/modules/invoice-module/InvoiceModule.sol index 7b2f2bd4..f3eaf71a 100644 --- a/src/modules/invoice-module/InvoiceModule.sol +++ b/src/modules/invoice-module/InvoiceModule.sol @@ -88,7 +88,7 @@ contract InvoiceModule is IInvoiceModule, StreamManager { // Checks: end time is not in the past uint40 currentTime = uint40(block.timestamp); if (currentTime >= invoice.endTime) { - revert Errors.EndTimeLowerThanCurrentTime(); + revert Errors.EndTimeInThePast(); } // Checks: validate the input parameters if the invoice must be paid in even recurring transfers or by a tranched stream @@ -111,7 +111,7 @@ contract InvoiceModule is IInvoiceModule, StreamManager { // Effects: create the invoice _invoices[id] = Types.Invoice({ - recipient: msg.sender, + recipient: invoice.recipient, status: Types.Status.Pending, startTime: invoice.startTime, endTime: invoice.endTime, @@ -132,12 +132,12 @@ contract InvoiceModule is IInvoiceModule, StreamManager { } // Effects: add the invoice on the list of invoices generated by the container - _invoicesOf[msg.sender].push(id); + _invoicesOf[invoice.recipient].push(id); // Log the invoice creation emit InvoiceCreated({ id: id, - recipient: msg.sender, + recipient: invoice.recipient, status: Types.Status.Pending, startTime: invoice.startTime, endTime: invoice.endTime, diff --git a/src/modules/invoice-module/libraries/Errors.sol b/src/modules/invoice-module/libraries/Errors.sol index e327a236..ee04f361 100644 --- a/src/modules/invoice-module/libraries/Errors.sol +++ b/src/modules/invoice-module/libraries/Errors.sol @@ -15,7 +15,7 @@ library Errors { error ContainerUnsupportedInterface(); /// @notice Thrown when the end time of an invoice is in the past - error EndTimeLowerThanCurrentTime(); + error EndTimeInThePast(); /// @notice Thrown when the start time is later than the end time error StartTimeGreaterThanEndTime(); From 78664be0e9c4dc142491527ce8d71900f657431b Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Tue, 16 Jul 2024 10:45:39 +0300 Subject: [PATCH 37/40] test: switch inheritance on the 'InvoiceModule.t.sol' --- test/integration/shared/InvoiceModule.t.sol | 26 ++++++++++----------- test/utils/Errors.sol | 6 ++++- test/utils/Events.sol | 22 +++++++++++++++++ 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/test/integration/shared/InvoiceModule.t.sol b/test/integration/shared/InvoiceModule.t.sol index 6077e801..b630448c 100644 --- a/test/integration/shared/InvoiceModule.t.sol +++ b/test/integration/shared/InvoiceModule.t.sol @@ -1,25 +1,25 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import { Base_Test } from "../../Base.t.sol"; +import { Integration_Test } from "../Integration.t.sol"; import { Types } from "./../../../src/modules/invoice-module/libraries/Types.sol"; import { Helpers } from "../../utils/Helpers.sol"; -abstract contract InvoiceModule_Integration_Shared_Test is Base_Test { - Types.Invoice _invoice; +abstract contract InvoiceModule_Integration_Shared_Test is Integration_Test { + Types.Invoice invoice; function setUp() public virtual override { - Base_Test.setUp(); + Integration_Test.setUp(); - _invoice.recipient = users.eve; - _invoice.status = Types.Status.Pending; + invoice.recipient = users.eve; + invoice.status = Types.Status.Pending; } /// @dev Creates an invoice with a one-off transfer payment function createInvoiceWithOneOffTransfer() internal { - _invoice.startTime = uint40(block.timestamp); - _invoice.endTime = uint40(block.timestamp) + 4 weeks; - _invoice.payment = Types.Payment({ + invoice.startTime = uint40(block.timestamp); + invoice.endTime = uint40(block.timestamp) + 4 weeks; + invoice.payment = Types.Payment({ method: Types.Method.Transfer, recurrence: Types.Recurrence.OneOff, paymentsLeft: 1, @@ -31,13 +31,13 @@ abstract contract InvoiceModule_Integration_Shared_Test is Base_Test { /// @dev Creates an invoice with a recurring transfer payment function createInvoiceWithRecurringTransfer(Types.Recurrence recurrence) internal { - _invoice.startTime = uint40(block.timestamp); - _invoice.endTime = uint40(block.timestamp) + 4 weeks; + invoice.startTime = uint40(block.timestamp); + invoice.endTime = uint40(block.timestamp) + 4 weeks; - uint40 interval = _invoice.endTime - _invoice.startTime; + uint40 interval = invoice.endTime - invoice.startTime; uint40 numberOfPayments = Helpers.computeNumberOfRecurringPayments(recurrence, interval); - _invoice.payment = Types.Payment({ + invoice.payment = Types.Payment({ method: Types.Method.Transfer, recurrence: recurrence, paymentsLeft: numberOfPayments, diff --git a/test/utils/Errors.sol b/test/utils/Errors.sol index 29e8e4b3..ddd3857f 100644 --- a/test/utils/Errors.sol +++ b/test/utils/Errors.sol @@ -47,7 +47,7 @@ library Errors { error ContainerUnsupportedInterface(); /// @notice Thrown when the end time of an invoice is in the past - error EndTimeLowerThanCurrentTime(); + error EndTimeInThePast(); /// @notice Thrown when the start time is later than the end time error StartTimeGreaterThanEndTime(); @@ -73,6 +73,10 @@ library Errors { /// @notice Thrown when a payer attempts to pay a canceled invoice error InvoiceCanceled(); + /// @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(); + /*////////////////////////////////////////////////////////////////////////// STREAM-MANAGER //////////////////////////////////////////////////////////////////////////*/ diff --git a/test/utils/Events.sol b/test/utils/Events.sol index 13f325d8..c10c6c7c 100644 --- a/test/utils/Events.sol +++ b/test/utils/Events.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; +import { Types } from "./../../src/modules/invoice-module/libraries/Types.sol"; + /// @notice Abstract contract to store all the events emitted in the tested contracts abstract contract Events { /*////////////////////////////////////////////////////////////////////////// @@ -36,4 +38,24 @@ abstract contract Events { /// @notice Emitted when a module is disabled on the container /// @param module The address of the disabled module event ModuleDisabled(address indexed module); + + /*////////////////////////////////////////////////////////////////////////// + INVOICE + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when a regular or recurring 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 + ); } From d0fd422cc3ff25ce6812c58c11ad49036cb86fc5 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Tue, 16 Jul 2024 17:50:06 +0300 Subject: [PATCH 38/40] refactor(invoice-module): calculate 'paymentsLeft' on-chain, add new checks and errors --- src/modules/invoice-module/InvoiceModule.sol | 60 ++++++++++++------- .../invoice-module/libraries/Errors.sol | 6 +- .../invoice-module/libraries/Helpers.sol | 10 +--- 3 files changed, 43 insertions(+), 33 deletions(-) diff --git a/src/modules/invoice-module/InvoiceModule.sol b/src/modules/invoice-module/InvoiceModule.sol index f3eaf71a..b9bc6973 100644 --- a/src/modules/invoice-module/InvoiceModule.sol +++ b/src/modules/invoice-module/InvoiceModule.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; @@ -91,19 +90,36 @@ contract InvoiceModule is IInvoiceModule, StreamManager { revert Errors.EndTimeInThePast(); } - // Checks: validate the input parameters if the invoice must be paid in even recurring transfers or by a tranched stream - if (invoice.payment.method == Types.Method.Transfer || invoice.payment.method == Types.Method.TranchedStream) { - if (invoice.payment.recurrence != Types.Recurrence.OneOff) { - _checkRecurringTransferInvoiceParams({ - recurrence: invoice.payment.recurrence, - paymentsLeft: invoice.payment.paymentsLeft, - startTime: invoice.startTime, - endTime: invoice.endTime - }); + // 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(); + } + } + + // Gets the number of payments for the invoice based on the payment method, interval and recurrence type + // + // Notes: + // - There should be only one payment when dealing with a one-off transfer-based invoice + // - When dealing with a recurring transfer or tranched stream, 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 = _checkAndComputeNumberOfPayments({ + recurrence: invoice.payment.recurrence, + startTime: invoice.startTime, + endTime: invoice.endTime + }); + } + + // 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(); } - // Or by using a linear or tranched stream in which case allow only ERC-20 assets - } else if (invoice.payment.asset == address(0)) { - revert Errors.OnlyERC20StreamsAllowed(); } // Get the next invoice ID @@ -118,7 +134,7 @@ contract InvoiceModule is IInvoiceModule, StreamManager { payment: Types.Payment({ recurrence: invoice.payment.recurrence, method: invoice.payment.method, - paymentsLeft: invoice.payment.paymentsLeft, + paymentsLeft: numberOfPayments, amount: invoice.payment.amount, asset: invoice.payment.asset, streamId: 0 @@ -233,13 +249,13 @@ contract InvoiceModule is IInvoiceModule, StreamManager { }); } - /// @dev Validates the input parameters if the invoice is recurring and must be paid in even transfers - function _checkRecurringTransferInvoiceParams( + /// @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 _checkAndComputeNumberOfPayments( Types.Recurrence recurrence, - uint40 paymentsLeft, uint40 startTime, uint40 endTime - ) internal pure { + ) 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; @@ -248,11 +264,11 @@ contract InvoiceModule is IInvoiceModule, StreamManager { } // Check and calculate the expected number of payments based on the invoice recurrence and payment interval - uint40 numberOfPayments = Helpers.checkAndComputeNumberOfRecurringPayments(recurrence, interval); + numberOfPayments = Helpers.computeNumberOfPayments(recurrence, interval); - // Checks: the specified number of payments is valid - if (paymentsLeft != numberOfPayments) { - revert Errors.InvalidNumberOfPayments({ expectedNumber: numberOfPayments }); + // 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/invoice-module/libraries/Errors.sol b/src/modules/invoice-module/libraries/Errors.sol index ee04f361..3cefaa23 100644 --- a/src/modules/invoice-module/libraries/Errors.sol +++ b/src/modules/invoice-module/libraries/Errors.sol @@ -29,9 +29,6 @@ library Errors { /// @notice Thrown when a payment in the native token (ETH) fails error NativeTokenPaymentFailed(); - /// @notice Thrown when the number of recurring payments set for a recurring transfer invoice is invalid - error InvalidNumberOfPayments(uint40 expectedNumber); - /// @notice Thrown when a linear or tranched stream is created with the native token as the payment asset error OnlyERC20StreamsAllowed(); @@ -45,6 +42,9 @@ library Errors { /// 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(); + /*////////////////////////////////////////////////////////////////////////// STREAM-MANAGER //////////////////////////////////////////////////////////////////////////*/ diff --git a/src/modules/invoice-module/libraries/Helpers.sol b/src/modules/invoice-module/libraries/Helpers.sol index d61494c0..d5486619 100644 --- a/src/modules/invoice-module/libraries/Helpers.sol +++ b/src/modules/invoice-module/libraries/Helpers.sol @@ -2,16 +2,15 @@ pragma solidity ^0.8.26; import { Types } from "./Types.sol"; -import { Errors } from "./Errors.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 invoice + /// @dev Calculates the number of payments that must be done for a recurring transfer or tranched stream invoice /// 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 checkAndComputeNumberOfRecurringPayments( + function computeNumberOfPayments( Types.Recurrence recurrence, uint40 interval ) internal pure returns (uint40 numberOfPayments) { @@ -23,10 +22,5 @@ library Helpers { } else if (recurrence == Types.Recurrence.Yearly) { numberOfPayments = interval / 48 weeks; } - - // Revert if there are zero payments to be made since the payment method cannot be a recurring transfer - if (numberOfPayments == 0) { - revert Errors.PaymentIntervalTooShortForSelectedRecurrence(); - } } } From aed988235551983f604f283cf19782af19b05b86 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Tue, 16 Jul 2024 17:50:45 +0300 Subject: [PATCH 39/40] test: add generic 'InvoiceModule' modifiers and update errors --- test/integration/shared/InvoiceModule.t.sol | 85 +++++++++++++++++++-- test/utils/Errors.sol | 6 +- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/test/integration/shared/InvoiceModule.t.sol b/test/integration/shared/InvoiceModule.t.sol index b630448c..7ffdd4ee 100644 --- a/test/integration/shared/InvoiceModule.t.sol +++ b/test/integration/shared/InvoiceModule.t.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.26; import { Integration_Test } from "../Integration.t.sol"; import { Types } from "./../../../src/modules/invoice-module/libraries/Types.sol"; -import { Helpers } from "../../utils/Helpers.sol"; abstract contract InvoiceModule_Integration_Shared_Test is Integration_Test { Types.Invoice invoice; @@ -15,10 +14,59 @@ abstract contract InvoiceModule_Integration_Shared_Test is Integration_Test { invoice.status = Types.Status.Pending; } + modifier whenCallerContract() { + _; + } + + modifier whenCompliantContainer() { + _; + } + + 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() internal { invoice.startTime = uint40(block.timestamp); invoice.endTime = uint40(block.timestamp) + 4 weeks; + invoice.payment = Types.Payment({ method: Types.Method.Transfer, recurrence: Types.Recurrence.OneOff, @@ -34,13 +82,40 @@ abstract contract InvoiceModule_Integration_Shared_Test is Integration_Test { invoice.startTime = uint40(block.timestamp); invoice.endTime = uint40(block.timestamp) + 4 weeks; - uint40 interval = invoice.endTime - invoice.startTime; - uint40 numberOfPayments = Helpers.computeNumberOfRecurringPayments(recurrence, interval); - invoice.payment = Types.Payment({ method: Types.Method.Transfer, recurrence: recurrence, - paymentsLeft: numberOfPayments, + paymentsLeft: 0, + asset: address(usdt), + amount: 100e18, + streamId: 0 + }); + } + + /// @dev Creates an invoice with a linear stream-based payment + function createInvoiceWithLinearStream() internal { + invoice.startTime = uint40(block.timestamp); + invoice.endTime = 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 { + invoice.startTime = uint40(block.timestamp); + invoice.endTime = uint40(block.timestamp) + 4 weeks; + + invoice.payment = Types.Payment({ + method: Types.Method.TranchedStream, + recurrence: recurrence, + paymentsLeft: 0, asset: address(usdt), amount: 100e18, streamId: 0 diff --git a/test/utils/Errors.sol b/test/utils/Errors.sol index ddd3857f..4fedf6a0 100644 --- a/test/utils/Errors.sol +++ b/test/utils/Errors.sol @@ -61,9 +61,6 @@ library Errors { /// @notice Thrown when a payment in the native token (ETH) fails error NativeTokenPaymentFailed(); - /// @notice Thrown when the number of recurring payments set for a recurring transfer invoice is invalid - error InvalidNumberOfPayments(uint40 expectedNumber); - /// @notice Thrown when a linear or tranched stream is created with the native token as the payment asset error OnlyERC20StreamsAllowed(); @@ -77,6 +74,9 @@ library Errors { /// 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(); + /*////////////////////////////////////////////////////////////////////////// STREAM-MANAGER //////////////////////////////////////////////////////////////////////////*/ From 1341a02eebd3d2ab2d993f7d51bd765c48d8c26e Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Tue, 16 Jul 2024 17:51:09 +0300 Subject: [PATCH 40/40] test(invoice-module): add 'createInvoice' integration tests --- .../create-invoice/createInvoice.t.sol | 507 ++++++++++++++++++ .../create-invoice/createInvoice.tree | 36 ++ 2 files changed, 543 insertions(+) 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