Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add "withdrawMaxAndTransfer" #561

Merged
merged 4 commits into from
Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,9 @@
"editor.formatOnSave": true,
"npm.exclude": "**/lib/**",
"prettier.documentSelectors": ["**/*.svg"],
"search.exclude": {
"**/node_modules": true,
"lib": true
},
"solidity.formatter": "forge"
}
40 changes: 18 additions & 22 deletions src/SablierV2LockupDynamic.sol
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ contract SablierV2LockupDynamic is
//////////////////////////////////////////////////////////////////////////*/

/// @dev Calculates the streamed amount without looking up the stream's status.
function _calculateStreamedAmount(uint256 streamId) internal view returns (uint128 streamedAmount) {
function _calculateStreamedAmount(uint256 streamId) internal view returns (uint128) {
// If the start time is in the future, return zero.
uint40 currentTime = uint40(block.timestamp);
if (_streams[streamId].startTime >= currentTime) {
Expand All @@ -325,10 +325,10 @@ contract SablierV2LockupDynamic is

if (_streams[streamId].segments.length > 1) {
// If there is more than one segment, it may be necessary to iterate over all of them.
streamedAmount = _calculateStreamedAmountForMultipleSegments(streamId);
return _calculateStreamedAmountForMultipleSegments(streamId);
} else {
// Otherwise, there is only one segment, and the calculation is simpler.
streamedAmount = _calculateStreamedAmountForOneSegment(streamId);
return _calculateStreamedAmountForOneSegment(streamId);
}
}

Expand All @@ -340,11 +340,7 @@ contract SablierV2LockupDynamic is
/// 2. The stream's start time must be in the past so that the calculations below do not overflow.
/// 3. The stream's end time must be in the future so that the the loop below does not panic with an "index out of
/// bounds" error.
function _calculateStreamedAmountForMultipleSegments(uint256 streamId)
internal
view
returns (uint128 streamedAmount)
{
function _calculateStreamedAmountForMultipleSegments(uint256 streamId) internal view returns (uint128) {
unchecked {
uint40 currentTime = uint40(block.timestamp);
LockupDynamic.Stream memory stream = _streams[streamId];
Expand Down Expand Up @@ -397,13 +393,13 @@ contract SablierV2LockupDynamic is

// Calculate the total streamed amount by adding the previous segment amounts and the amount streamed in
// the current segment. Casting to uint128 is safe due to the if statement above.
streamedAmount = previousSegmentAmounts + uint128(segmentStreamedAmount.intoUint256());
return previousSegmentAmounts + uint128(segmentStreamedAmount.intoUint256());
}
}

/// @dev Calculates the streamed amount for a a stream with one segment. Normalization to 18 decimals is not
/// needed because there is no mix of amounts with different decimals.
function _calculateStreamedAmountForOneSegment(uint256 streamId) internal view returns (uint128 streamedAmount) {
function _calculateStreamedAmountForOneSegment(uint256 streamId) internal view returns (uint128) {
unchecked {
// Calculate how much time has passed since the stream started, and the stream's total duration.
SD59x18 elapsedTime = (uint40(block.timestamp) - _streams[streamId].startTime).intoSD59x18();
Expand All @@ -418,27 +414,27 @@ contract SablierV2LockupDynamic is

// Calculate the streamed amount using the special formula.
SD59x18 multiplier = elapsedTimePercentage.pow(exponent);
SD59x18 streamedAmountSd = multiplier.mul(depositedAmount);
SD59x18 streamedAmount = multiplier.mul(depositedAmount);

// Although the streamed amount should never exceed the deposited amount, this condition is checked
// without asserting to avoid locking funds in case of a bug. If this situation occurs, the withdrawn
// amount is considered to be the streamed amount, and the stream is effectively frozen.
if (streamedAmountSd.gt(depositedAmount)) {
if (streamedAmount.gt(depositedAmount)) {
return _streams[streamId].amounts.withdrawn;
}

// Cast the streamed amount to uint128. This is safe due to the check above.
streamedAmount = uint128(streamedAmountSd.intoUint256());
return uint128(streamedAmount.intoUint256());
}
}

/// @inheritdoc SablierV2Lockup
function _isCallerStreamSender(uint256 streamId) internal view override returns (bool result) {
result = msg.sender == _streams[streamId].sender;
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 status) {
function _statusOf(uint256 streamId) internal view override returns (Lockup.Status) {
if (_streams[streamId].isDepleted) {
return Lockup.Status.DEPLETED;
} else if (_streams[streamId].wasCanceled) {
Expand All @@ -450,14 +446,14 @@ contract SablierV2LockupDynamic is
}

if (_calculateStreamedAmount(streamId) < _streams[streamId].amounts.deposited) {
status = Lockup.Status.STREAMING;
return Lockup.Status.STREAMING;
} else {
status = Lockup.Status.SETTLED;
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 streamedAmount) {
function _streamedAmountOf(uint256 streamId) internal view returns (uint128) {
Lockup.Amounts memory amounts = _streams[streamId].amounts;

if (_streams[streamId].isDepleted) {
Expand All @@ -466,12 +462,12 @@ contract SablierV2LockupDynamic is
return amounts.deposited - amounts.refunded;
}

streamedAmount = _calculateStreamedAmount(streamId);
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 withdrawableAmount) {
withdrawableAmount = _streamedAmountOf(streamId) - _streams[streamId].amounts.withdrawn;
function _withdrawableAmountOf(uint256 streamId) internal view override returns (uint128) {
return _streamedAmountOf(streamId) - _streams[streamId].amounts.withdrawn;
}

/*//////////////////////////////////////////////////////////////////////////
Expand Down
26 changes: 13 additions & 13 deletions src/SablierV2LockupLinear.sol
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ contract SablierV2LockupLinear is
//////////////////////////////////////////////////////////////////////////*/

/// @dev Calculates the streamed amount without looking up the stream's status.
function _calculateStreamedAmount(uint256 streamId) internal view returns (uint128 streamedAmount) {
function _calculateStreamedAmount(uint256 streamId) internal view returns (uint128) {
// If the cliff time is in the future, return zero.
uint256 cliffTime = uint256(_streams[streamId].cliffTime);
uint256 currentTime = block.timestamp;
Expand Down Expand Up @@ -329,27 +329,27 @@ contract SablierV2LockupLinear is
UD60x18 depositedAmount = ud(_streams[streamId].amounts.deposited);

// Calculate the streamed amount by multiplying the elapsed time percentage by the deposited amount.
UD60x18 streamedAmountUd = elapsedTimePercentage.mul(depositedAmount);
UD60x18 streamedAmount = elapsedTimePercentage.mul(depositedAmount);

// Although the streamed amount should never exceed the deposited amount, this condition is checked
// without asserting to avoid locking funds in case of a bug. If this situation occurs, the withdrawn
// amount is considered to be the streamed amount, and the stream is effectively frozen.
if (streamedAmountUd.gt(depositedAmount)) {
if (streamedAmount.gt(depositedAmount)) {
return _streams[streamId].amounts.withdrawn;
}

// Cast the streamed amount to uint128. This is safe due to the check above.
streamedAmount = uint128(streamedAmountUd.intoUint256());
return uint128(streamedAmount.intoUint256());
}
}

/// @inheritdoc SablierV2Lockup
function _isCallerStreamSender(uint256 streamId) internal view override returns (bool result) {
result = msg.sender == _streams[streamId].sender;
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 status) {
function _statusOf(uint256 streamId) internal view override returns (Lockup.Status) {
if (_streams[streamId].isDepleted) {
return Lockup.Status.DEPLETED;
} else if (_streams[streamId].wasCanceled) {
Expand All @@ -361,14 +361,14 @@ contract SablierV2LockupLinear is
}

if (_calculateStreamedAmount(streamId) < _streams[streamId].amounts.deposited) {
status = Lockup.Status.STREAMING;
return Lockup.Status.STREAMING;
} else {
status = Lockup.Status.SETTLED;
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 streamedAmount) {
function _streamedAmountOf(uint256 streamId) internal view returns (uint128) {
Lockup.Amounts memory amounts = _streams[streamId].amounts;

if (_streams[streamId].isDepleted) {
Expand All @@ -377,12 +377,12 @@ contract SablierV2LockupLinear is
return amounts.deposited - amounts.refunded;
}

streamedAmount = _calculateStreamedAmount(streamId);
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 withdrawableAmount) {
withdrawableAmount = _streamedAmountOf(streamId) - _streams[streamId].amounts.withdrawn;
function _withdrawableAmountOf(uint256 streamId) internal view override returns (uint128) {
return _streamedAmountOf(streamId) - _streams[streamId].amounts.withdrawn;
}

/*//////////////////////////////////////////////////////////////////////////
Expand Down
40 changes: 28 additions & 12 deletions src/abstracts/SablierV2Lockup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ abstract contract SablierV2Lockup is

/// @inheritdoc ISablierV2Lockup
function burn(uint256 streamId) external override noDelegateCall {
// Checks: only depleted streams can be burned.
// Checks: only depleted streams can be burned. This also checks that the stream is not null.
if (!isDepleted(streamId)) {
revert Errors.SablierV2Lockup_StreamNotDepleted(streamId);
}
Expand All @@ -120,7 +120,7 @@ abstract contract SablierV2Lockup is

/// @inheritdoc ISablierV2Lockup
function cancel(uint256 streamId) public override noDelegateCall {
// Checks: the stream is neither depleted nor canceled.
// Checks: the stream is neither depleted nor canceled. This also checks that the stream is not null.
if (isDepleted(streamId)) {
revert Errors.SablierV2Lockup_StreamDepleted(streamId);
} else if (wasCanceled(streamId)) {
Expand Down Expand Up @@ -188,7 +188,7 @@ abstract contract SablierV2Lockup is

/// @inheritdoc ISablierV2Lockup
function withdraw(uint256 streamId, address to, uint128 amount) public override noDelegateCall {
// Checks: the stream is not depleted.
// Checks: the stream is not depleted. This also checks that the stream is not null.
if (isDepleted(streamId)) {
revert Errors.SablierV2Lockup_StreamDepleted(streamId);
}
Expand Down Expand Up @@ -219,7 +219,25 @@ abstract contract SablierV2Lockup is

/// @inheritdoc ISablierV2Lockup
function withdrawMax(uint256 streamId, address to) external override {
withdraw(streamId, to, _withdrawableAmountOf(streamId));
withdraw({ streamId: streamId, to: to, amount: _withdrawableAmountOf(streamId) });
}

/// @inheritdoc ISablierV2Lockup
function withdrawMaxAndTransfer(uint256 streamId, address newRecipient) external override notNull(streamId) {
// Checks: the caller is the current recipient. This also checks that the NFT was not burned.
address currentRecipient = _ownerOf(streamId);
if (msg.sender != currentRecipient) {
revert Errors.SablierV2Lockup_Unauthorized(streamId, msg.sender);
}

// Skip the withdrawal if the withdrawable amount is zero.
uint128 withdrawableAmount = _withdrawableAmountOf(streamId);
if (withdrawableAmount > 0) {
_withdraw({ streamId: streamId, to: currentRecipient, amount: withdrawableAmount });
}

// Checks and Effects: transfer the NFT.
_transfer({ from: currentRecipient, to: newRecipient, tokenId: streamId });
}

/// @inheritdoc ISablierV2Lockup
Expand Down Expand Up @@ -257,23 +275,21 @@ abstract contract SablierV2Lockup is

/// @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 result) {
function _isCallerStreamRecipientOrApproved(uint256 streamId) internal view returns (bool) {
address recipient = _ownerOf(streamId);
result = (
msg.sender == recipient || isApprovedForAll({ owner: recipient, operator: msg.sender })
|| getApproved(streamId) == msg.sender
);
return msg.sender == recipient || isApprovedForAll({ owner: recipient, operator: msg.sender })
|| getApproved(streamId) == msg.sender;
}

/// @notice Checks whether `msg.sender` is the stream's sender.
/// @param streamId The stream id for the query.
function _isCallerStreamSender(uint256 streamId) internal view virtual returns (bool result);
function _isCallerStreamSender(uint256 streamId) internal view virtual returns (bool);

/// @dev Retrieves the stream's status without performing a null check.
function _statusOf(uint256 streamId) internal view virtual returns (Lockup.Status status);
function _statusOf(uint256 streamId) internal view virtual returns (Lockup.Status);

/// @dev See the documentation for the user-facing functions that call this internal function.
function _withdrawableAmountOf(uint256 streamId) internal view virtual returns (uint128 withdrawableAmount);
function _withdrawableAmountOf(uint256 streamId) internal view virtual returns (uint128);

/*//////////////////////////////////////////////////////////////////////////
INTERNAL NON-CONSTANT FUNCTIONS
Expand Down
28 changes: 23 additions & 5 deletions src/interfaces/ISablierV2Lockup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -240,18 +240,18 @@ interface ISablierV2Lockup is
///
/// Requirements:
/// - Must not be delegate called.
/// - `streamId` must not reference a null, pending, or depleted stream.
/// - `streamId` must not reference a null or depleted stream.
/// - `msg.sender` must be the stream's sender, the stream's recipient or an approved third party.
/// - `to` must be the recipient if `msg.sender` is the stream's sender.
/// - `to` must not be the zero address.
/// - `amount` must be greater than zero and must not exceed the withdrawable amount.
///
/// @param streamId The id of the stream to withdraw from.
/// @param to The address that receives the withdrawn assets.
/// @param to The address receiving the withdrawn assets.
/// @param amount The amount to withdraw, denoted in units of the asset's decimals.
function withdraw(uint256 streamId, address to, uint128 amount) external;

/// @notice Withdraws the maximum withdrawable amount from the stream to the `to` address.
/// @notice Withdraws the maximum withdrawable amount from the stream to the provided address `to`.
///
/// @dev Emits a {WithdrawFromLockupStream} and a {Transfer} event.
///
Expand All @@ -262,9 +262,27 @@ interface ISablierV2Lockup is
/// - Refer to the requirements in {withdraw}.
///
/// @param streamId The id of the stream to withdraw from.
/// @param to The address that receives the withdrawn assets.
/// @param to The address receiving the withdrawn assets.
function withdrawMax(uint256 streamId, address to) external;

/// @notice Withdraws the maximum withdrawable amount from the stream to the current recipient, and transfers the
/// NFT to `newRecipient`.
///
/// @dev Emits a {WithdrawFromLockupStream} and a {Transfer} event.
///
/// Notes:
/// - If the withdrawable amount is zero, the withdrawal is skipped.
/// - Refer to the notes in {withdraw}.
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
///
/// Requirements:
/// - `msg.sender` must be the stream's recipient.
/// - Refer to the requirements in {withdraw}.
PaulRBerg marked this conversation as resolved.
Show resolved Hide resolved
/// - Refer to the requirements in {IERC721.transferFrom}.
///
/// @param streamId The id of the stream NFT to transfer.
/// @param newRecipient The address of the new owner of the stream NFT.
function withdrawMaxAndTransfer(uint256 streamId, address newRecipient) external;

/// @notice Withdraws assets from streams to the provided address `to`.
///
/// @dev Emits multiple {WithdrawFromLockupStream} and {Transfer} events.
Expand All @@ -277,7 +295,7 @@ interface ISablierV2Lockup is
/// - There must be an equal number of `streamIds` and `amounts`.
///
/// @param streamIds The ids of the streams to withdraw from.
/// @param to The address that receives the withdrawn assets.
/// @param to The address receiving the withdrawn assets.
/// @param amounts The amounts to withdraw, denoted in units of the asset's decimals.
function withdrawMultiple(uint256[] calldata streamIds, address to, uint128[] calldata amounts) external;
}
11 changes: 2 additions & 9 deletions src/libraries/SVGElements.sol
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,8 @@ library SVGElements {
COMPONENTS
//////////////////////////////////////////////////////////////////////////*/

function card(
CardType cardType,
string memory content
)
internal
pure
returns (uint256 width, string memory card_)
{
(width, card_) = card(cardType, content, "");
function card(CardType cardType, string memory content) internal pure returns (uint256, string memory) {
return card({ cardType: cardType, content: content, circle: "" });
}

function card(
Expand Down
Loading
Loading