From e39449b98715fd46b3b0c55497956cf7fe17e9e9 Mon Sep 17 00:00:00 2001 From: andreivladbrg Date: Mon, 22 Apr 2024 15:43:46 +0300 Subject: [PATCH] feat: implement createAndDepositMultiple function refactor: use specific amount names instead of a generic one --- src/SablierV2OpenEnded.sol | 83 +++++++++++-------- src/interfaces/ISablierV2OpenEnded.sol | 58 +++++++++---- src/libraries/Errors.sol | 8 +- test/integration/deposit/deposit.t.sol | 7 +- .../refund-from-stream/refundFromStream.t.sol | 21 +++-- .../withdraw-multiple/withdrawMultiple.t.sol | 4 +- test/integration/withdraw/withdraw.t.sol | 2 +- test/invariant/handlers/OpenEndedHandler.sol | 2 +- test/utils/Events.sol | 6 +- 9 files changed, 118 insertions(+), 73 deletions(-) diff --git a/src/SablierV2OpenEnded.sol b/src/SablierV2OpenEnded.sol index ded3eef4..f51e955f 100644 --- a/src/SablierV2OpenEnded.sol +++ b/src/SablierV2OpenEnded.sol @@ -167,62 +167,73 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope /// @inheritdoc ISablierV2OpenEnded function createMultiple( - address[] calldata senders, address[] calldata recipients, + address[] calldata senders, uint128[] calldata ratesPerSecond, IERC20 asset ) - external + public returns (uint256[] memory streamIds) { - uint256 sendersCount = senders.length; uint256 recipientsCount = recipients.length; + uint256 sendersCount = senders.length; uint256 ratesPerSecondCount = ratesPerSecond.length; // Check: count of `senders`, `recipients`, `ratesPerSecond` matches. - if (sendersCount != recipientsCount || sendersCount != ratesPerSecondCount) { + if (recipientsCount != sendersCount || recipientsCount != ratesPerSecondCount) { revert Errors.SablierV2OpenEnded_CreateArrayCountsNotEqual( sendersCount, recipientsCount, ratesPerSecondCount ); } - streamIds = new uint256[](sendersCount); - for (uint256 i = 0; i < sendersCount; ++i) { + streamIds = new uint256[](recipientsCount); + for (uint256 i = 0; i < recipientsCount; ++i) { // Checks, Effects and Interactions: create the stream. streamIds[i] = _create(senders[i], recipients[i], ratesPerSecond[i], asset); } } /// @inheritdoc ISablierV2OpenEnded - function deposit(uint256 streamId, uint128 amount) external noDelegateCall notCanceled(streamId) { + function createAndDepositMultiple( + address[] calldata recipients, + address[] calldata senders, + uint128[] calldata ratesPerSecond, + IERC20 asset, + uint128[] calldata depositAmounts + ) + external + returns (uint256[] memory streamIds) + { + streamIds = new uint256[](recipients.length); + streamIds = createMultiple(recipients, senders, ratesPerSecond, asset); + + depositMultiple(streamIds, depositAmounts); + } + + /// @inheritdoc ISablierV2OpenEnded + function deposit(uint256 streamId, uint128 depositAmount) external noDelegateCall notCanceled(streamId) { // Checks, Effects and Interactions: deposit on stream. - _deposit(streamId, amount); + _deposit(streamId, depositAmount); } /// @inheritdoc ISablierV2OpenEnded - function depositMultiple(uint256[] calldata streamIds, uint128[] calldata amounts) external noDelegateCall { + function depositMultiple(uint256[] memory streamIds, uint128[] calldata amounts) public noDelegateCall { uint256 streamIdsCount = streamIds.length; - uint256 amountsCount = amounts.length; + uint256 depositAmountsCount = amounts.length; // Check: count of `streamIds` matches count of `amounts`. - if (streamIdsCount != amountsCount) { - revert Errors.SablierV2OpenEnded_DepositArrayCountsNotEqual(streamIdsCount, amountsCount); + if (streamIdsCount != depositAmountsCount) { + revert Errors.SablierV2OpenEnded_DepositArrayCountsNotEqual(streamIdsCount, depositAmountsCount); } - uint256 streamId; - uint128 amount; for (uint256 i = 0; i < streamIdsCount; ++i) { - streamId = streamIds[i]; - // Check: the stream is not canceled. - if (isCanceled(streamId)) { - revert Errors.SablierV2OpenEnded_StreamCanceled(streamId); + if (isCanceled(streamIds[i])) { + revert Errors.SablierV2OpenEnded_StreamCanceled(streamIds[i]); } - amount = amounts[i]; - // Checks, Effects and Interactions: deposit on stream. - _deposit(streamId, amount); + _deposit(streamIds[i], amounts[i]); } } @@ -244,7 +255,7 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope /// @inheritdoc ISablierV2OpenEnded function refundFromStream( uint256 streamId, - uint128 amount + uint128 refundAmount ) external noDelegateCall @@ -252,7 +263,7 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope onlySender(streamId) { // Checks, Effects and Interactions: make the refund. - _refundFromStream(streamId, amount); + _refundFromStream(streamId, refundAmount); } /// @inheritdoc ISablierV2OpenEnded @@ -346,7 +357,7 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope /// @dev Calculates the streamed amount. function _streamedAmountOf(uint256 streamId, uint40 time) internal view returns (uint128) { - uint128 lastTimeUpdate = uint128(_streams[streamId].lastTimeUpdate); + uint40 lastTimeUpdate = _streams[streamId].lastTimeUpdate; // If the time reference is less than or equal to the `lastTimeUpdate`, return zero. if (time <= lastTimeUpdate) { @@ -526,26 +537,26 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope } /// @dev See the documentation for the user-facing functions that call this internal function. - function _deposit(uint256 streamId, uint128 amount) internal { - // Check: the amount is not zero. - if (amount == 0) { + function _deposit(uint256 streamId, uint128 depositAmount) internal { + // Check: the deposit amount is not zero. + if (depositAmount == 0) { revert Errors.SablierV2OpenEnded_DepositAmountZero(); } // Effect: update the stream balance. - _streams[streamId].balance += amount; + _streams[streamId].balance += depositAmount; // Retrieve the ERC-20 asset from storage. IERC20 asset = _streams[streamId].asset; // Calculate the transfer amount. - uint128 transferAmount = _calculateTransferAmount(streamId, amount); + uint128 transferAmount = _calculateTransferAmount(streamId, depositAmount); // Interaction: transfer the deposit amount. asset.safeTransferFrom(msg.sender, address(this), transferAmount); // Log the deposit. - emit ISablierV2OpenEnded.DepositOpenEndedStream(streamId, msg.sender, asset, amount); + emit ISablierV2OpenEnded.DepositOpenEndedStream(streamId, msg.sender, asset, depositAmount); } /// @dev Helper function to update the `balance` and to perform the ERC-20 transfer. @@ -561,25 +572,25 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope } /// @dev See the documentation for the user-facing functions that call this internal function. - function _refundFromStream(uint256 streamId, uint128 amount) internal { + function _refundFromStream(uint256 streamId, uint128 refundAmount) internal { address sender = _streams[streamId].sender; uint128 refundableAmount = _refundableAmountOf(streamId, uint40(block.timestamp)); // Check: the amount is not zero. - if (amount == 0) { + if (refundAmount == 0) { revert Errors.SablierV2OpenEnded_RefundAmountZero(); } // Check: the withdraw amount is not greater than the refundable amount. - if (amount > refundableAmount) { - revert Errors.SablierV2OpenEnded_Overrefund(streamId, amount, refundableAmount); + if (refundAmount > refundableAmount) { + revert Errors.SablierV2OpenEnded_Overrefund(streamId, refundAmount, refundableAmount); } // Effects and interactions: update the `balance` and perform the ERC-20 transfer. - _extractFromStream(streamId, sender, amount); + _extractFromStream(streamId, sender, refundAmount); // Log the refund. - emit ISablierV2OpenEnded.RefundFromOpenEndedStream(streamId, sender, _streams[streamId].asset, amount); + emit ISablierV2OpenEnded.RefundFromOpenEndedStream(streamId, sender, _streams[streamId].asset, refundAmount); } /// @dev See the documentation for the user-facing functions that call this internal function. diff --git a/src/interfaces/ISablierV2OpenEnded.sol b/src/interfaces/ISablierV2OpenEnded.sol index 95c7d531..4579078e 100644 --- a/src/interfaces/ISablierV2OpenEnded.sol +++ b/src/interfaces/ISablierV2OpenEnded.sol @@ -62,18 +62,18 @@ interface ISablierV2OpenEnded is ISablierV2OpenEndedState { /// @param streamId The ID of the open-ended stream. /// @param funder The address which funded the stream. /// @param asset The contract address of the ERC-20 asset used for streaming. - /// @param amount The amount of assets deposited, denoted in 18 decimals. + /// @param depositAmount The amount of assets deposited, denoted in 18 decimals. event DepositOpenEndedStream( - uint256 indexed streamId, address indexed funder, IERC20 indexed asset, uint128 amount + uint256 indexed streamId, address indexed funder, IERC20 indexed asset, uint128 depositAmount ); /// @notice Emitted when assets are refunded from a open-ended stream. /// @param streamId The ID of the open-ended stream. /// @param sender The address of the stream's sender. /// @param asset The contract address of the ERC-20 asset used for streaming. - /// @param amount The amount of assets deposited, denoted in 18 decimals. + /// @param refundAmount The amount of assets refunded to the sender, denoted in 18 decimals. event RefundFromOpenEndedStream( - uint256 indexed streamId, address indexed sender, IERC20 indexed asset, uint128 amount + uint256 indexed streamId, address indexed sender, IERC20 indexed asset, uint128 refundAmount ); /// @notice Emitted when a open-ended stream is re-started. @@ -89,9 +89,9 @@ interface ISablierV2OpenEnded is ISablierV2OpenEndedState { /// @param streamId The ID of the stream. /// @param to The address that has received the withdrawn assets. /// @param asset The contract address of the ERC-20 asset used for streaming. - /// @param amount The amount of assets withdrawn, denoted in 18 decimals. + /// @param withdrawAmount The amount of assets withdrawn, denoted in 18 decimals. event WithdrawFromOpenEndedStream( - uint256 indexed streamId, address indexed to, IERC20 indexed asset, uint128 amount + uint256 indexed streamId, address indexed to, IERC20 indexed asset, uint128 withdrawAmount ); /*////////////////////////////////////////////////////////////////////////// @@ -155,8 +155,7 @@ interface ISablierV2OpenEnded is ISablierV2OpenEndedState { /// - `streamId` must not reference a null stream. /// - `streamId` must not reference a canceled stream. /// - `msg.sender` must be the stream's sender. - /// - `newRatePerSecond` must be greater than zero. - /// - `newRatePerSecond` must not be equal to the actual rate per second. + /// - `newRatePerSecond` must be greater than zero and not equal to the current rate per second. /// /// @param streamId The ID of the stream to adjust. /// @param newRatePerSecond The new rate per second of the open-ended stream, denoted in 18 decimals. @@ -237,6 +236,31 @@ interface ISablierV2OpenEnded is ISablierV2OpenEndedState { external returns (uint256 streamId); + /// @notice Creates multiple open-ended streams with the `block.timestamp` as the time reference and with + /// `depositAmounts` balances. + /// + /// @dev Emits multiple {CreateOpenEndedStream}, {Transfer} and {DepositOpenEndedStream} events. + /// + /// Requirements: + /// - All requirements from {create} must be met for each stream. + /// - There must be an equal number of `recipients`, `senders`, `ratesPerSecond` and `depositAmounts`. + /// + /// @param recipients The addresses receiving the assets. + /// @param senders The addresses streaming the assets, with the ability to adjust and cancel the stream. + /// @param ratesPerSecond The amounts of assets that are increasing by every second, denoted in 18 decimals. + /// @param asset The contract address of the ERC-20 asset used for streaming. + /// @param depositAmounts The amounts deposited in the streams. + /// @return streamIds The IDs of the newly created streams. + function createAndDepositMultiple( + address[] calldata recipients, + address[] calldata senders, + uint128[] calldata ratesPerSecond, + IERC20 asset, + uint128[] calldata depositAmounts + ) + external + returns (uint256[] memory streamIds); + /// @notice Creates multiple open-ended streams with the `block.timestamp` as the time reference and with zero /// balance. /// @@ -267,11 +291,11 @@ interface ISablierV2OpenEnded is ISablierV2OpenEndedState { /// - Must not be delegate called. /// - `streamId` must not reference a null stream. /// - `streamId` must not reference a canceled stream. - /// - `amount` must be greater than zero. + /// - `depositAmount` must be greater than zero. /// /// @param streamId The ID of the stream to deposit on. - /// @param amount The amount deposited in the stream, denoted in 18 decimals. - function deposit(uint256 streamId, uint128 amount) external; + /// @param depositAmount The amount deposited in the stream, denoted in 18 decimals. + function deposit(uint256 streamId, uint128 depositAmount) external; /// @notice Deposits assets in multiple streams. /// @@ -279,11 +303,11 @@ interface ISablierV2OpenEnded is ISablierV2OpenEndedState { /// /// Requirements: /// - All requirements from {deposit} must be met for each stream. - /// - There must be an equal number of `streamIds` and `amounts`. + /// - There must be an equal number of `streamIds` and `depositAmount`. /// /// @param streamIds The ids of the streams to deposit on. - /// @param amounts The amounts of assets to be deposited, denoted in 18 decimals. - function depositMultiple(uint256[] calldata streamIds, uint128[] calldata amounts) external; + /// @param depositAmounts The amount of assets to be deposited, denoted in 18 decimals. + function depositMultiple(uint256[] calldata streamIds, uint128[] calldata depositAmounts) external; /// @notice Refunds the provided amount of assets from the stream to the sender's address. /// @@ -294,11 +318,11 @@ interface ISablierV2OpenEnded is ISablierV2OpenEndedState { /// - `streamId` must not reference a null stream. /// - `streamId` must not reference a canceled stream. /// - `msg.sender` must be the sender. - /// - `amount` must be greater than zero and must not exceed the refundable amount. + /// - `refundAmount` must be greater than zero and must not exceed the refundable amount. /// /// @param streamId The ID of the stream to refund from. - /// @param amount The amount to refund, in units of the ERC-20 asset's decimals. - function refundFromStream(uint256 streamId, uint128 amount) external; + /// @param refundAmount The amount to refund, denoted in 18 decimals. + function refundFromStream(uint256 streamId, uint128 refundAmount) external; /// @notice Restarts the stream with the provided rate per second. /// diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 4d442a66..71aa1eaa 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -20,7 +20,7 @@ library Errors { /// @notice Thrown when trying to create multiple streams and the number of senders, recipients and rates per second /// does not match. error SablierV2OpenEnded_CreateArrayCountsNotEqual( - uint256 sendersCount, uint256 recipientsCount, uint256 ratesPerSecondCount + uint256 recipientsCount, uint256 sendersCount, uint256 ratesPerSecondCount ); /// @notice Thrown when trying to set the rate per second of a stream to zero. @@ -32,9 +32,9 @@ library Errors { /// @notice Thrown when trying to create a OpenEnded stream with a zero deposit amount. error SablierV2OpenEnded_DepositAmountZero(); - /// @notice Thrown when trying to deposit on multiple streams and the number of stream ids does + /// @notice Thrown when trying to deposit on multiple streams and the number of stream IDs does /// not match the number of deposit amounts. - error SablierV2OpenEnded_DepositArrayCountsNotEqual(uint256 streamIdsCount, uint256 amountsCount); + error SablierV2OpenEnded_DepositArrayCountsNotEqual(uint256 streamIdsCount, uint256 depositAmountsCount); /// @notice Thrown when trying to create a stream with an asset with no decimals. error SablierV2OpenEnded_InvalidAssetDecimals(IERC20 asset); @@ -46,7 +46,7 @@ library Errors { error SablierV2OpenEnded_Null(uint256 streamId); /// @notice Thrown when trying to refund an amount greater than the refundable amount. - error SablierV2OpenEnded_Overrefund(uint256 streamId, uint128 amount, uint128 refundableAmount); + error SablierV2OpenEnded_Overrefund(uint256 streamId, uint128 refundAmount, uint128 refundableAmount); /// @notice Thrown when trying to create a OpenEnded stream with the recipient as the zero address. error SablierV2OpenEnded_RecipientZeroAddress(); diff --git a/test/integration/deposit/deposit.t.sol b/test/integration/deposit/deposit.t.sol index 4a9cf5a0..6578cd40 100644 --- a/test/integration/deposit/deposit.t.sol +++ b/test/integration/deposit/deposit.t.sol @@ -57,7 +57,12 @@ contract Deposit_Integration_Test is Integration_Test { }); vm.expectEmit({ emitter: address(openEnded) }); - emit DepositOpenEndedStream({ streamId: streamId, funder: users.sender, asset: asset, amount: DEPOSIT_AMOUNT }); + emit DepositOpenEndedStream({ + streamId: streamId, + funder: users.sender, + asset: asset, + depositAmount: DEPOSIT_AMOUNT + }); expectCallToTransferFrom({ asset: asset, diff --git a/test/integration/refund-from-stream/refundFromStream.t.sol b/test/integration/refund-from-stream/refundFromStream.t.sol index 3c559660..ecca343c 100644 --- a/test/integration/refund-from-stream/refundFromStream.t.sol +++ b/test/integration/refund-from-stream/refundFromStream.t.sol @@ -24,12 +24,12 @@ contract RefundFromStream_Integration_Test is Integration_Test { function test_RevertGiven_Null() external whenNotDelegateCalled { expectRevertNull(); - openEnded.refundFromStream({ streamId: nullStreamId, amount: REFUND_AMOUNT }); + openEnded.refundFromStream({ streamId: nullStreamId, refundAmount: REFUND_AMOUNT }); } function test_RevertGiven_Canceled() external whenNotDelegateCalled givenNotNull { expectRevertCanceled(); - openEnded.refundFromStream({ streamId: defaultStreamId, amount: REFUND_AMOUNT }); + openEnded.refundFromStream({ streamId: defaultStreamId, refundAmount: REFUND_AMOUNT }); } function test_RevertWhen_CallerUnauthorized_Recipient() @@ -43,7 +43,7 @@ contract RefundFromStream_Integration_Test is Integration_Test { vm.expectRevert( abi.encodeWithSelector(Errors.SablierV2OpenEnded_Unauthorized.selector, defaultStreamId, users.recipient) ); - openEnded.refundFromStream({ streamId: defaultStreamId, amount: REFUND_AMOUNT }); + openEnded.refundFromStream({ streamId: defaultStreamId, refundAmount: REFUND_AMOUNT }); } function test_RevertWhen_CallerUnauthorized_MaliciousThirdParty() @@ -57,7 +57,7 @@ contract RefundFromStream_Integration_Test is Integration_Test { vm.expectRevert( abi.encodeWithSelector(Errors.SablierV2OpenEnded_Unauthorized.selector, defaultStreamId, users.eve) ); - openEnded.refundFromStream({ streamId: defaultStreamId, amount: REFUND_AMOUNT }); + openEnded.refundFromStream({ streamId: defaultStreamId, refundAmount: REFUND_AMOUNT }); } function test_RevertWhen_RefundAmountZero() @@ -68,7 +68,7 @@ contract RefundFromStream_Integration_Test is Integration_Test { whenCallerAuthorized { vm.expectRevert(Errors.SablierV2OpenEnded_RefundAmountZero.selector); - openEnded.refundFromStream({ streamId: defaultStreamId, amount: 0 }); + openEnded.refundFromStream({ streamId: defaultStreamId, refundAmount: 0 }); } function test_RevertWhen_Overrefund() @@ -87,7 +87,7 @@ contract RefundFromStream_Integration_Test is Integration_Test { DEPOSIT_AMOUNT - ONE_MONTH_STREAMED_AMOUNT ) ); - openEnded.refundFromStream({ streamId: defaultStreamId, amount: DEPOSIT_AMOUNT }); + openEnded.refundFromStream({ streamId: defaultStreamId, refundAmount: DEPOSIT_AMOUNT }); } function test_RefundFromStream_AssetNot18Decimals() @@ -129,10 +129,15 @@ contract RefundFromStream_Integration_Test is Integration_Test { }); vm.expectEmit({ emitter: address(openEnded) }); - emit RefundFromOpenEndedStream({ streamId: streamId, sender: users.sender, asset: asset, amount: REFUND_AMOUNT }); + emit RefundFromOpenEndedStream({ + streamId: streamId, + sender: users.sender, + asset: asset, + refundAmount: REFUND_AMOUNT + }); expectCallToTransfer({ asset: asset, to: users.sender, amount: normalizeTransferAmount(streamId, REFUND_AMOUNT) }); - openEnded.refundFromStream({ streamId: streamId, amount: REFUND_AMOUNT }); + openEnded.refundFromStream({ streamId: streamId, refundAmount: REFUND_AMOUNT }); uint128 actualStreamBalance = openEnded.getBalance(streamId); uint128 expectedStreamBalance = DEPOSIT_AMOUNT - REFUND_AMOUNT; diff --git a/test/integration/withdraw-multiple/withdrawMultiple.t.sol b/test/integration/withdraw-multiple/withdrawMultiple.t.sol index 42d497d0..7459020f 100644 --- a/test/integration/withdraw-multiple/withdrawMultiple.t.sol +++ b/test/integration/withdraw-multiple/withdrawMultiple.t.sol @@ -236,14 +236,14 @@ contract WithdrawMultiple_Integration_Concrete_Test is Integration_Test { streamId: defaultStreamIds[0], to: users.recipient, asset: dai, - amount: WITHDRAW_AMOUNT + withdrawAmount: WITHDRAW_AMOUNT }); vm.expectEmit({ emitter: address(openEnded) }); emit WithdrawFromOpenEndedStream({ streamId: defaultStreamIds[1], to: users.recipient, asset: dai, - amount: WITHDRAW_AMOUNT + withdrawAmount: WITHDRAW_AMOUNT }); openEnded.withdrawMultiple({ streamIds: defaultStreamIds, times: times }); diff --git a/test/integration/withdraw/withdraw.t.sol b/test/integration/withdraw/withdraw.t.sol index dc483c8d..91a92bdf 100644 --- a/test/integration/withdraw/withdraw.t.sol +++ b/test/integration/withdraw/withdraw.t.sol @@ -236,7 +236,7 @@ contract Withdraw_Integration_Test is Integration_Test { streamId: streamId, to: users.recipient, asset: asset, - amount: WITHDRAW_AMOUNT + withdrawAmount: WITHDRAW_AMOUNT }); expectCallToTransfer({ diff --git a/test/invariant/handlers/OpenEndedHandler.sol b/test/invariant/handlers/OpenEndedHandler.sol index 822935cb..b0aed418 100644 --- a/test/invariant/handlers/OpenEndedHandler.sol +++ b/test/invariant/handlers/OpenEndedHandler.sol @@ -125,7 +125,7 @@ contract OpenEndedHandler is BaseHandler { asset.approve({ spender: address(openEnded), value: depositAmount }); // Deposit into the stream. - openEnded.deposit({ streamId: currentStreamId, amount: depositAmount }); + openEnded.deposit({ streamId: currentStreamId, depositAmount: depositAmount }); // Store the deposited amount. openEndedStore.updateStreamDepositedAmountsSum(depositAmount); diff --git a/test/utils/Events.sol b/test/utils/Events.sol index 026e1891..2d70e7a0 100644 --- a/test/utils/Events.sol +++ b/test/utils/Events.sol @@ -33,11 +33,11 @@ abstract contract Events { ); event DepositOpenEndedStream( - uint256 indexed streamId, address indexed funder, IERC20 indexed asset, uint128 amount + uint256 indexed streamId, address indexed funder, IERC20 indexed asset, uint128 depositAmount ); event RefundFromOpenEndedStream( - uint256 indexed streamId, address indexed sender, IERC20 indexed asset, uint128 amount + uint256 indexed streamId, address indexed sender, IERC20 indexed asset, uint128 refundAmount ); event RestartOpenEndedStream( @@ -45,6 +45,6 @@ abstract contract Events { ); event WithdrawFromOpenEndedStream( - uint256 indexed streamId, address indexed to, IERC20 indexed asset, uint128 amount + uint256 indexed streamId, address indexed to, IERC20 indexed asset, uint128 withdrawAmount ); }