Skip to content

Commit

Permalink
fix: use uint256 for scaledOngoingDebt
Browse files Browse the repository at this point in the history
fix: add notNull in isTransferable getter
fix: use uint256 in depletionTimeOf
  • Loading branch information
andreivladbrg committed Oct 11, 2024
1 parent fbf6ff5 commit fa1b1af
Show file tree
Hide file tree
Showing 31 changed files with 128 additions and 122 deletions.
58 changes: 32 additions & 26 deletions src/SablierFlow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ contract SablierFlow is
override
notNull(streamId)
notPaused(streamId)
returns (uint40 depletionTime)
returns (uint256 depletionTime)
{
uint128 balance = _streams[streamId].balance;

Expand All @@ -76,8 +76,8 @@ contract SablierFlow is
return 0;
}

uint8 tokenDecimals = _streams[streamId].tokenDecimals;
uint128 solvencyAmount;
uint256 tokenDecimals = _streams[streamId].tokenDecimals;
uint256 solvencyAmount;

// Depletion time is defined as the UNIX timestamp beyond which the total debt exceeds stream balance.
// So we calculate it by solving: debt at depletion time = stream balance + 1. This ensures that we find the
Expand All @@ -87,16 +87,16 @@ contract SablierFlow is
if (tokenDecimals == 18) {
solvencyAmount = (balance - snapshotDebt + 1);
} else {
uint128 scaleFactor = (10 ** (18 - tokenDecimals)).toUint128();
uint256 scaleFactor = (10 ** (18 - tokenDecimals));
solvencyAmount = (balance - snapshotDebt + 1) * scaleFactor;
}
uint256 solvencyPeriod = solvencyAmount / _streams[streamId].ratePerSecond.unwrap();
return _streams[streamId].snapshotTime + solvencyPeriod.toUint40();
return _streams[streamId].snapshotTime + solvencyPeriod;
}
}

/// @inheritdoc ISablierFlow
function ongoingDebtOf(uint256 streamId) external view override notNull(streamId) returns (uint128 ongoingDebt) {
function ongoingDebtOf(uint256 streamId) external view override notNull(streamId) returns (uint256 ongoingDebt) {
ongoingDebt = _ongoingDebtOf(streamId);
}

Expand Down Expand Up @@ -141,7 +141,7 @@ contract SablierFlow is
}

/// @inheritdoc ISablierFlow
function totalDebtOf(uint256 streamId) external view override notNull(streamId) returns (uint128 totalDebt) {
function totalDebtOf(uint256 streamId) external view override notNull(streamId) returns (uint256 totalDebt) {
totalDebt = _totalDebtOf(streamId);
}

Expand All @@ -151,7 +151,7 @@ contract SablierFlow is
view
override
notNull(streamId)
returns (uint128 uncoveredDebt)
returns (uint256 uncoveredDebt)
{
uncoveredDebt = _uncoveredDebtOf(streamId);
}
Expand Down Expand Up @@ -437,41 +437,42 @@ contract SablierFlow is
return 0;
}

uint128 totalDebt = _totalDebtOf(streamId);
uint256 totalDebt = _totalDebtOf(streamId);

// If the stream balance is less than or equal to the total debt, return the stream balance.
if (balance < totalDebt) {
return balance;
}

return totalDebt;
// At this point, the total debt fits within `uint128`, as it is less than or equal to the balance.
return totalDebt.toUint128();
}

/// @dev Calculates the ongoing debt accrued since last snapshot. Return 0 if the stream is paused or
/// `block.timestamp` is less than or equal to snapshot time.
function _ongoingDebtOf(uint256 streamId) internal view returns (uint128 ongoingDebt) {
function _ongoingDebtOf(uint256 streamId) internal view returns (uint256 ongoingDebt) {
uint40 blockTimestamp = uint40(block.timestamp);
uint40 snapshotTime = _streams[streamId].snapshotTime;

uint128 ratePerSecond = _streams[streamId].ratePerSecond.unwrap();
uint256 ratePerSecond = _streams[streamId].ratePerSecond.unwrap();

// Check:if the rate per second is zero or the `block.timestamp` is less than the `snapshotTime`.
if (ratePerSecond == 0 || blockTimestamp <= snapshotTime) {
return 0;
}

uint128 elapsedTime;
uint256 elapsedTime;

// Safe to use unchecked because subtraction cannot underflow.
unchecked {
// Calculate time elapsed since the last snapshot.
elapsedTime = blockTimestamp - snapshotTime;
}

uint8 tokenDecimals = _streams[streamId].tokenDecimals;

// Calculate the ongoing debt accrued by multiplying the elapsed time by the rate per second.
uint128 scaledOngoingDebt = elapsedTime * ratePerSecond;
uint256 scaledOngoingDebt = elapsedTime * ratePerSecond;

uint8 tokenDecimals = _streams[streamId].tokenDecimals;

// If the token decimals are 18, return the scaled ongoing debt and the `block.timestamp`.
if (tokenDecimals == 18) {
Expand All @@ -480,7 +481,7 @@ contract SablierFlow is

// Safe to use unchecked because we use {SafeCast}.
unchecked {
uint128 scaleFactor = (10 ** (18 - tokenDecimals)).toUint128();
uint256 scaleFactor = 10 ** (18 - tokenDecimals);
// Since debt is denoted in token decimals, descale the amount.
ongoingDebt = scaledOngoingDebt / scaleFactor;
}
Expand All @@ -494,16 +495,16 @@ contract SablierFlow is
/// @notice Calculates the total debt.
/// @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 (uint128) {
function _totalDebtOf(uint256 streamId) internal view returns (uint256) {
// Calculate the total debt.
return _streams[streamId].snapshotDebt + _ongoingDebtOf(streamId);
}

/// @dev Calculates the uncovered debt.
function _uncoveredDebtOf(uint256 streamId) internal view returns (uint128) {
function _uncoveredDebtOf(uint256 streamId) internal view returns (uint256) {
uint128 balance = _streams[streamId].balance;

uint128 totalDebt = _totalDebtOf(streamId);
uint256 totalDebt = _totalDebtOf(streamId);

if (balance < totalDebt) {
return totalDebt - balance;
Expand All @@ -523,7 +524,7 @@ contract SablierFlow is
revert Errors.SablierFlow_RatePerSecondNotDifferent(streamId, newRatePerSecond);
}

uint128 ongoingDebt = _ongoingDebtOf(streamId);
uint128 ongoingDebt = _ongoingDebtOf(streamId).toUint128();

// Update the snapshot debt only if the stream has ongoing debt.
if (ongoingDebt > 0) {
Expand Down Expand Up @@ -709,11 +710,12 @@ contract SablierFlow is
revert Errors.SablierFlow_Unauthorized({ streamId: streamId, caller: msg.sender });
}

uint128 debtToWriteOff = _uncoveredDebtOf(streamId);
uint256 debtToWriteOff = _uncoveredDebtOf(streamId);

// If the stream is solvent, update the total debt normally.
if (debtToWriteOff == 0) {
uint128 ongoingDebt = _ongoingDebtOf(streamId);
// It is safe to downcast because if there is no uncovered debt, the ongoing debt fits within `uint128`.
uint128 ongoingDebt = _ongoingDebtOf(streamId).toUint128();
if (ongoingDebt > 0) {
// Effect: Update the snapshot debt by adding the ongoing debt.
_streams[streamId].snapshotDebt += ongoingDebt;
Expand Down Expand Up @@ -764,7 +766,8 @@ contract SablierFlow is
}

// Calculate the total debt.
uint128 totalDebt = _totalDebtOf(streamId);
uint256 totalDebt = _totalDebtOf(streamId);
uint128 totalDebtU128;

// Calculate the withdrawable amount.
uint128 balance = _streams[streamId].balance;
Expand All @@ -774,8 +777,11 @@ contract SablierFlow is
// If the stream balance is less than the total debt, the withdrawable amount is the balance.
withdrawableAmount = balance;
} else {
// If the balance is greater than or equal to the total debt, it should fit within `uint128`.
totalDebtU128 = totalDebt.toUint128();

// Otherwise, the withdrawable amount is the total debt.
withdrawableAmount = totalDebt;
withdrawableAmount = totalDebtU128;
}

// Check: the withdraw amount is not greater than the withdrawable amount.
Expand All @@ -793,7 +799,7 @@ contract SablierFlow is
// 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 = totalDebt - amount;
_streams[streamId].snapshotDebt = totalDebtU128 - amount;

// Effect: update the stream time.
_streams[streamId].snapshotTime = uint40(block.timestamp);
Expand Down
2 changes: 1 addition & 1 deletion src/abstracts/SablierFlowBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ abstract contract SablierFlowBase is
}

/// @inheritdoc ISablierFlowBase
function isTransferable(uint256 streamId) external view override returns (bool result) {
function isTransferable(uint256 streamId) external view override notNull(streamId) returns (bool result) {
result = _streams[streamId].isTransferable;
}

Expand Down
16 changes: 8 additions & 8 deletions src/interfaces/ISablierFlow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface ISablierFlow is
/// @param newRatePerSecond The new rate per second, denoted as a fixed-point number where 1e18 is 1 token
/// per second.
event AdjustFlowStream(
uint256 indexed streamId, uint128 totalDebt, UD21x18 oldRatePerSecond, UD21x18 newRatePerSecond
uint256 indexed streamId, uint256 totalDebt, UD21x18 oldRatePerSecond, UD21x18 newRatePerSecond
);

/// @notice Emitted when a Flow stream is created.
Expand Down Expand Up @@ -56,7 +56,7 @@ interface ISablierFlow is
/// @param recipient The address of the stream's recipient.
/// @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, uint128 totalDebt
uint256 indexed streamId, address indexed sender, address indexed recipient, uint256 totalDebt
);

/// @notice Emitted when a sender is refunded from a stream.
Expand Down Expand Up @@ -84,8 +84,8 @@ interface ISablierFlow is
address indexed sender,
address indexed recipient,
address caller,
uint128 newTotalDebt,
uint128 writtenOffDebt
uint256 newTotalDebt,
uint256 writtenOffDebt
);

/// @notice Emitted when tokens are withdrawn from a stream by a recipient or an approved operator.
Expand Down Expand Up @@ -119,12 +119,12 @@ interface ISablierFlow is
/// there already is uncovered debt, it returns zero.
/// @dev Reverts if `streamId` references a paused or a null stream.
/// @param streamId The stream ID for the query.
function depletionTimeOf(uint256 streamId) external view returns (uint40 depletionTime);
function depletionTimeOf(uint256 streamId) external view returns (uint256 depletionTime);

/// @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 (uint128 ongoingDebt);
function ongoingDebtOf(uint256 streamId) external view returns (uint256 ongoingDebt);

/// @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 All @@ -139,12 +139,12 @@ interface ISablierFlow is
/// @notice Returns the total amount owed by the sender to the recipient, denoted in token's decimals.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream ID for the query.
function totalDebtOf(uint256 streamId) external view returns (uint128 totalDebt);
function totalDebtOf(uint256 streamId) external view returns (uint256 totalDebt);

/// @notice Returns the amount of debt not covered by the stream balance, denoted in token's decimals.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream ID for the query.
function uncoveredDebtOf(uint256 streamId) external view returns (uint128 uncoveredDebt);
function uncoveredDebtOf(uint256 streamId) external view returns (uint256 uncoveredDebt);

/// @notice Calculates the amount that the recipient can withdraw from the stream, denoted in token decimals. This
/// is an alias for `coveredDebtOf`.
Expand Down
18 changes: 9 additions & 9 deletions tests/fork/Flow.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ contract Flow_Fork_Test is Fork_Test {
uint128 actualStreamBalance;
uint256 actualStreamId;
uint256 actualTokenBalance;
uint128 actualTotalDebt;
uint256 actualTotalDebt;
// Expected values.
uint256 expectedAggregateAmount;
UD21x18 expectedRatePerSecond;
Expand All @@ -62,7 +62,7 @@ contract Flow_Fork_Test is Fork_Test {
uint128 expectedStreamBalance;
uint256 expectedStreamId;
uint256 expectedTokenBalance;
uint128 expectedTotalDebt;
uint256 expectedTotalDebt;
}

/*//////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -252,8 +252,8 @@ contract Flow_Fork_Test is Fork_Test {
}

uint128 beforeSnapshotAmount = flow.getSnapshotDebt(streamId);
uint128 totalDebt = flow.totalDebtOf(streamId);
uint128 ongoingDebt = flow.ongoingDebtOf(streamId);
uint256 totalDebt = flow.totalDebtOf(streamId);
uint256 ongoingDebt = flow.ongoingDebtOf(streamId);

// Compute the snapshot time that will be stored post withdraw.
vars.expectedSnapshotTime = getBlockTimestamp();
Expand All @@ -274,7 +274,7 @@ contract Flow_Fork_Test is Fork_Test {

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

// It should set the new rate per second
Expand Down Expand Up @@ -443,7 +443,7 @@ contract Flow_Fork_Test is Fork_Test {
// If the refundable amount less than 1, deposit some funds.
if (flow.refundableAmountOf(streamId) <= 1) {
uint128 depositAmount =
flow.uncoveredDebtOf(streamId) + getDefaultDepositAmount(flow.getTokenDecimals(streamId));
uint128(flow.uncoveredDebtOf(streamId)) + getDefaultDepositAmount(flow.getTokenDecimals(streamId));
depositOnStream(streamId, depositAmount);
}

Expand Down Expand Up @@ -527,8 +527,8 @@ contract Flow_Fork_Test is Fork_Test {
// Make sure the requirements are respected.
address sender = flow.getSender(streamId);
address recipient = flow.getRecipient(streamId);
uint128 uncoveredDebt = flow.uncoveredDebtOf(streamId);
uint128 expectedTotalDebt;
uint256 uncoveredDebt = flow.uncoveredDebtOf(streamId);
uint256 expectedTotalDebt;

resetPrank({ msgSender: sender });

Expand Down Expand Up @@ -583,7 +583,7 @@ contract Flow_Fork_Test is Fork_Test {
);

uint256 initialTokenBalance = token.balanceOf(address(flow));
uint128 totalDebt = flow.totalDebtOf(streamId);
uint256 totalDebt = flow.totalDebtOf(streamId);

vars.expectedSnapshotTime =
withdrawAmount <= flow.getSnapshotDebt(streamId) ? flow.getSnapshotTime(streamId) : getBlockTimestamp();
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/concrete/batch/batch.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,8 @@ contract Batch_Integration_Concrete_Test is Integration_Test {
calls[0] = abi.encodeCall(flow.pause, (defaultStreamIds[0]));
calls[1] = abi.encodeCall(flow.pause, (defaultStreamIds[1]));

uint128 previousTotalDebt0 = flow.totalDebtOf(defaultStreamId);
uint128 previousTotalDebt1 = flow.totalDebtOf(defaultStreamIds[1]);
uint256 previousTotalDebt0 = flow.totalDebtOf(defaultStreamId);
uint256 previousTotalDebt1 = flow.totalDebtOf(defaultStreamIds[1]);

// It should emit 2 {PauseFlowStream}, 2 {MetadataUpdate} events.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@ contract DepletionTimeOf_Integration_Concrete_Test is Integration_Test {

function test_GivenBalanceZero() external view givenNotNull givenNotPaused {
// It should return 0
uint40 depletionTime = flow.depletionTimeOf(defaultStreamId);
uint40 depletionTime = uint40(flow.depletionTimeOf(defaultStreamId));
assertEq(depletionTime, 0, "depletion time");
}

function test_GivenUncoveredDebt() external givenNotNull givenNotPaused givenBalanceNotZero {
vm.warp({ newTimestamp: WARP_SOLVENCY_PERIOD });
// It should return 0
uint40 depletionTime = flow.depletionTimeOf(defaultStreamId);
uint40 depletionTime = uint40(flow.depletionTimeOf(defaultStreamId));
assertEq(depletionTime, 0, "depletion time");
}

function test_GivenNoUncoveredDebt() external givenNotNull givenNotPaused givenBalanceNotZero {
// It should return the time at which the stream depletes its balance
uint40 depletionTime = flow.depletionTimeOf(defaultStreamId);
uint40 depletionTime = uint40(flow.depletionTimeOf(defaultStreamId));
assertEq(depletionTime, WARP_SOLVENCY_PERIOD, "depletion time");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ contract DepositAndPause_Integration_Concrete_Test is Integration_Test {

function test_WhenCallerSender() external whenNoDelegateCall givenNotNull givenNotPaused {
uint128 previousStreamBalance = flow.getBalance(defaultStreamId);
uint128 previousTotalDebt = flow.totalDebtOf(defaultStreamId);
uint256 previousTotalDebt = flow.totalDebtOf(defaultStreamId);

// It should emit 1 {Transfer}, 1 {DepositFlowStream}, 1 {PauseFlowStream}, 1 {MetadataUpdate} events
vm.expectEmit({ emitter: address(usdc) });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ contract OngoingDebtOf_Integration_Concrete_Test is Integration_Test {
flow.pause(defaultStreamId);

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

Expand All @@ -22,13 +22,13 @@ contract OngoingDebtOf_Integration_Concrete_Test is Integration_Test {
updateSnapshotTimeAndWarp(defaultStreamId);

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

function test_WhenSnapshotTimeInPast() external view givenNotNull givenNotPaused {
// It should return the correct ongoing debt.
uint128 ongoingDebt = flow.ongoingDebtOf(defaultStreamId);
uint256 ongoingDebt = flow.ongoingDebtOf(defaultStreamId);
assertEq(ongoingDebt, ONE_MONTH_DEBT_6D, "ongoing debt");
}
}
Loading

0 comments on commit fa1b1af

Please sign in to comment.