Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: sd as 18 decimals #312

Merged
merged 5 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 |
100 changes: 48 additions & 52 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 scaledBalance = Helpers.scaleAmount({ amount: balance, decimals: tokenDecimals });
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved

uint256 snapshotDebtScaled = _streams[streamId].snapshotDebtScaled;

// If the stream has uncovered debt, return zero.
if (snapshotDebt + _ongoingDebtOf(streamId) > balance) {
if (snapshotDebtScaled + _ongoingDebtScaledOf(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 - 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,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 scaled ongoing debt accrued by multiplying the elapsed time by the rate per second.
return elapsedTime * ratePerSecond;
}

/// @dev Calculates the refundable amount.
Expand All @@ -497,8 +486,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 = _ongoingDebtScaledOf(streamId) + _streams[streamId].snapshotDebtScaled;
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
return Helpers.descaleAmount({ amount: scaledTotalDebt, decimals: _streams[streamId].tokenDecimals });
}

/// @dev Calculates the uncovered debt.
Expand All @@ -525,12 +514,12 @@ contract SablierFlow is
revert Errors.SablierFlow_RatePerSecondNotDifferent(streamId, newRatePerSecond);
}

uint256 ongoingDebt = _ongoingDebtOf(streamId);
uint256 scaledOngoingDebt = _ongoingDebtScaledOf(streamId);
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved

// 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].snapshotDebtScaled += scaledOngoingDebt;
}

// Effect: update the snapshot time.
Expand Down Expand Up @@ -574,7 +563,7 @@ contract SablierFlow is
isVoided: false,
ratePerSecond: ratePerSecond,
sender: sender,
snapshotDebt: 0,
snapshotDebtScaled: 0,
snapshotTime: uint40(block.timestamp),
token: token,
tokenDecimals: tokenDecimals
Expand Down Expand Up @@ -646,7 +635,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 +704,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 = _ongoingDebtScaledOf(streamId);
if (scaledOngoingDebt > 0) {
// Effect: Update the snapshot debt by adding the ongoing debt.
_streams[streamId].snapshotDebt += ongoingDebt;
_streams[streamId].snapshotDebtScaled += scaledOngoingDebt;
}
}
// If the stream is insolvent, write off the uncovered debt.
else {
// Effect: update the total debt by setting snapshot debt to the stream balance.
_streams[streamId].snapshotDebt = _streams[streamId].balance;
_streams[streamId].snapshotDebtScaled =
Helpers.scaleAmount({ amount: _streams[streamId].balance, decimals: _streams[streamId].tokenDecimals });
}

// Effect: update the snapshot time.
Expand All @@ -742,7 +732,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 +762,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 = _ongoingDebtScaledOf(streamId) + _streams[streamId].snapshotDebtScaled;
uint256 totalDebt = Helpers.descaleAmount(scaledTotalDebt, tokenDecimals);

// Calculate the withdrawable amount.
uint128 balance = _streams[streamId].balance;
Expand All @@ -792,17 +785,20 @@ contract SablierFlow is
revert Errors.SablierFlow_Overdraw(streamId, amount, withdrawableAmount);
}

// Calculate the amount scaled.
uint256 scaledAmount = Helpers.scaleAmount(amount, tokenDecimals);
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved

// 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].snapshotDebtScaled) {
_streams[streamId].snapshotDebtScaled -= scaledAmount;
}
// Else reduce the amount from the ongoing debt by setting snapshot time to `block.timestamp` and set the
// snapshot debt to the remaining total debt.
else {
_streams[streamId].snapshotDebt = totalDebt - amount;
_streams[streamId].snapshotDebtScaled = scaledTotalDebt - scaledAmount;

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

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

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

/// @notice Returns the amount that the sender can be refunded from the stream, denoted in token's decimals.
/// @dev Reverts if `streamId` references a null stream.
Expand Down
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) {
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
17 changes: 10 additions & 7 deletions tests/fork/Flow.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -249,8 +250,8 @@ contract Flow_Fork_Test is Fork_Test {
flow.adjustRatePerSecond({ streamId: streamId, newRatePerSecond: newRatePerSecond });

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

// It should set the new rate per second
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading