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

depositViaBroker and createAndDepositViaBroker functions #100

Merged
merged 4 commits into from
May 24, 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ internal balance and a rate-per-second in the Stream entity:
- Top up, which are public (you can ask a friend to deposit money for you instead)
- No deposits are required at the time of stream creation; thus, creation and deposit are distinct operations.
- There are no deposit limits.
- Streams can be created for an indefinite period, they will be collecting debt until the sender deposits or cancels the
- Streams can be created for an indefinite period, they will be collecting debt until the sender deposits or pauses the
stream.
- Ability to pause and restart streams.
- The sender can refund from the stream balance at any time.
Expand Down
94 changes: 88 additions & 6 deletions src/SablierV2OpenEnded.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/I
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import { ud } from "@prb/math/src/UD60x18.sol";

import { NoDelegateCall } from "./abstracts/NoDelegateCall.sol";
import { SablierV2OpenEndedState } from "./abstracts/SablierV2OpenEndedState.sol";
import { ISablierV2OpenEnded } from "./interfaces/ISablierV2OpenEnded.sol";
import { Errors } from "./libraries/Errors.sol";
import { OpenEnded } from "./types/DataTypes.sol";
import { Broker, OpenEnded } from "./types/DataTypes.sol";

/// @title SablierV2OpenEnded
/// @notice See the documentation in {ISablierV2OpenEnded}.
Expand Down Expand Up @@ -206,15 +207,38 @@ contract SablierV2OpenEnded is
)
external
override
noDelegateCall
returns (uint256 streamId)
{
// Checks, Effects and Interactions: create the stream.
streamId = create(sender, recipient, ratePerSecond, asset, isTransferable);
streamId = _create(sender, recipient, ratePerSecond, asset, isTransferable);

// Checks, Effects and Interactions: deposit on stream.
_deposit(streamId, amount);
}

/// @inheritdoc ISablierV2OpenEnded
function createAndDepositViaBroker(
address sender,
address recipient,
uint128 ratePerSecond,
IERC20 asset,
bool isTransferable,
uint128 totalAmount,
Broker calldata broker
)
external
override
noDelegateCall
returns (uint256 streamId)
{
// Checks, Effects and Interactions: create the stream.
streamId = _create(sender, recipient, ratePerSecond, asset, isTransferable);
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved

// Checks, Effects and Interactions: deposit into stream through {depositViaBroker}.
_depositViaBroker(streamId, totalAmount, broker);
}

/// @inheritdoc ISablierV2OpenEnded
function deposit(
uint256 streamId,
Expand All @@ -230,6 +254,21 @@ contract SablierV2OpenEnded is
_deposit(streamId, amount);
}

function depositViaBroker(
uint256 streamId,
uint128 totalAmount,
Broker calldata broker
)
public
override
noDelegateCall
notNull(streamId)
updateMetadata(streamId)
{
// Checks, Effects and Interactions: deposit on stream through broker.
_depositViaBroker(streamId, totalAmount, broker);
}

/// @inheritdoc ISablierV2OpenEnded
function pause(uint256 streamId)
public
Expand Down Expand Up @@ -261,9 +300,20 @@ contract SablierV2OpenEnded is
}

/// @inheritdoc ISablierV2OpenEnded
function restartStreamAndDeposit(uint256 streamId, uint128 ratePerSecond, uint128 amount) external override {
function restartStreamAndDeposit(
uint256 streamId,
uint128 ratePerSecond,
uint128 amount
)
external
override
noDelegateCall
notNull(streamId)
onlySender(streamId)
updateMetadata(streamId)
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
{
// Checks, Effects and Interactions: restart the stream.
restartStream(streamId, ratePerSecond);
_restartStream(streamId, ratePerSecond);

// Checks, Effects and Interactions: deposit on stream.
_deposit(streamId, amount);
Expand Down Expand Up @@ -301,9 +351,18 @@ contract SablierV2OpenEnded is
}

/// @inheritdoc ISablierV2OpenEnded
function withdrawMax(uint256 streamId, address to) external override {
function withdrawMax(
uint256 streamId,
address to
)
external
override
noDelegateCall
notNull(streamId)
updateMetadata(streamId)
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
{
// Checks, Effects and Interactions: make the withdrawal.
withdrawAt(streamId, to, uint40(block.timestamp));
_withdrawAt(streamId, to, uint40(block.timestamp));
}

/*//////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -538,6 +597,29 @@ contract SablierV2OpenEnded is
emit ISablierV2OpenEnded.DepositOpenEndedStream(streamId, msg.sender, asset, amount);
}

/// @dev See the documentation for the user-facing functions that call this internal function.
function _depositViaBroker(uint256 streamId, uint128 totalAmount, Broker memory broker) internal {
// Check: the broker's fee is not greater than `MAX_BROKER_FEE`.
if (broker.fee.gt(MAX_BROKER_FEE)) {
revert Errors.SablierV2OpenEnded_BrokerFeeTooHigh(streamId, broker.fee, MAX_BROKER_FEE);
}

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

// Calculate the broker's amount.
uint128 brokerAmountIn18Decimals = uint128(ud(totalAmount).mul(broker.fee).intoUint256());
uint128 brokerAmount = _calculateTransferAmount(streamId, brokerAmountIn18Decimals);

// Checks, Effects and Interactions: deposit on stream.
_deposit({ streamId: streamId, amount: totalAmount - brokerAmountIn18Decimals });

// Interaction: transfer the broker's amount.
_streams[streamId].asset.safeTransferFrom(msg.sender, broker.account, brokerAmount);
}

/// @dev Helper function to calculate the transfer amount and to perform the ERC-20 transfer.
function _extractFromStream(uint256 streamId, address to, uint128 amount) internal {
// Calculate the transfer amount.
Expand Down
4 changes: 4 additions & 0 deletions src/abstracts/SablierV2OpenEndedState.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity >=0.8.22;
import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import { UD60x18 } from "@prb/math/src/UD60x18.sol";

import { ISablierV2OpenEndedState } from "../interfaces/ISablierV2OpenEndedState.sol";
import { OpenEnded } from "../types/DataTypes.sol";
Expand All @@ -20,6 +21,9 @@ abstract contract SablierV2OpenEndedState is
STATE VARIABLES
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierV2OpenEndedState
UD60x18 public constant override MAX_BROKER_FEE = UD60x18.wrap(0.1e18);

/// @inheritdoc ISablierV2OpenEndedState
uint256 public override nextStreamId;

Expand Down
63 changes: 55 additions & 8 deletions src/interfaces/ISablierV2OpenEnded.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity >=0.8.22;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import { ISablierV2OpenEndedState } from "./ISablierV2OpenEndedState.sol";
import { Broker } from "../types/DataTypes.sol";

/// @title ISablierV2OpenEnded
/// @notice Creates and manages Open Ended streams with linear streaming functions.
Expand Down Expand Up @@ -44,7 +45,7 @@ interface ISablierV2OpenEnded is
/// @param streamId The ID of the open-ended stream.
/// @param funder The address which funded the stream.
/// @param asset The contract address of the ERC-20 asset used for streaming.
/// @param depositAmount The amount of assets deposited, denoted in 18 decimals.
/// @param depositAmount The amount of assets deposited into the stream, denoted in 18 decimals.
event DepositOpenEndedStream(
uint256 indexed streamId, address indexed funder, IERC20 indexed asset, uint128 depositAmount
);
Expand Down Expand Up @@ -170,7 +171,7 @@ interface ISablierV2OpenEnded is
/// @param newRatePerSecond The new rate per second of the open-ended stream, denoted in 18 decimals.
function adjustRatePerSecond(uint256 streamId, uint128 newRatePerSecond) external;

/// @notice Creates a new open-ended stream with the `block.timestamp` as the time reference and with zero balance.
/// @notice Creates a new open-ended stream with `block.timestamp` as `lastTimeUpdate` and set stream balance to 0.
/// The stream is wrapped in an ERC-721 NFT.
///
/// @dev Emits a {CreateOpenEndedStream} event.
Expand All @@ -185,6 +186,7 @@ interface ISablierV2OpenEnded is
/// @param recipient The address receiving the assets.
/// @param sender The address streaming the assets, with the ability to adjust and pause the stream. It doesn't
/// have to be the same as `msg.sender`.
/// @param sender The address streaming the assets. It doesn't have to be the same as `msg.sender`.
/// @param ratePerSecond The amount of assets that is increasing by every second, denoted in 18 decimals.
/// @param asset The contract address of the ERC-20 asset used for streaming.
/// @param isTransferable Boolean indicating if the stream NFT is transferable.
Expand All @@ -199,18 +201,16 @@ interface ISablierV2OpenEnded is
external
returns (uint256 streamId);

/// @notice Creates a new open-ended stream with the `block.timestamp` as the time reference
/// and with `amount` balance. The stream is wrapped in an ERC-721 NFT.
/// @notice Creates a new open-ended stream with `block.timestamp` as `lastTimeUpdate` and set the stream balance to
/// `amount`. The stream is wrapped in an ERC-721 NFT.
///
/// @dev Emits a {CreateOpenEndedStream}, {Transfer} and {DepositOpenEndedStream} events.
///
/// Requirements:
/// - `amount` must be greater than zero.
/// - Refer to the requirements in {create}.
/// - Refer to the requirements in {create} and {deposit}.
///
/// @param recipient The address receiving the assets.
/// @param sender The address streaming the assets, with the ability to adjust and pause the stream. It doesn't
/// have to be the same as `msg.sender`.
/// @param sender The address streaming the assets. It doesn't have to be the same as `msg.sender`.
/// @param ratePerSecond The amount of assets that is increasing by every second, denoted in 18 decimals.
/// @param asset The contract address of the ERC-20 asset used for streaming.
/// @param isTransferable Boolean indicating if the stream NFT is transferable.
Expand All @@ -227,6 +227,36 @@ interface ISablierV2OpenEnded is
external
returns (uint256 streamId);

/// @notice Creates a new open-ended stream with `block.timestamp` as `lastTimeUpdate` and set the stream balance to
/// an amount calculated from the `totalAmount` after broker fee amount deduction. The stream is wrapped in an
/// ERC-721 NFT.
///
/// @dev Emits a {CreateOpenEndedStream}, {Transfer} and {DepositOpenEndedStream} events.
///
/// Requirements:
/// - Refer to the requirements in {create} and {depositViaBroker}.
///
/// @param recipient The address receiving the assets.
/// @param sender The address streaming the assets. It doesn't have to be the same as `msg.sender`.
/// @param ratePerSecond The amount of assets that is increasing by every second, denoted in 18 decimals.
/// @param asset The contract address of the ERC-20 asset used for streaming.
/// @param isTransferable Boolean indicating if the stream NFT is transferable.
/// @param totalAmount The total amount, including the stream deposit and broker fee amount, both denoted in 18
/// decimals.
/// @param broker The broker's address and fee.
/// @return streamId The ID of the newly created stream.
function createAndDepositViaBroker(
address recipient,
address sender,
uint128 ratePerSecond,
IERC20 asset,
bool isTransferable,
uint128 totalAmount,
Broker calldata broker
)
external
returns (uint256 streamId);

/// @notice Deposits assets in a stream.
///
/// @dev Emits a {Transfer} and {DepositOpenEndedStream} event.
Expand All @@ -240,6 +270,23 @@ interface ISablierV2OpenEnded is
/// @param amount The amount deposited in the stream, denoted in 18 decimals.
function deposit(uint256 streamId, uint128 amount) external;

/// @notice Deposits assets in a stream.
///
/// @dev Emits a {Transfer} and {DepositOpenEndedStream} event.
///
/// Requirements:
/// - Must not be delegate called.
/// - `streamId` must not reference a null stream.
/// - `totalAmount` must be greater than zero. Otherwise it will revert inside {deposit}.
/// - `broker.account` must not be 0 address.
/// - `broker.fee` must not be greater than `MAX_BROKER_FEE`. It can be zero.
///
/// @param streamId The ID of the stream to deposit on.
/// @param totalAmount The total amount, including the stream deposit and broker fee amount, both denoted in 18
/// decimals.
/// @param broker The broker's address and fee.
function depositViaBroker(uint256 streamId, uint128 totalAmount, Broker calldata broker) external;

/// @notice Pauses the stream and refunds available assets to the sender.
///
/// @dev Emits a {Transfer} and {PauseOpenEndedStream} event.
Expand Down
6 changes: 6 additions & 0 deletions src/interfaces/ISablierV2OpenEndedState.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity >=0.8.22;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol";
import { UD60x18 } from "@prb/math/src/UD60x18.sol";

import { OpenEnded } from "../types/DataTypes.sol";

Expand Down Expand Up @@ -72,6 +73,11 @@ interface ISablierV2OpenEndedState is
/// @param streamId The stream ID for the query.
function isStream(uint256 streamId) external view returns (bool result);

/// @notice Retrieves the maximum broker fee that can be charged by the broker, denoted as a fixed-point number
/// where 1e18 is 100%.
/// @dev This value is hard coded as a constant.
function MAX_BROKER_FEE() external view returns (UD60x18 fee);

/// @notice Counter for stream ids.
/// @return The next stream id.
function nextStreamId() external view returns (uint256);
Expand Down
11 changes: 9 additions & 2 deletions src/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity >=0.8.22;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { UD60x18 } from "@prb/math/src/UD60x18.sol";

/// @title Errors
/// @notice Library with custom erros used across the OpenEnded contract.
Expand All @@ -17,7 +18,13 @@ library Errors {
SABLIER-V2-OpenEnded
//////////////////////////////////////////////////////////////////////////*/

/// @notice Thrown when trying to create a OpenEnded stream with a zero deposit amount.
/// @notice Thrown when trying to create a stream with a broker fee more than the allowed.
error SablierV2OpenEnded_BrokerFeeTooHigh(uint256 streamId, UD60x18 fee, UD60x18 maxFee);

/// @notice Thrown when trying to create a stream with a broker recipient address as zero.
error SablierV2OpenEnded_BrokerAddressZero();

/// @notice Thrown when trying to create a stream with a zero deposit amount.
error SablierV2OpenEnded_DepositAmountZero();

/// @notice Thrown when trying to create a stream with an asset with no decimals.
Expand Down Expand Up @@ -48,7 +55,7 @@ library Errors {
/// @notice Thrown when trying to refund zero assets from a stream.
error SablierV2OpenEnded_RefundAmountZero();

/// @notice Thrown when trying to create a OpenEnded stream with the sender as the zero address.
/// @notice Thrown when trying to create a stream with the sender as the zero address.
error SablierV2OpenEnded_SenderZeroAddress();

/// @notice Thrown when trying to perform an action with a paused stream.
Expand Down
10 changes: 9 additions & 1 deletion src/types/DataTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@
pragma solidity >=0.8.22;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { UD60x18 } from "@prb/math/src/UD60x18.sol";

// TODO: add Broker
/// @notice Struct encapsulating the broker parameters passed to the `depositViaBroker` and `createAndDepositViaBroker`
/// functions.
/// @param account The address receiving the broker's fee.
/// @param fee The broker's percentage fee from the amount passed, denoted as a fixed-point number where 1e18 is 100%.
struct Broker {
address account;
UD60x18 fee;
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
}

library OpenEnded {
/// @notice OpenEnded stream.
Expand Down
Loading