Skip to content

Commit

Permalink
feat: refundMax
Browse files Browse the repository at this point in the history
  • Loading branch information
smol-ninja committed Oct 28, 2024
1 parent 72c2d34 commit 7f2dd50
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 15 deletions.
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}.
///
/// @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");
}
}

0 comments on commit 7f2dd50

Please sign in to comment.