Skip to content

Commit

Permalink
build: install solmate library
Browse files Browse the repository at this point in the history
feat: inherit "ERC721" in "SablierV2Linear"
feat: inherit "ERC721" in "SablierV2Pro"
feat: mint NFT in the "_create" function
feat: add "isApprovedOrOwner" function
feat: change modifier name to "isAuthorizedForStream"
feat: add "tokenURI" function
perf: remove the recipient address from the "Stream" struct
refactor: swap the order of the checks in the "withdrawTo" function
test: change the struct recipient with the "users.recipient"
test: make recipient the default caller in the "cancel" function
test: order correctly the branches in the "cancel" tree
test: order correctly the branches in the "cancelAll" tree
test: order correctly the branches in the "withdraw" tree
test: when the caller is an approved third party in the "cancel" function
test: when the caller is an approved third party in the "cancelAll" funtion
test: when the caller is an approved third party in the "withdraw" function
test: when the caller is an approved third party in the "withdrawAll" function
test: when the caller is an approved third party in the "withdrawAllTo" function
test: when the caller is an approved third party in the "withdtawTo" function
test: when the recipient is no longer the owner of the stream in the "cancel" function
test: when the recipient is no longer the owner of the stream in the "withdraw" function
test: when the recipient is no longer the owner of the streams in the "cancelAll" function
test: when the recipient is no longer the owner of the streams in the "withdrawAll" function
test: when the recipient is no longer the owner of the streams in the "withdrawAllTo" function
test: when the recipient is no longer the owner of the stream in the "withrawTo" function
test: remove the local "withdrawAmount" variable from the test functions
test: change the new owner of the streams to "eve" when using "safeTransferFrom" function
test: "isApprovedOrOwner" function in the "SablierV2Linear" contract
test: "isApprovedOrOwner" function in the "SablierV2Pro" contract
test: correct comment about return value for the "isCancelable" function
  • Loading branch information
andreivladbrg committed Aug 15, 2022
1 parent 490f601 commit 2a4e756
Show file tree
Hide file tree
Showing 38 changed files with 1,078 additions and 347 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@
branch = "refactor/foundry"
path = "lib/prb-contracts"
url = "https://github.com/paulrberg/prb-contracts"
[submodule "lib/solmate"]
path = "lib/solmate"
url = "https://github.com/transmissions11/solmate"
1 change: 1 addition & 0 deletions lib/solmate
Submodule solmate added at 9cf142
1 change: 1 addition & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@prb/contracts/=lib/prb-contracts/src/
@prb/math/=lib/prb-math/contracts/
@sablier/v2-core/=src/
@solmate/tokens/=lib/solmate/src/tokens/
forge-std/=lib/forge-std/src/
33 changes: 19 additions & 14 deletions src/SablierV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ abstract contract SablierV2 is ISablierV2 {
MODIFIERS
//////////////////////////////////////////////////////////////////////////*/

/// @notice Checks that `msg.sender` is either the sender or the recipient of the stream.
modifier onlySenderOrRecipient(uint256 streamId) {
if (msg.sender != getSender(streamId) && msg.sender != getRecipient(streamId)) {
/// @notice Checks that `msg.sender` is either the sender or the recipient of the stream either approved.
modifier isAuthorizedForStream(uint256 streamId) {
if (msg.sender != getSender(streamId) && !isApprovedOrOwner(streamId)) {
revert SablierV2__Unauthorized(streamId, msg.sender);
}
_;
Expand Down Expand Up @@ -54,6 +54,9 @@ abstract contract SablierV2 is ISablierV2 {
/// @inheritdoc ISablierV2
function getSender(uint256 streamId) public view virtual override returns (address sender);

/// @inheritdoc ISablierV2
function isApprovedOrOwner(uint256 streamId) public view virtual override returns (bool);

/// @inheritdoc ISablierV2
function isCancelable(uint256 streamId) public view virtual override returns (bool cancelable);

Expand Down Expand Up @@ -110,7 +113,7 @@ abstract contract SablierV2 is ISablierV2 {
function withdraw(uint256 streamId, uint256 amount)
external
streamExists(streamId)
onlySenderOrRecipient(streamId)
isAuthorizedForStream(streamId)
{
address to = getRecipient(streamId);
_withdraw(streamId, to, amount);
Expand All @@ -128,16 +131,18 @@ abstract contract SablierV2 is ISablierV2 {
// Iterate over the provided array of stream ids and withdraw from each stream.
address recipient;
address sender;
bool isApprovedOrOwner_;
uint256 streamId;
for (uint256 i = 0; i < streamIdsCount; ) {
streamId = streamIds[i];

// If the `streamId` points to a stream that does not exist, skip it.
isApprovedOrOwner_ = isApprovedOrOwner(streamId);
recipient = getRecipient(streamId);
sender = getSender(streamId);
if (sender != address(0)) {
// Checks: the `msg.sender` is either the sender or the recipient of the stream.
if (msg.sender != sender && msg.sender != recipient) {
// Checks: the `msg.sender` is either the sender or the recipient of the stream either approved.
if (msg.sender != sender && !isApprovedOrOwner_) {
revert SablierV2__Unauthorized(streamId, msg.sender);
}

Expand Down Expand Up @@ -177,8 +182,8 @@ abstract contract SablierV2 is ISablierV2 {

// If the `streamId` points to a stream that does not exist, skip it.
if (getSender(streamId) != address(0)) {
// Checks: the `msg.sender` is the recipient of the stream.
if (msg.sender != getRecipient(streamId)) {
// Checks: the `msg.sender` is either the recipient of the stream or approved.
if (!isApprovedOrOwner(streamId)) {
revert SablierV2__Unauthorized(streamId, msg.sender);
}

Expand All @@ -199,15 +204,15 @@ abstract contract SablierV2 is ISablierV2 {
address to,
uint256 amount
) external streamExists(streamId) {
// Checks: the `msg.sender` is the recipient of the stream.
if (msg.sender != getRecipient(streamId)) {
revert SablierV2__Unauthorized(streamId, msg.sender);
}

// Checks: the provided address to withdraw to is not zero.
if (to == address(0)) {
revert SablierV2__WithdrawZeroAddress();
}

// Checks: the `msg.sender` is either the recipient of the stream or is approved.
if (!isApprovedOrOwner(streamId)) {
revert SablierV2__Unauthorized(streamId, msg.sender);
}
_withdraw(streamId, to, amount);
}

Expand All @@ -216,7 +221,7 @@ abstract contract SablierV2 is ISablierV2 {
//////////////////////////////////////////////////////////////////////////*/

/// @dev Checks the basic requiremenets for the `create` function.
function _checkCreateArguments(
function checkCreateArguments(
address sender,
address recipient,
uint256 depositAmount,
Expand Down
31 changes: 24 additions & 7 deletions src/SablierV2Linear.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: LGPL-3.0
pragma solidity >=0.8.13;

import { ERC721 } from "@solmate/tokens/ERC721.sol";
import { IERC20 } from "@prb/contracts/token/erc20/IERC20.sol";
import { SafeERC20 } from "@prb/contracts/token/erc20/SafeERC20.sol";
import { UD60x18, toUD60x18 } from "@prb/math/UD60x18.sol";
Expand All @@ -12,8 +13,9 @@ import { SablierV2 } from "./SablierV2.sol";
/// @title SablierV2Linear
/// @author Sablier Labs Ltd.
contract SablierV2Linear is
ERC721("Sablier V2 Linear", "SAB-V2-LIN"), // one dependency
ISablierV2Linear, // one dependency
SablierV2 // one dependency
SablierV2 // two dependencies
{
using SafeERC20 for IERC20;

Expand All @@ -40,7 +42,7 @@ contract SablierV2Linear is

/// @inheritdoc ISablierV2
function getRecipient(uint256 streamId) public view override(ISablierV2, SablierV2) returns (address recipient) {
recipient = _streams[streamId].recipient;
recipient = _ownerOf[streamId];
}

/// @inheritdoc ISablierV2
Expand Down Expand Up @@ -117,11 +119,22 @@ contract SablierV2Linear is
withdrawnAmount = _streams[streamId].withdrawnAmount;
}

/// @inheritdoc ISablierV2
function isApprovedOrOwner(uint256 streamId) public view override(ISablierV2, SablierV2) returns (bool) {
address owner = _ownerOf[streamId];
return (msg.sender == owner || isApprovedForAll[owner][msg.sender] || getApproved[streamId] == msg.sender);
}

/// @inheritdoc ISablierV2
function isCancelable(uint256 streamId) public view override(ISablierV2, SablierV2) returns (bool cancelable) {
cancelable = _streams[streamId].cancelable;
}

/// @inheritdoc ERC721
function tokenURI(uint256 streamId) public view override streamExists(streamId) returns (string memory) {
return "";
}

/*//////////////////////////////////////////////////////////////////////////
NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -171,7 +184,7 @@ contract SablierV2Linear is
//////////////////////////////////////////////////////////////////////////*/

/// @dev See the documentation for the public functions that call this internal function.
function _cancel(uint256 streamId) internal override onlySenderOrRecipient(streamId) {
function _cancel(uint256 streamId) internal override isAuthorizedForStream(streamId) {
Stream memory stream = _streams[streamId];

// Calculate the withdraw and the return amounts.
Expand All @@ -181,12 +194,14 @@ contract SablierV2Linear is
returnAmount = stream.depositAmount - stream.withdrawnAmount - withdrawAmount;
}

address recipient = getRecipient(streamId);

// Effects: delete the stream from storage.
delete _streams[streamId];

// Interactions: withdraw the tokens to the recipient, if any.
if (withdrawAmount > 0) {
stream.token.safeTransfer(stream.recipient, withdrawAmount);
stream.token.safeTransfer(recipient, withdrawAmount);
}

// Interactions: return the tokens to the sender, if any.
Expand All @@ -195,7 +210,7 @@ contract SablierV2Linear is
}

// Emit an event.
emit Cancel(streamId, stream.recipient, withdrawAmount, returnAmount);
emit Cancel(streamId, recipient, withdrawAmount, returnAmount);
}

/// @dev See the documentation for the public functions that call this internal function.
Expand All @@ -210,7 +225,7 @@ contract SablierV2Linear is
bool cancelable
) internal returns (uint256 streamId) {
// Checks: the common requirements for the `create` function arguments.
_checkCreateArguments(sender, recipient, depositAmount, startTime, stopTime);
checkCreateArguments(sender, recipient, depositAmount, startTime, stopTime);

// Checks: the cliff time is greater than or equal to the start time.
if (startTime > cliffTime) {
Expand All @@ -228,14 +243,16 @@ contract SablierV2Linear is
cancelable: cancelable,
cliffTime: cliffTime,
depositAmount: depositAmount,
recipient: recipient,
sender: sender,
startTime: startTime,
stopTime: stopTime,
token: token,
withdrawnAmount: 0
});

// Effects: mint the NFT to the recipient's address.
_mint(recipient, streamId);

// Effects: bump the next stream id.
// We're using unchecked arithmetic here because this cannot realistically overflow, ever.
unchecked {
Expand Down
37 changes: 27 additions & 10 deletions src/SablierV2Pro.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: LGPL-3.0
pragma solidity >=0.8.13;

import { ERC721 } from "@solmate/tokens/ERC721.sol";
import { IERC20 } from "@prb/contracts/token/erc20/IERC20.sol";
import { SafeERC20 } from "@prb/contracts/token/erc20/SafeERC20.sol";
import { SCALE, SD59x18, toSD59x18, ZERO } from "@prb/math/SD59x18.sol";
Expand All @@ -12,6 +13,7 @@ import { SablierV2 } from "./SablierV2.sol";
/// @title SablierV2Pro
/// @author Sablier Labs Ltd.
contract SablierV2Pro is
ERC721("Sablier V2 Pro", "SAB-V2-PRO"), // one dependency
ISablierV2Pro, // one dependency
SablierV2 // two dependencies
{
Expand Down Expand Up @@ -53,7 +55,7 @@ contract SablierV2Pro is

/// @inheritdoc ISablierV2
function getRecipient(uint256 streamId) public view override(ISablierV2, SablierV2) returns (address recipient) {
recipient = _streams[streamId].recipient;
recipient = _ownerOf[streamId];
}

/// @inheritdoc ISablierV2
Expand Down Expand Up @@ -197,11 +199,22 @@ contract SablierV2Pro is
withdrawnAmount = _streams[streamId].withdrawnAmount;
}

/// @inheritdoc ISablierV2
function isApprovedOrOwner(uint256 streamId) public view override(ISablierV2, SablierV2) returns (bool) {
address owner = _ownerOf[streamId];
return (msg.sender == owner || isApprovedForAll[owner][msg.sender] || getApproved[streamId] == msg.sender);
}

/// @inheritdoc ISablierV2
function isCancelable(uint256 streamId) public view override(ISablierV2, SablierV2) returns (bool cancelable) {
cancelable = _streams[streamId].cancelable;
}

/// @inheritdoc ERC721
function tokenURI(uint256 streamId) public view override streamExists(streamId) returns (string memory) {
return "";
}

/*//////////////////////////////////////////////////////////////////////////
PUBLIC NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -278,7 +291,7 @@ contract SablierV2Pro is
/// @dev Checks that the counts of segments match. The counts must be equal and less than or equal to
/// the maximum segment count permitted in Sablier.
/// @return segmentCount The count of the segments.
function _checkSegmentCounts(
function checkSegmentCounts(
uint256[] memory segmentAmounts,
SD59x18[] memory segmentExponents,
uint256[] memory segmentMilestones
Expand Down Expand Up @@ -316,7 +329,7 @@ contract SablierV2Pro is
/// 2. The milestones are ordered chronologically.
/// 3. The exponents are within the bounds permitted in Sablier.
/// 4. The deposit amount is equal to the segment amounts summed up.
function _checkSegments(
function checkSegments(
uint256 depositAmount,
uint256 startTime,
uint256[] memory segmentAmounts,
Expand Down Expand Up @@ -373,7 +386,7 @@ contract SablierV2Pro is
//////////////////////////////////////////////////////////////////////////*/

/// @dev See the documentation for the public functions that call this internal function.
function _cancel(uint256 streamId) internal override onlySenderOrRecipient(streamId) {
function _cancel(uint256 streamId) internal override isAuthorizedForStream(streamId) {
Stream memory stream = _streams[streamId];

// Calculate the withdraw and the return amounts.
Expand All @@ -383,12 +396,14 @@ contract SablierV2Pro is
returnAmount = stream.depositAmount - stream.withdrawnAmount - withdrawAmount;
}

address recipient = getRecipient(streamId);

// Effects: delete the stream from storage.
delete _streams[streamId];

// Interactions: withdraw the tokens to the recipient, if any.
if (withdrawAmount > 0) {
stream.token.safeTransfer(stream.recipient, withdrawAmount);
stream.token.safeTransfer(recipient, withdrawAmount);
}

// Interactions: return the tokens to the sender, if any.
Expand All @@ -397,7 +412,7 @@ contract SablierV2Pro is
}

// Emit an event.
emit Cancel(streamId, stream.recipient, withdrawAmount, returnAmount);
emit Cancel(streamId, recipient, withdrawAmount, returnAmount);
}

/// @dev See the documentation for the public functions that call this internal function.
Expand All @@ -413,23 +428,22 @@ contract SablierV2Pro is
bool cancelable
) internal returns (uint256 streamId) {
// Checks: segment counts match.
uint256 segmentCount = _checkSegmentCounts(segmentAmounts, segmentExponents, segmentMilestones);
uint256 segmentCount = checkSegmentCounts(segmentAmounts, segmentExponents, segmentMilestones);

// Imply the stop time from the last segment milestone.
uint256 stopTime = segmentMilestones[segmentCount - 1];

// Checks: the common requirements for the `create` function arguments.
_checkCreateArguments(sender, recipient, depositAmount, startTime, stopTime);
checkCreateArguments(sender, recipient, depositAmount, startTime, stopTime);

// Checks: requirements of segments variables.
_checkSegments(depositAmount, startTime, segmentAmounts, segmentExponents, segmentMilestones, segmentCount);
checkSegments(depositAmount, startTime, segmentAmounts, segmentExponents, segmentMilestones, segmentCount);

// Effects: create and store the stream.
streamId = nextStreamId;
_streams[streamId] = Stream({
cancelable: cancelable,
depositAmount: depositAmount,
recipient: recipient,
segmentAmounts: segmentAmounts,
segmentExponents: segmentExponents,
segmentMilestones: segmentMilestones,
Expand All @@ -440,6 +454,9 @@ contract SablierV2Pro is
withdrawnAmount: 0
});

// Effects: mint the NFT to the recipient's address.
_mint(recipient, streamId);

// Effects: bump the next stream id. This cannot realistically overflow, ever.
unchecked {
nextStreamId = streamId + 1;
Expand Down
4 changes: 4 additions & 0 deletions src/interfaces/ISablierV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ interface ISablierV2 {
/// @return withdrawnAmount The amount withdrawn from the stream, in units of the ERC-20 token's decimals.
function getWithdrawnAmount(uint256 streamId) external view returns (uint256 withdrawnAmount);

/// @notice Returns whether the `msg.sender` is authorized to manage `streamId` or not.
/// @param streamId The id of the stream to make the query for.
function isApprovedOrOwner(uint256 streamId) external view returns (bool);

/// @notice Checks whether the stream is cancelable or not.
/// @param streamId The id of the stream to make the query for.
/// @return cancelable Whether the stream is cancelable or not.
Expand Down
1 change: 0 additions & 1 deletion src/interfaces/ISablierV2Linear.sol
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ interface ISablierV2Linear is ISablierV2 {
uint256 startTime;
uint256 stopTime;
uint256 withdrawnAmount;
address recipient;
address sender;
IERC20 token;
bool cancelable;
Expand Down
1 change: 0 additions & 1 deletion src/interfaces/ISablierV2Pro.sol
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ interface ISablierV2Pro is ISablierV2 {
uint256 startTime;
uint256 stopTime;
uint256 withdrawnAmount;
address recipient;
address sender;
IERC20 token;
bool cancelable;
Expand Down
6 changes: 6 additions & 0 deletions test/unit/abstract-sablier-v2/AbstractSablierV2UnitTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ contract AbstractSablierV2 is SablierV2 {
cancelable;
}

/// @inheritdoc ISablierV2
function isApprovedOrOwner(uint256 streamId) public pure override returns (bool result) {
streamId;
result;
}

/*//////////////////////////////////////////////////////////////////////////
INTERNAL NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down
Loading

0 comments on commit 2a4e756

Please sign in to comment.