From 9d140641bc876f0fa32b94591c37b6c28c001c00 Mon Sep 17 00:00:00 2001 From: andreivladbrg Date: Thu, 25 Jan 2024 20:14:55 +0200 Subject: [PATCH] feat: add Lockup.Stream struct feat: add mapping for cliffs in linear feat: add mapping for segments in dynamic refactor: move all common functions in SablierV2Lockup --- src/SablierV2LockupDynamic.sol | 338 ++----------------- src/SablierV2LockupLinear.sol | 317 ++--------------- src/abstracts/SablierV2Lockup.sol | 282 ++++++++++++++-- src/interfaces/ISablierV2Lockup.sol | 7 +- src/interfaces/ISablierV2LockupDynamic.sol | 4 +- src/interfaces/ISablierV2LockupLinear.sol | 3 +- src/interfaces/hooks/ISablierV2Recipient.sol | 2 +- src/interfaces/hooks/ISablierV2Sender.sol | 2 +- src/types/DataTypes.sol | 74 ++-- 9 files changed, 369 insertions(+), 660 deletions(-) diff --git a/src/SablierV2LockupDynamic.sol b/src/SablierV2LockupDynamic.sol index 710ab5caa..08514369f 100644 --- a/src/SablierV2LockupDynamic.sol +++ b/src/SablierV2LockupDynamic.sol @@ -11,11 +11,8 @@ import { UD60x18 } from "@prb/math/src/UD60x18.sol"; import { SablierV2Lockup } from "./abstracts/SablierV2Lockup.sol"; import { ISablierV2Comptroller } from "./interfaces/ISablierV2Comptroller.sol"; -import { ISablierV2Lockup } from "./interfaces/ISablierV2Lockup.sol"; import { ISablierV2LockupDynamic } from "./interfaces/ISablierV2LockupDynamic.sol"; -import { ISablierV2Recipient } from "./interfaces/hooks/ISablierV2Recipient.sol"; import { ISablierV2NFTDescriptor } from "./interfaces/ISablierV2NFTDescriptor.sol"; -import { Errors } from "./libraries/Errors.sol"; import { Helpers } from "./libraries/Helpers.sol"; import { Lockup, LockupDynamic } from "./types/DataTypes.sol"; @@ -54,8 +51,8 @@ contract SablierV2LockupDynamic is /// @inheritdoc ISablierV2LockupDynamic uint256 public immutable override MAX_SEGMENT_COUNT; - /// @dev Sablier V2 Lockup Dynamic streams mapped by unsigned integer ids. - mapping(uint256 id => LockupDynamic.Stream stream) private _streams; + /// @dev Stream segments mapped by stream id. + mapping(uint256 id => LockupDynamic.Segment[] segments) internal _segments; /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -83,27 +80,6 @@ contract SablierV2LockupDynamic is USER-FACING CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @inheritdoc ISablierV2Lockup - function getAsset(uint256 streamId) external view override notNull(streamId) returns (IERC20 asset) { - asset = _streams[streamId].asset; - } - - /// @inheritdoc ISablierV2Lockup - function getDepositedAmount(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 depositedAmount) - { - depositedAmount = _streams[streamId].amounts.deposited; - } - - /// @inheritdoc ISablierV2Lockup - function getEndTime(uint256 streamId) external view override notNull(streamId) returns (uint40 endTime) { - endTime = _streams[streamId].endTime; - } - /// @inheritdoc ISablierV2LockupDynamic function getRange(uint256 streamId) external @@ -115,17 +91,6 @@ contract SablierV2LockupDynamic is range = LockupDynamic.Range({ start: _streams[streamId].startTime, end: _streams[streamId].endTime }); } - /// @inheritdoc ISablierV2Lockup - function getRefundedAmount(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 refundedAmount) - { - refundedAmount = _streams[streamId].amounts.refunded; - } - /// @inheritdoc ISablierV2LockupDynamic function getSegments(uint256 streamId) external @@ -134,23 +99,7 @@ contract SablierV2LockupDynamic is notNull(streamId) returns (LockupDynamic.Segment[] memory segments) { - segments = _streams[streamId].segments; - } - - /// @inheritdoc ISablierV2Lockup - function getSender(uint256 streamId) - public - view - override(ISablierV2Lockup, SablierV2Lockup) - notNull(streamId) - returns (address sender) - { - sender = _streams[streamId].sender; - } - - /// @inheritdoc ISablierV2Lockup - function getStartTime(uint256 streamId) external view override notNull(streamId) returns (uint40 startTime) { - startTime = _streams[streamId].startTime; + segments = _segments[streamId]; } /// @inheritdoc ISablierV2LockupDynamic @@ -161,101 +110,36 @@ contract SablierV2LockupDynamic is notNull(streamId) returns (LockupDynamic.Stream memory stream) { - stream = _streams[streamId]; + Lockup.Stream memory lockupStream = _streams[streamId]; // Settled streams cannot be canceled. if (_statusOf(streamId) == Lockup.Status.SETTLED) { - stream.isCancelable = false; + lockupStream.isCancelable = false; } - } - /// @inheritdoc ISablierV2Lockup - function getWithdrawnAmount(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 withdrawnAmount) - { - withdrawnAmount = _streams[streamId].amounts.withdrawn; - } - - /// @inheritdoc ISablierV2Lockup - function isCancelable(uint256 streamId) external view override notNull(streamId) returns (bool result) { - if (_statusOf(streamId) != Lockup.Status.SETTLED) { - result = _streams[streamId].isCancelable; - } - } - - /// @inheritdoc SablierV2Lockup - function isTransferable(uint256 streamId) - public - view - override(ISablierV2Lockup, SablierV2Lockup) - notNull(streamId) - returns (bool result) - { - result = _streams[streamId].isTransferable; - } - - /// @inheritdoc ISablierV2Lockup - function isDepleted(uint256 streamId) - public - view - override(ISablierV2Lockup, SablierV2Lockup) - notNull(streamId) - returns (bool result) - { - result = _streams[streamId].isDepleted; - } - - /// @inheritdoc ISablierV2Lockup - function isStream(uint256 streamId) public view override(ISablierV2Lockup, SablierV2Lockup) returns (bool result) { - result = _streams[streamId].isStream; - } - - /// @inheritdoc ISablierV2Lockup - function refundableAmountOf(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 refundableAmount) - { - // These checks are needed because {_calculateStreamedAmount} does not look up the stream's status. Note that - // checking for `isCancelable` also checks if the stream `wasCanceled` thanks to the protocol invariant that - // canceled streams are not cancelable anymore. - if (_streams[streamId].isCancelable && !_streams[streamId].isDepleted) { - refundableAmount = _streams[streamId].amounts.deposited - _calculateStreamedAmount(streamId); - } - // Otherwise, the result is implicitly zero. - } - - /// @inheritdoc ISablierV2Lockup - function statusOf(uint256 streamId) external view override notNull(streamId) returns (Lockup.Status status) { - status = _statusOf(streamId); + stream = LockupDynamic.Stream({ + amounts: lockupStream.amounts, + asset: lockupStream.asset, + endTime: lockupStream.endTime, + isCancelable: lockupStream.isCancelable, + isTransferable: lockupStream.isTransferable, + isDepleted: lockupStream.isDepleted, + isStream: lockupStream.isStream, + sender: lockupStream.sender, + segments: _segments[streamId], + startTime: lockupStream.startTime, + wasCanceled: lockupStream.wasCanceled + }); } /// @inheritdoc ISablierV2LockupDynamic function streamedAmountOf(uint256 streamId) public view - override(ISablierV2Lockup, ISablierV2LockupDynamic) - notNull(streamId) - returns (uint128 streamedAmount) - { - streamedAmount = _streamedAmountOf(streamId); - } - - /// @inheritdoc ISablierV2Lockup - function wasCanceled(uint256 streamId) - public - view - override(ISablierV2Lockup, SablierV2Lockup) - notNull(streamId) - returns (bool result) + override(SablierV2Lockup, ISablierV2LockupDynamic) + returns (uint128) { - result = _streams[streamId].wasCanceled; + return super.streamedAmountOf(streamId); } /*////////////////////////////////////////////////////////////////////////// @@ -304,7 +188,7 @@ contract SablierV2LockupDynamic is //////////////////////////////////////////////////////////////////////////*/ /// @dev Calculates the streamed amount without looking up the stream's status. - function _calculateStreamedAmount(uint256 streamId) internal view returns (uint128) { + function _calculateStreamedAmount(uint256 streamId) internal view override returns (uint128) { // If the start time is in the future, return zero. uint40 currentTime = uint40(block.timestamp); if (_streams[streamId].startTime >= currentTime) { @@ -317,7 +201,7 @@ contract SablierV2LockupDynamic is return _streams[streamId].amounts.deposited; } - if (_streams[streamId].segments.length > 1) { + if (_segments[streamId].length > 1) { // If there is more than one segment, it may be necessary to iterate over all of them. return _calculateStreamedAmountForMultipleSegments(streamId); } else { @@ -326,39 +210,32 @@ contract SablierV2LockupDynamic is } } - /// @dev Calculates the streamed amount for a stream with multiple segments. - /// - /// Notes: - /// - /// 1. Normalization to 18 decimals is not needed because there is no mix of amounts with different decimals. - /// 2. The stream's start time must be in the past so that the calculations below do not overflow. - /// 3. The stream's end time must be in the future so that the loop below does not panic with an "index out of - /// bounds" error. function _calculateStreamedAmountForMultipleSegments(uint256 streamId) internal view returns (uint128) { unchecked { uint40 currentTime = uint40(block.timestamp); - LockupDynamic.Stream memory stream = _streams[streamId]; + Lockup.Stream memory stream = _streams[streamId]; + LockupDynamic.Segment[] memory segments = _segments[streamId]; // Sum the amounts in all segments that precede the current time. uint128 previousSegmentAmounts; - uint40 currentSegmentTimestamp = stream.segments[0].timestamp; + uint40 currentSegmentTimestamp = segments[0].timestamp; uint256 index = 0; while (currentSegmentTimestamp < currentTime) { - previousSegmentAmounts += stream.segments[index].amount; + previousSegmentAmounts += segments[index].amount; index += 1; - currentSegmentTimestamp = stream.segments[index].timestamp; + currentSegmentTimestamp = segments[index].timestamp; } // After exiting the loop, the current segment is at `index`. - SD59x18 currentSegmentAmount = stream.segments[index].amount.intoSD59x18(); - SD59x18 currentSegmentExponent = stream.segments[index].exponent.intoSD59x18(); - currentSegmentTimestamp = stream.segments[index].timestamp; + SD59x18 currentSegmentAmount = segments[index].amount.intoSD59x18(); + SD59x18 currentSegmentExponent = segments[index].exponent.intoSD59x18(); + currentSegmentTimestamp = segments[index].timestamp; uint40 previousTimestamp; if (index > 0) { // When the current segment's index is greater than or equal to 1, it implies that the segment is not // the first. In this case, use the previous segment's timestamp. - previousTimestamp = stream.segments[index - 1].timestamp; + previousTimestamp = segments[index - 1].timestamp; } else { // Otherwise, the current segment is the first, so use the start time as the previous timestamp. previousTimestamp = stream.startTime; @@ -403,7 +280,7 @@ contract SablierV2LockupDynamic is SD59x18 elapsedTimePercentage = elapsedTime.div(totalTime); // Cast the stream parameters to SD59x18. - SD59x18 exponent = _streams[streamId].segments[0].exponent.intoSD59x18(); + SD59x18 exponent = _segments[streamId][0].exponent.intoSD59x18(); SD59x18 depositedAmount = _streams[streamId].amounts.deposited.intoSD59x18(); // Calculate the streamed amount using the special formula. @@ -422,116 +299,10 @@ contract SablierV2LockupDynamic is } } - /// @inheritdoc SablierV2Lockup - function _isCallerStreamSender(uint256 streamId) internal view override returns (bool) { - return msg.sender == _streams[streamId].sender; - } - - /// @inheritdoc SablierV2Lockup - function _statusOf(uint256 streamId) internal view override returns (Lockup.Status) { - if (_streams[streamId].isDepleted) { - return Lockup.Status.DEPLETED; - } else if (_streams[streamId].wasCanceled) { - return Lockup.Status.CANCELED; - } - - if (block.timestamp < _streams[streamId].startTime) { - return Lockup.Status.PENDING; - } - - if (_calculateStreamedAmount(streamId) < _streams[streamId].amounts.deposited) { - return Lockup.Status.STREAMING; - } else { - return Lockup.Status.SETTLED; - } - } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _streamedAmountOf(uint256 streamId) internal view returns (uint128) { - Lockup.Amounts memory amounts = _streams[streamId].amounts; - - if (_streams[streamId].isDepleted) { - return amounts.withdrawn; - } else if (_streams[streamId].wasCanceled) { - return amounts.deposited - amounts.refunded; - } - - return _calculateStreamedAmount(streamId); - } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _withdrawableAmountOf(uint256 streamId) internal view override returns (uint128) { - return _streamedAmountOf(streamId) - _streams[streamId].amounts.withdrawn; - } - /*////////////////////////////////////////////////////////////////////////// INTERNAL NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @dev See the documentation for the user-facing functions that call this internal function. - function _cancel(uint256 streamId) internal override { - // Calculate the streamed amount. - uint128 streamedAmount = _calculateStreamedAmount(streamId); - - // Retrieve the amounts from storage. - Lockup.Amounts memory amounts = _streams[streamId].amounts; - - // Checks: the stream is not settled. - if (streamedAmount >= amounts.deposited) { - revert Errors.SablierV2Lockup_StreamSettled(streamId); - } - - // Checks: the stream is cancelable. - if (!_streams[streamId].isCancelable) { - revert Errors.SablierV2Lockup_StreamNotCancelable(streamId); - } - - // Calculate the sender's and the recipient's amount. - uint128 senderAmount = amounts.deposited - streamedAmount; - uint128 recipientAmount = streamedAmount - amounts.withdrawn; - - // Effects: mark the stream as canceled. - _streams[streamId].wasCanceled = true; - - // Effects: make the stream not cancelable anymore, because a stream can only be canceled once. - _streams[streamId].isCancelable = false; - - // Effects: If there are no assets left for the recipient to withdraw, mark the stream as depleted. - if (recipientAmount == 0) { - _streams[streamId].isDepleted = true; - } - - // Effects: set the refunded amount. - _streams[streamId].amounts.refunded = senderAmount; - - // Retrieve the sender and the recipient from storage. - address sender = _streams[streamId].sender; - address recipient = _ownerOf(streamId); - - // Retrieve the ERC-20 asset from storage. - IERC20 asset = _streams[streamId].asset; - - // Interactions: refund the sender. - asset.safeTransfer({ to: sender, value: senderAmount }); - - // Log the cancellation. - emit ISablierV2Lockup.CancelLockupStream(streamId, sender, recipient, asset, senderAmount, recipientAmount); - - // Emits an ERC-4906 event to trigger an update of the NFT metadata. - emit MetadataUpdate({ _tokenId: streamId }); - - // Interactions: if the recipient is a contract, try to invoke the cancel hook on the recipient without - // reverting if the hook is not implemented, and without bubbling up any potential revert. - if (recipient.code.length > 0) { - try ISablierV2Recipient(recipient).onLockupStreamCanceled({ - streamId: streamId, - sender: sender, - senderAmount: senderAmount, - recipientAmount: recipientAmount - }) { } catch { } - } - } - /// @dev See the documentation for the user-facing functions that call this internal function. function _createWithTimestamps(LockupDynamic.CreateWithTimestamps memory params) internal @@ -552,7 +323,7 @@ contract SablierV2LockupDynamic is streamId = nextStreamId; // Effects: create the stream. - LockupDynamic.Stream storage stream = _streams[streamId]; + Lockup.Stream storage stream = _streams[streamId]; stream.amounts.deposited = createAmounts.deposit; stream.asset = params.asset; stream.isCancelable = params.cancelable; @@ -569,7 +340,7 @@ contract SablierV2LockupDynamic is // Effects: store the segments. Since Solidity lacks a syntax for copying arrays directly from // memory to storage, a manual approach is necessary. See https://github.com/ethereum/solidity/issues/12783. for (uint256 i = 0; i < segmentCount; ++i) { - stream.segments.push(params.segments[i]); + _segments[streamId].push(params.segments[i]); } // Effects: bump the next stream id and record the protocol fee. @@ -611,43 +382,4 @@ contract SablierV2LockupDynamic is broker: params.broker.account }); } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _renounce(uint256 streamId) internal override { - // Checks: the stream is cancelable. - if (!_streams[streamId].isCancelable) { - revert Errors.SablierV2Lockup_StreamNotCancelable(streamId); - } - - // Effects: renounce the stream by making it not cancelable. - _streams[streamId].isCancelable = false; - } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _withdraw(uint256 streamId, address to, uint128 amount) internal override { - // Effects: update the withdrawn amount. - _streams[streamId].amounts.withdrawn = _streams[streamId].amounts.withdrawn + amount; - - // Retrieve the amounts from storage. - Lockup.Amounts memory amounts = _streams[streamId].amounts; - - // Using ">=" instead of "==" for additional safety reasons. In the event of an unforeseen increase in the - // withdrawn amount, the stream will still be marked as depleted. - if (amounts.withdrawn >= amounts.deposited - amounts.refunded) { - // Effects: mark the stream as depleted. - _streams[streamId].isDepleted = true; - - // Effects: make the stream not cancelable anymore, because a depleted stream cannot be canceled. - _streams[streamId].isCancelable = false; - } - - // Retrieve the ERC-20 asset from storage. - IERC20 asset = _streams[streamId].asset; - - // Interactions: perform the ERC-20 transfer. - asset.safeTransfer({ to: to, value: amount }); - - // Log the withdrawal. - emit ISablierV2Lockup.WithdrawFromLockupStream(streamId, to, asset, amount); - } } diff --git a/src/SablierV2LockupLinear.sol b/src/SablierV2LockupLinear.sol index d359ba790..53fb7b8a0 100644 --- a/src/SablierV2LockupLinear.sol +++ b/src/SablierV2LockupLinear.sol @@ -6,13 +6,11 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import { UD60x18, ud } from "@prb/math/src/UD60x18.sol"; +import { SablierV2Lockup } from "./abstracts/SablierV2Lockup.sol"; import { SablierV2Lockup } from "./abstracts/SablierV2Lockup.sol"; import { ISablierV2Comptroller } from "./interfaces/ISablierV2Comptroller.sol"; -import { ISablierV2Lockup } from "./interfaces/ISablierV2Lockup.sol"; import { ISablierV2LockupLinear } from "./interfaces/ISablierV2LockupLinear.sol"; import { ISablierV2NFTDescriptor } from "./interfaces/ISablierV2NFTDescriptor.sol"; -import { ISablierV2Recipient } from "./interfaces/hooks/ISablierV2Recipient.sol"; -import { Errors } from "./libraries/Errors.sol"; import { Helpers } from "./libraries/Helpers.sol"; import { Lockup, LockupLinear } from "./types/DataTypes.sol"; @@ -46,8 +44,8 @@ contract SablierV2LockupLinear is STATE VARIABLES //////////////////////////////////////////////////////////////////////////*/ - /// @dev Sablier V2 Lockup Linear streams mapped by unsigned integers. - mapping(uint256 id => LockupLinear.Stream stream) private _streams; + /// @dev Cliff times mapped by stream id. + mapping(uint256 id => uint40 cliff) internal _cliffs; /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -72,30 +70,9 @@ contract SablierV2LockupLinear is USER-FACING CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @inheritdoc ISablierV2Lockup - function getAsset(uint256 streamId) external view override notNull(streamId) returns (IERC20 asset) { - asset = _streams[streamId].asset; - } - /// @inheritdoc ISablierV2LockupLinear function getCliffTime(uint256 streamId) external view override notNull(streamId) returns (uint40 cliffTime) { - cliffTime = _streams[streamId].cliffTime; - } - - /// @inheritdoc ISablierV2Lockup - function getDepositedAmount(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 depositedAmount) - { - depositedAmount = _streams[streamId].amounts.deposited; - } - - /// @inheritdoc ISablierV2Lockup - function getEndTime(uint256 streamId) external view override notNull(streamId) returns (uint40 endTime) { - endTime = _streams[streamId].endTime; + cliffTime = _cliffs[streamId]; } /// @inheritdoc ISablierV2LockupLinear @@ -108,38 +85,11 @@ contract SablierV2LockupLinear is { range = LockupLinear.Range({ start: _streams[streamId].startTime, - cliff: _streams[streamId].cliffTime, + cliff: _cliffs[streamId], end: _streams[streamId].endTime }); } - /// @inheritdoc ISablierV2Lockup - function getRefundedAmount(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 refundedAmount) - { - refundedAmount = _streams[streamId].amounts.refunded; - } - - /// @inheritdoc ISablierV2Lockup - function getSender(uint256 streamId) - public - view - override(ISablierV2Lockup, SablierV2Lockup) - notNull(streamId) - returns (address sender) - { - sender = _streams[streamId].sender; - } - - /// @inheritdoc ISablierV2Lockup - function getStartTime(uint256 streamId) external view override notNull(streamId) returns (uint40 startTime) { - startTime = _streams[streamId].startTime; - } - /// @inheritdoc ISablierV2LockupLinear function getStream(uint256 streamId) external @@ -148,101 +98,35 @@ contract SablierV2LockupLinear is notNull(streamId) returns (LockupLinear.Stream memory stream) { - stream = _streams[streamId]; + Lockup.Stream memory lockupStream = _streams[streamId]; // Settled streams cannot be canceled. if (_statusOf(streamId) == Lockup.Status.SETTLED) { - stream.isCancelable = false; - } - } - - /// @inheritdoc ISablierV2Lockup - function getWithdrawnAmount(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 withdrawnAmount) - { - withdrawnAmount = _streams[streamId].amounts.withdrawn; - } - - /// @inheritdoc ISablierV2Lockup - function isCancelable(uint256 streamId) external view override notNull(streamId) returns (bool result) { - if (_statusOf(streamId) != Lockup.Status.SETTLED) { - result = _streams[streamId].isCancelable; + lockupStream.isCancelable = false; } - } - - /// @inheritdoc SablierV2Lockup - function isTransferable(uint256 streamId) - public - view - override(ISablierV2Lockup, SablierV2Lockup) - notNull(streamId) - returns (bool result) - { - result = _streams[streamId].isTransferable; - } - /// @inheritdoc ISablierV2Lockup - function isDepleted(uint256 streamId) - public - view - override(ISablierV2Lockup, SablierV2Lockup) - notNull(streamId) - returns (bool result) - { - result = _streams[streamId].isDepleted; - } - - /// @inheritdoc ISablierV2Lockup - function isStream(uint256 streamId) public view override(ISablierV2Lockup, SablierV2Lockup) returns (bool result) { - result = _streams[streamId].isStream; - } - - /// @inheritdoc ISablierV2Lockup - function refundableAmountOf(uint256 streamId) - external - view - override - notNull(streamId) - returns (uint128 refundableAmount) - { - // These checks are needed because {_calculateStreamedAmount} does not look up the stream's status. Note that - // checking for `isCancelable` also checks if the stream `wasCanceled` thanks to the protocol invariant that - // canceled streams are not cancelable anymore. - if (_streams[streamId].isCancelable && !_streams[streamId].isDepleted) { - refundableAmount = _streams[streamId].amounts.deposited - _calculateStreamedAmount(streamId); - } - // Otherwise, the result is implicitly zero. - } - - /// @inheritdoc ISablierV2Lockup - function statusOf(uint256 streamId) external view override notNull(streamId) returns (Lockup.Status status) { - status = _statusOf(streamId); + stream = LockupLinear.Stream({ + amounts: lockupStream.amounts, + asset: lockupStream.asset, + cliffTime: _cliffs[streamId], + endTime: lockupStream.endTime, + isCancelable: lockupStream.isCancelable, + isTransferable: lockupStream.isTransferable, + isDepleted: lockupStream.isDepleted, + isStream: lockupStream.isStream, + sender: lockupStream.sender, + startTime: lockupStream.startTime, + wasCanceled: lockupStream.wasCanceled + }); } - /// @inheritdoc ISablierV2LockupLinear function streamedAmountOf(uint256 streamId) public view - override(ISablierV2Lockup, ISablierV2LockupLinear) - notNull(streamId) - returns (uint128 streamedAmount) - { - streamedAmount = _streamedAmountOf(streamId); - } - - /// @inheritdoc ISablierV2Lockup - function wasCanceled(uint256 streamId) - public - view - override(ISablierV2Lockup, SablierV2Lockup) - notNull(streamId) - returns (bool result) + override(SablierV2Lockup, ISablierV2LockupLinear) + returns (uint128) { - result = _streams[streamId].wasCanceled; + return super.streamedAmountOf(streamId); } /*////////////////////////////////////////////////////////////////////////// @@ -298,9 +182,9 @@ contract SablierV2LockupLinear is //////////////////////////////////////////////////////////////////////////*/ /// @dev Calculates the streamed amount without looking up the stream's status. - function _calculateStreamedAmount(uint256 streamId) internal view returns (uint128) { + function _calculateStreamedAmount(uint256 streamId) internal view override returns (uint128) { // If the cliff time is in the future, return zero. - uint256 cliffTime = uint256(_streams[streamId].cliffTime); + uint256 cliffTime = uint256(_cliffs[streamId]); uint256 currentTime = block.timestamp; if (cliffTime > currentTime) { return 0; @@ -341,116 +225,10 @@ contract SablierV2LockupLinear is } } - /// @inheritdoc SablierV2Lockup - function _isCallerStreamSender(uint256 streamId) internal view override returns (bool) { - return msg.sender == _streams[streamId].sender; - } - - /// @inheritdoc SablierV2Lockup - function _statusOf(uint256 streamId) internal view override returns (Lockup.Status) { - if (_streams[streamId].isDepleted) { - return Lockup.Status.DEPLETED; - } else if (_streams[streamId].wasCanceled) { - return Lockup.Status.CANCELED; - } - - if (block.timestamp < _streams[streamId].startTime) { - return Lockup.Status.PENDING; - } - - if (_calculateStreamedAmount(streamId) < _streams[streamId].amounts.deposited) { - return Lockup.Status.STREAMING; - } else { - return Lockup.Status.SETTLED; - } - } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _streamedAmountOf(uint256 streamId) internal view returns (uint128) { - Lockup.Amounts memory amounts = _streams[streamId].amounts; - - if (_streams[streamId].isDepleted) { - return amounts.withdrawn; - } else if (_streams[streamId].wasCanceled) { - return amounts.deposited - amounts.refunded; - } - - return _calculateStreamedAmount(streamId); - } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _withdrawableAmountOf(uint256 streamId) internal view override returns (uint128) { - return _streamedAmountOf(streamId) - _streams[streamId].amounts.withdrawn; - } - /*////////////////////////////////////////////////////////////////////////// INTERNAL NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @dev See the documentation for the user-facing functions that call this internal function. - function _cancel(uint256 streamId) internal override { - // Calculate the streamed amount. - uint128 streamedAmount = _calculateStreamedAmount(streamId); - - // Retrieve the amounts from storage. - Lockup.Amounts memory amounts = _streams[streamId].amounts; - - // Checks: the stream is not settled. - if (streamedAmount >= amounts.deposited) { - revert Errors.SablierV2Lockup_StreamSettled(streamId); - } - - // Checks: the stream is cancelable. - if (!_streams[streamId].isCancelable) { - revert Errors.SablierV2Lockup_StreamNotCancelable(streamId); - } - - // Calculate the sender's and the recipient's amount. - uint128 senderAmount = amounts.deposited - streamedAmount; - uint128 recipientAmount = streamedAmount - amounts.withdrawn; - - // Effects: mark the stream as canceled. - _streams[streamId].wasCanceled = true; - - // Effects: make the stream not cancelable anymore, because a stream can only be canceled once. - _streams[streamId].isCancelable = false; - - // Effects: If there are no assets left for the recipient to withdraw, mark the stream as depleted. - if (recipientAmount == 0) { - _streams[streamId].isDepleted = true; - } - - // Effects: set the refunded amount. - _streams[streamId].amounts.refunded = senderAmount; - - // Retrieve the sender and the recipient from storage. - address sender = _streams[streamId].sender; - address recipient = _ownerOf(streamId); - - // Retrieve the ERC-20 asset from storage. - IERC20 asset = _streams[streamId].asset; - - // Interactions: refund the sender. - asset.safeTransfer({ to: sender, value: senderAmount }); - - // Log the cancellation. - emit ISablierV2Lockup.CancelLockupStream(streamId, sender, recipient, asset, senderAmount, recipientAmount); - - // Emits an ERC-4906 event to trigger an update of the NFT metadata. - emit MetadataUpdate({ _tokenId: streamId }); - - // Interactions: if the recipient is a contract, try to invoke the cancel hook on the recipient without - // reverting if the hook is not implemented, and without bubbling up any potential revert. - if (recipient.code.length > 0) { - try ISablierV2Recipient(recipient).onLockupStreamCanceled({ - streamId: streamId, - sender: sender, - senderAmount: senderAmount, - recipientAmount: recipientAmount - }) { } catch { } - } - } - /// @dev See the documentation for the user-facing functions that call this internal function. function _createWithTimestamps(LockupLinear.CreateWithTimestamps memory params) internal @@ -471,10 +249,9 @@ contract SablierV2LockupLinear is streamId = nextStreamId; // Effects: create the stream. - _streams[streamId] = LockupLinear.Stream({ + _streams[streamId] = Lockup.Stream({ amounts: Lockup.Amounts({ deposited: createAmounts.deposit, refunded: 0, withdrawn: 0 }), asset: params.asset, - cliffTime: params.range.cliff, endTime: params.range.end, isCancelable: params.cancelable, isTransferable: params.transferable, @@ -485,6 +262,9 @@ contract SablierV2LockupLinear is wasCanceled: false }); + // Effects: set the cliff time. + _cliffs[streamId] = params.range.cliff; + // Effects: bump the next stream id and record the protocol fee. // Using unchecked arithmetic because these calculations cannot realistically overflow, ever. unchecked { @@ -524,43 +304,4 @@ contract SablierV2LockupLinear is broker: params.broker.account }); } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _renounce(uint256 streamId) internal override { - // Checks: the stream is cancelable. - if (!_streams[streamId].isCancelable) { - revert Errors.SablierV2Lockup_StreamNotCancelable(streamId); - } - - // Effects: renounce the stream by making it not cancelable. - _streams[streamId].isCancelable = false; - } - - /// @dev See the documentation for the user-facing functions that call this internal function. - function _withdraw(uint256 streamId, address to, uint128 amount) internal override { - // Effects: update the withdrawn amount. - _streams[streamId].amounts.withdrawn = _streams[streamId].amounts.withdrawn + amount; - - // Retrieve the amounts from storage. - Lockup.Amounts memory amounts = _streams[streamId].amounts; - - // Using ">=" instead of "==" for additional safety reasons. In the event of an unforeseen increase in the - // withdrawn amount, the stream will still be marked as depleted. - if (amounts.withdrawn >= amounts.deposited - amounts.refunded) { - // Effects: mark the stream as depleted. - _streams[streamId].isDepleted = true; - - // Effects: make the stream not cancelable anymore, because a depleted stream cannot be canceled. - _streams[streamId].isCancelable = false; - } - - // Retrieve the ERC-20 asset from storage. - IERC20 asset = _streams[streamId].asset; - - // Interactions: perform the ERC-20 transfer. - asset.safeTransfer({ to: to, value: amount }); - - // Log the withdrawal. - emit ISablierV2Lockup.WithdrawFromLockupStream(streamId, to, asset, amount); - } } diff --git a/src/abstracts/SablierV2Lockup.sol b/src/abstracts/SablierV2Lockup.sol index 2f6a30ea8..d4cddf42a 100644 --- a/src/abstracts/SablierV2Lockup.sol +++ b/src/abstracts/SablierV2Lockup.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity >=0.8.22; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; @@ -22,6 +24,8 @@ abstract contract SablierV2Lockup is ISablierV2Lockup, // 4 inherited components ERC721 // 6 inherited components { + using SafeERC20 for IERC20; + /*////////////////////////////////////////////////////////////////////////// STATE VARIABLES //////////////////////////////////////////////////////////////////////////*/ @@ -29,8 +33,11 @@ abstract contract SablierV2Lockup is /// @inheritdoc ISablierV2Lockup uint256 public override nextStreamId; - /// @inheritdoc ISablierV2Lockup - ISablierV2NFTDescriptor public override nftDescriptor; + /// @dev Contract that generates the non-fungible token URI. + ISablierV2NFTDescriptor public nftDescriptor; + + /// @dev Sablier V2 Lockup streams mapped by unsigned integers. + mapping(uint256 id => Lockup.Stream stream) internal _streams; /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -71,6 +78,16 @@ abstract contract SablierV2Lockup is USER-FACING CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc ISablierV2Lockup + function getAsset(uint256 streamId) external view override notNull(streamId) returns (IERC20 asset) { + asset = _streams[streamId].asset; + } + + /// @inheritdoc ISablierV2Lockup + function getEndTime(uint256 streamId) external view override notNull(streamId) returns (uint40 endTime) { + endTime = _streams[streamId].endTime; + } + /// @inheritdoc ISablierV2Lockup function getRecipient(uint256 streamId) external view override returns (address recipient) { // Checks: the stream NFT exists. @@ -80,21 +97,12 @@ abstract contract SablierV2Lockup is recipient = _ownerOf(streamId); } - /// @inheritdoc ISablierV2Lockup - function getSender(uint256 streamId) public view virtual override returns (address sender); - /// @inheritdoc ISablierV2Lockup function isCold(uint256 streamId) external view override notNull(streamId) returns (bool result) { Lockup.Status status = _statusOf(streamId); result = status == Lockup.Status.SETTLED || status == Lockup.Status.CANCELED || status == Lockup.Status.DEPLETED; } - /// @inheritdoc ISablierV2Lockup - function isDepleted(uint256 streamId) public view virtual override returns (bool result); - - /// @inheritdoc ISablierV2Lockup - function isStream(uint256 streamId) public view virtual override returns (bool result); - /// @inheritdoc ISablierV2Lockup function isWarm(uint256 streamId) external view override notNull(streamId) returns (bool result) { Lockup.Status status = _statusOf(streamId); @@ -109,9 +117,110 @@ abstract contract SablierV2Lockup is // Generate the URI describing the stream NFT. uri = nftDescriptor.tokenURI({ sablier: this, streamId: streamId }); } + /// @inheritdoc ISablierV2Lockup + + function getDepositedAmount(uint256 streamId) + external + view + override + notNull(streamId) + returns (uint128 depositedAmount) + { + depositedAmount = _streams[streamId].amounts.deposited; + } /// @inheritdoc ISablierV2Lockup - function wasCanceled(uint256 streamId) public view virtual override returns (bool result); + function getSender(uint256 streamId) public view override notNull(streamId) returns (address sender) { + sender = _streams[streamId].sender; + } + + /// @inheritdoc ISablierV2Lockup + function getStartTime(uint256 streamId) external view override notNull(streamId) returns (uint40 startTime) { + startTime = _streams[streamId].startTime; + } + + /// @inheritdoc ISablierV2Lockup + function refundableAmountOf(uint256 streamId) + external + view + override + notNull(streamId) + returns (uint128 refundableAmount) + { + // These checks are needed because {_calculateStreamedAmount} does not look up the stream's status. Note that + // checking for `isCancelable` also checks if the stream `wasCanceled` thanks to the protocol invariant that + // canceled streams are not cancelable anymore. + if (_streams[streamId].isCancelable && !_streams[streamId].isDepleted) { + refundableAmount = _streams[streamId].amounts.deposited - _calculateStreamedAmount(streamId); + } + // Otherwise, the result is implicitly zero. + } + + /// @inheritdoc ISablierV2Lockup + function getRefundedAmount(uint256 streamId) + external + view + override + notNull(streamId) + returns (uint128 refundedAmount) + { + refundedAmount = _streams[streamId].amounts.refunded; + } + + /// @inheritdoc ISablierV2Lockup + function getWithdrawnAmount(uint256 streamId) + external + view + override + notNull(streamId) + returns (uint128 withdrawnAmount) + { + withdrawnAmount = _streams[streamId].amounts.withdrawn; + } + + /// @inheritdoc ISablierV2Lockup + function isCancelable(uint256 streamId) external view override notNull(streamId) returns (bool result) { + if (_statusOf(streamId) != Lockup.Status.SETTLED) { + result = _streams[streamId].isCancelable; + } + } + + /// @inheritdoc ISablierV2Lockup + function isTransferable(uint256 streamId) public view override notNull(streamId) returns (bool result) { + result = _streams[streamId].isTransferable; + } + + /// @inheritdoc ISablierV2Lockup + function isDepleted(uint256 streamId) public view override notNull(streamId) returns (bool result) { + result = _streams[streamId].isDepleted; + } + + /// @inheritdoc ISablierV2Lockup + function isStream(uint256 streamId) public view override returns (bool result) { + result = _streams[streamId].isStream; + } + + /// @inheritdoc ISablierV2Lockup + function statusOf(uint256 streamId) external view override notNull(streamId) returns (Lockup.Status status) { + status = _statusOf(streamId); + } + + /// @inheritdoc ISablierV2Lockup + function streamedAmountOf(uint256 streamId) + public + view + virtual + override + notNull(streamId) + returns (uint128 streamedAmount) + { + streamedAmount = _streamedAmountOf(streamId); + } + + /// @inheritdoc ISablierV2Lockup + function wasCanceled(uint256 streamId) public view override notNull(streamId) returns (bool result) { + result = _streams[streamId].wasCanceled; + } /// @inheritdoc ISablierV2Lockup function withdrawableAmountOf(uint256 streamId) @@ -124,9 +233,6 @@ abstract contract SablierV2Lockup is withdrawableAmount = _withdrawableAmountOf(streamId); } - /// @inheritdoc ISablierV2Lockup - function isTransferable(uint256 streamId) public view virtual returns (bool); - /*////////////////////////////////////////////////////////////////////////// USER-FACING NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ @@ -388,6 +494,10 @@ abstract contract SablierV2Lockup is } } + /// @notice Calculates the streamed amount of the stream, which is implemented by child contracts, it can vary + /// depending on the model. + function _calculateStreamedAmount(uint256 streamId) internal view virtual returns (uint128); + /// @notice Checks whether `msg.sender` is the stream's recipient or an approved third party. /// @param streamId The stream id for the query. function _isCallerStreamRecipientOrApproved(uint256 streamId) internal view returns (bool) { @@ -396,26 +506,150 @@ abstract contract SablierV2Lockup is || getApproved(streamId) == msg.sender; } - /// @notice Checks whether `msg.sender` is the stream's sender. - /// @param streamId The stream id for the query. - function _isCallerStreamSender(uint256 streamId) internal view virtual returns (bool); + function _isCallerStreamSender(uint256 streamId) internal view returns (bool) { + return msg.sender == _streams[streamId].sender; + } - /// @dev Retrieves the stream's status without performing a null check. - function _statusOf(uint256 streamId) internal view virtual returns (Lockup.Status); + function _statusOf(uint256 streamId) internal view returns (Lockup.Status) { + if (_streams[streamId].isDepleted) { + return Lockup.Status.DEPLETED; + } else if (_streams[streamId].wasCanceled) { + return Lockup.Status.CANCELED; + } + + if (block.timestamp < _streams[streamId].startTime) { + return Lockup.Status.PENDING; + } + + if (_calculateStreamedAmount(streamId) < _streams[streamId].amounts.deposited) { + return Lockup.Status.STREAMING; + } else { + return Lockup.Status.SETTLED; + } + } + + /// @dev See the documentation for the user-facing functions that call this internal function. + function _streamedAmountOf(uint256 streamId) internal view returns (uint128) { + Lockup.Amounts memory amounts = _streams[streamId].amounts; + + if (_streams[streamId].isDepleted) { + return amounts.withdrawn; + } else if (_streams[streamId].wasCanceled) { + return amounts.deposited - amounts.refunded; + } + + return _calculateStreamedAmount(streamId); + } /// @dev See the documentation for the user-facing functions that call this internal function. - function _withdrawableAmountOf(uint256 streamId) internal view virtual returns (uint128); + function _withdrawableAmountOf(uint256 streamId) internal view returns (uint128) { + return _streamedAmountOf(streamId) - _streams[streamId].amounts.withdrawn; + } /*////////////////////////////////////////////////////////////////////////// INTERNAL NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ /// @dev See the documentation for the user-facing functions that call this internal function. - function _cancel(uint256 tokenId) internal virtual; + function _cancel(uint256 streamId) internal { + // Calculate the streamed amount. + uint128 streamedAmount = _calculateStreamedAmount(streamId); + + // Retrieve the amounts from storage. + Lockup.Amounts memory amounts = _streams[streamId].amounts; + + // Checks: the stream is not settled. + if (streamedAmount >= amounts.deposited) { + revert Errors.SablierV2Lockup_StreamSettled(streamId); + } + + // Checks: the stream is cancelable. + if (!_streams[streamId].isCancelable) { + revert Errors.SablierV2Lockup_StreamNotCancelable(streamId); + } + + // Calculate the sender's and the recipient's amount. + uint128 senderAmount = amounts.deposited - streamedAmount; + uint128 recipientAmount = streamedAmount - amounts.withdrawn; + + // Effects: mark the stream as canceled. + _streams[streamId].wasCanceled = true; + + // Effects: make the stream not cancelable anymore, because a stream can only be canceled once. + _streams[streamId].isCancelable = false; + + // Effects: If there are no assets left for the recipient to withdraw, mark the stream as depleted. + if (recipientAmount == 0) { + _streams[streamId].isDepleted = true; + } + + // Effects: set the refunded amount. + _streams[streamId].amounts.refunded = senderAmount; + + // Retrieve the sender and the recipient from storage. + address sender = _streams[streamId].sender; + address recipient = _ownerOf(streamId); + + // Retrieve the ERC-20 asset from storage. + IERC20 asset = _streams[streamId].asset; + + // Interactions: refund the sender. + asset.safeTransfer({ to: sender, value: senderAmount }); + + // Log the cancellation. + emit ISablierV2Lockup.CancelLockupStream(streamId, sender, recipient, asset, senderAmount, recipientAmount); + + // Emits an ERC-4906 event to trigger an update of the NFT metadata. + emit MetadataUpdate({ _tokenId: streamId }); + + // Interactions: if the recipient is a contract, try to invoke the cancel hook on the recipient without + // reverting if the hook is not implemented, and without bubbling up any potential revert. + if (recipient.code.length > 0) { + try ISablierV2Recipient(recipient).onLockupStreamCanceled({ + streamId: streamId, + sender: sender, + senderAmount: senderAmount, + recipientAmount: recipientAmount + }) { } catch { } + } + } /// @dev See the documentation for the user-facing functions that call this internal function. - function _renounce(uint256 streamId) internal virtual; + function _renounce(uint256 streamId) internal { + // Checks: the stream is cancelable. + if (!_streams[streamId].isCancelable) { + revert Errors.SablierV2Lockup_StreamNotCancelable(streamId); + } + + // Effects: renounce the stream by making it not cancelable. + _streams[streamId].isCancelable = false; + } /// @dev See the documentation for the user-facing functions that call this internal function. - function _withdraw(uint256 streamId, address to, uint128 amount) internal virtual; + function _withdraw(uint256 streamId, address to, uint128 amount) internal { + // Effects: update the withdrawn amount. + _streams[streamId].amounts.withdrawn = _streams[streamId].amounts.withdrawn + amount; + + // Retrieve the amounts from storage. + Lockup.Amounts memory amounts = _streams[streamId].amounts; + + // Using ">=" instead of "==" for additional safety reasons. In the event of an unforeseen increase in the + // withdrawn amount, the stream will still be marked as depleted. + if (amounts.withdrawn >= amounts.deposited - amounts.refunded) { + // Effects: mark the stream as depleted. + _streams[streamId].isDepleted = true; + + // Effects: make the stream not cancelable anymore, because a depleted stream cannot be canceled. + _streams[streamId].isCancelable = false; + } + + // Retrieve the ERC-20 asset from storage. + IERC20 asset = _streams[streamId].asset; + + // Interactions: perform the ERC-20 transfer. + asset.safeTransfer({ to: to, value: amount }); + + // Log the withdrawal. + emit ISablierV2Lockup.WithdrawFromLockupStream(streamId, to, asset, amount); + } } diff --git a/src/interfaces/ISablierV2Lockup.sol b/src/interfaces/ISablierV2Lockup.sol index e129860e8..19d1c18d7 100644 --- a/src/interfaces/ISablierV2Lockup.sol +++ b/src/interfaces/ISablierV2Lockup.sol @@ -245,15 +245,16 @@ interface ISablierV2Lockup is /// @dev Emits a {Transfer}, {WithdrawFromLockupStream}, and {MetadataUpdate} event. /// /// Notes: - /// - This function attempts to call a hook on the recipient of the stream, unless `msg.sender` is the recipient. - /// - This function attempts to call a hook on the sender of the stream, unless `msg.sender` is the sender. + /// - This function attempts to invoke a hook on the stream's recipient, provided that the recipient is a contract + /// and `msg.sender` is either the sender or an approved operator. /// /// Requirements: /// - Must not be delegate called. /// - `streamId` must not reference a null or depleted stream. + /// - `msg.sender` must be the stream's sender, the stream's recipient or an approved third party. + /// - `to` must be the recipient if `msg.sender` is the stream's sender. /// - `to` must not be the zero address. /// - `amount` must be greater than zero and must not exceed the withdrawable amount. - /// - `to` must be the recipient if `msg.sender` is not the stream's recipient or an approved third party. /// /// @param streamId The id of the stream to withdraw from. /// @param to The address receiving the withdrawn assets. diff --git a/src/interfaces/ISablierV2LockupDynamic.sol b/src/interfaces/ISablierV2LockupDynamic.sol index 8f646cec3..6f2fb6fb3 100644 --- a/src/interfaces/ISablierV2LockupDynamic.sol +++ b/src/interfaces/ISablierV2LockupDynamic.sol @@ -24,7 +24,6 @@ interface ISablierV2LockupDynamic is ISablierV2Lockup { /// @param cancelable Boolean indicating whether the stream will be cancelable or not. /// @param transferable Boolean indicating whether the stream NFT is transferable or not. /// @param segments The segments the protocol uses to compose the custom streaming curve. - /// @param range Struct containing (i) the stream's start time and (ii) end time, both as Unix timestamps. /// @param broker The address of the broker who has helped create the stream, e.g. a front-end website. event CreateLockupDynamicStream( uint256 streamId, @@ -59,7 +58,8 @@ interface ISablierV2LockupDynamic is ISablierV2Lockup { /// @param streamId The stream id for the query. function getSegments(uint256 streamId) external view returns (LockupDynamic.Segment[] memory segments); - /// @notice Retrieves the stream entity. + /// @notice Retrieves the stream details, which is a struct containing the `Lockup.Stream` entity and the stream's + /// segments. /// @dev Reverts if `streamId` references a null stream. /// @param streamId The stream id for the query. function getStream(uint256 streamId) external view returns (LockupDynamic.Stream memory stream); diff --git a/src/interfaces/ISablierV2LockupLinear.sol b/src/interfaces/ISablierV2LockupLinear.sol index a8f066896..86bf02b7e 100644 --- a/src/interfaces/ISablierV2LockupLinear.sol +++ b/src/interfaces/ISablierV2LockupLinear.sol @@ -54,7 +54,8 @@ interface ISablierV2LockupLinear is ISablierV2Lockup { /// @param streamId The stream id for the query. function getRange(uint256 streamId) external view returns (LockupLinear.Range memory range); - /// @notice Retrieves the stream entity. + /// @notice Retrieves the stream details, which is a struct containing the `Lockup.Stream` entity and stream's cliff + /// time. /// @dev Reverts if `streamId` references a null stream. /// @param streamId The stream id for the query. function getStream(uint256 streamId) external view returns (LockupLinear.Stream memory stream); diff --git a/src/interfaces/hooks/ISablierV2Recipient.sol b/src/interfaces/hooks/ISablierV2Recipient.sol index 128d3b958..d6e029af6 100644 --- a/src/interfaces/hooks/ISablierV2Recipient.sol +++ b/src/interfaces/hooks/ISablierV2Recipient.sol @@ -33,7 +33,7 @@ interface ISablierV2Recipient { /// @param streamId The id of the renounced stream. function onLockupStreamRenounced(uint256 streamId) external; - /// @notice Responds to withdrawals triggered by any address except the contract implementing this interface. + /// @notice Responds to withdrawals triggered by either the stream's sender or an approved third party. /// /// @dev Notes: /// - This function may revert, but the Sablier contract will ignore the revert. diff --git a/src/interfaces/hooks/ISablierV2Sender.sol b/src/interfaces/hooks/ISablierV2Sender.sol index 904380460..632e13065 100644 --- a/src/interfaces/hooks/ISablierV2Sender.sol +++ b/src/interfaces/hooks/ISablierV2Sender.sol @@ -6,7 +6,7 @@ pragma solidity >=0.8.22; /// @dev Implementation of this interface is optional. If a sender contract doesn't implement this /// interface or implements it partially, function execution will not revert. interface ISablierV2Sender { - /// @notice Responds to withdrawals triggered by any address except the contract implementing this interface. + /// @notice Responds to withdrawals triggered by either the stream's recipient or an approved third party. /// /// @dev Notes: /// - This function may revert, but the Sablier contract will ignore the revert. diff --git a/src/types/DataTypes.sol b/src/types/DataTypes.sol index 543e8de4e..8ca0f0365 100644 --- a/src/types/DataTypes.sol +++ b/src/types/DataTypes.sol @@ -66,11 +66,40 @@ library Lockup { CANCELED, DEPLETED } + + /// @notice A common data structure to be stored in all contracts inheriting {SablierV2Lockup}. + /// @dev The fields are arranged like this to save gas via tight variable packing. + /// @param sender The address streaming the assets, with the ability to cancel the stream. + /// @param startTime The Unix timestamp indicating the stream's start. + /// @param endTime The Unix timestamp indicating the stream's end. + /// @param isCancelable Boolean indicating if the stream is cancelable. + /// @param wasCanceled Boolean indicating if the stream was canceled. + /// @param asset The contract address of the ERC-20 asset used for streaming. + /// @param isDepleted Boolean indicating if the stream is depleted. + /// @param isStream Boolean indicating if the struct entity exists. + /// @param isTransferable Boolean indicating if the stream NFT is transferable. + /// @param amounts Struct containing the deposit, withdrawn, and refunded amounts, all denoted in units of the + /// asset's decimals. + struct Stream { + // slot 0 + address sender; + uint40 startTime; + uint40 endTime; + bool isCancelable; + bool wasCanceled; + // slot 1 + IERC20 asset; + bool isDepleted; + bool isStream; + bool isTransferable; + // slot 2 and 3 + Lockup.Amounts amounts; + } } /// @notice Namespace for the structs used in {SablierV2LockupDynamic}. library LockupDynamic { - /// @notice Struct encapsulating the parameters for the {SablierV2LockupDynamic.createWithDurations} function. + /// @notice Struct enc apsulating the parameters for the {SablierV2LockupDynamic.createWithDurations} function. /// @param sender The address streaming the assets, with the ability to cancel the stream. It doesn't have to be the /// same as `msg.sender`. /// @param recipient The address receiving the assets. @@ -149,35 +178,20 @@ library LockupDynamic { uint40 duration; } - /// @notice Lockup Dynamic stream. - /// @dev The fields are arranged like this to save gas via tight variable packing. - /// @param sender The address streaming the assets, with the ability to cancel the stream. - /// @param startTime The Unix timestamp indicating the stream's start. - /// @param endTime The Unix timestamp indicating the stream's end. - /// @param isCancelable Boolean indicating if the stream is cancelable. - /// @param wasCanceled Boolean indicating if the stream was canceled. - /// @param asset The contract address of the ERC-20 asset used for streaming. - /// @param isDepleted Boolean indicating if the stream is depleted. - /// @param isStream Boolean indicating if the struct entity exists. - /// @param isTransferable Boolean indicating if the stream NFT is transferable. - /// @param amounts Struct containing the deposit, withdrawn, and refunded amounts, all denoted in units of the - /// asset's decimals. - /// @param segments Segments used to compose the custom streaming curve. + /// @notice Struct encapsulating all the data for a specific id, so that an integrator can obtain all informations + /// within one call to our contract. + /// @dev It cointains the same data as the `Lockup.Stream` struct, plus the segments. struct Stream { - // slot 0 address sender; uint40 startTime; uint40 endTime; bool isCancelable; bool wasCanceled; - // slot 1 IERC20 asset; bool isDepleted; bool isStream; bool isTransferable; - // slot 2 and 3 Lockup.Amounts amounts; - // slots [4..n] Segment[] segments; } } @@ -249,34 +263,20 @@ library LockupLinear { uint40 end; } - /// @notice Lockup Linear stream. - /// @dev The fields are arranged like this to save gas via tight variable packing. - /// @param sender The address streaming the assets, with the ability to cancel the stream. - /// @param startTime The Unix timestamp indicating the stream's start. - /// @param cliffTime The Unix timestamp indicating the cliff period's end. - /// @param isCancelable Boolean indicating if the stream is cancelable. - /// @param wasCanceled Boolean indicating if the stream was canceled. - /// @param asset The contract address of the ERC-20 asset used for streaming. - /// @param endTime The Unix timestamp indicating the stream's end. - /// @param isDepleted Boolean indicating if the stream is depleted. - /// @param isStream Boolean indicating if the struct entity exists. - /// @param isTransferable Boolean indicating if the stream NFT is transferable. - /// @param amounts Struct containing the deposit, withdrawn, and refunded amounts, all denoted in units of the - /// asset's decimals. + /// @notice Struct encapsulating all the data for a specific id, so that an integrator can obtain all informations + /// within one call to our contract. + /// @dev It cointains the same data as the `Lockup.Stream` struct, plus the range. struct Stream { - // slot 0 address sender; uint40 startTime; - uint40 cliffTime; bool isCancelable; bool wasCanceled; - // slot 1 IERC20 asset; uint40 endTime; bool isDepleted; bool isStream; bool isTransferable; - // slot 2 and 3 Lockup.Amounts amounts; + uint40 cliffTime; } }