-
Notifications
You must be signed in to change notification settings - Fork 3
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
fix: precision issues #240
Conversation
Thanks for the PR. I want to mention that this solution seems to work for You can run this: Delay test
Ran on this commit eb096bc function testDelayUsdc_OngoingDebt() public {
uint128 rps = 0.000000011574e18; // 0.001e6 USDC per day, less than smallest value of USDC 0.00000001e6
uint128 depositAmount = 0.001e6;
uint128 factor = uint128(10 ** (18 - 6));
uint40 constantInterval = uint40(factor / rps); // 10^12 / (1.1574 * 10^10)
assertEq(constantInterval, 86, "constant interval");
uint256 streamId = flow.createAndDeposit(users.sender, users.recipient, ud21x18(rps), usdc, true, depositAmount);
uint40 initialSnapshotTime = MAY_1_2024;
assertEq(flow.getSnapshotTime(streamId), initialSnapshotTime, "snapshot time");
// rps * 1 days = 0.000999e6 due to how the rational numbers work in math :))
// so we need to warp one more second in the future to get the deposit amount
vm.warp(initialSnapshotTime + 1 days + 1 seconds);
assertEq(flow.ongoingDebtOf(streamId), depositAmount, "ongoing debt vs deposit amount");
// now, since everything has work as expected, let's go back in time to pause and then immediately restart
// the first discrete release is at constantInterval + 1 second
// after that, it is periodic to constantInterval
// warp to a timestamp that ongoing debt is greater than zero
vm.warp(initialSnapshotTime + constantInterval + 1);
assertEq(flow.ongoingDebtOf(streamId), 1, "ongoing debt vs first discrete release");
// to test the constant interval is correct
uint40 delay = constantInterval - 1;
vm.warp(initialSnapshotTime + (constantInterval + 1) + delay);
assertEq(flow.ongoingDebtOf(streamId), 1, "ongoing debt vs delay"); // same as before
// we will have delay of (constantInterval - 1)
flow.pause(streamId);
flow.restart(streamId, ud21x18(rps));
// assert that the snapshot debt has been updated with the ongoing debt
assertEq(flow.getSnapshotDebt(streamId), 1, "snapshot debt");
// now, let's go again at the time we've tested ongoingDebt == depositAmount
vm.warp(initialSnapshotTime + 1 days + 1 seconds);
// theoretically, it needs to be depositAmount - snapshotDebt, but it is not
// as we have discrete intervals, the full initial deposited amount gets released now after the delay
assertFalse(
flow.ongoingDebtOf(streamId) == depositAmount - flow.getSnapshotDebt(streamId),
"ongoing debt vs deposit amount - snapshot debt first warp"
);
vm.warp(initialSnapshotTime + 1 days + 1 seconds + delay + 1 seconds);
assertEq(
flow.ongoingDebtOf(streamId),
depositAmount - flow.getSnapshotDebt(streamId),
"ongoing debt vs deposit amount - snapshot debt second warp"
);
} Therefore, my alternative is to reintroduce correctedTime, but calculate it only if certain criteria are met:
Not yet very sure about 3. , but re. 4: newElapsedTime = correctedTime - snapshotTime;
uint128 recalculatedOngoingDebt = (newElapsedTime * ratePerSecond) / factor; |
Thanks for the feedback @andreivladbrg. The following code in _streams[streamId].snapshotTime += uint40(
((amount - _streams[streamId].snapshotDebt) * (10 ** (18 - _streams[streamId].tokenDecimals)))
/ _streams[streamId].ratePerSecond.unwrap()
); Are you talking about using storing
Thus, I intentionally did not use corrected time in both of them. |
Yes
In
Hmm, I don't understand these arguments, as Having said that, how would this lead to over-streaming? Even more, what if the |
One other alternative would be: uint40 constantInterval = uint40((1e18 * factor) / rps);
// so ongoing debt would be how many intervals can fit in elapsed time
uint128 ongoingDebt = (1e18 * elapsedTime) / constantInterval; what it does, it finds the "period" of time when the ongoingDebt remains constant, and then calculates the ongoing debt, by finding how many intervals can fit within elapsed time but this version, since it has one more division involved might be more problematic |
In
Its a small number we can adjust for it, but then we might have to modify some of the failing invariants because of this. |
Damn, yeah got it now |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feedback below. I think I found two important issues:
- Invariant
new total debt == ongoing debt
does not seem to hold at the end of the execution in_withdraw
- Return amount is incorrect
P.S. sorry for renaming previous
to initial
and normalized
to scaled
in this PR.
2425778
to
d6f14ff
Compare
d6f14ff
to
8294419
Compare
since the idea behind the adjusment of if (prevAmount != withdrawAmount) {
assert(10 ** (18 - _streams[streamId].tokenDecimals) * (prevAmount - withdrawAmount) < ratePerSecond.unwrap());
} |
i find the current version of also, we could consider dividing it in multiple functions (regardless of the gas implications) wydt? @sablier-labs/solidity |
Since these logics are only used in
On the contrary, the comments helps readers while reading the code instead of distracting them. Without the comments, the readers might not even understand why does it have some lines. |
One idea could be: Toggle to see code
/// FlowBase
function _updateProtocolState(
uint128 totalAmount,
IERC20 token
)
internal
returns (uint128 netAmount, uint128 feeAmount)
{
// Load the variables in memory.
UD60x18 protocolFee = protocolFee[token];
if (protocolFee > ZERO) {
// Calculate the fee amount based on the fee percentage.
feeAmount = ud(totalAmount).mul(protocolFee).intoUint128();
// Calculate the net amount after subtracting the fee from the total amount.
netAmount = totalAmount - feeAmount;
// Safe to use unchecked because addition cannot overflow.
unchecked {
// Effect: update the protocol revenue.
protocolRevenue[token] += feeAmount;
}
} else {
netAmount = totalAmount;
}
unchecked {
// Effect: update the aggregate amount.
aggregateBalance[token] -= netAmount;
}
}
/// Flow
function _withdraw(uint256 streamId, address to, uint128 withdrawAmount) internal {
// --snip--
(withdrawAmount, feeAmount) = _updateProtocolState(withdrawAmount, token);
} i think solutions are
i consider self-explanatory code + succinct comments to be the best way to write code. anything more detailed can be addressed in the docs IMO |
As discussed on Slack, we will keep the verbose comments in the withdraw function because the logic is sophisticated and non-intuitive and requires a proper explanation. In addition:
|
The test failed because I moved |
Also in BTT trees right? |
6e27253
to
03a98bb
Compare
I am not sure if there is a need to round down the refund amount. Even if refund amount modifies the balance such that withdrawable is not a multiple of rps (when balance is withdrawable), the |
20b2658
to
3dec9ac
Compare
Yeah, I think you're right @smol-ninja. If the stream is paused or voided, the withdrawable amount that is not a multiple of |
@andreivladbrg can we please merge this PR now since we have all agreed on the solution? And #255 also helps in verifying that the approach used in this PR is better. |
fix: return correct amount in "_withdraw" refactor: reorder parameters in "WithdrawFromFlowStream" refactor: rename "amount" to "withdrawAmount" refactor: rename "previous" to "initial" refactor: rename variables in "_withdraw" refactor: use "scaled" terminology instead of "normalized"
Co-authored-by: Paul Razvan Berg <prberg@proton.me>
perf: unchecked around protocol invariant
* test: amount withdrawn after multiple withdrawals * test: rename mvt to scaleFactor * test: rename test to "withdraw no delay" test: polish delay test --------- Co-authored-by: andreivladbrg <andreivladbrg@gmail.com>
b48fde5
to
7a80f4d
Compare
658e122
to
962bdc7
Compare
Closing this PR. Thanks everyone the long productive discussion. I have created a new branch: https://github.com/sablier-labs/flow/tree/feat/support-extremely-low-rps for future reference. |
Please see natspec comments for the explanation.
Tasks:
amount
in withdrawwithdrawAmount
is not less thanrps
withdrawAmount