Skip to content

Commit

Permalink
feat: add sender and recipient params in deposit
Browse files Browse the repository at this point in the history
feat: add notDifferentActors modifier
test: add concrete tests for sender and recipient miss match
test: update all tests accordingly
  • Loading branch information
andreivladbrg committed Oct 16, 2024
1 parent 4ebdf2c commit 0974651
Show file tree
Hide file tree
Showing 16 changed files with 227 additions and 67 deletions.
8 changes: 7 additions & 1 deletion src/SablierFlow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,16 @@ contract SablierFlow is
/// @inheritdoc ISablierFlow
function deposit(
uint256 streamId,
uint128 amount
uint128 amount,
address sender,
address recipient
)
external
override
noDelegateCall
notNull(streamId)
notVoided(streamId)
notDifferentActors(streamId, sender, recipient)
updateMetadata(streamId)
{
// Checks, Effects, and Interactions: deposit on stream.
Expand Down Expand Up @@ -279,13 +282,16 @@ contract SablierFlow is
function depositViaBroker(
uint256 streamId,
uint128 totalAmount,
address sender,
address recipient,
Broker calldata broker
)
external
override
noDelegateCall
notNull(streamId)
notVoided(streamId)
notDifferentActors(streamId, sender, recipient)
updateMetadata(streamId)
{
// Checks, Effects, and Interactions: deposit on stream through broker.
Expand Down
15 changes: 15 additions & 0 deletions src/abstracts/SablierFlowBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,21 @@ abstract contract SablierFlowBase is
MODIFIERS
//////////////////////////////////////////////////////////////////////////*/

/// @dev Checks whether the provided addresses matches stream's sender and recipient.
modifier notDifferentActors(uint256 streamId, address sender, address recipient) {
// Check: the sender address matches the stream's sender.
if (sender != _streams[streamId].sender) {
revert Errors.SablierFlow_NotStreamSender(sender, _streams[streamId].sender);
}

// Check: the recipient address matches the stream's recipient.
if (recipient != _ownerOf(streamId)) {
revert Errors.SablierFlow_NotStreamRecipient(recipient, _ownerOf(streamId));
}

_;
}

/// @dev Checks that `streamId` does not reference a null stream.
modifier notNull(uint256 streamId) {
if (!_streams[streamId].isStream) {
Expand Down
29 changes: 21 additions & 8 deletions src/interfaces/ISablierFlow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,30 +52,30 @@ interface ISablierFlow is

/// @notice Emitted when a stream is paused by the sender.
/// @param streamId The ID of the stream.
/// @param sender The address of the stream's sender.
/// @param recipient The address of the stream's recipient.
/// @param sender The stream's sender address.
/// @param recipient The stream's recipient address.
/// @param totalDebt The amount of tokens owed by the sender to the recipient, denoted in token's decimals.
event PauseFlowStream(
uint256 indexed streamId, address indexed sender, address indexed recipient, uint256 totalDebt
);

/// @notice Emitted when a sender is refunded from a stream.
/// @param streamId The ID of the stream.
/// @param sender The address of the stream's sender.
/// @param sender The stream's sender address.
/// @param amount The amount of tokens refunded to the sender, denoted in token's decimals.
event RefundFromFlowStream(uint256 indexed streamId, address indexed sender, uint128 amount);

/// @notice Emitted when a stream is restarted by the sender.
/// @param streamId The ID of the stream.
/// @param sender The address of the stream's sender.
/// @param sender The stream's sender address.
/// @param ratePerSecond The amount by which the debt is increasing every second, denoted as a fixed-point number
/// where 1e18 is 1 token per second.
event RestartFlowStream(uint256 indexed streamId, address indexed sender, UD21x18 ratePerSecond);

/// @notice Emitted when a stream is voided by the sender, recipient or an approved operator.
/// @param streamId The ID of the stream.
/// @param sender The address of the stream's sender.
/// @param recipient The address of the stream's recipient.
/// @param sender The stream's sender address.
/// @param recipient The stream's recipient address.
/// @param caller The address that performed the void, which can be the sender, recipient or an approved operator.
/// @param newTotalDebt The new total debt, denoted in token's decimals.
/// @param writtenOffDebt The amount of debt written off by the caller, denoted in token's decimals.
Expand Down Expand Up @@ -245,10 +245,14 @@ interface ISablierFlow is
/// - Must not be delegate called.
/// - `streamId` must not reference a null or a voided stream.
/// - `amount` must be greater than zero.
/// - `sender` and `recipient` must match the stream's sender and recipient addresses.
///
/// @param streamId The ID of the stream to deposit to.
/// @param amount The deposit amount, denoted in token's decimals.
function deposit(uint256 streamId, uint128 amount) external;
/// @param amount The deposit amount, in token decimals.
/// @param sender The stream's sender address.
/// @param recipient The stream's recipient address.
function deposit(uint256 streamId, uint128 amount, address sender, address recipient) external;

/// @notice Deposits tokens in a stream and pauses it.
///
Expand Down Expand Up @@ -280,9 +284,18 @@ interface ISablierFlow is
///
/// @param streamId The ID of the stream to deposit on.
/// @param totalAmount The total amount, including the deposit and any broker fee, denoted in token's decimals.
/// @param sender The stream's sender address.
/// @param recipient The stream's recipient address.
/// @param broker Struct encapsulating (i) the address of the broker assisting in creating the stream, and (ii) the
/// percentage fee paid to the broker from `totalAmount`, denoted as a fixed-point percentage.
function depositViaBroker(uint256 streamId, uint128 totalAmount, Broker calldata broker) external;
function depositViaBroker(
uint256 streamId,
uint128 totalAmount,
address sender,
address recipient,
Broker calldata broker
)
external;

/// @notice Pauses the stream.
///
Expand Down
6 changes: 6 additions & 0 deletions src/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ library Errors {
/// @notice Thrown when trying to withdraw an amount greater than the withdrawable amount.
error SablierFlow_Overdraw(uint256 streamId, uint128 amount, uint128 withdrawableAmount);

/// @notice Thrown when the recipient address does not match the stream's recipient.
error SablierFlow_NotStreamRecipient(address recipient, address streamRecipient);

/// @notice Thrown when the sender address does not match the stream's sender.
error SablierFlow_NotStreamSender(address sender, address streamSender);

/// @notice Thrown when trying to change the rate per second with the same rate per second.
error SablierFlow_RatePerSecondNotDifferent(uint256 streamId, UD21x18 ratePerSecond);

Expand Down
2 changes: 1 addition & 1 deletion tests/fork/Flow.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ contract Flow_Fork_Test is Fork_Test {
expectCallToTransferFrom({ token: token, from: sender, to: address(flow), amount: depositAmount });

// Make the deposit.
flow.deposit(streamId, depositAmount);
flow.deposit(streamId, depositAmount, sender, flow.getRecipient(streamId));

// Assert that the token balance of stream has been updated.
vars.actualTokenBalance = token.balanceOf(address(flow));
Expand Down
7 changes: 6 additions & 1 deletion tests/fork/Fork.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,12 @@ abstract contract Fork_Test is Base_Test {
resetPrank({ msgSender: sender });
deal({ token: address(token), to: sender, give: depositAmount });
safeApprove(depositAmount);
flow.deposit({ streamId: streamId, amount: depositAmount });
flow.deposit({
streamId: streamId,
amount: depositAmount,
sender: sender,
recipient: flow.getRecipient(streamId)
});
}

/// @dev Use a low-level call to ignore reverts in case of USDT.
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/Integration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ abstract contract Integration_Test is Base_Test {
deal({ token: address(token), to: users.sender, give: UINT128_MAX });
token.approve(address(flow), UINT128_MAX);

flow.deposit(streamId, amount);
flow.deposit(streamId, amount, users.sender, users.recipient);
}

function depositDefaultAmount(uint256 streamId) internal {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ contract AdjustRatePerSecond_Integration_Concrete_Test is Integration_Test {
whenCallerSender
whenNewRatePerSecondNotEqualsCurrentRatePerSecond
{
flow.deposit(defaultStreamId, DEPOSIT_AMOUNT_6D);
flow.deposit(defaultStreamId, DEPOSIT_AMOUNT_6D, users.sender, users.recipient);

UD21x18 actualRatePerSecond = flow.getRatePerSecond(defaultStreamId);
UD21x18 expectedRatePerSecond = RATE_PER_SECOND;
Expand Down
6 changes: 3 additions & 3 deletions tests/integration/concrete/batch/batch.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ contract Batch_Integration_Concrete_Test is Integration_Test {

// The calls declared as bytes
bytes[] memory calls = new bytes[](1);
calls[0] = abi.encodeCall(flow.deposit, (streamId, DEPOSIT_AMOUNT_6D));
calls[0] = abi.encodeCall(flow.deposit, (streamId, DEPOSIT_AMOUNT_6D, users.sender, users.recipient));

bytes memory expectedRevertData = abi.encodeWithSelector(
Errors.BatchError.selector, abi.encodeWithSignature("Error(string)", "ERC20: insufficient allowance")
Expand Down Expand Up @@ -176,8 +176,8 @@ contract Batch_Integration_Concrete_Test is Integration_Test {
function test_Batch_DepositMultiple() external {
// The calls declared as bytes
bytes[] memory calls = new bytes[](2);
calls[0] = abi.encodeCall(flow.deposit, (defaultStreamIds[0], DEPOSIT_AMOUNT_6D));
calls[1] = abi.encodeCall(flow.deposit, (defaultStreamIds[1], DEPOSIT_AMOUNT_6D));
calls[0] = abi.encodeCall(flow.deposit, (defaultStreamIds[0], DEPOSIT_AMOUNT_6D, users.sender, users.recipient));
calls[1] = abi.encodeCall(flow.deposit, (defaultStreamIds[1], DEPOSIT_AMOUNT_6D, users.sender, users.recipient));

// It should emit 2 {Transfer}, 2 {DepositFlowStream}, 2 {MetadataUpdate} events.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,61 +12,113 @@ import { Integration_Test } from "../../Integration.t.sol";

contract DepositViaBroker_Integration_Concrete_Test is Integration_Test {
function test_RevertWhen_DelegateCall() external {
bytes memory callData =
abi.encodeCall(flow.depositViaBroker, (defaultStreamId, TOTAL_AMOUNT_WITH_BROKER_FEE_6D, defaultBroker));
bytes memory callData = abi.encodeCall(
flow.depositViaBroker,
(defaultStreamId, TOTAL_AMOUNT_WITH_BROKER_FEE_6D, users.sender, users.recipient, defaultBroker)
);
expectRevert_DelegateCall(callData);
}

function test_RevertGiven_Null() external whenNoDelegateCall {
bytes memory callData =
abi.encodeCall(flow.depositViaBroker, (nullStreamId, TOTAL_AMOUNT_WITH_BROKER_FEE_6D, defaultBroker));
bytes memory callData = abi.encodeCall(
flow.depositViaBroker,
(nullStreamId, TOTAL_AMOUNT_WITH_BROKER_FEE_6D, users.sender, users.recipient, defaultBroker)
);
expectRevert_Null(callData);
}

function test_RevertGiven_Voided() external whenNoDelegateCall givenNotNull {
bytes memory callData =
abi.encodeCall(flow.depositViaBroker, (defaultStreamId, TOTAL_AMOUNT_WITH_BROKER_FEE_6D, defaultBroker));
bytes memory callData = abi.encodeCall(
flow.depositViaBroker,
(defaultStreamId, TOTAL_AMOUNT_WITH_BROKER_FEE_6D, users.sender, users.recipient, defaultBroker)
);
expectRevert_Voided(callData);
}

function test_RevertWhen_BrokerFeeGreaterThanMaxFee() external whenNoDelegateCall givenNotNull givenNotVoided {
function test_RevertWhen_SenderNotMatch(address otherSender)
external
whenNoDelegateCall
givenNotNull
givenNotVoided
{
vm.assume(otherSender != users.sender);
vm.expectRevert(abi.encodeWithSelector(Errors.SablierFlow_NotStreamSender.selector, otherSender, users.sender));
flow.depositViaBroker(
defaultStreamId, TOTAL_AMOUNT_WITH_BROKER_FEE_6D, otherSender, users.recipient, defaultBroker
);
}

function test_RevertWhen_RecipientNotMatch(address otherRecipient)
external
whenNoDelegateCall
givenNotNull
givenNotVoided
whenSenderMatches
{
vm.assume(otherRecipient != users.recipient);
vm.expectRevert(
abi.encodeWithSelector(Errors.SablierFlow_NotStreamRecipient.selector, otherRecipient, users.recipient)
);
flow.depositViaBroker(
defaultStreamId, TOTAL_AMOUNT_WITH_BROKER_FEE_6D, users.sender, otherRecipient, defaultBroker
);
}

function test_RevertWhen_BrokerFeeGreaterThanMaxFee()
external
whenNoDelegateCall
givenNotNull
givenNotVoided
whenSenderMatches
whenRecipientMatches
{
defaultBroker.fee = MAX_FEE.add(ud(1));
vm.expectRevert(
abi.encodeWithSelector(Errors.SablierFlow_BrokerFeeTooHigh.selector, defaultBroker.fee, MAX_FEE)
);
flow.depositViaBroker(defaultStreamId, TOTAL_AMOUNT_WITH_BROKER_FEE_6D, defaultBroker);
flow.depositViaBroker(
defaultStreamId, TOTAL_AMOUNT_WITH_BROKER_FEE_6D, users.sender, users.recipient, defaultBroker
);
}

function test_RevertWhen_BrokeAddressZero()
external
whenNoDelegateCall
givenNotNull
givenNotVoided
whenSenderMatches
whenRecipientMatches
whenBrokerFeeNotGreaterThanMaxFee
{
defaultBroker.account = address(0);
vm.expectRevert(Errors.SablierFlow_BrokerAddressZero.selector);
flow.depositViaBroker(defaultStreamId, TOTAL_AMOUNT_WITH_BROKER_FEE_6D, defaultBroker);
flow.depositViaBroker(
defaultStreamId, TOTAL_AMOUNT_WITH_BROKER_FEE_6D, users.sender, users.recipient, defaultBroker
);
}

function test_RevertWhen_TotalAmountZero()
external
whenNoDelegateCall
givenNotNull
givenNotVoided
whenSenderMatches
whenRecipientMatches
whenBrokerFeeNotGreaterThanMaxFee
whenBrokerAddressNotZero
{
vm.expectRevert(abi.encodeWithSelector(Errors.SablierFlow_DepositAmountZero.selector, defaultStreamId));

flow.depositViaBroker(defaultStreamId, 0, defaultBroker);
flow.depositViaBroker(defaultStreamId, 0, users.sender, users.recipient, defaultBroker);
}

function test_WhenTokenMissesERC20Return()
external
whenNoDelegateCall
givenNotNull
givenNotVoided
whenSenderMatches
whenRecipientMatches
whenBrokerFeeNotGreaterThanMaxFee
whenBrokerAddressNotZero
whenTotalAmountNotZero
Expand All @@ -87,6 +139,8 @@ contract DepositViaBroker_Integration_Concrete_Test is Integration_Test {
whenNoDelegateCall
givenNotNull
givenNotVoided
whenSenderMatches
whenRecipientMatches
whenBrokerFeeNotGreaterThanMaxFee
whenBrokerAddressNotZero
whenTotalAmountNotZero
Expand All @@ -107,6 +161,8 @@ contract DepositViaBroker_Integration_Concrete_Test is Integration_Test {
whenNoDelegateCall
givenNotNull
givenNotVoided
whenSenderMatches
whenRecipientMatches
whenBrokerFeeNotGreaterThanMaxFee
whenBrokerAddressNotZero
whenTotalAmountNotZero
Expand Down Expand Up @@ -148,7 +204,7 @@ contract DepositViaBroker_Integration_Concrete_Test is Integration_Test {
expectCallToTransferFrom({ token: token, from: users.sender, to: address(flow), amount: depositAmount });
expectCallToTransferFrom({ token: token, from: users.sender, to: users.broker, amount: brokerFeeAmount });

flow.depositViaBroker(streamId, totalAmount, defaultBroker);
flow.depositViaBroker(streamId, totalAmount, users.sender, users.recipient, defaultBroker);

// It should update the stream balance
uint128 actualStreamBalance = flow.getBalance(streamId);
Expand Down
Loading

0 comments on commit 0974651

Please sign in to comment.