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

Allow anyone to call withdraw function #43

Merged
merged 6 commits into from
Apr 22, 2024
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
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
auto_detect_solc = false
bytecode_hash = "none"
evm_version = "paris"
fs_permissions = [{access = "read", path = "package.json"}]
gas_reports = ["*"]
optimizer = true
optimizer_runs = 1000
Expand Down
22 changes: 22 additions & 0 deletions script/Base.s.sol
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22;

import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";

import { console2 } from "forge-std/src/console2.sol";
import { Script } from "forge-std/src/Script.sol";
import { stdJson } from "forge-std/src/StdJson.sol";

abstract contract BaseScript is Script {
using Strings for uint256;
using stdJson for string;

/// @dev Included to enable compilation of the script without a $MNEMONIC environment variable.
string internal constant TEST_MNEMONIC = "test test test test test test test test test test test junk";

Expand Down Expand Up @@ -38,4 +45,19 @@ abstract contract BaseScript is Script {
_;
vm.stopBroadcast();
}

/// @dev The presence of the salt instructs Forge to deploy contracts via this deterministic CREATE2 factory:
/// https://github.com/Arachnid/deterministic-deployment-proxy
///
/// Notes:
/// - The salt format is "ChainID <chainid>, Version <version>".
/// - The version is obtained from `package.json`.
function constructCreate2Salt() public view returns (bytes32) {
string memory chainId = block.chainid.toString();
string memory json = vm.readFile("package.json");
string memory version = json.readString(".version");
string memory create2Salt = string.concat("ChainID ", chainId, ", Version ", version);
console2.log("The CREATE2 salt is \"%s\"", create2Salt);
return bytes32(abi.encodePacked(create2Salt));
}
}
14 changes: 14 additions & 0 deletions script/DeployDeterministicOpenEnded.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22 <0.9.0;

import { SablierV2OpenEnded } from "src/SablierV2OpenEnded.sol";

import { BaseScript } from "./Base.s.sol";

/// @notice Deploys {SablierV2OpenEnded} at a deterministic address across chains.
/// @dev Reverts if the contract has already been deployed.
contract DeployDeterministicOpenEnded is BaseScript {
function run() public broadcast returns (SablierV2OpenEnded openEnded) {
openEnded = new SablierV2OpenEnded{ salt: constructCreate2Salt() }();
}
}
13 changes: 13 additions & 0 deletions script/DeployOpenEnded.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22 <0.9.0;

import { SablierV2OpenEnded } from "src/SablierV2OpenEnded.sol";

import { BaseScript } from "./Base.s.sol";

/// @notice Deploys {SablierV2OpenEnded}.
contract DeployOpenEnded is BaseScript {
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
function run() public broadcast returns (SablierV2OpenEnded openEnded) {
openEnded = new SablierV2OpenEnded();
}
}
90 changes: 45 additions & 45 deletions src/SablierV2OpenEnded.sol
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope
uint256 streamIdsCount = streamIds.length;
uint256 amountsCount = amounts.length;

// Checks: count of `streamIds` matches count of `amounts`.
// Check: count of `streamIds` matches count of `amounts`.
if (streamIdsCount != amountsCount) {
revert Errors.SablierV2OpenEnded_DepositArrayCountsNotEqual(streamIdsCount, amountsCount);
}
Expand All @@ -176,7 +176,7 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope
for (uint256 i = 0; i < streamIdsCount; ++i) {
streamId = streamIds[i];

// Checks: the stream is not canceled.
// Check: the stream is not canceled.
if (isCanceled(streamId)) {
revert Errors.SablierV2OpenEnded_StreamCanceled(streamId);
}
Expand Down Expand Up @@ -336,14 +336,14 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope

/// @dev See the documentation for the user-facing functions that call this internal function.
function _adjustRatePerSecond(uint256 streamId, uint128 newRatePerSecond) internal {
// Checks: the new rate per second is not zero.
// Check: the new rate per second is not zero.
if (newRatePerSecond == 0) {
revert Errors.SablierV2OpenEnded_RatePerSecondZero();
}

uint128 oldRatePerSecond = _streams[streamId].ratePerSecond;

// Checks: the new rate per second is not equal to the actual rate per second.
// Check: the new rate per second is not equal to the actual rate per second.
if (newRatePerSecond == oldRatePerSecond) {
revert Errors.SablierV2OpenEnded_RatePerSecondNotDifferent(newRatePerSecond);
}
Expand All @@ -354,10 +354,10 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope
// in case of a bug.
_checkCalculatedAmount(streamId, recipientAmount);

// Effects: change the rate per second.
// Effect: change the rate per second.
_streams[streamId].ratePerSecond = newRatePerSecond;

// Effects: update the stream time.
// Effect: update the stream time.
_updateTime(streamId, uint40(block.timestamp));

// Effects and Interactions: withdraw the assets to the recipient, if any assets available.
Expand Down Expand Up @@ -388,10 +388,10 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope
// condition is checked to avoid exploits in case of a bug.
_checkCalculatedAmount(streamId, sum);

// Effects: set the stream as canceled.
// Effect: set the stream as canceled.
_streams[streamId].isCanceled = true;

// Effects: set the rate per second to zero.
// Effect: set the rate per second to zero.
_streams[streamId].ratePerSecond = 0;

// Effects and Interactions: refund the sender, if any assets available.
Expand Down Expand Up @@ -421,32 +421,32 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope
noDelegateCall
returns (uint256 streamId)
{
// Checks: the sender is not the zero address.
// Check: the sender is not the zero address.
if (sender == address(0)) {
revert Errors.SablierV2OpenEnded_SenderZeroAddress();
}

// Checks: the recipient is not the zero address.
// Check: the recipient is not the zero address.
if (recipient == address(0)) {
revert Errors.SablierV2OpenEnded_RecipientZeroAddress();
}

// Checks: the rate per second is not zero.
// Check: the rate per second is not zero.
if (ratePerSecond == 0) {
revert Errors.SablierV2OpenEnded_RatePerSecondZero();
}

uint8 assetDecimals = _safeAssetDecimals(address(asset));

// Checks: the asset has decimals.
// Check: the asset does not have decimals.
if (assetDecimals == 0) {
revert Errors.SablierV2OpenEnded_InvalidAssetDecimals(asset);
}

// Load the stream id.
streamId = nextStreamId;

// Effects: create the stream.
// Effect: create the stream.
_streams[streamId] = OpenEnded.Stream({
asset: asset,
assetDecimals: assetDecimals,
Expand All @@ -459,8 +459,8 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope
sender: sender
});

// Effects: bump the next stream id.
// Using unchecked arithmetic because these calculations cannot realistically overflow, ever.
// Effect: bump the next stream id.
// Using unchecked arithmetic because this calculation cannot realistically overflow, ever.
unchecked {
nextStreamId = streamId + 1;
}
Expand All @@ -473,12 +473,12 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope

/// @dev See the documentation for the user-facing functions that call this internal function.
function _deposit(uint256 streamId, uint128 amount) internal {
// Checks: the amount is not zero.
// Check: the amount is not zero.
if (amount == 0) {
revert Errors.SablierV2OpenEnded_DepositAmountZero();
}

// Effects: update the stream balance.
// Effect: update the stream balance.
_streams[streamId].balance += amount;

// Retrieve the ERC-20 asset from storage.
Expand All @@ -487,7 +487,7 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope
// Calculate the transfer amount.
uint128 transferAmount = _calculateTransferAmount(streamId, amount);

// Interactions: transfer the deposit amount.
// Interaction: transfer the deposit amount.
asset.safeTransferFrom(msg.sender, address(this), transferAmount);

// Log the deposit.
Expand All @@ -496,13 +496,13 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope

/// @dev Helper function to update the `balance` and to perform the ERC-20 transfer.
function _extractFromStream(uint256 streamId, address to, uint128 amount) internal {
// Effects: update the stream balance.
// Effect: update the stream balance.
_streams[streamId].balance -= amount;

// Calculate the transfer amount.
uint128 transferAmount = _calculateTransferAmount(streamId, amount);

// Interactions: perform the ERC-20 transfer.
// Interaction: perform the ERC-20 transfer.
_streams[streamId].asset.safeTransfer(to, transferAmount);
}

Expand All @@ -511,12 +511,12 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope
address sender = _streams[streamId].sender;
uint128 refundableAmount = _refundableAmountOf(streamId, uint40(block.timestamp));

// Checks: the amount is not zero.
// Check: the amount is not zero.
if (amount == 0) {
revert Errors.SablierV2OpenEnded_RefundAmountZero();
}

// Checks: the withdraw amount is not greater than the refundable amount.
// Check: the withdraw amount is not greater than the refundable amount.
if (amount > refundableAmount) {
revert Errors.SablierV2OpenEnded_Overrefund(streamId, amount, refundableAmount);
}
Expand All @@ -538,23 +538,23 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope
notNull(streamId)
onlySender(streamId)
{
// Checks: the stream is canceled.
// Check: the stream is canceled.
if (!_streams[streamId].isCanceled) {
revert Errors.SablierV2OpenEnded_StreamNotCanceled(streamId);
}

// Checks: the rate per second is not zero.
// Check: the rate per second is not zero.
if (ratePerSecond == 0) {
revert Errors.SablierV2OpenEnded_RatePerSecondZero();
}

// Effects: set the rate per second.
// Effect: set the rate per second.
_streams[streamId].ratePerSecond = ratePerSecond;

// Effects: set the stream as not canceled.
// Effect: set the stream as not canceled.
_streams[streamId].isCanceled = false;

// Effects: update the stream time.
// Effect: update the stream time.
_updateTime(streamId, uint40(block.timestamp));

// Log the restart.
Expand All @@ -568,34 +568,34 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope

/// @dev See the documentation for the user-facing functions that call this internal function.
function _withdraw(uint256 streamId, address to, uint40 time) internal noDelegateCall notCanceled(streamId) {
bool isCallerStreamSender = _isCallerStreamSender(streamId);
address recipient = _streams[streamId].recipient;

// Checks: `msg.sender` is the stream's sender or the stream's recipient.
if (!isCallerStreamSender && msg.sender != recipient) {
revert Errors.SablierV2OpenEnded_Unauthorized(streamId, msg.sender);
// Check: the withdrawal address is not zero.
if (to == address(0)) {
revert Errors.SablierV2OpenEnded_WithdrawToZeroAddress();
}

// Checks: the provided address is the recipient if `msg.sender` is the sender of the stream.
if (isCallerStreamSender && to != recipient) {
revert Errors.SablierV2OpenEnded_Unauthorized(streamId, msg.sender);
}
// Retrieve the recipient from storage.
address recipient = _streams[streamId].recipient;

// Checks: the withdrawal address is not zero.
if (to == address(0)) {
revert Errors.SablierV2OpenEnded_WithdrawToZeroAddress();
// Check: if `msg.sender` is not the stream's recipient, the withdrawal address must be the recipient.
if (to != recipient && msg.sender != recipient) {
revert Errors.SablierV2OpenEnded_WithdrawalAddressNotRecipient(streamId, msg.sender, to);
}

uint40 lastTimeUpdate = _streams[streamId].lastTimeUpdate;

// Checks: the time reference is greater than the `lastTimeUpdate`.
// Check: the withdrawal time is greater than the `lastTimeUpdate`.
if (time <= lastTimeUpdate) {
revert Errors.SablierV2OpenEnded_TimeNotGreaterThanLastUpdate(time, lastTimeUpdate);
revert Errors.SablierV2OpenEnded_WithdrawalTimeNotGreaterThanLastUpdate(time, lastTimeUpdate);
}

// Checks: the time reference is less than or equal to the current time.
// Check: the time reference is not in the future.
if (time > uint40(block.timestamp)) {
revert Errors.SablierV2OpenEnded_TimeNotLessOrEqualToCurrentTime(time, uint40(block.timestamp));
revert Errors.SablierV2OpenEnded_WithdrawalTimeInTheFuture(time, block.timestamp);
}

// Check: the stream balance is not zero.
if (_streams[streamId].balance == 0) {
revert Errors.SablierV2OpenEnded_WithdrawBalanceZero(streamId);
}

// Calculate how much to withdraw based on the time reference.
Expand All @@ -605,7 +605,7 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope
// in case of a bug.
_checkCalculatedAmount(streamId, withdrawAmount);

// Effects: update the stream time.
// Effect: update the stream time.
_updateTime(streamId, time);

// Effects and interactions: update the `balance` and perform the ERC-20 transfer.
Expand Down
10 changes: 4 additions & 6 deletions src/interfaces/ISablierV2OpenEnded.sol
Original file line number Diff line number Diff line change
Expand Up @@ -305,13 +305,11 @@ interface ISablierV2OpenEnded is ISablierV2OpenEndedState {
///
/// Requirements:
/// - Must not be delegate called.
/// - `streamId` must not reference a null stream.
/// - `streamId` must not reference a canceled stream.
/// - `msg.sender` must be the stream's sender or the stream's recipient.
/// - `to` must be the recipient if `msg.sender` is the stream's sender.
/// - `streamId` must not reference a null or canceled stream.
/// - `to` must not be the zero address.
/// - `time` must be greater than the stream's `lastTimeUpdate`.
/// - `time` must be less than or equal to the current `block.timestamp`.
/// - `to` must be the recipient if `msg.sender` is not the stream's recipient.
/// - `time` must be greater than the stream's `lastTimeUpdate` and must not be in the future.
/// - The stream balance must be greater than zero.
///
/// @param streamId The id of the stream to withdraw from.
/// @param to The address receiving the withdrawn assets.
Expand Down
19 changes: 12 additions & 7 deletions src/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,21 @@ library Errors {
/// @notice Thrown when trying to restart a stream that is not canceled.
error SablierV2OpenEnded_StreamNotCanceled(uint256 streamId);

/// @notice Thrown when trying to withdraw assets with a time reference less than or equal to stream's
/// `lastTimeUpdate`.
error SablierV2OpenEnded_TimeNotGreaterThanLastUpdate(uint40 time, uint40 lastUpdate);

/// @notice Thrown when trying to withdraw assets with a time reference greater than the current time.
error SablierV2OpenEnded_TimeNotLessOrEqualToCurrentTime(uint40 time, uint40 currentTime);

/// @notice Thrown when `msg.sender` lacks authorization to perform an action.
error SablierV2OpenEnded_Unauthorized(uint256 streamId, address caller);

/// @notice Thrown when trying to withdraw to an address other than the recipient's.
error SablierV2OpenEnded_WithdrawalAddressNotRecipient(uint256 streamId, address caller, address to);

/// @notice Thrown when trying to withdraw assets with a withdrawal time in the future.
error SablierV2OpenEnded_WithdrawalTimeInTheFuture(uint40 time, uint256 currentTime);

/// @notice Thrown when trying to withdraw assets with a withdrawal time not greater than `lastTimeUpdate`.
error SablierV2OpenEnded_WithdrawalTimeNotGreaterThanLastUpdate(uint40 time, uint40 lastUpdate);

/// @notice Thrown when trying to withdraw to the zero address.
error SablierV2OpenEnded_WithdrawToZeroAddress();

/// @notice Thrown when trying to withdraw but the stream balance is zero.
error SablierV2OpenEnded_WithdrawBalanceZero(uint256 streamId);
}
Loading