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

feat: refundMax #324

Merged
merged 2 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
10 changes: 8 additions & 2 deletions benchmark/Flow.Gas.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,14 @@ contract Flow_Gas_Test is Integration_Test {
// {flow.pause}
computeGas("pause", abi.encodeCall(flow.pause, (streamId)));

// {flow.refund}
computeGas("refund", abi.encodeCall(flow.refund, (streamId, REFUND_AMOUNT_6D)));
// {flow.refund} on an incremented stream ID
computeGas("refund", abi.encodeCall(flow.refund, (++streamId, REFUND_AMOUNT_6D)));

// {flow.refundMax} on an incremented stream ID.
computeGas("refundMax", abi.encodeCall(flow.refundMax, (++streamId)));

// Pause the current stream to test the restart function.
flow.pause(streamId);

// {flow.restart}
computeGas("restart", abi.encodeCall(flow.restart, (streamId, RATE_PER_SECOND)));
Expand Down
25 changes: 13 additions & 12 deletions benchmark/results/SablierFlow.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@

| Function | Gas Usage |
| ----------------------------- | --------- |
| `adjustRatePerSecond` | 44171 |
| `create` | 113681 |
| `deposit` | 32975 |
| `depositViaBroker` | 22732 |
| `pause` | 7522 |
| `refund` | 11939 |
| `restart` | 7036 |
| `void (solvent stream)` | 10060 |
| `void (insolvent stream)` | 37460 |
| `withdraw (insolvent stream)` | 57688 |
| `withdraw (solvent stream)` | 38156 |
| `withdrawMax` | 51988 |
| `adjustRatePerSecond` | 44193 |
| `create` | 113703 |
| `deposit` | 32997 |
| `depositViaBroker` | 22754 |
| `pause` | 7544 |
| `refund` | 22842 |
| `refundMax` | 23840 |
| `restart` | 7058 |
| `void (solvent stream)` | 9982 |
| `void (insolvent stream)` | 37482 |
| `withdraw (insolvent stream)` | 57711 |
| `withdraw (solvent stream)` | 38178 |
| `withdrawMax` | 52010 |
2 changes: 1 addition & 1 deletion precompiles/Precompiles.sol

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions src/SablierFlow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,21 @@ contract SablierFlow is
_pause(streamId);
}

/// @inheritdoc ISablierFlow
function refundMax(uint256 streamId)
external
override
noDelegateCall
notNull(streamId)
onlySender(streamId)
updateMetadata(streamId)
{
uint128 refundableAmount = _refundableAmountOf(streamId);

// Checks, Effects, and Interactions: make the refund.
_refund(streamId, refundableAmount);
}

/// @inheritdoc ISablierFlow
function restart(
uint256 streamId,
Expand Down
10 changes: 10 additions & 0 deletions src/interfaces/ISablierFlow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,16 @@ interface ISablierFlow is
/// @param amount The amount to refund, denoted in token's decimals.
function refundAndPause(uint256 streamId, uint128 amount) external;

/// @notice Refunds the entire refundable amount of tokens from the stream to the sender's address.
///
/// @dev Emits {Transfer} and {RefundFromFlowStream} events.
///
/// Requirements:
/// - Refer to the notes in {refund}.
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
///
/// @param streamId The ID of the stream to refund from.
function refundMax(uint256 streamId) external;

/// @notice Restarts the stream with the provided rate per second.
///
/// @dev Emits {RestartFlowStream} event.
Expand Down
77 changes: 77 additions & 0 deletions tests/integration/concrete/refund-max/refundMax.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.22;

import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import { ISablierFlow } from "src/interfaces/ISablierFlow.sol";

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

contract RefundMax_Integration_Concrete_Test is Integration_Test {
function setUp() public override {
Integration_Test.setUp();

// Deposit to the default stream.
depositToDefaultStream();
}

function test_RevertWhen_DelegateCall() external {
bytes memory callData = abi.encodeCall(flow.refundMax, (defaultStreamId));
expectRevert_DelegateCall(callData);
}

function test_RevertGiven_Null() external whenNoDelegateCall {
bytes memory callData = abi.encodeCall(flow.refundMax, (nullStreamId));
expectRevert_Null(callData);
}

function test_RevertWhen_CallerRecipient() external whenNoDelegateCall givenNotNull whenCallerNotSender {
bytes memory callData = abi.encodeCall(flow.refundMax, (defaultStreamId));
expectRevert_CallerRecipient(callData);
}

function test_RevertWhen_CallerMaliciousThirdParty() external whenNoDelegateCall givenNotNull whenCallerNotSender {
bytes memory callData = abi.encodeCall(flow.refundMax, (defaultStreamId));
expectRevert_CallerMaliciousThirdParty(callData);
}

function test_GivenPaused() external whenNoDelegateCall givenNotNull whenCallerSender {
flow.pause(defaultStreamId);

// It should make the refund.
_test_RefundMax({ streamId: defaultStreamId, token: usdc, depositedAmount: DEPOSIT_AMOUNT_6D });
}

function test_GivenNotPaused() external whenNoDelegateCall givenNotNull whenCallerSender {
// It should make the refund.
_test_RefundMax({ streamId: defaultStreamId, token: usdc, depositedAmount: DEPOSIT_AMOUNT_6D });
}

function _test_RefundMax(uint256 streamId, IERC20 token, uint128 depositedAmount) private {
uint256 previousAggregateAmount = flow.aggregateBalance(token);
uint128 refundableAmount = flow.refundableAmountOf(streamId);

// It should emit 1 {Transfer}, 1 {RefundFromFlowStream}, 1 {MetadataUpdate} events.
vm.expectEmit({ emitter: address(token) });
emit IERC20.Transfer({ from: address(flow), to: users.sender, value: refundableAmount });

vm.expectEmit({ emitter: address(flow) });
emit ISablierFlow.RefundFromFlowStream({ streamId: streamId, sender: users.sender, amount: refundableAmount });

vm.expectEmit({ emitter: address(flow) });
emit IERC4906.MetadataUpdate({ _tokenId: streamId });

// It should perform the ERC-20 transfer.
expectCallToTransfer({ token: token, to: users.sender, amount: refundableAmount });
flow.refundMax(streamId);

// It should update the stream balance.
uint128 actualStreamBalance = flow.getBalance(streamId);
uint128 expectedStreamBalance = depositedAmount - refundableAmount;
assertEq(actualStreamBalance, expectedStreamBalance, "stream balance");

// It should decrease the aggregate amount.
assertEq(flow.aggregateBalance(token), previousAggregateAmount - refundableAmount, "aggregate amount");
}
}
21 changes: 21 additions & 0 deletions tests/integration/concrete/refund-max/refundMax.tree
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
RefundMax_Integration_Concrete_Test
├── when delegate call
│ └── it should revert
└── when no delegate call
├── given null
│ └── it should revert
└── given not null
├── when caller not sender
│ ├── when caller recipient
│ │ └── it should revert
│ └── when caller malicious third party
│ └── it should revert
└── when caller sender
├── given paused
│ └── it should make the refund
└── given not paused
├── it should make the refund
├── it should update the stream balance
├── it should decrease the aggregate amount
├── it should perform the ERC20 transfer
└── it should emit 1 {Transfer}, 1 {RefundFromFlowStream}, 1 {MetadataUpdate} event
76 changes: 76 additions & 0 deletions tests/integration/fuzz/refundMax.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.22;

import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import { ISablierFlow } from "src/interfaces/ISablierFlow.sol";

import { Shared_Integration_Fuzz_Test } from "./Fuzz.t.sol";

contract RefundMax_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test {
/// @dev Checklist:
/// - It should refund the refundable amount of tokens from a stream.
/// - It should emit the following events: {Transfer}, {MetadataUpdate}, {RefundFromFlowStream}
///
/// Given enough runs, all of the following scenarios should be fuzzed:
/// - Multiple streams to refund from, each with different token decimals and rate per second.
/// - Multiple points in time prior to depletion period.
function testFuzz_RefundMax(
uint256 streamId,
uint40 timeJump,
uint8 decimals
)
external
whenNoDelegateCall
givenNotNull
{
(streamId,,) = useFuzzedStreamOrCreate(streamId, decimals);

// Bound the time jump so that it is less than the depletion timestamp.
uint40 depletionPeriod = uint40(flow.depletionTimeOf(streamId));
timeJump = boundUint40(timeJump, getBlockTimestamp(), depletionPeriod - 1);

// Simulate the passage of time.
vm.warp({ newTimestamp: timeJump });

uint128 refundableAmount = flow.refundableAmountOf(streamId);

// Ensure refundable amount is not zero. It could be zero for a small time range upto the depletion time due to
// precision error.
vm.assume(refundableAmount != 0);

// Following variables are used during assertions.
uint256 initialAggregateAmount = flow.aggregateBalance(token);
uint256 initialTokenBalance = token.balanceOf(address(flow));
uint128 initialStreamBalance = flow.getBalance(streamId);

// Expect the relevant events to be emitted.
vm.expectEmit({ emitter: address(token) });
emit IERC20.Transfer({ from: address(flow), to: users.sender, value: refundableAmount });

vm.expectEmit({ emitter: address(flow) });
emit ISablierFlow.RefundFromFlowStream({ streamId: streamId, sender: users.sender, amount: refundableAmount });

vm.expectEmit({ emitter: address(flow) });
emit IERC4906.MetadataUpdate({ _tokenId: streamId });

// Request the maximum refund.
flow.refundMax(streamId);

// Assert that the token balance of stream has been updated.
uint256 actualTokenBalance = token.balanceOf(address(flow));
uint256 expectedTokenBalance = initialTokenBalance - refundableAmount;
assertEq(actualTokenBalance, expectedTokenBalance, "token balanceOf");

// Assert that stored balance in stream has been updated.
uint256 actualStreamBalance = flow.getBalance(streamId);
uint256 expectedStreamBalance = initialStreamBalance - refundableAmount;
assertEq(actualStreamBalance, expectedStreamBalance, "stream balance");

// Assert that the aggregate amount has been updated.
uint256 actualAggregateAmount = flow.aggregateBalance(token);
uint256 expectedAggregateAmount = initialAggregateAmount - refundableAmount;
assertEq(actualAggregateAmount, expectedAggregateAmount, "aggregate amount");
}
}
Loading