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

Extend status enum #467

Merged
merged 28 commits into from
May 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
eecb246
refactor: extend status enum
PaulRBerg Apr 25, 2023
2d8ee1e
refactor: shorten error name
PaulRBerg Apr 25, 2023
68bfd90
feat: add "isStream"
PaulRBerg Apr 25, 2023
96941f6
feat: add "isCold" and "isWarm" utils
PaulRBerg Apr 25, 2023
eaf6bfd
test: simplify directory nesting structure
PaulRBerg Apr 25, 2023
52120a5
test: check status in create function tests
PaulRBerg Apr 27, 2023
a67a38f
refactor: de-dup "withdraw" and "withdrawMultiple"
PaulRBerg Apr 27, 2023
f15e073
refactor: rename error
PaulRBerg Apr 27, 2023
80d0edd
test: improve and simplify "cancelMultiple" tests
PaulRBerg Apr 27, 2023
6b62bde
test: be more specific in comments
PaulRBerg Apr 27, 2023
bde0f3a
test: rename call expects helpers
PaulRBerg May 1, 2023
5faa8ec
test: differentiate streams in "cancelMultiple"
PaulRBerg May 1, 2023
b6f2bac
test: disable some tests temporarily
PaulRBerg May 1, 2023
e40bcbd
test: refactor defaults
PaulRBerg May 1, 2023
f7d62d5
test: add invariant tests for statuses
PaulRBerg May 2, 2023
f9ca075
test: de-dup modifiers
PaulRBerg May 4, 2023
48818a0
test: refactor default params
PaulRBerg May 4, 2023
666d088
test: use operator instead of "UD60x18.add"
PaulRBerg May 4, 2023
4d07554
fix: stream not cancelable when status settled
PaulRBerg May 4, 2023
6856903
test: move common logic to "Lockup_Shared_Test"
PaulRBerg May 4, 2023
7f8045e
docs: remove superfluous header separators
PaulRBerg May 9, 2023
7b4d237
test: rename "usdc" to "dai"
PaulRBerg May 9, 2023
021362b
perf: remove unneeded "notNull" modifier
PaulRBerg May 10, 2023
a3229d5
refactor: refactor "isCanceled" to "wasCanceled"
PaulRBerg May 11, 2023
7d21c34
perf: simplify withdraw functions
PaulRBerg May 11, 2023
3fbc3ba
perf: do not call "statusOf" in "cancel"
PaulRBerg May 11, 2023
ac6f04a
feat: add "isDepleted" and "wasCanceled" getters
PaulRBerg May 11, 2023
2a70537
refactor: merge "isDepleted" with "_isDepleted"
PaulRBerg May 12, 2023
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
2 changes: 1 addition & 1 deletion script/bootstrap/BootstrapProtocol.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ contract BootstrapProtocol is BaseScript {
// Create 7 linear streams with various amounts and durations.
//
// - 1st stream: meant to be depleted.
// - 2th to 4th streams: active.
// - 2th to 4th streams: warm.
// - 5th stream: meant to be renounced.
// - 6th stream: meant to canceled.
// - 7th stream: meant to be transferred to a third party.
Expand Down
239 changes: 144 additions & 95 deletions src/SablierV2LockupDynamic.sol

Large diffs are not rendered by default.

236 changes: 143 additions & 93 deletions src/SablierV2LockupLinear.sol

Large diffs are not rendered by default.

18 changes: 1 addition & 17 deletions src/abstracts/NoDelegateCall.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,21 @@ import { Errors } from "../libraries/Errors.sol";
/// @title NoDelegateCall
/// @notice This contract implements logic to prevent delegate calls.
abstract contract NoDelegateCall {
/*//////////////////////////////////////////////////////////////////////////
INTERNAL STORAGE
//////////////////////////////////////////////////////////////////////////*/

/// @dev The address of the original contract that was deployed.
address private immutable _original;

/*//////////////////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////////////////*/

/// @dev Sets the original contract address.
constructor() {
_original = address(this);
}

/*//////////////////////////////////////////////////////////////////////////
MODIFIERS
//////////////////////////////////////////////////////////////////////////*/

/// @notice Prevents delegate calls.
modifier noDelegateCall() {
_preventDelegateCall();
_;
}

/*//////////////////////////////////////////////////////////////////////////
INTERNAL CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @dev This function checks whether a delegate call is being made.
/// @dev This function checks whether the current call is a delegate call, and reverts if it is.
///
/// - A private function is used instead of inlining this logic in a modifier because Solidity copies modifiers into
/// every function that uses them. The `_original` address would get copied in every place the modifier is used,
Expand Down
157 changes: 63 additions & 94 deletions src/abstracts/SablierV2Lockup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,41 +31,25 @@ abstract contract SablierV2Lockup is

/// @param initialAdmin The address of the initial contract admin.
/// @param initialComptroller The address of the initial comptroller.
/// @param initialNftDescriptor The address of the initial NFT descriptor.
/// @param initialNFTDescriptor The address of the initial NFT descriptor.
constructor(
address initialAdmin,
ISablierV2Comptroller initialComptroller,
ISablierV2NFTDescriptor initialNftDescriptor
ISablierV2NFTDescriptor initialNFTDescriptor
)
SablierV2Base(initialAdmin, initialComptroller)
{
_nftDescriptor = initialNftDescriptor;
_nftDescriptor = initialNFTDescriptor;
}

/*//////////////////////////////////////////////////////////////////////////
MODIFIERS
//////////////////////////////////////////////////////////////////////////*/

/// @dev Checks that `streamId` references an active stream.
modifier isActive(uint256 streamId) {
if (getStatus(streamId) != Lockup.Status.ACTIVE) {
revert Errors.SablierV2Lockup_StreamNotActive(streamId);
}
_;
}

/// @dev Checks that `streamId` references a stream that is not null.
modifier isNotNull(uint256 streamId) {
if (getStatus(streamId) == Lockup.Status.NULL) {
revert Errors.SablierV2Lockup_StreamNull(streamId);
}
_;
}

/// @notice Checks that `msg.sender` is either the stream's sender or the stream's recipient (i.e. the NFT owner).
modifier onlySenderOrRecipient(uint256 streamId) {
if (!_isCallerStreamSender(streamId) && msg.sender != _ownerOf(streamId)) {
revert Errors.SablierV2Lockup_Unauthorized(streamId, msg.sender);
/// @dev Checks that `streamId` does not reference a null stream.
modifier notNull(uint256 streamId) {
if (!isStream(streamId)) {
revert Errors.SablierV2Lockup_Null(streamId);
}
_;
}
Expand All @@ -84,7 +68,13 @@ abstract contract SablierV2Lockup is
}

/// @inheritdoc ISablierV2Lockup
function getStatus(uint256 streamId) public view virtual override returns (Lockup.Status status);
function isDepleted(uint256 streamId) public view virtual override returns (bool result);

/// @inheritdoc ISablierV2Lockup
function isStream(uint256 streamId) public view virtual override returns (bool result);

/// @inheritdoc ISablierV2Lockup
function statusOf(uint256 streamId) public view virtual override returns (Lockup.Status status);

/// @inheritdoc ERC721
function tokenURI(uint256 streamId) public view override(IERC721Metadata, ERC721) returns (string memory uri) {
Expand All @@ -95,19 +85,33 @@ abstract contract SablierV2Lockup is
uri = _nftDescriptor.tokenURI(this, streamId);
}

/// @inheritdoc ISablierV2Lockup
function wasCanceled(uint256 streamId) public view virtual override returns (bool result);

/// @inheritdoc ISablierV2Lockup
function withdrawableAmountOf(uint256 streamId)
public
view
override
notNull(streamId)
returns (uint128 withdrawableAmount)
{
withdrawableAmount = _withdrawableAmountOf(streamId);
}

/*//////////////////////////////////////////////////////////////////////////
USER-FACING NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierV2Lockup
function burn(uint256 streamId) external override noDelegateCall {
// Checks: the stream is depleted.
if (getStatus(streamId) != Lockup.Status.DEPLETED) {
// Checks: only depleted streams can be burned.
if (!isDepleted(streamId)) {
revert Errors.SablierV2Lockup_StreamNotDepleted(streamId);
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
}

// Checks:
// 1. NFT exists (see `getApproved`).
// 1. NFT exists (see {IERC721.getApproved}).
// 2. `msg.sender` is either the owner of the NFT or an approved third party.
if (!_isCallerStreamRecipientOrApproved(streamId)) {
revert Errors.SablierV2Lockup_Unauthorized(streamId, msg.sender);
Expand All @@ -118,13 +122,20 @@ abstract contract SablierV2Lockup is
}

/// @inheritdoc ISablierV2Lockup
function cancel(uint256 streamId)
public
override
noDelegateCall
isActive(streamId)
onlySenderOrRecipient(streamId)
{
function cancel(uint256 streamId) public override noDelegateCall {
// Checks: the stream is neither depleted nor canceled.
if (isDepleted(streamId)) {
revert Errors.SablierV2Lockup_StreamDepleted(streamId);
} else if (wasCanceled(streamId)) {
revert Errors.SablierV2Lockup_StreamCanceled(streamId);
}

// Checks: `msg.sender` is either the stream's sender or the stream's recipient (i.e. the NFT owner).
if (!_isCallerStreamSender(streamId) && msg.sender != _ownerOf(streamId)) {
revert Errors.SablierV2Lockup_Unauthorized(streamId, msg.sender);
}

// Checks, Effects, and Interactions: cancel the stream.
_cancel(streamId);
}

Expand All @@ -144,7 +155,17 @@ abstract contract SablierV2Lockup is
}

/// @inheritdoc ISablierV2Lockup
function renounce(uint256 streamId) external override noDelegateCall isActive(streamId) {
function renounce(uint256 streamId) external override noDelegateCall {
// Checks: the stream is not cold.
Lockup.Status status = statusOf(streamId);
if (status == Lockup.Status.DEPLETED) {
revert Errors.SablierV2Lockup_StreamDepleted(streamId);
} else if (status == Lockup.Status.CANCELED) {
revert Errors.SablierV2Lockup_StreamCanceled(streamId);
} else if (status == Lockup.Status.SETTLED) {
revert Errors.SablierV2Lockup_StreamSettled(streamId);
}

// Checks: `msg.sender` is the stream's sender.
if (!_isCallerStreamSender(streamId)) {
revert Errors.SablierV2Lockup_Unauthorized(streamId, msg.sender);
Expand All @@ -169,30 +190,20 @@ abstract contract SablierV2Lockup is
}

/// @inheritdoc ISablierV2Lockup
function withdraw(
uint256 streamId,
address to,
uint128 amount
)
public
override
noDelegateCall
isNotNull(streamId)
{
function withdraw(uint256 streamId, address to, uint128 amount) public override noDelegateCall {
// Checks: the stream is not depleted.
if (getStatus(streamId) == Lockup.Status.DEPLETED) {
if (isDepleted(streamId)) {
revert Errors.SablierV2Lockup_StreamDepleted(streamId);
}

// Checks: `msg.sender` is the stream's sender, the stream's recipient (i.e. the NFT owner), or an
// approved third party.
// Checks: `msg.sender` is the stream's sender, the stream's recipient, or an approved third party.
if (!_isCallerStreamSender(streamId) && !_isCallerStreamRecipientOrApproved(streamId)) {
revert Errors.SablierV2Lockup_Unauthorized(streamId, msg.sender);
}

// Checks: if `msg.sender` is the stream's sender, the withdrawal address must be the recipient.
if (_isCallerStreamSender(streamId) && to != _ownerOf(streamId)) {
revert Errors.SablierV2Lockup_WithdrawSenderUnauthorized(streamId, msg.sender, to);
revert Errors.SablierV2Lockup_InvalidSenderWithdrawal(streamId, msg.sender, to);
}

// Checks: the withdrawal address is not zero.
Expand All @@ -205,15 +216,7 @@ abstract contract SablierV2Lockup is
revert Errors.SablierV2Lockup_WithdrawAmountZero(streamId);
}

// Checks: the withdraw amount is not greater than the withdrawable amount.
uint128 withdrawableAmount = _withdrawableAmountOf(streamId);
if (amount > withdrawableAmount) {
revert Errors.SablierV2Lockup_WithdrawAmountGreaterThanWithdrawableAmount(
streamId, amount, withdrawableAmount
);
}

// Effects and Interactions: make the withdrawal.
// Checks, Effects and Interactions: make the withdrawal.
_withdraw(streamId, to, amount);
}

Expand All @@ -232,11 +235,6 @@ abstract contract SablierV2Lockup is
override
noDelegateCall
{
// Checks: the withdrawal address is not zero.
if (to == address(0)) {
revert Errors.SablierV2Lockup_WithdrawToZeroAddress();
}

// Checks: there is an equal number of `streamIds` and `amounts`.
uint256 streamIdsCount = streamIds.length;
uint256 amountsCount = amounts.length;
Expand All @@ -245,38 +243,9 @@ abstract contract SablierV2Lockup is
}

// Iterate over the provided array of stream ids and withdraw from each stream.
uint256 streamId;
for (uint256 i = 0; i < streamIdsCount;) {
streamId = streamIds[i];

// Checks: the stream is neither null nor depleted.
Lockup.Status status = getStatus(streamId);
if (status == Lockup.Status.NULL) {
revert Errors.SablierV2Lockup_StreamNull(streamId);
} else if (status == Lockup.Status.DEPLETED) {
revert Errors.SablierV2Lockup_StreamDepleted(streamId);
}

// Checks: `msg.sender` is the stream's recipient (i.e. the NFT owner) or an approved third party.
if (!_isCallerStreamRecipientOrApproved(streamId)) {
revert Errors.SablierV2Lockup_Unauthorized(streamId, msg.sender);
}

// Checks: the withdraw amount is not zero.
if (amounts[i] == 0) {
revert Errors.SablierV2Lockup_WithdrawAmountZero(streamId);
}

// Checks: the withdraw amount is not greater than the withdrawable amount.
uint128 withdrawableAmount = _withdrawableAmountOf(streamId);
if (amounts[i] > withdrawableAmount) {
revert Errors.SablierV2Lockup_WithdrawAmountGreaterThanWithdrawableAmount(
streamId, amounts[i], withdrawableAmount
);
}

// Checks, Effects and Interactions: make the withdrawal.
_withdraw(streamId, to, amounts[i]);
// Checks, Effects, and Interactions: check the parameters and make the withdrawal.
withdraw(streamIds[i], to, amounts[i]);

// Increment the loop iterator.
unchecked {
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/ISablierV2Base.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { IAdminable } from "./IAdminable.sol";
import { ISablierV2Comptroller } from "./ISablierV2Comptroller.sol";

/// @title ISablierV2Base
/// @notice Common base between all Sablier V2 streaming contracts.
/// @notice Base logic for all Sablier V2 streaming contracts.
interface ISablierV2Base is IAdminable {
/*//////////////////////////////////////////////////////////////////////////
EVENTS
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/ISablierV2Comptroller.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ interface ISablierV2Comptroller is IAdminable {
/// - Unlike the protocol fee, this is a global fee applied to all flash loans, not a per-asset fee.
function flashFee() external view returns (UD60x18 fee);

/// @notice Retrieves a flag that indicates whether the provided ERC-20 asset can be flash loaned.
/// @notice Retrieves a flag indicating whether the provided ERC-20 asset can be flash loaned.
/// @param token The contract address of the ERC-20 asset to check.
function isFlashAsset(IERC20 token) external view returns (bool result);

Expand Down
Loading