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

perf: don't update st if withdraw amount is less than sd #266

Merged
merged 4 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
22 changes: 15 additions & 7 deletions src/SablierFlow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -811,18 +811,26 @@ contract SablierFlow is
revert Errors.SablierFlow_Overdraw(streamId, amount, withdrawableAmount);
}

// Safe to use unchecked, the balance or total debt cannot be less than `amount` at this point.
// 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;
}
// 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;

// Effect: update the stream time.
_streams[streamId].snapshotTime = uint40(block.timestamp);
}

// Effect: update the stream balance.
_streams[streamId].balance -= amount;

// Effect: update the snapshot debt.
_streams[streamId].snapshotDebt = totalDebt - amount;
}

// Effect: update the stream time.
_streams[streamId].snapshotTime = uint40(block.timestamp);

// Load the variables in memory.
IERC20 token = _streams[streamId].token;
UD60x18 protocolFee = protocolFee[token];
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/ISablierFlow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ interface ISablierFlow is
/// @dev Emits a {Transfer} and {WithdrawFromFlowStream} event.
///
/// Notes:
/// - It sets the snapshot time to the `block.timestamp`.
/// - It sets the snapshot time to the `block.timestamp` if `amount` is greater than snapshot debt.
/// - A protocol fee may be charged on the withdrawn amount if the protocol fee is enabled for the streaming token.
///
/// Requirements:
Expand Down
3 changes: 2 additions & 1 deletion tests/fork/Flow.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,8 @@ contract Flow_Fork_Test is Fork_Test {
uint256 initialTokenBalance = token.balanceOf(address(flow));
uint128 totalDebt = flow.totalDebtOf(streamId);

vars.expectedSnapshotTime = getBlockTimestamp();
vars.expectedSnapshotTime =
smol-ninja marked this conversation as resolved.
Show resolved Hide resolved
withdrawAmount <= flow.getSnapshotDebt(streamId) ? flow.getSnapshotTime(streamId) : getBlockTimestamp();

(, address caller,) = vm.readCallers();
address recipient = flow.getRecipient(streamId);
Expand Down
8 changes: 5 additions & 3 deletions tests/integration/concrete/withdraw-max/withdrawMax.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,11 @@ contract WithdrawMax_Integration_Concrete_Test is Integration_Test {
uint128 actualSnapshotDebt = flow.getSnapshotDebt(defaultStreamId);
assertEq(actualSnapshotDebt, 0, "snapshot debt");

// It should update snapshot time.
uint128 actualSnapshotTime = flow.getSnapshotTime(defaultStreamId);
assertEq(actualSnapshotTime, getBlockTimestamp(), "snapshot time");
if (!flow.isPaused(defaultStreamId)) {
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
// It should update snapshot time.
uint128 actualSnapshotTime = flow.getSnapshotTime(defaultStreamId);
assertEq(actualSnapshotTime, getBlockTimestamp(), "snapshot time");
}

// It should return the actual withdrawn amount.
assertEq(actualWithdrawnAmount, expectedWithdrawAmount, "withdrawn amount");
Expand Down
79 changes: 62 additions & 17 deletions tests/integration/concrete/withdraw/withdraw.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,33 @@ contract Withdraw_Integration_Concrete_Test is Integration_Test {
});
}

function test_WhenAmountNotExceedSnapshotDebt()
external
whenNoDelegateCall
givenNotNull
whenAmountNotZero
whenWithdrawalAddressNotZero
whenWithdrawalAddressOwner
whenAmountNotOverdraw
{
// It should not update snapshot time
// It should make the withdrawal
resetPrank({ msgSender: users.sender });
flow.pause(defaultStreamId);

vm.warp({ newTimestamp: getBlockTimestamp() + 1 });

resetPrank({ msgSender: users.recipient });
// It should make the withdrawal.
_test_Withdraw({
streamId: defaultStreamId,
to: users.recipient,
depositAmount: DEPOSIT_AMOUNT_6D,
feeAmount: 0,
withdrawAmount: WITHDRAW_AMOUNT_6D
});
}

function test_GivenProtocolFeeNotZero()
external
whenNoDelegateCall
Expand Down Expand Up @@ -235,6 +262,19 @@ contract Withdraw_Integration_Concrete_Test is Integration_Test {
});
}

struct Vars {
IERC20 token;
uint40 previousSnapshotTime;
uint128 previousTotalDebt;
uint256 previousAggregateAmount;
uint128 expectedProtocolRevenue;
uint256 initialTokenBalance;
uint40 expectedSnapshotTime;
uint128 expectedTotalDebt;
uint128 expectedStreamBalance;
uint256 expectedTokenBalance;
}

function _test_Withdraw(
uint256 streamId,
address to,
Expand All @@ -244,21 +284,23 @@ contract Withdraw_Integration_Concrete_Test is Integration_Test {
)
private
{
IERC20 token = flow.getToken(streamId);
uint128 previousTotalDebt = flow.totalDebtOf(streamId);
uint256 previousAggregateAmount = flow.aggregateBalance(token);
Vars memory vars;
vars.token = flow.getToken(streamId);
vars.previousSnapshotTime = flow.getSnapshotTime(streamId);
vars.previousTotalDebt = flow.totalDebtOf(streamId);
vars.previousAggregateAmount = flow.aggregateBalance(vars.token);

uint128 expectedProtocolRevenue = flow.protocolRevenue(token) + feeAmount;
vars.expectedProtocolRevenue = flow.protocolRevenue(vars.token) + feeAmount;

// It should emit 1 {Transfer}, 1 {WithdrawFromFlowStream} and 1 {MetadataUpdated} events.
vm.expectEmit({ emitter: address(token) });
vm.expectEmit({ emitter: address(vars.token) });
emit IERC20.Transfer({ from: address(flow), to: to, value: withdrawAmount - feeAmount });

vm.expectEmit({ emitter: address(flow) });
emit WithdrawFromFlowStream({
streamId: streamId,
to: to,
token: token,
token: vars.token,
caller: users.recipient,
protocolFeeAmount: feeAmount,
withdrawAmount: withdrawAmount - feeAmount
Expand All @@ -268,33 +310,36 @@ contract Withdraw_Integration_Concrete_Test is Integration_Test {
emit MetadataUpdate({ _tokenId: streamId });

// It should perform the ERC-20 transfer.
expectCallToTransfer({ token: token, to: to, amount: withdrawAmount - feeAmount });
expectCallToTransfer({ token: vars.token, to: to, amount: withdrawAmount - feeAmount });

uint256 initialTokenBalance = token.balanceOf(address(flow));
vars.initialTokenBalance = vars.token.balanceOf(address(flow));

flow.withdraw({ streamId: streamId, to: to, amount: withdrawAmount });

// Assert the protocol revenue.
assertEq(flow.protocolRevenue(token), expectedProtocolRevenue, "protocol revenue");
assertEq(flow.protocolRevenue(vars.token), vars.expectedProtocolRevenue, "protocol revenue");

// It should update snapshot time.
assertEq(flow.getSnapshotTime(streamId), getBlockTimestamp(), "snapshot time");
vars.expectedSnapshotTime = flow.isPaused(streamId) ? vars.previousSnapshotTime : getBlockTimestamp();
assertEq(flow.getSnapshotTime(streamId), vars.expectedSnapshotTime, "snapshot time");

// It should decrease the total debt by the withdrawn value and fee amount.
uint128 expectedTotalDebt = previousTotalDebt - withdrawAmount;
assertEq(flow.totalDebtOf(streamId), expectedTotalDebt, "total debt");
vars.expectedTotalDebt = vars.previousTotalDebt - withdrawAmount;
assertEq(flow.totalDebtOf(streamId), vars.expectedTotalDebt, "total debt");

// It should reduce the stream balance by the withdrawn value and fee amount.
uint128 expectedStreamBalance = depositAmount - withdrawAmount;
assertEq(flow.getBalance(streamId), expectedStreamBalance, "stream balance");
vars.expectedStreamBalance = depositAmount - withdrawAmount;
assertEq(flow.getBalance(streamId), vars.expectedStreamBalance, "stream balance");

// It should reduce the token balance of stream.
uint256 expectedTokenBalance = initialTokenBalance - (withdrawAmount - feeAmount);
assertEq(token.balanceOf(address(flow)), expectedTokenBalance, "token balance");
vars.expectedTokenBalance = vars.initialTokenBalance - (withdrawAmount - feeAmount);
assertEq(vars.token.balanceOf(address(flow)), vars.expectedTokenBalance, "token balance");

// It should decrease the aggregate amount.
assertEq(
flow.aggregateBalance(token), previousAggregateAmount - (withdrawAmount - feeAmount), "aggregate amount"
flow.aggregateBalance(vars.token),
vars.previousAggregateAmount - (withdrawAmount - feeAmount),
"aggregate amount"
);
}
}
32 changes: 18 additions & 14 deletions tests/integration/concrete/withdraw/withdraw.tree
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,21 @@ Withdraw_Integration_Concrete_Test
│ ├── it should update snapshot debt
│ └── it should make the withdrawal
└── when amount equal total debt
├── given protocol fee not zero
│ ├── it should update the protocol revenue
│ └── it should withdraw the net amount
└── given protocol fee zero
├── given token has 18 decimals
│ └── it should make the withdrawal
└── given token not have 18 decimals
├── it should withdraw the withdrawable amount
├── it should reduce the stream balance by the withdrawn amount
├── it should reduce the aggregate amount by the withdrawn amount
├── it should update snapshot debt to zero
├── it should update snapshot time
├── it should emit 1 {Transfer}, 1 {WithdrawFromFlowStream} and 1 {MetadataUpdated} events
└── it should return the withdrawn amount
├── when amount not exceed snapshot debt
│ ├── it should not update snapshot time
│ └── it should make the withdrawal
└── when amount exceed snapshot debt
├── given protocol fee not zero
│ ├── it should update the protocol revenue
│ └── it should withdraw the net amount
└── given protocol fee zero
├── given token has 18 decimals
│ └── it should make the withdrawal
└── given token not have 18 decimals
├── it should withdraw the withdrawable amount
├── it should reduce the stream balance by the withdrawn amount
├── it should reduce the aggregate amount by the withdrawn amount
├── it should update snapshot debt to zero
├── it should update snapshot time
├── it should emit 1 {Transfer}, 1 {WithdrawFromFlowStream} and 1 {MetadataUpdated} events
└── it should return the withdrawn amount
Loading