diff --git a/src/SablierV2LockupDynamic.sol b/src/SablierV2LockupDynamic.sol index 710ab5caa..197c3c78d 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 ids. + 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 @@ -159,103 +108,38 @@ contract SablierV2LockupDynamic is view override notNull(streamId) - returns (LockupDynamic.Stream memory stream) + returns (LockupDynamic.StreamLD 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.StreamLD({ + 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); } /*////////////////////////////////////////////////////////////////////////// @@ -303,8 +187,8 @@ contract SablierV2LockupDynamic is INTERNAL CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Calculates the streamed amount without looking up the stream's status. - function _calculateStreamedAmount(uint256 streamId) internal view returns (uint128) { + /// @inheritdoc SablierV2Lockup + 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 { @@ -337,28 +221,29 @@ contract SablierV2LockupDynamic is 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 +288,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 +307,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,24 +331,24 @@ 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; stream.isTransferable = params.transferable; stream.isStream = true; stream.sender = params.sender; + stream.startTime = params.startTime; unchecked { // The segment count cannot be zero at this point. uint256 segmentCount = params.segments.length; stream.endTime = params.segments[segmentCount - 1].timestamp; - stream.startTime = params.startTime; // 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 +390,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..6a89afdd8 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 ids. + 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,141 +85,48 @@ 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 view override notNull(streamId) - returns (LockupLinear.Stream memory stream) + returns (LockupLinear.StreamLL 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 = LockupLinear.StreamLL({ + 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); } /*////////////////////////////////////////////////////////////////////////// @@ -262,11 +146,12 @@ contract SablierV2LockupLinear is // Calculate the cliff time and the end time. It is safe to use unchecked arithmetic because // {_createWithTimestamps} will nonetheless check that the end time is greater than the cliff time, - // and also that the cliff time is greater than or equal to the start time. + // and also that the cliff time, if set, is greater than or equal to the start time. unchecked { - range.cliff = range.start + params.durations.cliff; + range.cliff = params.durations.cliff > 0 ? range.start + params.durations.cliff : 0; range.end = range.start + params.durations.total; } + // Checks, Effects and Interactions: create the stream. streamId = _createWithTimestamps( LockupLinear.CreateWithTimestamps({ @@ -297,10 +182,10 @@ contract SablierV2LockupLinear is INTERNAL CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Calculates the streamed amount without looking up the stream's status. - function _calculateStreamedAmount(uint256 streamId) internal view returns (uint128) { + /// @inheritdoc SablierV2Lockup + 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 +226,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 +250,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 +263,11 @@ contract SablierV2LockupLinear is wasCanceled: false }); + // Effects: set the cliff time if it is greater than 0. + if (params.range.cliff > 0) { + _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 +307,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 93301d4c0..47ef57fff 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 //////////////////////////////////////////////////////////////////////////*/ @@ -32,6 +36,9 @@ abstract contract SablierV2Lockup is /// @inheritdoc ISablierV2Lockup ISablierV2NFTDescriptor public override nftDescriptor; + /// @dev Sablier V2 Lockup streams mapped by unsigned integers. + mapping(uint256 id => Lockup.Stream stream) internal _streams; + /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ @@ -55,7 +62,7 @@ abstract contract SablierV2Lockup is /// @dev Checks that `streamId` does not reference a null stream. modifier notNull(uint256 streamId) { - if (!isStream(streamId)) { + if (!_streams[streamId].isStream) { revert Errors.SablierV2Lockup_Null(streamId); } _; @@ -71,14 +78,71 @@ 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 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 ISablierV2Lockup function getRecipient(uint256 streamId) external view override returns (address recipient) { - // Checks: the stream NFT exists, and return the owner, which is the stream's recipient. + // Checks: the stream NFT exists and return the owner, which is the stream's recipient. recipient = _requireOwned({ tokenId: streamId }); } /// @inheritdoc ISablierV2Lockup - function getSender(uint256 streamId) public view virtual override returns (address sender); + function getRefundedAmount(uint256 streamId) + external + view + override + notNull(streamId) + returns (uint128 refundedAmount) + { + refundedAmount = _streams[streamId].amounts.refunded; + } + + /// @inheritdoc ISablierV2Lockup + function getSender(uint256 streamId) external 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 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 isCold(uint256 streamId) external view override notNull(streamId) returns (bool result) { @@ -87,10 +151,19 @@ abstract contract SablierV2Lockup is } /// @inheritdoc ISablierV2Lockup - function isDepleted(uint256 streamId) public view virtual override returns (bool result); + function isDepleted(uint256 streamId) external view override notNull(streamId) returns (bool result) { + result = _streams[streamId].isDepleted; + } /// @inheritdoc ISablierV2Lockup - function isStream(uint256 streamId) public view virtual override returns (bool result); + function isStream(uint256 streamId) external view override returns (bool result) { + result = _streams[streamId].isStream; + } + + /// @inheritdoc ISablierV2Lockup + function isTransferable(uint256 streamId) external view override notNull(streamId) returns (bool result) { + result = _streams[streamId].isTransferable; + } /// @inheritdoc ISablierV2Lockup function isWarm(uint256 streamId) external view override notNull(streamId) returns (bool result) { @@ -98,6 +171,40 @@ abstract contract SablierV2Lockup is result = status == Lockup.Status.PENDING || status == Lockup.Status.STREAMING; } + /// @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); + } + + /// @inheritdoc ISablierV2Lockup + function streamedAmountOf(uint256 streamId) + public + view + virtual + override + notNull(streamId) + returns (uint128 streamedAmount) + { + streamedAmount = _streamedAmountOf(streamId); + } + /// @inheritdoc ERC721 function tokenURI(uint256 streamId) public view override(IERC721Metadata, ERC721) returns (string memory uri) { // Checks: the stream NFT exists. @@ -108,7 +215,9 @@ abstract contract SablierV2Lockup is } /// @inheritdoc ISablierV2Lockup - function wasCanceled(uint256 streamId) public view virtual override returns (bool result); + function wasCanceled(uint256 streamId) external view override notNull(streamId) returns (bool result) { + result = _streams[streamId].wasCanceled; + } /// @inheritdoc ISablierV2Lockup function withdrawableAmountOf(uint256 streamId) @@ -121,17 +230,14 @@ abstract contract SablierV2Lockup is withdrawableAmount = _withdrawableAmountOf(streamId); } - /// @inheritdoc ISablierV2Lockup - function isTransferable(uint256 streamId) public view virtual returns (bool); - /*////////////////////////////////////////////////////////////////////////// USER-FACING NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc ISablierV2Lockup - function burn(uint256 streamId) external override noDelegateCall { - // Checks: only depleted streams can be burned. This also checks that the stream is not null. - if (!isDepleted(streamId)) { + function burn(uint256 streamId) external override noDelegateCall notNull(streamId) { + // Checks: only depleted streams can be burned. + if (!_streams[streamId].isDepleted) { revert Errors.SablierV2Lockup_StreamNotDepleted(streamId); } @@ -147,11 +253,11 @@ abstract contract SablierV2Lockup is } /// @inheritdoc ISablierV2Lockup - function cancel(uint256 streamId) public override noDelegateCall { - // Checks: the stream is neither depleted nor canceled. This also checks that the stream is not null. - if (isDepleted(streamId)) { + function cancel(uint256 streamId) public override noDelegateCall notNull(streamId) { + // Checks: the stream is neither depleted nor canceled. + if (_streams[streamId].isDepleted) { revert Errors.SablierV2Lockup_StreamDepleted(streamId); - } else if (wasCanceled(streamId)) { + } else if (_streams[streamId].wasCanceled) { revert Errors.SablierV2Lockup_StreamCanceled(streamId); } @@ -231,10 +337,11 @@ abstract contract SablierV2Lockup is public override noDelegateCall + notNull(streamId) updateMetadata(streamId) { - // Checks: the stream is not depleted. This also checks that the stream is not null. - if (isDepleted(streamId)) { + // Checks: the stream is not depleted. + if (_streams[streamId].isDepleted) { revert Errors.SablierV2Lockup_StreamDepleted(streamId); } @@ -264,7 +371,7 @@ abstract contract SablierV2Lockup is } // Retrieve the sender from storage. - address sender = getSender(streamId); + address sender = _streams[streamId].sender; // Effects and Interactions: make the withdrawal. _withdraw(streamId, to, amount); @@ -352,6 +459,10 @@ abstract contract SablierV2Lockup is INTERNAL CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ + /// @notice Calculates the streamed amount of the stream without looking up the stream's status, 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) { @@ -362,10 +473,41 @@ abstract contract SablierV2Lockup is /// @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); + } /// @notice Overrides the internal ERC-721 `_update` function to check that the stream is transferable and emit /// an ERC-4906 event. @@ -389,7 +531,7 @@ abstract contract SablierV2Lockup is { address from = _ownerOf(streamId); - if (!isTransferable(streamId) && from != address(0) && to != address(0)) { + if (!_streams[streamId].isTransferable && from != address(0) && to != address(0)) { revert Errors.SablierV2Lockup_NotTransferable(streamId); } @@ -397,18 +539,114 @@ abstract contract SablierV2Lockup is } /// @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/ISablierV2LockupDynamic.sol b/src/interfaces/ISablierV2LockupDynamic.sol index 8f646cec3..ef6e68ea9 100644 --- a/src/interfaces/ISablierV2LockupDynamic.sol +++ b/src/interfaces/ISablierV2LockupDynamic.sol @@ -59,10 +59,10 @@ 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 documented in {DataTypes}. /// @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); + function getStream(uint256 streamId) external view returns (LockupDynamic.StreamLD memory stream); /// @notice Calculates the amount streamed to the recipient, denoted in units of the asset's decimals. /// diff --git a/src/interfaces/ISablierV2LockupLinear.sol b/src/interfaces/ISablierV2LockupLinear.sol index a8f066896..f2a5fdd7c 100644 --- a/src/interfaces/ISablierV2LockupLinear.sol +++ b/src/interfaces/ISablierV2LockupLinear.sol @@ -43,7 +43,8 @@ interface ISablierV2LockupLinear is ISablierV2Lockup { CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @notice Retrieves the stream's cliff time, which is a Unix timestamp. + /// @notice Retrieves the stream's cliff time, which is a Unix timestamp. A value of zero means there + /// is no cliff. /// @dev Reverts if `streamId` references a null stream. /// @param streamId The stream id for the query. function getCliffTime(uint256 streamId) external view returns (uint40 cliffTime); @@ -54,10 +55,10 @@ 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 documented in {DataTypes}. /// @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); + function getStream(uint256 streamId) external view returns (LockupLinear.StreamLL memory stream); /// @notice Calculates the amount streamed to the recipient, denoted in units of the asset's decimals. /// @@ -106,14 +107,17 @@ interface ISablierV2LockupLinear is ISablierV2Lockup { /// @dev Emits a {Transfer} and {CreateLockupLinearStream} event. /// /// Notes: + /// - A cliff time of zero means there is no cliff. /// - As long as the times are ordered, it is not an error for the start or the cliff time to be in the past. /// /// Requirements: /// - Must not be delegate called. /// - `params.totalAmount` must be greater than zero. /// - If set, `params.broker.fee` must not be greater than `MAX_FEE`. - /// - `params.range.start` must be less than or equal to `params.range.cliff`. - /// - `params.range.cliff` must be less than `params.range.end`. + /// - `params.range.start` must be greater than zero. + /// - `params.range.start` must be less than `params.range.end`. + /// - If set, `params.range.cliff` must be greater than `params.range.start`. + /// - If set, `params.range.cliff` must be less than `params.range.end`. /// - `params.range.end` must be in the future. /// - `params.recipient` must not be the zero address. /// - `msg.sender` must have allowed this contract to spend at least `params.totalAmount` assets. diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 278c43f8b..dd7c6ad57 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -116,7 +116,13 @@ library Errors { error SablierV2LockupLinear_CliffTimeNotLessThanEndTime(uint40 cliffTime, uint40 endTime); /// @notice Thrown when trying to create a stream with a start time greater than the cliff time. - error SablierV2LockupLinear_StartTimeGreaterThanCliffTime(uint40 startTime, uint40 cliffTime); + error SablierV2LockupLinear_StartTimeNotLessThanCliffTime(uint40 startTime, uint40 cliffTime); + + /// @notice Thrown when trying to create a stream with a start time greater than the end time. + error SablierV2LockupLinear_StartTimeNotLessThanEndTime(uint40 startTime, uint40 endTime); + + /// @notice Thrown when trying to create a stream with a start time equal to zero. + error SablierV2LockupLinear_StartTimeZero(); /*////////////////////////////////////////////////////////////////////////// SABLIER-V2-NFT-DESCRIPTOR diff --git a/src/libraries/Helpers.sol b/src/libraries/Helpers.sol index 73ee41f34..894ccf27a 100644 --- a/src/libraries/Helpers.sol +++ b/src/libraries/Helpers.sol @@ -92,9 +92,19 @@ library Helpers { revert Errors.SablierV2Lockup_DepositAmountZero(); } - // Checks: the start time is less than or equal to the cliff time. - if (range.start > range.cliff) { - revert Errors.SablierV2LockupLinear_StartTimeGreaterThanCliffTime(range.start, range.cliff); + // Checks: the start time is not zero. + if (range.start == 0) { + revert Errors.SablierV2LockupLinear_StartTimeZero(); + } + + // Checks: the start time is strictly less than the end time. + if (range.start >= range.end) { + revert Errors.SablierV2LockupLinear_StartTimeNotLessThanEndTime(range.start, range.end); + } + + // Checks: the start time is strictly less than the cliff time when cliff time is not zero. + if (range.cliff > 0 && range.start >= range.cliff) { + revert Errors.SablierV2LockupLinear_StartTimeNotLessThanCliffTime(range.start, range.cliff); } // Checks: the cliff time is strictly less than the end time. diff --git a/src/types/DataTypes.sol b/src/types/DataTypes.sol index 543e8de4e..f024ae71c 100644 --- a/src/types/DataTypes.sol +++ b/src/types/DataTypes.sol @@ -66,6 +66,35 @@ library Lockup { CANCELED, DEPLETED } + + /// @notice A common data structure to be stored in all child contracts of {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}. @@ -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. - struct Stream { - // slot 0 + /// @notice Struct encapsulating all the data for a specific id, allowing anyone to retrieve all information within + /// one call to the contract. + /// @dev It contains the same data as the `Lockup.Stream` struct, plus the segments. + struct StreamLD { 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; } } @@ -241,7 +255,7 @@ library LockupLinear { /// @notice Struct encapsulating the time range. /// @param start The Unix timestamp for the stream's start. - /// @param cliff The Unix timestamp for the cliff period's end. + /// @param cliff The Unix timestamp for the cliff period's end. A value of zero means there is no cliff. /// @param end The Unix timestamp for the stream's end. struct Range { uint40 start; @@ -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. - struct Stream { - // slot 0 + /// @notice Struct encapsulating all the data for a specific id, allowing anyone to retrieve all information within + /// one call to the contract. + /// @dev It contains the same data as the `Lockup.Stream` struct, plus the cliff value. + struct StreamLL { 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; } } diff --git a/test/fork/LockupDynamic.t.sol b/test/fork/LockupDynamic.t.sol index 931e565a0..b9a983c4a 100644 --- a/test/fork/LockupDynamic.t.sol +++ b/test/fork/LockupDynamic.t.sol @@ -197,7 +197,7 @@ abstract contract LockupDynamic_Fork_Test is Fork_Test { vars.isCancelable = vars.isSettled ? false : true; // Assert that the stream has been created. - LockupDynamic.Stream memory actualStream = lockupDynamic.getStream(vars.streamId); + LockupDynamic.StreamLD memory actualStream = lockupDynamic.getStream(vars.streamId); assertEq(actualStream.amounts, Lockup.Amounts(vars.createAmounts.deposit, 0, 0)); assertEq(actualStream.asset, ASSET, "asset"); assertEq(actualStream.endTime, vars.range.end, "endTime"); diff --git a/test/fork/LockupLinear.t.sol b/test/fork/LockupLinear.t.sol index d390e49d0..65247b60a 100644 --- a/test/fork/LockupLinear.t.sol +++ b/test/fork/LockupLinear.t.sol @@ -121,7 +121,7 @@ abstract contract LockupLinear_Fork_Test is Fork_Test { params.broker.fee = _bound(params.broker.fee, 0, MAX_FEE); params.protocolFee = _bound(params.protocolFee, 0, MAX_FEE); params.range.start = boundUint40(params.range.start, currentTime - 1000 seconds, currentTime + 10_000 seconds); - params.range.cliff = boundUint40(params.range.cliff, params.range.start, params.range.start + 52 weeks); + params.range.cliff = boundUint40(params.range.cliff, params.range.start + 1, params.range.start + 52 weeks); params.totalAmount = boundUint128(params.totalAmount, 1, uint128(initialHolderBalance)); params.transferable = true; @@ -193,7 +193,7 @@ abstract contract LockupLinear_Fork_Test is Fork_Test { ); // Assert that the stream has been created. - LockupLinear.Stream memory actualStream = lockupLinear.getStream(vars.streamId); + LockupLinear.StreamLL memory actualStream = lockupLinear.getStream(vars.streamId); assertEq(actualStream.amounts, Lockup.Amounts(vars.createAmounts.deposit, 0, 0)); assertEq(actualStream.asset, ASSET, "asset"); assertEq(actualStream.cliffTime, params.range.cliff, "cliffTime"); diff --git a/test/integration/concrete/lockup-dynamic/create-with-durations/createWithDurations.t.sol b/test/integration/concrete/lockup-dynamic/create-with-durations/createWithDurations.t.sol index 41d2449b6..4ae6afc20 100644 --- a/test/integration/concrete/lockup-dynamic/create-with-durations/createWithDurations.t.sol +++ b/test/integration/concrete/lockup-dynamic/create-with-durations/createWithDurations.t.sol @@ -171,8 +171,8 @@ contract CreateWithDurations_LockupDynamic_Integration_Concrete_Test is createDefaultStreamWithDurations(); // Assert that the stream has been created. - LockupDynamic.Stream memory actualStream = lockupDynamic.getStream(streamId); - LockupDynamic.Stream memory expectedStream = defaults.lockupDynamicStream(); + LockupDynamic.StreamLD memory actualStream = lockupDynamic.getStream(streamId); + LockupDynamic.StreamLD memory expectedStream = defaults.lockupDynamicStream(); expectedStream.endTime = range.end; expectedStream.segments = segments; expectedStream.startTime = range.start; diff --git a/test/integration/concrete/lockup-dynamic/create-with-timestamps/createWithTimestamps.t.sol b/test/integration/concrete/lockup-dynamic/create-with-timestamps/createWithTimestamps.t.sol index 75ad84408..524a69d30 100644 --- a/test/integration/concrete/lockup-dynamic/create-with-timestamps/createWithTimestamps.t.sol +++ b/test/integration/concrete/lockup-dynamic/create-with-timestamps/createWithTimestamps.t.sol @@ -383,8 +383,8 @@ contract CreateWithTimestamps_LockupDynamic_Integration_Concrete_Test is streamId = createDefaultStreamWithAsset(IERC20(asset)); // Assert that the stream has been created. - LockupDynamic.Stream memory actualStream = lockupDynamic.getStream(streamId); - LockupDynamic.Stream memory expectedStream = defaults.lockupDynamicStream(); + LockupDynamic.StreamLD memory actualStream = lockupDynamic.getStream(streamId); + LockupDynamic.StreamLD memory expectedStream = defaults.lockupDynamicStream(); expectedStream.asset = IERC20(asset); assertEq(actualStream, expectedStream); diff --git a/test/integration/concrete/lockup-dynamic/get-stream/getStream.t.sol b/test/integration/concrete/lockup-dynamic/get-stream/getStream.t.sol index 0c9e1f110..50f3a75ac 100644 --- a/test/integration/concrete/lockup-dynamic/get-stream/getStream.t.sol +++ b/test/integration/concrete/lockup-dynamic/get-stream/getStream.t.sol @@ -26,8 +26,8 @@ contract GetStream_LockupDynamic_Integration_Concrete_Test is LockupDynamic_Inte function test_GetStream_StatusSettled() external givenNotNull { vm.warp({ timestamp: defaults.END_TIME() }); - LockupDynamic.Stream memory actualStream = lockupDynamic.getStream(defaultStreamId); - LockupDynamic.Stream memory expectedStream = defaults.lockupDynamicStream(); + LockupDynamic.StreamLD memory actualStream = lockupDynamic.getStream(defaultStreamId); + LockupDynamic.StreamLD memory expectedStream = defaults.lockupDynamicStream(); expectedStream.isCancelable = false; assertEq(actualStream, expectedStream); } @@ -38,8 +38,8 @@ contract GetStream_LockupDynamic_Integration_Concrete_Test is LockupDynamic_Inte function test_GetStream() external givenNotNull givenStatusNotSettled { uint256 streamId = createDefaultStream(); - LockupDynamic.Stream memory actualStream = lockupDynamic.getStream(streamId); - LockupDynamic.Stream memory expectedStream = defaults.lockupDynamicStream(); + LockupDynamic.StreamLD memory actualStream = lockupDynamic.getStream(streamId); + LockupDynamic.StreamLD memory expectedStream = defaults.lockupDynamicStream(); assertEq(actualStream, expectedStream); } } diff --git a/test/integration/concrete/lockup-linear/create-with-durations/createWithDurations.t.sol b/test/integration/concrete/lockup-linear/create-with-durations/createWithDurations.t.sol index 1268f5a57..1a5c5acd7 100644 --- a/test/integration/concrete/lockup-linear/create-with-durations/createWithDurations.t.sol +++ b/test/integration/concrete/lockup-linear/create-with-durations/createWithDurations.t.sol @@ -28,30 +28,6 @@ contract CreateWithDurations_LockupLinear_Integration_Concrete_Test is expectRevertDueToDelegateCall(success, returnData); } - function test_RevertWhen_CliffDurationCalculationOverflows() external whenNotDelegateCalled { - uint40 startTime = getBlockTimestamp(); - uint40 cliffDuration = MAX_UINT40 - startTime + 1 seconds; - - // Calculate the end time. Needs to be "unchecked" to avoid an overflow. - uint40 cliffTime; - unchecked { - cliffTime = startTime + cliffDuration; - } - - // Expect the relevant error to be thrown. - vm.expectRevert( - abi.encodeWithSelector( - Errors.SablierV2LockupLinear_StartTimeGreaterThanCliffTime.selector, startTime, cliffTime - ) - ); - - // Set the total duration to be the same as the cliff duration. - uint40 totalDuration = cliffDuration; - - // Create the stream. - createDefaultStreamWithDurations(LockupLinear.Durations({ cliff: cliffDuration, total: totalDuration })); - } - function test_RevertWhen_TotalDurationCalculationOverflows() external whenNotDelegateCalled @@ -61,7 +37,7 @@ contract CreateWithDurations_LockupLinear_Integration_Concrete_Test is LockupLinear.Durations memory durations = LockupLinear.Durations({ cliff: 0, total: MAX_UINT40 - startTime + 1 seconds }); - // Calculate the cliff time and the end time. Needs to be "unchecked" to avoid an overflow. + // Calculate the cliff time and the end time. Needs to be "unchecked" to allow an overflow. uint40 cliffTime; uint40 endTime; unchecked { @@ -72,7 +48,7 @@ contract CreateWithDurations_LockupLinear_Integration_Concrete_Test is // Expect the relevant error to be thrown. vm.expectRevert( abi.encodeWithSelector( - Errors.SablierV2LockupLinear_CliffTimeNotLessThanEndTime.selector, cliffTime, endTime + Errors.SablierV2LockupLinear_StartTimeNotLessThanEndTime.selector, startTime, endTime ) ); @@ -131,8 +107,8 @@ contract CreateWithDurations_LockupLinear_Integration_Concrete_Test is createDefaultStreamWithDurations(); // Assert that the stream has been created. - LockupLinear.Stream memory actualStream = lockupLinear.getStream(streamId); - LockupLinear.Stream memory expectedStream = defaults.lockupLinearStream(); + LockupLinear.StreamLL memory actualStream = lockupLinear.getStream(streamId); + LockupLinear.StreamLL memory expectedStream = defaults.lockupLinearStream(); expectedStream.startTime = range.start; expectedStream.cliffTime = range.cliff; expectedStream.endTime = range.end; diff --git a/test/integration/concrete/lockup-linear/create-with-durations/createWithDurations.tree b/test/integration/concrete/lockup-linear/create-with-durations/createWithDurations.tree index ca9a10630..1b742b1c2 100644 --- a/test/integration/concrete/lockup-linear/create-with-durations/createWithDurations.tree +++ b/test/integration/concrete/lockup-linear/create-with-durations/createWithDurations.tree @@ -2,9 +2,6 @@ createWithDurations.t.sol ├── when delegate called │ └── it should revert └── when not delegate called - ├── when the cliff duration calculation overflows uint256 - │ └── it should revert due to the start time being greater than the cliff time - └── when the cliff duration calculation does not overflow uint256 ├── when the total duration calculation overflows uint256 │ └── it should revert └── when the total duration calculation does not overflow uint256 diff --git a/test/integration/concrete/lockup-linear/create-with-timestamps/createWithTimestamps.t.sol b/test/integration/concrete/lockup-linear/create-with-timestamps/createWithTimestamps.t.sol index f4b4b656a..25d952f72 100644 --- a/test/integration/concrete/lockup-linear/create-with-timestamps/createWithTimestamps.t.sol +++ b/test/integration/concrete/lockup-linear/create-with-timestamps/createWithTimestamps.t.sol @@ -46,18 +46,80 @@ contract CreateWithTimestamps_LockupLinear_Integration_Concrete_Test is createDefaultStreamWithTotalAmount(0); } + function test_RevertWhen_StartTimeZero() external whenNotDelegateCalled whenRecipientNonZeroAddress { + uint40 cliffTime = defaults.CLIFF_TIME(); + uint40 endTime = defaults.END_TIME(); + + vm.expectRevert(Errors.SablierV2LockupLinear_StartTimeZero.selector); + createDefaultStreamWithRange(LockupLinear.Range({ start: 0, cliff: cliffTime, end: endTime })); + } + + modifier whenCliffTimeZero() { + _; + } + + function test_RevertWhen_StartTimeGreaterThanEndTime() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenCliffTimeZero + { + uint40 startTime = defaults.END_TIME(); + uint40 endTime = defaults.START_TIME(); + + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierV2LockupLinear_StartTimeNotLessThanEndTime.selector, startTime, endTime + ) + ); + createDefaultStreamWithRange(LockupLinear.Range({ start: startTime, cliff: 0, end: endTime })); + } + + function test_CreateWithTimestamps_CliffTimeZero() + external + whenNotDelegateCalled + whenRecipientNonZeroAddress + whenDepositAmountNotZero + whenCliffTimeZero + { + createDefaultStreamWithRange( + LockupLinear.Range({ start: defaults.START_TIME(), cliff: 0, end: defaults.END_TIME() }) + ); + + // Assert that the stream has been created. + LockupLinear.StreamLL memory actualStream = lockupLinear.getStream(streamId); + LockupLinear.StreamLL memory expectedStream = defaults.lockupLinearStream(); + expectedStream.cliffTime = 0; + assertEq(actualStream, expectedStream); + + // Assert that the next stream id has been bumped. + uint256 actualNextStreamId = lockupLinear.nextStreamId(); + uint256 expectedNextStreamId = streamId + 1; + assertEq(actualNextStreamId, expectedNextStreamId, "nextStreamId"); + + // Assert that the NFT has been minted. + address actualNFTOwner = lockupLinear.ownerOf({ tokenId: streamId }); + address expectedNFTOwner = users.recipient; + assertEq(actualNFTOwner, expectedNFTOwner, "NFT owner"); + } + + modifier whenCliffTimeGreaterThanZero() { + _; + } + function test_RevertWhen_StartTimeGreaterThanCliffTime() external whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenCliffTimeGreaterThanZero { uint40 startTime = defaults.CLIFF_TIME(); uint40 cliffTime = defaults.START_TIME(); uint40 endTime = defaults.END_TIME(); vm.expectRevert( abi.encodeWithSelector( - Errors.SablierV2LockupLinear_StartTimeGreaterThanCliffTime.selector, startTime, cliffTime + Errors.SablierV2LockupLinear_StartTimeNotLessThanCliffTime.selector, startTime, cliffTime ) ); createDefaultStreamWithRange(LockupLinear.Range({ start: startTime, cliff: cliffTime, end: endTime })); @@ -68,6 +130,7 @@ contract CreateWithTimestamps_LockupLinear_Integration_Concrete_Test is whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenCliffTimeGreaterThanZero whenStartTimeNotGreaterThanCliffTime { uint40 startTime = defaults.START_TIME(); @@ -86,6 +149,7 @@ contract CreateWithTimestamps_LockupLinear_Integration_Concrete_Test is whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenCliffTimeGreaterThanZero whenStartTimeNotGreaterThanCliffTime whenCliffTimeLessThanEndTime whenEndTimeInTheFuture @@ -101,6 +165,7 @@ contract CreateWithTimestamps_LockupLinear_Integration_Concrete_Test is whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenCliffTimeGreaterThanZero whenStartTimeNotGreaterThanCliffTime whenCliffTimeLessThanEndTime whenEndTimeInTheFuture @@ -124,6 +189,7 @@ contract CreateWithTimestamps_LockupLinear_Integration_Concrete_Test is whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenCliffTimeGreaterThanZero whenStartTimeNotGreaterThanCliffTime whenCliffTimeLessThanEndTime whenEndTimeInTheFuture @@ -139,6 +205,7 @@ contract CreateWithTimestamps_LockupLinear_Integration_Concrete_Test is whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenCliffTimeGreaterThanZero whenStartTimeNotGreaterThanCliffTime whenCliffTimeLessThanEndTime whenEndTimeInTheFuture @@ -155,6 +222,7 @@ contract CreateWithTimestamps_LockupLinear_Integration_Concrete_Test is whenNotDelegateCalled whenRecipientNonZeroAddress whenDepositAmountNotZero + whenCliffTimeGreaterThanZero whenStartTimeNotGreaterThanCliffTime whenCliffTimeLessThanEndTime whenEndTimeInTheFuture @@ -169,6 +237,7 @@ contract CreateWithTimestamps_LockupLinear_Integration_Concrete_Test is external whenNotDelegateCalled whenDepositAmountNotZero + whenCliffTimeGreaterThanZero whenStartTimeNotGreaterThanCliffTime whenCliffTimeLessThanEndTime whenEndTimeInTheFuture @@ -222,8 +291,8 @@ contract CreateWithTimestamps_LockupLinear_Integration_Concrete_Test is createDefaultStreamWithAsset(IERC20(asset)); // Assert that the stream has been created. - LockupLinear.Stream memory actualStream = lockupLinear.getStream(streamId); - LockupLinear.Stream memory expectedStream = defaults.lockupLinearStream(); + LockupLinear.StreamLL memory actualStream = lockupLinear.getStream(streamId); + LockupLinear.StreamLL memory expectedStream = defaults.lockupLinearStream(); expectedStream.asset = IERC20(asset); assertEq(actualStream, expectedStream); diff --git a/test/integration/concrete/lockup-linear/create-with-timestamps/createWithTimestamps.tree b/test/integration/concrete/lockup-linear/create-with-timestamps/createWithTimestamps.tree index 407705603..e1537f793 100644 --- a/test/integration/concrete/lockup-linear/create-with-timestamps/createWithTimestamps.tree +++ b/test/integration/concrete/lockup-linear/create-with-timestamps/createWithTimestamps.tree @@ -8,38 +8,47 @@ createWithTimestamps.t.sol ├── when the deposit amount is zero │ └── it should revert └── when the deposit amount is not zero - ├── when the start time is greater than the cliff time + ├── when the start time is zero │ └── it should revert - └── when the start time is not greater than the cliff time - ├── when the cliff time is not less than the end time - │ └── it should revert - └── when the cliff time is less than the end time - ├── when the end time is not in the future + └── when the start time is not zero + ├── when the cliff time is zero + │ ├── when the start time is greater than the end time + │ │ └── it should revert + │ └── when the start time is not greater than the end time + │ └── it should create the stream + └── when the cliff time is greater than zero + ├── when the start time is not less than the cliff time │ └── it should revert - └── when the end time is in the future - ├── given the protocol fee is too high + └── when the start time is not greater than the cliff time + ├── when the cliff time is less than the end time │ └── it should revert - └── given the protocol fee is not too high - ├── when the broker fee is too high + └── when the cliff time is less than the end time + ├── when the end time is not in the future │ └── it should revert - └── when the broker fee is not too high - ├── when the asset is not a contract + └── when the end time is in the future + ├── given the protocol fee is too high │ └── it should revert - └── when the asset is a contract - ├── when the asset misses the ERC-20 return value - │ ├── it should create the stream - │ ├── it should bump the next stream id - │ ├── it should record the protocol fee - │ ├── it should mint the NFT - │ ├── it should emit a {MetadataUpdate} event - │ ├── it should perform the ERC-20 transfers - │ └── it should emit a {CreateLockupLinearStream} event - └── when the asset does not miss the ERC-20 return value - ├── it should create the stream - ├── it should bump the next stream id - ├── it should record the protocol fee - ├── it should mint the NFT - ├── it should emit a {MetadataUpdate} event - ├── it should perform the ERC-20 transfers - └── it should emit a {CreateLockupLinearStream} event + └── given the protocol fee is not too high + ├── when the broker fee is too high + │ └── it should revert + └── when the broker fee is not too high + ├── when the asset is not a contract + │ └── it should revert + └── when the asset is a contract + ├── when the asset misses the ERC-20 return value + │ ├── it should create the stream + │ ├── it should bump the next stream id + │ ├── it should record the protocol fee + │ ├── it should mint the NFT + │ ├── it should emit a {MetadataUpdate} event + │ ├── it should perform the ERC-20 transfers + │ └── it should emit a {CreateLockupLinearStream} event + └── when the asset does not miss the ERC-20 return value + ├── it should create the stream + ├── it should bump the next stream id + ├── it should record the protocol fee + ├── it should mint the NFT + ├── it should emit a {MetadataUpdate} event + ├── it should perform the ERC-20 transfers + └── it should emit a {CreateLockupLinearStream} event diff --git a/test/integration/concrete/lockup-linear/get-stream/getStream.t.sol b/test/integration/concrete/lockup-linear/get-stream/getStream.t.sol index eedcae912..0524c819c 100644 --- a/test/integration/concrete/lockup-linear/get-stream/getStream.t.sol +++ b/test/integration/concrete/lockup-linear/get-stream/getStream.t.sol @@ -26,8 +26,8 @@ contract GetStream_LockupLinear_Integration_Concrete_Test is LockupLinear_Integr function test_GetStream_StatusSettled() external givenNotNull { vm.warp({ timestamp: defaults.END_TIME() }); - LockupLinear.Stream memory actualStream = lockupLinear.getStream(defaultStreamId); - LockupLinear.Stream memory expectedStream = defaults.lockupLinearStream(); + LockupLinear.StreamLL memory actualStream = lockupLinear.getStream(defaultStreamId); + LockupLinear.StreamLL memory expectedStream = defaults.lockupLinearStream(); expectedStream.isCancelable = false; assertEq(actualStream, expectedStream); } @@ -37,8 +37,8 @@ contract GetStream_LockupLinear_Integration_Concrete_Test is LockupLinear_Integr } function test_GetStream() external givenNotNull givenStatusNotSettled { - LockupLinear.Stream memory actualStream = lockupLinear.getStream(defaultStreamId); - LockupLinear.Stream memory expectedStream = defaults.lockupLinearStream(); + LockupLinear.StreamLL memory actualStream = lockupLinear.getStream(defaultStreamId); + LockupLinear.StreamLL memory expectedStream = defaults.lockupLinearStream(); assertEq(actualStream, expectedStream); } } diff --git a/test/integration/fuzz/lockup-dynamic/createWithDurations.t.sol b/test/integration/fuzz/lockup-dynamic/createWithDurations.t.sol index 7ceb58e9d..023157f3b 100644 --- a/test/integration/fuzz/lockup-dynamic/createWithDurations.t.sol +++ b/test/integration/fuzz/lockup-dynamic/createWithDurations.t.sol @@ -110,7 +110,7 @@ contract CreateWithDurations_LockupDynamic_Integration_Fuzz_Test is vars.isCancelable = vars.isSettled ? false : true; // Assert that the stream has been created. - LockupDynamic.Stream memory actualStream = lockupDynamic.getStream(streamId); + LockupDynamic.StreamLD memory actualStream = lockupDynamic.getStream(streamId); assertEq(actualStream.amounts, Lockup.Amounts(vars.createAmounts.deposit, 0, 0)); assertEq(actualStream.asset, dai, "asset"); assertEq(actualStream.endTime, range.end, "endTime"); diff --git a/test/integration/fuzz/lockup-dynamic/createWithTimestamps.t.sol b/test/integration/fuzz/lockup-dynamic/createWithTimestamps.t.sol index 861f34eb9..e2cad95c0 100644 --- a/test/integration/fuzz/lockup-dynamic/createWithTimestamps.t.sol +++ b/test/integration/fuzz/lockup-dynamic/createWithTimestamps.t.sol @@ -305,7 +305,7 @@ contract CreateWithTimestamps_LockupDynamic_Integration_Fuzz_Test is vars.isCancelable = vars.isSettled ? false : params.cancelable; // Assert that the stream has been created. - LockupDynamic.Stream memory actualStream = lockupDynamic.getStream(streamId); + LockupDynamic.StreamLD memory actualStream = lockupDynamic.getStream(streamId); assertEq(actualStream.amounts, Lockup.Amounts(vars.createAmounts.deposit, 0, 0)); assertEq(actualStream.asset, dai, "asset"); assertEq(actualStream.endTime, range.end, "endTime"); diff --git a/test/integration/fuzz/lockup-linear/createWithDurations.t.sol b/test/integration/fuzz/lockup-linear/createWithDurations.t.sol index d6194802d..f9d2c440f 100644 --- a/test/integration/fuzz/lockup-linear/createWithDurations.t.sol +++ b/test/integration/fuzz/lockup-linear/createWithDurations.t.sol @@ -20,33 +20,6 @@ contract CreateWithDurations_LockupLinear_Integration_Fuzz_Test is CreateWithDurations_Integration_Shared_Test.setUp(); } - function testFuzz_RevertWhen_CliffDurationCalculationOverflows(uint40 cliffDuration) - external - whenNotDelegateCalled - { - uint40 startTime = getBlockTimestamp(); - cliffDuration = boundUint40(cliffDuration, MAX_UINT40 - startTime + 1 seconds, MAX_UINT40); - - // Calculate the end time. Needs to be "unchecked" to avoid an overflow. - uint40 cliffTime; - unchecked { - cliffTime = startTime + cliffDuration; - } - - // Expect the relevant error to be thrown. - vm.expectRevert( - abi.encodeWithSelector( - Errors.SablierV2LockupLinear_StartTimeGreaterThanCliffTime.selector, startTime, cliffTime - ) - ); - - // Set the total duration to be the same as the cliff duration. - uint40 totalDuration = cliffDuration; - - // Create the stream. - createDefaultStreamWithDurations(LockupLinear.Durations({ cliff: cliffDuration, total: totalDuration })); - } - function testFuzz_RevertWhen_TotalDurationCalculationOverflows(LockupLinear.Durations memory durations) external whenNotDelegateCalled @@ -56,7 +29,7 @@ contract CreateWithDurations_LockupLinear_Integration_Fuzz_Test is durations.cliff = boundUint40(durations.cliff, 0, MAX_UINT40 - startTime); durations.total = boundUint40(durations.total, MAX_UINT40 - startTime + 1 seconds, MAX_UINT40); - // Calculate the cliff time and the end time. Needs to be "unchecked" to avoid an overflow. + // Calculate the cliff time and the end time. Needs to be "unchecked" to allow an overflow. uint40 cliffTime; uint40 endTime; unchecked { @@ -67,7 +40,7 @@ contract CreateWithDurations_LockupLinear_Integration_Fuzz_Test is // Expect the relevant error to be thrown. vm.expectRevert( abi.encodeWithSelector( - Errors.SablierV2LockupLinear_CliffTimeNotLessThanEndTime.selector, cliffTime, endTime + Errors.SablierV2LockupLinear_StartTimeNotLessThanEndTime.selector, startTime, endTime ) ); @@ -81,7 +54,7 @@ contract CreateWithDurations_LockupLinear_Integration_Fuzz_Test is whenCliffDurationCalculationDoesNotOverflow whenTotalDurationCalculationDoesNotOverflow { - durations.total = boundUint40(durations.total, 0, MAX_UNIX_TIMESTAMP); + durations.total = boundUint40(durations.total, 1, MAX_UNIX_TIMESTAMP); vm.assume(durations.cliff < durations.total); // Make the Sender the stream's funder (recall that the Sender is the default caller). @@ -103,7 +76,7 @@ contract CreateWithDurations_LockupLinear_Integration_Fuzz_Test is // Create the range struct by calculating the start time, cliff time and the end time. LockupLinear.Range memory range = LockupLinear.Range({ start: getBlockTimestamp(), - cliff: getBlockTimestamp() + durations.cliff, + cliff: durations.cliff == 0 ? 0 : getBlockTimestamp() + durations.cliff, end: getBlockTimestamp() + durations.total }); @@ -126,8 +99,8 @@ contract CreateWithDurations_LockupLinear_Integration_Fuzz_Test is createDefaultStreamWithDurations(durations); // Assert that the stream has been created. - LockupLinear.Stream memory actualStream = lockupLinear.getStream(streamId); - LockupLinear.Stream memory expectedStream = defaults.lockupLinearStream(); + LockupLinear.StreamLL memory actualStream = lockupLinear.getStream(streamId); + LockupLinear.StreamLL memory expectedStream = defaults.lockupLinearStream(); expectedStream.cliffTime = range.cliff; expectedStream.endTime = range.end; expectedStream.startTime = range.start; diff --git a/test/integration/fuzz/lockup-linear/createWithTimestamps.t.sol b/test/integration/fuzz/lockup-linear/createWithTimestamps.t.sol index 0e2e23baa..641e6bc50 100644 --- a/test/integration/fuzz/lockup-linear/createWithTimestamps.t.sol +++ b/test/integration/fuzz/lockup-linear/createWithTimestamps.t.sol @@ -28,10 +28,10 @@ contract CreateWithTimestamps_LockupLinear_Integration_Fuzz_Test is whenRecipientNonZeroAddress whenDepositAmountNotZero { - startTime = boundUint40(startTime, defaults.CLIFF_TIME() + 1 seconds, MAX_UNIX_TIMESTAMP); + startTime = boundUint40(startTime, defaults.CLIFF_TIME() + 1 seconds, defaults.END_TIME() - 1 seconds); vm.expectRevert( abi.encodeWithSelector( - Errors.SablierV2LockupLinear_StartTimeGreaterThanCliffTime.selector, startTime, defaults.CLIFF_TIME() + Errors.SablierV2LockupLinear_StartTimeNotLessThanCliffTime.selector, startTime, defaults.CLIFF_TIME() ) ); createDefaultStreamWithStartTime(startTime); @@ -48,7 +48,7 @@ contract CreateWithTimestamps_LockupLinear_Integration_Fuzz_Test is whenStartTimeNotGreaterThanCliffTime { uint40 startTime = defaults.START_TIME(); - endTime = boundUint40(endTime, startTime, startTime + 2 weeks); + endTime = boundUint40(endTime, startTime + 1, startTime + 2 weeks); cliffTime = boundUint40(cliffTime, endTime, MAX_UNIX_TIMESTAMP); vm.expectRevert( @@ -143,7 +143,7 @@ contract CreateWithTimestamps_LockupLinear_Integration_Fuzz_Test is vm.assume(params.totalAmount != 0); params.range.start = boundUint40(params.range.start, defaults.START_TIME(), defaults.START_TIME() + 10_000 seconds); - params.range.cliff = boundUint40(params.range.cliff, params.range.start, params.range.start + 52 weeks); + params.range.cliff = boundUint40(params.range.cliff, params.range.start + 1, params.range.start + 52 weeks); params.range.end = boundUint40(params.range.end, params.range.cliff + 1 seconds, MAX_UNIX_TIMESTAMP); params.broker.fee = _bound(params.broker.fee, 0, MAX_FEE); protocolFee = _bound(protocolFee, 0, MAX_FEE); @@ -210,7 +210,7 @@ contract CreateWithTimestamps_LockupLinear_Integration_Fuzz_Test is ); // Assert that the stream has been created. - LockupLinear.Stream memory actualStream = lockupLinear.getStream(streamId); + LockupLinear.StreamLL memory actualStream = lockupLinear.getStream(streamId); assertEq(actualStream.amounts, Lockup.Amounts(vars.createAmounts.deposit, 0, 0)); assertEq(actualStream.asset, dai, "asset"); assertEq(actualStream.cliffTime, params.range.cliff, "cliffTime"); diff --git a/test/invariant/LockupDynamic.t.sol b/test/invariant/LockupDynamic.t.sol index 811db9f30..415586a7e 100644 --- a/test/invariant/LockupDynamic.t.sol +++ b/test/invariant/LockupDynamic.t.sol @@ -64,7 +64,7 @@ contract LockupDynamic_Invariant_Test is Lockup_Invariant_Test { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); - LockupDynamic.Stream memory stream = lockupDynamic.getStream(streamId); + LockupDynamic.StreamLD memory stream = lockupDynamic.getStream(streamId); assertNotEq(stream.amounts.deposited, 0, "Invariant violated: stream non-null, deposited amount zero"); } } @@ -74,7 +74,7 @@ contract LockupDynamic_Invariant_Test is Lockup_Invariant_Test { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); - LockupDynamic.Stream memory stream = lockupDynamic.getStream(streamId); + LockupDynamic.StreamLD memory stream = lockupDynamic.getStream(streamId); assertNotEq(stream.endTime, 0, "Invariant violated: end time zero"); } } diff --git a/test/invariant/LockupLinear.t.sol b/test/invariant/LockupLinear.t.sol index adb8b5db2..84ac74858 100644 --- a/test/invariant/LockupLinear.t.sol +++ b/test/invariant/LockupLinear.t.sol @@ -58,16 +58,18 @@ contract LockupLinear_Invariant_Test is Lockup_Invariant_Test { INVARIANTS //////////////////////////////////////////////////////////////////////////*/ - /// @dev The cliff time must not be less than the start time. - function invariant_CliffTimeGteStartTime() external useCurrentTimestamp { + /// @dev The cliff time must be greater than the start time, if it is not zero. + function invariant_CliffTimeGtStartTimeOrZero() external useCurrentTimestamp { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); - assertGte( - lockupLinear.getCliffTime(streamId), - lockupLinear.getStartTime(streamId), - "Invariant violated: cliff time < start time" - ); + if (lockupLinear.getCliffTime(streamId) > 0) { + assertGt( + lockupLinear.getCliffTime(streamId), + lockupLinear.getStartTime(streamId), + "Invariant violated: cliff time <= start time" + ); + } } } @@ -76,7 +78,7 @@ contract LockupLinear_Invariant_Test is Lockup_Invariant_Test { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); - LockupLinear.Stream memory stream = lockupLinear.getStream(streamId); + LockupLinear.StreamLL memory stream = lockupLinear.getStream(streamId); assertNotEq(stream.amounts.deposited, 0, "Invariant violated: stream non-null, deposited amount zero"); } } @@ -99,7 +101,7 @@ contract LockupLinear_Invariant_Test is Lockup_Invariant_Test { uint256 lastStreamId = lockupStore.lastStreamId(); for (uint256 i = 0; i < lastStreamId; ++i) { uint256 streamId = lockupStore.streamIds(i); - LockupLinear.Stream memory stream = lockupLinear.getStream(streamId); + LockupLinear.StreamLL memory stream = lockupLinear.getStream(streamId); assertNotEq(stream.endTime, 0, "Invariant violated: stream non-null, end time zero"); } } diff --git a/test/invariant/handlers/LockupLinearCreateHandler.sol b/test/invariant/handlers/LockupLinearCreateHandler.sol index 940b771ea..5aab6bfa6 100644 --- a/test/invariant/handlers/LockupLinearCreateHandler.sol +++ b/test/invariant/handlers/LockupLinearCreateHandler.sol @@ -94,8 +94,8 @@ contract LockupLinearCreateHandler is BaseHandler { uint40 currentTime = getBlockTimestamp(); params.broker.fee = _bound(params.broker.fee, 0, MAX_FEE); - params.range.start = boundUint40(params.range.start, 0, currentTime); - params.range.cliff = boundUint40(params.range.cliff, params.range.start, params.range.start + 52 weeks); + params.range.start = boundUint40(params.range.start, 1, currentTime); + params.range.cliff = boundUint40(params.range.cliff, params.range.start + 1, params.range.start + 52 weeks); params.totalAmount = boundUint128(params.totalAmount, 1, 1_000_000_000e18); // Bound the end time so that it is always greater than both the current time and the cliff time (this is diff --git a/test/utils/Assertions.sol b/test/utils/Assertions.sol index 86b14ef04..05e6761b2 100644 --- a/test/utils/Assertions.sol +++ b/test/utils/Assertions.sol @@ -40,7 +40,7 @@ abstract contract Assertions is PRBTest, PRBMathAssertions { } /// @dev Compares two {LockupLinear.Stream} struct entities. - function assertEq(LockupLinear.Stream memory a, LockupLinear.Stream memory b) internal { + function assertEq(LockupLinear.StreamLL memory a, LockupLinear.StreamLL memory b) internal { assertEq(a.amounts, b.amounts); assertEq(a.asset, b.asset, "asset"); assertEq(a.cliffTime, b.cliffTime, "cliffTime"); @@ -55,7 +55,7 @@ abstract contract Assertions is PRBTest, PRBMathAssertions { } /// @dev Compares two {LockupDynamic.Stream} struct entities. - function assertEq(LockupDynamic.Stream memory a, LockupDynamic.Stream memory b) internal { + function assertEq(LockupDynamic.StreamLD memory a, LockupDynamic.StreamLD memory b) internal { assertEq(a.asset, b.asset, "asset"); assertEq(a.endTime, b.endTime, "endTime"); assertEq(a.isCancelable, b.isCancelable, "isCancelable"); diff --git a/test/utils/Defaults.sol b/test/utils/Defaults.sol index c190854dc..7fad153ec 100644 --- a/test/utils/Defaults.sol +++ b/test/utils/Defaults.sol @@ -91,8 +91,8 @@ contract Defaults is Constants { return LockupDynamic.Range({ start: START_TIME, end: END_TIME }); } - function lockupDynamicStream() public view returns (LockupDynamic.Stream memory) { - return LockupDynamic.Stream({ + function lockupDynamicStream() public view returns (LockupDynamic.StreamLD memory) { + return LockupDynamic.StreamLD({ amounts: lockupAmounts(), asset: asset, endTime: END_TIME, @@ -111,8 +111,8 @@ contract Defaults is Constants { return LockupLinear.Range({ start: START_TIME, cliff: CLIFF_TIME, end: END_TIME }); } - function lockupLinearStream() public view returns (LockupLinear.Stream memory) { - return LockupLinear.Stream({ + function lockupLinearStream() public view returns (LockupLinear.StreamLL memory) { + return LockupLinear.StreamLL({ amounts: lockupAmounts(), asset: asset, cliffTime: CLIFF_TIME,