Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement payInvoice integration tests #6

Merged
merged 11 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 65 additions & 8 deletions src/modules/invoice-module/InvoiceModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -98,21 +98,26 @@ contract InvoiceModule is IInvoiceModule, StreamManager {
}
}

// Gets the number of payments for the invoice based on the payment method, interval and recurrence type
// Validates the invoice interval (endTime - startTime) and returns the number of payments of the invoice
// based on the payment method, interval and recurrence type
//
// Notes:
// - The number of payments is taken into account only for transfer-based invoices
// - There should be only one payment when dealing with a one-off transfer-based invoice
// - When dealing with a recurring transfer or tranched stream, the number of payments must be calculated based
// - When dealing with a recurring transfer, the number of payments must be calculated based
// on the payment interval (endTime - startTime) and recurrence type
uint40 numberOfPayments;
if (invoice.payment.method == Types.Method.Transfer && invoice.payment.recurrence == Types.Recurrence.OneOff) {
numberOfPayments = 1;
} else if (invoice.payment.method != Types.Method.LinearStream) {
numberOfPayments = _checkAndComputeNumberOfPayments({
numberOfPayments = _checkIntervalPayments({
recurrence: invoice.payment.recurrence,
startTime: invoice.startTime,
endTime: invoice.endTime
});

// Set the number of payments to zero if dealing with a tranched-based invoice
if (invoice.payment.method == Types.Method.TranchedStream) numberOfPayments = 0;
}

// Checks: the asset is different than the native token if dealing with either a linear or tranched stream-based invoice
Expand Down Expand Up @@ -166,6 +171,11 @@ contract InvoiceModule is IInvoiceModule, StreamManager {
// Load the invoice from storage
Types.Invoice memory invoice = _invoices[id];

// Checks: the invoice is not null
if (invoice.recipient == address(0)) {
revert Errors.InvoiceNull();
}

// Checks: the invoice is not already paid or canceled
if (invoice.status == Types.Status.Paid) {
revert Errors.InvoiceAlreadyPaid();
Expand All @@ -178,14 +188,55 @@ contract InvoiceModule is IInvoiceModule, StreamManager {
_payByTransfer(id, invoice);
} else {
uint256 streamId;
// Check to see wether to pay by creating a linear or tranched stream
// Check to see whether the invoice must be paid through a linear or tranched stream
if (invoice.payment.method == Types.Method.LinearStream) {
streamId = _payByLinearStream(invoice);
} else streamId = _payByTranchedStream(invoice);

// Effects: update the status of the invoice and stream ID
_invoices[id].status = Types.Status.Paid;
_invoices[id].payment.streamId = streamId;
}

// Log the payment transaction
emit InvoicePaid({ id: id, payer: msg.sender, status: invoice.status, payment: invoice.payment });
emit InvoicePaid({ id: id, payer: msg.sender, status: _invoices[id].status, payment: _invoices[id].payment });
}

/// @inheritdoc IInvoiceModule
function cancelInvoice(uint256 id) external {
// Load the invoice from storage
Types.Invoice memory invoice = _invoices[id];

// Checks: the invoice is paid or already canceled
if (invoice.status == Types.Status.Paid) {
revert Errors.CannotCancelPaidInvoice();
} else if (invoice.status == Types.Status.Canceled) {
revert Errors.CannotCancelCanceledInvoice();
}

// Checks: the `msg.sender` is the creator if dealing with a transfer-based invoice
//
// Notes:
// - for a linear or tranched stream-based invoice, the `msg.sender` is checked in the
// {SablierV2Lockup} `cancel` method
if (invoice.payment.method == Types.Method.Transfer) {
if (invoice.recipient != msg.sender) {
revert Errors.InvoiceOwnerUnauthorized();
}
}

// Effects: cancel the stream accordingly depending on its type
if (invoice.payment.method == Types.Method.LinearStream) {
cancelLinearStream({ streamId: invoice.payment.streamId });
} else if (invoice.payment.method == Types.Method.TranchedStream) {
cancelTranchedStream({ streamId: invoice.payment.streamId });
}

// Effects: mark the invoice as canceled
_invoices[id].status = Types.Status.Canceled;

// Log the invoice cancelation
emit InvoiceCanceled(id);
}

/*//////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -219,7 +270,8 @@ contract InvoiceModule is IInvoiceModule, StreamManager {
if (!success) revert Errors.NativeTokenPaymentFailed();
} else {
// Interactions: pay the recipient with the ERC-20 token
IERC20(invoice.payment.asset).safeTransfer({
IERC20(invoice.payment.asset).safeTransferFrom({
from: msg.sender,
to: address(invoice.recipient),
value: invoice.payment.amount
});
Expand All @@ -239,19 +291,24 @@ contract InvoiceModule is IInvoiceModule, StreamManager {

/// @dev Create the tranched stream payment
function _payByTranchedStream(Types.Invoice memory invoice) internal returns (uint256 streamId) {
uint40 numberOfTranches = Helpers.computeNumberOfPayments(
invoice.payment.recurrence,
invoice.endTime - invoice.startTime
);

streamId = StreamManager.createTranchedStream({
asset: IERC20(invoice.payment.asset),
totalAmount: invoice.payment.amount,
startTime: invoice.startTime,
recipient: invoice.recipient,
numberOfTranches: invoice.payment.paymentsLeft,
numberOfTranches: numberOfTranches,
recurrence: invoice.payment.recurrence
});
}

/// @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(
function _checkIntervalPayments(
Types.Recurrence recurrence,
uint40 startTime,
uint40 endTime
Expand Down
21 changes: 19 additions & 2 deletions src/modules/invoice-module/interfaces/IInvoiceModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface IInvoiceModule {
EVENTS
//////////////////////////////////////////////////////////////////////////*/

/// @notice Emitted when a regular or recurring invoice is created
/// @notice Emitted when an invoice is created
/// @param id The ID of the invoice
/// @param recipient The address receiving the payment
/// @param status The status of the invoice
Expand All @@ -26,13 +26,17 @@ interface IInvoiceModule {
Types.Payment payment
);

/// @notice Emitted when a regular or recurring invoice is paid
/// @notice Emitted when an invoice is paid
/// @param id The ID of the invoice
/// @param payer The address of the payer
/// @param status The status of the invoice
/// @param payment Struct representing the payment details associated with the invoice
event InvoicePaid(uint256 indexed id, address indexed payer, Types.Status status, Types.Payment payment);

/// @notice Emitted when an invoice is canceled
/// @param id The ID of the invoice
event InvoiceCanceled(uint256 indexed id);

/*//////////////////////////////////////////////////////////////////////////
CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -64,4 +68,17 @@ interface IInvoiceModule {
///
/// @param id The ID of the invoice to pay
function payInvoice(uint256 id) external payable;

/// @notice Cancels the `id` invoice
///
/// Notes:
/// - if the invoice has a linear or tranched stream payment method, the streaming flow will be
/// stopped and the remaining funds will be refunded to the stream payer
///
/// Important:
/// - if the invoice has a linear or tranched stream payment method, the portion that has already
/// been streamed is NOT automatically transferred
///
/// @param id The ID of the invoice
function cancelInvoice(uint256 id) external;
}
12 changes: 12 additions & 0 deletions src/modules/invoice-module/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,25 @@ library Errors {
/// @notice Thrown when a payer attempts to pay a canceled invoice
error InvoiceCanceled();

/// @notice Thrown when the invoice ID references a null invoice
error InvoiceNull();

/// @notice Thrown when `msg.sender` is not the creator (recipient) of the invoice
error InvoiceOwnerUnauthorized();

/// @notice Thrown when the payment interval (endTime - startTime) is too short for the selected recurrence
/// i.e. recurrence is set to weekly but interval is shorter than 1 week
error PaymentIntervalTooShortForSelectedRecurrence();

/// @notice Thrown when a tranched stream has a one-off recurrence type
error TranchedStreamInvalidOneOffRecurence();

/// @notice Thrown when an attempt is made to cancel an already paid invoice
error CannotCancelPaidInvoice();

/// @notice Thrown when an attempt is made to cancel an already canceled invoice
error CannotCancelCanceledInvoice();

/*//////////////////////////////////////////////////////////////////////////
STREAM-MANAGER
//////////////////////////////////////////////////////////////////////////*/
Expand Down
80 changes: 34 additions & 46 deletions src/modules/invoice-module/sablier-v2/StreamManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lock
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 { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { ud60x18, UD60x18 } from "@prb/math/src/UD60x18.sol";

import { IStreamManager } from "./interfaces/IStreamManager.sol";
Expand All @@ -16,6 +17,8 @@ import { Types } from "./../libraries/Types.sol";
/// @title StreamManager
/// @dev See the documentation in {IStreamManager}
contract StreamManager is IStreamManager {
using SafeERC20 for IERC20;

/*//////////////////////////////////////////////////////////////////////////
PUBLIC STORAGE
//////////////////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -71,7 +74,7 @@ contract StreamManager is IStreamManager {
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 });
_transferFromAndApprove({ asset: asset, amount: totalAmount, spender: address(LOCKUP_LINEAR) });

// Create the Lockup Linear stream
streamId = _createLinearStream(asset, totalAmount, startTime, endTime, recipient);
Expand All @@ -87,14 +90,14 @@ contract StreamManager is IStreamManager {
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 });
_transferFromAndApprove({ asset: asset, amount: totalAmount, spender: address(LOCKUP_TRANCHED) });

// Create the Lockup Linear stream
streamId = _createTranchedStream(asset, totalAmount, startTime, recipient, numberOfTranches, recurrence);
}

/// @inheritdoc IStreamManager
function updateBrokerFee(UD60x18 newBrokerFee) public onlyBrokerAdmin {
function updateStreamBrokerFee(UD60x18 newBrokerFee) public onlyBrokerAdmin {
// Log the broker fee update
emit BrokerFeeUpdated({ oldFee: brokerFee, newFee: newBrokerFee });

Expand All @@ -107,70 +110,41 @@ contract StreamManager is IStreamManager {
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc IStreamManager
function withdraw(ISablierV2Lockup sablier, uint256 streamId, address to, uint128 amount) external {
sablier.withdraw(streamId, to, amount);
}

/// @inheritdoc IStreamManager
function withdrawableAmountOf(
ISablierV2Lockup sablier,
uint256 streamId
) external view returns (uint128 withdrawableAmount) {
withdrawableAmount = sablier.withdrawableAmountOf(streamId);
function withdrawLinearStream(uint256 streamId, address to, uint128 amount) public {
_withdrawStream({ sablier: LOCKUP_LINEAR, streamId: streamId, to: to, amount: amount });
}

/// @inheritdoc IStreamManager
function withdrawMax(
ISablierV2Lockup sablier,
uint256 streamId,
address to
) external returns (uint128 withdrawnAmount) {
withdrawnAmount = sablier.withdrawMax(streamId, to);
}

/// @inheritdoc IStreamManager
function withdrawMultiple(
ISablierV2Lockup sablier,
uint256[] calldata streamIds,
uint128[] calldata amounts
) external {
sablier.withdrawMultiple(streamIds, amounts);
function withdrawTranchedStream(uint256 streamId, address to, uint128 amount) public {
_withdrawStream({ sablier: LOCKUP_TRANCHED, streamId: streamId, to: to, amount: amount });
}

/*//////////////////////////////////////////////////////////////////////////
CANCEL FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc IStreamManager
function cancel(ISablierV2Lockup sablier, uint256 streamId) external {
sablier.cancel(streamId);
function cancelLinearStream(uint256 streamId) public {
_cancelStream({ sablier: LOCKUP_LINEAR, streamId: streamId });
}

/// @inheritdoc IStreamManager
function cancelMultiple(ISablierV2Lockup sablier, uint256[] calldata streamIds) external {
sablier.cancelMultiple(streamIds);
function cancelTranchedStream(uint256 streamId) public {
_cancelStream({ sablier: LOCKUP_TRANCHED, streamId: streamId });
}

/*//////////////////////////////////////////////////////////////////////////
RENOUNCE FUNCTIONS
CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc IStreamManager
function renounce(ISablierV2Lockup sablier, uint256 streamId) external {
sablier.renounce(streamId);
function getLinearStream(uint256 streamId) public view returns (LockupLinear.StreamLL memory stream) {
stream = LOCKUP_LINEAR.getStream(streamId);
}

/*//////////////////////////////////////////////////////////////////////////
TRANSFER FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc IStreamManager
function withdrawMaxAndTransfer(
ISablierV2Lockup sablier,
uint256 streamId,
address newRecipient
) external returns (uint128 withdrawnAmount) {
withdrawnAmount = sablier.withdrawMaxAndTransfer(streamId, newRecipient);
function getTranchedStream(uint256 streamId) public view returns (LockupTranched.StreamLT memory stream) {
stream = LOCKUP_TRANCHED.getStream(streamId);
}

/*//////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -225,6 +199,7 @@ contract StreamManager is IStreamManager {
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.startTime = startTime; // The timestamp when to start streaming

// Calculate the duration of each tranche based on the payment recurrence
uint40 durationPerTranche = _computeDurationPerTrache(recurrence);
Expand All @@ -251,14 +226,27 @@ contract StreamManager is IStreamManager {
streamId = LOCKUP_TRANCHED.createWithTimestamps(params);
}

/// @dev Withdraws from either a linear or tranched stream
function _withdrawStream(ISablierV2Lockup sablier, uint256 streamId, address to, uint128 amount) internal {
sablier.withdraw(streamId, to, amount);
}

/// @dev Cancels the `streamId` stream
function _cancelStream(ISablierV2Lockup sablier, uint256 streamId) internal {
sablier.cancel(streamId);
}

/// @dev Transfers the `amount` of `asset` tokens to this address (or the contract inherting from)
/// and approves either the `SablierV2LockupLinear` or `SablierV2LockupTranched` to spend the amount
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);
IERC20(asset).safeTransferFrom(msg.sender, address(this), amount);

// Approve the Sablier contract to spend the ERC-20 tokens
asset.approve(spender, amount);
}

/// @dev Calculates the duration of each tranches from a tranched stream based on a recurrence
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;
Expand Down
Loading