diff --git a/TECHNICAL-DOC.md b/TECHNICAL-DOC.md index 98426a07..ecbcafa1 100644 --- a/TECHNICAL-DOC.md +++ b/TECHNICAL-DOC.md @@ -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 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 3353ba1b..7e1bb27e 100644 --- a/src/SablierFlow.sol +++ b/src/SablierFlow.sol @@ -69,35 +69,38 @@ contract SablierFlow is return 0; } - uint256 snapshotDebt = _streams[streamId].snapshotDebt; + uint8 tokenDecimals = _streams[streamId].tokenDecimals; + uint256 balanceScaled = Helpers.scaleAmount({ amount: balance, decimals: tokenDecimals }); + + uint256 snapshotDebtScaled = _streams[streamId].snapshotDebtScaled; // If the stream has uncovered debt, return zero. - if (snapshotDebt + _ongoingDebtOf(streamId) > balance) { + if (snapshotDebtScaled + _ongoingDebtScaledOf(streamId) > balanceScaled) { 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 = + balanceScaled - snapshotDebtScaled + 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); + function ongoingDebtScaledOf(uint256 streamId) + external + view + override + notNull(streamId) + returns (uint256 ongoingDebtScaled) + { + ongoingDebtScaled = _ongoingDebtScaledOf(streamId); } /// @inheritdoc ISablierFlow @@ -192,7 +195,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 +452,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 _ongoingDebtScaledOf(uint256 streamId) internal view returns (uint256) { + uint256 blockTimestamp = block.timestamp; + uint256 snapshotTime = _streams[streamId].snapshotTime; uint256 ratePerSecond = _streams[streamId].ratePerSecond.unwrap(); @@ -470,22 +473,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 ongoing debt scaled accrued by multiplying the elapsed time by the rate per second. + return elapsedTime * ratePerSecond; } /// @dev Calculates the refundable amount. @@ -493,12 +482,11 @@ contract SablierFlow is return _streams[streamId].balance - _coveredDebtOf(streamId); } - /// @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. + /// @dev The total debt is the sum of the snapshot debt and the ongoing debt descaled to token's decimal. 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 totalDebtScaled = _ongoingDebtScaledOf(streamId) + _streams[streamId].snapshotDebtScaled; + return Helpers.descaleAmount({ amount: totalDebtScaled, decimals: _streams[streamId].tokenDecimals }); } /// @dev Calculates the uncovered debt. @@ -525,12 +513,12 @@ contract SablierFlow is revert Errors.SablierFlow_RatePerSecondNotDifferent(streamId, newRatePerSecond); } - uint256 ongoingDebt = _ongoingDebtOf(streamId); + uint256 ongoingDebtScaled = _ongoingDebtScaledOf(streamId); // Update the snapshot debt only if the stream has ongoing debt. - if (ongoingDebt > 0) { + if (ongoingDebtScaled > 0) { // Effect: update the snapshot debt. - _streams[streamId].snapshotDebt += ongoingDebt; + _streams[streamId].snapshotDebtScaled += ongoingDebtScaled; } // Effect: update the snapshot time. @@ -574,7 +562,7 @@ contract SablierFlow is isVoided: false, ratePerSecond: ratePerSecond, sender: sender, - snapshotDebt: 0, + snapshotDebtScaled: 0, snapshotTime: uint40(block.timestamp), token: token, tokenDecimals: tokenDecimals @@ -646,7 +634,7 @@ contract SablierFlow is streamId: streamId, sender: _streams[streamId].sender, recipient: _ownerOf(streamId), - totalDebt: _streams[streamId].snapshotDebt + totalDebt: _totalDebtOf(streamId) }); } @@ -715,16 +703,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 ongoingDebtScaled = _ongoingDebtScaledOf(streamId); + if (ongoingDebtScaled > 0) { // Effect: Update the snapshot debt by adding the ongoing debt. - _streams[streamId].snapshotDebt += ongoingDebt; + _streams[streamId].snapshotDebtScaled += ongoingDebtScaled; } } // 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].snapshotDebtScaled = + Helpers.scaleAmount({ amount: _streams[streamId].balance, decimals: _streams[streamId].tokenDecimals }); } // Effect: update the snapshot time. @@ -742,7 +731,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 +761,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 totalDebtScaled = _ongoingDebtScaledOf(streamId) + _streams[streamId].snapshotDebtScaled; + uint256 totalDebt = Helpers.descaleAmount(totalDebtScaled, tokenDecimals); // Calculate the withdrawable amount. uint128 balance = _streams[streamId].balance; @@ -792,17 +784,20 @@ contract SablierFlow is revert Errors.SablierFlow_Overdraw(streamId, amount, withdrawableAmount); } + // Calculate the amount scaled. + uint256 amountScaled = 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 (amountScaled <= _streams[streamId].snapshotDebtScaled) { + _streams[streamId].snapshotDebtScaled -= amountScaled; } // 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].snapshotDebtScaled = totalDebtScaled - amountScaled; // Effect: update the stream time. _streams[streamId].snapshotTime = uint40(block.timestamp); diff --git a/src/abstracts/SablierFlowBase.sol b/src/abstracts/SablierFlowBase.sol index 3a52c7cf..31cae3e3 100644 --- a/src/abstracts/SablierFlowBase.sol +++ b/src/abstracts/SablierFlowBase.sol @@ -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 diff --git a/src/interfaces/ISablierFlow.sol b/src/interfaces/ISablierFlow.sol index 35859c38..28ff56b3 100644 --- a/src/interfaces/ISablierFlow.sol +++ b/src/interfaces/ISablierFlow.sol @@ -121,10 +121,11 @@ interface ISablierFlow is /// @param streamId The stream ID for the query. 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. + /// @notice Returns the amount of debt accrued since the snapshot time until now, 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 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. diff --git a/src/interfaces/ISablierFlowBase.sol b/src/interfaces/ISablierFlowBase.sol index d6278eca..4d17f1c7 100644 --- a/src/interfaces/ISablierFlowBase.sol +++ b/src/interfaces/ISablierFlowBase.sol @@ -84,10 +84,10 @@ 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); + 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. diff --git a/src/libraries/Helpers.sol b/src/libraries/Helpers.sol index 5c252dc9..22a3c9f3 100644 --- a/src/libraries/Helpers.sol +++ b/src/libraries/Helpers.sol @@ -49,4 +49,28 @@ 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` from 18 decimals fixed-point number to token's decimals number. + 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` from 18 decimals fixed-point number to token's decimals number. + 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..8f68ee9d 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 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; @@ -72,6 +72,6 @@ library Flow { IERC20 token; uint8 tokenDecimals; // slot 3 - uint256 snapshotDebt; + uint256 snapshotDebtScaled; } } diff --git a/tests/fork/Flow.t.sol b/tests/fork/Flow.t.sol index d33c0939..6f40cbf9 100644 --- a/tests/fork/Flow.t.sol +++ b/tests/fork/Flow.t.sol @@ -227,13 +227,14 @@ 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); - uint256 ongoingDebt = flow.ongoingDebtOf(streamId); // Compute the snapshot time that will be stored post withdraw. vars.expectedSnapshotTime = getBlockTimestamp(); + uint256 ongoingDebtScaled = flow.ongoingDebtScaledOf(streamId); + // It should emit 1 {AdjustFlowStream}, 1 {MetadataUpdate} events. vm.expectEmit({ emitter: address(flow) }); emit ISablierFlow.AdjustFlowStream({ @@ -249,9 +250,9 @@ 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 = ongoingDebt + beforeSnapshotAmount; - assertEq(vars.actualSnapshotDebt, vars.expectedSnapshotDebt, "AdjustRatePerSecond: snapshot debt"); + vars.actualSnapshotDebtScaled = flow.getSnapshotDebtScaled(streamId); + vars.expectedSnapshotDebtScaled = ongoingDebtScaled + beforeSnapshotAmount; + assertEq(vars.actualSnapshotDebtScaled, vars.expectedSnapshotDebtScaled, "AdjustRatePerSecond: snapshot debt"); // It should set the new rate per second vars.actualRatePerSecond = flow.getRatePerSecond(streamId); @@ -302,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() @@ -561,8 +562,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.getSnapshotDebtScaled(streamId), flow.getTokenDecimals(streamId)) + ? flow.getSnapshotTime(streamId) + : getBlockTimestamp(); (, address caller,) = vm.readCallers(); address recipient = flow.getRecipient(streamId); diff --git a/tests/integration/Integration.t.sol b/tests/integration/Integration.t.sol index 74008979..5a3d5175 100644 --- a/tests/integration/Integration.t.sol +++ b/tests/integration/Integration.t.sol @@ -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 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..e9c21635 100644 --- a/tests/integration/concrete/adjust-rate-per-second/adjustRatePerSecond.t.sol +++ b/tests/integration/concrete/adjust-rate-per-second/adjustRatePerSecond.t.sol @@ -95,9 +95,9 @@ contract AdjustRatePerSecond_Integration_Concrete_Test is Integration_Test { uint40 expectedSnapshotTime = getBlockTimestamp() - ONE_MONTH; assertEq(actualSnapshotTime, expectedSnapshotTime, "snapshot time"); - uint256 actualSnapshotDebt = flow.getSnapshotDebt(defaultStreamId); - uint128 expectedSnapshotDebt = 0; - assertEq(actualSnapshotDebt, expectedSnapshotDebt, "snapshot debt"); + uint256 actualSnapshotDebtScaled = flow.getSnapshotDebtScaled(defaultStreamId); + uint128 expectedSnapshotDebtScaled = 0; + assertEq(actualSnapshotDebtScaled, expectedSnapshotDebtScaled, "snapshot debt"); UD21x18 newRatePerSecond = ud21x18(RATE_PER_SECOND.unwrap() / 2); @@ -118,9 +118,9 @@ 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); - expectedSnapshotDebt = ONE_MONTH_DEBT_6D; - assertEq(actualSnapshotDebt, expectedSnapshotDebt, "snapshot debt"); + actualSnapshotDebtScaled = flow.getSnapshotDebtScaled(defaultStreamId); + expectedSnapshotDebtScaled = ONE_MONTH_DEBT_18D; + assertEq(actualSnapshotDebtScaled, expectedSnapshotDebtScaled, "snapshot debt"); // It should set the new rate per second actualRatePerSecond = flow.getRatePerSecond(defaultStreamId); diff --git a/tests/integration/concrete/deposit-and-pause/depositAndPause.t.sol b/tests/integration/concrete/deposit-and-pause/depositAndPause.t.sol index 2c7db077..19c4bfb6 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 expectedSnapshotDebtScaled = + flow.getSnapshotDebtScaled(defaultStreamId) + flow.ongoingDebtScaledOf(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) }); @@ -98,7 +99,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); - assertEq(actualSnapshotDebt, previousTotalDebt, "snapshot debt"); + uint256 actualSnapshotDebtScaled = flow.getSnapshotDebtScaled(defaultStreamId); + assertEq(actualSnapshotDebtScaled, expectedSnapshotDebtScaled, "snapshot debt"); } } diff --git a/tests/integration/concrete/ongoing-debt-of/ongoingDebtOf.t.sol b/tests/integration/concrete/ongoing-debt-of/ongoingDebtScaledOf.t.sol similarity index 56% rename from tests/integration/concrete/ongoing-debt-of/ongoingDebtOf.t.sol rename to tests/integration/concrete/ongoing-debt-of/ongoingDebtScaledOf.t.sol index c5c25a92..fca5f92b 100644 --- a/tests/integration/concrete/ongoing-debt-of/ongoingDebtOf.t.sol +++ b/tests/integration/concrete/ongoing-debt-of/ongoingDebtScaledOf.t.sol @@ -3,9 +3,9 @@ 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); } @@ -13,8 +13,8 @@ contract OngoingDebtOf_Integration_Concrete_Test is Integration_Test { 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 { @@ -22,13 +22,13 @@ contract OngoingDebtOf_Integration_Concrete_Test is Integration_Test { 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"); } } diff --git a/tests/integration/concrete/ongoing-debt-of/ongoingDebtOf.tree b/tests/integration/concrete/ongoing-debt-of/ongoingDebtScaledOf.tree similarity index 88% rename from tests/integration/concrete/ongoing-debt-of/ongoingDebtOf.tree rename to tests/integration/concrete/ongoing-debt-of/ongoingDebtScaledOf.tree index 6873b0d3..607e6052 100644 --- a/tests/integration/concrete/ongoing-debt-of/ongoingDebtOf.tree +++ b/tests/integration/concrete/ongoing-debt-of/ongoingDebtScaledOf.tree @@ -1,4 +1,4 @@ -OngoingDebtOf_Integration_Concrete_Test +OngoingDebtScaledOf_Integration_Concrete_Test ├── given null │ └── it should revert └── given not null diff --git a/tests/integration/concrete/pause/pause.t.sol b/tests/integration/concrete/pause/pause.t.sol index e0ce7cee..51588df6 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,19 +62,17 @@ 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 { // It should emit 1 {PauseFlowStream}, 1 {MetadataUpdate} events. vm.expectEmit({ emitter: address(flow) }); emit ISablierFlow.PauseFlowStream({ streamId: defaultStreamId, sender: users.sender, recipient: users.recipient, - totalDebt: initialTotalDebt + totalDebt: flow.totalDebtOf(defaultStreamId) }); vm.expectEmit({ emitter: address(flow) }); @@ -90,7 +88,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); - assertEq(actualSnapshotDebt, initialTotalDebt, "snapshot debt"); + uint256 actualSnapshotDebtScaled = flow.getSnapshotDebtScaled(defaultStreamId); + assertEq(actualSnapshotDebtScaled, ONE_MONTH_DEBT_18D, "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..b7d25a32 100644 --- a/tests/integration/concrete/refund-and-pause/refundAndPause.t.sol +++ b/tests/integration/concrete/refund-and-pause/refundAndPause.t.sol @@ -54,8 +54,6 @@ contract RefundAndPause_Integration_Concrete_Test is Integration_Test { } function test_WhenCallerSender() external whenNoDelegateCall givenNotNull givenNotPaused { - uint256 previousTotalDebt = flow.totalDebtOf(defaultStreamId); - // It should emit 1 {Transfer}, 1 {RefundFromFlowStream}, 1 {PauseFlowStream}, 1 {MetadataUpdate} events vm.expectEmit({ emitter: address(usdc) }); emit IERC20.Transfer({ from: address(flow), to: users.sender, value: REFUND_AMOUNT_6D }); @@ -72,7 +70,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) }); @@ -96,7 +94,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); - assertEq(actualSnapshotDebt, previousTotalDebt, "snapshot debt"); + uint256 actualSnapshotDebtScaled = flow.getSnapshotDebtScaled(defaultStreamId); + assertEq(actualSnapshotDebtScaled, ONE_MONTH_DEBT_18D, "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..cb8a5071 100644 --- a/tests/integration/concrete/total-debt-of/totalDebtOf.t.sol +++ b/tests/integration/concrete/total-debt-of/totalDebtOf.t.sol @@ -14,27 +14,30 @@ contract TotalDebtOf_Integration_Concrete_Test is Integration_Test { function test_GivenPaused() external givenNotNull { flow.pause(defaultStreamId); - uint256 snapshotDebt = flow.getSnapshotDebt(defaultStreamId); - uint256 totalDebt = flow.totalDebtOf(defaultStreamId); - - assertEq(totalDebt, snapshotDebt, "total debt"); + assertEq( + flow.totalDebtOf(defaultStreamId), + getDescaledAmount(flow.getSnapshotDebtScaled(defaultStreamId), 6), + "total debt" + ); } function test_WhenCurrentTimeEqualsSnapshotTime() external givenNotNull givenNotPaused { // Set the snapshot time to the current time by changing rate per second. 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( + flow.totalDebtOf(defaultStreamId), + getDescaledAmount(flow.getSnapshotDebtScaled(defaultStreamId), 6), + "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 actualTotalDebt = flow.totalDebtOf(defaultStreamId); + uint256 expectedTotalDebt = getDescaledAmount( + flow.getSnapshotDebtScaled(defaultStreamId) + flow.ongoingDebtScaledOf(defaultStreamId), 6 + ); - assertEq(snapshotDebt + ongoingDebt, totalDebt, "total debt"); + assertEq(actualTotalDebt, expectedTotalDebt, "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/concrete/withdraw-delay/withdrawDelay.t.sol b/tests/integration/concrete/withdraw-delay/withdrawDelay.t.sol index b4d0f104..59ba6161 100644 --- a/tests/integration/concrete/withdraw-delay/withdrawDelay.t.sol +++ b/tests/integration/concrete/withdraw-delay/withdrawDelay.t.sol @@ -21,7 +21,7 @@ contract WithdrawDelay_Integration_Concrete_Test is Integration_Test { // Assert that one token has been unlocked. vm.warp(initialSnapshotTime + 87 seconds); - assertEq(flow.ongoingDebtOf(streamId), 1); + assertEq(getDescaledAmount(flow.ongoingDebtScaledOf(streamId), flow.getTokenDecimals(streamId)), 1); // Withdraw the token. (uint128 withdrawnAmount,) = flow.withdrawMax(streamId, users.recipient); @@ -29,7 +29,9 @@ contract WithdrawDelay_Integration_Concrete_Test is Integration_Test { // Now warp to the expected third token unlock. vm.warp(initialSnapshotTime + 260 seconds); - assertEq(withdrawnAmount + flow.ongoingDebtOf(streamId), 3); + assertEq( + withdrawnAmount + getDescaledAmount(flow.ongoingDebtScaledOf(streamId), flow.getTokenDecimals(streamId)), 3 + ); } function test_Withdraw_LongestDelay() external { @@ -45,7 +47,7 @@ contract WithdrawDelay_Integration_Concrete_Test is Integration_Test { // Assert that there is still only one token unlocked. vm.warp(initialSnapshotTime + 172 seconds); - assertEq(flow.ongoingDebtOf(streamId), 1); + assertEq(getDescaledAmount(flow.ongoingDebtScaledOf(streamId), flow.getTokenDecimals(streamId)), 1); // Withdraw the token. (uint128 withdrawnAmount,) = flow.withdrawMax(streamId, users.recipient); @@ -53,15 +55,21 @@ contract WithdrawDelay_Integration_Concrete_Test is Integration_Test { // Warp to a second before second token unlock so that we prove the delay. vm.warp(initialSnapshotTime + 258 seconds); - assertEq(withdrawnAmount + flow.ongoingDebtOf(streamId), 1); + assertEq( + withdrawnAmount + getDescaledAmount(flow.ongoingDebtScaledOf(streamId), flow.getTokenDecimals(streamId)), 1 + ); // Warp to the expected second token unlock. vm.warp(initialSnapshotTime + 259 seconds); - assertEq(withdrawnAmount + flow.ongoingDebtOf(streamId), 2); + assertEq( + withdrawnAmount + getDescaledAmount(flow.ongoingDebtScaledOf(streamId), flow.getTokenDecimals(streamId)), 2 + ); // Warp to the expected third token unlock. vm.warp(initialSnapshotTime + 345 seconds); - assertEq(withdrawnAmount + flow.ongoingDebtOf(streamId), 3); + assertEq( + withdrawnAmount + getDescaledAmount(flow.ongoingDebtScaledOf(streamId), flow.getTokenDecimals(streamId)), 3 + ); } /// @dev A test that demonstrates there is no delay when the rate per second is greater than the scale scaleFactor, @@ -98,9 +106,11 @@ contract WithdrawDelay_Integration_Concrete_Test is Integration_Test { // Find the time when the ongoing debt has increased by 38 uint256 diff; while (diff != 39) { - uint256 beforeWarpOd = flow.ongoingDebtOf(streamId); + uint256 beforeWarpOd = + getDescaledAmount(flow.ongoingDebtScaledOf(streamId), flow.getTokenDecimals(streamId)); vm.warp(getBlockTimestamp() + 1 seconds); - diff = flow.ongoingDebtOf(streamId) - beforeWarpOd; + diff = getDescaledAmount(flow.ongoingDebtScaledOf(streamId), flow.getTokenDecimals(streamId)) + - beforeWarpOd; } (uint128 withdrawnAmount,) = flow.withdrawMax(streamId, users.recipient); diff --git a/tests/integration/concrete/withdraw-max/withdrawMax.t.sol b/tests/integration/concrete/withdraw-max/withdrawMax.t.sol index ee507d63..74ce0824 100644 --- a/tests/integration/concrete/withdraw-max/withdrawMax.t.sol +++ b/tests/integration/concrete/withdraw-max/withdrawMax.t.sol @@ -71,8 +71,8 @@ contract WithdrawMax_Integration_Concrete_Test is Integration_Test { assertEq(vars.actualStreamBalance, vars.expectedStreamBalance, "stream balance"); // It should set the snapshot debt to zero. - vars.actualSnapshotDebt = flow.getSnapshotDebt(defaultStreamId); - assertEq(vars.actualSnapshotDebt, 0, "snapshot debt"); + vars.actualSnapshotDebtScaled = flow.getSnapshotDebtScaled(defaultStreamId); + assertEq(vars.actualSnapshotDebtScaled, 0, "snapshot debt"); if (flow.getRatePerSecond(defaultStreamId).unwrap() > 0) { // It should update snapshot time. 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/adjustRatePerSecond.t.sol b/tests/integration/fuzz/adjustRatePerSecond.t.sol index d7d43203..f519c6bf 100644 --- a/tests/integration/fuzz/adjustRatePerSecond.t.sol +++ b/tests/integration/fuzz/adjustRatePerSecond.t.sol @@ -47,7 +47,7 @@ contract AdjustRatePerSecond_Integration_Fuzz_Test is Shared_Integration_Fuzz_Te // Adjust the rate per second. flow.adjustRatePerSecond(streamId, newRatePerSecond); - assertEq(flow.ongoingDebtOf(streamId), 0, "ongoing debt"); + assertEq(flow.ongoingDebtScaledOf(streamId), 0, "ongoing debt"); assertEq(previousTotalDebt, flow.totalDebtOf(streamId), "rate per second"); } @@ -108,7 +108,7 @@ contract AdjustRatePerSecond_Integration_Fuzz_Test is Shared_Integration_Fuzz_Te // Adjust the rate per second. flow.adjustRatePerSecond(streamId, newRatePerSecond); - assertEq(flow.ongoingDebtOf(streamId), 0, "ongoing debt"); + assertEq(flow.ongoingDebtScaledOf(streamId), 0, "ongoing debt"); assertEq(previousTotalDebt, flow.totalDebtOf(streamId), "rate per second"); } 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/create.t.sol b/tests/integration/fuzz/create.t.sol index e9811b59..bc9d5a3c 100644 --- a/tests/integration/fuzz/create.t.sol +++ b/tests/integration/fuzz/create.t.sol @@ -73,7 +73,7 @@ contract Create_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { assertEq(flow.getSnapshotTime(actualStreamId), getBlockTimestamp()); assertEq(flow.getRatePerSecond(actualStreamId), ratePerSecond); assertEq(flow.getRecipient(actualStreamId), recipient); - assertEq(flow.getSnapshotDebt(actualStreamId), 0); + assertEq(flow.getSnapshotDebtScaled(actualStreamId), 0); assertEq(flow.getSender(actualStreamId), sender); assertEq(flow.getToken(actualStreamId), token); assertEq(flow.getTokenDecimals(actualStreamId), decimals); 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/ongoingDebtOf.t.sol b/tests/integration/fuzz/ongoingDebtScaledOf.t.sol similarity index 79% rename from tests/integration/fuzz/ongoingDebtOf.t.sol rename to tests/integration/fuzz/ongoingDebtScaledOf.t.sol index c264843c..3fcd5185 100644 --- a/tests/integration/fuzz/ongoingDebtOf.t.sol +++ b/tests/integration/fuzz/ongoingDebtScaledOf.t.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.22; import { Shared_Integration_Fuzz_Test } from "./Fuzz.t.sol"; -contract OngoingDebtOf_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { +contract OngoingDebtScaledOf_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { /// @dev It should return the expected value. /// /// Given enough runs, all of the following scenarios should be fuzzed: @@ -21,14 +21,14 @@ contract OngoingDebtOf_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { // Pause the stream. flow.pause(streamId); - uint256 expectedOngoingDebt = flow.ongoingDebtOf(streamId); + uint256 expectedOngoingDebtScaled = flow.ongoingDebtScaledOf(streamId); // Simulate the passage of time after pause. vm.warp({ newTimestamp: getBlockTimestamp() + timeJump }); // Assert that the ongoing debt did not change. - uint256 actualOngoingDebt = flow.ongoingDebtOf(streamId); - assertEq(actualOngoingDebt, expectedOngoingDebt, "ongoing debt"); + uint256 actualOngoingDebtScaled = flow.ongoingDebtScaledOf(streamId); + assertEq(actualOngoingDebtScaled, expectedOngoingDebtScaled, "ongoing debt"); } /// @dev It should return 0. @@ -56,8 +56,8 @@ contract OngoingDebtOf_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { updateSnapshotTimeAndWarp(streamId); // Assert that ongoing debt is zero. - uint256 actualOngoingDebt = flow.ongoingDebtOf(streamId); - assertEq(actualOngoingDebt, 0, "ongoing debt"); + uint256 actualOngoingDebtScaled = flow.ongoingDebtScaledOf(streamId); + assertEq(actualOngoingDebtScaled, 0, "ongoing debt"); } /// @dev It should return the ongoing debt. @@ -65,7 +65,7 @@ contract OngoingDebtOf_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { /// Given enough runs, all of the following scenarios should be fuzzed: /// - Multiple non-paused streams, each with different token decimals and rps. /// - Multiple points in time after the value of snapshotTime. - function testFuzz_OngoingDebtOf( + function testFuzz_OngoingDebtScaledOf( uint256 streamId, uint40 timeJump, uint8 decimals @@ -88,8 +88,8 @@ contract OngoingDebtOf_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { uint128 ratePerSecond = flow.getRatePerSecond(streamId).unwrap(); // Assert that the ongoing debt equals the expected value. - uint256 actualOngoingDebt = flow.ongoingDebtOf(streamId); - uint256 expectedOngoingDebt = getDescaledAmount(ratePerSecond * timeJump, decimals); - assertEq(actualOngoingDebt, expectedOngoingDebt, "ongoing debt"); + uint256 actualOngoingDebtScaled = flow.ongoingDebtScaledOf(streamId); + uint256 expectedOngoingDebtScaled = ratePerSecond * timeJump; + assertEq(actualOngoingDebtScaled, expectedOngoingDebtScaled, "ongoing debt"); } } diff --git a/tests/integration/fuzz/pause.t.sol b/tests/integration/fuzz/pause.t.sol index 8fcb5c65..cd3edfcb 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) }); @@ -85,7 +85,7 @@ contract Pause_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { // Assert that the stream is paused. assertTrue(flow.isPaused(streamId), "paused"); - assertEq(flow.ongoingDebtOf(streamId), 0, "ongoing debt"); + assertEq(flow.ongoingDebtScaledOf(streamId), 0, "ongoing debt"); // Assert that the rate per second is 0. assertEq(flow.getRatePerSecond(streamId), 0, "rate per second"); 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/void.t.sol b/tests/integration/fuzz/void.t.sol index 2e942b19..adbf845e 100644 --- a/tests/integration/fuzz/void.t.sol +++ b/tests/integration/fuzz/void.t.sol @@ -154,7 +154,7 @@ contract Void_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { assertTrue(flow.isVoided(streamId), "voided"); assertTrue(flow.isPaused(streamId), "paused"); assertEq(flow.getRatePerSecond(streamId), 0, "rate per second"); - assertEq(flow.ongoingDebtOf(streamId), 0, "ongoing debt"); + assertEq(flow.ongoingDebtScaledOf(streamId), 0, "ongoing debt"); assertEq(flow.uncoveredDebtOf(streamId), 0, "uncovered debt"); assertEq(flow.totalDebtOf(streamId), expectedTotalDebt, "total debt"); } diff --git a/tests/integration/fuzz/withdraw.t.sol b/tests/integration/fuzz/withdraw.t.sol index 31fcff3e..52f1f13b 100644 --- a/tests/integration/fuzz/withdraw.t.sol +++ b/tests/integration/fuzz/withdraw.t.sol @@ -147,8 +147,10 @@ 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.previousOngoingDebtScaled = flow.totalDebtOf(streamId); + vars.previousTotalDebt = getDescaledAmount( + flow.getSnapshotDebtScaled(streamId), flow.getTokenDecimals(streamId) + ) + vars.previousOngoingDebtScaled; vars.previousStreamBalance = flow.getBalance(streamId); vars.expectedProtocolRevenue = flow.protocolRevenue(token); @@ -184,7 +186,7 @@ contract Withdraw_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { assertEq(vars.actualWithdrawnAmount, withdrawAmount - vars.protocolFeeAmount, "withdrawn amount"); assertEq(vars.actualProtocolFeeAmount, vars.protocolFeeAmount, "protocol fee amount"); - assertEq(flow.ongoingDebtOf(streamId), 0, "ongoing debt"); + assertEq(flow.ongoingDebtScaledOf(streamId), 0, "ongoing debt"); // Assert the protocol revenue. vars.actualProtocolRevenue = flow.protocolRevenue(token); diff --git a/tests/integration/fuzz/withdrawMax.t.sol b/tests/integration/fuzz/withdrawMax.t.sol index 9c0cd6f8..d63c9605 100644 --- a/tests/integration/fuzz/withdrawMax.t.sol +++ b/tests/integration/fuzz/withdrawMax.t.sol @@ -122,7 +122,7 @@ contract WithdrawMax_Integration_Fuzz_Test is Shared_Integration_Fuzz_Test { assertEq(vars.actualWithdrawnAmount, withdrawAmount, "withdrawn amount"); assertEq(vars.actualProtocolFeeAmount, 0, "protocol fee amount"); - assertEq(flow.ongoingDebtOf(streamId), 0, "ongoing debt"); + assertEq(flow.ongoingDebtScaledOf(streamId), 0, "ongoing debt"); // It should update snapshot time. assertEq(flow.getSnapshotTime(streamId), vars.expectedSnapshotTime, "snapshot time"); @@ -137,12 +137,14 @@ 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.getSnapshotDebtScaled(streamId) + flow.ongoingDebtScaledOf(streamId), + flow.getTokenDecimals(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/Assertions.sol b/tests/utils/Assertions.sol index e5b89ac3..00be33c8 100644 --- a/tests/utils/Assertions.sol +++ b/tests/utils/Assertions.sol @@ -28,7 +28,7 @@ abstract contract Assertions is PRBMathAssertions { assertEq(a.isStream, b.isStream, "isStream"); assertEq(a.isTransferable, b.isTransferable, "isTransferable"); assertEq(a.isVoided, b.isVoided, "isVoided"); - assertEq(a.snapshotDebt, b.snapshotDebt, "snapshotDebt"); + assertEq(a.snapshotDebtScaled, b.snapshotDebtScaled, "snapshotDebtScaled"); assertEq(a.sender, b.sender, "sender"); assertEq(a.token, b.token, "token"); assertEq(a.tokenDecimals, b.tokenDecimals, "tokenDecimals"); 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..88ca1d7b 100644 --- a/tests/utils/Utils.sol +++ b/tests/utils/Utils.sol @@ -60,22 +60,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; } diff --git a/tests/utils/Vars.sol b/tests/utils/Vars.sol index d612400e..cb4ef013 100644 --- a/tests/utils/Vars.sol +++ b/tests/utils/Vars.sol @@ -10,7 +10,7 @@ struct Vars { IERC20 token; // previous values. uint256 previousAggregateAmount; - uint256 previousOngoingDebt; + uint256 previousOngoingDebtScaled; uint40 previousSnapshotTime; uint128 previousStreamBalance; uint256 previousTokenBalance; @@ -20,7 +20,7 @@ struct Vars { uint128 actualProtocolFeeAmount; uint128 actualProtocolRevenue; UD21x18 actualRatePerSecond; - uint256 actualSnapshotDebt; + uint256 actualSnapshotDebtScaled; uint40 actualSnapshotTime; uint128 actualStreamBalance; uint256 actualStreamId; @@ -32,7 +32,7 @@ struct Vars { uint128 expectedProtocolFeeAmount; uint128 expectedProtocolRevenue; UD21x18 expectedRatePerSecond; - uint256 expectedSnapshotDebt; + uint256 expectedSnapshotDebtScaled; uint40 expectedSnapshotTime; uint128 expectedStreamBalance; uint256 expectedStreamId;