Skip to content

Commit

Permalink
refactor: sd as 18 decimals
Browse files Browse the repository at this point in the history
test: update tests accordingly
  • Loading branch information
andreivladbrg committed Oct 15, 2024
1 parent 04f3ed6 commit 3ab42ac
Show file tree
Hide file tree
Showing 25 changed files with 152 additions and 117 deletions.
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 });

uint256 snapshotDebt = _streams[streamId].snapshotDebt;

// If the stream has uncovered debt, return zero.
if (snapshotDebt + _ongoingDebtOf(streamId) > balance) {
if (snapshotDebt + _scaledOngoingDebtOf(streamId) > scaledBalance) {
return 0;
}

uint256 tokenDecimals = _streams[streamId].tokenDecimals;
uint256 solvencyAmount;

// Depletion time is defined as the UNIX timestamp beyond which the total debt exceeds stream balance.
// So we calculate it by solving: debt at depletion time = stream balance + 1. This ensures that we find the
// lowest timestamp at which the debt exceeds the balance.
// Safe to use unchecked because the calculations cannot overflow or underflow.
unchecked {
if (tokenDecimals == 18) {
solvencyAmount = (balance - snapshotDebt + 1);
} else {
uint256 scaleFactor = (10 ** (18 - tokenDecimals));
solvencyAmount = (balance - snapshotDebt + 1) * scaleFactor;
}
uint256 solvencyAmount =
scaledBalance - snapshotDebt + Helpers.scaleAmount({ amount: 1, decimals: tokenDecimals });
uint256 solvencyPeriod = solvencyAmount / _streams[streamId].ratePerSecond.unwrap();
return _streams[streamId].snapshotTime + solvencyPeriod;

depletionTime = _streams[streamId].snapshotTime + solvencyPeriod;
}
}

/// @inheritdoc ISablierFlow
function ongoingDebtOf(uint256 streamId) external view override notNull(streamId) returns (uint256 ongoingDebt) {
ongoingDebt = _ongoingDebtOf(streamId);
ongoingDebt = Helpers.descaleAmount({
amount: _scaledOngoingDebtOf(streamId),
decimals: _streams[streamId].tokenDecimals
});
}

/// @inheritdoc ISablierFlow
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) {
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);

// 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.
/// @dev The following logic is used to denormalize the amount:
/// - If the token has exactly 18 decimals, the amount is returned as is.
/// - if the token has fewer than 18 decimals, the amount is divided by $10^(18 - tokenDecimals)$.
function descaleAmount(uint256 amount, uint8 decimals) internal pure returns (uint256) {
if (decimals > 18) {
return amount;
}

unchecked {
uint256 scaleFactor = 10 ** (18 - decimals);
return amount / scaleFactor;
}
}

/// @notice Scales the provided `amount` to be denoted in 18 decimals.
/// @dev The following logic is used to normalize the amount:
/// - If the token has exactly 18 decimals, the amount is returned as is.
/// - If the token has fewer than 18 decimals, the amount is multiplied by $10^(18 - tokenDecimals)$.
function scaleAmount(uint256 amount, uint8 decimals) internal pure returns (uint256) {
if (decimals > 18) {
return amount;
}

unchecked {
uint256 scaleFactor = 10 ** (18 - decimals);
return amount * scaleFactor;
}
}
}
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
11 changes: 7 additions & 4 deletions tests/fork/Flow.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,6 @@ 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();
Expand All @@ -250,7 +249,9 @@ contract Flow_Fork_Test is Fork_Test {

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

// It should set the new rate per second
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.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

0 comments on commit 3ab42ac

Please sign in to comment.