Skip to content

Commit

Permalink
feat: add depositViaBroker and createAndDepositViaBroker
Browse files Browse the repository at this point in the history
test: depositViaBroker

test: createAnddepositViaBroker

refactor: address pr review

test: move broker function from Base to Integration

test: remove duplicated modifiers

test: remove unneeded tree branches

test: fix CreateAndDepositViaBroker_Integration_Test
  • Loading branch information
smol-ninja committed May 24, 2024
1 parent 0835718 commit 9ea6bb6
Show file tree
Hide file tree
Showing 26 changed files with 590 additions and 57 deletions.
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);

// 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)
{
// 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)
{
// 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
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();

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

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

0 comments on commit 9ea6bb6

Please sign in to comment.