Skip to content

Commit

Permalink
refactor: sd as 18 decimals (#312)
Browse files Browse the repository at this point in the history
* refactor: sd as 18 decimals

test: update tests accordingly

* feedback on helpers

* refactor: add "scaled" in ongoing and snapshot debt

test: update tests accordingly

* docs: improve natspec

test: use `ONE_MONTH_DEBT_18D` constant

Co-authored-by: smol-ninja <shubhamy2015@gmail.com>

* last fixes

Co-authored-by: smol-ninja <shubhamy2015@gmail.com>

---------

Co-authored-by: smol-ninja <shubhamy2015@gmail.com>
  • Loading branch information
andreivladbrg and smol-ninja authored Oct 16, 2024
1 parent f15d4c1 commit 4ebdf2c
Show file tree
Hide file tree
Showing 39 changed files with 232 additions and 193 deletions.
5 changes: 3 additions & 2 deletions TECHNICAL-DOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ pause it or void it at a later date.
A stream is represented by a struct, which can be found in
[`DataTypes.sol`](https://github.com/sablier-labs/flow/blob/ba1c9ba64907200c82ccfaeaa6ab91f6229c433d/src/types/DataTypes.sol#L41-L76).

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

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

The recipient can withdraw the streamed amount at any point. However, if there aren't sufficient funds, the recipient
Expand Down
18 changes: 9 additions & 9 deletions benchmark/results/SablierFlow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
105 changes: 50 additions & 55 deletions src/SablierFlow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
});
Expand Down Expand Up @@ -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();

Expand All @@ -470,35 +473,20 @@ 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.
function _refundableAmountOf(uint256 streamId) internal view returns (uint128) {
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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -646,7 +634,7 @@ contract SablierFlow is
streamId: streamId,
sender: _streams[streamId].sender,
recipient: _ownerOf(streamId),
totalDebt: _streams[streamId].snapshotDebt
totalDebt: _totalDebtOf(streamId)
});
}

Expand Down Expand Up @@ -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.
Expand All @@ -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
});
}
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions src/abstracts/SablierFlowBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,14 @@ abstract contract SablierFlowBase is
}

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

/// @inheritdoc ISablierFlowBase
Expand Down
5 changes: 3 additions & 2 deletions src/interfaces/ISablierFlow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions src/interfaces/ISablierFlowBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 24 additions & 0 deletions src/libraries/Helpers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
8 changes: 4 additions & 4 deletions src/types/DataTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ library Flow {
/// be restarted. Voiding an insolvent stream sets its uncovered debt to zero.
/// @param token The contract address of the ERC-20 token to stream.
/// @param tokenDecimals The decimals of the ERC-20 token to stream.
/// @param snapshotDebt The amount of tokens that the sender owed to the recipient at snapshot time, denoted 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;
Expand All @@ -72,6 +72,6 @@ library Flow {
IERC20 token;
uint8 tokenDecimals;
// slot 3
uint256 snapshotDebt;
uint256 snapshotDebtScaled;
}
}
Loading

0 comments on commit 4ebdf2c

Please sign in to comment.