Skip to content

Commit

Permalink
refactor: add "scaled" in ongoing and snapshot debt
Browse files Browse the repository at this point in the history
test: update tests accordingly
  • Loading branch information
andreivladbrg committed Oct 15, 2024
1 parent 8778889 commit 7ac5945
Show file tree
Hide file tree
Showing 27 changed files with 112 additions and 106 deletions.
5 changes: 3 additions & 2 deletions TECHNICAL-DOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ pause it or void it at a later date.
A stream is represented by a struct, which can be found in
[`DataTypes.sol`](https://github.com/sablier-labs/flow/blob/ba1c9ba64907200c82ccfaeaa6ab91f6229c433d/src/types/DataTypes.sol#L41-L76).

The debt is tracked using `snapshotDebt` and `snapshotTime`. At snapshot, the following events are taking place:
The debt is tracked using `snapshotDebtScaled` and `snapshotTime`. At snapshot, the following events are taking place:

1. `snapshotDebt` is incremented by `ongoingDebt` where `ongoingDebt = rps * (block.timestamp - snapshotTime)`.
1. `snapshotDebtScaled` is incremented by `ongoingDebtScaled` where
`ongoingDebtScaled = rps * (block.timestamp - snapshotTime)`.
2. `snapshotTime` is updated to `block.timestamp`.

The recipient can withdraw the streamed amount at any point. However, if there aren't sufficient funds, the recipient
Expand Down
43 changes: 23 additions & 20 deletions src/SablierFlow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ contract SablierFlow is
uint8 tokenDecimals = _streams[streamId].tokenDecimals;
uint256 scaledBalance = Helpers.scaleAmount({ amount: balance, decimals: tokenDecimals });

uint256 snapshotDebt = _streams[streamId].snapshotDebt;
uint256 snapshotDebtScaled = _streams[streamId].snapshotDebtScaled;

// If the stream has uncovered debt, return zero.
if (snapshotDebt + _scaledOngoingDebtOf(streamId) > scaledBalance) {
if (snapshotDebtScaled + _ongoingDebtScaledOf(streamId) > scaledBalance) {
return 0;
}

Expand All @@ -85,19 +85,22 @@ contract SablierFlow is
// Safe to use unchecked because the calculations cannot overflow or underflow.
unchecked {
uint256 solvencyAmount =
scaledBalance - snapshotDebt + Helpers.scaleAmount({ amount: 1, decimals: tokenDecimals });
scaledBalance - snapshotDebtScaled + Helpers.scaleAmount({ amount: 1, decimals: tokenDecimals });
uint256 solvencyPeriod = solvencyAmount / _streams[streamId].ratePerSecond.unwrap();

depletionTime = _streams[streamId].snapshotTime + solvencyPeriod;
}
}

/// @inheritdoc ISablierFlow
function ongoingDebtOf(uint256 streamId) external view override notNull(streamId) returns (uint256 ongoingDebt) {
ongoingDebt = Helpers.descaleAmount({
amount: _scaledOngoingDebtOf(streamId),
decimals: _streams[streamId].tokenDecimals
});
function ongoingDebtScaledOf(uint256 streamId)
external
view
override
notNull(streamId)
returns (uint256 ongoingDebtScaled)
{
ongoingDebtScaled = _ongoingDebtScaledOf(streamId);
}

/// @inheritdoc ISablierFlow
Expand Down Expand Up @@ -451,7 +454,7 @@ contract SablierFlow is

/// @dev Calculates the ongoing debt, as a 18-decimals fixed point number, accrued since last snapshot. Return 0 if
/// the stream is paused or `block.timestamp` is less than or equal to snapshot time.
function _scaledOngoingDebtOf(uint256 streamId) internal view returns (uint256) {
function _ongoingDebtScaledOf(uint256 streamId) internal view returns (uint256) {
uint256 blockTimestamp = block.timestamp;
uint256 snapshotTime = _streams[streamId].snapshotTime;

Expand Down Expand Up @@ -483,7 +486,7 @@ contract SablierFlow is
/// @dev The total debt is the sum of the snapshot debt and the ongoing debt. This value is independent of the
/// stream's balance.
function _totalDebtOf(uint256 streamId) internal view returns (uint256) {
uint256 scaledTotalDebt = _scaledOngoingDebtOf(streamId) + _streams[streamId].snapshotDebt;
uint256 scaledTotalDebt = _ongoingDebtScaledOf(streamId) + _streams[streamId].snapshotDebtScaled;
return Helpers.descaleAmount({ amount: scaledTotalDebt, decimals: _streams[streamId].tokenDecimals });
}

Expand Down Expand Up @@ -511,12 +514,12 @@ contract SablierFlow is
revert Errors.SablierFlow_RatePerSecondNotDifferent(streamId, newRatePerSecond);
}

uint256 scaledOngoingDebt = _scaledOngoingDebtOf(streamId);
uint256 scaledOngoingDebt = _ongoingDebtScaledOf(streamId);

// Update the snapshot debt only if the stream has ongoing debt.
if (scaledOngoingDebt > 0) {
// Effect: update the snapshot debt.
_streams[streamId].snapshotDebt += scaledOngoingDebt;
_streams[streamId].snapshotDebtScaled += scaledOngoingDebt;
}

// Effect: update the snapshot time.
Expand Down Expand Up @@ -560,7 +563,7 @@ contract SablierFlow is
isVoided: false,
ratePerSecond: ratePerSecond,
sender: sender,
snapshotDebt: 0,
snapshotDebtScaled: 0,
snapshotTime: uint40(block.timestamp),
token: token,
tokenDecimals: tokenDecimals
Expand Down Expand Up @@ -701,16 +704,16 @@ contract SablierFlow is

// If the stream is solvent, update the total debt normally.
if (debtToWriteOff == 0) {
uint256 scaledOngoingDebt = _scaledOngoingDebtOf(streamId);
uint256 scaledOngoingDebt = _ongoingDebtScaledOf(streamId);
if (scaledOngoingDebt > 0) {
// Effect: Update the snapshot debt by adding the ongoing debt.
_streams[streamId].snapshotDebt += scaledOngoingDebt;
_streams[streamId].snapshotDebtScaled += scaledOngoingDebt;
}
}
// If the stream is insolvent, write off the uncovered debt.
else {
// Effect: update the total debt by setting snapshot debt to the stream balance.
_streams[streamId].snapshotDebt =
_streams[streamId].snapshotDebtScaled =
Helpers.scaleAmount({ amount: _streams[streamId].balance, decimals: _streams[streamId].tokenDecimals });
}

Expand Down Expand Up @@ -762,7 +765,7 @@ contract SablierFlow is
uint8 tokenDecimals = _streams[streamId].tokenDecimals;

// Calculate the total debt.
uint256 scaledTotalDebt = _scaledOngoingDebtOf(streamId) + _streams[streamId].snapshotDebt;
uint256 scaledTotalDebt = _ongoingDebtScaledOf(streamId) + _streams[streamId].snapshotDebtScaled;
uint256 totalDebt = Helpers.descaleAmount(scaledTotalDebt, tokenDecimals);

// Calculate the withdrawable amount.
Expand All @@ -789,13 +792,13 @@ contract SablierFlow is
unchecked {
// If the amount is less than the snapshot debt, reduce it from the snapshot debt and leave the snapshot
// time unchanged.
if (scaledAmount <= _streams[streamId].snapshotDebt) {
_streams[streamId].snapshotDebt -= scaledAmount;
if (scaledAmount <= _streams[streamId].snapshotDebtScaled) {
_streams[streamId].snapshotDebtScaled -= scaledAmount;
}
// Else reduce the amount from the ongoing debt by setting snapshot time to `block.timestamp` and set the
// snapshot debt to the remaining total debt.
else {
_streams[streamId].snapshotDebt = scaledTotalDebt - scaledAmount;
_streams[streamId].snapshotDebtScaled = scaledTotalDebt - scaledAmount;

// Effect: update the stream time.
_streams[streamId].snapshotTime = uint40(block.timestamp);
Expand Down
6 changes: 3 additions & 3 deletions src/abstracts/SablierFlowBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,14 @@ abstract contract SablierFlowBase is
}

/// @inheritdoc ISablierFlowBase
function getSnapshotDebt(uint256 streamId)
function getSnapshotDebtScaled(uint256 streamId)
external
view
override
notNull(streamId)
returns (uint256 snapshotDebt)
returns (uint256 snapshotDebtScaled)
{
snapshotDebt = _streams[streamId].snapshotDebt;
snapshotDebtScaled = _streams[streamId].snapshotDebtScaled;
}

/// @inheritdoc ISablierFlowBase
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/ISablierFlow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ interface ISablierFlow is
/// @notice Returns the amount of debt accrued since the snapshot time until now, denoted in token's decimals.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream ID for the query.
function ongoingDebtOf(uint256 streamId) external view returns (uint256 ongoingDebt);
function ongoingDebtScaledOf(uint256 streamId) external view returns (uint256 ongoingDebtScaled);

/// @notice Returns the amount that the sender can be refunded from the stream, denoted in token's decimals.
/// @dev Reverts if `streamId` references a null stream.
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/ISablierFlowBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ interface ISablierFlowBase is
/// @notice Retrieves the snapshot debt of the stream, denoted as a fixed-point number where 1e18 is 1 token.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream ID for the query.
function getSnapshotDebt(uint256 streamId) external view returns (uint256 snapshotDebt);
function getSnapshotDebtScaled(uint256 streamId) external view returns (uint256 snapshotDebtScaled);

/// @notice Retrieves the snapshot time of the stream, which is a Unix timestamp.
/// @dev Reverts if `streamId` references a null stream.
Expand Down
8 changes: 4 additions & 4 deletions src/types/DataTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ library Flow {
/// be restarted. Voiding an insolvent stream sets its uncovered debt to zero.
/// @param token The contract address of the ERC-20 token to stream.
/// @param tokenDecimals The decimals of the ERC-20 token to stream.
/// @param snapshotDebt The amount of tokens that the sender owed to the recipient at snapshot time, denoted as a
/// 18-decimals fixed-point number. This, along with the ongoing debt, can be used to calculate the total debt at
/// any given point in time.
/// @param snapshotDebtScaled The amount of tokens that the sender owed to the recipient at snapshot time, denoted
/// as a 18-decimals fixed-point number. This, along with the ongoing debt, can be used to calculate the total debt
/// at any given point in time.
struct Stream {
// slot 0
uint128 balance;
Expand All @@ -72,6 +72,6 @@ library Flow {
IERC20 token;
uint8 tokenDecimals;
// slot 3
uint256 snapshotDebt;
uint256 snapshotDebtScaled;
}
}
13 changes: 6 additions & 7 deletions tests/fork/Flow.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -227,14 +227,13 @@ contract Flow_Fork_Test is Fork_Test {
newRatePerSecond = ud21x18(newRatePerSecond.unwrap() + 1);
}

uint256 beforeSnapshotAmount = flow.getSnapshotDebt(streamId);
uint256 beforeSnapshotAmount = flow.getSnapshotDebtScaled(streamId);
uint256 totalDebt = flow.totalDebtOf(streamId);

// Compute the snapshot time that will be stored post withdraw.
vars.expectedSnapshotTime = getBlockTimestamp();

uint256 scaledOngoingDebt =
calculateScaledOngoingDebt(flow.getRatePerSecond(streamId).unwrap(), flow.getSnapshotTime(streamId));
uint256 ongoingDebtScaled = flow.ongoingDebtScaledOf(streamId);

// It should emit 1 {AdjustFlowStream}, 1 {MetadataUpdate} events.
vm.expectEmit({ emitter: address(flow) });
Expand All @@ -251,8 +250,8 @@ contract Flow_Fork_Test is Fork_Test {
flow.adjustRatePerSecond({ streamId: streamId, newRatePerSecond: newRatePerSecond });

// It should update snapshot debt.
vars.actualSnapshotDebt = flow.getSnapshotDebt(streamId);
vars.expectedSnapshotDebt = scaledOngoingDebt + beforeSnapshotAmount;
vars.actualSnapshotDebt = flow.getSnapshotDebtScaled(streamId);
vars.expectedSnapshotDebt = ongoingDebtScaled + beforeSnapshotAmount;
assertEq(vars.actualSnapshotDebt, vars.expectedSnapshotDebt, "AdjustRatePerSecond: snapshot debt");

// It should set the new rate per second
Expand Down Expand Up @@ -304,7 +303,7 @@ contract Flow_Fork_Test is Fork_Test {
isTransferable: transferable,
snapshotTime: getBlockTimestamp(),
ratePerSecond: ratePerSecond,
snapshotDebt: 0,
snapshotDebtScaled: 0,
sender: sender,
token: token,
tokenDecimals: IERC20Metadata(address(token)).decimals()
Expand Down Expand Up @@ -564,7 +563,7 @@ contract Flow_Fork_Test is Fork_Test {
uint256 totalDebt = flow.totalDebtOf(streamId);

vars.expectedSnapshotTime = withdrawAmount
<= getDescaledAmount(flow.getSnapshotDebt(streamId), flow.getTokenDecimals(streamId))
<= getDescaledAmount(flow.getSnapshotDebtScaled(streamId), flow.getTokenDecimals(streamId))
? flow.getSnapshotTime(streamId)
: getBlockTimestamp();

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 @@ -94,7 +94,7 @@ abstract contract Integration_Test is Base_Test {
isTransferable: TRANSFERABLE,
isVoided: false,
ratePerSecond: RATE_PER_SECOND,
snapshotDebt: 0,
snapshotDebtScaled: 0,
sender: users.sender,
token: usdc,
tokenDecimals: DECIMALS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ contract AdjustRatePerSecond_Integration_Concrete_Test is Integration_Test {
uint40 expectedSnapshotTime = getBlockTimestamp() - ONE_MONTH;
assertEq(actualSnapshotTime, expectedSnapshotTime, "snapshot time");

uint256 actualSnapshotDebt = flow.getSnapshotDebt(defaultStreamId);
uint256 actualSnapshotDebt = flow.getSnapshotDebtScaled(defaultStreamId);
uint128 expectedSnapshotDebt = 0;
assertEq(actualSnapshotDebt, expectedSnapshotDebt, "snapshot debt");

Expand All @@ -118,7 +118,7 @@ contract AdjustRatePerSecond_Integration_Concrete_Test is Integration_Test {
assertEq(uint8(flow.statusOf(defaultStreamId)), uint8(Flow.Status.STREAMING_SOLVENT), "status not streaming");

// It should update snapshot debt.
actualSnapshotDebt = flow.getSnapshotDebt(defaultStreamId);
actualSnapshotDebt = flow.getSnapshotDebtScaled(defaultStreamId);
expectedSnapshotDebt = ONE_MONTH_DEBT_18D;
assertEq(actualSnapshotDebt, expectedSnapshotDebt, "snapshot debt");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ contract DepositAndPause_Integration_Concrete_Test is Integration_Test {

function test_WhenCallerSender() external whenNoDelegateCall givenNotNull givenNotPaused {
uint128 previousStreamBalance = flow.getBalance(defaultStreamId);
uint256 expectedSnapshotDebt =
calculateScaledOngoingDebt(RATE_PER_SECOND_U128, flow.getSnapshotTime(defaultStreamId));
uint256 expectedSnapshotDebt = flow.ongoingDebtScaledOf(defaultStreamId);

// It should emit 1 {Transfer}, 1 {DepositFlowStream}, 1 {PauseFlowStream}, 1 {MetadataUpdate} events
vm.expectEmit({ emitter: address(usdc) });
Expand Down Expand Up @@ -99,7 +98,7 @@ contract DepositAndPause_Integration_Concrete_Test is Integration_Test {
assertEq(actualRatePerSecond, 0, "rate per second");

// It should update the snapshot debt
uint256 actualSnapshotDebt = flow.getSnapshotDebt(defaultStreamId);
uint256 actualSnapshotDebt = flow.getSnapshotDebtScaled(defaultStreamId);
assertEq(actualSnapshotDebt, expectedSnapshotDebt, "snapshot debt");
}
}
16 changes: 8 additions & 8 deletions tests/integration/concrete/ongoing-debt-of/ongoingDebtOf.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,32 @@ pragma solidity >=0.8.22;

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

contract OngoingDebtOf_Integration_Concrete_Test is Integration_Test {
contract OngoingDebtScaledOf_Integration_Concrete_Test is Integration_Test {
function test_RevertGiven_Null() external {
bytes memory callData = abi.encodeCall(flow.ongoingDebtOf, nullStreamId);
bytes memory callData = abi.encodeCall(flow.ongoingDebtScaledOf, nullStreamId);
expectRevert_Null(callData);
}

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

// It should return zero.
uint256 ongoingDebt = flow.ongoingDebtOf(defaultStreamId);
assertEq(ongoingDebt, 0, "ongoing debt");
uint256 ongoingDebtScaled = flow.ongoingDebtScaledOf(defaultStreamId);
assertEq(ongoingDebtScaled, 0, "ongoing debt");
}

function test_WhenSnapshotTimeInPresent() external givenNotNull givenNotPaused {
// Update the snapshot time and warp the current block timestamp to it.
updateSnapshotTimeAndWarp(defaultStreamId);

// It should return zero.
uint256 ongoingDebt = flow.ongoingDebtOf(defaultStreamId);
assertEq(ongoingDebt, 0, "ongoing debt");
uint256 ongoingDebtScaled = flow.ongoingDebtScaledOf(defaultStreamId);
assertEq(ongoingDebtScaled, 0, "ongoing debt");
}

function test_WhenSnapshotTimeInPast() external view givenNotNull givenNotPaused {
// It should return the correct ongoing debt.
uint256 ongoingDebt = flow.ongoingDebtOf(defaultStreamId);
assertEq(ongoingDebt, ONE_MONTH_DEBT_6D, "ongoing debt");
uint256 ongoingDebtScaled = flow.ongoingDebtScaledOf(defaultStreamId);
assertEq(ongoingDebtScaled, ONE_MONTH_DEBT_18D, "ongoing debt");
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
OngoingDebtOf_Integration_Concrete_Test
OngoingDebtScaledOf_Integration_Concrete_Test
├── given null
│ └── it should revert
└── given not null
Expand Down
5 changes: 2 additions & 3 deletions tests/integration/concrete/pause/pause.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,7 @@ contract Pause_Integration_Concrete_Test is Integration_Test {
}

function _test_Pause() private {
uint256 expectedSnapshotDebt =
calculateScaledOngoingDebt(RATE_PER_SECOND_U128, flow.getSnapshotTime(defaultStreamId));
uint256 expectedSnapshotDebt = flow.ongoingDebtScaledOf(defaultStreamId);

// It should emit 1 {PauseFlowStream}, 1 {MetadataUpdate} events.
vm.expectEmit({ emitter: address(flow) });
Expand All @@ -91,7 +90,7 @@ contract Pause_Integration_Concrete_Test is Integration_Test {
assertEq(actualRatePerSecond, 0, "rate per second");

// It should update the snapshot debt.
uint256 actualSnapshotDebt = flow.getSnapshotDebt(defaultStreamId);
uint256 actualSnapshotDebt = flow.getSnapshotDebtScaled(defaultStreamId);
assertEq(actualSnapshotDebt, expectedSnapshotDebt, "snapshot debt");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ contract RefundAndPause_Integration_Concrete_Test is Integration_Test {
}

function test_WhenCallerSender() external whenNoDelegateCall givenNotNull givenNotPaused {
uint256 expectedSnapshotDebt =
calculateScaledOngoingDebt(RATE_PER_SECOND_U128, flow.getSnapshotTime(defaultStreamId));
uint256 expectedSnapshotDebt = flow.ongoingDebtScaledOf(defaultStreamId);

// It should emit 1 {Transfer}, 1 {RefundFromFlowStream}, 1 {PauseFlowStream}, 1 {MetadataUpdate} events
vm.expectEmit({ emitter: address(usdc) });
Expand Down Expand Up @@ -97,7 +96,7 @@ contract RefundAndPause_Integration_Concrete_Test is Integration_Test {
assertEq(actualRatePerSecond, 0, "rate per second");

// It should update the snapshot debt
uint256 actualSnapshotDebt = flow.getSnapshotDebt(defaultStreamId);
uint256 actualSnapshotDebt = flow.getSnapshotDebtScaled(defaultStreamId);
assertEq(actualSnapshotDebt, expectedSnapshotDebt, "snapshot debt");
}
}
Loading

0 comments on commit 7ac5945

Please sign in to comment.