Skip to content

Commit

Permalink
feat: Add broker fee during deposits
Browse files Browse the repository at this point in the history
feat: Add MAX_BROKER_FEE constant
feat: Event DepositOpenEndedStream emits broker and brokerFeeAmount
test: New utility contracts: Defaults.sol, Types.sol Constants.sol
test: Add constructor test
test: Use constants from Defaults contract
test: Replace StdUtils with PRBMathUtils
test: Replace StdAssertions with PRBMathAssertions
test(integration): Include broker fee in deposit.t.sol
test(invariant): Incluse broker address in checkUsers
test(invariant): Include broker in createAndDeposit, deposit and restartStreamAndDeposit
  • Loading branch information
smol-ninja committed May 12, 2024
1 parent 4405fdf commit 6678282
Show file tree
Hide file tree
Showing 35 changed files with 581 additions and 255 deletions.
78 changes: 63 additions & 15 deletions src/SablierV2OpenEnded.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.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 @@ -185,7 +186,8 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope
address recipient,
uint128 ratePerSecond,
IERC20 asset,
uint128 amount
uint128 amount,
Broker calldata broker
)
external
override
Expand All @@ -195,7 +197,7 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope
streamId = _create(sender, recipient, ratePerSecond, asset);

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

/// @inheritdoc ISablierV2OpenEnded
Expand Down Expand Up @@ -233,7 +235,8 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope
address[] calldata senders,
uint128[] calldata ratesPerSecond,
IERC20 asset,
uint128[] calldata amounts
uint128[] calldata amounts,
Broker calldata broker
)
external
override
Expand All @@ -250,14 +253,15 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope
// Deposit on each stream.
for (uint256 i = 0; i < streamIdsCount; ++i) {
// Checks, Effects and Interactions: deposit on stream.
_deposit(streamIds[i], amounts[i]);
_deposit(streamIds[i], amounts[i], broker);
}
}

/// @inheritdoc ISablierV2OpenEnded
function deposit(
uint256 streamId,
uint128 amount
uint128 amount,
Broker calldata broker
)
external
override
Expand All @@ -266,11 +270,19 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope
notNull(streamId)
{
// Checks, Effects and Interactions: deposit on stream.
_deposit(streamId, amount);
_deposit(streamId, amount, broker);
}

/// @inheritdoc ISablierV2OpenEnded
function depositMultiple(uint256[] memory streamIds, uint128[] calldata amounts) public override noDelegateCall {
function depositMultiple(
uint256[] memory streamIds,
uint128[] calldata amounts,
Broker calldata broker
)
public
override
noDelegateCall
{
uint256 streamIdsCount = streamIds.length;
uint256 amountsCount = amounts.length;

Expand All @@ -286,7 +298,7 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope
}

// Checks, Effects and Interactions: deposit on stream.
_deposit(streamIds[i], amounts[i]);
_deposit(streamIds[i], amounts[i], broker);
}
}

Expand All @@ -297,12 +309,20 @@ contract SablierV2OpenEnded is ISablierV2OpenEnded, NoDelegateCall, SablierV2Ope
}

/// @inheritdoc ISablierV2OpenEnded
function restartStreamAndDeposit(uint256 streamId, uint128 ratePerSecond, uint128 amount) external override {
function restartStreamAndDeposit(
uint256 streamId,
uint128 ratePerSecond,
uint128 amount,
Broker calldata broker
)
external
override
{
// Checks, Effects and Interactions: restart the stream.
_restartStream(streamId, ratePerSecond);

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

/// @inheritdoc ISablierV2OpenEnded
Expand Down Expand Up @@ -599,26 +619,54 @@ 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 {
function _deposit(uint256 streamId, uint128 amount, Broker calldata broker) internal {
// Check: the deposit amount is not zero.
if (amount == 0) {
revert Errors.SablierV2OpenEnded_DepositAmountZero();
}

// Effect: update the stream balance.
_streams[streamId].balance += amount;
// Check: broker fee is not greather than `MAX_BROKER_FEE`.
if (broker.fee.gt(MAX_BROKER_FEE)) {
revert Errors.SablierV2OpenEnded_BrokerFeeTooHigh(broker.fee, MAX_BROKER_FEE);
}

// Retrieve the ERC-20 asset from storage.
IERC20 asset = _streams[streamId].asset;

// Calculate the broker fee amount.
uint128 brokerFeeAmount;
uint128 brokerTransferAmount;
if (broker.fee.gt(ud(0))) {
unchecked {
brokerFeeAmount = ud(amount).mul(broker.fee).intoUint128();
amount -= brokerFeeAmount;
}
brokerTransferAmount = _calculateTransferAmount(streamId, brokerFeeAmount);
}

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

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

// Interaction: pay the broker fee, if not zero.
if (brokerTransferAmount > 0) {
asset.safeTransferFrom({ from: msg.sender, to: broker.account, value: brokerTransferAmount });
}

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

// Log the deposit.
emit ISablierV2OpenEnded.DepositOpenEndedStream(streamId, msg.sender, asset, amount);
emit ISablierV2OpenEnded.DepositOpenEndedStream({
streamId: streamId,
funder: msg.sender,
asset: asset,
depositAmount: amount,
broker: broker.account,
brokerFeeAmount: brokerFeeAmount
});
}

/// @dev Helper function to update the `balance` and to perform the ERC-20 transfer.
Expand Down
4 changes: 4 additions & 0 deletions src/abstracts/SablierV2OpenEndedState.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";

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

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

/// @inheritdoc ISablierV2OpenEndedState
uint256 public override nextStreamId;

Expand Down
43 changes: 37 additions & 6 deletions src/interfaces/ISablierV2OpenEnded.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 { Broker } from "../types/DataTypes.sol";
import { ISablierV2OpenEndedState } from "./ISablierV2OpenEndedState.sol";

/// @title ISablierV2OpenEnded
Expand Down Expand Up @@ -63,8 +64,15 @@ interface ISablierV2OpenEnded is ISablierV2OpenEndedState {
/// @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 broker The address of the broker who has helped deposit into the stream, e.g. a front-end website.
/// @param brokerFeeAmount The amount of assets paid to the broker as a fee, denoted in 18 decimals.
event DepositOpenEndedStream(
uint256 indexed streamId, address indexed funder, IERC20 indexed asset, uint128 depositAmount
uint256 indexed streamId,
address indexed funder,
IERC20 indexed asset,
uint128 depositAmount,
address broker,
uint128 brokerFeeAmount
);

/// @notice Emitted when assets are refunded from a open-ended stream.
Expand Down Expand Up @@ -227,12 +235,15 @@ interface ISablierV2OpenEnded is ISablierV2OpenEndedState {
/// @param asset The contract address of the ERC-20 asset used for streaming.
/// @param amount The amount deposited in the stream.
/// @return streamId The ID of the newly created stream.
/// @param broker Struct containing (i) the address of the broker assisting in depositing into the stream, and (ii)
/// the percentage fee paid to the broker from `amount`, denoted as a fixed-point number. Both can be set to zero.
function createAndDeposit(
address recipient,
address sender,
uint128 ratePerSecond,
IERC20 asset,
uint128 amount
uint128 amount,
Broker calldata broker
)
external
returns (uint256 streamId);
Expand All @@ -252,12 +263,15 @@ interface ISablierV2OpenEnded is ISablierV2OpenEndedState {
/// @param asset The contract address of the ERC-20 asset used for streaming.
/// @param amounts The amounts deposited in the streams.
/// @return streamIds The IDs of the newly created streams.
/// @param broker Struct containing (i) the address of the broker assisting in depositing into the stream, and (ii)
/// the percentage fee paid to the broker from `amounts`, denoted as a fixed-point number. Both can be set to zero.
function createAndDepositMultiple(
address[] calldata recipients,
address[] calldata senders,
uint128[] calldata ratesPerSecond,
IERC20 asset,
uint128[] calldata amounts
uint128[] calldata amounts,
Broker calldata broker
)
external
returns (uint256[] memory streamIds);
Expand Down Expand Up @@ -295,7 +309,9 @@ interface ISablierV2OpenEnded is ISablierV2OpenEndedState {
///
/// @param streamId The ID of the stream to deposit on.
/// @param amount The amount deposited in the stream, denoted in 18 decimals.
function deposit(uint256 streamId, uint128 amount) external;
/// @param broker Struct containing (i) the address of the broker assisting in depositing into the stream, and (ii)
/// the percentage fee paid to the broker from `amount`, denoted as a fixed-point number. Both can be set to zero.
function deposit(uint256 streamId, uint128 amount, Broker calldata broker) external;

/// @notice Deposits assets in multiple streams.
///
Expand All @@ -307,7 +323,14 @@ interface ISablierV2OpenEnded is ISablierV2OpenEndedState {
///
/// @param streamIds The ids of the streams to deposit on.
/// @param amounts The amount of assets to be deposited, denoted in 18 decimals.
function depositMultiple(uint256[] calldata streamIds, uint128[] calldata amounts) external;
/// @param broker Struct containing (i) the address of the broker assisting in depositing into the stream, and (ii)
/// the percentage fee paid to the broker from `amounts`, denoted as a fixed-point number. Both can be set to zero.
function depositMultiple(
uint256[] calldata streamIds,
uint128[] calldata amounts,
Broker calldata broker
)
external;

/// @notice Refunds the provided amount of assets from the stream to the sender's address.
///
Expand Down Expand Up @@ -350,7 +373,15 @@ interface ISablierV2OpenEnded is ISablierV2OpenEndedState {
/// @param streamId The ID of the stream to restart.
/// @param ratePerSecond The amount of assets that is increasing by every second, denoted in 18 decimals.
/// @param amount The amount deposited in the stream.
function restartStreamAndDeposit(uint256 streamId, uint128 ratePerSecond, uint128 amount) external;
/// @param broker Struct containing (i) the address of the broker assisting in depositing into the stream, and (ii)
/// the percentage fee paid to the broker from `amount`, denoted as a fixed-point number. Both can be set to zero.
function restartStreamAndDeposit(
uint256 streamId,
uint128 ratePerSecond,
uint128 amount,
Broker calldata broker
)
external;

/// @notice Withdraws the amount of assets calculated based on time reference, from the stream
/// to the provided `to` address.
Expand Down
6 changes: 6 additions & 0 deletions src/interfaces/ISablierV2OpenEndedState.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";

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

Expand Down Expand Up @@ -64,6 +65,11 @@ interface ISablierV2OpenEndedState {
/// @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);

/// @notice Counter for stream ids.
/// @return The next stream id.
function nextStreamId() external view returns (uint256);
Expand Down
4 changes: 4 additions & 0 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,6 +18,9 @@ library Errors {
SABLIER-V2-OpenEnded
//////////////////////////////////////////////////////////////////////////*/

/// @notice Thrown when the broker fee exceeds the maximum allowed fee.
error SablierV2OpenEnded_BrokerFeeTooHigh(UD60x18 brokerFee, UD60x18 maxBrokerFee);

/// @notice Thrown when trying to create multiple streams and the number of senders, recipients and rates per second
/// does not match.
error SablierV2OpenEnded_CreateMultipleArrayCountsNotEqual(
Expand Down
9 changes: 8 additions & 1 deletion src/types/DataTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@
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 _deposit function. Both can be set to zero.
/// @param account The address receiving the broker's fee.
/// @param fee The broker's percentage fee from the total amount, 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 6678282

Please sign in to comment.