Skip to content

Commit

Permalink
feat: implement createAndDepositMultiple function
Browse files Browse the repository at this point in the history
refactor: use specific amount names instead of a generic one
  • Loading branch information
andreivladbrg committed Apr 22, 2024
1 parent cb1de8a commit e39449b
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 73 deletions.
83 changes: 47 additions & 36 deletions src/SablierV2OpenEnded.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
}

Expand All @@ -244,15 +255,15 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope
/// @inheritdoc ISablierV2OpenEnded
function refundFromStream(
uint256 streamId,
uint128 amount
uint128 refundAmount
)
external
noDelegateCall
notCanceled(streamId)
onlySender(streamId)
{
// Checks, Effects and Interactions: make the refund.
_refundFromStream(streamId, amount);
_refundFromStream(streamId, refundAmount);
}

/// @inheritdoc ISablierV2OpenEnded
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down
58 changes: 41 additions & 17 deletions src/interfaces/ISablierV2OpenEnded.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
);

/*//////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -267,23 +291,23 @@ 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.
///
/// @dev Emits multiple {Transfer} and {DepositOpenEndedStream} events.
///
/// 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.
///
Expand All @@ -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.
///
Expand Down
8 changes: 4 additions & 4 deletions src/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
Expand All @@ -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();
Expand Down
7 changes: 6 additions & 1 deletion test/integration/deposit/deposit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit e39449b

Please sign in to comment.