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 1 commit
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
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 |
91 changes: 42 additions & 49 deletions src/SablierFlow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -69,35 +69,35 @@ contract SablierFlow is
return 0;
}

uint8 tokenDecimals = _streams[streamId].tokenDecimals;
uint256 scaledBalance = Helpers.scaleAmount({ amount: balance, decimals: tokenDecimals });
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved

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({
smol-ninja marked this conversation as resolved.
Show resolved Hide resolved
amount: _scaledOngoingDebtOf(streamId),
decimals: _streams[streamId].tokenDecimals
});
}

/// @inheritdoc ISablierFlow
Expand Down Expand Up @@ -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
});
Expand Down Expand Up @@ -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) {
smol-ninja marked this conversation as resolved.
Show resolved Hide resolved
uint256 blockTimestamp = block.timestamp;
uint256 snapshotTime = _streams[streamId].snapshotTime;

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

Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -646,7 +632,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 +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.
Expand All @@ -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
});
}
Expand Down Expand Up @@ -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;
Expand All @@ -792,17 +782,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].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);
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/ISablierFlowBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
30 changes: 30 additions & 0 deletions src/libraries/Helpers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
/// @dev The following logic is used to denormalize the amount:
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
/// - 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) {
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
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) {
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
if (decimals > 18) {
return amount;
}

unchecked {
uint256 scaleFactor = 10 ** (18 - decimals);
return amount * scaleFactor;
}
}
}
6 changes: 3 additions & 3 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 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;
Expand Down
12 changes: 8 additions & 4 deletions tests/fork/Flow.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) });
Expand All @@ -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) });
Expand All @@ -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");
}
}
13 changes: 7 additions & 6 deletions tests/integration/concrete/pause/pause.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -62,19 +62,20 @@ 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) });
emit ISablierFlow.PauseFlowStream({
streamId: defaultStreamId,
sender: users.sender,
recipient: users.recipient,
totalDebt: initialTotalDebt
totalDebt: flow.totalDebtOf(defaultStreamId)
});

vm.expectEmit({ emitter: address(flow) });
Expand All @@ -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");
}
}
Loading
Loading