Skip to content

Commit

Permalink
feat: add depositViaBroker and createAndDepositViaBroker
Browse files Browse the repository at this point in the history
  • Loading branch information
smol-ninja committed May 21, 2024
1 parent a3e1be5 commit 1ceca95
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 20 deletions.
96 changes: 90 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 @@ -197,15 +198,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);

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

/// @inheritdoc ISablierV2OpenEnded
function deposit(
uint256 streamId,
Expand All @@ -222,6 +246,23 @@ contract SablierV2OpenEnded is
_deposit(streamId, amount);
}

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

/// @inheritdoc ISablierV2OpenEnded
function restartStream(
uint256 streamId,
Expand All @@ -239,9 +280,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)
{
// 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 @@ -280,9 +332,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)
{
// Checks, Effects and Interactions: make the withdrawal.
withdrawAt(streamId, to, uint40(block.timestamp));
_withdrawAt(streamId, to, uint40(block.timestamp));
}

/*//////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -544,6 +605,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(streamId);
}

// 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
62 changes: 52 additions & 10 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 @@ -60,7 +61,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 @@ -173,7 +174,7 @@ interface ISablierV2OpenEnded is
/// @param streamId The ID of the stream to cancel.
function cancel(uint256 streamId) 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 @@ -186,8 +187,7 @@ interface ISablierV2OpenEnded is
/// - 'asset' must have valid decimals.
///
/// @param recipient The address receiving the assets.
/// @param sender The address streaming the assets, with the ability to adjust and cancel 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 @@ -202,18 +202,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 cancel 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 @@ -230,6 +228,34 @@ 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 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, 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 @@ -243,6 +269,22 @@ 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 or a canceled stream.
/// - `totalAmount` must be greater than broker amount.
/// - `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, both denoted in 18 decimals.
/// @param broker The broker's address and fee.
function depositViaBroker(uint256 streamId, uint128 totalAmount, Broker calldata broker) external;

/// @notice Refunds the provided amount of assets from the stream to the sender's address.
///
/// @dev Emits a {Transfer} and {RefundFromOpenEndedStream} event.
Expand Down
8 changes: 7 additions & 1 deletion src/interfaces/ISablierV2OpenEndedState.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ 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";
import { Broker, OpenEnded } from "../types/DataTypes.sol";

/// @title ISablierV2OpenEndedState
/// @notice State variables, storage and constants, for the {SablierV2OpenEnded} contract, and their respective getters.
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(uint256 streamId);

/// @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 canceled 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;
}

library OpenEnded {
/// @notice OpenEnded stream.
Expand Down
8 changes: 8 additions & 0 deletions test/integration/constructor.t.sol
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.22;

import { UD60x18 } from "@prb/math/src/UD60x18.sol";
import { SablierV2OpenEnded } from "src/SablierV2OpenEnded.sol";

import { Integration_Test } from "./Integration.t.sol";

contract Constructor_Integration_Concrete_Test is Integration_Test {
function test_Constructor() external {
// Construct the contract.
SablierV2OpenEnded constructedOpenEnded = new SablierV2OpenEnded();

// {SablierV2OpenEndedState.MAX_BROKER_FEE}
UD60x18 actualMaxBrokerFee = constructedOpenEnded.MAX_BROKER_FEE();
UD60x18 expectedMaxBrokerFee = UD60x18.wrap(0.1e18);
assertEq(actualMaxBrokerFee, expectedMaxBrokerFee, "MAX_BROKER_FEE");

// {SablierV2OpenEndedState.nextStreamId}
uint256 actualStreamId = constructedOpenEnded.nextStreamId();
uint256 expectedStreamId = 1;
assertEq(actualStreamId, expectedStreamId, "nextStreamId");
Expand Down

0 comments on commit 1ceca95

Please sign in to comment.