Skip to content

Commit

Permalink
Merge pull request #43 from sablier-labs/refactor/withdraw-public
Browse files Browse the repository at this point in the history
Allow anyone to call withdraw function
  • Loading branch information
andreivladbrg authored Apr 22, 2024
2 parents 9a409fb + a442b18 commit 55ce73f
Show file tree
Hide file tree
Showing 12 changed files with 265 additions and 132 deletions.
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 {
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

0 comments on commit 55ce73f

Please sign in to comment.