diff --git a/benchmark/results/SablierFlow.md b/benchmark/results/SablierFlow.md index 4a88ae74..2c35d009 100644 --- a/benchmark/results/SablierFlow.md +++ b/benchmark/results/SablierFlow.md @@ -2,15 +2,15 @@ | Function | Gas Usage | | ----------------------------- | --------- | -| `adjustRatePerSecond` | 43628 | +| `adjustRatePerSecond` | 44149 | | `create` | 113659 | | `deposit` | 30035 | | `depositViaBroker` | 21953 | -| `pause` | 8983 | -| `refund` | 11534 | -| `restart` | 7031 | -| `void (solvent stream)` | 9517 | -| `void (insolvent stream)` | 36241 | -| `withdraw (insolvent stream)` | 57034 | -| `withdraw (solvent stream)` | 39502 | -| `withdrawMax` | 51379 | +| `pause` | 9522 | +| `refund` | 11894 | +| `restart` | 7013 | +| `void (solvent stream)` | 10038 | +| `void (insolvent stream)` | 37438 | +| `withdraw (insolvent stream)` | 57688 | +| `withdraw (solvent stream)` | 40156 | +| `withdrawMax` | 51966 | diff --git a/src/SablierFlow.sol b/src/SablierFlow.sol index 6ded6da4..50000bf0 100644 --- a/src/SablierFlow.sol +++ b/src/SablierFlow.sol @@ -69,35 +69,35 @@ contract SablierFlow is return 0; } + uint8 tokenDecimals = _streams[streamId].tokenDecimals; + uint256 scaledBalance = Helpers.scaleAmount({ amount: balance, decimals: tokenDecimals }); + uint256 snapshotDebt = _streams[streamId].snapshotDebt; // If the stream has uncovered debt, return zero. - if (snapshotDebt + _ongoingDebtOf(streamId) > balance) { + if (snapshotDebt + _scaledOngoingDebtOf(streamId) > scaledBalance) { return 0; } - 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 // lowest timestamp at which the debt exceeds the balance. // Safe to use unchecked because the calculations cannot overflow or underflow. unchecked { - if (tokenDecimals == 18) { - solvencyAmount = (balance - snapshotDebt + 1); - } else { - uint256 scaleFactor = (10 ** (18 - tokenDecimals)); - solvencyAmount = (balance - snapshotDebt + 1) * scaleFactor; - } + uint256 solvencyAmount = + scaledBalance - snapshotDebt + Helpers.scaleAmount({ amount: 1, decimals: tokenDecimals }); uint256 solvencyPeriod = solvencyAmount / _streams[streamId].ratePerSecond.unwrap(); - return _streams[streamId].snapshotTime + solvencyPeriod; + + depletionTime = _streams[streamId].snapshotTime + solvencyPeriod; } } /// @inheritdoc ISablierFlow function ongoingDebtOf(uint256 streamId) external view override notNull(streamId) returns (uint256 ongoingDebt) { - ongoingDebt = _ongoingDebtOf(streamId); + ongoingDebt = Helpers.descaleAmount({ + amount: _scaledOngoingDebtOf(streamId), + decimals: _streams[streamId].tokenDecimals + }); } /// @inheritdoc ISablierFlow @@ -192,7 +192,7 @@ contract SablierFlow is // Log the adjustment. emit ISablierFlow.AdjustFlowStream({ streamId: streamId, - totalDebt: _streams[streamId].snapshotDebt, + totalDebt: _totalDebtOf(streamId), oldRatePerSecond: oldRatePerSecond, newRatePerSecond: newRatePerSecond }); @@ -449,11 +449,11 @@ contract SablierFlow is 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 (uint256 ongoingDebt) { - uint40 blockTimestamp = uint40(block.timestamp); - uint40 snapshotTime = _streams[streamId].snapshotTime; + /// @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) { + uint256 blockTimestamp = block.timestamp; + uint256 snapshotTime = _streams[streamId].snapshotTime; uint256 ratePerSecond = _streams[streamId].ratePerSecond.unwrap(); @@ -470,22 +470,8 @@ contract SablierFlow is elapsedTime = blockTimestamp - snapshotTime; } - // Calculate the ongoing debt accrued by multiplying the elapsed time by the rate per second. - 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) { - return scaledOngoingDebt; - } - - // Safe to use unchecked because we use {SafeCast}. - unchecked { - uint256 scaleFactor = 10 ** (18 - tokenDecimals); - // Since debt is denoted in token decimals, descale the amount. - ongoingDebt = scaledOngoingDebt / scaleFactor; - } + // Calculate the scaled ongoing debt accrued by multiplying the elapsed time by the rate per second. + return elapsedTime * ratePerSecond; } /// @dev Calculates the refundable amount. @@ -497,8 +483,8 @@ 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) { - // Calculate the total debt. - return _streams[streamId].snapshotDebt + _ongoingDebtOf(streamId); + uint256 scaledTotalDebt = _scaledOngoingDebtOf(streamId) + _streams[streamId].snapshotDebt; + return Helpers.descaleAmount({ amount: scaledTotalDebt, decimals: _streams[streamId].tokenDecimals }); } /// @dev Calculates the uncovered debt. @@ -525,12 +511,12 @@ contract SablierFlow is revert Errors.SablierFlow_RatePerSecondNotDifferent(streamId, newRatePerSecond); } - uint256 ongoingDebt = _ongoingDebtOf(streamId); + uint256 scaledOngoingDebt = _scaledOngoingDebtOf(streamId); // Update the snapshot debt only if the stream has ongoing debt. - if (ongoingDebt > 0) { + if (scaledOngoingDebt > 0) { // Effect: update the snapshot debt. - _streams[streamId].snapshotDebt += ongoingDebt; + _streams[streamId].snapshotDebt += scaledOngoingDebt; } // Effect: update the snapshot time. @@ -646,7 +632,7 @@ contract SablierFlow is streamId: streamId, sender: _streams[streamId].sender, recipient: _ownerOf(streamId), - totalDebt: _streams[streamId].snapshotDebt + totalDebt: _totalDebtOf(streamId) }); } @@ -715,16 +701,17 @@ contract SablierFlow is // If the stream is solvent, update the total debt normally. if (debtToWriteOff == 0) { - uint256 ongoingDebt = _ongoingDebtOf(streamId); - if (ongoingDebt > 0) { + uint256 scaledOngoingDebt = _scaledOngoingDebtOf(streamId); + if (scaledOngoingDebt > 0) { // Effect: Update the snapshot debt by adding the ongoing debt. - _streams[streamId].snapshotDebt += ongoingDebt; + _streams[streamId].snapshotDebt += 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].balance; + _streams[streamId].snapshotDebt = + Helpers.scaleAmount({ amount: _streams[streamId].balance, decimals: _streams[streamId].tokenDecimals }); } // Effect: update the snapshot time. @@ -742,7 +729,7 @@ contract SablierFlow is sender: _streams[streamId].sender, recipient: _ownerOf(streamId), caller: msg.sender, - newTotalDebt: _streams[streamId].snapshotDebt, + newTotalDebt: _totalDebtOf(streamId), writtenOffDebt: debtToWriteOff }); } @@ -772,8 +759,11 @@ contract SablierFlow is revert Errors.SablierFlow_WithdrawalAddressNotRecipient({ streamId: streamId, caller: msg.sender, to: to }); } + uint8 tokenDecimals = _streams[streamId].tokenDecimals; + // Calculate the total debt. - uint256 totalDebt = _totalDebtOf(streamId); + uint256 scaledTotalDebt = _scaledOngoingDebtOf(streamId) + _streams[streamId].snapshotDebt; + uint256 totalDebt = Helpers.descaleAmount(scaledTotalDebt, tokenDecimals); // Calculate the withdrawable amount. uint128 balance = _streams[streamId].balance; @@ -792,17 +782,20 @@ contract SablierFlow is revert Errors.SablierFlow_Overdraw(streamId, amount, withdrawableAmount); } + // Calculate the amount scaled. + uint256 scaledAmount = Helpers.scaleAmount(amount, tokenDecimals); + // Safe to use unchecked, `amount` cannot be greater than the balance or total debt at this point. unchecked { // If the amount is less than the snapshot debt, reduce it from the snapshot debt and leave the snapshot // time unchanged. - if (amount <= _streams[streamId].snapshotDebt) { - _streams[streamId].snapshotDebt -= amount; + if (scaledAmount <= _streams[streamId].snapshotDebt) { + _streams[streamId].snapshotDebt -= 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 = totalDebt - amount; + _streams[streamId].snapshotDebt = scaledTotalDebt - scaledAmount; // Effect: update the stream time. _streams[streamId].snapshotTime = uint40(block.timestamp); diff --git a/src/interfaces/ISablierFlowBase.sol b/src/interfaces/ISablierFlowBase.sol index d6411b7d..227fab1b 100644 --- a/src/interfaces/ISablierFlowBase.sol +++ b/src/interfaces/ISablierFlowBase.sol @@ -84,7 +84,7 @@ interface ISablierFlowBase is /// @param streamId The stream ID for the query. function getSender(uint256 streamId) external view returns (address sender); - /// @notice Retrieves the snapshot debt of the stream, denoted in token's decimals. + /// @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); diff --git a/src/libraries/Helpers.sol b/src/libraries/Helpers.sol index 5c252dc9..7d013024 100644 --- a/src/libraries/Helpers.sol +++ b/src/libraries/Helpers.sol @@ -49,4 +49,34 @@ library Helpers { // Calculate the broker fee amount that is going to be transferred to the `broker.account`. (brokerFeeAmount, depositAmount) = calculateAmountsFromFee(totalAmount, broker.fee); } + + /// @notice Descales the provided `amount` to be denoted in the token's decimals. + /// @dev The following logic is used to denormalize the amount: + /// - If the token has exactly 18 decimals, the amount is returned as is. + /// - if the token has fewer than 18 decimals, the amount is divided by $10^(18 - tokenDecimals)$. + function descaleAmount(uint256 amount, uint8 decimals) internal pure returns (uint256) { + if (decimals > 18) { + return amount; + } + + unchecked { + uint256 scaleFactor = 10 ** (18 - decimals); + return amount / scaleFactor; + } + } + + /// @notice Scales the provided `amount` to be denoted in 18 decimals. + /// @dev The following logic is used to normalize the amount: + /// - If the token has exactly 18 decimals, the amount is returned as is. + /// - If the token has fewer than 18 decimals, the amount is multiplied by $10^(18 - tokenDecimals)$. + function scaleAmount(uint256 amount, uint8 decimals) internal pure returns (uint256) { + if (decimals > 18) { + return amount; + } + + unchecked { + uint256 scaleFactor = 10 ** (18 - decimals); + return amount * scaleFactor; + } + } } diff --git a/src/types/DataTypes.sol b/src/types/DataTypes.sol index fa4806f1..cb2dd7ff 100644 --- a/src/types/DataTypes.sol +++ b/src/types/DataTypes.sol @@ -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 in - /// token's decimals. This, along with the ongoing debt, can be used to calculate the total debt at any given point - /// in time. + /// @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. struct Stream { // slot 0 uint128 balance; diff --git a/tests/fork/Flow.t.sol b/tests/fork/Flow.t.sol index d33c0939..45f7368f 100644 --- a/tests/fork/Flow.t.sol +++ b/tests/fork/Flow.t.sol @@ -229,11 +229,13 @@ contract Flow_Fork_Test is Fork_Test { uint256 beforeSnapshotAmount = flow.getSnapshotDebt(streamId); uint256 totalDebt = flow.totalDebtOf(streamId); - uint256 ongoingDebt = flow.ongoingDebtOf(streamId); // Compute the snapshot time that will be stored post withdraw. vars.expectedSnapshotTime = getBlockTimestamp(); + uint256 scaledOngoingDebt = + calculateScaledOngoingDebt(flow.getRatePerSecond(streamId).unwrap(), flow.getSnapshotTime(streamId)); + // It should emit 1 {AdjustFlowStream}, 1 {MetadataUpdate} events. vm.expectEmit({ emitter: address(flow) }); emit ISablierFlow.AdjustFlowStream({ @@ -250,7 +252,7 @@ contract Flow_Fork_Test is Fork_Test { // It should update snapshot debt. vars.actualSnapshotDebt = flow.getSnapshotDebt(streamId); - vars.expectedSnapshotDebt = ongoingDebt + beforeSnapshotAmount; + vars.expectedSnapshotDebt = scaledOngoingDebt + beforeSnapshotAmount; assertEq(vars.actualSnapshotDebt, vars.expectedSnapshotDebt, "AdjustRatePerSecond: snapshot debt"); // It should set the new rate per second @@ -561,8 +563,10 @@ contract Flow_Fork_Test is Fork_Test { uint256 initialTokenBalance = token.balanceOf(address(flow)); uint256 totalDebt = flow.totalDebtOf(streamId); - vars.expectedSnapshotTime = - withdrawAmount <= flow.getSnapshotDebt(streamId) ? flow.getSnapshotTime(streamId) : getBlockTimestamp(); + vars.expectedSnapshotTime = withdrawAmount + <= getDescaledAmount(flow.getSnapshotDebt(streamId), flow.getTokenDecimals(streamId)) + ? flow.getSnapshotTime(streamId) + : getBlockTimestamp(); (, address caller,) = vm.readCallers(); address recipient = flow.getRecipient(streamId); diff --git a/tests/integration/concrete/adjust-rate-per-second/adjustRatePerSecond.t.sol b/tests/integration/concrete/adjust-rate-per-second/adjustRatePerSecond.t.sol index 65b952f5..bffbf075 100644 --- a/tests/integration/concrete/adjust-rate-per-second/adjustRatePerSecond.t.sol +++ b/tests/integration/concrete/adjust-rate-per-second/adjustRatePerSecond.t.sol @@ -119,7 +119,7 @@ contract AdjustRatePerSecond_Integration_Concrete_Test is Integration_Test { // It should update snapshot debt. actualSnapshotDebt = flow.getSnapshotDebt(defaultStreamId); - expectedSnapshotDebt = ONE_MONTH_DEBT_6D; + expectedSnapshotDebt = ONE_MONTH_DEBT_18D; assertEq(actualSnapshotDebt, expectedSnapshotDebt, "snapshot debt"); // It should set the new rate per second diff --git a/tests/integration/concrete/deposit-and-pause/depositAndPause.t.sol b/tests/integration/concrete/deposit-and-pause/depositAndPause.t.sol index 2c7db077..e7396464 100644 --- a/tests/integration/concrete/deposit-and-pause/depositAndPause.t.sol +++ b/tests/integration/concrete/deposit-and-pause/depositAndPause.t.sol @@ -56,7 +56,8 @@ contract DepositAndPause_Integration_Concrete_Test is Integration_Test { function test_WhenCallerSender() external whenNoDelegateCall givenNotNull givenNotPaused { uint128 previousStreamBalance = flow.getBalance(defaultStreamId); - uint256 previousTotalDebt = flow.totalDebtOf(defaultStreamId); + uint256 expectedSnapshotDebt = + calculateScaledOngoingDebt(RATE_PER_SECOND_U128, flow.getSnapshotTime(defaultStreamId)); // It should emit 1 {Transfer}, 1 {DepositFlowStream}, 1 {PauseFlowStream}, 1 {MetadataUpdate} events vm.expectEmit({ emitter: address(usdc) }); @@ -74,7 +75,7 @@ contract DepositAndPause_Integration_Concrete_Test is Integration_Test { streamId: defaultStreamId, sender: users.sender, recipient: users.recipient, - totalDebt: previousTotalDebt + totalDebt: flow.totalDebtOf(defaultStreamId) }); vm.expectEmit({ emitter: address(flow) }); @@ -99,6 +100,6 @@ contract DepositAndPause_Integration_Concrete_Test is Integration_Test { // It should update the snapshot debt uint256 actualSnapshotDebt = flow.getSnapshotDebt(defaultStreamId); - assertEq(actualSnapshotDebt, previousTotalDebt, "snapshot debt"); + assertEq(actualSnapshotDebt, expectedSnapshotDebt, "snapshot debt"); } } diff --git a/tests/integration/concrete/pause/pause.t.sol b/tests/integration/concrete/pause/pause.t.sol index e0ce7cee..8f8c16e0 100644 --- a/tests/integration/concrete/pause/pause.t.sol +++ b/tests/integration/concrete/pause/pause.t.sol @@ -51,7 +51,7 @@ contract Pause_Integration_Concrete_Test is Integration_Test { assertGt(flow.uncoveredDebtOf(defaultStreamId), 0, "uncovered debt"); // It should pause the stream. - test_Pause(); + _test_Pause(); } function test_GivenNoUncoveredDebt() external whenNoDelegateCall givenNotNull givenNotPaused whenCallerSender { @@ -62,11 +62,12 @@ contract Pause_Integration_Concrete_Test is Integration_Test { assertEq(flow.uncoveredDebtOf(defaultStreamId), 0, "uncovered debt"); // It should pause the stream. - test_Pause(); + _test_Pause(); } - function test_Pause() internal { - uint256 initialTotalDebt = flow.totalDebtOf(defaultStreamId); + function _test_Pause() private { + uint256 expectedSnapshotDebt = + calculateScaledOngoingDebt(RATE_PER_SECOND_U128, flow.getSnapshotTime(defaultStreamId)); // It should emit 1 {PauseFlowStream}, 1 {MetadataUpdate} events. vm.expectEmit({ emitter: address(flow) }); @@ -74,7 +75,7 @@ contract Pause_Integration_Concrete_Test is Integration_Test { streamId: defaultStreamId, sender: users.sender, recipient: users.recipient, - totalDebt: initialTotalDebt + totalDebt: flow.totalDebtOf(defaultStreamId) }); vm.expectEmit({ emitter: address(flow) }); @@ -91,6 +92,6 @@ contract Pause_Integration_Concrete_Test is Integration_Test { // It should update the snapshot debt. uint256 actualSnapshotDebt = flow.getSnapshotDebt(defaultStreamId); - assertEq(actualSnapshotDebt, initialTotalDebt, "snapshot debt"); + assertEq(actualSnapshotDebt, expectedSnapshotDebt, "snapshot debt"); } } diff --git a/tests/integration/concrete/refund-and-pause/refundAndPause.t.sol b/tests/integration/concrete/refund-and-pause/refundAndPause.t.sol index 9bc8cc14..21f79cac 100644 --- a/tests/integration/concrete/refund-and-pause/refundAndPause.t.sol +++ b/tests/integration/concrete/refund-and-pause/refundAndPause.t.sol @@ -54,7 +54,8 @@ contract RefundAndPause_Integration_Concrete_Test is Integration_Test { } function test_WhenCallerSender() external whenNoDelegateCall givenNotNull givenNotPaused { - uint256 previousTotalDebt = flow.totalDebtOf(defaultStreamId); + uint256 expectedSnapshotDebt = + calculateScaledOngoingDebt(RATE_PER_SECOND_U128, flow.getSnapshotTime(defaultStreamId)); // It should emit 1 {Transfer}, 1 {RefundFromFlowStream}, 1 {PauseFlowStream}, 1 {MetadataUpdate} events vm.expectEmit({ emitter: address(usdc) }); @@ -72,7 +73,7 @@ contract RefundAndPause_Integration_Concrete_Test is Integration_Test { streamId: defaultStreamId, sender: users.sender, recipient: users.recipient, - totalDebt: previousTotalDebt + totalDebt: flow.totalDebtOf(defaultStreamId) }); vm.expectEmit({ emitter: address(flow) }); @@ -97,6 +98,6 @@ contract RefundAndPause_Integration_Concrete_Test is Integration_Test { // It should update the snapshot debt uint256 actualSnapshotDebt = flow.getSnapshotDebt(defaultStreamId); - assertEq(actualSnapshotDebt, previousTotalDebt, "snapshot debt"); + assertEq(actualSnapshotDebt, expectedSnapshotDebt, "snapshot debt"); } } diff --git a/tests/integration/concrete/total-debt-of/totalDebtOf.t.sol b/tests/integration/concrete/total-debt-of/totalDebtOf.t.sol index 0dda0a9d..a2bfd20c 100644 --- a/tests/integration/concrete/total-debt-of/totalDebtOf.t.sol +++ b/tests/integration/concrete/total-debt-of/totalDebtOf.t.sol @@ -15,9 +15,8 @@ contract TotalDebtOf_Integration_Concrete_Test is Integration_Test { flow.pause(defaultStreamId); uint256 snapshotDebt = flow.getSnapshotDebt(defaultStreamId); - uint256 totalDebt = flow.totalDebtOf(defaultStreamId); - assertEq(totalDebt, snapshotDebt, "total debt"); + assertEq(ONE_MONTH_DEBT_18D, snapshotDebt, "total debt"); } function test_WhenCurrentTimeEqualsSnapshotTime() external givenNotNull givenNotPaused { @@ -25,16 +24,15 @@ contract TotalDebtOf_Integration_Concrete_Test is Integration_Test { flow.adjustRatePerSecond(defaultStreamId, ud21x18(RATE_PER_SECOND_U128 * 2)); uint256 snapshotDebt = flow.getSnapshotDebt(defaultStreamId); - uint256 totalDebt = flow.totalDebtOf(defaultStreamId); - assertEq(totalDebt, snapshotDebt, "total debt"); + assertEq(ONE_MONTH_DEBT_18D, snapshotDebt, "total debt"); } function test_WhenCurrentTimeGreaterThanSnapshotTime() external view givenNotNull givenNotPaused { uint256 snapshotDebt = flow.getSnapshotDebt(defaultStreamId); - uint256 ongoingDebt = flow.ongoingDebtOf(defaultStreamId); - uint256 totalDebt = flow.totalDebtOf(defaultStreamId); + uint256 scaledOngoingDebt = + calculateScaledOngoingDebt(RATE_PER_SECOND_U128, flow.getSnapshotTime(defaultStreamId)); - assertEq(snapshotDebt + ongoingDebt, totalDebt, "total debt"); + assertEq(snapshotDebt + scaledOngoingDebt, ONE_MONTH_DEBT_18D, "total debt"); } } diff --git a/tests/integration/concrete/uncovered-debt-of/uncoveredDebtOf.t.sol b/tests/integration/concrete/uncovered-debt-of/uncoveredDebtOf.t.sol index ad8dff0a..65a6c299 100644 --- a/tests/integration/concrete/uncovered-debt-of/uncoveredDebtOf.t.sol +++ b/tests/integration/concrete/uncovered-debt-of/uncoveredDebtOf.t.sol @@ -26,7 +26,7 @@ contract UncoveredDebtOf_Integration_Concrete_Test is Integration_Test { // Simulate the passage of time to accumulate uncovered debt for one month. vm.warp({ newTimestamp: WARP_SOLVENCY_PERIOD + ONE_MONTH }); - uint128 totalStreamed = getDescaledAmount(RATE_PER_SECOND_U128 * (SOLVENCY_PERIOD + ONE_MONTH), 6); + uint256 totalStreamed = getDescaledAmount(RATE_PER_SECOND_U128 * (SOLVENCY_PERIOD + ONE_MONTH), 6); // It should return non-zero value. uint256 actualUncoveredDebt = flow.uncoveredDebtOf(defaultStreamId); diff --git a/tests/integration/fuzz/Fuzz.t.sol b/tests/integration/fuzz/Fuzz.t.sol index 717ef128..e1fa6e96 100644 --- a/tests/integration/fuzz/Fuzz.t.sol +++ b/tests/integration/fuzz/Fuzz.t.sol @@ -55,7 +55,7 @@ abstract contract Shared_Integration_Fuzz_Test is Integration_Test { uint128 amountSeed = uint128(uint256(keccak256(abi.encodePacked(flow.nextStreamId(), decimals)))); // Bound the amount between a realistic range. uint128 amount = boundUint128(amountSeed, 1e18, 200_000e18); - uint128 depositAmount = getDescaledAmount(amount, decimals); + uint128 depositAmount = uint128(getDescaledAmount(amount, decimals)); // Deposit into the stream. deposit(streamId, depositAmount); diff --git a/tests/integration/fuzz/coveredDebtOf.t.sol b/tests/integration/fuzz/coveredDebtOf.t.sol index a7871656..067a1b5d 100644 --- a/tests/integration/fuzz/coveredDebtOf.t.sol +++ b/tests/integration/fuzz/coveredDebtOf.t.sol @@ -25,7 +25,7 @@ contract CoveredDebtOf_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { // Simulate the passage of time. vm.warp({ newTimestamp: warpTimestamp }); - uint128 expectedCoveredDebt = flow.coveredDebtOf(streamId); + uint256 expectedCoveredDebt = flow.coveredDebtOf(streamId); // Pause the stream. flow.pause(streamId); @@ -34,7 +34,7 @@ contract CoveredDebtOf_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { vm.warp({ newTimestamp: boundUint40(warpTimestamp, getBlockTimestamp() + 1, UINT40_MAX) }); // Assert that the covered debt did not change. - uint128 actualCoveredDebt = flow.coveredDebtOf(streamId); + uint256 actualCoveredDebt = flow.coveredDebtOf(streamId); assertEq(actualCoveredDebt, expectedCoveredDebt); } @@ -63,8 +63,8 @@ contract CoveredDebtOf_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { uint128 ratePerSecond = flow.getRatePerSecond(streamId).unwrap(); // Assert that the covered debt equals the ongoing debt. - uint128 actualCoveredDebt = flow.coveredDebtOf(streamId); - uint128 expectedCoveredDebt = getDescaledAmount(ratePerSecond * (warpTimestamp - OCT_1_2024), decimals); + uint256 actualCoveredDebt = flow.coveredDebtOf(streamId); + uint256 expectedCoveredDebt = getDescaledAmount(ratePerSecond * (warpTimestamp - OCT_1_2024), decimals); assertEq(actualCoveredDebt, expectedCoveredDebt); } @@ -84,7 +84,7 @@ contract CoveredDebtOf_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { vm.warp({ newTimestamp: warpTimestamp }); // Assert that the covered debt equals the stream balance. - uint128 actualCoveredDebt = flow.coveredDebtOf(streamId); + uint256 actualCoveredDebt = flow.coveredDebtOf(streamId); assertEq(actualCoveredDebt, flow.getBalance(streamId), "covered debt vs stream balance"); // Assert that the covered debt is same as the deposited amount. diff --git a/tests/integration/fuzz/depletionTimeOf.t.sol b/tests/integration/fuzz/depletionTimeOf.t.sol index 11e63ba7..9fbed235 100644 --- a/tests/integration/fuzz/depletionTimeOf.t.sol +++ b/tests/integration/fuzz/depletionTimeOf.t.sol @@ -23,8 +23,8 @@ contract DepletionTimeOf_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { (streamId, decimals,) = useFuzzedStreamOrCreate(streamId, decimals); // Calculate the solvency period based on the stream deposit. - uint40 solvencyPeriod = - uint40(getScaledAmount(flow.getBalance(streamId) + 1, decimals) / flow.getRatePerSecond(streamId).unwrap()); + uint256 solvencyPeriod = + getScaledAmount(flow.getBalance(streamId) + 1, decimals) / flow.getRatePerSecond(streamId).unwrap(); // Bound the time jump to provide a realistic time frame. timeJump = boundUint40(timeJump, 0 seconds, 100 weeks); diff --git a/tests/integration/fuzz/pause.t.sol b/tests/integration/fuzz/pause.t.sol index 8fcb5c65..0a73f4d4 100644 --- a/tests/integration/fuzz/pause.t.sol +++ b/tests/integration/fuzz/pause.t.sol @@ -73,7 +73,7 @@ contract Pause_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { streamId: streamId, sender: users.sender, recipient: users.recipient, - totalDebt: uint128(flow.totalDebtOf(streamId)) + totalDebt: flow.totalDebtOf(streamId) }); vm.expectEmit({ emitter: address(flow) }); diff --git a/tests/integration/fuzz/refundableAmountOf.t.sol b/tests/integration/fuzz/refundableAmountOf.t.sol index f6e3470f..7c036498 100644 --- a/tests/integration/fuzz/refundableAmountOf.t.sol +++ b/tests/integration/fuzz/refundableAmountOf.t.sol @@ -65,8 +65,8 @@ contract RefundableAmountOf_Integration_Fuzz_Test is Shared_Integration_Fuzz_Tes uint128 ratePerSecond = flow.getRatePerSecond(streamId).unwrap(); // Assert that the refundable amount same as the deposited amount minus streamed amount. - uint128 actualRefundableAmount = flow.refundableAmountOf(streamId); - uint128 expectedRefundableAmount = + uint256 actualRefundableAmount = flow.refundableAmountOf(streamId); + uint256 expectedRefundableAmount = depositedAmount - getDescaledAmount(ratePerSecond * (warpTimestamp - OCT_1_2024), decimals); assertEq(actualRefundableAmount, expectedRefundableAmount); } @@ -87,7 +87,7 @@ contract RefundableAmountOf_Integration_Fuzz_Test is Shared_Integration_Fuzz_Tes vm.warp({ newTimestamp: warpTimestamp }); // Assert that the refundable amount is zero. - uint128 actualRefundableAmount = flow.refundableAmountOf(streamId); + uint256 actualRefundableAmount = flow.refundableAmountOf(streamId); assertEq(actualRefundableAmount, 0); } } diff --git a/tests/integration/fuzz/totalDebtOf.t.sol b/tests/integration/fuzz/totalDebtOf.t.sol index 0fe8589e..6188fb66 100644 --- a/tests/integration/fuzz/totalDebtOf.t.sol +++ b/tests/integration/fuzz/totalDebtOf.t.sol @@ -57,7 +57,7 @@ contract TotalDebtOf_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { // Assert that total debt is the ongoing debt. uint256 actualTotalDebt = flow.totalDebtOf(streamId); - uint128 expectedTotalDebt = getDescaledAmount(ratePerSecond * timeJump, decimals); + uint256 expectedTotalDebt = getDescaledAmount(ratePerSecond * timeJump, decimals); assertEq(actualTotalDebt, expectedTotalDebt, "total debt"); } } diff --git a/tests/integration/fuzz/withdraw.t.sol b/tests/integration/fuzz/withdraw.t.sol index 31fcff3e..f7d0c5a7 100644 --- a/tests/integration/fuzz/withdraw.t.sol +++ b/tests/integration/fuzz/withdraw.t.sol @@ -148,7 +148,8 @@ contract Withdraw_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { vars.previousAggregateAmount = flow.aggregateBalance(token); vars.previousTokenBalance = token.balanceOf(address(flow)); vars.previousOngoingDebt = flow.totalDebtOf(streamId); - vars.previousTotalDebt = flow.getSnapshotDebt(streamId) + vars.previousOngoingDebt; + vars.previousTotalDebt = getDescaledAmount(flow.getSnapshotDebt(streamId), flow.getTokenDecimals(streamId)) + + vars.previousOngoingDebt; vars.previousStreamBalance = flow.getBalance(streamId); vars.expectedProtocolRevenue = flow.protocolRevenue(token); diff --git a/tests/integration/fuzz/withdrawMax.t.sol b/tests/integration/fuzz/withdrawMax.t.sol index 9c0cd6f8..bd72847e 100644 --- a/tests/integration/fuzz/withdrawMax.t.sol +++ b/tests/integration/fuzz/withdrawMax.t.sol @@ -137,12 +137,12 @@ contract WithdrawMax_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { vars.expectedStreamBalance = vars.previousStreamBalance - withdrawAmount; assertEq(vars.actualStreamBalance, vars.expectedStreamBalance, "stream balance"); - // Assert that snapshot time is updated correctly. - assertEq(flow.getSnapshotTime(streamId), vars.expectedSnapshotTime, "snapshot time"); - // Assert that total debt equals snapshot debt and ongoing debt assertEq( - flow.totalDebtOf(streamId), flow.getSnapshotDebt(streamId) + flow.ongoingDebtOf(streamId), "snapshot debt" + flow.totalDebtOf(streamId), + getDescaledAmount(flow.getSnapshotDebt(streamId), flow.getTokenDecimals(streamId)) + + flow.ongoingDebtOf(streamId), + "snapshot debt" ); // It should reduce the token balance of stream. diff --git a/tests/integration/fuzz/withdrawMultiple.t.sol b/tests/integration/fuzz/withdrawMultiple.t.sol index 16d89a4e..bb49a501 100644 --- a/tests/integration/fuzz/withdrawMultiple.t.sol +++ b/tests/integration/fuzz/withdrawMultiple.t.sol @@ -126,7 +126,7 @@ contract WithdrawMultiple_Delay_Fuzz_Test is Shared_Integration_Fuzz_Test { } // For all other decimals, choose the minimum rps such that it takes 1 minute to stream 1 token. else { - rps = boundUint128(rps, getScaledAmount(1, decimals) / 60 + 1, 1e18); + rps = boundUint128(rps, uint128(getScaledAmount(1, decimals)) / 60 + 1, 1e18); } uint256 streamId = createDefaultStream(ud21x18(rps), token); diff --git a/tests/invariant/handlers/FlowCreateHandler.sol b/tests/invariant/handlers/FlowCreateHandler.sol index 17cd473e..e6712b83 100644 --- a/tests/invariant/handlers/FlowCreateHandler.sol +++ b/tests/invariant/handlers/FlowCreateHandler.sol @@ -82,8 +82,8 @@ contract FlowCreateHandler is BaseHandler { vm.assume(flowStore.lastStreamId() < MAX_STREAM_COUNT); // Calculate the upper bound, based on the token decimals, for the deposit amount. - uint128 upperBound = getDescaledAmount(1_000_000e18, IERC20Metadata(address(currentToken)).decimals()); - uint128 lowerBound = getDescaledAmount(1e18, IERC20Metadata(address(currentToken)).decimals()); + uint256 upperBound = getDescaledAmount(1_000_000e18, IERC20Metadata(address(currentToken)).decimals()); + uint256 lowerBound = getDescaledAmount(1e18, IERC20Metadata(address(currentToken)).decimals()); // Make sure the deposit amount is non-zero and less than values that could cause an overflow. vm.assume(params.depositAmount >= lowerBound && params.depositAmount <= upperBound); @@ -133,7 +133,7 @@ contract FlowCreateHandler is BaseHandler { uint8 decimals = IERC20Metadata(address(currentToken)).decimals(); // Calculate the minimum value in scaled version that can be withdrawn for this token. - uint128 mvt = getScaledAmount(1, decimals); + uint256 mvt = getScaledAmount(1, decimals); // For 18 decimal, check the rate per second is within a realistic range. if (decimals == 18) { diff --git a/tests/invariant/handlers/FlowHandler.sol b/tests/invariant/handlers/FlowHandler.sol index b81f3e39..b34408f0 100644 --- a/tests/invariant/handlers/FlowHandler.sol +++ b/tests/invariant/handlers/FlowHandler.sol @@ -86,7 +86,7 @@ contract FlowHandler is BaseHandler { uint8 decimals = flow.getTokenDecimals(currentStreamId); // Calculate the minimum value in scaled version that can be withdrawn for this token. - uint128 mvt = getScaledAmount(1, decimals); + uint256 mvt = getScaledAmount(1, decimals); // Check the rate per second is within a realistic range such that it can also be smaller than mvt. if (decimals == 18) { @@ -123,8 +123,8 @@ contract FlowHandler is BaseHandler { vm.assume(!flow.isVoided(currentStreamId)); // Calculate the upper bound, based on the token decimals, for the deposit amount. - uint128 upperBound = getDescaledAmount(1_000_000e18, flow.getTokenDecimals(currentStreamId)); - uint128 lowerBound = getDescaledAmount(1e18, flow.getTokenDecimals(currentStreamId)); + uint256 upperBound = getDescaledAmount(1_000_000e18, flow.getTokenDecimals(currentStreamId)); + uint256 lowerBound = getDescaledAmount(1e18, flow.getTokenDecimals(currentStreamId)); // Make sure the deposit amount is non-zero and less than values that could cause an overflow. vm.assume(depositAmount >= lowerBound && depositAmount <= upperBound); @@ -219,7 +219,7 @@ contract FlowHandler is BaseHandler { uint8 decimals = flow.getTokenDecimals(currentStreamId); // Calculate the minimum value in scaled version that can be withdrawn for this token. - uint128 mvt = getScaledAmount(1, decimals); + uint256 mvt = getScaledAmount(1, decimals); // Check the rate per second is within a realistic range such that it can also be smaller than mvt. if (decimals == 18) { diff --git a/tests/utils/Constants.sol b/tests/utils/Constants.sol index 7c7a1bbc..b559a7a6 100644 --- a/tests/utils/Constants.sol +++ b/tests/utils/Constants.sol @@ -38,6 +38,7 @@ abstract contract Constants { // Streaming amounts uint128 internal constant ONE_MONTH_DEBT_6D = 2592e6; // 86.4 * 30 + uint128 internal constant ONE_MONTH_DEBT_18D = 2592e18; // 86.4 * 30 uint128 internal constant ONE_MONTH_REFUNDABLE_AMOUNT_6D = DEPOSIT_AMOUNT_6D - ONE_MONTH_DEBT_6D; // Time diff --git a/tests/utils/Utils.sol b/tests/utils/Utils.sol index ad969ee8..47665ab2 100644 --- a/tests/utils/Utils.sol +++ b/tests/utils/Utils.sol @@ -49,6 +49,12 @@ abstract contract Utils is CommonBase, Constants, PRBMathUtils { return uint8(_bound(uint256(x), uint256(min), uint256(max))); } + /// @dev A function that mirrors the internal logic from {SablierFlow._scaledOngoingDebt}. + function calculateScaledOngoingDebt(uint128 ratePerSecond, uint40 snapshotTime) internal view returns (uint256) { + uint256 elapsedTime = getBlockTimestamp() - snapshotTime; + return ratePerSecond * elapsedTime; + } + /// @dev Retrieves the current block timestamp as an `uint40`. function getBlockTimestamp() internal view returns (uint40) { return uint40(block.timestamp); @@ -60,22 +66,22 @@ abstract contract Utils is CommonBase, Constants, PRBMathUtils { } /// @dev Descales the amount to denote it in token's decimals. - function getDescaledAmount(uint128 amount, uint8 decimals) internal pure returns (uint128) { + function getDescaledAmount(uint256 amount, uint8 decimals) internal pure returns (uint256) { if (decimals == 18) { return amount; } - uint128 scaleFactor = (10 ** (18 - decimals)).toUint128(); + uint256 scaleFactor = (10 ** (18 - decimals)); return amount / scaleFactor; } /// @dev Scales the amount to denote it in 18 decimals. - function getScaledAmount(uint128 amount, uint8 decimals) internal pure returns (uint128) { + function getScaledAmount(uint256 amount, uint8 decimals) internal pure returns (uint256) { if (decimals == 18) { return amount; } - uint128 scaleFactor = (10 ** (18 - decimals)).toUint128(); + uint256 scaleFactor = (10 ** (18 - decimals)); return amount * scaleFactor; }